Содержание


Разработка модулей ядра Linux

Часть 32. Принципы работы с сетевой подсистемой

Comments

Серия контента:

Этот контент является частью # из серии # статей: Разработка модулей ядра Linux

Следите за выходом новых статей этой серии.

Этот контент является частью серии:Разработка модулей ядра Linux

Следите за выходом новых статей этой серии.

Мы уже получили достаточно информации, чтобы проследить путь пакетов по стеку сетевых протоколов и разобраться, как буфера сокетов возникают в системе и покидают её. Также мы сможем ответить на вопрос, почему высокоуровневые протоколы не создают и не уничтожают буферы сокетов, а только обрабатывают содержащуюся в них информацию.

Традиционный подход к реализации сетевых интерфейсов

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

  1. Считав конфигурационную область сетевого PCI-адартера при инициализации модуля, можно определить линию прерывания IRQ, которая будет обслуживать сетевой обмен.
    char irq; 
    pci_read_config_byte( pdev, PCI_INTERRUPT_LINE, &byte );
  2. При инициализации сетевого интерфейса для этой IRQ-линии устанавливается обработчик прерывания my_interrupt():
    request_irq( (int)irq, my_interrupt, IRQF_SHARED, "my_interrupt", &my_dev_id );
  3. В обработчике прерывания при приёме нового пакета (это же прерывание может происходить и при завершении отправки пакета, поэтому необходимо точно определить его причину) создаётся (или запрашивается из пула используемых) новый экземпляр буфера сокетов:
    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 для исполнения.

Высокоскоростные интерфейсы

Особенность сетевых интерфейсов состоит в том, что их функционирование имеет "взрывной" характер: после продолжительных периодов молчания возникают интервалы пиковой активности, когда сетевые пакеты (сегментированные на IP-пакеты объёмы передаваемых данных) следуют плотным потоком. После такого всплеска активности может снова наступить "затишье" или активность снизится до минимума (например, обмен ARP-пакетами для обновления информации разрешения локальных адресов). Хотя современные Ethernet-адаптеры используют скорости обмена до 10Gbit/s, но уже даже при значительно меньшей нагрузке традиционный подход к построению сетевых интерфейсов становится нецелесообразным. Так, в периоды высокой плотности поступления пакетов:

  • новые приходящие пакеты создают вложенные многоуровневые IRQ-запросы при ещё не обслуженном приёме текущего IRQ;
  • асинхронное обслуживание каждого IRQ в плотном потоке создаёт слишком большие накладные расходы.

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

  • при поступлении первого пакета из "комплекта" инициируется IRQ-прерывание адаптера, как и при традиционном подходе.
  • в обработчике прерывания запрещается поступление дальнейших запросов прерывания с этой линии IRQ-запросов для приёма пакетов, но IRQ-запросы с этой же линии для отправки пакетов могут продолжать поступать. Это ограничение реализуется не за счёт программного запрета IRQ-линии со стороны процессора, а благодаря записи управляющей информации в аппаратные регистры сетевого адаптера. Поэтому адаптер должен предусматривать раздельное управление поступлением прерываний по приёму и передаче, но большинство современных высокоскоростных адаптеров обладает такой возможностью.
  • после прекращения прерываний по приёму обработчик переходит в режим циклического считывания и обработки принятых из сети пакетов, сетевой адаптер при этом накапливает поступающие пакеты во внутреннем кольцевом буфере. Считывание производится до полного исчерпания кольцевого буфера или до определённого порогового числа считанных пакетов (10, 20, ...), называемого бюджетом (budget) функции полинга.
  • это считывание и обработка пакетов происходит не в самом обработчике прерывания (верхней половине), а в его отсроченной части. Для каждого принятого пакета генерируется сокетный буфер для последующей отправки по стеку сетевых протоколов.
  • после завершения цикла программного опроса и по его результатам устанавливается состояние завершения NAPI_STATE_DISABLE (если в кольцевом буфере адаптера не осталось несчитанных пакетов) или NAPI_STATE_SCHED (это говорит, что опрос адаптера должен быть продолжен, когда ядро в следующий раз перейдёт к циклу опросов в отложенном обработчике прерываний).
  • если результатом является NAPI_STATE_DISABLE, то после завершения цикла программного опроса восстанавливается разрешение генерации прерываний по IRQ-линии приёма пакетов с записью в порты сетевого адаптера;

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

  1. Программист модуля обязан предварительно создать и зарегистрировать специфичную для модуля функцию опроса (poll-функцию), используя вызов netif_napi_add() c указанными параметрами (см. файл <netdevice.h>):
    static inline void netif_napi_add( struct net_device *dev,
                                       struct napi_struct *napi,
                                       int (*poll)( struct napi_struct *, int ),
                                       int weight );
    • dev — рассмотренная ранее структура зарегистрированного сетевого интерфейса;
    • poll — регистрируемая функция программного опроса, которая будет описана далее;
    • weight — приоритет, устанавливаемый разработчиком для этого интерфейса; для 10Mb и 100Mb адаптеров часто указывается значение 16, а для 10Gb и 100Gb — значение 64;
    • napi — дополнительный параметр, указатель на специальную структуру, которая будет передаваться при каждом вызове poll-функции; поле state этой структуры после выполнения функции будет заполняться значениями NAPI_STATE_DISABLE или NAPI_STATE_SCHED; информацию об этой структуре можно найти в файле <netdevice.h>.
  2. Пример функции программного опроса (которая является специфической для каждой конкретной задачи и реализуется в коде модуля) приведен в листинге 1.
    Листинг 1. Функция для программного опроса
    static int my_card_poll( struct napi_struct *napi, int budget ) {
       int work_done; // число реально обработанных в цикле опроса сетевых пакетов// приём пакетов в соответствии со спецификацией задачи
       work_done = my_card_input( budget, ... ); 
       if( work_done < budget ) {
          netif_rx_complete( netdev, napi );
          my_card_enable_irq( ... ); // разрешить IRQ приёма
       }
       return work_done;
    }

    В представленном фрагменте пользовательская функция my_card_input() в цикле пытается аппаратно считать budget сетевых пакетов, и для каждого считанного сетевого пакета создаёт сокетный буфер и вызывает метод netif_receive_skb(), после чего буфер начинает движение вверх по стеку протоколов. Если кольцевой буфер сетевого адаптера исчерпался до считывания budget пакетов, то адаптеру разрешается возбуждать прерывания по приёму, а ядро вызовом netif_rx_complete() уведомляется, что отложенное программное прерывание NET_RX_SOFTIRQ для дальнейшего вызова функции опроса отменяется. Если же удалось считать budget пакетов (и в буфере адаптера, видимо, остались ещё не обработанные пакеты), то опрос продолжится при следующем цикле обработки отложенного программного прерывания NET_RX_SOFTIRQ.

  3. В листинге 2 приведен пример обработчика аппаратного прерывания по IRQ-линии сетевого адаптера (активизирующийся при прибытии первого сетевого пакета).
    Листинг 2. Функция для обработки прерывания
    static irqreturn_t my_interrupt( int irq, void *dev_id ) { 
       struct net_device *netdev = dev_id;
       if( likely( netif_rx_schedule_prep( netdev, ... ) ) ) {
          my_card_disable_irq( ... ); // запретить IRQ приёма
          __netif_rx_schedule( netdev, ... );
       }
       return IRQ_HANDLED;
    }

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

    • вызов netif_rx_schedule_prep() подготавливает устройство к помещению в список для программного опроса, устанавливая состояние в NAPI_STATE_SCHED;
    • если вызов функции netif_rx_schedule_prep() был успешен (а противное возможно, только если NAPI уже активен), то вызов __netif_rx_schedule() помещает устройство в список для программного опроса в цикл обработки отложенного программного прерывания NET_RX_SOFTIRQ.

