Linux, принципы работы с сетевой подсистемой

Сетевая подсистема Linux

Сетевая модель TCP/IP условно согласуется с 7-ми уровневой моделью OSI. На платформе Linux сложилась следующая терминология разделения на подуровни, которой пользуются разработчики ядра:

  • Всё, что относится к поддержке оборудования и канальному уровню, описывается как сетевые интерфейсы и обозначается как L2 (обычно тут у нас Ethernet);
  • Протоколы сетевого уровня OSI (IP, IPX, RIP и т.д.) - как сетевой уровень стека протоколов или уровень L3;
  • Всё, что выше (ICMP, UDP, TCP, SCTP и т.д.) - как протоколы транспортного уровня или уровень L4;
  • Всё то, что относится к выше лежащим уровням (сеансовый, представительский, прикладной) модели OSI (например: SSH, SIP, RTP, HTTP, FTP и т.д.), не проявляется в ядре и относится уже к области клиентских и серверных утилит пространства пользователя.

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

Протоколы (TCP/IP)

(буферы, опции, таймауты)

Очереди на отправку и прием

Физический (Опрос, IRQ, отложенные IRQ)

Каждый из них имеет свои характеристики и настройки, и, хотя они относительно независимы между собой, неправильная настройка одного из них может привести к некорректной работе всей модели. Например, неправильная работа физической части приведет к тому, что при сильном трафике система будет затоплена прерываниями, или неверно настроенный TCP/IP может стать причиной низкой производительности протокола. И хотя при этом будут идеально настроены остальные 2 уровня, сетевая подсистема по-прежнему не будет работать в полную силу.

Как и в модели OSI, в сетевой модели ОС, каждый уровень выполняет свою функцию. Физический уровень занимается тем, что принимает и отправляет пакеты в сеть, а также заботится о размещении их в буферах сетевой карты. Для этого уровня необходимо произвести настройку буферов сетевой карты и задать способ обработки событий от него: по прерыванию (отложенное как правило) или режим опроса.

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

Условно, по уровню взаимодействия с узлом, весь трафик в сети можно разделить на две категории - собственный и маршрутный:

Трафик в сети

 

 Физический уровень

Всю работу сетевого интерфейса можно разделить на два процесса - это прием/передача пакетов и размещение их в буферах. Оба эти процесса взаимосвязаны между собой - перед тем как пакет будет отправлен в сеть, сначала он помещается в буфер сетевой карты, а в случае приема пакета из сети, наоборот.

Драйвер выделяет буфера в физической памяти компьютера (как правило используется DMA – прямой доступ к памяти (direct memory access)), где сетевая карта сохраняет вновь прибывшие пакеты. Для определения размера выделяемой памяти используются, как правило, два параметра - это количество буферов (один буфер - один пакет), которые задаются в конфигурации сетевой карты, и максимальный размер передаваемого сегмента (maximum transfer unit, MTU). Последний параметр помогает драйверу определить, сколько памяти необходимо выделить под один буфер. Если MTU не использовать, то может случиться, что выделенный буфер окажется меньше, чем принимаемый пакет, или выделенной памяти окажется слишком много, что тоже неприятно. Чем больше MTU и/или количество буферов, тем больше памяти будет зарезервировано.

При инициализации драйвера память для входящих пакетов будет выделена статически, и в будущем никто не сможет использовать её, пока работает драйвер. Необходимость такого подхода обусловлена тем, что если сетевая карта будет работать в режиме опроса или отложенных прерываний, о которых речь пойдет чуть ниже, то ей придется последовательно принимать несколько пакетов и размещать их в памяти без уведомления об этом событии центрального процессора (ЦП). А раз так, то и место, куда будут складываться пакеты, должно быть заранее подготовлено.

Для исходящих ситуация выглядит несколько иначе. Для них также задается в конфигурации максимальное количество буферов, но память выделяется динамически, по запросу. В этом случае ЦП так или иначе будет задействован, к тому же размер данных заранее известен, поэтому можно выделить ровно столько памяти, сколько необходимо.

 

