Бывает, что приложению требуется узнать точное время приема или отправки сетевого пакета. Например, для синхронизации часов (см. PTP, NTP) или тестирования задержек в сети (см. RFC2544). Наивным решением будет запоминать в приложении время сразу после получения пакета от ядра (или перед отправкой ядру): Ясно, что полученное таким образом время может заметно отличаться от момента, когда пакет был получен сетевым устройством. Для получения более точного времени нужна поддержка от операционной системы, драйвера и/или сетевого устройства. Начиная с версии 2.6.30 Линукс поддерживает опцию сокета SO_TIMESTAMPING. Она позволяет пользовательскому сокету получать временные метки для отправляемых и принимаемых пакетов. Временные метки могут быть сняты самим ядром, драйвером или сетевым устройством (см. список поддерживающих устройств и драйверов). О том, что это вообще такое и как этим пользоваться, стоит почитать в Documentation/networking/timestamping.txt В этой статье я расскажу о том, как пакеты доставляются от сетевого устройства пользователю, когда при этом снимаются временные метки, как они доставляются пользователю и насколько они точны. Приведенные примеры кода ядра взяты из версии 4.1. Все сетевые пакеты в ядре представляются структурой struct sk_buff, которая объявлена в файле include/linux/skbuff.h. Рассмотрим некоторые из ее полей: Экземпляры этой структуры я буду коротко называть skb. Вместе с каждой struct sk_buff выделяется буфер под заголовки с полезными данными(тот самый, в который указывают skb->head, skb->tail) и следующую сразу за ними структуру struct skb_shared_info. (см. include/linux/skbuff.h). Причем несколько разных skb могут ссылаться на один буфер и соответствующую ему struct skb_shared_info. Это бывает удобно при доставке одного skb нескольким пользователям, которым позволено менять поля struct sk_buff, но не данные в буфере. Самые интересные для нас поля struct skb_shared_info: Для доступа к skb_shared_info есть макрос skb_shinfo(skb), а для доступа к полю hwtstamps — функция skb_hwtstamps(skb). (см. include/linux/skbuff.h) Мы не станем подробно рассматривать эти структуры. Сейчас достаточно понять, что первая позволяет ядру общаться с устройством, а вторая — с пользовательским сокетом. Для принятого пакета: skb->dev указывает на устройство, которым он был получен; skb->sk на сокет, которому будет доставлен. Для отправляемого — наоборот. Когда процессор получает прерывание, он вызывает соответствующий ему обработчик. Выполнение обработчика происходит в контексте прерывания — для обслуживающего прерывание процессора почти все прерывания выключены. То есть обработчик прерывания не будет прерван, пока не завершится сам. Чем меньше процессор находится в контексте прерывания, тем скорее он сможет обслужить новые прерывания и отреагировать на события от других устройств. Несмотря на то, что обработчику прерывания может требоваться много процессорного времени, обычно большая часть его работы может подождать. Именно поэтому действия обработчика прерывания принято разделять на верхнюю(Top Half) и нижнюю(Bottom Half) половины. Top Half в контексте прерывания выполняет срочные действия и планирует для выполнения Bottom Half. Bottom Half будет запущена ядром позже вне контекста прерывания и может быть прервана во время работы другими прерываниями. SOFTIRQ — Механизм ядра, позволяющий запланировать отложенный вызов функции. Часто используется для реализации Bottom Half. Всего в ядре v4.1 десять разных SOFTIRQ (см. список), обработчики для которых определяются при компиляции ядра. Во время своего выполнения обработчик может быть прерван только аппаратным прерыванием. Для каждого процессора своя маска запланированных SOFTIRQ, т.е обработчик будет вызван на том же процессоре, с которого был запланирован. (На самом деле, можно ухитриться и указать на каком конкретно процессоре запланировать SOFTIRQ.) Одно и то же SOFTIRQ может быть запланировано и
выполнено независимо на двух разных процессорах. При отправке и получении пакетов используются два из них: NET_RX_SOFTIRQ с обработчиком net_rx_action и NET_TX_SOFTIRQ с обработчиком net_tx_action. (см. net_rx_action и net_tx_action) Для выполнения запланированных SOFTIRQ служит функция do_softirq() (см kernel/softirq.c). Она поочередно вызывает обработчики SOFTIRQ, начиная с самых приоритетных(меньший номер — более высокий приоритет). Она вызывается после каждого обработчика аппаратного прерывания. Кроме того, на каждом процессоре крутится процесс ядра ksoftirqd, который периодически(как часто — зависит от нагрузки на процессор) вызывает do_softirq(). Для общения с сетевыми устройствами Линукс использует смесь прерываний и поллинга (polling). (см. NAPI) Вот как это выглядит: poll_list&ast; — список, создаваемый ядром для каждого ядра процессора(как одно из полей struct softnet_data). Он хранит устройства, с которых NET_RX_SOFTIRQ предстоит получить пакеты. Произошел вызов do_softirq(). После выполнения более приоритетных SOFTIRQ, будет вызван обработчик NET_RX_SOFTIRQ — net_rx_action(). Эта функция проходит по списку poll_list и для каждого устройства dev, пока на нем есть пакеты, вызывает виртуальную функцию драйвера napi->poll (см. include/linux/netdevice.h.), которая: Стоит отметить, что net_rx_action() позволяет обрабатывать не более netdev_budget(экспортируется в /proc/sys/net/core/netdev_budget) пакетов за раз и ограничивает время своего выполнения 2/HZ секундами(на x86 по умолчанию HZ = 1000, т.е ограничение времени = 2мс). netif_receive_skb() — это функция, начиная с которой пакет попадает из драйвера в ядро. (На самом деле, она просто служит оберткой для других функций, которые и выполняют всю работу.) Посмотрим, что же делает ядро с полученным skb: Мы видим, что первым делом после получения пакета, функция вызывает макрос net_timestamp_check. Теперь по порядку: Обычно пакет обрабатывается тем процессором, на котором был запланирован SOFTIRQ, а это тот процессор, на который пришло прерывание. Некоторые сетевые карты шлют только одно прерывание только одному процессору, не позволяя распараллелить обработку пакетов на многопроцессорных системах. Для решения этой проблемы был придуман Receive Packet Steering (RPS). Если в ядре включен RPS, netif_receive_skb_internal() может поставить пакет в очередь (backlog) другого процессора и запланировать на нем NET_RX_SOFTIRQ. Через какое-то время, другой процессор начнет обработку этого пакета с функции __netif_receive_skb(), которая вызывает __netif_receive_skb_core(). Помните переменную netdev_tstamp_prequeue? В случае с RPS она позволяет выбрать когда снимать временную метку: до отправки пакета в чужую очередь или уже после извлечения его оттуда. RPS настраивается через /sys/class/net/<interface>/queues/. Подробнее см. Документацию на redhat.com. Прежде всего эта функция снимает временную метку, если она не была снята в netif_receive_skb_internal(): Теперь мы имеем skb, который содержит: Остается доставить его всем желающим получателям. В зависимости от требуемого L3 протокола и устройства получатели могут регистрироваться в нескольких местах: Частые пользователи всех 4-х списков — AF_PACKET сокеты, каждый из них при системном вызове bind регистрирует в соответствующем списке обработчик. (Обработчика зовут packet_rcv) На самом деле есть еще куча получателей, но мы ограничимся только теми, которые доставляют пакет сокетам в пространстве пользователя. UDP или TCP пакет будет принят функцией ip_recv. Через эту функцию пакеты попадают в обработку стеком протоколов TCP/IP. Обработка включает в себя проверки контрольных сумм, прохождение через таблицы iptables, поиск сокета-получателя по ip адресу и номеру порта, удаление заловков L2,L3,L4(которые не должны попасть в userspace). Когда стало ясно, какой сокет должен получить этот skb, skb помещается в очередь приема сокета. Когда пользователь вызовет recvmsg на этом сокете, в указанный им буфер в userspace будут скопированы данные пакета(те, что идут после tcp/udp заголовка), а в контрольном сообщении (см. man 3 cmsg и man 2 recvmsg) будут лежать обе временных метки в формате struct timespec. (См  Documentation/networking/timestamping.txt.) Временные метки кладутся в контрольное сообщение функцией __sock_recv_timestamp. (см. net/socket.c). Важное различие: ip_recv зарегистрирована как обработчик всего один раз в ptype_base сколько бы AF_INET сокетов вы не создали, а packet_rcv регистрируется для каждого AF_PACKET сокета по разу в каком-нибудь из списков. Мы видели, что для принимаемых пакетов дважды снимаются временные метки: сетевой картой(Thard), ядром сразу при получении (Tsoft). Подводя итог, посмотрим чем вызываются задержки между ними и моментом доставки пакета пользователю(Tuser): Чтобы примерно оценить, насколько велики эти задержки и насколько они предсказуемы, воспользуемся программой rxtest. Эта программа: Проверка проводилась на Core i7 с Linux 4.0 с сетевой картой Intel 82599ES под управлением драйвера ixgbe. В моем случае сетевая карта имеет свои аппаратные часы и снимает временные метки по ним. И нет никакой гарантии, что эти часы как-то синхронизированы с jiffies ядра. Для того, чтобы это исправить, запустим программу phc2sys из linuxptp: Её нужно держать открытой на протяжении всей проверки. Она будет заниматься подстройкой часов сетевой карты под системное время и выводить текущее расхождение часов. В моем случае абсолютное значение расхождения не превышало 10нс. Кроме установки настроек SO_TIMESTAMPING на сокете, нам нужно попросить сетевую карту запоминать временные метки. Воспользуемся для этого утилитой hwstamp_ctl из того же linuxptp. Это приведет к тому, что сетевая карта будет снимать временные метки для всех пакетов типа Sync протокола PTP. Почему именно Sync PTP? Потому что наша сетевая карта не умеет снимать временные метки для всех пакетов. Ей обязательно нужно указать какой-нибудь тип пакетов протокола PTP. (Это свзязано с тем, что поддержка аппаратных временных меток в Linux была введена для работы протокола PTP, позволяющего синхронизировать время с точностью до наносекунд.) Стартуем rxtest: Тем временем шлем с другого конца кабеля по 10 PTP Sync пакетов в секунду. (Я брал примеры PTP пакетов с https://wiki.wireshark.org/Protocols/ptp и слал их с помощью tcpreplay.) Результат выполнения rxtest: При этом тестируемому сетевому интерфейсу не был присвоен ip-адрес и наши пакеты не попадали на обработку протоколом IP. Этим можно объяснить совсем небольшую задержку Tuser — Tsoft. Адекватного исследования задержек и их зависимости от разных параметров (netdev_budget, частота принимаемых пакетов, нагрузка на процессор, конфигурация ядра, количество и тип открытых сокетов) хватило бы на целую статью. Цель этой статьи — полить воду рассмотреть механизм доставки пакетов и то, чем вызываются задержки. На этом все. Буду рад любым отзывам и критике. http://www.linuxfoundation.org/collaborate/workgroups/networking Информационная Безопасность