Являются ли повторные вызовы recv () дорогими? - PullRequest
6 голосов
/ 24 февраля 2011

У меня есть вопрос о ситуации, с которой я сталкиваюсь довольно часто.Время от времени мне приходится реализовывать различные протоколы на основе TCP.Большинство из них определяют пакеты данных переменной длины, которые начинаются с общего заголовка ([идентификатор пакета, длина, полезная нагрузка] или что-то действительно похожее).Очевидно, что есть два подхода к чтению этих пакетов:

  1. Чтение заголовка (поскольку длина заголовка обычно фиксирована), извлечение длины полезной нагрузки, чтение полезной нагрузки
  2. Чтение всех доступных данныхи сохранить его в буфере;потом анализируем буфер

Очевидно, что первый подход прост, но требует двух вызовов read() (или, возможно, больше).Второй вариант немного сложнее, но требует меньше вызовов.

Вопрос: достаточно ли сильно влияет первый подход на производительность, чтобы беспокоиться об этом?

Ответы [ 5 ]

9 голосов
/ 24 февраля 2011

Да, системные вызовы обычно дороги по сравнению с копиями памяти. ИМХО это особенно актуально на архитектуре x86, и спорно на RISC машине (ARM, MIPS, ...).

Если честно, если вы не обработаете сотни или тысячи запросов в секунду, вы вряд ли заметите разницу.

В зависимости от протокола, гибридный подход может быть лучшим. Когда протокол использует много маленьких пакетов и менее большие, вы можете прочитать заголовок и частичный объем данных. Когда это маленький пакет, вы выигрываете, избегая большого memcpy, когда пакет большой, вы выигрываете, выполняя второй системный вызов только для этого случая.

4 голосов
/ 24 февраля 2011

Если ваше приложение является сервером, способным обрабатывать несколько клиентов одновременно, а неблокирующие сокеты используются для обработки нескольких клиентов в одном потоке, у вас нет другого выбора, кроме как когда-либо выдавать только один вызов recv (), когда сокет готов кread.

Причина в том, что если вы продолжаете вызывать recv () в цикле, а клиент отправляет большой объем данных, то может случиться, что ваш цикл recv () может заблокировать поток на долгое времяделать что-то еще.Например, recv () считывает некоторое количество данных из сокета, определяет, что теперь в буфере есть полное сообщение, и перенаправляет это сообщение в обратный вызов.Обратный вызов как-то обрабатывает сообщение и возвращает.Если вы вызываете recv () еще раз, может появиться больше сообщений, поступивших во время обработки обратного вызова предыдущего сообщения.Это приводит к занятому циклу recv () на одном сокете, не позволяющему потоку обрабатывать любые другие ожидающие события.

Эта проблема усугубляется, если буфер чтения сокета в вашем приложении меньше, чем приемный буфер сокета ядра.Другими словами, все содержимое буфера приема ядра не может быть прочитано за один вызов recv ().Неподтвержденное свидетельство - то, что я столкнулся с этой проблемой в занятой производственной системе, когда был буфер пространства пользователя в 16 КБ для буфера приема сокета ядра 2 МБ.Клиент, отправляющий много сообщений подряд, блокирует поток в этом цикле recv () на несколько минут, поскольку при обработке только что прочитанных сообщений будет поступать больше сообщений, что приведет к прерыванию службы.

В таком событииДля управляемых архитектур лучше всего иметь буфер чтения пользовательского пространства, равный размеру приемного буфера сокета ядра (или максимальному размеру сообщения, в зависимости от того, что больше), чтобы все данные, доступные в буфере ядра, можно было прочитать в одномвызов recv ().Это работает, выполняя один вызов recv (), обрабатывая все полные сообщения в буфере чтения пользовательского пространства и затем возвращая управление в цикл обработки событий.Таким образом, соединения с большим количеством поступающих данных не блокируют поток от обработки других событий и соединений, скорее это циклическая обработка всех соединений с доступными входящими данными.

3 голосов
/ 24 февраля 2011

Лучший способ получить ответ - измерить. Программа strace предназначена для измерения времени системных вызовов. Использование этого добавляет много накладных расходов само по себе, но если вы просто сравниваете стоимость одного recv для этой цели со стоимостью двух, это должно быть разумно значимым. Используйте опцию -tt, чтобы узнать время. Или вы можете использовать опцию -c, чтобы получить общее представление о затраченном времени, разделенном на то, на какой системный вызов он был потрачен.

Лучший способ измерить, хотя и с большим количеством кривой обучения, это oprofile.

Также обратите внимание, что если вы решите, что буферизация имеет смысл, вы можете использовать fdopen и функции stdio, чтобы позаботиться об этом за вас. Это очень просто и будет хорошо работать, если вы имеете дело только с одним соединением или если у вас есть поток / процесс на соединение, но не будет работать вообще, если вы хотите использовать select / poll - основанная модель.

1 голос
/ 26 февраля 2011

Обратите внимание, что вы, как правило, должны в любом случае «прочитать все доступные данные в буфер и обработать их впоследствии», чтобы учесть (маловероятный, но возможный) сценарий, когда вызов recv() возвращает только часть вашего заголовка - такс таким же успехом можно пройти всю свинью и использовать вариант 2.

0 голосов
/ 24 февраля 2011

Да, в зависимости от сценария, вызовы read / recv могут быть дорогими. Например, если вы выполняете огромное количество вызовов recv () для чтения очень небольшого объема данных через каждый небольшой интервал, это может привести к снижению производительности. В таком случае вы можете запустить recv () с достаточно большим буфером, скажем, 4k, а затем проанализировать этот буфер 4k. Может содержать несколько заголовков + комбо данных. Сначала прочитав заголовок, вы сможете найти данные и их длину. И чтобы избежать копирования копии данных в новый буфер, вы можете просто использовать смещение, откуда начинаются фактические данные, и сохранить этот указатель.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...