Можно было бы сказать, что компьютер, как и человек, плохо справляется с многозадачностью. В основном поэтому возникает желание отправлять MTU-пакеты максимального размера, позволяющего избежать фрагментации. Механизмы наподобие алгоритма Нагля и метода Кларка также являются попытками избежать отправки маленьких пакетов.
Минимизируйте число операций с данными
Самый простой способ реализовать многоуровневый стек протоколов — использовать один модуль для каждого уровня. К сожалению, это приводит к многократному копированию данных (или, по крайней мере, многократному доступу к ним). К примеру, после того как пакет принимается сетевой картой, он обычно копируется в системный буфер ядра. Оттуда он копируется в буфер сетевого уровня для обработки сетевого уровня, а затем — в буфер транспортного уровня для обработки транспортного уровня, и наконец, доставляется получающему приложению. Часто полученный пакет копируется три или четыре раза, прежде чем содержащийся в нем сегмент доставляется по назначению.
Такое копирование может сильно снизить производительность, так как операции с памятью часто оказываются в десятки раз медленнее, чем операции с использованием только внутренних регистров. К примеру, если 20 % инструкций действительно связано с обращением к памяти (то есть данных нет в кэше), — а это вполне вероятная цифра при обработке входящих пакетов, — среднее время выполнения инструкции снизится в 2,8 раз (0,8 х 1 + 0,2 х 10). Аппаратные улучшения здесь не помогут. Проблема состоит в слишком большом числе операций копирования, выполняемых операционной системой.
Разумная операционная система постарается уменьшить число операций копирования, объединяя процессы обработки на разных уровнях. К примеру, TCP и IP обычно работают вместе («TCP/IP»), поэтому, когда обработка переходит от сетевого уровня к транспортному, копировать полезную нагрузку пакета не нужно. Еще один популярный прием состоит в том, чтобы выполнять несколько операций за один обход. Например, контрольные суммы часто вычисляются во время копирования данных (когда это действительно необходимо), и новое значение добавляется в конец.
Минимизируйте количество переключений контекста
Переключения контекста (например, из режима ядра в режим пользователя) обладают рядом неприятных свойств, в этом они сходны с прерываниями. Самое неприятное — потеря содержимого кэша. Количество переключений контекста может быть снижено при помощи библиотечной процедуры, посылающей данные во внутренний буфер до тех пор, пока их не наберется достаточное количество. Аналогично, на получающей стороне небольшие сегменты следует собирать вместе и передавать пользователю за один раз, минимизируя количество переключений контекста.
В лучшем случае, прибывший пакет вызывает одно переключение контекста из текущего пользовательского процесса в режим ядра, а затем еще одно переключение контекста при передаче управления принимающему процессу и предоставлении ему прибывших данных. К сожалению, во многих операционных системах происходит еще одно переключение контекста. Например, если сетевой менеджер работает в виде отдельного процесса в пространстве пользователя, поступление пакета вызывает передачу управления от процесса пользователя ядру, затем от ядра сетевому менеджеру, затем снова ядру, и наконец, от ядра получающему процессу. Эта последовательность переключений контекста показана на рис. 6.44. Все переключения контекста при получении каждого пакета сильно расходуют время центрального процессора и существенно снижают производительность сети.
Рис. 6.44. Четыре переключения контекста для обработки одного пакета, в случае если сетевой менеджер находится в пространстве пользователя
Лучше избегать перегрузки, чем бороться с уже возникшей перегрузкой
Старая пословица, гласящая, что профилактика лучше лечения, справедлива и в деле борьбы с перегрузками в сетях. Когда в сети образуется затор, пакеты теряются, пропускная способность растрачивается впустую, увеличиваются задержки и т. п. Все эти издержки нежелательны. Процесс восстановления после перегрузки требует времени и терпения. Гораздо более эффективной стратегией является предотвращение перегрузки, напоминающее прививку от болезни — это несколько неприятно, зато избавляет от возможных больших неприятностей.
Избегайте тайм-аутов
Таймеры необходимы в сетях, но их следует применять умеренно и стремиться минимизировать количество тайм-аутов. Когда срабатывает таймер, обычно повторяется какое-либо действие. Если повтор действия необходим, так и следует поступить, однако повторение действия без особой необходимости является расточительным.
Чтобы избегать излишней работы, следует устанавливать период ожидания с небольшим запасом. Таймер, срабатывающий слишком поздно, несколько увеличивает задержку в (маловероятном) случае потери сегмента. Преждевременно срабатывающий таймер растрачивает попусту время процессора, пропускную способность и напрасно увеличивает нагрузку на, возможно, десятки маршрутизаторов.
6.6.4. Быстрая обработка сегментов
Теперь, когда мы поговорили об общих правилах, рассмотрим несколько методов, позволяющих ускорить обработку сегментов. Дополнительные сведения по этой теме см. в (Clark и др., 1989; Chase и др., 2001).
Накладные расходы по обработке сегментов состоят из двух компонентов: затрат по обработке заголовка и затрат по обработке каждого байта. Следует вести наступление сразу на обоих направлениях. Ключ к быстрой обработке сегментов лежит в выделении нормального случая (односторонней передачи данных) и отдельной обработке этого случая. Многие протоколы придают особое значение тому, что следует делать при возникновении нештатной ситуации (например, при потере пакета). Однако, чтобы протоколы работали быстро, разработчик должен пытаться минимизировать время обработки данных в нормальном режиме. Минимизация времени обработки при возникновении ошибок является лишь второстепенной задачей.
Хотя для перехода в состояние ESTABLISHED требуется передача последовательности специальных сегментов, но, как только это состояние достигнуто, обработка сегментов не вызывает затруднений, пока одна из сторон не начнет закрывать соединение. Начнем с рассмотрения посылающей стороны, находящейся в состоянии ESTABLISHED, когда должны отправляться данные. Для простоты мы предположим, что транспортная подсистема расположена в ядре, хотя те же самые идеи применимы и в случае, когда она представляет собой процесс, находящийся в пространстве пользователя, или набор библиотечных процедур в посылающем процессе. На рис. 6.45 отправляющий процесс эмулирует прерывание, выполняя базовую операцию SEND, и передает управление ядру. Прежде всего, транспортная подсистема проверяет, находится ли она в нормальном состоянии, то есть таком, когда установлено состояние ESTABLISHED, ни одна сторона не пытается закрыть соединение, посылается стандартный полный сегмент (без флага URGENT ) и у получателя достаточный размер окна. Если все эти условия выполнены, то никаких дополнительных проверок не требуется, и алгоритм транспортной подсистемы может выбрать быстрый путь. В большинстве случаев именно так и происходит.
В нормальной ситуации заголовки соседних сегментов почти одинаковы. Чтобы использовать этот факт, транспортная подсистема сохраняет в своем буфере прототип заголовка. В начале быстрого пути он как можно быстрее, пословно, копируется в буфер нового заголовка. Затем поверх перезаписываются все отличающиеся поля. Часто эти поля легко выводятся из переменных состояния — например, следующий порядковый номер сегмента. Затем указатель на заполненный заголовок сегмента и указатель на данные пользователя передаются сетевому уровню. Здесь может быть применена та же стратегия (на рис. 6.45 это не показано). Наконец, сетевой уровень передает полученный в результате пакет канальному уровню для отправки.