На этом описание новой модели приёма сетевых пакетов можно считать завершенным, но нужно учитывать, что количество пакетов (параметр budget), устанавливаемое в функции опроса, не должно быть чрезмерно большим:

  1. Опрос не должен потреблять более одного системного тика (глобальная переменная jiffies), иначе это будет искажать диспетчеризацию потоков ядра;
  2. Количество пакетов не должно быть больше глобально установленного ограничения
    $ cat /proc/sys/net/core/netdev_budget 
    300

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

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

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

struct net_device_ops ndo = { 
   .ndo_open = my_open, 
   .ndo_stop = my_close, 
   .ndo_start_xmit = stub_start_xmit, 
};

Эта функция при вызове должна обеспечить аппаратную передачу полученного сокета в сеть, после чего уничтожить или возвратить буфер сокета в пул.

static int stub_start_xmit( struct sk_buff *skb, struct net_device *dev ) { 
   // ... аппартное обслуживание передачи
   dev_kfree_skb( skb ); 
   return 0; 
}

Чаще уничтожение отправляемого буфера будет происходить не при инициализации операции, а при её (успешном) завершении, что отслеживается по той же линии IRQ, упоминавшейся выше.

Заключение

Часто возникает вопрос: а где же в этом процессе находится место, где создаётся информация, помещаемая в буфер, или потребляется информация из принимаемых буферов? Это место находится за пределами сетевого стека ядра, так как любая информация, отправляемая в сеть или потребляемая из сети, становится видимой только на прикладных уровнях, в приложениях из пользовательского пространства. Интерфейс из этого прикладного уровня в стек протоколов ядра обеспечивается известным POSIX API сокетов прикладного уровня.

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


Ресурсы для скачивания


Похожие темы


Комментарии

Войдите или зарегистрируйтесь для того чтобы оставлять комментарии или подписаться на них.

static.content.url=http://www.ibm.com/developerworks/js/artrating/
SITE_ID=40
Zone=Linux, Open source
ArticleID=844077
ArticleTitle=Разработка модулей ядра Linux: Часть 32. Принципы работы с сетевой подсистемой
publish-date=11012012