[Перевод] Мониторинг и настройка сетевого стека Linux: получение данных
В этой статье мы рассмотрим, как осуществляется приём пакетов на компьютерах под управлением ядра Linux, а также разберём вопросы мониторинга и настройки каждого компонента сетевого стека по мере движения пакетов из сети в приложения пользовательского пространства. Здесь вы найдёте много исходного кода, потому что без глубокого понимания процессов вы не сможете настроить и отслеживать сетевой стек Linux.
Также рекомендуем ознакомиться с иллюстрированным руководством на ту же тему, там есть поясняющие схемы и дополнительная информация.
2. Обзор проблематики
3. Подробный разбор
3.1. Драйвер сетевого устройства
3.2. SoftIRQ
3.3. Подсистема сетевого устройства в Linux
3.4. Механизм управления принимаемыми пакетами (Receive Packet Steering (RPS))
3.5. Механизм управления принимаемыми потоками (Receive Flow Steering (RFS))
3.6. Аппаратно ускоренное управление принимаемыми потоками (Accelerated Receive Flow Steering (aRFS))
3.7. Повышение (moving up) сетевого стека с помощью netif_receive_skb
3.8. netif_receive_skb
3.9. Регистрация уровня протокола
3.10. Дополнительная информация
4. Заключение
1. Общий совет по мониторингу и настройке сетевого стека в Linux
Сетевой стек устроен сложно, и не существует универсального решения на все случаи жизни. Если для вас или вашего бизнеса критически важны производительность и корректность при работе с сетью, то вам придётся инвестировать немало времени, сил и средств в то, чтобы понять, как взаимодействуют друг с другом различные части системы.
В идеале, вам следует измерять потери пакетов на каждом уровне сетевого стека. В этом случае необходимо выбрать, какие компоненты нуждаются в настройке. Именно на этом моменте, как мне кажется, сдаются многие. Это предположение основано на том, что настройки sysctl или значения /proc можно использовать многократно и скопом. В ряде случаев, вероятно, система бывает настолько пронизана взаимосвязями и наполнена нюансами, что если вы пожелаете реализовать полезный мониторинг или выполнить настройку, то придётся разобраться с функционированием системы на низком уровне. В противном случае просто используйте настройки по умолчанию. Этого может быть достаточно до тех пор, пока не понадобится дальнейшая оптимизация (и вложения для отслеживания этих настроек).
Многие из приведённых в этой статье примеров настроек используются исключительно в качестве иллюстраций, и не являются рекомендацией «за» или «против» использования в качестве определённой конфигурации или настроек по умолчанию. Так что перед применением каждой настройки сначала подумайте, что вам нужно мониторить, чтобы выявить значимое изменение.
Опасно применять сетевые настройки, подключившись к машине удалённо. Можно легко заблокировать себе доступ или вообще уронить систему работы с сетью. Не применяйте настройки на рабочих машинах, сначала обкатайте их, по мере возможности, на новых, а затем применяйте в production.
2. Обзор проблематикиВы можете захотеть иметь под рукой копию спецификации (data sheet) устройства. В этой статье будет рассмотрен контроллер Intel I350, управляемый драйвером igb. Скачать спецификацию можно отсюда.
Высокоуровневый путь, по которому проходит пакет от прибытия до приёмного буфера сокета выглядит так:
- Драйвер загружается и инициализируется.
- Пакет прибывает из сети в сетевую карту.
- Пакет копируется (посредством DMA) в кольцевой буфер памяти ядра.
- Генерируется аппаратное прерывание, чтобы система узнала о появлении пакета в памяти.
- Драйвер вызывает NAPI, чтобы начать цикл опроса (poll loop), если он ещё не начат.
- На каждом CPU системы работают процессы ksoftirqd. Они регистрируются во время загрузки. Эти процессы вытаскивают пакеты из кольцевого буфера с помощью вызова NAPI-функции poll, зарегистрированной драйвером устройства во время инициализации.
- Очищаются (unmapped) те области памяти в кольцевом буфере, в которые были записаны сетевые данные.
- Данные, отправленные напрямую в память (DMA), передаются для дальнейшей обработки на сетевой уровень в виде «skb».
- Если включено управление пакетами, или если в сетевой карте есть несколько очередей приёма, то фреймы входящих сетевых данных распределяются по нескольким CPU системы.
- Фреймы сетевых данных передаются из очереди на уровни протоколов.
- Уровни протоколов обрабатывают данные.
- Данные добавляются в буферы приёма, прикреплённые к сокетам уровнями протоколов.
Далее мы подробно рассмотрим весь этот поток. В качестве уровне протоколов будут рассмотрены уровни IP и UDP. Большая часть информации верна и для других уровней протоколов. 3. Подробный разбор
Мы будем рассматривать ядро Linux версии 3.13.0. Также по всей статье используются примеры кода и ссылки на GitHub.
Очень важно разобраться, как именно пакеты принимаются ядром. Нам придётся внимательно ознакомиться и понять работу сетевого драйвера, чтобы потом было легче вникнуть в описание работы сетевого стека.
В качестве сетевого драйвера будет рассмотрен igb. Он используется в довольно распространённой серверной сетевой карте, Intel I350. Так что давайте начнём с разбора работы этого драйвера.
3.1. Драйвер сетевого устройства
Инициализация
Драйвер регистрирует функцию инициализации, вызванную ядром при загрузке драйвера. Регистрация выполняется с помощью макроса module_init.
Вы можете найти функцию инициализации igb (igb_init_module) и её регистрацию с помощью module_init в drivers/net/ethernet/intel/igb/igb_main.c. Всё довольно просто:
/**
* igb_init_module – подпрограмма (routine) регистрации драйвера
*
* igb_init_module — это первая подпрограмма, вызываемая при загрузке драйвера.
* Она выполняет регистрацию с помощью подсистемы PCI.
**/
static int __init igb_init_module(void)
{
int ret;
pr_info("%s - version %s\n", igb_driver_string, igb_driver_version);
pr_info("%s\n", igb_copyright);
/* ... */
ret = pci_register_driver(&igb_driver);
return ret;
}
module_init(igb_init_module);
Как мы увидим дальше, основная часть работы по инициализации устройства происходит пи вызове pci_register_driver.
Инициализация PCI
Сетевая карта Intel I350 — это устройство с интерфейсом PCI express.
PCI-устройства идентифицируют себя с помощью серии регистров в конфигурационном пространстве PCI.
Когда драйвер устройства скомпилирован, то для экспорта таблицы идентификаторов PCI-устройств, которыми может управлять драйвер, используется макрос MODULE_DEVICE_TABLE (из include/module.h). Ниже мы увидим, что таблица также регистрируется как часть структуры.
Эта таблица используется ядром для определения, какой нужно загрузить драйвер для управления устройством. Таким образом операционная система понимает, какое устройство подключено и какой драйвер позволяет с ним взаимодействовать.
Вы можете найти таблицу и идентификаторы PCI-устройств для драйвера igb, соответственно, здесь drivers/net/ethernet/intel/igb/igb_main.c и здесь drivers/net/ethernet/intel/igb/e1000_hw.h:
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:
static struct pci_driver igb_driver = {
.name = igb_driver_name,
.id_table = igb_pci_tbl,
.probe = igb_probe,
.remove = igb_remove,
/* ... */
};
Probe-функция PCI
Когда устройство опознано по его PCI ID, ядро может выбрать подходящий драйвер. Каждый драйвер регистрирует probe-функцию в PCI-системе ядра. Ядро вызывает эту функцию для тех устройств, на которые ещё не претендовали драйверы. Когда один из драйверов претендует на устройство, то другие уже не опрашиваются. Большинство драйверов содержат много кода, которые выполняется для подготовки устройства к использованию. Выполняемые процедуры сильно варьируются в зависимости от драйвера.
Вот некоторые типичные процедуры:
- Включение PCI-устройства.
- Запрашивание областей памяти и портов ввода-вывода.
- Настройка маски DMA.
- Регистрируются поддерживаемые драйвером функции ethtool (будут описаны ниже).
- Выполняются сторожевые таймеры (например, у e1000e есть таймер, проверяющий, не зависло ли железо).
- Другие процедуры, характерные для данного устройства. Например, обход или разрешение аппаратных выкрутасов, и тому подобное.
- Создание, инициализация и регистрация структуры struct net_device_ops. Она содержит указатели на разные функции, нужные для открытия устройства, отправки данных в сеть, настройки MAC-адреса и так далее.
- Создание, инициализация и регистрация высокоуровневой структуры struct net_device, представляющей сетевое устройство.
Давайте пробежимся по некоторым из этих процедур применительно к драйверу igb и функции igb_probe.
Беглый взгляд на инициализацию PCI
Приведённый ниже код из функции igb_probe выполняет базовое конфигурирование PCI. Взято из drivers/net/ethernet/intel/igb/igb_main.c:
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), если загружен её драйвер. С помощью вызова pci_set_master активируется DMA, а конфигурационное пространство PCI сохраняется с помощью вызова pci_save_state.
Фух.
Дополнительная информация о драйвере PCI для Linux
Полный разбор работы PCI-устройства выходит за рамки этой статьи, но вы можете почитать эти материалы:
- free-electrons.com/doc/pci-drivers.pdf
- wiki.osdev.org/PCI
- github.com/torvalds/linux/blob/v3.13/Documentation/PCI/pci.txt
Инициализация сетевого устройства
Функция igb_probe выполняет важную работу по инициализации сетевого устройства. В дополнение к процедурам, характерным для PCI, она выполняет и более общие операции для работы с сетью и функционирования сетевого устройства:
- Регистрирует struct net_device_ops.
- Регистрирует операции ethtool.
- Получает от сетевой карты MAC-адрес по умолчанию.
- Настраивает флаги свойств net_device.
- И делает многое другое.
Всё это нам понадобится позднее, так что давайте кратко пробежимся.
struct net_device_ops
struct net_device_ops содержит указатели функций на многие важные операции, необходимые сетевой подсистеме для управления устройством. Эту структуру мы ещё не раз упомянем в статье.
Структура net_device_ops прикреплена к struct net_device в igb_probe. Взято из drivers/net/ethernet/intel/igb/igb_main.c:
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:
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
ethtool — это программа, управляемая из командной строки. С её помощью вы можете получать и настраивать различные драйверы и опции оборудования. Под Ubuntu эту программу можно установить так: apt-get install ethtool.
Обычно ethtool применяется для сбора с сетевых устройств детальной статистики. Другие способы применения будут описаны ниже.
Программа общается с драйверами с помощью системного вызова ioctl. Драйвер устройства регистрирует серию функций, выполняемых для операций 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.
Взято из drivers/net/ethernet/intel/igb/igb_ethtool.c:
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:
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 для получения этой статистики.
IRQ
Когда фрейм данных с помощью DMA записывается в память, как сетевая карта сообщает системе о том, что данные готовы к обработке?
Обычно карта генерирует прерывание, сигнализирующее о прибытии данных. Есть три распространённых типа прерываний: MSI-X, MSI и легаси-IRQ. Вскоре мы их рассмотрим. Генерируемое при записи данных в память прерывание достаточно простое, но если приходит много фреймов, то генерируется и большое количество IRQ. Чем больше прерываний, тем меньше времени работы CPU доступно для обслуживания более высокоуровневых задач, например, пользовательских процессов.
New Api (NAPI) был создан в качестве механизма снижения количества прерываний, генерируемых сетевыми устройствами по мере прибытия пакетов. Но всё же NAPI не может совсем избавить нас от прерываний. Позднее мы узнаем, почему.
NAPI
По ряду важных признаков NAPI отличается от легаси-метода сбора данных. Он позволяет драйверу устройства регистрировать функцию poll, вызываемую подсистемой NAPI для сбора фрейма данных.
Алгоритм использования NAPI драйверами сетевых устройств выглядит так:
- Драйвер включает NAPI, но изначально тот находится в неактивном состоянии.
- Прибывает пакет, и сетевая карта напрямую отправляет его в память.
- Сетевая карта генерирует IRQ посредством запуска обработчика прерываний в драйвере.
- Драйвер будит подсистему NAPI с помощью SoftIRQ (подробнее об этом — ниже). Та начинает собирать пакеты, вызывая в отдельном треде исполнения (thread of execution) зарегистрированную драйвером функцию poll.
- Драйвер должен отключить последующие генерирования прерываний сетевой картой. Это нужно для того, чтобы позволить подсистеме NAPI обрабатывать пакеты без помех со стороны устройства.
- Когда вся работа выполнена, подсистема NAPI отключается, а генерирование прерываний устройством включается снова.
- Цикл повторяется, начиная с пункта 2.
Этот метод сбора фреймов данных позволил уменьшить нагрузку по сравнению с легаси-методом, поскольку многие фреймы могут одновременно приниматься без необходимости одновременного генерирования IRQ для каждого из них.
Драйвер устройства реализует функцию poll и регистрирует её с помощью NAPI, вызывая netif_napi_add. При этом драйвер также задаёт weight. Большинство драйверов хардкодят значение 64. Почему именно его, мы увидим дальше.
Обычно драйверы регистрируют свои NAPI-функции poll в процессе инициализации драйвера.
Инициализация NAPI в драйвере igb
Драйвер igb делает это с помощью длинной цепочки вызовов:
- igb_probe вызывает igb_sw_init.
- igb_sw_init вызывает igb_init_interrupt_scheme.
- igb_init_interrupt_scheme вызывает igb_alloc_q_vectors.
- igb_alloc_q_vectors вызывает igb_alloc_q_vector.
- igb_alloc_q_vector вызывает netif_napi_add.
В результате выполняется ряд высокоуровневых операций:
- Если поддерживается MSI-X, то она включается с помощью вызова pci_enable_msix.
- Высчитываются и инициализируются различные настройки; например, количество очередей передачи и приёма, которые будут использоваться устройством и драйвером для отправки и получения пакетов.
- igb_alloc_q_vector вызывается однократно для каждой создаваемой очереди передачи и приёма.
- При каждом вызове igb_alloc_q_vector также вызывается netif_napi_add для регистрации функции poll для конкретной очереди. Когда функция poll будет вызвана для сбора пакетов, ей будет передан экземпляр struct napi_struct.
Давайте взглянем на igb_alloc_q_vector чтобы понять, как регистрируется callback poll и её личные данные (private data).
Взято из drivers/net/ethernet/intel/igb/igb_main.c:
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, ей передадут эту ссылку.
Важность описанного алгоритма мы поймём, когда изучим поток данных из драйвера в сетевой стек.
Загрузка (bring up) сетевого устройства
Помните структуру net_device_ops, которая регистрировала набор функций для загрузки сетевого устройства, передачи пакетов, настройки MAC-адреса и так далее?
Когда сетевое устройство загружено (например, с помощью ifconfig eth0 up), вызывается функция, прикреплённая к полю ndo_open структуры net_device_ops.
Функция ndo_open обычно делает следующее:
- Выделяет память для очередей приёма и передачи.
- Включает NAPI.
- Регистрирует обработчика прерываний.
- Включает аппаратные прерывания.
- И многое другое.
В случае с драйвером igb, igb_open вызывает функцию, прикреплённая к полю ndo_open структуры net_device_ops.
Подготовка к получению данных из сети
Большинство современных сетевых карт используют DMA для записи данных напрямую в память, откуда операционная система может их извлечь для последующей обработки. Чаще всего используемая для этого структура похожа на очередь, созданную на базе кольцевого буфера.
Сначала драйвер устройства должен совместно с ОС зарезервировать в памяти область, которая будет использоваться сетевой картой. Далее карта информируется о выделении памяти, куда позднее будут записываться входящие данные, которые можно брать и обрабатывать с помощью сетевой подсистемы.
Выглядит просто, но что если частота пакетов так высока, что один CPU не успевает их обрабатывать? Структура данных базируется на области памяти фиксированного размера, поэтому пакеты будут отбрасываться.
В этом случае может помочь механизм Receive Side Scaling (RSS), система с несколькими очередями.
Некоторые устройства могут одновременно писать входящие пакеты в несколько разных областей памяти. Каждая область обслуживает отдельную очередь. Это позволяет ОС использовать несколько CPU для параллельной обработки входящих данных на аппаратном уровне. Но такое умеют делать не все сетевые карты.
Intel I350 — умеет. Свидетельства этого умения мы видим в драйвере igb. Одной из первых вещей, выполняемых им после загрузки, является вызов функции igb_setup_all_rx_resources. Эта функция вызывает однократно для каждой очереди приёма другую функцию — igb_setup_rx_resources, упорядочивающая DMA-память, в которую сетевая карта будет писать входящие данные.
Если вас интересуют подробности, почитайте github.com/torvalds/linux/blob/v3.13/Documentation/DMA-API-HOWTO.txt.
С помощью ethtool можно настраивать количество и размер очередей приёма. Изменение этих параметров позволяет существенно повлиять на отношение обработанных и отброшенных фреймов.
Чтобы определить, в какую очередь нужно отправить данные, сетевая карта использует хэш-функцию в полях заголовка (источник, пункт назначения, порт и так далее).
Некоторые сетевые карты позволяют настраивать вес очередей приёма, так что вы можете направлять больше трафика в конкретные очереди.
Реже встречается возможность настройки самой хэш-функции. Если вы можете её настраивать, то можете направлять конкретный поток в конкретную очередь, или даже отбрасывать пакеты на аппаратном уровне.
Ниже мы рассмотрим, как настраивается хэш-функция.
Включение NAPI
Когда сетевое устройство загружено, драйвер обычно включает NAPI. Мы уже видели, как драйверы с помощью NAPI регистрируют функции poll. Обычно NAPI не включается, пока устройство не загружено.
Включить его довольно просто. Вызов napi_enable сигнализирует struct napi_struct, что NAPI включена. Как отмечалось выше, после включения NAPI находится в неактивном состоянии.
В случае с драйвером igb, NAPI включается для каждого q_vector, инициализируемого после загрузки драйвера, или когда счётчик или размер очереди изменяется с помощью ethtool.
Взято из drivers/net/ethernet/intel/igb/igb_main.c:
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 (если поддерживается), которая всё ещё имеет преимущества по сравнению с легаси-прерываниями. Подробнее об этом читайте в английской Википедии.
В драйвере igb в качестве обработчиков прерываний MSI-X, MSI и легаси выступают соответственно функции igb_msix_ring, igb_intr_msi, igb_intr.
Код драйвера, пробующий каждый способ, можно найти в drivers/net/ethernet/intel/igb/igb_main.c:
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 interrupt\n", 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 -S
Установить 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
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, предоставляющий выжимку по каждому сетевому адаптеру в системе.
Читаем /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
Примечание: у большинства драйверов такие изменения приведут к падению и перезагрузке интерфейса, потому что подключения к нему будут прерваны. Хотя при однократном изменении это не слишком важно.
Настройка веса обработки очередей приёма
Некоторые сетевые карты позволяют настраивать распределение сетевых данных между очередями приёма путём изменения их весов.
Это можно сделать, если:
- Сетевая карта поддерживает косвенную адресацию потока (flow indirection).
- Ваш драйвер реализует функции get_rxfh_indir_size и get_rxfh_indir из ethtool.
- У вас работает достаточно новая версия ethtool, поддерживающая опции командной строки -x и -X, соответственно отображающие и настраивающие таблицу косвенной адресации (indirection table).
Проверка таблицы косвенной адресации потока приёма:
$ 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» (ntuple filtering). Она позволяет указывать (посредством ethtool) набор параметров, используемых для фильтрования входных данных на уровне железа и адресации в конкретную очередь приёма. Например, можно прописать, чтобы TCP-пакеты, пришедшие на определённый порт, передавались в очередь 1.
В сетевых картах Intel эта функция обычно называется Intel Ethernet Flow Director. Другие производители могут давать другие названия.
Как мы увидим дальше, фильтрование ntuple — критически важный компонент другой функции, Accelerated Receive Flow Steering (aRFS). Это сильно облегчает использование ntuple, если ваша сетевая карта поддерживает его. aRFS мы рассмотрим позднее.
Эта функция может быть полезна, если о