Реализация в Linux

Реализация сетевой подсистемы в Linux организована так, чтобы не зависеть от особенностей реализации протоколов. Основной структурой данных, описывающей сетевое устройство, является struct net_device, а основной структурой, с помощью которой происходит обмен данными между сетевыми уровнями и на основе которой построена работа всей сетевой подсистемы является буфер сокета - struct sk_buff.

Cтруктура sk_buff

Буфер сокета состоит их двух частей:

  1. Управляющие данные, находящиеся в структуре struct sk_buff;
  2. Данные пакета (указываемые в struct sk_buff указателями head и data).

Буферы сокетов упорядочиваются в виде очереди (struct sk_queue_head) посредством своих двух первых полей next и prev.

  Фрагмент структуры sk_buff

typedef unsigned char *sk_buff_data_t;

struct sk_buff {

   struct sk_buff *next; /* эти два элемента должны быть объявлены первыми. */

   struct sk_buff *prev;

...

   sk_buff_data_t  transport_header;

   sk_buff_data_t  network_header;

   sk_buff_data_t  mac_header;

...

   unsigned char *head,

                 *data;

...

};

Структура вложенности заголовков сетевых уровней в точности соответствует структуре инкапсуляции сетевых протоколов внутри друг друга.

Экземпляры данных типа struct sk_buff:

  • создаются при поступлении очередного сетевого пакета из внешней физической среды (нужно учитывать возможность сегментации пакетов). Об этом событии извещает прерывание (IRQ), генерируемое сетевым адаптером. При этом создаётся (или извлекается из пула использованных) экземпляр буфера сокета, который заполняется данными из поступившего пакета и далее передаётся вверх от сетевого слоя до приложения прикладного уровня, являющегося получателем пакета. После этого экземпляр буфера сокета уничтожается.
  • создаются в среде приложения прикладного уровня, являющегося отправителем пакета данных. Пакет отправляемых данных помещается в созданный буфер сокета, который передается вниз от сетевого слоя до канального уровня L2. На этом уровне осуществляется физическая передача пакета через сетевой адаптер. В случае успешного завершения передачи (что подтверждается прерыванием, генерируемым сетевым адаптером) буфер сокета уничтожается (утилизируется). При отсутствии подтверждения отправки обычно предпринимается несколько повторных попыток прежде, чем принять решение об ошибке канала.

 

Сетевой интерфейс – это конечная точка, где начинается или завершается обработка буфера сокета (пакета данных).

Основу описания сетевого интерфейса составляет структура struct net_device. Эта структура, содержит не только описание аппаратных средств, но и конфигурационные параметры сетевого интерфейса по отношению к выше лежащим протоколам.

Фрагмент структуры net_device

struct net_device {

  char  name[ IFNAMSIZ ] ;

...

   unsigned long  base_addr; /* I/O-адрес устройства */

   unsigned int   irq;       /* IRQ-номер устройства */

...

   unsigned short type;      /* тип интерфейса */

...

}

Поле type, например, определяет тип аппаратного адаптера с точки зрения ARP-механизма разрешения MAC-адресов (файл <linux/if_arp.h>), как показано ниже:

#define ARPHRD_ETHER       1    /* Ethernet 10Mbps           */

...

#define ARPHRD_IEEE80211 801    /* IEEE 802.11               */

 

Есть две реализации сетевой подсистемы в Linux: традиционный с прерываниями и NAPI (New API), специально разаработанный для гигабитных сетевых карточек.

Устаревшая (традиционная) модель (модель прерываний)

По умолчанию многие сетевые карты работают в режиме простого прерывания, при котором прием и отправка пакетов выглядит следующим образом:

