- PVSM.RU - https://www.pvsm.ru -

В этой статье мы рассмотрим, как осуществляется приём пакетов на компьютерах под управлением ядра Linux, а также разберём вопросы мониторинга и настройки каждого компонента сетевого стека по мере движения пакетов из сети в приложения пользовательского пространства. Здесь вы найдёте много исходного кода, потому что без глубокого понимания процессов вы не сможете настроить и отслеживать сетевой стек Linux.
Также рекомендуем ознакомиться с иллюстрированным руководством на ту же тему [1], там есть поясняющие схемы и дополнительная информация.
Сетевой стек устроен сложно, и не существует универсального решения на все случаи жизни. Если для вас или вашего бизнеса критически важны производительность и корректность при работе с сетью, то вам придётся инвестировать немало времени, сил и средств в то, чтобы понять, как взаимодействуют друг с другом различные части системы.
В идеале, вам следует измерять потери пакетов на каждом уровне сетевого стека. В этом случае необходимо выбрать, какие компоненты нуждаются в настройке. Именно на этом моменте, как мне кажется, сдаются многие. Это предположение основано на том, что настройки sysctl или значения /proc можно использовать многократно и скопом. В ряде случаев, вероятно, система бывает настолько пронизана взаимосвязями и наполнена нюансами, что если вы пожелаете реализовать полезный мониторинг или выполнить настройку, то придётся разобраться с функционированием системы на низком уровне. В противном случае просто используйте настройки по умолчанию. Этого может быть достаточно до тех пор, пока не понадобится дальнейшая оптимизация (и вложения для отслеживания этих настроек).
Многие из приведённых в этой статье примеров настроек используются исключительно в качестве иллюстраций, и не являются рекомендацией «за» или «против» использования в качестве определённой конфигурации или настроек по умолчанию. Так что перед применением каждой настройки сначала подумайте, что вам нужно мониторить, чтобы выявить значимое изменение.
Опасно применять сетевые настройки, подключившись к машине удалённо. Можно легко заблокировать себе доступ или вообще уронить систему работы с сетью. Не применяйте настройки на рабочих машинах, сначала обкатайте их, по мере возможности, на новых, а затем применяйте в production.
Вы можете захотеть иметь под рукой копию спецификации (data sheet) устройства. В этой статье будет рассмотрен контроллер Intel I350, управляемый драйвером igb. Скачать спецификацию можно отсюда [16].
Высокоуровневый путь, по которому проходит пакет от прибытия до приёмного буфера сокета выглядит так:
Далее мы подробно рассмотрим весь этот поток. В качестве уровне протоколов будут рассмотрены уровни IP и UDP. Большая часть информации верна и для других уровней протоколов.
Мы будем рассматривать ядро Linux версии 3.13.0. Также по всей статье используются примеры кода и ссылки на GitHub.
Очень важно разобраться, как именно пакеты принимаются ядром. Нам придётся внимательно ознакомиться и понять работу сетевого драйвера, чтобы потом было легче вникнуть в описание работы сетевого стека.
В качестве сетевого драйвера будет рассмотрен igb. Он используется в довольно распространённой серверной сетевой карте, Intel I350. Так что давайте начнём с разбора работы этого драйвера.
Драйвер регистрирует функцию инициализации, вызванную ядром при загрузке драйвера. Регистрация выполняется с помощью макроса module_init.
Вы можете найти функцию инициализации igb (igb_init_module) и её регистрацию с помощью module_init в drivers/net/ethernet/intel/igb/igb_main.c [18]. Всё довольно просто:
/**
* igb_init_module – подпрограмма (routine) регистрации драйвера
*
* igb_init_module — это первая подпрограмма, вызываемая при загрузке драйвера.
* Она выполняет регистрацию с помощью подсистемы PCI.
**/
static int __init igb_init_module(void)
{
int ret;
pr_info("%s - version %sn", igb_driver_string, igb_driver_version);
pr_info("%sn", igb_copyright);
/* ... */
ret = pci_register_driver(&igb_driver);
return ret;
}
module_init(igb_init_module);
Как мы увидим дальше, основная часть работы по инициализации устройства происходит при вызове pci_register_driver.
Сетевая карта Intel I350 — это устройство с интерфейсом PCI express [19].
PCI-устройства идентифицируют себя с помощью серии регистров в конфигурационном пространстве PCI [20].
Когда драйвер устройства скомпилирован, то для экспорта таблицы идентификаторов PCI-устройств, которыми может управлять драйвер, используется макрос MODULE_DEVICE_TABLE (из include/module.h [21]). Ниже мы увидим, что таблица также регистрируется как часть структуры.
Эта таблица используется ядром для определения, какой нужно загрузить драйвер для управления устройством. Таким образом операционная система понимает, какое устройство подключено и какой драйвер позволяет с ним взаимодействовать.
Вы можете найти таблицу и идентификаторы PCI-устройств для драйвера igb, соответственно, здесь drivers/net/ethernet/intel/igb/igb_main.c [22] и здесь drivers/net/ethernet/intel/igb/e1000_hw.h [23]:
static DEFINE_PCI_DEVICE_TABLE(igb_pci_tbl) = {
{ PCI_VDEVICE(INTEL, E1000_DEV_ID_I354_BACKPLANE_1GBPS) },
{ PCI_VDEVICE(INTEL, E1000_DEV_ID_I354_SGMII) },
{ PCI_VDEVICE(INTEL, E1000_DEV_ID_I354_BACKPLANE_2_5GBPS) },
{ PCI_VDEVICE(INTEL, E1000_DEV_ID_I211_COPPER), board_82575 },
{ PCI_VDEVICE(INTEL, E1000_DEV_ID_I210_COPPER), board_82575 },
{ PCI_VDEVICE(INTEL, E1000_DEV_ID_I210_FIBER), board_82575 },
{ PCI_VDEVICE(INTEL, E1000_DEV_ID_I210_SERDES), board_82575 },
{ PCI_VDEVICE(INTEL, E1000_DEV_ID_I210_SGMII), board_82575 },
{ PCI_VDEVICE(INTEL, E1000_DEV_ID_I210_COPPER_FLASHLESS), board_82575 },
{ PCI_VDEVICE(INTEL, E1000_DEV_ID_I210_SERDES_FLASHLESS), board_82575 },
/* ... */
};
MODULE_DEVICE_TABLE(pci, igb_pci_tbl);
Как мы видели выше, pci_register_driver вызывается драйверной функцией инициализации.
Эта функция регистрирует структуру указателей. Большинство из них являются указателями функций, но таблица идентификаторов PC-устройства тоже регистрируется. Ядро использует регистрируемые драйвером функции для запуска PCI-устройства.
Из drivers/net/ethernet/intel/igb/igb_main.c [24]:
static struct pci_driver igb_driver = {
.name = igb_driver_name,
.id_table = igb_pci_tbl,
.probe = igb_probe,
.remove = igb_remove,
/* ... */
};
Когда устройство опознано по его PCI ID, ядро может выбрать подходящий драйвер. Каждый драйвер регистрирует probe-функцию в PCI-системе ядра. Ядро вызывает эту функцию для тех устройств, на которые ещё не претендовали драйверы. Когда один из драйверов претендует на устройство, то другие уже не опрашиваются. Большинство драйверов содержат много кода, которые выполняется для подготовки устройства к использованию. Выполняемые процедуры сильно варьируются в зависимости от драйвера.
Вот некоторые типичные процедуры:
Давайте пробежимся по некоторым из этих процедур применительно к драйверу igb и функции igb_probe [27].
Приведённый ниже код из функции igb_probe выполняет базовое конфигурирование PCI. Взято из drivers/net/ethernet/intel/igb/igb_main.c [28]:
err = pci_enable_device_mem(pdev);
/* ... */
err = dma_set_mask_and_coherent(&pdev->dev, DMA_BIT_MASK(64));
/* ... */
err = pci_request_selected_regions(pdev, pci_select_bars(pdev,
IORESOURCE_MEM),
igb_driver_name);
pci_enable_pcie_error_reporting(pdev);
pci_set_master(pdev);
pci_save_state(pdev);
Сначала устройство инициализируется с помощью pci_enable_device_mem. Если устройство находится в спящем режиме, то оно пробуждается, активируются источники памяти и так далее.
Затем настраивается маска DMA. Наше устройство может читать и писать в адреса 64-битной памяти, поэтому с помощью DMA_BIT_MASK(64) вызывается dma_set_mask_and_coherent.
С помощью вызова pci_request_selected_regions резервируются области памяти. Запускается служба расширенной регистрации ошибок (PCI Express Advanced Error Reporting [29]), если загружен её драйвер. С помощью вызова pci_set_master активируется DMA, а конфигурационное пространство PCI сохраняется с помощью вызова pci_save_state.
Фух.
Полный разбор работы PCI-устройства выходит за рамки этой статьи, но вы можете почитать эти материалы:
Функция igb_probe выполняет важную работу по инициализации сетевого устройства. В дополнение к процедурам, характерным для PCI, она выполняет и более общие операции для работы с сетью и функционирования сетевого устройства:
Всё это нам понадобится позднее, так что давайте кратко пробежимся.
struct net_device_ops содержит указатели функций на многие важные операции, необходимые сетевой подсистеме для управления устройством. Эту структуру мы ещё не раз упомянем в статье.
Структура net_device_ops прикреплена к struct net_device в igb_probe. Взято из drivers/net/ethernet/intel/igb/igb_main.c [33]:
static int igb_probe(struct pci_dev *pdev, const struct pci_device_id *ent)
{
/* ... */
netdev->netdev_ops = &igb_netdev_ops;
В том же файле настраиваются указатели функций, хранящиеся в структуре net_device_ops. Взято из drivers/net/ethernet/intel/igb/igb_main.c [34]:
static const struct net_device_ops igb_netdev_ops = {
.ndo_open = igb_open,
.ndo_stop = igb_close,
.ndo_start_xmit = igb_xmit_frame,
.ndo_get_stats64 = igb_get_stats64,
.ndo_set_rx_mode = igb_set_rx_mode,
.ndo_set_mac_address = igb_set_mac,
.ndo_change_mtu = igb_change_mtu,
.ndo_do_ioctl = igb_ioctl,
/* ... */
Как видите, в struct есть несколько интересных полей, например, ndo_open, ndo_stop, ndo_start_xmit и ndo_get_stats64, которые содержат адреса функций, реализованных драйвером igb. Некоторые из них мы далее рассмотрим подробнее.
ethtool [35] — это программа, управляемая из командной строки. С её помощью вы можете получать и настраивать различные драйверы и опции оборудования. Под Ubuntu эту программу можно установить так: apt-get install ethtool.
Обычно ethtool применяется для сбора с сетевых устройств детальной статистики. Другие способы применения будут описаны ниже.
Программа общается с драйверами с помощью системного вызова ioctl [36]. Драйвер устройства регистрирует серию функций, выполняемых для операций ethtool, а ядро обеспечивает glue.
Когда ethtool вызывает ioctl, ядро находит структуру ethtool, зарегистрированную соответствующим драйвером, и выполняет зарегистрированные функции. Реализация драйверной функции ethtool может делать что угодно — от изменения простого программного флага в драйвере до регулирования работы сетевой карты путём записи в память устройства значений из реестра.
Драйвер igb с помощью вызова igb_set_ethtool_ops регистрирует в igb_probe операции ethtool:
static int igb_probe(struct pci_dev *pdev, const struct pci_device_id *ent)
{
/* ... */
igb_set_ethtool_ops(netdev);
Весь ethtool-код драйвера igb вместе с функцией igb_set_ethtool_ops можно найти в файле drivers/net/ethernet/intel/igb/igb_ethtool.c [37].
Взято из drivers/net/ethernet/intel/igb/igb_ethtool.c [38]:
void igb_set_ethtool_ops(struct net_device *netdev)
{
SET_ETHTOOL_OPS(netdev, &igb_ethtool_ops);
}
Помимо этого, вы можете найти структуру igb_ethtool_ops с поддерживаемыми драйвером igb функциями ethtool, настроенными в соответствующих полях.
Взято из drivers/net/ethernet/intel/igb/igb_ethtool.c [39]:
static const struct ethtool_ops igb_ethtool_ops = {
.get_settings = igb_get_settings,
.set_settings = igb_set_settings,
.get_drvinfo = igb_get_drvinfo,
.get_regs_len = igb_get_regs_len,
.get_regs = igb_get_regs,
/* ... */
Каждый драйвер по своему усмотрению решает, какие функции ethtool релевантные и какие нужно реализовать. К сожалению, не все драйверы реализуют все функции ethtool.
Довольна интересна функция get_ethtool_stats, которая (если она реализована) создаёт подробные статистические счётчики, которые отслеживаются либо программно драйвером, либо самим устройством.
В посвящённой мониторингу части мы рассмотрим, как использовать ethtool для получения этой статистики.
Когда фрейм данных с помощью DMA записывается в память, как сетевая карта сообщает системе о том, что данные готовы к обработке?
Обычно карта генерирует прерывание [40], сигнализирующее о прибытии данных. Есть три распространённых типа прерываний: MSI-X, MSI и легаси-IRQ. Вскоре мы их рассмотрим. Генерируемое при записи данных в память прерывание достаточно простое, но если приходит много фреймов, то генерируется и большое количество IRQ. Чем больше прерываний, тем меньше времени работы CPU доступно для обслуживания более высокоуровневых задач, например, пользовательских процессов.
New Api (NAPI) [17] был создан в качестве механизма снижения количества прерываний, генерируемых сетевыми устройствами по мере прибытия пакетов. Но всё же NAPI не может совсем избавить нас от прерываний. Позднее мы узнаем, почему.
По ряду важных признаков NAPI [17] отличается от легаси-метода сбора данных. Он позволяет драйверу устройства регистрировать функцию poll, вызываемую подсистемой NAPI для сбора фрейма данных.
Алгоритм использования NAPI драйверами сетевых устройств выглядит так:
Этот метод сбора фреймов данных позволил уменьшить нагрузку по сравнению с легаси-методом, поскольку многие фреймы могут одновременно приниматься без необходимости одновременного генерирования IRQ для каждого из них.
Драйвер устройства реализует функцию poll и регистрирует её с помощью NAPI, вызывая netif_napi_add. При этом драйвер также задаёт weight. Большинство драйверов хардкодят значение 64. Почему именно его, мы увидим дальше.
Обычно драйверы регистрируют свои NAPI-функции poll в процессе инициализации драйвера.
Драйвер igb делает это с помощью длинной цепочки вызовов:
В результате выполняется ряд высокоуровневых операций:
Давайте взглянем на igb_alloc_q_vector чтобы понять, как регистрируется callback poll и её личные данные (private data).
Взято из drivers/net/ethernet/intel/igb/igb_main.c [42]:
static int igb_alloc_q_vector(struct igb_adapter *adapter,
int v_count, int v_idx,
int txr_count, int txr_idx,
int rxr_count, int rxr_idx)
{
/* ... */
/* размещает в памяти q_vector и кольца (rings) */
q_vector = kzalloc(size, GFP_KERNEL);
if (!q_vector)
return -ENOMEM;
/* инициализирует NAPI */
netif_napi_add(adapter->netdev, &q_vector->napi, igb_poll, 64);
/* ... */
Выше приведён код размещения в памяти очереди приёма и регистрации функции igb_poll с помощью подсистемы NAPI. Мы получаем ссылку на struct napi_struct, ассоциированную с этой новой созданной очередью приёма (&q_vector->napi). Когда придёт время сбора пакетов из очереди и подсистемой NAPI будет вызвана igb_poll, ей передадут эту ссылку.
Важность описанного алгоритма мы поймём, когда изучим поток данных из драйвера в сетевой стек.
Помните структуру net_device_ops, которая регистрировала набор функций для загрузки сетевого устройства, передачи пакетов, настройки MAC-адреса и так далее?
Когда сетевое устройство загружено (например, с помощью ifconfig eth0 up), вызывается функция, прикреплённая к полю ndo_open структуры net_device_ops.
Функция ndo_open обычно делает следующее:
В случае с драйвером igb, igb_open вызывает функцию, прикреплённая к полю ndo_open структуры net_device_ops.
Большинство современных сетевых карт используют DMA для записи данных напрямую в память, откуда операционная система может их извлечь для последующей обработки. Чаще всего используемая для этого структура похожа на очередь, созданную на базе кольцевого буфера.
Сначала драйвер устройства должен совместно с ОС зарезервировать в памяти область, которая будет использоваться сетевой картой. Далее карта информируется о выделении памяти, куда позднее будут записываться входящие данные, которые можно брать и обрабатывать с помощью сетевой подсистемы.
Выглядит просто, но что если частота пакетов так высока, что один CPU не успевает их обрабатывать? Структура данных базируется на области памяти фиксированного размера, поэтому пакеты будут отбрасываться.
В этом случае может помочь механизм Receive Side Scaling (RSS) [43], система с несколькими очередями.
Некоторые устройства могут одновременно писать входящие пакеты в несколько разных областей памяти. Каждая область обслуживает отдельную очередь. Это позволяет ОС использовать несколько CPU для параллельной обработки входящих данных на аппаратном уровне. Но такое умеют делать не все сетевые карты.
Intel I350 — умеет. Свидетельства этого умения мы видим в драйвере igb. Одной из первых вещей, выполняемых им после загрузки, является вызов функции igb_setup_all_rx_resources [44]. Эта функция вызывает однократно для каждой очереди приёма другую функцию — igb_setup_rx_resources, упорядочивающая DMA-память, в которую сетевая карта будет писать входящие данные.
Если вас интересуют подробности, почитайте github.com/torvalds/linux/blob/v3.13/Documentation/DMA-API-HOWTO.txt [45].
С помощью ethtool можно настраивать количество и размер очередей приёма. Изменение этих параметров позволяет существенно повлиять на отношение обработанных и отброшенных фреймов.
Чтобы определить, в какую очередь нужно отправить данные, сетевая карта использует хэш-функцию в полях заголовка (источник, пункт назначения, порт и так далее).
Некоторые сетевые карты позволяют настраивать вес очередей приёма, так что вы можете направлять больше трафика в конкретные очереди.
Реже встречается возможность настройки самой хэш-функции. Если вы можете её настраивать, то можете направлять конкретный поток в конкретную очередь, или даже отбрасывать пакеты на аппаратном уровне.
Ниже мы рассмотрим, как настраивается хэш-функция.
Когда сетевое устройство загружено, драйвер обычно включает NAPI. Мы уже видели, как драйверы с помощью NAPI регистрируют функции poll. Обычно NAPI не включается, пока устройство не загружено.
Включить его довольно просто. Вызов napi_enable сигнализирует struct napi_struct, что NAPI включена. Как отмечалось выше, после включения NAPI находится в неактивном состоянии.
В случае с драйвером igb, NAPI включается для каждого q_vector, инициализируемого после загрузки драйвера, или когда счётчик или размер очереди изменяется с помощью ethtool.
Взято из drivers/net/ethernet/intel/igb/igb_main.c [46]:
for (i = 0; i < adapter->num_q_vectors; i++)
napi_enable(&(adapter->q_vector[i]->napi));
После включения NAPI нужно зарегистрировать обработчика прерываний. Устройство может генерировать прерывания разными способами: MSI-X, MSI и легаси-прерывания. Поэтому код может быть различным, в зависимости от поддерживаемых способов.
Драйвер должен определить, какой способ поддерживается данным устройством, и зарегистрировать соответствующую функцию-обработчика, которая выполняется при получении прерывания.
Некоторые драйверы, в том числе и igb, пытаются зарегистрировать обработчика для каждого способа, в случае неудачи переходя к следующему неопробованному.
Предпочтительнее использовать прерывания MSI-X, особенно для сетевых карт, поддерживающих несколько очередей приёма. Причина в том, что каждой очереди присвоено собственное аппаратное прерывание, которая может быть обработано конкретным CPU (с помощью irqbalance или модифицирования /proc/irq/IRQ_NUMBER/smp_affinity). Как мы скоро увидим, прерывание и пакет обрабатывает один и тот же CPU. Таким образом, входящие пакеты будут обрабатываться разными CPU в рамках всего сетевого стека, начиная с уровня аппаратных прерываний.
Если MSI-X недоступна, то драйвер использует MSI (если поддерживается), которая всё ещё имеет преимущества по сравнению с легаси-прерываниями. Подробнее об этом читайте в английской Википедии [47].
В драйвере igb в качестве обработчиков прерываний MSI-X, MSI и легаси выступают соответственно функции igb_msix_ring, igb_intr_msi, igb_intr.
Код драйвера, пробующий каждый способ, можно найти в drivers/net/ethernet/intel/igb/igb_main.c [48]:
static int igb_request_irq(struct igb_adapter *adapter)
{
struct net_device *netdev = adapter->netdev;
struct pci_dev *pdev = adapter->pdev;
int err = 0;
if (adapter->msix_entries) {
err = igb_request_msix(adapter);
if (!err)
goto request_done;
/* переход к MSI */
/* ... */
}
/* ... */
if (adapter->flags & IGB_FLAG_HAS_MSI) {
err = request_irq(pdev->irq, igb_intr_msi, 0,
netdev->name, adapter);
if (!err)
goto request_done;
/* переход к легаси */
/* ... */
}
err = request_irq(pdev->irq, igb_intr, IRQF_SHARED,
netdev->name, adapter);
if (err)
dev_err(&pdev->dev, "Error %d getting interruptn", err);
request_done:
return err;
}
Как видите, драйвер сначала пытается использовать обработчика igb_request_msix для прерываний MSI-X, если не получается, то переходит к MSI. Для регистрации MSI-обработчика igb_intr_msi используется request_irq. Если и это не срабатывает, драйвер переходит к легаси-прерываниям. Для регистрации igb_intr снова используется request_irq.
Таким образом драйвер igb регистрирует функцию, которая будет выполняться при генерировании сетевой картой прерывания, сигнализирующего о получении данных и их готовности к обработке.
К данному моменту почти всё уже настроено. Осталось только включить прерывания и ожидать прихода данных. Процедура включения зависит от конкретного оборудования, то драйвер igb делает это в __igb_open, с помощью вызова вспомогательной функции igb_irq_enable.
С помощью записи регистров включаются прерывания для данного устройства:
static void igb_irq_enable(struct igb_adapter *adapter)
{
/* ... */
wr32(E1000_IMS, IMS_ENABLE_MASK | E1000_IMS_DRSTA);
wr32(E1000_IAM, IMS_ENABLE_MASK | E1000_IMS_DRSTA);
/* ... */
}
Теперь сетевое устройство загружено. Драйверы могут выполнять и другие процедуры, например, запускать таймеры, рабочие очереди или другие операции, в зависимости от конкретного устройства. По их завершении сетевая карта загружена и готова к использованию.
Давайте теперь рассмотрим вопросы мониторинга и настройки опций драйверов сетевых устройств.
Есть несколько разных способов мониторинга, отличающихся подробностью статистики и сложностью. Начнём с самого детального.
Установить ethtool под Ubuntu можно так: sudo apt-get install ethtool.
Теперь можно просмотреть статистику, передав флаг -S с именем сетевого устройства, чья статистка вас интересует.
Мониторьте подробную статистику (например, отбрасывание пакетов) с помощью `ethtool -S`.
$ sudo ethtool -S eth0
NIC statistics:
rx_packets: 597028087
tx_packets: 5924278060
rx_bytes: 112643393747
tx_bytes: 990080156714
rx_broadcast: 96
tx_broadcast: 116
rx_multicast: 20294528
....
Мониторинг может быть трудной задачей. Данные легко получить, но не существует стандартного представления значений полей. Разные драйверы, даже разные версии одного драйвера могут генерировать разные имена полей, имеющие одинаковое значение.
Ищите значения, в названиях которых есть “drop”, “buffer”, “miss” и так далее. Дальше нужно считать данные из источника. Вы сможете определить, какие значения относятся только к ПО (например, инкрементируются при отсутствии памяти), а какие поступают напрямую из оборудования посредством чтения регистров. В случае со значениями регистров, сверьтесь со спецификацией сетевой карты чтобы узнать, что означают конкретные счётчики. Многие названия, присваиваемые ethtool, могут быть ошибочны.
sysfs тоже предоставляют много статистики, но она чуть более высокоуровневая чем та, что предоставляется напрямую сетевой картой.
Вы сможете узнать количество отброшенных входящих фреймов для, например, eth0, применив cat к файлу.
Мониторинг более высокоуровневой статистки сетевой карты с помощью sysfs:
$ cat /sys/class/net/eth0/statistics/rx_dropped
2
Значения счётчиков будут раскиданы по файлам: collisions, rx_dropped, rx_errors, rx_missed_errors и так далее.
К сожалению, это драйвер решает, что означают те или иные поля, какие из них нужно инкрементировать и откуда брать значения. Вы могли заметить, что одни драйверы расценивают определённый тип ошибочной ситуации как отбрасывание пакета, а другие — как отсутствие.
Если для вас важны эти значения, то изучите исходный код драйвера, чтобы точно выяснить, что он думает про каждое из значений.
Ещё более высокоуровневый файл /proc/net/dev, предоставляющий выжимку по каждому сетевому адаптеру в системе.
Читаем /proc/net/dev, чтобы мониторить высокоуровневую статистику сетевых карт:
$ cat /proc/net/dev
Inter-| Receive | Transmit
face |bytes packets errs drop fifo frame compressed multicast|bytes packets errs drop fifo colls carrier compressed
eth0: 110346752214 597737500 0 2 0 0 0 20963860 990024805984 6066582604 0 0 0 0 0 0
lo: 428349463836 1579868535 0 0 0 0 0 0 428349463836 1579868535 0 0 0 0 0 0
Этот файл содержит набор некоторых значений, которые вы можете найти в упомянутых файлах файлах sysfs. Его можно использоваться как общий источник информации.
Как и в предыдущем случае, если эти значения важны для вас, то обратитесь к исходникам драйвера. Только в этом случае вы сможете точно выяснить, когда, где и почему инкрементируются эти значения, то есть что здесь считается ошибкой, отбрасыванием или FIFO.
Если загруженные в вашей системе сетевая карта и драйвер устройства поддерживают RSS (множественные очереди), то обычно с помощью ethtool можно настраивать количество очередей приёма (каналов приёма, RX-channels).
Проверка количества очередей приёма сетевой карты:
$ sudo ethtool -l eth0
Channel parameters for eth0:
Pre-set maximums:
RX: 0
TX: 0
Other: 0
Combined: 8
Current hardware settings:
RX: 0
TX: 0
Other: 0
Combined: 4
Выходные данные отражают предварительно настроенные максимумы (налагаемые драйвером или оборудованием) и текущие настройки.
Примечание: не все драйверы устройств поддерживают эту операцию.
Ошибка, возникающая, если ваша сетевая карта не поддерживает операцию:
$ sudo ethtool -l eth0
Channel parameters for eth0:
Cannot get device channel parameters
: Operation not supported
Это означает, что драйвер не реализовало операцию ethtool get_channels. Причина может быть в отсутствии поддержки настройки количества очередей со стороны сетевой карты, в отсутствии поддержки RSS, или у вас слишком старая версия драйвера.
После того, как вы нашли счётчики текущего и максимального количества очередей, вы можете настроить их значения с помощью sudo ethtool -L.
Примечание: некоторые устройства и их драйверы поддерживают только комбинированные очереди, — на приём и передачу — как в примере в предыдущей главе.
С помощью ethtool -L назначим 8 комбинированных очередей:
$ sudo ethtool -L eth0 combined 8
Если ваше устройство и драйвер позволяют отдельно настраивать количество очередей приёма и передачи, то можно отдельно задать 8 очередей приёма:
$ sudo ethtool -L eth0 rx 8
Примечание: у большинства драйверов такие изменения приведут к падению и перезагрузке интерфейса, потому что подключения к нему будут прерваны. Хотя при однократном изменении это не слишком важно.
Некоторые сетевые карты и их драйверы поддерживают настройку размера очереди приёма. Конкретная реализация зависит от оборудования, но, к счастью, ethtool обеспечивает стандартный метод настройки. Увеличение размера позволяет предотвратить отбрасывание сетевых данных при большом количестве входящих фреймов. Правда, данные ещё могут быть отброшены на уровне ПО, так что для полного исключения или снижения отбрасывания необходимо будет провести дополнительную настройку.
Проверка текущего размера очереди сетевой карты с помощью ethtool –g:
$ sudo ethtool -g eth0
Ring parameters for eth0:
Pre-set maximums:
RX: 4096
RX Mini: 0
RX Jumbo: 0
TX: 4096
Current hardware settings:
RX: 512
RX Mini: 0
RX Jumbo: 0
TX: 512
Выходные данные показывают, что оборудование поддерживает 4096 дескрипторов приёма и передачи, но в данный момент используется 512.
Увеличим размер каждой очереди до 4096:
$ sudo ethtool -G eth0 rx 4096
Примечание: у большинства драйверов такие изменения приведут к падению и перезагрузке интерфейса, потому что подключения к нему будут прерваны. Хотя при однократном изменении это не слишком важно.
Некоторые сетевые карты позволяют настраивать распределение сетевых данных между очередями приёма путём изменения их весов.
Это можно сделать, если:
Проверка таблицы косвенной адресации потока приёма:
$ sudo ethtool -x eth0
RX flow hash indirection table for eth3 with 2 RX ring(s):
0: 0 1 0 1 0 1 0 1
8: 0 1 0 1 0 1 0 1
16: 0 1 0 1 0 1 0 1
24: 0 1 0 1 0 1 0 1
Здесь слева отображены значения хэшей пакетов и очереди приёма — 0 и 1. Пакет с хэшем 2 будет адресован в очередь 0, а пакет с хэшем 3 — в очередь 1.
Пример: равномерно распределим обработку между первыми двумя очередями приёма:
$ sudo ethtool -X eth0 equal 2
Если вам нужно настроить кастомные веса, чтобы количество пакетов, адресуемых в конкретные очереди (а следовательно, и CPU), то вы можете сделать это в командной строке с помощью ethtool –X:
$ sudo ethtool -X eth0 weight 6 2
Здесь очереди 0 присваивается вес 6, а очереди 1 — вес 2. Таким образом, большая часть данных будет обрабатываться очередью 0.
Как мы сейчас увидим, некоторые сетевые карты также позволяют настраивать поля, которые используются в алгоритме хэширования.
С помощью ethtool можно настраивать поля, которые будут участвовать в вычислении хэшей, используемых в RSS.
C помощью ethtool -n проверим, какие поля используются для хэша потока приёма UPD:
$ sudo ethtool -n eth0 rx-flow-hash udp4
UDP over IPV4 flows use these fields for computing Hash flow key:
IP SA
IP DA
В случае с eth0, для вычисления хэша UDP-потока используется источник IPv4 и адреса назначения. Давайте добавим ещё входящий и исходящий порты:
$ sudo ethtool -N eth0 rx-flow-hash udp4 sdfn
Значение sdfn выглядит непонятно. Объяснение каждой буквы можно найти на странице автора ethtool man.
Настройка полей хэша довольно полезна, но фильтрование ntuple ещё полезнее и обеспечивает более тонкое управление распределением потоков по очередям приёма.
Некоторые сетевые карты поддерживают функцию «фильтрование ntuple» (ntuple filtering). Она позволяет указывать (посредством ethtool) набор параметров, используемых для фильтрования входных данных на уровне железа и адресации в конкретную очередь приёма. Например, можно прописать, чтобы TCP-пакеты, пришедшие на определённый порт, передавались в очередь 1.
В сетевых картах Intel эта функция обычно называется Intel Ethernet Flow Director [49]. Другие производители могут давать другие названия.
Как мы увидим дальше, фильтрование ntuple — критически важный компонент другой функции, Accelerated Receive Flow Steering (aRFS). Это сильно облегчает использование ntuple, если ваша сетевая карта поддерживает его. aRFS мы рассмотрим позднее.
Эта функция может быть полезна, если операционные требования системы подразумевают максимизацию локальности данных (data locality) ради увеличения частоты успешных обращений (hit rates) к кэшу CPU при обработке сетевых данных. Например, так выглядит конфигурирование веб-сервера, работающего на порте 80:
Как упоминалось выше, фильтрование ntuple можно настроить с помощью ethtool, но для начала вам нужно удостовериться, что эта функция включена на вашем устройстве. Проверить это можно так:
$ sudo ethtool -k eth0
Offload parameters for eth0:
...
ntuple-filters: off
receive-hashing: on
Как видите, ntuple-filters в состоянии off.
Включим ntuple-фильтры:
$ sudo ethtool -K eth0 ntuple on
После их включения или проверки, что фильтры работают, можно проверить настроенные правила:
$ sudo ethtool -u eth0
40 RX rings available
Total 0 rules
Здесь фильтры не имеют настроенных правил. Вы можете добавить их через командную строку с помощью ethtool. Давайте пропишем, чтобы весь TCP-трафик, приходящий на порт 80, переадресовывался очередь приёма 2:
$ sudo ethtool -U eth0 flow-type tcp4 dst-port 80 action 2
Фильтрование ntuple можно использовать и для отбрасывания пакетов определённых потоков на аппаратном уровне. Это бывает удобно для снижения нагрузки по трафику от каких-то IP-адресов. Подробнее о конфигурации правил фильтрации читайте man ethtool.
Получать статистику о степени успешности своих ntuple-правил можно с помощью проверки выходных данных ethtool -S [device name]. Например, на сетевых картах Intel fdir_match и fdir_miss выдают количество совпадений и промахов правил фильтрования. По вопросам отслеживания статистики своего устройства сверьтесь с исходниками драйвера и спецификацией.
Прежде чем переходить к рассмотрению сетевого стека, нам нужно кратко пройтись по системе SoftIRQ из ядра Linux.
Это механизм исполнения кода вне контекста обработчика прерываний, реализованного драйвером. Данная система важна, потому что аппаратные прерывания могут быть отключены на протяжении всего или части времени исполнения обработчика. Чем дольше отключены прерывания, тем выше вероятность пропустить события. Важно выносить выполнение любых длительных действий за пределы обработчика прерываний, чтобы завершить их как можно скорее и снова включить прерывания от устройства.
Существуют механизмы, которые могут использоваться для откладывания работы в ядре ради нужд сетевого стека. Мы рассмотрим SoftIRQ.
Систему SoftIRQ можно представить в виде серии тредов ядра (по одному на CPU), в которых работают функции-обработчики, зарегистрированные для разных SoftIRQ-событий. Если вы когда-нибудь поднимались наверх и встречали ksoftirqd/0 в списке тредов ядра, то это как раз тред SoftIRQ, выполняющийся на CPU 0.
Подсистемы ядра (например, по работе с сетью) могут регистрировать обработчика SoftIRQ посредством выполнения функции open_softirq. Дальше мы увидим, как система по работе с сетью регистрирует своих SoftIRQ-обработчиков. А теперь давайте немного поговорим о том, как работает SoftIRQ.
Поскольку SoftIRQ очень важны для откладывания работы драйверов устройств, то процесс ksoftirqd создаётся в рамках жизненного цикла ядра довольно рано.
Взгляните на код из kernel/softirq.c [50], показывающий, как инициализируется система ksoftirqd:
static struct smp_hotplug_thread softirq_threads = {
.store = &ksoftirqd,
.thread_should_run = ksoftirqd_should_run,
.thread_fn = run_ksoftirqd,
.thread_comm = "ksoftirqd/%u",
};
static __init int spawn_ksoftirqd(void)
{
register_cpu_notifier(&cpu_nfb);
BUG_ON(smpboot_register_percpu_thread(&softirq_threads));
return 0;
}
early_initcall(spawn_ksoftirqd);
Как следует из определения struct smp_hotplug_thread, регистрируются два указателя функций: ksoftirqd_should_run и run_ksoftirqd.
Обе функции вызываются kernel/smpboot.c [51] как часть чего-то, напоминающего цикл события (event loop).
Код в kernel/smpboot.c сначала вызывает ksoftirqd_should_run, который определяет, есть ли ожидающие SoftIRQ. Если есть, то выполняется run_ksoftirqd, которая выполняет некоторые второстепенные вычисления, прежде чем вызвать __do_softirq.
Функция __do_softirq делает несколько интересных вещей:
Так что когда вы смотрите на графики использования CPU и видите softirq или si, то это выполняется измерение объёма ресурсов CPU, которые используются отложенным рабочим контекстом.
Система softirq инкрементирует статистические счётчики, которые можно считывать из /proc/softirqs. Мониторинг этой статистики даст вам понимание того, с какой частотой генерируются SoftIRQ для разных событий.
Читаем /proc/softirqs, чтобы мониторить статистику SoftIRQ:
$ cat /proc/softirqs
CPU0 CPU1 CPU2 CPU3
HI: 0 0 0 0
TIMER: 2831512516 1337085411 1103326083 1423923272
NET_TX: 15774435 779806 733217 749512
NET_RX: 1671622615 1257853535 2088429526 2674732223
BLOCK: 1800253852 1466177 1791366 634534
BLOCK_IOPOLL: 0 0 0 0
TASKLET: 25 0 0 0
SCHED: 2642378225 1711756029 629040543 682215771
HRTIMER: 2547911 2046898 1558136 1521176
RCU: 2056528783 4231862865 3545088730 844379888
Благодаря этому файлу вы поймёте, как на данный момент распределена между процессами обработка входящих сетевых данных (NET_RX). Если неравномерно, счётчики одних CPU будут иметь значения выше, чем у других. Это показатель того, что вы можете извлечь выгоду из описанных ниже Receive Packet Steering / Receive Flow Steering. Будьте осторожны, полагаясь при мониторинге производительности лишь на этот файл: на протяжении периода высокой сетевой активности вы можете ожидать увеличения NET_RX, но этого может не произойти. На частоту SoftIRQ NET_RX могут повлиять дополнительные настройки в сетевом стеке, мы дальше этого коснёмся.
Так вот, будьте осторожны, но если вы измените упомянутые настройки, то изменения можно будет увидеть в /proc/softirqs.
Теперь перейдём к сетевому стеку и посмотрим, как получаемые сетевые данные проходят по нему сверху вниз.
Мы познакомились с работой сетевых драйверов и SoftIRQ, теперь займёмся инициализацией подсистемы сетевого устройства. Затем мы проследим путь следования пакета начиная с его прихода.
Подсистема сетевого устройства (netdev) инициализируется в функции net_dev_init. В ней вообще происходит много интересного.
net_dev_init создаёт набор структур struct softnet_data для каждого CPU в системе. Эти структуры содержат указатели на несколько важных для обработки сетевых данных вещей:
net_dev_init регистрирует обработчика SoftIRQ приёма и передачи, который будет использоваться для обработки входящих или исходящих сетевых данных. Код довольно простой:
static int __init net_dev_init(void)
{
/* ... */
open_softirq(NET_TX_SOFTIRQ, net_tx_action);
open_softirq(NET_RX_SOFTIRQ, net_rx_action);
/* ... */
}
Теперь посмотрим, как драйверный обработчик прерываний «поднимет» (или запустит) функцию net_rx_action, зарегистрированную на SoftIRQ NET_RX_SOFTIRQ.
Наконец-то прибыли сетевые данные!
Будем считать, что очередь приёма имеет достаточно доступных дескрипторов, так что пакет записывается в память посредством DMA. Затем устройство генерирует приписанное к пакету прерывание (или, в случае с MSI-X, прерывание привязано к очереди приёма, в которую адресуется прибывший пакет).
Выполняемый после генерирования прерывания обработчик, в целом должен пытаться вынести как можно больший объём работы за пределы контекста прерывания. Это критически важно, потому что в ходе обработки одного прерывания другие могут быть заблокированы.
Давайте посмотрим на исходный код обработчика прерываний MSI-X. Это поможет проиллюстрировать идею, что обработчик выполняет минимальный объём работы.
Взято из drivers/net/ethernet/intel/igb/igb_main.c [54]:
static irqreturn_t igb_msix_ring(int irq, void *data)
{
struct igb_q_vector *q_vector = data;
/* Пишет значение ITR, вычисленное из предыдущего прерывания. */
igb_write_itr(q_vector);
napi_schedule(&q_vector->napi);
return IRQ_HANDLED;
}
Это очень короткий обработчик, и перед возвратом он выполняет две очень быстрые операции.
Важно увидеть реальный код, демонстрирующий работу вышеописанных операций. Это поможет нам понять, как сетевые данные обрабатываются в многопроцессорных системах.
Давайте посмотрим, как работает вызов napi_schedule из обработчика аппаратных прерываний.
Как вы помните, NAPI существует именно для сбора сетевых данных без использования прерываний от сетевой карты, сигнализирующих о готовности данных к обработке. Выше упоминалось, что цикл poll загружается (bootstrapped) при получении аппаратного прерывания. Иными словами, NAPI включён, но не активен до тех пор, пока не придёт первый пакет. В этот момент сетевая карта генерирует прерывание и NAPI стартует. Ниже мы рассмотрим и другие случаи, когда NAPI может быть выключен, и прежде чем его запустить, понадобится сгенерировать прерывание.
Цикл poll запускается, когда драйверный обработчик прерываний вызывает napi_schedule. Это просто функция-обёртка, заданная в заголовочном файле, которая в свою очередь вызывает __napi_schedule.
Взято из net/core/dev.c [55]:
/**
* __napi_schedule – расписание получения
* @n: запись в расписании
*
* Будет задан старт функции получения записи
*/
void __napi_schedule(struct napi_struct *n)
{
unsigned long flags;
local_irq_save(flags);
____napi_schedule(&__get_cpu_var(softnet_data), n);
local_irq_restore(flags);
}
EXPORT_SYMBOL(__napi_schedule);
В этом коде для получения структуры softnet_data, зарегистрированной на текущий CPU, используется __get_cpu_var. Данная структура передаётся в ____napi_schedule вместе со взятой из драйвера структурой struct napi_struct. Как много нижних подчёркиваний.
Давайте посмотрим на ____napi_schedule, взято из net/core/dev.c [56]:
/* Вызывается с отключённым IRQ */
static inline void ____napi_schedule(struct softnet_data *sd,
struct napi_struct *napi)
{
list_add_tail(&napi->poll_list, &sd->poll_list);
__raise_softirq_irqoff(NET_RX_SOFTIRQ);
}
Этот код делает две важные вещи:
Как мы скоро увидим, SoftIRQ функция-обработчик net_rx_action для сбора пакетов вызовет функцию NAPI poll.
Обратите внимание, что весь код до этого, который переносил работу из обработчика аппаратных прерываний в SoftIRQ, использовал структуры, ассоциированные с текущим CPU.
Поскольку сам драйверный IRQ-обработчик мало что делает, то SoftIRQ-обработчик будет выполняться на том же CPU, что и IRQ-обработчик. Поэтому важно настраивать, каким CPU будет обрабатываться конкретный IRQ: ведь этот CPU будет использован не только для выполнения драйверного обработчика прерываний, но и для сбора пакетов в SoftIRQ посредством NAPI.
Позднее мы увидим, что механизмы наподобие управления принимаемыми пакетами (Receive Packet Steering [53]) могут распределять часть работы по другим CPU в рамках сетевого стека.
Примечание: мониторинг аппаратных прерываний не даёт полной картины корректности обработки пакетов. Многие драйверы отключают аппаратные прерывания во время работы NAPI. Но всё же это важная часть общего решения мониторинга.
Читаем /proc/interrupts, чтобы мониторить статистику аппаратных прерываний:
$ cat /proc/interrupts
CPU0 CPU1 CPU2 CPU3
0: 46 0 0 0 IR-IO-APIC-edge timer
1: 3 0 0 0 IR-IO-APIC-edge i8042
30: 3361234770 0 0 0 IR-IO-APIC-fasteoi aacraid
64: 0 0 0 0 DMAR_MSI-edge dmar0
65: 1 0 0 0 IR-PCI-MSI-edge eth0
66: 863649703 0 0 0 IR-PCI-MSI-edge eth0-TxRx-0
67: 986285573 0 0 0 IR-PCI-MSI-edge eth0-TxRx-1
68: 45 0 0 0 IR-PCI-MSI-edge eth0-TxRx-2
69: 394 0 0 0 IR-PCI-MSI-edge eth0-TxRx-3
NMI: 9729927 4008190 3068645 3375402 Non-maskable interrupts
LOC: 2913290785 1585321306 1495872829 1803524526 Local timer interrupts
Мониторя статистику в /proc/interrupts, можно увидеть изменение количества и частоты аппаратных прерываний по мере прибытия пакетов. Также можно удостовериться, что каждая очередь приёма вашей сетевой карты обрабатывается соответствующим CPU. Количество говорит нам лишь о том, сколько было прерываний, но эта метрика не обязательно поможет понять, сколько данных было получено или обработано, потому что многие драйверы отключают прерывания сетевой карты в качестве части договора с подсистемой NAPI. Более того, использование объединения прерываний (interrupt coalescing) также влияет на статистику, получаемую из этого файла. Его мониторинг позволяет определить, действительно ли работают выбранные вами настройки объединения прерываний.
Для получения более полного представления о корректности обработки сетевых данных, вам нужно мониторить /proc/softirqs и ещё ряд файлов в /proc. Об этом мы поговорим ниже.
Так называется метод [57] предотвращения передачи прерываний из устройства в CPU, пока не наберётся определённое количество событий или объём конкретной работы.
Это помогает избегать «штормов прерываний [58]» и увеличивать пропускную способность или задержку, в зависимости от настроек. Уменьшение количества генерируемых прерываний приводит к повышению пропускной способности и задержки, снижению нагрузки на CPU. С ростом количества прерываний происходит обратное: снижается задержка и пропускная способность, но увеличивается нагрузка на CPU.
Исторически сложилось так, что ранние версии igb, e1000 и ряда других драйверов поддерживали только параметр InterruptThrottleRate. В более свежих версиях его заменили generic функцией из ethtool.
Получение текущих настроек объединения IRQ:
$ sudo ethtool -c eth0
Coalesce parameters for eth0:
Adaptive RX: off TX: off
stats-block-usecs: 0
sample-interval: 0
pkt-rate-low: 0
pkt-rate-high: 0
...
ethtool предоставляет generic-интерфейс для оперирования настройками объединения. Однако помните, что не каждое устройство или драйвер поддерживают все настройки. Сверьтесь с документацией или исходным кодом, чтобы узнать, что поддерживается в вашем случае. Как сказано в документации к ethtool: «Всё, что не реализовано драйвером, будет тихо игнорироваться».
Некоторые драйверы поддерживают любопытную опцию «адаптивного объединения прерываний приёма/передачи» (adaptive RX/TX IRQ coalescing). Обычно она реализуется в оборудовании. Драйверу нужно проделать некоторую работу, чтобы сообщить сетевой карте о включении этой опции, а также кое-какие вычисления (bookkeeping) (как мы видели ранее в коде драйвера igb).
Включение этой опции позволяет адаптировать количество прерываний для улучшения задержки при небольшом количестве пакетов, и улучшения пропускной способности при большом количестве пакетов.
Включение адаптивного объединения прерываний приёма:
$ sudo ethtool -C eth0 adaptive-rx on
Можно использовать ethtool -C для настройки нескольких опций. Некоторые из наиболее часто используемых:
И есть ещё очень много других.
Напоминаю, что ваше оборудование и драйвер могут поддерживать лишь часть опций. Изучите исходный код драйвера и спецификацию сетевой карты.
К сожалению, доступные для настраивания опции плохо документированы везде, кроме заголовочного файла. Обратитесь к исходнику include/uapi/linux/ethtool.h [59] и найдите объяснение для каждой опции, поддерживаемой ethtool (но не факт, что они поддерживаются вашим драйвером и сетевой картой).
Примечание: на первый взгляд объединение прерываний выглядит очень полезной оптимизацией. В некоторых случаях это действительно так, но вам нужно удостовериться, что остальная часть сетевого стека тоже настроена правильно. Одно лишь изменение параметров объединения наверняка принесёт мало пользы.
Если ваша карта поддерживает RSS, или если вы пытаетесь оптимизировать локальность данных, ты вам может понадобиться использовать специфический набор CPU для обработки генерируемых картой прерываний.
Это позволяет сегментировать использование CPU. Подобные изменения могут влиять на работу верхних уровней, как мы видели на примере сетевого стека.
Если вы решили настроить привязки IRQ, то сначала проверьте, работает ли демон irqbalance. Он пытается автоматически сбалансировать прерывания и CPU, и потому может перезаписать ваши настройки. Отключите irqbalance, или используйте --banirq вместе с IRQBALANCE_BANNED_CPUS, чтобы irqbalance знал, что ему не следует трогать настраиваемый вами набор прерываний и CPU.
Затем проверьте файл /proc/interrupts на наличие списка номеров прерываний для каждой очереди приёма вашей сетевой карты. Наконец, внесите изменения в /proc/irq/IRQ_NUMBER/smp_affinity, прописав для каждого номера, какой CPU должен обрабатывать данное прерывание. Просто укажите для этого файла шестнадцатеричную битовую маску, чтобы ядро знало, какие CPU использовать для обработки прерываний.
Пример: настроим привязку IRQ 8 к CPU 0:
$ sudo bash -c 'echo 1 > /proc/irq/8/smp_affinity'
Когда SoftIRQ-код определяет, какие из SoftIRQ находятся в состоянии ожидания, исполняется net_rx_action и начинается процесс обработки сетевых данных.
Давайте на части цикла обработки net_rx_action, чтобы понять, как это работает, что можно настроить и мониторить.
net_rx_action начинает обработку пакетов из памяти, которые были отправлены туда устройством посредством DMA.
Функция итерирует по списку структур NAPI, стоящих в очереди текущего CPU, поочерёдно извлекает каждую структуру работает с ней.
Цикл обработки ограничивает объём работы и время исполнения зарегистрированных NAPI-функций poll. Он делает это двумя способами:
Взято из net/core/dev.c [60]:
while (!list_empty(&sd->poll_list)) {
struct napi_struct *n;
int work, weight;
/* Если исчерпалось окно SoftIRQ - отфутболиваем
* Выполняйте это в течение двух тиков, это позволит сделать
* среднюю задержку на уровне 1.5/Гц.
*/
if (unlikely(budget <= 0 || time_after_eq(jiffies, time_limit)))
goto softnet_break;
Таким образом ядро не позволяет обработке пакетов занять все ресурсы CPU. budget — это весь доступный бюджет, который будет разделён на все доступные NAPI-структуры, зарегистрированные на этот CPU.
Это ещё одна причина, почему сетевые карты с несколькими очередями требуют аккуратной настройки привязки IRQ. Вспомните, что на CPU, который обрабатывает прерывания от устройства, будет также выполняться и обработчик SoftIRQ. И в результате на том же CPU работают вышеприведённый цикл и выполняются вычисления бюджета.
Системы с несколькими сетевыми картами, каждая из которых поддерживает несколько очередей, могут оказаться в ситуации, когда на один CPU зарегистрировано несколько NAPI-структур. Обработка данных всех структур на одном CPU «оплачивается» из одного и того же бюджета.
Если вам не хватает CPU для распределения прерываний, то можно увеличить net_rx_action budget, чтобы каждый CPU обрабатывал больше пакетов. Увеличение бюджета повлечёт увеличение нагрузки на CPU (в особенности sitime или si в top или других программах), но должно снизить задержку, потому что данные будут обрабатываться быстрее.
Примечание: CPU будет ограничен по времени двумя jiffies [61], вне зависимости от присвоенного бюджета.
Напомню, что драйверы сетевых устройств для регистрации функции poll используют netif_napi_add. Как мы видели ранее, драйвер igb содержит такой код:
/* инициализация NAPI */
netif_napi_add(adapter->netdev, &q_vector->napi, igb_poll, 64);
Здесь регистрируется NAPI-структура, которой в коде прописан вес 64. Давайте посмотрим, как этот вес используется в цикле обработки net_rx_action.
Взято из net/core/dev.c [62]:
weight = n->weight;
work = 0;
if (test_bit(NAPI_STATE_SCHED, &n->state)) {
work = n->poll(n, weight);
trace_napi_poll(n);
}
WARN_ON_ONCE(work > weight);
budget -= work;
Мы получаем вес, зарегистрированный на структуру NAPI, и передаём его в функцию poll, тоже зарегистрированную на структуру NAPI (в приведённом примере igb_poll).
Функция poll возвращает количество обработанных фреймов. Оно сохраняется в виде work, и потом вычитается из общего budget.
Допустим:
Ваша система прекратит обрабатывать данные, если:
Пока ещё не упоминалась важная информация, касающаяся договора между подсистемой NAPI и драйверами устройств. Речь об условиях остановки NAPI.
Теперь посмотрим, как net_rx_action работает с первой частью договора. Затем, после рассмотрения функции poll, выясним, как выполняется вторая часть договора.
Цикл обработки net_rx_action завершается секцией кода, которая выполняет первую часть договора с NAPI. Взято из net/core/dev.c [63]:
/* Драйверы не должны изменять состояние NAPI, если они
* расходуют весь свой вес. В таких случаях код всё ещё
* «владеет» экземпляром NAPI, и, следовательно, может
* перемещать его по списку по своему желанию.
*/
if (unlikely(work == weight)) {
if (unlikely(napi_disable_pending(n))) {
local_irq_enable();
napi_complete(n);
local_irq_disable();
} else {
if (n->gro_list) {
/* сбрасываем слишком старые пакеты
* Если HZ < 1000, сбрасываем все пакеты.
*/
local_irq_enable();
napi_gro_flush(n, HZ >= 1000);
local_irq_disable();
}
list_move_tail(&n->poll_list, &sd->poll_list);
}
}
Если расходуется весь объём работы, то net_rx_action обрабатывает две ситуации:
Таким образом цикл обработки пакетов вызывает зарегистрированную драйверную функцию poll, которая займётся обработкой. Дальше мы увидим, что эта функция собирает сетевые данные и отправляет их в стек для дальнейшей обработки.
Выход из цикла net_rx_action будет совершён, если:
Вот один из вышеприведённых примеров кода
/* Если исчерпалось окно SoftIRQ - отфутболиваем.
* Выполняйте это в течение двух тиков, это позволит сделать
* среднюю задержку на уровне 1.5/Гц.
*/
if (unlikely(budget <= 0 || time_after_eq(jiffies, time_limit)))
goto softnet_break;
Если вы отследите label softnet_break, наткнётесь на кое-что интересное. Взято из net/core/dev.c [65]:
softnet_break:
sd->time_squeeze++;
__raise_softirq_irqoff(NET_RX_SOFTIRQ);
goto out;
Инкрементируется некоторая статистика структуры struct softnet_data и закрывается SoftIRQ NET_RX_SOFTIRQ. Поле time_squeeze — это количество раз, когда у net_rx_action была работа, но бюджета не хватало либо было достигнуто ограничение по времени, прежде чем работа была завершена. Этот счётчик крайне полезен для понимания узких мест в сетевой обработке. Скоро мы этого коснёмся. NET_RX_SOFTIRQ отключено, чтобы освободить время обработки для других задач. В этом есть смысл, поскольку этот маленький кусок кода выполняется только тогда, когда можно сделать больше работы, но нам не нужно монополизировать CPU.
Затем исполнение передаётся ярлыку (label) out. Оно может быть передано out и в том случае, если больше не осталось NAPI-структур для обработки, то есть бюджета больше, чем сетевой активности, все драйверы закрыли NAPI, а net_rx_action нечем заняться.
Прежде чем выполнить возврат из net_rx_action, в секции out делается важная вещь: вызывается net_rps_action_and_irq_enable. Если включено управление принимаемыми пакетами (Receive Packet Steering [53]), то эта функция пробуждает удалённые CPU, чтобы они начали обрабатывать сетевые данные.
Ниже мы разберём работу RPS. А пока посмотрим, как мониторить корректность цикла обработки net_rx_action, и перейдём к «внутренностям» NAPI-функций poll, продвигаясь по сетевому стеку.
Напомню, что драйверы выделяют область памяти, в которую устройство может напрямую отправлять входящие пакеты. Драйвер отвечает не только за выделение памяти, но и за их освобождение, сбор данных и их отправку в сетевой стек.
Как драйвер igb всё это делает?
Наконец-то мы можем проанализировать работу igb_poll. Её код обманчиво прост. Взято из drivers/net/ethernet/intel/igb/igb_main.c [66]:
/**
* igb_poll – NAPI Rx polling callback
* @napi: структура опроса (polling) NAPI
* @budget: счётчик количества пакетов, которые нужно обработать
**/
static int igb_poll(struct napi_struct *napi, int budget)
{
struct igb_q_vector *q_vector = container_of(napi,
struct igb_q_vector,
napi);
bool clean_complete = true;
#ifdef CONFIG_IGB_DCA
if (q_vector->adapter->flags & IGB_FLAG_DCA_ENABLED)
igb_update_dca(q_vector);
#endif
/* ... */
if (q_vector->rx.ring)
clean_complete &= igb_clean_rx_irq(q_vector, budget);
/* Если вся работа не завершена, бюджета возвращается и продолжается опрос */
if (!clean_complete)
return budget;
/* Если выполнено недостаточно работы по приёму, выходит из режима опроса */
napi_complete(napi);
igb_ring_irq_enable(q_vector);
return 0;
}
Здесь есть ряд интересных вещей:
Давайте посмотрим, как igb_clean_rx_irq отправляет сетевые данные вверх по стеку.
Функция igb_clean_rx_irq — это цикл, обрабатывающая по одному пакету за раз, пока не кончится budget или данные для обработки.
В этом цикле выполняются следующие вещи:
Когда цикл прерывается, функция записывает счётчики обработанных пакетов и байтов.
Прежде чем продолжить рассмотрение сетевого стека, сделаем пару отступлений. Во-первых, посмотрим, как мониторить и настраиваться SoftIRQ сетевой подсистемы. Во-вторых, поговорим о Generic Receive Offloading (GRO). После того, как мы вникнем в napi_gro_receive, нам будет понятнее работа остальной части сетевого стека.
Как мы видели в предыдущей главе, статистические счётчики инкрементируются при выходе из цикла net_rx_action, либо если осталась работа, которая могла быть выполнена, но мы упёрлись в ограничение бюджета или времени для SoftIRQ. Эта статистика отслеживается как часть структуры struct softnet_data, ассоциированной с CPU. Она сбрасывается в файл /proc/net/softnet_stat, по которому, к сожалению, очень мало документации. Поля не проименованы и могут меняться в зависимости от версии ядра.
В Linux 3.13.0 можно выяснить, какие значения соответствуют каким полям в /proc/net/softnet_stat посредством чтения исходников ядра. Взято из net/core/net-procfs.c [68]:
seq_printf(seq,
"%08x %08x %08x %08x %08x %08x %08x %08x %08x %08x %08xn",
sd->processed, sd->dropped, sd->time_squeeze, 0,
0, 0, 0, 0, /* was fastroute */
sd->cpu_collision, sd->received_rps, flow_limit_count);
Многие из этих счётчиков имеют странные имена и инкрементируются в неожиданных местах. Только изучив сетевой стек можно понять, когда и где инкрементируется каждый из них. Поскольку статистика squeeze_time встречалась в net_rx_action, мне кажется, есть смысла задокументировать сейчас этот файл.
Читаем /proc/net/softnet_stat, чтобы мониторить статистику обработки сетевых данных:
$ cat /proc/net/softnet_stat
6dcad223 00000000 00000001 00000000 00000000 00000000 00000000 00000000 00000000 00000000
6f0e1565 00000000 00000002 00000000 00000000 00000000 00000000 00000000 00000000 00000000
660774ec 00000000 00000003 00000000 00000000 00000000 00000000 00000000 00000000 00000000
61c99331 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000000
6794b1b3 00000000 00000005 00000000 00000000 00000000 00000000 00000000 00000000 00000000
6488cb92 00000000 00000001 00000000 00000000 00000000 00000000 00000000 00000000 00000000
Важные подробности относительно /proc/net/softnet_stat:
Если вы решили мониторить этот файл и отображать результаты графически, то будьте крайне осторожны, чтобы не поменять порядок полей и сохранить их смысл. Для этого ознакомьтесь с исходниками ядра.
Настройка бюджета net_rx_action позволяет определять, сколько можно потратить на обработку пакетов для всех NAPI-структур, зарегистрированных на CPU. Для этого нужно с помощью sysctl настроить значение net.core.netdev_budget.
Пример: присвоим общему бюджету обработки пакетов значение 600.
$ sudo sysctl -w net.core.netdev_budget=600
Ещё эту настройку можно записать в файл /etc/sysctl.conf так, чтобы изменение сохранялось после перезагрузки. В Linux 3.13.0 значение по умолчанию 300.
Generic Receive Offloading (GRO) — это программная реализация аппаратной оптимизации, известной как Large Receive Offloading [52] (LRO).
Суть обоих механизмов в том, чтобы уменьшить количество пакетов, передаваемых по сетевому стеку, за счёт комбинирования «достаточно похожих» пакетов. Это позволяет снизить нагрузку на CPU. Представим, что у нас передаётся большой файл, и большинство пакетов содержат чанки данных из этого файла. Вместо отправки по стеку маленьких пакетов по одному, входящие пакеты можно комбинировать в один, с огромной полезной нагрузкой. А затем уже передавать его по стеку. Таким образом уровни протоколов обрабатывают заголовки одного пакета, при этом передавая пользовательской программе более крупные чанки.
Но этой оптимизации присуща проблема потери информации. Если какой-то пакет имеет настроенную важную опцию или флаг, то эта опция или флаг могут быть потеряны при объединении с другими пакетами. В целом реализации LRO имеют очень нестрогие правила объединения пакетов.
GRO является программной реализацией LRO, но с более строгими правилами объединения.
Кстати: если вы когда-то использовали tcpdump и встречали слишком большие размеры входящих пакетов, то это наверняка было связано с включённой GRO в вашей системе. Как мы скоро увидим, tap'ы захвата пакетов вставлены дальше по стеку, уже после GRO.
ethtool можно использовать для проверки, включена ли GRO, а также для её настройки.
Проверка настроек:
$ ethtool -k eth0 | grep generic-receive-offload
generic-receive-offload: on
В данном случае включена generic-receive-offload. Включаем GRO:
$ sudo ethtool -K eth0 gro on
Примечание: у большинства драйверов такие изменения приведут к падению и перезагрузке интерфейса, потому что подключения к нему будут прерваны. Хотя при однократном изменении это не слишком важно.
Функция napi_gro_receive занимается обработкой сетевых данных для GRO (если GRO включена) и отправкой их по стеку на уровни протоколов. Большая часть логики находится в функции dev_gro_receive.
Эта функция сначала проверяет, включена ли GRO. Если да, то готовится к её применению: проходит по списку offload-фильтров, чтобы высокоуровневые стеки протоколов могли работать с данными, предназначенными для GRO. Это нужно для того, чтобы уровни протоколов могли сообщать уровню сетевого устройства, является ли пакет частью сетевого потока [71], который в данный момент свободен, а также могли обрабатывать всё относящееся к протоколу, что должно произойти в рамках GRO. Например, TCP-протоколу нужно решить, можно ли/когда подтверждать объединение пакета с уже имеющимся.
Вот пример кода, который это делает, взятый из net/core/dev.c [72]:
list_for_each_entry_rcu(ptype, head, list) {
if (ptype->type != type || !ptype->callbacks.gro_receive)
continue;
skb_set_network_header(skb, skb_gro_offset(skb));
skb_reset_mac_len(skb);
NAPI_GRO_CB(skb)->same_flow = 0;
NAPI_GRO_CB(skb)->flush = 0;
NAPI_GRO_CB(skb)->free = 0;
pp = ptype->callbacks.gro_receive(&napi->gro_list, skb);
break;
}
Если уровни протоколов сообщают, что пришло время сбросить GRO-пакет, то далее выполняется эта процедура. В этом случае вызывается napi_gro_complete, которая вызывает callback gro_complete для уровней протоколов, а затем передаёт пакет по стеку посредством вызова netif_receive_skb.
Пример кода из net/core/dev.c [73]:
if (pp) {
struct sk_buff *nskb = *pp;
*pp = nskb->next;
nskb->next = NULL;
napi_gro_complete(nskb);
napi->gro_count--;
}
Если уровни протоколов объединили этот пакет с имеющимся потоком, то napi_gro_receive посто возвращается.
Если пакет не было объединён и в системе меньше MAX_GRO_SKBS (8) GRO-потоков, то в список gro_list NAPI-структуры данного CPU добавляется новая запись.
Пример кода из net/core/dev.c [74]:
if (NAPI_GRO_CB(skb)->flush || napi->gro_count >= MAX_GRO_SKBS)
goto normal;
napi->gro_count++;
NAPI_GRO_CB(skb)->count = 1;
NAPI_GRO_CB(skb)->age = jiffies;
skb_shinfo(skb)->gso_size = skb_gro_len(skb);
skb->next = napi->gro_list;
napi->gro_list = skb;
ret = GRO_HELD;
Так работает система GRO в сетевом стеке Linux.
По завершении dev_gro_receive вызывается napi_skb_finish, которая освобождает структуры данных, невостребованные по причине слияния пакета, либо для передачи данных по сетевому стеку вызывается netif_receive_skb (потому что GRO уже применена к потокам MAX_GRO_SKBS).
Теперь пришло время рассмотреть механизм управления принимаемыми пакетами (Receive Packet Steering (RPS)).
Помните, выше мы обсуждали, как драйверы сетевых устройств регистрируют NAPI-функцию poll? Каждый экземпляр поллера NAPI исполняется в контексте SoftIRQ, по одному на каждый CPU. Теперь вспомним, что CPU, на котором выполняется драйверный обработчик прерываний, для обработки пакетов активирует SoftIRQ-цикл обработки.
Иными словами, в рамках обработки входящих данных одиночный CPU обрабатывает для пакетов прерывания и функции poll.
Некоторые сетевые карты (вроде Intel I350) на аппаратном уровне поддерживают несколько очередей. Это означает, что входящие пакеты могут напрямую отправляться в разные области памяти, выделенные для каждого очереди. При этом поллинг каждой области выполняется с помощью отдельных NAPI-структур. Так что прерывания и пакеты будут обрабатываться несколькими CPU.
Этот механизм называется Receive Side Scaling (RSS).
Receive Packet Steering (RPS) [75] — это программная реализация RSS. А раз реализовано в коде, то может быть применено для любой сетевой карты, даже если она имеет лишь одну очередь приёма. С другой стороны, программная природа приводит к тому, что RPS может входить в поток только после пакета, извлечённого из DMA-области памяти.
Это означает, что CPU не будет тратить меньше времени на обработку прерываний или цикла poll, но зато вы сможете распределять нагрузку по обработке пакетов после того, как они были собраны, и снижать продолжительность работы CPU в сетевом стеке.
RPS генерирует для входящих данных хэш, чтобы определить, какой CPU должен их обработать. Затем данные помещаются во входящую очередь (backlog) этого процессора в ожидании последующей обработки. В процессор с backlog передаётся межпроцессорное прерывание [69] (IPI), инициирующее обработку очереди, если это ещё не делается. /proc/net/softnet_stat содержит счётчик количества раз, когда каждая структура softnet_data получала IPI (поле received_rps).
Следовательно, netif_receive_skb продолжит отправлять данные по сетевому стеку или передаст в RPS для обработки другим CPU.
Для начала нужно включить механизм RPS в конфигурации ядра (на Ubuntu верно для ядра 3.13.0), а также создать битовую маску, описывающую, какие CPU должны обрабатывать пакеты для конкретного интерфейса или очереди приёма.
Информацию об этих битовых масках можно найти в документации к ядру [76]. Если вкратце, взять и модифицировать маски можно отсюда:
/sys/class/net/DEVICE_NAME/queues/QUEUE/rps_cpus
Например, для eth0 и очереди приёма 0 нужно внести в файл /sys/class/net/eth0/queues/rx-0/rps_cpus шестнадцатеричное число, обозначающее, какой CPU должен обрабатывать пакеты из очереди 0 eth0. Как указано в документации [77], на определённых конфигурациях в RPS нет нужды.
Примечание: включение RPS для распределения обработки пакетов по CPU, которые раньше этого не делали, для каждого из этих CPU приведёт к увеличению количества SoftIRQ `NET_RX`, а также `si` или `sitime` на графике потребления ресурсов CPU. Можете сравнить показатели «до» и «после», чтобы выяснить, соответствует ли конфигурация RPS вашим пожеланиям.
Receive flow steering (RFS) используется совместно с RPS. RPS пытается распределять входящие пакеты среди нескольких CPU, но не принимает во внимание вопросы локальности данных для увеличения частоты попадания в кэш CPU. Если вам нужно увеличить эту частоту, то в этом поможет механизм RFS, переадресующий пакеты одного потока на один и тот же CPU.
Чтобы RFS заработал, нужно его включить и сконфигурировать. RFS отслеживает глобальную хэш-таблицу всех потоков. Размер таблицы настраивается через sysctl параметром net.core.rps_sock_flow_entries.
Увеличим размер хэша потоков в сокете RFS:
$ sudo sysctl -w net.core.rps_sock_flow_entries=32768
Теперь можно настроить количество потоков для каждой очереди приёма. Это делается в файле rps_flow_cnt для каждой очереди.
Пример: увеличим количество потоков до 2048 в eth0 для очереди 0:
$ sudo bash -c 'echo 2048 > /sys/class/net/eth0/queues/rx-0/rps_flow_cnt'
Работу RFS можно аппаратно ускорять. Сетевая карта и ядро могут совместно определять, какой поток на каком CPU нужно обрабатывать. Эта функция должна поддерживаться картой и драйвером, сверьтесь со спецификацией. Если драйвер вашей карты предоставляет функцию ndo_rx_flow_steer, значит он поддерживает aRFS.
Допустим, ваш драйвер поддерживает этот механизм. Порядок его включения и настройки:
Когда всё это будет сделано, aRFS автоматически задействуется для перемещения данных в очередь приёма, закреплённую за ядром CPU, обрабатывающим данные из этого потока. Вам не нужно вручную прописывать правила фильтрации ntuple для каждого потока.
Вернёмся к тому месту, где мы оставили netif_receive_skb, вызываемую из нескольких мест. Чаще всего из двух (мы их уже рассмотрели):
Напоминаю: netif_receive_skb и его потомки оперируют в контексте цикла обработки SoftIRQ. Инструменты вроде top учитывают затраченное здесь время как sitime или si.
netif_receive_skb сначала проверят значение sysctl чтобы определить, включено ли у пользователя присвоение временных меток при получении до или после того, как пакет попадает в backlog-очередь. Если эта настройка включена, то данным теперь присваиваются временные метки до того, как они подвергаются работе механизма RPS (и backlog-очереди, ассоциированной с CPU). Если эта настройка выключена, то данным присваиваются временные метки после попадания в очередь. При включённом RPS это можно использовать для распределения нагрузки по присваиванию меток среди нескольких CPU, но даст некоторую задержку.
Порядок присваивания меток можно настраивать в sysctl посредством net.core.netdev_tstamp_prequeue.
Отключим присваивание меток принимаемым пакетам:
$ sudo sysctl -w net.core.netdev_tstamp_prequeue=0
По умолчанию значение равно 1. Значение этой настройки объяснено в предыдущей главе.
Разобравшись с временными метками, далее netif_receive_skb действует по разному, в зависимости от того, включён ли RPS. Начнём с более простого случая, когда RPS выключен.
В этом случае вызывается __netif_receive_skb, которая регистрирует какие-то данные (bookkeeping), а затем вызывает __netif_receive_skb_core, чтобы та перенесла данные поближе к стекам протоколов.
Потом мы рассмотрим, как работает __netif_receive_skb_core, но сначала разберём работу кода со включённым RPS, поскольку в этом случае также вызывается __netif_receive_skb_core.
После работы с опциями присваивания временных меток, netif_receive_skb выполняет ряд вычислений чтобы определить, backlog-очередь какого CPU нужно использовать. Это делается с помощью функции get_rps_cpu. Взято из net/core/dev.c [78]:
cpu = get_rps_cpu(skb->dev, skb, &rflow);
if (cpu >= 0) {
ret = enqueue_to_backlog(skb, cpu, &rflow->last_qtail);
rcu_read_unlock();
return ret;
}
get_rps_cpu также принимает во внимание настройки RFS и aRFS, чтобы удостовериться, что посредством вызова enqueue_to_backlog данные попадают в backlog нужного CPU.
Эта функция сначала получает указатель на структуру softnet_data удалённого CPU, содержащую указатель на input_pkt_queue. Затем проверяется длина очереди input_pkt_queue этого удалённого CPU. Взято из net/core/dev.c [79]:
qlen = skb_queue_len(&sd->input_pkt_queue);
if (qlen <= netdev_max_backlog && !skb_flow_limit(skb, qlen)) {
Сначала длина очереди input_pkt_queue сравнивается с netdev_max_backlog. Если длина большего этого значения, то данные отбрасываются. Аналогично проверяется ограничение потока, и если оно достигнуто, то данные отбрасываются. В обоих случаях инкрементируется счётчик отбрасываний структуры softnet_data. Обратите внимание, что это структура того CPU, в чью очередь должны были быть помещены данные. Мониторинг счётчика отбрасываний описан в главе о /proc/net/softnet_stat.
enqueue_to_backlog вызывается при обработке пакетов с включённым RPS, а также из netif_rx. Большинству драйверов следует использовать не netif_rx, а netif_receive_skb. Если вы не применяете RPS и ваш драйвер не использует netif_rx, то увеличение backlog’а не окажет на систему заметного влияния, поскольку он и не используется.
Примечание: проверьте используемый драйвер. Если он вызывает netif_receive_skb и вы не используете RPS, то увеличение netdev_max_backlog никак не повысит производительность, потому что никакие данные не доберутся до input_pkt_queue.
Допустим, что input_pkt_queue достаточно мала и ограничение потока не достигнуто (или отключено), а данные могут быть помещены в очередь. Логика здесь немного смешная, её можно обобщить так:
Из-за использования goto код довольно хитрый, так что читайте внимательно. Взято из net/core/dev.c [80]:
if (skb_queue_len(&sd->input_pkt_queue)) {
enqueue:
__skb_queue_tail(&sd->input_pkt_queue, skb);
input_queue_tail_incr_save(sd, qtail);
rps_unlock(sd);
local_irq_restore(flags);
return NET_RX_SUCCESS;
}
/* Schedule NAPI for backlog device
* We can use non atomic operation since we own the queue lock
*/
if (!__test_and_set_bit(NAPI_STATE_SCHED, &sd->backlog.state)) {
if (!rps_ipi_queued(sd))
____napi_schedule(sd, &sd->backlog);
}
goto enqueue;
RPS распределяет нагрузку по обработке пакетов между несколькими CPU, но один большой поток способен монополизировать время обработки и ущемлять более мелкие потоки. Ограничения потоков позволяют лимитировать количество пакетов от одного потока, помещаемых в backlog. В результате маленькие потоки могут обрабатываться параллельно с гораздо более крупными.
Выражение if из net/core/dev.c [79] посредством вызова skb_flow_limit проверяет ограничение потока:
if (qlen <= netdev_max_backlog && !skb_flow_limit(skb, qlen)) {
Здесь проверяется, есть ли ещё место в очереди и не достигнуто ли ограничение [81]. По умолчанию ограничения отключены. Для их включения нужно задать битовую маску (аналогично RPS).
См. главу о мониторинге /proc/net/softnet_stat. Поле dropped — это счётчик, инкрементируемый при каждом отбрасывании данных вместо их помещения в очередь input_pkt_queue CPU.
Прежде чем настраивать это значение, прочитайте примечание в предыдущей главе.
Если вы используете RPS или ваш драйвер вызывает netif_rx, то можно помочь предотвращать отбрасывания в enqueue_to_backlog с помощью увеличения netdev_max_backlog.
Пример: увеличим backlog до 3000:
$ sudo sysctl -w net.core.netdev_max_backlog=3000
По умолчанию значение равно 1000.
Настроить вес поллера NAPI в backlog’е можно с помощью net.core.dev_weight sysctl. Это значение определяет, какую часть общего бюджета может потратить цикл poll backlog'а (см. главу о настройке net.core.netdev_budget).
Пример: увеличим в backlog’е цикл обработки poll:
$ sudo sysctl -w net.core.dev_weight=600
По умолчанию значение равно 64.
Помните, что обработка backlog’а выполняется в контексте SoftIRQ аналогично зарегистрированной на драйвер устройства функции poll. Она будет ограничена общим бюджетом и временным ограничением, как описано в предыдущей главе.
Настроим размер таблицы ограничений потоков:
$ sudo sysctl -w net.core.flow_limit_table_len=8192
По умолчанию значение равно 4096.
Это изменение влияет только на заново размещённые таблицы. Так что сначала настройте размер таблицы, а потом включайте ограничения потоков.
Для включения ограничений нужно задать битовую маску в /proc/sys/net/core/flow_limit_cpu_bitmap, аналогично маске RPS, которая показывает, на каких CPU включены ограничения потоков.
Backlog-очереди каждого CPU используют NAPI так же, как и драйвер устройства. Предоставляется функция poll, используемая для обработки пакетов из контекста SoftIRQ. Как и в случае с драйвером, здесь тоже применяется weight.
Структура NAPI предоставляется в ходе инициализации сетевой подсистемы.
Взято из net_dev_init в net/core/dev.c [82]:
sd->backlog.poll = process_backlog;
sd->backlog.weight = weight_p;
sd->backlog.gro_list = NULL;
sd->backlog.gro_count = 0;
NAPI-cтруктура backlog'а отличается от структуры драйвера возможностью настройки параметра weight: у драйвера его значение жёстко закодировано. Дальше мы рассмотрим процесс настройки веса.
Функция process_backlog — это цикл, выполняемый до тех пор, пока его вес (как описано в предыдущей главе) не будет израсходован или пока в backlog’е не останется больше данных.
Данные вынимаются по частям из backlog-очереди и передаются в __netif_receive_skb. Ветвь кода будет такой же, как и в случае с отключённым RPS. А именно, __netif_receive_skb выполняет те же процедуры перед вызовом __netif_receive_skb_core, чтобы передать данные на уровни протоколов.
process_backlog соблюдает тот же договор с NAPI, что и драйверы устройства: NAPI отключается, если не расходуется весь вес. Поллер перезапускается посредством вызова ____napi_schedule из enqueue_to_backlog, как описано выше.
Функция возвращает выполненную работу, которую затем net_rx_action (описано выше) вычтет из бюджета (настраивается с помощью net.core.netdev_budget, описано выше).
__netif_receive_skb_core выполняет трудную рабту по передаче данных в стеки протоколов. Перед этим она сначала проверяет, установлены ли какие-нибудь packet taps, которые ловят все входящие пакеты. Пример – семейство адресов AF_PACKET, обычно используемое посредством библиотеки libpcap [83].
Если имеется такой tap, до данные передаются сначала туда, а затем на уровни протоколов.
Передачу пакетов выполняет следующий код. Взято из net/core/dev.c [84]:
list_for_each_entry_rcu(ptype, &ptype_all, list) {
if (!ptype->dev || ptype->dev == skb->dev) {
if (pt_prev)
ret = deliver_skb(skb, pt_prev, orig_dev);
pt_prev = ptype;
}
}
Если вам интересно, как данные проходят через pcap, читайте net/packet/af_packet.c [85].
После того, как тапы будут удовлетворены, __netif_receive_skb_core передаёт данные на уровни протоколов. Для этого из данных извлекается поле протокола и выполняется итерирование по списку передающих функций (deliver functions), зарегистрированных для этого типа протокола.
Это можно посмотреть в __netif_receive_skb_core в net/core/dev.c [84]:
type = skb->protocol;
list_for_each_entry_rcu(ptype,
&ptype_base[ntohs(type) & PTYPE_HASH_MASK], list) {
if (ptype->type == type &&
(ptype->dev == null_or_dev || ptype->dev == skb->dev ||
ptype->dev == orig_dev)) {
if (pt_prev)
ret = deliver_skb(skb, pt_prev, orig_dev);
pt_prev = ptype;
}
}
Здесь идентификатор ptype_base определён как хэш-таблица списка в net/core/dev.c [86]:
struct list_head ptype_base[PTYPE_HASH_SIZE] __read_mostly;
Каждый уровень протокола добавляет фильтр в список заданного слота хэш-таблицы, вычисляемой вспомогательной функцией ptype_head:
static inline struct list_head *ptype_head(const struct packet_type *pt)
{
if (pt->type == htons(ETH_P_ALL))
return &ptype_all;
else
return &ptype_base[ntohs(pt->type) & PTYPE_HASH_MASK];
}
Добавление фильтра в список завершается вызовом dev_add_pack. Таким способом уровни протоколов регистрируют себя для передачи им данных.
Теперь вы знаете, как сетевые данные попадают из сетевой карты на уровень протокола.
Давайте рассмотрим, как уровни протоколов регистрируют сами себя. В этой статье мы ограничимся стеком протокола IP, поскольку он крайне широко используется и будет понятен большинству читателей.
Уровень протокола IP сам включает себя в хэш-таблицу ptype_base, и в предыдущих главах мы уже рассматривали данные, которые предаются на этот уровень с уровня сетевого устройства.
Это происходит в функции inet_init, взято из net/ipv4/af_inet.c [87]:
dev_add_pack(&ip_packet_type);
Здесь регистрируется структура типа IP-пакета, определённая в <a href="https://github.com/torvalds/linux/blob/v3.13/net/ipv4/af_inet.c#L1673-L1676">net/ipv4/af_inet.c</a>:
static struct packet_type ip_packet_type __read_mostly = {
.type = cpu_to_be16(ETH_P_IP),
.func = ip_rcv,
};
__netif_receive_skb_core вызывает deliver_skb (как мы видели в предыдущей главе), которая вызывает func (в данном случае – ip_rcv).
На высоком уровне функция ip_rcv весьма незатейлива. В ней есть несколько проверок целостности, чтобы удостовериться в валидности данных, а также дёргаются счётчики статистики.
ip_rcv завершается передачей пакета в ip_rcv_finish посредством netfilter [88]. Это делается так, чтобы любое правило iptables [89], которое должно быть соблюдено на уровне протокола IP, могло проверить пакет, прежде чем он отправится дальше.
Код, передающий данные через netfilter в конце ip_rcv в net/ipv4/ip_input.c [90]:
return NF_HOOK(NFPROTO_IPV4, NF_INET_PRE_ROUTING, skb, dev, NULL, ip_rcv_finish);
Ради краткости изложения я решил пропустить подробное рассмотрение netfilter, iptables и conntrack.
Короткая версия: NF_HOOK_THRESH проверяет, установлены ли какие-нибудь фильтры и пытается вернуть исполнение обратно на уровень протокола IP, чтобы не углубляться в netfilter и всё, что под ним, вроде iptables и conntrack.
Помните: если у вас много правил netfilter или iptables, или они слишком сложны, то эти правила будут исполняться в контексте SoftIRQ, а это риск возникновения задержек в сетевом стеке. Возможно, вы этого и не избежите, если вам нужен конкретный набор установленных правил.
После того, как netfilter получает возможность взглянуть на данные и решить, что с ними делать, вызывается ip_rcv_finish. Конечно, если только данные не отбрасываются netfilter'ом.
ip_rcv_finish начинается с оптимизации. Чтобы доставить пакет в нужное место, должна быть готова структура dst_entry от системы маршрутизации. Для её получения код сначала пытается вызвать функцию early_demux из протокола более высокого уровня, чем тот, для которого предназначены эти данные.
early_demux — это оптимизация [91], в ходе которой мы пытаемся найти необходимую для доставки пакета структуру dst_entry. Для этого проверяется, не закэширована ли dst_entry в структуре сокета.
Вот как это выглядит, взято из net/ipv4/ip_input.c [92]:
if (sysctl_ip_early_demux && !skb_dst(skb) && skb->sk == NULL) {
const struct net_protocol *ipprot;
int protocol = iph->protocol;
ipprot = rcu_dereference(inet_protos[protocol]);
if (ipprot && ipprot->early_demux) {
ipprot->early_demux(skb);
/* нужно перезагрузить iph, skb->head могла измениться */
iph = ip_hdr(skb);
}
}
Как видите, этот код защищается sysctl_ip_early_demux. По умолчанию включена early_demux. Из следующей главы вы узнаете о том, как её отключить и почему вы можете захотеть это сделать.
Если оптимизация включена и в запись не закэширована (потому что это первый прибывающий пакет), то пакет будет передан системе маршрутизации ядра, где dst_entry будет вычислена и присвоена.
После завершение слоя маршрутизации обновляются счётчики статистики, а функция завершается вызовом dst_input(skb). Та, в свою очередь, вызывает указатель функции ввода на структуру пакета dst_entry, приписанную системой маршрутизации.
Если пункт назначения пакета — локальная система, то система маршрутизации прикрепит функцию ip_local_deliver к указателю функции ввода в структуре пакета dst_entry.
Отключаем оптимизацию early_demux:
$ sudo sysctl -w net.ipv4.ip_early_demux=0
Значение по умолчанию равно 1; early_demux включена.
В ряде случаев эта sysctl примерно на 5% ухудшает пропускную способность [93] с оптимизацией early_demux.
Вспоминаем следующий шаблон на уровне протокола IP:
В случае с ip_local_deliver используется тот же шаблон. Взято из net/ipv4/ip_input.c [94]:
/*
* Доставляет IP-пакеты на более высокие уровни протоколов.
*/
int ip_local_deliver(struct sk_buff *skb)
{
/*
* Пересобирает IP-пакеты.
*/
if (ip_is_fragment(ip_hdr(skb))) {
if (ip_defrag(skb, IP_DEFRAG_LOCAL_DELIVER))
return 0;
}
return NF_HOOK(NFPROTO_IPV4, NF_INET_LOCAL_IN, skb, skb->dev, NULL,
ip_local_deliver_finish);
}
После того, как netfilter получает возможность взглянуть на данные, вызывается ip_local_deliver_finish. Конечно, если только данные не отброшены netfilter'ом.
ip_local_deliver_finish извлекает из пакета протокол, ищет зарегистрированную на этот протокол структуру net_protocol и вызывает функцию, на которую указывает handler в этой структуре.
Таким образом пакет передаётся на более высокий уровень протокола.
Читаем /proc/net/snmp, чтобы мониторить подробную статистику протокола IP:
$ cat /proc/net/snmp
Ip: Forwarding DefaultTTL InReceives InHdrErrors InAddrErrors ForwDatagrams InUnknownProtos InDiscards InDelivers OutRequests OutDiscards OutNoRoutes ReasmTimeout ReasmReqds ReasmOKs ReasmFails FragOKs FragFails FragCreates
Ip: 1 64 25922988125 0 0 15771700 0 0 25898327616 22789396404 12987882 51 1 10129840 2196520 1 0 0 0
...
Этот файл содержит статистику по нескольким уровням протоколов. Первым идёт уровень IP. В первой строке представлены разделённые пробелами имена соответствующих значений из второй строки.
На уровне протокола IP можно найти ряд счётчиков статистики, которые используются в С-перечислении. Все валидные enum-значения и имена полей в /proc/net/snmp, которым они соответствуют, можно найти в include/uapi/linux/snmp.h [95]:
enum
{
IPSTATS_MIB_NUM = 0,
/* часто записываемые напрямую поля, хранятся в той же кэш-строке */
IPSTATS_MIB_INPKTS, /* InReceives */
IPSTATS_MIB_INOCTETS, /* InOctets */
IPSTATS_MIB_INDELIVERS, /* InDelivers */
IPSTATS_MIB_OUTFORWDATAGRAMS, /* OutForwDatagrams */
IPSTATS_MIB_OUTPKTS, /* OutRequests */
IPSTATS_MIB_OUTOCTETS, /* OutOctets */
/* ... */
Читаем /proc/net/netstat, чтобы мониторить расширенную статистику протокола IP:
$ cat /proc/net/netstat | grep IpExt
IpExt: InNoRoutes InTruncatedPkts InMcastPkts OutMcastPkts InBcastPkts OutBcastPkts InOctets OutOctets InMcastOctets OutMcastOctets InBcastOctets OutBcastOctets InCsumErrors InNoECTPkts InECT0Pktsu InCEPkts
IpExt: 0 0 0 0 277959 0 14568040307695 32991309088496 0 0 58649349 0 0 0 0 0
Формат аналогичен /proc/net/snmp, за исключением строк с префиксом IpExt.
Некоторые любопытные метрики:
Все эти счётчики инкрементируются в специфических местах уровня IP. Время от времени код проходит по ним, и могут возникать ошибки двойного подсчёта и прочие баги. Если для вас важна эта статистика, то очень рекомендую изучить исходники этих метрик на уровне протокола IP, чтобы понимать, как они инкрементируются.
Здесь мы рассмотрим UDP, но обработчик протокола TCP регистрируется так же и в то же время, что и обработчик протокола UDP.
В net/ipv4/af_inet.c можно найти определения структур, которые содержат функции-обработчики для подключения протоколов UDP, TCP и ICMP к уровню протокола IP. Взято из net/ipv4/af_inet.c [96]:
static const struct net_protocol tcp_protocol = {
.early_demux = tcp_v4_early_demux,
.handler = tcp_v4_rcv,
.err_handler = tcp_v4_err,
.no_policy = 1,
.netns_ok = 1,
};
static const struct net_protocol udp_protocol = {
.early_demux = udp_v4_early_demux,
.handler = udp_rcv,
.err_handler = udp_err,
.no_policy = 1,
.netns_ok = 1,
};
static const struct net_protocol icmp_protocol = {
.handler = icmp_rcv,
.err_handler = icmp_err,
.no_policy = 1,
.netns_ok = 1,
};
Эти структуры регистрируются в коде инициализации семейства адресов inet. Взято из net/ipv4/af_inet.c [97]:
/*
* Добавляем все базовые протоколы.
*/
if (inet_add_protocol(&icmp_protocol, IPPROTO_ICMP) < 0)
pr_crit("%s: Cannot add ICMP protocoln", __func__);
if (inet_add_protocol(&udp_protocol, IPPROTO_UDP) < 0)
pr_crit("%s: Cannot add UDP protocoln", __func__);
if (inet_add_protocol(&tcp_protocol, IPPROTO_TCP) < 0)
pr_crit("%s: Cannot add TCP protocoln", __func__);
Рассмотрим уровень протокола UDP. Как мы видели выше, функция handler для UDP вызывается udp_rcv. Это точка входа на уровень UPD, куда передаются данные с уровня IP.
Код уровня протокола UDP можно найти здесь: net/ipv4/udp.c [98].
Код функции udp_rcv состоит из одной строки, в которой напрямую вызывается функция __udp4_lib_rcv для обработки принимаемых датаграмм.
Функция __udp4_lib_rcv удостоверяется в валидности пакета и извлекает заголовок UDP, длину UDP-датаграммы, адрес источник и адрес пункта назначения. Далее выполняются проверки целостности и контрольной суммы.
Вспоминаем, что в части, посвящённой уровню протокола IP, мы видели оптимизацию, в результате которой dst_entry прикрепляется к пакету до того, как он передаётся на более высокий уровень (в нашем случае — UDP).
Если найден сокет и соответствующая dst_entry, то функция __udp4_lib_rcv помещает пакет в очередь сокета:
sk = skb_steal_sock(skb);
if (sk) {
struct dst_entry *dst = skb_dst(skb);
int ret;
if (unlikely(sk->sk_rx_dst != dst))
udp_sk_rx_dst_set(sk, dst);
ret = udp_queue_rcv_skb(sk, skb);
sock_put(sk);
/* возвращаемое значение > 0 означает повторный ввод данных,
* но он хочет вернуть –protocol или 0
*/
if (ret > 0)
return -ret;
return 0;
} else {
Если от операции early_demux не осталось прикреплённого принимающего сокета, то он ищется путём вызова __udp4_lib_lookup_skb.
В обоих случаях датаграмма будет помещена в очередь сокета:
ret = udp_queue_rcv_skb(sk, skb);
sock_put(sk);
Если сокет так и не находится, то датаграмма будет отброшена:
/* Сокета нет. Пакет тихо отбрасывается, если контрольная сумма ошибочна */
if (udp_lib_checksum_complete(skb))
goto csum_error;
UDP_INC_STATS_BH(net, UDP_MIB_NOPORTS, proto == IPPROTO_UDPLITE);
icmp_send(skb, ICMP_DEST_UNREACH, ICMP_PORT_UNREACH, 0);
/*
* Хм. Мы получили UDP-пакет на порт, который
* не хотим прослушивать. Игнорируем его.
*/
kfree_skb(skb);
return 0;
Начальная часть функции:
Наконец-то мы добрались до логики очереди приёма. Начинается она с проверки заполненности очереди для сокета. Взято из net/ipv4/udp.c [100]:
if (sk_rcvqueues_full(sk, skb, sk->sk_rcvbuf))
goto drop;
Функция sk_rcvqueues_full проверяет длину backlog’а и sk_rmem_alloc сокета, чтобы определить, не превышает ли сумма их длин размер sk_rcvbuf для сокета (sk->sk_rcvbuf в вышеприведённом примере):
/*
* Учитывает размер очереди приёма и backlog-очереди.
* Не учитывает этот skb truesize,
* чтобы мог прибыть даже одиночный большой пакет.
*/
static inline bool sk_rcvqueues_full(const struct sock *sk, const struct sk_buff *skb,
unsigned int limit)
{
unsigned int qsize = sk->sk_backlog.len + atomic_read(&sk->sk_rmem_alloc);
return qsize > limit;
}
Эти значения настраиваются довольно хитро, и можно много чего подкрутить.
Значение sk->sk_rcvbuf (вызываемое ограничение в sk_rcvqueues_full) можно увеличить до любого уровня в пределах sysctl net.core.rmem_max.
Увеличим максимальный размер буфера приёма:
$ sudo sysctl -w net.core.rmem_max=8388608
sk->sk_rcvbuf начинается со значения net.core.rmem_default, которое тоже можно настроить с помощью sysctl.
Настроим исходный размер буфера приёма по умолчанию:
$ sudo sysctl -w net.core.rmem_default=8388608
Также можно настроить размер sk->sk_rcvbuf, посредством вызова setsockopt [101] из вашего приложения и передачи SO_RCVBUF. Максимальное значение setsockopt не превышает net.core.rmem_max.
Зато можно превысить ограничение net.core.rmem_max, вызвав setsockopt и передав SO_RCVBUFFORCE. Но пользователю, у которого выполняется приложение, потребуется возможность CAP_NET_ADMIN.
Значение sk->sk_rmem_alloc инкрементируется с помощью вызовов skb_set_owner_r, которые задают владеющий датаграммой сокет. Позднее мы ещё столкнёмся с этим на уровне UDP.
Значение sk->sk_backlog.len инкрементируется с помощью вызовов sk_add_backlog.
После проверки заполненности очереди продолжается процедура помещения датаграммы в очередь. Взято из net/ipv4/udp.c [102]:
bh_lock_sock(sk);
if (!sock_owned_by_user(sk))
rc = __udp_queue_rcv_skb(sk, skb);
else if (sk_add_backlog(sk, skb, sk->sk_rcvbuf)) {
bh_unlock_sock(sk);
goto drop;
}
bh_unlock_sock(sk);
return rc;
Сначала выясняется, есть ли в данный момент системные вызовы к сокету из программы пользовательского пространства. Если нет, то датаграмма может быть добавлена в очередь приёма с помощью вызова __udp_queue_rcv_skb. Если есть, то датаграмма помещается в backlog-очередь с помощью вызова sk_add_backlog.
Датаграммы добавляются в очередь приёма из backlog’а, когда системные вызовы освобождают сокет с помощью вызова release_sock в ядре.
Функция __udp_queue_rcv_skb добавляет датаграммы в очередь приёма с помощью вызова sock_queue_rcv_skb. А если датаграмму нельзя добавить в очередь приёма сокета, то __udp_queue_rcv_skb дёргает счётчики статистики.
Взято из net/ipv4/udp.c [103]:
rc = sock_queue_rcv_skb(sk, skb);
if (rc < 0) {
int is_udplite = IS_UDPLITE(sk);
/* Обратите внимание, что ошибка ENOMEM выдаётся дважды */
if (rc == -ENOMEM)
UDP_INC_STATS_BH(sock_net(sk), UDP_MIB_RCVBUFERRORS,is_udplite);
UDP_INC_STATS_BH(sock_net(sk), UDP_MIB_INERRORS, is_udplite);
kfree_skb(skb);
trace_udp_fail_queue_rcv_skb(rc, sk);
return -1;
}
Два очень полезных файла для получения статистики по протоколу UDP:
Читаем /proc/net/snmp, чтобы мониторить подробную статистику протокола UDP.
$ cat /proc/net/snmp | grep Udp:
Udp: InDatagrams NoPorts InErrors OutDatagrams RcvbufErrors SndbufErrors
Udp: 16314 0 0 17161 0 0
Подобно детальной статистике по протоколу IP, которая имеется в этом файле, вам нужно будет почитать исходники уровня протокола, чтобы точно определить, когда и где инкрементируются эти значения.
InDatagrams: инкрементируется, когда:
NoPorts: инкрементируется, когда UDP-пакеты прибывают на определённый порт, который не слушает ни одна программа.
InErrors: инкрементируется, если:
OutDatagrams: инкрементируется, когда UDP-пакет безошибочно передаётся вниз, на уровень протокола IP для последующей отправки.
RcvbufErrors: инкрементируется, когда sock_queue_rcv_skb сообщает об отсутствии доступной памяти; такое случается, если sk->sk_rmem_alloc больше или равно sk->sk_rcvbuf.
SndbufErrors: инкрементируется, если:
InCsumErrors: инкрементируется, когда обнаруживается сбой контрольной суммы UDP. Обратите внимание, что во всех случаях, с которыми я сталкивался, InCsumErrors инкрементируется одновременно с InErrors. Следовательно, связка InErrors — InCsumErros должна отражать количество ошибок памяти.
Читаем /proc/net/udp, чтобы мониторить подробную статистику сокета UDP.
$ cat /proc/net/udp
sl local_address rem_address st tx_queue rx_queue tr tm->when retrnsmt uid timeout inode ref pointer drops
515: 00000000:B346 00000000:0000 07 00000000:00000000 00:00000000 00000000 104 0 7518 2 0000000000000000 0
558: 00000000:0371 00000000:0000 07 00000000:00000000 00:00000000 00000000 0 0 7408 2 0000000000000000 0
588: 0100007F:038F 00000000:0000 07 00000000:00000000 00:00000000 00000000 0 0 7511 2 0000000000000000 0
769: 00000000:0044 00000000:0000 07 00000000:00000000 00:00000000 00000000 0 0 7673 2 0000000000000000 0
812: 00000000:006F 00000000:0000 07 00000000:00000000 00:00000000 00000000 0 0 7407 2 0000000000000000 0
Первая строка описывает каждое из полей из последующих строк:
Отображающий всё это код можно найти в net/ipv4/udp.c [104].
Сетевые данные помещаются в очередь сокета с помощью вызова sock_queue_rcv. Прежде чем добавить датаграмму в очередь, эта функция делает несколько вещей:
Таким образом данные прибывают в систему и проходят по сетевому стеку, пока не достигают сокета. Теперь они готовы к использованию пользовательской программой.
Нужно упомянуть о нескольких дополнительных вещах, о которых не было повода рассказать выше.
Я уже писал, что сетевой стек может собирать временные метки входящих данных. В sysctl есть значения, совместно с RPS позволяющие контролировать момент и способ сбора меток. Подробнее об этом читайте в главах, посвящённых RPS и временным меткам. Некоторые сетевые карты даже поддерживают метки аппаратно.
Это полезная возможность, если вы захотите определить размер задержки, добавляемой к получаемым пакетам сетевым стеком ядра.
В документации ядра прекрасно освещён вопрос [105] присваивания временных меток, туда даже включена программа-образец и сборочный файл [106]!
Определяем, какие режимы временных меток поддерживают ваши драйвер и устройство:
$ sudo ethtool -T eth0
Time stamping parameters for eth0:
Capabilities:
software-transmit (SOF_TIMESTAMPING_TX_SOFTWARE)
software-receive (SOF_TIMESTAMPING_RX_SOFTWARE)
software-system-clock (SOF_TIMESTAMPING_SOFTWARE)
PTP Hardware Clock: none
Hardware Transmit Timestamp Modes: none
Hardware Receive Filter Modes: none
К сожалению, эта сетевая карта не поддерживает временные метки аппаратно, но ещё можно использовать их программно. Они помогут определять, какую задержку ядро добавляет к моему пути прохождения пакетов.
Можно использовать опцию сокета, называющуюся SO_BUSY_POLL. Она заставляет ядро применять к новым данным нагруженный поллинг, когда завершается блокирующий приём и нет данных.
ВАЖНОЕ ЗАМЕЧАНИЕ: чтобы эта опция работала, она должна поддерживаться вашим драйвером устройства. Драйвер igb ядра 3.13.0 её не поддерживает. А ixgbe — поддерживает. Если ваш драйвер имеет функцию, настроенную в поле ndo_busy_poll структуры struct net_device_ops (упоминалась выше), то он поддерживает SO_BUSY_POLL.
У Intel прекрасно описано [107], как это работает и как это использовать.
При использовании этой опции для одного сокета, нужно в очередь приёма драйвера устройства передавать для новых данных значение времени в микросекундах в качестве объёма времени для поллинга. Когда после настройки этого значения выполняется блокирующее чтение сокета, ядро будет применять поллинг к новым данным.
Также можно настроить в sysctl значение net.core.busy_poll — как долго вызовы с poll или select должны ожидать прибытия новых данных в условиях нагруженного поллинга(в микросекундах).
Эта опция позволяет снизить задержку, но увеличивает нагрузку на CPU и потребление энергии.
Ядро Linux предоставляет способ использовать драйверы устройств для отправки и приёма данных на сетевой карте, если ядро падает. API для этой функциональности называется Netpoll. Он используется разными системами, но особенно kgdb [108] и netconsole [109].
Netpoll поддерживается большинство драйверов. Ваш драйвер должен реализовать функцию ndo_poll_controller и прикрепить её к структуре struct net_device_ops, регистрируемой во время probe.
Когда подсистема сетевого устройства выполняет операции со входящими и исходящими данными, то сначала проверяется система Netpoll, чтобы определить, не предназначен ли пакет для неё.
Следующий код можно найти в __netif_receive_skb_core из net/dev/core.c [110]:
static int __netif_receive_skb_core(struct sk_buff *skb, bool pfmemalloc)
{
/* ... */
/* если мы попали сюда через NAPI, проверяем netpoll */
if (netpoll_receive_skb(skb))
goto out;
/* ... */
}
Проверки Netpoll довольно рано выполняются в большинстве подсистем сетевых устройств Linux, работающих с передачей или приёмом сетевых данных.
Потребители Netpoll API могут зарегистрировать структуры struct netpoll с помощью вызова netpoll_setup. Эти структуры содержат указатели функций для прикрепления хуков, а API экспортирует функцию для отправки данных.
Если вас интересует использование Netpoll API, то изучите драйвер netconsole [111], заголовочный файл Netpoll API, ‘include/linux/netpoll.h` [112] и этот прекрасный текст [113].
Флаг SO_INCOMING_CPU отсутствовал в Linux вплоть до версии 3.19, но он достаточно полезен, поэтому я его упомяну.
Для определения, какой CPU обрабатывает сетевые пакеты для конкретного сокета, можно использовать getsockopt и опцию SO_INCOMING_CPU. Тогда ваше приложение сможет использовать эту информацию для передачи сокетов тредам, выполняющимся на нужном CPU. Это поможет улучшить локальность данных и частоту попадания в кэш CPU.
Короткий пример архитектуры, когда эта опция полезна, приведён здесь: patchwork.ozlabs.org/patch/408257 [114].
Движок DMA [26] — это аппаратная часть, позволяющая освобождать CPU от больших операций копирования, которые перекладываются на другое железо. Так что включение использования движка DMA и запуск кода, использующего его преимущества, должно привести к снижению нагрузки на CPU.
Ядро Linux содержит обощённый интерфейс движка DMA, который может использоваться авторами драйверов движка. Подробнее об этом интерфейсе можно почитать в документации к ядру [115].
Ядро поддерживает несколько движков DMA, но мы будем говорить об одном из самых распространённых — Intel IOAT DMA engine [116].
Многие серверы содержат пакет Intel I/O AT [117], который вносит в производительность ряд изменений. Одно из них — использование аппаратного движка DMA. Проверьте выходные данные своего dmesg для ioatdma чтобы определить, загружен ли модуль и нашёл ли он поддерживаемое оборудование. Движок DMA используется в ряде мест, но особенно активно — в стеке TCP.
Поддержка движка Intel IOAT была включена в Linux 2.6.18, но в 3.13.11.10 от неё отказались из-за неожиданных багов, повреждавших данные [118]. Пользователи ядер до версии 3.13.11.10 могут по умолчанию использовать модуль ioatdma. Возможно, в будущих релизах его пофиксят.
Другая интересная функция, идущая в пакете Intel I/O AT [117] — Direct Cache Access (DCA).
Она позволяет сетевым устройствам (через их драйверы) помещать сетевые данные напрямую в кэш CPU. Конкретная реализация зависит от драйвера. В случае с igb можете посмотреть код функции igb_update_dca [119], а также igb_update_rx_dca [120]. Драйвер igb использует DCA при записи значений регистров в сетевую карту.
Чтобы использовать DCA, вам нужно включить её в BIOS, проверить, загружен ли модуль dca и поддерживают ли её ваша сетевая карта и драйвер.
Если, несмотря на риск повреждения данных, вы используете модуль ioatdma, то можете мониторить его через некоторые записи в sysfs.
Мониторим общее количество разгруженных операций memcpy в DMA-канале:
$ cat /sys/class/dma/dma0chan0/memcpy_count
123205655
Мониторим общее количество байтов, переданных через DMA-канал:
$ cat /sys/class/dma/dma0chan0/bytes_transferred
131791916307
Движок IOAT DMA используется только тогда, когда размер пакета превышает определённый порог — copybreak. Эта проверка нужна потому, что для маленьких копий накладные расходы при установке и использовании движка DMA не покрывают выгоды от ускорения.
Настроим copybreak для движка DMA:
$ sudo sysctl -w net.ipv4.tcp_dma_copybreak=2048
Значение по умолчанию равно 4096.
Сетевой стек Linux довольно сложный. Без глубокого понимания происходящих процессов вы не сможете мониторить или настроить его (как и любое другое сложное ПО). На просторах интернета вы можете встретить примеры sysctl.conf, содержащие наборы значений, которые предлагается скопировать и вставить на вашем компьютере. Это не лучший способ оптимизации своего сетевого стека.
Мониторинг сетевого стека требует аккуратного управления сетевыми данными на каждом уровне, начиная с драйверов. Тогда вы сможете определять, где происходят отбрасывания и возникают ошибки, после чего производить настройку, уменьшая эти вредные эффекты.
К сожалению, лёгкого пути тут нет.
Автор: Mail.Ru Group
Источник [121]
Сайт-источник PVSM.RU: https://www.pvsm.ru
Путь до страницы источника: https://www.pvsm.ru/optimizatsiya/212124
Ссылки в тексте:
[1] иллюстрированным руководством на ту же тему: http://blog.packagecloud.io/eng/2016/10/11/monitoring-tuning-linux-networking-stack-receiving-data-illustrated/
[2] Общий совет по мониторингу и настройке сетевого стека Linux: #1
[3] Обзор проблематики: #2
[4] Подробный разбор: #3
[5] Драйвер сетевого устройства: #4
[6] SoftIRQ: #5
[7] Подсистема сетевого устройства в Linux: #6
[8] Механизм управления принимаемыми пакетами (Receive Packet Steering (RPS)): #7
[9] Механизм управления принимаемыми потоками (Receive Flow Steering (RFS)): #8
[10] Аппаратно ускоренное управление принимаемыми потоками (Accelerated Receive Flow Steering (aRFS)): #9
[11] Повышение (moving up) сетевого стека с помощью netif_receive_skb: #10
[12] netif_receive_skb: #11
[13] Регистрация уровня протокола: #12
[14] Дополнительная информация: #13
[15] Заключение: #14
[16] отсюда: http://www.intel.com/content/dam/www/public/us/en/documents/datasheets/ethernet-controller-i350-datasheet.pdf
[17] NAPI: http://www.linuxfoundation.org/collaborate/workgroups/networking/napi
[18] drivers/net/ethernet/intel/igb/igb_main.c: https://github.com/torvalds/linux/blob/v3.13/drivers/net/ethernet/intel/igb/igb_main.c#L676-L697
[19] PCI express: https://ru.wikipedia.org/wiki/PCI_Express
[20] конфигурационном пространстве PCI: https://ru.wikipedia.org/wiki/%D0%9A%D0%BE%D0%BD%D1%84%D0%B8%D0%B3%D1%83%D1%80%D0%B0%D1%86%D0%B8%D0%BE%D0%BD%D0%BD%D0%BE%D0%B5_%D0%BF%D1%80%D0%BE%D1%81%D1%82%D1%80%D0%B0%D0%BD%D1%81%D1%82%D0%B2%D0%BE_PCI#.D0.A1.D1.82.D0.B0.D0.BD.D0.B4.D0.B0.D1.80.D1.82.D0.B8.D0.B7.D0.B8.D1.80.D0.BE.D0.B2.D0.B0.D0.BD.D0.BD.D1.8B.D0.B5_.D1.80.D0.B5.D0.B3.D0.B8.D1.81.D1.82.D1.80.D1.8B
[21] include/module.h: https://github.com/torvalds/linux/blob/v3.13/include/linux/module.h#L145-L146
[22] drivers/net/ethernet/intel/igb/igb_main.c: https://github.com/torvalds/linux/blob/v3.13/drivers/net/ethernet/intel/igb/igb_main.c#L79-L117
[23] drivers/net/ethernet/intel/igb/e1000_hw.h: https://github.com/torvalds/linux/blob/v3.13/drivers/net/ethernet/intel/igb/e1000_hw.h#L41-L75
[24] drivers/net/ethernet/intel/igb/igb_main.c: https://github.com/torvalds/linux/blob/v3.13/drivers/net/ethernet/intel/igb/igb_main.c#L238-L249
[25] портов ввода-вывода: http://wiki.osdev.org/I/O_Ports
[26] DMA: https://ru.wikipedia.org/wiki/%D0%9F%D1%80%D1%8F%D0%BC%D0%BE%D0%B9_%D0%B4%D0%BE%D1%81%D1%82%D1%83%D0%BF_%D0%BA_%D0%BF%D0%B0%D0%BC%D1%8F%D1%82%D0%B8
[27] igb_probe: https://github.com/torvalds/linux/blob/v3.13/drivers/net/ethernet/intel/igb/igb_main.c#L2005-L2429
[28] drivers/net/ethernet/intel/igb/igb_main.c: https://github.com/torvalds/linux/blob/v3.13/drivers/net/ethernet/intel/igb/igb_main.c#L2038-L2059
[29] PCI Express Advanced Error Reporting: https://github.com/torvalds/linux/blob/v3.13/Documentation/PCI/pcieaer-howto.txt
[30] free-electrons.com/doc/pci-drivers.pdf: http://free-electrons.com/doc/pci-drivers.pdf
[31] wiki.osdev.org/PCI: http://wiki.osdev.org/PCI
[32] github.com/torvalds/linux/blob/v3.13/Documentation/PCI/pci.txt: https://github.com/torvalds/linux/blob/v3.13/Documentation/PCI/pci.txt
[33] drivers/net/ethernet/intel/igb/igb_main.c: https://github.com/torvalds/linux/blob/v3.13/drivers/net/ethernet/intel/igb/igb_main.c#L2090
[34] drivers/net/ethernet/intel/igb/igb_main.c: https://github.com/torvalds/linux/blob/v3.13/drivers/net/ethernet/intel/igb/igb_main.c#L1905-L1913
[35] ethtool: https://www.kernel.org/pub/software/network/ethtool/
[36] ioctl: http://man7.org/linux/man-pages/man2/ioctl.2.html
[37] drivers/net/ethernet/intel/igb/igb_ethtool.c: https://github.com/torvalds/linux/blob/v3.13/drivers/net/ethernet/intel/igb/igb_ethtool.c
[38] drivers/net/ethernet/intel/igb/igb_ethtool.c: https://github.com/torvalds/linux/blob/v3.13/drivers/net/ethernet/intel/igb/igb_ethtool.c#L3012-L3015
[39] drivers/net/ethernet/intel/igb/igb_ethtool.c: https://github.com/torvalds/linux/blob/v3.13/drivers/net/ethernet/intel/igb/igb_ethtool.c#L2970-L2979
[40] прерывание: https://ru.wikipedia.org/wiki/%D0%9F%D1%80%D0%B5%D1%80%D1%8B%D0%B2%D0%B0%D0%BD%D0%B8%D0%B5
[41] MSI-X: https://ru.wikipedia.org/wiki/Message_Signaled_Interrupts#MSI-X
[42] drivers/net/ethernet/intel/igb/igb_main.c: https://github.com/torvalds/linux/blob/v3.13/drivers/net/ethernet/intel/igb/igb_main.c#L1145-L1271
[43] Receive Side Scaling (RSS): https://en.wikipedia.org/wiki/Network_interface_controller#RSS
[44] igb_setup_all_rx_resources: https://github.com/torvalds/linux/blob/v3.13/drivers/net/ethernet/intel/igb/igb_main.c#L2801-L2804
[45] github.com/torvalds/linux/blob/v3.13/Documentation/DMA-API-HOWTO.txt: https://github.com/torvalds/linux/blob/v3.13/Documentation/DMA-API-HOWTO.txt
[46] drivers/net/ethernet/intel/igb/igb_main.c: https://github.com/torvalds/linux/blob/v3.13/drivers/net/ethernet/intel/igb/igb_main.c#L2833-L2834
[47] Википедии: https://en.wikipedia.org/wiki/Message_Signaled_Interrupts
[48] drivers/net/ethernet/intel/igb/igb_main.c: https://github.com/torvalds/linux/blob/v3.13/drivers/net/ethernet/intel/igb/igb_main.c#L1360-L1413
[49] Intel Ethernet Flow Director: http://www.intel.com/content/www/us/en/ethernet-products/ethernet-flow-director-video.html
[50] kernel/softirq.c: https://github.com/torvalds/linux/blob/v3.13/kernel/softirq.c#L743-L758
[51] kernel/smpboot.c: https://github.com/torvalds/linux/blob/v3.13/kernel/smpboot.c#L94-L163
[52] receive offload: https://en.wikipedia.org/wiki/Large_receive_offload
[53] Receive packet steering: https://lwn.net/Articles/362339/
[54] drivers/net/ethernet/intel/igb/igb_main.c: https://github.com/torvalds/linux/blob/v3.13/drivers/net/ethernet/intel/igb/igb_main.c#L5148-L5158
[55] net/core/dev.c: https://github.com/torvalds/linux/blob/v3.13/net/core/dev.c#L4154-L4168
[56] net/core/dev.c: https://github.com/torvalds/linux/blob/v3.13/net/core/dev.c#L2914-L2920
[57] метод: https://en.wikipedia.org/wiki/Interrupt_coalescing
[58] штормов прерываний: https://en.wikipedia.org/wiki/Interrupt_storm
[59] include/uapi/linux/ethtool.h: https://github.com/torvalds/linux/blob/v3.13/include/uapi/linux/ethtool.h#L184-L255
[60] net/core/dev.c: https://github.com/torvalds/linux/blob/v3.13/net/core/dev.c#L4300-L4309
[61] jiffies: http://elinux.org/Kernel_Timer_Systems#Timer_Wheel.2C_Jiffies_and_HZ_.28or.2C_the_way_it_was.29
[62] net/core/dev.c: https://github.com/torvalds/linux/blob/v3.13/net/core/dev.c#L4322-L4338
[63] net/core/dev.c: https://github.com/torvalds/linux/blob/v3.13/net/core/dev.c#L4342-L4363
[64] timer tick rate: http://www.makelinux.net/books/lkd2/ch10lev1sec2
[65] net/core/dev.c: https://github.com/torvalds/linux/blob/v3.13/net/core/dev.c#L4380-L4383
[66] drivers/net/ethernet/intel/igb/igb_main.c: https://github.com/torvalds/linux/blob/v3.13/drivers/net/ethernet/intel/igb/igb_main.c#L5987-L6018
[67] Direct Cache Access (DCA): https://lwn.net/Articles/247493/
[68] net/core/net-procfs.c: https://github.com/torvalds/linux/blob/v3.13/net/core/net-procfs.c#L161-L165
[69] межпроцессорного прерывания: https://en.wikipedia.org/wiki/Inter-processor_interrupt
[70] Receive Packet Steering: https://lwn.net/Articles/362339
[71] сетевого потока: https://en.wikipedia.org/wiki/Traffic_flow_%28computer_networking%29
[72] net/core/dev.c: https://github.com/torvalds/linux/blob/v3.13/net/core/dev.c#L3844-L3856
[73] net/core/dev.c: https://github.com/torvalds/linux/blob/v3.13/net/core/dev.c#L3862-L3872
[74] net/core/dev.c: https://github.com/torvalds/linux/blob/v3.13/net/core/dev.c#L3877-L3886
[75] Receive Packet Steering (RPS): https://github.com/torvalds/linux/blob/v3.13/Documentation/networking/scaling.txt#L99-L222
[76] документации к ядру: https://github.com/torvalds/linux/blob/v3.13/Documentation/networking/scaling.txt#L138-L164
[77] документации: https://github.com/torvalds/linux/blob/v3.13/Documentation/networking/scaling.txt#L160-L164
[78] net/core/dev.c: https://github.com/torvalds/linux/blob/v3.13/net/core/dev.c#L3699-L3705
[79] net/core/dev.c: https://github.com/torvalds/linux/blob/v3.13/net/core/dev.c#L3199-L3200
[80] net/core/dev.c: https://github.com/torvalds/linux/blob/v3.13/net/core/dev.c#L3201-L3218
[81] ограничение: https://github.com/torvalds/linux/blob/v3.13/Documentation/networking/scaling.txt#L166-L188
[82] net/core/dev.c: https://github.com/torvalds/linux/blob/v3.13/net/core/dev.c#L6952-L6955
[83] libpcap: http://www.tcpdump.org/manpages/pcap.3pcap.html
[84] net/core/dev.c: https://github.com/torvalds/linux/blob/v3.13/net/core/dev.c#L3548-L3554
[85] net/packet/af_packet.c: https://github.com/torvalds/linux/blob/v3.13/net/packet/af_packet.c
[86] net/core/dev.c: https://github.com/torvalds/linux/blob/v3.13/net/core/dev.c#L146
[87] net/ipv4/af_inet.c: https://github.com/torvalds/linux/blob/v3.13/net/ipv4/af_inet.c#L1788
[88] netfilter: https://ru.wikipedia.org/wiki/Netfilter
[89] iptables: https://ru.wikipedia.org/wiki/Iptables
[90] net/ipv4/ip_input.c: https://github.com/torvalds/linux/blob/v3.13/net/ipv4/ip_input.c#L453-L454
[91] оптимизация: https://patchwork.ozlabs.org/patch/280718/
[92] net/ipv4/ip_input.c: https://github.com/torvalds/linux/blob/v3.13/net/ipv4/ip_input.c#L317-L327
[93] примерно на 5% ухудшает пропускную способность: https://patchwork.ozlabs.org/patch/166441/
[94] net/ipv4/ip_input.c: https://github.com/torvalds/linux/blob/v3.13/net/ipv4/ip_input.c#L241-L258
[95] include/uapi/linux/snmp.h: https://github.com/torvalds/linux/blob/v3.13/include/uapi/linux/snmp.h#L10-L59
[96] net/ipv4/af_inet.c: https://github.com/torvalds/linux/blob/v3.13/net/ipv4/af_inet.c#L1526-L1547
[97] net/ipv4/af_inet.c: https://github.com/torvalds/linux/blob/v3.13/net/ipv4/af_inet.c#L1720-L1725
[98] net/ipv4/udp.c: https://github.com/torvalds/linux/blob/v3.13/net/ipv4/udp.c
[99] инкапсулирующим: https://tools.ietf.org/html/rfc3948
[100] net/ipv4/udp.c: https://github.com/torvalds/linux/blob/v3.13/net/ipv4/udp.c#L1548-L1549
[101] setsockopt: http://www.manpagez.com/man/2/setsockopt/
[102] net/ipv4/udp.c: https://github.com/torvalds/linux/blob/v3.13/net/ipv4/udp.c#L1554-L1561
[103] net/ipv4/udp.c: https://github.com/torvalds/linux/blob/v3.13/net/ipv4/udp.c#L1431-L1443
[104] net/ipv4/udp.c: https://github.com/torvalds/linux/blob/master/net/ipv4/udp.c#L2396-L2431
[105] документации ядра прекрасно освещён вопрос: https://github.com/torvalds/linux/blob/v3.13/Documentation/networking/timestamping.txt
[106] программа-образец и сборочный файл: https://github.com/torvalds/linux/tree/v3.13/Documentation/networking/timestamping
[107] прекрасно описано: http://www.intel.com/content/dam/www/public/us/en/documents/white-papers/open-source-kernel-enhancements-paper.pdf
[108] kgdb: http://sysprogs.com/VisualKernel/kgdboe/launch/
[109] netconsole: https://github.com/torvalds/linux/blob/v3.13/Documentation/networking/netconsole.txt
[110] net/dev/core.c: https://github.com/torvalds/linux/blob/v3.13/net/core/dev.c#L3511-L3514
[111] netconsole: https://github.com/torvalds/linux/blob/v3.13/drivers/net/netconsole.c
[112] ‘include/linux/netpoll.h`: https://github.com/torvalds/linux/blob/v3.13/include/linux/netpoll.h
[113] этот прекрасный текст: http://people.redhat.com/~jmoyer/netpoll-linux_kongress-2005.pdf
[114] patchwork.ozlabs.org/patch/408257: https://patchwork.ozlabs.org/patch/408257/
[115] документации к ядру: https://github.com/torvalds/linux/blob/v3.13/Documentation/dmaengine.txt
[116] Intel IOAT DMA engine: https://en.wikipedia.org/wiki/I/O_Acceleration_Technology
[117] Intel I/O AT: http://www.intel.com/content/www/us/en/wireless-network/accel-technology.html
[118] багов, повреждавших данные: https://git.kernel.org/cgit/linux/kernel/git/torvalds/linux.git/commit/?id=77873803363c9e831fc1d1e6895c084279090c22
[119] igb_update_dca: https://github.com/torvalds/linux/blob/v3.13/drivers/net/ethernet/intel/igb/igb_main.c#L5202-L5219
[120] igb_update_rx_dca: https://github.com/torvalds/linux/blob/v3.13/drivers/net/ethernet/intel/igb/igb_main.c#L5182-L5200
[121] Источник: https://habrahabr.ru/post/314168/?utm_source=habrahabr&utm_medium=rss&utm_campaign=best
Нажмите здесь для печати.