- PVSM.RU - https://www.pvsm.ru -
Бывает, что приложению требуется узнать точное время приема или отправки сетевого пакета. Например, для синхронизации часов (см. PTP [1], NTP [2]) или тестирования задержек в сети (см. RFC2544).
Наивным решением будет запоминать в приложении время сразу после получения пакета от ядра (или перед отправкой ядру):
recv(sock, buffer, length, flags);
clock_gettime(CLOCK_REALTIME, timespec);
Ясно, что полученное таким образом время может заметно отличаться от момента, когда пакет был получен Сетевым стройством. Для получения более точного времени нужна поддержка от опереционной системы, драйвера и/или Сетевого Устройства.
Начиная с версии 2.6.30 Линукс поддерживает опцию сокета SO_TIMESTAMPING. Она позволяет пользовательскому сокету получать временные метки для отправляемых и принимаемых пакетов. Временные метки могут быть сняты самим ядром, драйвером или сетевым устройством (см. список поддерживающих устройств и драйверов [3]). О том, что это вообще такое и как этим пользоваться, стоит почитать в Documentation/networking/timestamping.txt [4]
В этой статье я расскажу о том, как пакеты доставляются от сетевого устройства пользователю, когда при этом снимаются временные метки, как они доставляются пользователю и насколько они точны. Приведенные примеры кода ядра взяты из версии 4.1.
Все сетевые пакеты в ядре представляются структурой struct sk_buff
, которая объявлена в файле include/linux/skbuff.h [5]. Рассмотрим некоторые из ее полей:
struct sk_buff {
/* временная метка, обычно снятая программно */
ktime_t tstamp;
/* указатель на сокет, который отправил или получил этот пакет */
struct sock *sk;
/* указатель на устройство, с которого получен или которому будет отправлен пакет */
struct net_device *dev;
/* L3 протокол */
__be16 protocol;
/* смещения заголовков относительно head */
__u16 transport_header;
__u16 network_header;
__u16 mac_header;
/* sk_buff_data_t - может быть или указателем, или смещением относительно head
* tail - указатель на конец данных
* end - указатель на конец буфера выделенного под данные
*/
sk_buff_data_t tail;
sk_buff_data_t end;
/* head - указатель на начало буфера выделенного под данные
* data - указатель на начало данных. Т.к для разных
* протоколов данными могут называться разные вещи,
* этот указатель меняется при движении пакета по сетевому стеку.
* (Например: для IP данные начинаются с заголовка TCP/UDP,
* для Ethernet - с IP заголовка)
*/
unsigned char *head,
*data;
/* счетчик ссылок */
atomic_t users;
};
Экземпляры этой структуры я буду коротко называть skb.
Вместе с каждой struct sk_buff
выделяется буфер под заголовки с полезными данными(тот самый, в который указывают skb->head
, skb->tail
) и следующую сразу за ними структуру struct skb_shared_info
. (см. include/linux/skbuff.h [6]).
Причем несколько разных skb могут ссылаться на один буфер и соответствующую ему struct skb_shared_info
. Это бывает удобно при доставке одного skb нескольким пользователям, которым позволено менять поля struct sk_buff
, но не данные в буфере.
Самые интересные для нас поля struct skb_shared_info
:
struct skb_shared_hwtstamps {
ktime_t hwtstamp;
};
/* ... */
struct skb_shared_info {
/* флаги отправляемого пакета */
__u8 tx_flags;
/* и снова временная метка, обычно снятая аппаратно */
struct skb_shared_hwtstamps hwtstamps;
};
Для доступа к skb_shared_info
есть макрос skb_shinfo(skb)
, а для доступа к полю hwtstamps
— функция skb_hwtstamps(skb)
. См. include/linux/skbuff.h [7]
Мы не станем подробно рассматривать эти структуры. Сейчас достаточно понять, что первая позволяет ядру общаться с устройством, а вторая — с пользовательским сокетом. Для принятого пакета: skb->dev
указывает на устройство, которым он был получен; skb->sk
на сокет, которому будет доставлен. Для отправляемого — наоборот.
Когда процессор получает прерывание, он вызывает соответствующий ему обработчик. Выполнение обработчика происходит в контексте прерывания — для обслуживающего прерывание процессора почти все прерывания выключены. То есть обработчик прерывания не будет прерван, пока не завершится сам. Чем меньше процессор находится в контексте прерывания, тем скорее он сможет обслужить новые прерывания и отреагировать на события от других устройств.
Несмотря на то, что обработчику прерывания может требоваться много процессорного времени, обычно большая часть его работы может подождать. Именно поэтому действия обработчика прерывания принято разделять на верхнюю(Top Half) и нижнюю(Bottom Half) половины. Top Half в контексте прерывания выполняет срочные действия и планирует для выполнения Bottom Half. Bottom Half будет запущена ядром позже вне контекста прерывания и может быть прервана во время работы другими прерываниями.
SOFTIRQ — Механизм ядра, позволяющий запланировать отложенный вызов функции. Часто используется для реализации Bottom Half. Всего в ядре v4.1 десять разных SOFTIRQ (См. список [10]), обработчики для которых определяются при компиляции ядра. Во время своего выполнения обработчик может быть прерван только аппаратным прерыванием. Для каждого процессора своя маска запланированных SOFTIRQ, т.е обработчик будет вызван на том же процессоре, с которого был запланирован. (На самом деле, можно ухитриться и указать на каком конкретно процессоре запланировать SOFTIRQ.) Одно и то же SOFTIRQ может быть запланировано и
выполнено независимо на двух разных процессорах. При отправке и получении пакетов используются два из них: NET_RX_SOFTIRQ с обработчиком net_rx_action и NET_TX_SOFTIRQ с обработчиком net_tx_action. (см. net_rx_action [11] и net_tx_action [12])
Для выполнения запланированных SOFTIRQ служит функция do_softirq()
(см kernel/softirq.c [13]). Она поочередно вызывает обработчики SOFTIRQ, начиная с самых приоритетных(меньший номер — более высокий приоритет). Она вызывается после каждого обработчика аппаратного прерывания. Кроме того, на каждом процессоре крутится процесс ядра ksoftirqd, который периодически(как часто — зависит от нагрузки на процессор) вызывает do_softirq()
.
Для общения с Сетевыми Устройствами Линукс использует смесь прерываний и поллинга(polling). (См. NAPI [14])
Вот как это выглядит:
Заметим, что первая операция — добавление элемента в двухсвязный список, и вторая — установка бита в маске SOFTIRQ очень быстрые. Т.к ядро уже в курсе, что у Устройства есть пакеты, скорее всего драйвер захочет на время выключить на нем прерывания.
poll_list
* — список, создаваемый ядром для каждого ядра процессора(как одно из полей struct softnet_data
). Он хранит устройства, с которых NET_RX_SOFTIRQ предстоит получить пакеты.
Произошел вызов do_softirq()
. После выполнения более приоритетных SOFTIRQ, будет вызван обработчик NET_RX_SOFTIRQ — net_rx_action()
.
Эта функция проходит по списку poll_list и для каждого устройства dev, пока на нем есть пакеты, вызывает виртуальную функцию драйвера napi->poll (См. include/linux/netdevice.h [16].), которая:
Стоит отметить, что net_rx_action()
позволяет обрабатывать не более netdev_budget
(экспортируется в /proc/sys/net/core/netdev_budget
) пакетов за раз и ограничивает время своего выполнения 2/HZ секундами(на x86 по умолчанию HZ = 1000, т.е ограничение времени = 2мс).
netif_receive_skb()
— это функция, начиная с которой пакет попадает из драйвера в ядро. (На самом деле, она просто служит оберткой для других функций, которые и выполняют всю работу.) Посмотрим, что же делает ядро с полученным skb:
#define net_timestamp_check(COND, SKB)
if (static_key_false(&netstamp_needed)) {
if ((COND) && !(SKB)->tstamp.tv64)
__net_timestamp(SKB);
}
static int netif_receive_skb_internal(struct sk_buff *skb)
{
net_timestamp_check(netdev_tstamp_prequeue, skb);
/* ... */
return __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 [18].
Прежде всего эта функция снимает временную метку, если она не была снята в netif_receive_skb_internal()
:
net_timestamp_check(!netdev_tstamp_prequeue, skb);
Теперь мы имеем skb, который содержит:
Остается доставить его всем желающим получателям.
В зависимости от требуемого L3 протокола и устройства получатели могут регистрироваться в нескольких местах:
Частые пользователи всех 4-х списков — AF_PACKET сокеты, каждый из них при системном вызове bind
регистрирует в соответствующем списке обработчик. (Обработчика зовут packet_rcv
[19]) На самом деле есть еще куча получателей, но мы ограничимся только теми, которые доставляют пакет сокетам в пространстве пользователя.
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 [4].)
Временные метки кладутся в контрольное сообщение функцией __sock_recv_timestamp
. См. net/socket.c [20].
Важное различие: ip_recv
зарегистрирована как обработчик всего один раз в ptype_base сколько бы AF_INET сокетов вы не создали, а packet_rcv
регистрируется для каждого AF_PACKET сокета по разу в каком-нибудь из списков.
Мы видели, что для принимаемых пакетов дважды снимаются временные метки: сетевой картой(Thard), ядром сразу при получении (Tsoft). Подводя итог, посмотрим чем вызываются задержки между ними и моментом доставки пакета пользователю(Tuser):
Если за один вызов NET_RX_SOFTIRQ не удалось обработать наш пакет(был превышен netdev_budget или ограничение по времени 2/HZ), то ядро позволит выполняться другим процессам, пока аппаратное прерывание или ksoftirqd снова не вызовут `do_softirq()`. Также не стоит забывать о том, что есть и другие SOFTIRQ, на выполнение которых тоже уходит время.
Даже при выполнении NET_RX_SOFTIRQ некоторые пакеты будут обработаны раньше нашего.
В зависимости от того, что это за сокеты, доставка может занимать разное время.
Чтобы примерно оценить, насколько велики эти задержки и насколько они предсказуемы, воспользуемся программой rxtest [21]. Эта программа:
Проверка проводилась на Core i7 с Linux 4.0 с сетевой картой Intel 82599ES [22] под управлением драйвера ixgbe [23].
В моем случае сетевая карта имеет свои аппаратные часы и снимает временные метки по ним. И нет никакой гарантии, что эти часы как-то синхронизированы с jiffies ядра. Для того, чтобы это исправить, запустим программу phc2sys
из linuxptp [3]:
# тестируемый сетевой интерфейс называется eth5
phc2sys -s CLOCK_REALTIME -c eth5 -m
Её нужно держать открытой на протяжении всей проверки. Она будет заниматься подстройкой часов сетевой карты под системное время и выводить текущее расхождение часов. В моем случае абсолютное значение расхождения не превышало 10нс.
Кроме установки настроек SO_TIMESTAMPING на сокете, нам нужно попросить сетевую карту запоминать временные метки. Воспользуемся для этого утилитой hwstamp_ctl
из того же linuxptp
.
hwstamp_ctl -i eth5 -r 13
Это приведет к тому, что сетевая карта будет снимать временные метки для всех пакетов типа Sync протокола PTP. Почему именно Sync PTP? Потому что наша сетевая карта не умеет снимать временные метки для всех пакетов. Ей обязательно нужно указать какой-нибудь тип пакетов протокола PTP. (Это свзязано с тем, что поддержка аппаратных временных меток в Linux была введена для работы протокола PTP, позволяющего синхронизировать время с точностью до наносекунд.)
Стартуем rxtest:
rxtest packet eth5 1000
Тем временем шлем с другого конца кабеля по 10 PTP Sync пакетов в секунду. (Я брал примеры PTP пакетов с https://wiki.wireshark.org/Protocols/ptp [24] и слал их с помощью tcpreplay.)
Результат выполнения rxtest:
hard->soft delay: packets 1000: 18.1603 +- 1.18737 microseconds
soft->user delay: packets 1000: 5.54756 +- 1.88607 microseconds
При этом тестируемому сетевому интерфейсу не был присвоен ip-адрес и наши пакеты не попадали на обработку протоколом IP. Этим можно объяснить совсем небольшую задержку Tuser — Tsoft.
Адекватного исследования задержек и их зависимости от разных параметров (netdev_budget, частота принимаемых пакетов, нагрузка на процессор, конфигурация ядра, количество и тип открытых сокетов) хватило бы на целую статью. Цель этой статьи — полить воду рассмотреть механизм доставки пакетов и то, чем вызываются задержки.
На этом все. Буду рад любым отзывам и критике.
Автор: ph14nix
Источник [29]
Сайт-источник PVSM.RU: https://www.pvsm.ru
Путь до страницы источника: https://www.pvsm.ru/sistemnoe-programmirovanie/150262
Ссылки в тексте:
[1] PTP: https://habrahabr.ru/post/163253/
[2] NTP: https://ru.wikipedia.org/wiki/NTP
[3] список поддерживающих устройств и драйверов: http://linuxptp.sourceforge.net/
[4] Documentation/networking/timestamping.txt: https://github.com/torvalds/linux/blob/master/Documentation/networking/timestamping.txt
[5] include/linux/skbuff.h: http://lxr.free-electrons.com/source/include/linux/skbuff.h?v=4.1#L519
[6] include/linux/skbuff.h: http://lxr.free-electrons.com/source/include/linux/skbuff.h?v=4.1#L315
[7] include/linux/skbuff.h: http://lxr.free-electrons.com/source/include/linux/skbuff.h?v=4.1#L986
[8] include/linux/netdevice.h: http://lxr.free-electrons.com/source/include/linux/netdevice.h?v=4.1#L1505
[9] include/net/sock.h: http://lxr.free-electrons.com/source/include/net/sock.h?v=4.1#L301
[10] список: http://lxr.free-electrons.com/source/include/linux/interrupt.h?v=4.1#L406
[11] net_rx_action: http://lxr.free-electrons.com/source/net/core/dev.c?v=4.1#L4674
[12] net_tx_action: http://lxr.free-electrons.com/source/net/core/dev.c?v=4.1#L3446
[13] kernel/softirq.c: http://lxr.free-electrons.com/source/kernel/softirq.c?v=4.1#L304
[14] NAPI: http://www.linuxfoundation.org/collaborate/workgroups/networking/napi
[15] *: #poll_list
[16] include/linux/netdevice.h: http://lxr.free-electrons.com/source/include/linux/netdevice.h?v=4.1#L311
[17] Documentation/static-keys.txt: https://raw.githubusercontent.com/torvalds/linux/master/Documentation/static-keys.txt
[18] Документацию на redhat.com: https://access.redhat.com/documentation/en-US/Red_Hat_Enterprise_Linux/6/html/Performance_Tuning_Guide/network-rps.html
[19] packet_rcv
: http://lxr.free-electrons.com/source/net/packet/af_packet.c?v=4.1#L1766
[20] net/socket.c: http://lxr.free-electrons.com/source/net/socket.c?v=4.1#L638
[21] rxtest: https://github.com/gnull/tstest
[22] Intel 82599ES: http://www.intel.com/content/www/us/en/embedded/products/networking/82599-10-gbe-controller-brief.html
[23] ixgbe: http://lxr.free-electrons.com/source/drivers/net/ethernet/intel/ixgbe/?v=4.0
[24] https://wiki.wireshark.org/Protocols/ptp: https://wiki.wireshark.org/Protocols/ptp
[25] Christian Benvenuti. Understanding Linux Network Internals: http://shop.oreilly.com/product/9780596002558.do
[26] Jonathan Corbet, Alessandro Rubini, Greg Kroah-Hartman. Linux Device Drivers: http://free-electrons.com/doc/books/ldd3.pdf
[27] http://lxr.free-electrons.com/: http://lxr.free-electrons.com/
[28] http://www.linuxfoundation.org/collaborate/workgroups/networking: http://www.linuxfoundation.org/collaborate/workgroups/networking
[29] Источник: https://habrahabr.ru/post/304644/?utm_source=habrahabr&utm_medium=rss&utm_campaign=best
Нажмите здесь для печати.