каждый приходящий сетевой пакет (который будет храниться в внутреннем буфере устройства) порождает аппаратное прерывание по IRQ-линии адаптера, что служит сигналом на приём очередного сетевого пакета и создание буфера сокета для его сохранения и обработки принятых данных. Порядок действий модуля сетевого интерфейса при этом следующий:

 Классическая модель прерываний

  1. Считав конфигурационную область сетевого PCI-адартера при инициализации модуля, определяется линия прерывания IRQ, которая будет обслуживать сетевой обмен.
  2. При инициализации сетевого интерфейса для этой IRQ-линии устанавливается обработчик прерывания my_interrupt().Вызываемый обработчик помещает в очередь backlog (netdev_max_backlog) только указатель на пакет, нет смысла копировать сам пакет, так как система может напрямую обратиться к нему. В случае с отложенными прерываниями будет помещаться не один пакет, а группа пакетов;
  3. Затем, из этой очереди пакеты поступают на обработку в TCP/IP стек.

static irqreturn_t my_interrupt( int irq, void *dev_id ) {

   ...

   struct sk_buff *skb = kmalloc( sizeof( struct sk_buff ), ... );

   // заполнение *skb данными, считанными из портов сетевого адаптера

   netif_rx( skb );

   return IRQ_HANDLED;

}

Все эти действия выполняются не в самом обработчике верхней половины прерываний от сетевого адаптера, а в обработчике отложенного прерывания NET_RX_SOFTIRQ для этой IRQ-линии. Последним действием является передача заполненного сокетного буфера вызову netif_rx() (или netif_receive_skb()), который и запустит процесс перемещения буфера вверх по структуре сетевого стека, т.е. отметит отложенное программное прерывание NET_RX_SOFTIRQ для исполнения. Другими словами, в устаревшей модели доставка фреймов к стеку протоколов происходит через функцию netif_rx, которая обычно вызывается в контексте обработчика прерываний (во время приема фреймов).

Если при попытке расположить очередной указатель на пакет, окажется, что очередь полная, то пакет просто отбрасывается. Это будет происходить до тех пор, пока очередь не освободится. Во время работы очередь ведет статистику, которую можно посмотреть через /proc/net/softnet_stat.

Такой подход обработки событий на сетевом интерфейсе подходит лишь для небольшого трафика. Обрабатывать 5-10 тыс. пакетов в секунду не составляет труда даже для среднего компьютера, но с увеличением потока система начнет все больше и больше времени тратить на обработку прерываний с сетевой карты, что отрицательно скажется на общей производительности операионной системы. С дальнейшим ростом трафика обрабатывать каждый кадр отдельно станет слишком дорогим удовольствием, начинается катастрофа. Даже для современных процессоров, трафик в 200 тыс. кадров в секунду приведет к тому, что система будет полностью поглощена его обработкой, и на работу пользовательских приложений совершенно не останется времени. Новые приходящие пакеты создают вложенные многоуровневые IRQ-запросы при ещё не обслуженном приёме текущего IRQ. Большой трафик станет причиной возникновения ошибок на сетевом интерфейсе. Для нормальной работы сети необходимо снизить количество вызываемых прерываний на сетевой карте. Добиться этого можно двумя способами - обработкой событий в режиме опроса устройства (polling) и использованием отложенных прерываний. Режим опроса является программной реализацией на уровне операционной системы, а отложенные прерывания - аппаратной.

NAPI (новая модель)

Поэтому в Linux был добавлен набор API для обработки подобных потоков пакетов, поступающих с высокоскоростных интерфейсов, который получил название NAPI (New API). Его идея состоит в том, чтобы осуществлять приём пакетов не методом аппаратного прерывания, а методом программного опроса (polling), точнее, комбинацией этих двух возможностей:

 NAPI модель

  • при поступлении первого пакета из комплекта инициируется IRQ-прерывание адаптера, как и при традиционном подходе
  • драйвер сообщает сетевой подсистеме о доступности нового фрейма (а не обработайте их сразу), чтобы использовать "метод опроса" за пределами контекста выполнения ISR (обработчика прерываний).

Сетевая карта регистрирует себя в poll списке и отключает прерывания с этой линии IRQ-запросов для приёма пакетов, но IRQ-запросы с этой же линии для отправки пакетов могут продолжать поступать. Это ограничение реализуется не за счёт программного запрета IRQ-линии со стороны процессора, а благодаря записи управляющей информации в аппаратные регистры сетевого адаптера. Поэтому адаптер должен предусматривать раздельное управление поступлением прерываний по приёму и передаче, но большинство современных высокоскоростных адаптеров обладает такой возможностью.

  • после прекращения прерываний по приёму обработчик переходит в режим циклического считывания (время от времени, проверяет poll список) и обработки принятых из сети пакетов (когда обнаружит устройство в poll списке), сетевой адаптер при этом накапливает поступающие пакеты во внутреннем кольцевом буфере. Считывание производится до полного исчерпания кольцевого буфера или до определённого порогового числа считанных пакетов, называемого бюджетом (budget, /proc/sys/net/core/netdev_budget) функции полинга.  net.core.netdev_max_backlog с NAPI не работает. Если кольцевой буфер сетевого адаптера исчерпался до считывания budget пакетов, то адаптеру разрешается возбуждать прерывания по приёму.
  • это считывание и обработка пакетов происходит не в самом обработчике прерывания (верхней половине), а в его отсроченной части. Для каждого принятого пакета (а их за раз может быть принято budget штук) генерируется сокетный буфер и вызывается метод netif_receive_skb(),  после чего буфер начинает движение вверх по стеку протоколов.
  • после завершения цикла программного опроса и по его результатам устанавливается состояние завершения NAPI_STATE_DISABLE(если в кольцевом буфере адаптера не осталось несчитанных пакетов, сетевая карта удаляется из poll списка и снова включаются прерывания) или NAPI_STATE_SCHED (это говорит, что опрос адаптера должен быть продолжен, когда ядро в следующий раз перейдёт к циклу опросов в отложенном обработчике прерываний).
  • если результатом является NAPI_STATE_DISABLE, то после завершения цикла программного опроса восстанавливается разрешение генерации прерываний по IRQ-линии приёма пакетов с записью в порты сетевого адаптера;

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

  

Передача пакетов

Выше было описано, как происходит создание и перемещение сокетного буфера вверх по стеку.

Схема отправки пакетов, как в режиме NAPI, прерываний и отложенных прерываний выглядит одинаково.

 Передача пакетов

Очередь на отправку называется txqueue и регулируется через команду ifconfig (Например, ifconfig eth0 txqueuelen 3000. Значение по умолчанию 1000. Минимальное значение должно быть равным количеству буферов для отправки на сетевом адаптере иначе можно переполнить). По своему принципу работы txqueue схожи с очередью backlog, в неё также помещаются только указатели на пакет, и в случае переполнения очереди отбрасывается. Отправка пакетов через эту очередь выглядит следующим образом.

  • После того как данные были упакованы в IP пакет, в очередь размещается указатель на пакет. В случае, если очередь переполнена, пакеты отбрасываются;
  • Драйверу сетевой карты сообщают о том, что готовы пакеты для отправки;
  • Сетевая карта отправляет пакет или группу пакетов и сообщает об этом системе через генерацию прерывания.

 

Комментарии  

 
+2 #2 Сергей Кулешов 18.06.2020 19:40
Цитирую Evgeny:
Спасибо! Отличное, подробное объяснение.
Подскажите пожалуйста, это ваша статья или перевод?

Эта статья получилась из поиска понимания как это работает и как этим управлять, чтения различных документаций, интернет форумов, статей.
Цитировать
 
 
+3 #1 Evgeny 18.06.2020 13:44
Спасибо! Отличное, подробное объяснение.
Подскажите пожалуйста, это ваша статья или перевод?
Цитировать
 
unix-way