Моніторинг та налаштування мережевого стека Linux: отримання даних



У цій статті ми розглянемо, як здійснюється прийом пакетів на комп'ютерах під управлінням ядра Linux, а також розберемо питання моніторингу і налаштування кожного компонента мережевого стека по мірі руху пакетів з мережі в додатку користувача простору. Тут ви знайдете багато вихідного коду, тому що без глибокого розуміння процесів ви не зможете налаштувати і відслідковувати мережевий стек Linux.

Також рекомендуємо ознайомитися з ілюстрованим керівництвом на ту ж тему, там є пояснювальні схеми та додаткова інформація.

Зміст1. Загальний рада з моніторингу та налаштування мережевого стека 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. Завантажити специфікацію можна отсюда.
Високорівневий шлях, по якому проходить пакет від прибуття до приймального буфера сокета виглядає так:

  1. Драйвер завантажується і ініціалізується.
  2. Пакет прибуває з мережі в мережеву карту.
  3. Пакет копіюється (за допомогою DMA) в кільцевий буфер пам'яті ядра.
  4. Генерується апаратне переривання, щоб система дізналася про появу пакету в пам'яті.
  5. Драйвер викликає NAPI, щоб почати цикл опитування (poll loop), якщо він ще не розпочато.
  6. На кожному CPU системи працюють процеси ksoftirqd. Вони реєструються під час завантаження. Ці процеси витягують пакети з кільцевого буфера за допомогою виклику NAPI-функції poll, зареєстрованої драйвером пристрою під час ініціалізації.
  7. Очищаються (unmapped) ті області пам'яті в кільцевому буфері, які були записані мережеві дані.
  8. Дані, надіслані безпосередньо до пам'яті (DMA), передаються для подальшої обробки на мережевий рівень у вигляді 'skb'.
  9. Якщо включено управління пакетами, або якщо в мережевій карті є кілька черг прийому, то фрейми вхідних мережевих даних розподіляються за декількома CPU системи.
  10. Фрейми мережевих даних передаються з черги на рівні протоколів.
  11. Рівні протоколів обробляють дані.
  12. Дані додаються в буфер прийому, прикріплені до сокетам рівнями протоколів.
Далі ми докладно розглянемо весь цей потік. Як рівні протоколів будуть розглянуті рівні 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-системі ядра. Ядро викликає цю функцію для тих пристроїв, які ще не претендували драйвери. Коли один із драйверів претендує на пристрій, то інші вже не опитуються. Більшість драйверів містять багато коду, що виконується для підготовки пристрою до використання. Виконувані процедури сильно варіюються залежно від драйвера.

Ось деякі типові процедури:

  1. Включення PCI-пристрої.
  2. Запитував областей пам'яті і портів вводу-виводу.
  3. Налаштування маски DMA.
  4. Реєструються підтримувані драйвером функції ethtool (будуть описані нижче).
  5. Виконуються сторожові таймери (наприклад, у e1000e є таймер, перевіряючий, не зависло чи залізо).
  6. Інші процедури, характерні для даного пристрою. Наприклад, обхід або дозвіл апаратних викрутасів, тощо.
  7. Створення, ініціалізація та реєстрація структури struct net_device_ops. Вона містить покажчики на різні функції, потрібні для відкриття пристрою, відправлення даних у мережу, налаштування MAC-адреси і так далі.
  8. Створення, ініціалізація та реєстрація високорівневої структури 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-пристрою виходить за рамки цієї статті, але ви можете почитати ці матеріали:

Ініціалізація мережевого пристрою

Функція igb_probe виконує важливу роботу з ініціалізації мережевого пристрою. На додаток до процедур, характерним для PCI, вона виконує і більш загальні операції для роботи з мережею і функціонування мережевого пристрою:

  1. Реєструє struct net_device_ops.
  2. Реєструє операції ethtool.
  3. Отримує від мережевої карти MAC-адресу за замовчуванням.
  4. Налаштовує прапори властивостей net_device.
  5. І робить багато іншого.
Все це нам знадобиться пізніше, так що давайте коротко пробіжимося.

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 драйверами мережних пристроїв виглядає так:

  1. Драйвер включає NAPI, але спочатку той знаходиться в неактивному стані.
  2. Прибуває пакет, і мережева карта безпосередньо відправляє його в пам'ять.
  3. Мережева карта генерує IRQ допомогою запуску обробника переривань в драйвері.
  4. Драйвер будить підсистему NAPI з допомогою SoftIRQ (детальніше про це — нижче). Та починає збирати пакети, викликаючи в окремому тред виконання (thread of execution) зареєстровану драйвером функцію poll.
  5. Драйвер повинен відключити наступні генерації переривань мережевою картою. Це потрібно для того, щоб дозволити підсистемі NAPI обробляти пакети без перешкод з боку пристрою.
  6. Коли вся робота виконана, підсистема NAPI відключається, а генерування переривань пристроєм включається знову.
  7. Цикл повторюється, починаючи з пункту 2.
Цей метод збору фреймів даних дозволив зменшити навантаження порівняно з легасі-методом, оскільки багато кадри, що одночасно можуть прийматися без необхідності одночасного генерування IRQ для кожного з них.

Драйвер пристрою реалізує функцію poll і реєструє її з допомогою NAPI, викликаючи netif_napi_add. При цьому драйвер також задає weight. Більшість драйверів хардкодят значення 64. Чому саме його, ми побачимо далі.

Зазвичай драйвери реєструють свої NAPI-функції poll в процесі ініціалізації драйвера.

Ініціалізація NAPI в драйвері igb


Драйвер igb робить це з допомогою довгого ланцюжка викликів:

  1. igb_probe викликає igb_sw_init.
  2. igb_sw_init викликає igb_init_interrupt_scheme.
  3. igb_init_interrupt_scheme викликає igb_alloc_q_vectors.
  4. igb_alloc_q_vectors викликає igb_alloc_q_vector.
  5. igb_alloc_q_vector викликає netif_napi_add.
В результаті виконується ряд високорівневих операцій:

  1. Якщо підтримується MSI X, то вона включається за допомогою виклику pci_enable_msix.
  2. Вираховуються і ініціалізуються різні налаштування; наприклад, кількість черг передачі і прийому, які будуть використовуватися пристроєм і драйвером для відправки та отримання пакетів.
  3. igb_alloc_q_vector викликається одноразово для кожної створюваної черги передачі і прийому.
  4. При кожному виклику 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 зазвичай робить наступне:

  1. Виділяє пам'ять для черг прийому і передачі.
  2. Включає NAPI.
  3. Реєструє обробника переривань.
  4. Включає апаратні переривання.
  5. І багато іншого.
У випадку з драйвером 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 ми розглянемо пізніше.

Ця функція може бути корисна, якщо операційні вимоги системи передбачають максимізацію локальності даних (data locality) заради збільшення частоти успішних звернень (hit rates) кешу CPU при обробці мережевих даних. Наприклад, так виглядає конфігурування веб-сервера, який працює на порт 80:

  • Сервер закріплюється за CPU 2.
  • Обробка IRQ для черзі прийому закріплюється за тим же CPU.
  • трафік TCP на порт 80 «фільтрується» з допомогою ntuple і відправляється на CPU 2.
  • Весь вхідний трафік на порт 80 обробляється цим CPU, починаючи з отримання даних і аж до програм простору користувача.
  • Для визначення ефективності потрібно акуратний моніторинг системи, включаючи частоту успішних звернень до кешу і затримку мережевого стека.
Як згадувалося вище, фільтрування 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 видають кількість збігів і промахів правил фільтрування. З питань відстеження статистики свого пристрою звіртеся з исходниками драйвера і специфікацією.

3.2. SoftIRQ
Перш ніж переходити до розгляду мережевого стека, нам потрібно коротко пройтися по системі SoftIRQ з ядра Linux.

Що таке SoftIRQ?

Це механізм виконання коду поза контекстом обробника переривань, реалізованого драйвером. Дана система важлива, тому що апаратні переривання можуть бути відключені на протязі всього або частини часу виконання обробника. Чим довше відключені переривання, тим вище ймовірність пропустити події. Важливо виносити виконання будь-яких тривалих дій за межі обробника переривань, щоб завершити їх як можна швидше і знову включити переривання від пристрою.

Існують механізми, які можуть використовуватися для відкладання роботи в ядрі заради потреб мережевого стека. Ми розглянемо SoftIRQ.

Систему SoftIRQ можна представити у вигляді серії тредів ядра (по одному на CPU), в яких працюють функції-обробники, зареєстровані для різних SoftIRQ-подій. Якщо ви коли-небудь піднімалися вгору і зустрічали ksoftirqd/0 списку тредів ядра, то це як раз тред SoftIRQ, що виконується на CPU 0.

Підсистеми ядра (наприклад, по роботі з мережею) можуть реєструвати обробника SoftIRQ допомогою виконання функції open_softirq. Далі ми побачимо, як система по роботі з мережею реєструє своїх SoftIRQ-обробників. А тепер давайте небагато поговоримо про те, як працює SoftIRQ.

ksoftirqd

Оскільки SoftIRQ дуже важливі для відкладання роботи драйверів пристроїв, то процес ksoftirqd створюється в рамках життєвого циклу ядра досить рано.

Погляньте на код kernel/softirq.c, що показує, як саме система 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 як частина чогось, що нагадує цикл події (event loop).

Код kernel/smpboot.c спочатку викликає ksoftirqd_should_run, який визначає, чи є очікують SoftIRQ. Якщо є, то виконується run_ksoftirqd, яка виконує деякі другорядні обчислення, перш ніж викликати __do_softirq.

__do_softirq

Функція __do_softirq робить кілька цікавих речей:

  • Визначає очікує SoftIRQ.
  • Враховує для статистики час SoftIRQ.
  • Инкрементирует статистику виконання SoftIRQ.
  • Виконує обробника для очікує SoftIRQ (який був зареєстрований за допомогою виклику open_softirq).
Так що коли ви дивитеся на графіки використання CPU і бачите softirq або si, то це виконується вимірювання обсягу ресурсів CPU, які використовуються відкладеним робочим контекстом.

Моніторинг

/proc/softirqs

Система 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.

Тепер перейдемо до мережевого стека і подивимося, як одержувані мережеві дані проходять по ньому зверху вниз.

3.3. Підсистема мережного пристрою Linux
Ми познайомилися з роботою мережевих драйверів і SoftIRQ, тепер займемося ініціалізацією підсистеми мережного пристрою. Потім ми простежимо шлях проходження пакета починаючи з його приходу.

Ініціалізація підсистеми мережного пристрою

Підсистема мережного пристрою (netdev) ініціалізується в функції net_dev_init. У ній взагалі відбувається багато цікавого.

Ініціалізація структур struct softnet_data

net_dev_init створює набір структур struct softnet_data для кожного CPU в системі. Ці структури містять покажчики на кілька важливих для обробки мережевих даних речей:

  • Список структур NAPI, які потрібно зареєструвати для CPU.
  • Backlog для обробки даних.
  • Вага обробки (processing weight).
  • Список структури receive offload.
  • Налаштування управління прийнятими пакетами (Receive packet steering).
  • І багато іншого.

Ініціалізація обробників SoftIRQ

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:
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;
}

Це дуже короткий оброблювач, і перед поверненням він виконує дві дуже швидкі операції.

  1. Функція igb_write_itr просто оновлює певний апаратний регістр. Він використовується для відстеження частоти появи апаратних переривань. Регістр використовується спільно з апаратною функцією «Interrupt Throttling» (ще її називають «об'єднанням переривань», Interrupt Coalescing), що дозволяє підлаштовувати доставку переривань CPU. Скоро ми побачимо, як ethtool забезпечує механізм настройки частоти генерування IRQ.
  2. Викликається napi_schedule, пробуджує цикл обробки NAPI, якщо він ще не активний. Зверніть увагу, що цей цикл виконується в SoftIRQ, а не обробника переривань. Обробник просто запускає його виконання, якщо це ще не зроблено.
Важливо побачити реальний код, що демонструє роботу вищеописаних операцій. Це допоможе нам зрозуміти, як мережеві дані обробляються в багатопроцесорних системах.

NAPI і napi_schedule

Давайте подивимося, як працює виклик napi_schedule з обробника апаратних переривань.

Як ви пам'ятаєте, NAPI існує саме для збору мережевих даних без використання переривань від мережевої карти, що сигналізують про готовність даних до обробки. Вище згадувалося, що цикл poll завантажується (bootstrapped) при отриманні апаратного переривання. Іншими словами, NAPI включений, але не активний до тих пір, поки не прийде перший пакет. У цей момент мережева карта генерує переривання і NAPI стартує. Нижче ми розглянемо й інші випадки, коли NAPI може бути вимкнений, і перш ніж його запустити, потрібно згенерувати переривання.

Цикл poll запускається, коли драйверний обробник переривань викликає napi_schedule. Це просто функція-обгортка, задана в заголовочном файлі, яка в свою чергу викликає __napi_schedule.

Взято з net/core/dev.c:

/**
* __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:

/* Викликається з відключеним 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);
}

Цей код робить дві важливі речі:

  1. Структура struct napi_struct, взята з коду драйверного обробника переривань, додається в poll_list, прикріплений до структури softnet_data, асоційованої з поточним CPU.
  2. __raise_softirq_irqoff використовується для запуску SoftIRQ NET_RX_SOFTIRQ. У результаті починає виконуватися net_rx_action, зареєстрована під час ініціалізації підсистеми мережного пристрою, якщо це ще не зроблено.
Як ми скоро побачимо, SoftIRQ функція-обробник net_rx_action для збору пакетів викличе функцію NAPI poll.

Примітка про CPU і обробці мережевих даних

Зверніть увагу, що весь код до цього, який переносив роботу з обробника апаратних переривань в SoftIRQ, використовував структури, асоційовані з поточним CPU.

Оскільки сам драйверний IRQ-обробник мало що робить, то SoftIRQ-обробник буде виконуватися на тому ж CPU, що і IRQ-обробник. Тому важливо налаштовувати, яким CPU буде оброблятися конкретний IRQ: адже цей CPU буде використаний не тільки для виконання драйверного обробника переривань, але й для збору пакетів в SoftIRQ через NAPI.

Пізніше ми побачимо, що механізми на кшталт управління прийнятими пакетами (Receive Packet Steering) можуть розподіляти частину роботи по іншим 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. Про це ми поговоримо нижче.

Налаштування надходження мережевих даних

Об'єднання переривань

Так називається метод запобігання передачі переривань з пристрою в CPU, поки не набереться певна кількість подій або обсяг конкретної роботи.

Це допомагає уникати «штормів переривань» і збільшувати пропускну здатність або затримку, в залежності від налаштувань. Зменшення кількості генерованих переривань призводить до підвищення пропускної спроможності і затримки, зниження навантаження на 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 для налаштування декількох опцій. Деякі з найбільш часто використовуваних:

  • rx-usecs: тривалість затримки між прибуттям пакету і генеруванням переривання прийому, в мілісекундах.
  • rx-frames: максимальна кількість фреймів, одержуваних до генерування переривання прийому.
  • rx-usecs-irq: тривалість затримки переривання прийому, поки воно обслуговується хостом, в мілісекундах.
  • rx-frames-irq: максимальна кількість фреймів, одержуваних до генерування переривання прийому, поки система обслуговує переривання.
І є ще дуже багато інших.

Нагадую, що ваше обладнання і драйвер можуть підтримувати лише частина опцій. Вивчіть вихідний код драйвера і специфікацію мережевої карти.

На жаль, доступні для налаштування опції погано документовані скрізь, крім заголовкого файлу. Зверніться до ісходнику include/uapi/linux/ethtool.h знайдіть пояснення для кожної опції, підтримуваної ethtool (але не факт, що вони підтримуються вашим драйвером і мережевою картою).

Примітка: на перший погляд об'єднання переривань виглядає дуже корисною оптимізацією. У деяких випадках це дійсно так, але вам потрібно впевнитися, що інша частина мережевого стека теж налаштована правильно. Одне лише зміна параметрів об'єднання напевно принесе мало користі.

Налаштування прив'язок IRQ

Якщо ваша карта підтримує 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

net_rx_action починає обробку пакетів з пам'яті, які були відправлені туди пристроєм за допомогою DMA.

Функція итерирует за списком структур NAPI, що стоять в черзі поточного CPU, по черзі витягує кожну структуру працює з нею.

Цикл обробки обмежує обсяг роботи і час виконання зареєстрованих NAPI-функцій poll. Він робить це двома способами:

  1. відстежуючи робочий бюджет (work budget) (який можна настроювати),
  2. а також перевіряє витрачений час.
Взято з net/core/dev.c:

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незалежно від присвоєного бюджету.

NAPI-функція poll і weight

Нагадаю, що драйвери мережевих пристроїв для реєстрації функції 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:

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.

Припустимо:
  1. ви використовуєте зі свого драйвера вага 64 (у всіх драйверів Linux 3.13.0 жорстко прописано це значення),
  2. і розмір вашого budget за замовчуванням 300.
Ваша система припинить обробляти дані, якщо:

  1. Функція igb_poll була викликана саме більше 5 раз (може бути і менше, якщо не оброблялося жодних даних, як ми побачимо далі),
  2. пройшло як мінімум 2 jiffies.

Договір між NAPI і драйвер мережного пристрою

Поки ще не згадувалася важлива інформація, що стосується договору між підсистемою NAPI і драйверами пристроїв. Мова про умови зупинки NAPI.

  • Якщо драйверная функція poll витрачає весь свій вага (64), вона повинно змінювати стан NAPI. Вступить в роботу цикл net_rx_action.
  • Якщо драйверная функція poll НЕ витрачає весь свій вагу, вона повинна відключити NAPI. NAPI буде знову включений при отриманні наступного IRQ, коли драйверний обробник переривань викличе napi_schedule.
Тепер подивимося, як net_rx_action працює з першою частиною договору. Потім, після розгляду функції poll, з'ясуємо, як виконується друга частина договору.

Завершення циклу net_rx_action

Цикл обробки net_rx_action завершується секцією коду, яка виконує першу частину договору з NAPI. Взято з net/core/dev.c:

/* Драйвери не повинні змінювати стан 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 обробляє дві ситуації:

  1. Мережеве пристрій повинно бути виключене (наприклад, тому що користувач виконав ifconfig eth0 down).
  2. Якщо пристрій не вимикається, то перевірте, чи є список generic receive offload (GRO). Якщо частота таймера (timer tick rate >= 1000, то всі мережеві потоки з GRO, які нещодавно оновлювалися, будуть скинуті. Пізніше ми детальніше розглянемо GRO. NAPI-структура переміщається у кінець списку даного CPU, так що наступна ітерація циклу отримає наступну зареєстровану NAPI-структуру.
Таким чином цикл обробки пакетів викликає зареєстровану драйверную функцію poll, яка займеться обробкою. Далі ми побачимо, що ця функція збирає мережеві дані і відправляє їх в стек для подальшої обробки.

Вихід з циклу після досягнення обмежень

Вихід з циклу net_rx_action буде здійснений, якщо:

  • список poll, зареєстрований для даного CPU, більше не містить NAPI-структур (!list_empty(&sd->poll_list)),
  • залишок бюджету <= 0,
  • було досягнуто тимчасової межа в два jiffies.
Ось один з вищенаведених прикладів коду

/* Якщо вичерпалася вікно SoftIRQ - отфутболиваем.
* Виконуйте це протягом двох тактів, це дозволить зробити
* середню затримку на рівні 1.5/Гц.
*/
if (unlikely(budget <= 0 || time_after_eq(jiffies, time_limit)))
goto softnet_break;

Якщо ви відстежте label softnet_break, натрапите на щось цікаве. Взято з net/core/dev.c:

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), то ця функція пробуджує видалені CPU, щоб вони почали обробляти мережеві дані.

Нижче ми розберемо роботу RPS. А поки подивимося, як моніторити коректність циклу обробки net_rx_action, і перейдемо до «нутрощів» NAPI-функцій poll, просуваючись по мережевого стека.

NAPI-функція poll

Нагадаю, що драйвери виділяють область пам'яті, в яку пристрої можуть безпосередньо надсилати вхідні пакети. Драйвер відповідає не тільки за виділення пам'яті, але і за їх звільнення, збір даних та їх відправку в мережевий стек.

Як драйвер igb все це робить?

igb_poll

Нарешті-то ми можемо проаналізувати роботу igb_poll. Її код оманливе простий. Взято з drivers/net/ethernet/intel/igb/igb_main.c:

/**
* 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;
}

Тут є ряд цікавих речей:

  • Якщо в ядрі включена підтримка прямого доступу до кешу (Direct Cache Access (DCA)), то кеш CPU «прогрівається», щоб у нього потрапляли звернення до RX-кільцю. Детальніше про це можна почитати на чолі з дод. матеріалами в кінці статті.
  • Потім викликається функція igb_clean_rx_irq, виконує нелегку задачу. Про це нижче.
  • Далі перевіряється clean_complete, щоб визначити, чи залишилася ще робота, яку можна виконати. Якщо так, то бюджет повертається (жорстко прописано значення 64). net_rx_action переміщує NAPI-структуру в кінець списку poll.
  • В іншому випадку драйвер вимкне NAPI допомогою виклику napi_complete, і з допомогою igb_ring_irq_enable знову включить переривання. Таке прийшло переривання знову включить NAPI.
Давайте подивимося, як igb_clean_rx_irq відправляє мережеві дані вгору по стеку.

igb_clean_rx_irq

Функція igb_clean_rx_irq — це цикл, обробна по одному пакету за раз, поки не скінчиться budget або дані для обробки.

У цьому циклі виконуються такі речі:

  1. В пам'яті розміщуються додаткові буфери прийому даних, на випадок очищення використовуються буферів. Вони додаються по IGB_RX_BUFFER_WRITE (16).
  2. З черги прийому витягується буфер і зберігається в структурі skb.
  3. Перевіряється, є буфер «End of Packet». Якщо так, то обробка триває. В іншому випадку триває витяг додаткових буферів з черги прийому і додавання в skb. Це необхідно на випадок, якщо отриманий кадр перевищує розмір буфера.
  4. Перевіряється коректність схеми (layout) і заголовків.
  5. За допомогою skb->len збільшується лічильник кількості оброблених байтів.
  6. skb налаштовуються хеш, контрольна сума, тимчасова мітка, VLAN id і поля протоколу. Перші чотири позиції надаються мережевою картою. Якщо вона повідомляє про помилку контрольної суми, то инкрементируется csum_error. Якщо з контрольною сумою все в порядку, а дані отримані по UDP або TCP, то skb позначається як CHECKSUM_UNNECESSARY. Якщо виникає помилка контрольної суми, то з пакетом розбираються стеки протоколів. Потрібний протокол обчислюється з допомогою виклику eth_type_trans і зберігається в структурі skb.
  7. Зроблений skb передається вгору по мережевому стеку з допомогою виклику napi_gro_receive.
  8. Инкрементируется лічильник кількості оброблених пакетів.
  9. Цикл продовжує виконуватися до тих пір, поки кількість оброблених пакетів не вичерпає бюджету.
Коли цикл переривається, функція записує лічильники оброблених пакетів і байтів.

Перш ніж продовжити розгляд мережевого стека, зробимо кілька відступів. По-перше, подивимося, як моніторити і налаштовуватися SoftIRQ мережевої підсистеми. По-друге, поговоримо про Generic Receive Offloading (GRO). Після того, як ми заглибимося в napi_gro_receive, нам буде зрозуміліше робота решті частини мережевого стека.

Моніторинг обробки мережевих даних

/proc/net/softnet_stat



Як ми бачили в попередньому розділі, статистичні лічильники инкрементируются при виході з циклу 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:

seq_printf(seq,
"%08x %08x %08x %08x %08x %08x %08x %08x %08x %08x %08x\n",
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:

  • Кожна рядок /proc/net/softnet_stat відповідає структурі struct softnet_data, по одній на кожен CPU.
  • Значення розділені одиночними пробілами і відображаються в шістнадцятковій формі.
  • Перше значення, sd->processed, — це кількість оброблених мережі фреймів. Воно може перевищувати загальну кількість отриманих фреймів, якщо ви використовуєте зв'язування Ethernet (Ethernet bonding). Драйвер зв'язування може стати причиною повторної обробки даних, і тоді лічильник sd->processed для одного і того ж пакету буде инкрементироваться більше одного разу.
  • Друге значення, sd->dropped, — це кількість відкинутих мережі фреймів через брак місця в черзі обробки. Про це поговоримо нижче.
  • Третій елемент, sd->time_squeeze, — це кількість разів, коли цикл net_rx_action переривався через виснаження бюджету або досягнення обмеження по часу, хоча ще залишалася. Як пояснювалося вище, допомогти тут може збільшення budget.
  • Наступні п'ять значень завжди дорівнюють 0.
  • Дев'яте значення, sd->cpu_collision, — це кількість колізій, які виникали при спробах одержання блокування пристрою під час передачі пакетів. Оскільки ця стаття присвячена прийому пакетів, то ми цю статистику розглядати не будемо.
  • Десяте значення, sd->received_rps, — це кількість разів, коли за допомогою межпроцессорного переривання будили CPU для обробки пакетів.
  • Останнє значення, flow_limit_count, — це кількість разів, коли було досягнуто обмеження потоку (flow limit). Обмеження потоку — це опціональна можливість управління прийнятими пакетами (Receive Packet Steering), яку ми незабаром розглянемо.
Якщо ви вирішили моніторити цей файл і відображати результати графічно, то будьте вкрай обережні, щоб не поміняти порядок полів і зберегти їх зміст. Для цього ознайомтеся з цими кодами ядра.

Налаштування обробки мережевих даних

Настройка бюджету net_rx_action

Настройка бюджету 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)

Generic Receive Offloading (GRO) — це програмна реалізація апаратної оптимізації, відомого як Large Receive Offloading (LRO).

Суть обох механізмів у тому, щоб зменшити кількість пакетів, що передаються з мережевого стека, за рахунок комбінування «досить схожих» пакетів. Це дозволяє знизити навантаження на CPU. Уявімо, що у нас передається великий файл, і більшість пакетів містять чанкі даних з цього файлу. Замість відправки по стеку маленьких пакетів по одному, вхідні пакети можна комбінувати в один, з величезною корисним навантаженням. А потім вже передавати його по стеку. Таким чином рівні протоколів обробляють заголовки пакетів, при цьому передаючи користувальницької програми більш великі чанкі.

Але цієї оптимізації притаманна проблема втрати інформації. Якщо якийсь пакет має налаштовану важливу опцію чи прапор, то ця опція чи прапор можуть бути втрачені при об'єднанні з іншими пакетами. В цілому реалізації LRO мають дуже несуворі правила об'єднання пакетів.
GRO є програмною реалізацією LRO, але з більш суворими правилами об'єднання.

До речі: якщо ви колись використовували tcpdump і зустрічали занадто великі розміри вхідних пакетів, то це, напевно, було пов'язано з включеною GRO у вашій системі. Як ми скоро побачимо, tap'и захоплення пакетів вставлені далі по стеку, вже після GRO.

Настройка параметрів GRO з допомогою ethtool

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

Функція napi_gro_receive займається обробкою мережевих даних для GRO (якщо GRO включена) і відправкою їх в стеку на рівні протоколів. Велика частина логіки знаходиться у функції dev_gro_receive.

dev_gro_receive

Ця функція спочатку перевіряє, чи включена GRO. Якщо так, то готується до її застосування: проходить за списком offload-фільтрів, щоб високорівневі стеки протоколів могли працювати з даними, призначеними для GRO. Це потрібно для того, щоб рівні протоколів могли повідомляти рівня мережевого пристрою, є пакет частиною мережного потоку, який в даний момент вільний, а також могли обробляти все що відноситься до протоколу, що має відбутися в рамках GRO. Наприклад, TCP-протоколу потрібно вирішити, чи можна/коли підтверджувати об'єднання пакета з вже наявними.

Ось приклад коду, який це робить, взятий з net/core/dev.c:

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:
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:

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.

napi_skb_finish

По завершенні dev_gro_receive викликається napi_skb_finish, яка звільняє структури даних, незатребувані причини злиття пакета, або для передачі по мережевому стеку викликається netif_receive_skb (тому що GRO вже застосована до потоків MAX_GRO_SKBS).

Тепер настав час розглянути механізм управління прийнятими пакетами (Receive Packet Steering (RPS)).

3.4. Механізм управління прийнятими пакетами 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) — це програмна реалізація RSS. А раз реалізовано в коді, то може бути застосована для будь-якої мережевої карти, навіть якщо вона має лише одну чергу прийому. З іншого боку, програмна природа призводить до того, що RPS може входити в потік тільки після пакету, витягнутого з DMA-пам'яті.

Це означає, що CPU не буде витрачати менше часу на обробку переривань або циклу poll, але зате ви зможете розподіляти навантаження по обробці пакетів після того, як вони були зібрані, і знижувати тривалість роботи CPU в мережевому стеку.

RPS генерує для вхідних даних хеш, щоб визначити, який CPU має їх опрацювати. Потім дані поміщаються у вхідну чергу (backlog) цього процесора в очікуванні подальшої обробки. У процесор з backlog передається межпроцессорное переривання (IPI), ініціююча обробку черги, якщо це ще не робиться. /proc/net/softnet_stat містить лічильник кількості разів, коли кожна структура softnet_data отримувала IPI (поле received_rps).

Отже, netif_receive_skb продовжить надсилати дані мережевого стека або передасть у RPS для обробки іншим CPU.

Включення RPS

Для початку потрібно включити механізм RPS конфігурації ядра (на Ubuntu вірно для ядра 3.13.0), а також створити бітову маску, що описує, які CPU повинні обробляти пакети для конкретного користувача або черги прийому.

Інформацію про цих бітових масках можна знайти на документації до ядра. Якщо коротко, взяти і модифікувати маски можна звідси:

/sys/class/net/DEVICE_NAME/queues/QUEUE/rps_cpus

Наприклад, для eth0 і черги прийому 0 потрібно внести в файл /sys/class/net/eth0/queues/rx-0/rps_cpus шістнадцяткове число, що означає, який CPU повинен обробляти пакети з черги 0 eth0. Як зазначено в документациина певних конфігураціях в RPS немає потреби.

Примітка: включення RPS для розподілу обробки пакетів з CPU, які раніше цього не робили, для кожного з цих CPU призведе до збільшення кількості SoftIRQ `NET_RX`, а також `si` або `sitime` на графіку споживання ресурсів CPU. Можете порівняти показники «до» і «після», щоб з'ясувати, чи відповідає конфігурація RPS вашим побажанням.

3.5. Механізм управління прийнятими потоками (Receive Flow Steering (RFS))
Receive flow steering (RFS) використовується спільно з RPS. RPS намагається розподіляти вхідні пакети серед декількох CPU, але не бере до уваги питання локальності даних для збільшення частоти попадання в кеш CPU. Якщо вам потрібно збільшити цю частоту, то в цьому допоможе механізм RFS, переадресующий пакети одного потоку на один і той же CPU.

Включення RFS

Щоб 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'

3.6. Апаратно прискорений управління прийнятими потоками (Accelerated Receive Flow Steering (aRFS))
Роботу RFS можна апаратно прискорювати. Мережева карта і ядро можуть спільно визначати, який потік на якому CPU потрібно обробляти. Ця функція має підтримуватися картою і драйвером, звіртеся зі специфікацією. Якщо драйвер вашої картки надає функцію ndo_rx_flow_steer, значить він підтримує aRFS.

Включення aRFS

Припустимо, ваш драйвер підтримує цей механізм. Порядок його включення і параметри:

  1. Включаємо і налаштовуємо RPS.
  2. Включаємо і налаштовуємо RFS.
  3. В ході компіляції ядра активується CONFIG_RFS_ACCEL. Зокрема, в ядрі Ubuntu 3.13.0.
  4. Включаємо для нашого пристрою підтримку ntuple, як описано вище. Перевірити, чи включена вона, можна з допомогою ethtool.
  5. Налаштовуємо параметри IRQ щоб упевнитися, що кожна чергу прийому обробляється одним із ваші CPU, що займаються обробкою мережевих даних.
Коли все це буде зроблено, aRFS автоматично задіюється для переміщення даних в чергу прийому, закріплену за ядром CPU, які обробляють дані з цього потоку. Вам не потрібно вручну прописувати правила фільтрації ntuple для кожного потоку.

3.7. Підвищення (moving up) мережевого стека з допомогою netif_receive_skb
Повернемося до того місця, де ми залишили netif_receive_skb, викликану з декількох місць. Найчастіше з двох (ми їх вже розглянули):

  • napi_skb_finish — якщо пакет не буде об'єднаний з наявним GRO-потоком, АБО
  • napi_gro_complete — якщо рівні протоколів сигналізують про те, що пора скидати потік, АБО
Нагадую: 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. Значення цієї налаштування пояснено в попередній главі.

3.8. netif_receive_skb
Розібравшись з тимчасовими мітками, далі netif_receive_skb діє по різному, в залежності від того, чи включений RPS. Почнемо з більш простого випадку, коли RPS вимкнений.

Без RPS (за замовчуванням)

У цьому випадку викликається __netif_receive_skb, яка реєструє якісь дані (bookkeeping), а потім викликає __netif_receive_skb_core, щоб та перенесла дані ближче до стекам протоколів.

Потім ми розглянемо, як працює __netif_receive_skb_core, але спочатку розберемо роботу коду з включеним RPS, оскільки в цьому випадку також викликається __netif_receive_skb_core.

З включеним RPS

Після роботи з опціями присвоювання тимчасових міток, netif_receive_skb виконує ряд обчислень щоб визначити, backlog-черга якого CPU потрібно використовувати. Це робиться за допомогою функції get_rps_cpu. Взято з net/core/dev.c:

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.

enqueue_to_backlog

Ця функція спочатку отримує покажчик на структуру softnet_data віддаленого CPU, що містить вказівник на input_pkt_queue. Потім перевіряється довжина черги input_pkt_queue цього віддаленого CPU. Взято з net/core/dev.c:

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 досить мала і обмеження потоку не досягнуто (або вимкнено), а дані можуть бути поміщені в чергу. Логіка тут трохи смішна, її можна узагальнити так:

  • Якщо черга порожня: перевіряється, чи запущений NAPI на віддаленому CPU. Якщо ні, перевіряється, перебуває у черзі на відправлення IPI. Якщо ні, то IPI поміщається в чергу, а за допомогою виклику ____napi_schedule запускається цикл обробки NAPI. Переходимо до передачі даних в чергу.
  • Якщо черга не порожня, або якщо завершена попередня операція, дані передаються в чергу.
Через використання goto код досить хитрий, так що читайте уважно. Взято з net/core/dev.c:

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 device backlog
* 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 за допомогою виклику skb_flow_limit перевіряє обмеження потоку:

if (qlen <= netdev_max_backlog && !skb_flow_limit(skb, qlen)) {

Тут перевіряється, чи є ще місце в черзі і не досягнута ограничение. За замовчуванням обмеження відключені. Для їх включення потрібно задати бітову маску (аналогічно RPS).

Моніторинг отбрасываний через заповнення input_pkt_queue або обмеження потоку

См. главу про моніторинг /proc/net/softnet_stat. Поле dropped — це лічильник, инкрементируемый при кожному відкиданні даних замість їх приміщення в чергу input_pkt_queue CPU.

Налаштування

Налаштування netdev_max_backlog для запобігання отбрасываний

Перш ніж настроювати значення, прочитайте примітка у попередній главі.

Якщо ви використовуєте RPS або ваш драйвер викликає netif_rx, то можна допомогти запобігати відкидання в enqueue_to_backlog з допомогою збільшення netdev_max_backlog.

Приклад: збільшимо backlog до 3000:

$ sudo sysctl -w net.core.netdev_max_backlog=3000

За замовчуванням дорівнює 1000.

Налаштування ваги NAPI в backlog циклу poll

Налаштувати вага поллера 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 включені обмеження потоків.

Поллер NAPI backlog-черги

Backlog-черзі кожного CPU використовують NAPI так само, як і драйвер пристрою. Надається функція poll, яка використовується для обробки пакетів з контексту SoftIRQ. Як і у випадку з драйвером, тут теж застосовується weight.

Структура NAPI надається у ході ініціалізації мережевої підсистеми.

Взято з net_dev_init net/core/dev.c:

sd->backlog.poll = process_backlog;
sd->backlog.weight = weight_p;
sd->backlog.gro_list = NULL;
sd->backlog.gro_count = 0;

NAPI-структура backlog'а відрізняється від структури драйвера можливістю настройки параметра weight: у драйвера його значення жорстко закодовано. Далі ми розглянемо процес налаштування ваги.

process_backlog

Функція 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 і на рівні протоколів

__netif_receive_skb_core виконує важку рабту з передачі даних в стеки протоколів. Перед цим вона спочатку перевіряє, чи встановлені які-небудь packet taps, які ловлять всі вхідні пакети. Приклад – сімейство адрес AF_PACKET, зазвичай використовуване допомогою бібліотеки libpcap.

Якщо є такий tap, до дані передаються спочатку туди, а потім на рівні протоколів.

Передача в packet tap

Передачу пакетів виконує наступний код. Взято з net/core/dev.c:

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.

Передача на рівні протоколів

Після того, як тапи будуть задоволені, __netif_receive_skb_core передає дані на рівні протоколів. Для цього з даних витягується полі протоколу і виконується итерирование за списком передавальних функцій (deliver functions), зареєстрованих для цього типу протоколу.

Це можна подивитися в __netif_receive_skb_core net/core/dev.c:

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:

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. Таким способом рівні протоколів реєструють себе для передачі їм даних.
Тепер ви знаєте, як мережеві дані потрапляють з мережевої карти на рівень протоколу.

3.9. Реєстрація рівня протоколу
Давайте розглянемо, як рівні протоколів реєструють самі себе. У цій статті ми обмежимося стеком протоколу IP, оскільки він вкрай широко використовується і буде зрозумілий більшості читачів.

Рівень протоколу IP

Рівень протоколу IP сам включає себе в хеш-таблицю ptype_base, і у попередніх розділах ми вже розглядали дані, які віддаються на цей рівень з рівня мережевого пристрою.

Це відбувається у функції inet_init, взято з net/ipv4/af_inet.c:

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 завершується передачею пакета в ip_rcv_finish допомогою netfilter. Це робиться так, щоб будь-яке правило iptables, яке має виконуватись на рівні протоколу IP, могло перевірити пакет, перш ніж він вирушить далі.

Код, що передає дані через netfilter в кінці ip_rcv net/ipv4/ip_input.c:

return NF_HOOK(NFPROTO_IPV4, NF_INET_PRE_ROUTING, skb, dev, NULL, ip_rcv_finish);

netfilter та iptables

Заради стислості викладу я вирішив пропустити докладний розгляд netfilter, iptables і conntrack.

Коротка версія: NF_HOOK_THRESH перевіряє, чи встановлені які-небудь фільтри і намагається повернути назад виконання на рівень протоколу IP, щоб не заглиблюватися в netfilter і все, що під ним, начебто iptables і conntrack.

Пам'ятайте: якщо у вас багато правил netfilter або iptables, або вони занадто складні, то ці правила будуть виконуватися в контексті SoftIRQ, а це ризик виникнення затримок у мережевому стеку. Можливо, ви цього не уникнете, якщо вам потрібен конкретний набір встановлених правил.

ip_rcv_finish

Після того, як netfilter отримує можливість поглянути на дані і вирішити, що з ними робити, викликається ip_rcv_finish. Звичайно, якщо тільки дані не відкидаються netfilter'ом.

ip_rcv_finish починається з оптимізації. Щоб доставити пакет в потрібне місце, повинна бути готова структура dst_entry від системи маршрутизації. Для її отримання код спочатку намагається викликати функцію early_demux з протоколу більш високого рівня, ніж той, для якого призначені дані.

early_demux — це оптимізація, в ході якої ми намагаємося знайти необхідну для доставки пакета структуру dst_entry. Для цього перевіряється, не закэширована чи dst_entry в структурі сокета.

Ось як це виглядає, взято з net/ipv4/ip_input.c:

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 протоколу IP

Відключаємо оптимізацію early_demux:

$ sudo sysctl -w net.ipv4.ip_early_demux=0

Значення за замовчуванням дорівнює 1; early_demux включена.

У ряді випадків ця sysctl приблизно на 5% погіршує пропускну здатність з оптимізацією early_demux.

ip_local_deliver

Згадуємо наступний шаблон на рівні протоколу IP:

  1. При виклику ip_rcv виконується ряд процедур (bookkeeping).
  2. Пакет передається netfilter для подальшої обробки, з покажчиком на виконання callback'а, коли обробка буде завершена.
  3. ip_rcv_finish — це callback, завершальний обробку і продовжує просування пакету з мережевого стека.
У випадку з ip_local_deliver використовується той самий шаблон. Взято з net/ipv4/ip_input.c:

/*
* Доставляє 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

ip_local_deliver_finish витягує з пакета протокол, шукає зареєстровану на цей протокол структуру net_protocol і викликає функцію, на яку вказує handler в цій структурі.

Таким чином пакет передається на більш високий рівень протоколу.

Моніторинг статистики рівня протоколу IP

Читаємо /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:

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.

Деякі цікаві метрики:

  • InReceives: загальна кількість IP-пакетів, які досягли ip_rcv до всіх перевірок цілісності.
  • InHdrErrors: загальна кількість IP-пакетів з пошкодженими заголовками. Заголовок був занадто маленьким або довгим, взагалі не існує, містив неправильний номер версії протоколу IP і так далі.
  • InAddrErrors: загальна кількість IP-пакетів, коли хост був недоступний.
  • ForwDatagrams: загальна кількість IP-пакетів, які були переадресовані (forwarded).
  • InUnknownProtos: загальна кількість IP-пакетів з невідомим або непідтримуваних протоколом, зазначеним у заголовку.
  • InDiscards: загальна кількість IP-пакетів, відхилених в результаті збою виділення пам'яті, або в результаті збою контрольної суми при обрізанні пакета.
  • InDelivers: загальна кількість IP-пакетів, успішно доставлених на більш високі рівні протоколів. Пам'ятайте, що ці рівні можуть відкидати дані, навіть якщо цього не зробив рівень IP.
  • InCsumErrors: загальна кількість IP-пакетів з помилками контрольної суми.
Всі ці лічильники инкрементируются в специфічних місцях рівня IP. Час від часу код проходить за ним, і можуть виникати помилки подвійного підрахунку та інші баги. Якщо для вас важлива ця статистика, то дуже рекомендую вивчити исходники цих метрик на рівні протоколу IP, щоб розуміти, як вони инкрементируются.

Реєстрація протоколу більш високого рівня

Тут ми розглянемо UDP, але обробник протоколу TCP реєструється так само і в той же час, що і обробник протоколу UDP.

В net/ipv4/af_inet.c можна знайти визначення структур, які містять функції-обробники для підключення протоколів UDP, TCP і ICMP до рівня протоколу IP. Взято з net/ipv4/af_inet.c:

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:

/*
* Додаємо всі базові протоколи.
*/

if (inet_add_protocol(&icmp_protocol, IPPROTO_ICMP) < 0)
pr_crit("%s: Cannot add ICMP protocol\n", __func__);
if (inet_add_protocol(&udp_protocol, IPPROTO_UDP) < 0)
pr_crit("%s: Cannot add UDP protocol\n", __func__);
if (inet_add_protocol(&tcp_protocol, IPPROTO_TCP) < 0)
pr_crit("%s: Cannot add protocol TCP\n", __func__);

Розглянемо рівень протоколу UDP. Як ми бачили вище, функція handler для UDP викликається udp_rcv. Це точка входу на рівень UPD, куди передаються дані з рівня IP.

Рівень протоколу UDP

Код рівня протоколу UDP можна знайти тут: net/ipv4/udp.c.

udp_rcv

Код функції udp_rcv складається з одного рядка, в якій безпосередньо викликається функція __udp4_lib_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;

udp_queue_rcv_skb

Початкова частина функції:

  1. Визначає, чи є сокет, асоційований з датаграммой, инкапсулирующим сокетом. Якщо так, то перед обробкою пакет передається функції-обробника цього рівня.
  2. Визначає, чи відноситься дейтаграму до UDP-Lite і виконується перевірки цілісності.
  3. Перевіряє UDP-контрольну суму дейтаграми і відкидає останню в разі збою.
Нарешті ми добралися до логіки черзі прийому. Починається вона з перевірки заповнення черги для сокета. Взято з net/ipv4/udp.c:

if (sk_rcvqueues_full(sk, skb, sk->sk_rcvbuf))
goto drop;

sk_rcvqueues_full

Функція 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 з вашого програми та передачі 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.

udp_queue_rcv_skb

Після перевірки заповнення черги триває процедура приміщення дейтаграми в чергу. Взято з net/ipv4/udp.c:

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

Функція __udp_queue_rcv_skb додає дейтаграми в чергу прийому з допомогою виклику sock_queue_rcv_skb. А якщо датаграмму можна додати в чергу прийому сокета, то __udp_queue_rcv_skb смикає лічильники статистики.

Взято з net/ipv4/udp.c:

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

Два дуже корисних файлу для отримання статистики по протоколу UDP:

  • /proc/net/snmp
  • /proc/net/udp

/proc/net/snmp

Читаємо /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: инкрементируется, коли:

  • recvmsg використовується програмою користувача простору для читання дейтаграми.
  • UDP-пакет інкапсулюється і повертається для обробки.
NoPorts: инкрементируется, коли UDP-пакети прибувають на певний порт, який не слухає ні одна програма.

InErrors: инкрементируется, якщо:

  • закінчилася пам'ять черзі прийому,
  • помилкова контрольна сума,
  • sk_add_backlog не виходить додати датаграмму.
OutDatagrams: инкрементируется, коли UDP-пакет безпомилково передається вниз, на рівень протоколу IP для подальшої відправки.

RcvbufErrors: инкрементируется, коли sock_queue_rcv_skb повідомляє про відсутність доступної пам'яті; таке трапляється, якщо sk->sk_rmem_alloc більше або дорівнює sk->sk_rcvbuf.

SndbufErrors: инкрементируется, якщо:

  • рівень протоколу IP повідомив про помилку при спробі відправлення пакета,
  • ядру не вистачає пам'яті,
  • закінчилося місце в буфері відправлення.
InCsumErrors: инкрементируется, коли виявляється помилка контрольної суми UDP. Зверніть увагу, що у всіх випадках, з якими я стикався, InCsumErrors инкрементируется одночасно з InErrors. Отже, зв'язка InErrors — InCsumErros повинна відображати кількість помилок пам'яті.

/proc/net/udp

Читаємо /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

Перша рядок описує кожне з полів з наступних рядків:

  • sl: хеш-слот ядра для сокета.
  • local_address: шістнадцятковий локальний адреса сокету і номер порту, розділені :.
  • rem_address: шістнадцятковий віддалений адреса сокету і номер порту, розділені :.
  • st: стан сокета. Досить дивно, але рівень протоколу UDP, судячи з усього, використовує стану сокети TCP. У наведеному прикладі, 7 — це TCP_CLOSE.
  • tx_queue: кількість виділеної в ядрі пам'яті для вихідних датаграм UDP.
  • rx_queue: кількість виділеної в ядрі пам'яті для вхідних датаграм UDP.
  • tr, tm->when, retrnsmt: ці поля не використовуються рівнем протоколу UDP.
  • uid: дійсний ідентифікатор користувача, який створив цей сокет.
  • timeout: не використовується рівнем протоколу UDP.
  • inode: номер індексного дескриптора (inode number), відповідного цьому сокету. Його можна використовувати для визначення, який користувальницький процес відкрив цей сокет. Перегляньте /proc/[pid]/fd, вона містить symlink'і на socket[:inode].
  • ref: поточний лічильник посилань на цей сокет.
  • pointer: адреса struct sock в пам'яті ядра.
  • drops: кількість відкинутих дейтаграм, пов'язаних з цим сокетом.
Відображає весь цей код можна знайти на net/ipv4/udp.c.

Приміщення даних в чергу сокета

Мережеві дані поміщаються в чергу сокета з допомогою виклику sock_queue_rcv. Перш ніж додати датаграмму в чергу, ця функція робить кілька речей:

  1. Перевіряється виділена для сокета пам'ять щоб визначити, досягнутий чи межа розміру буфера прийому. Якщо так, то для цього сокета инкрементируется лічильник отбрасываний.
  2. sk_filter використовується для обробки фільтрів Berkeley Packet Filter, застосованих до сокета.
  3. sk_rmem_schedule дозволяє упевнитися, що в буфері прийому є достатньо місця, щоб прийняти датаграмму.
  4. Потім за допомогою виклику skb_set_owner_r розмір дейтаграми заноситься в сокет. Инкрементируется sk->sk_rmem_alloc.
  5. За допомогою виклику __skb_queue_tail в чергу додаються дані.
  6. Нарешті, з допомогою виклику функції обробки повідомлень sk_data_ready повідомляються всі процеси, які очікують прибуття даних в сокет.
Таким чином дані прибувають в систему і проходять з мережевого стека, поки не досягають сокета. Тепер вони готові до використання користувача програмою.

3.10. Додаткова інформація
Потрібно згадати про декілька додаткових речі, про які не було приводу розповісти вище.

Присвоювання тимчасових міток

Я вже писав, що мережевий стек може збирати тимчасові мітки вхідних даних. У sysctl є значення, спільно з RPS дозволяють контролювати момент і спосіб збору підписів. Детальніше про це читайте в розділах, присвячених RPS і тимчасовими мітками. Деякі мережеві карти навіть підтримують мітки апаратно.

Це корисна можливість, якщо ви захочете визначити розмір затримки, доданої до одержуваних пакетів мережевим стеком ядра.

документації ядра чудово висвітлено питання присвоювання тимчасових міток, туди навіть включена програма-зразок та складальний файл!

Визначаємо які режими тимчасових міток підтримують ваші драйвер і пристрій:

$ 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 чудово описано, як це працює і як це використовувати.

При використанні цієї опції для одного сокета, треба в чергу прийому драйвера пристрою передавати для нових даних значення часу в мікросекундах як обсягу часу для поллінг. Коли після установки цього значення, виконується блокуючу читання сокету, ядро буде застосовувати полінг до нових даних.

Також можна налаштувати в sysctl значення net.core.busy_poll — як довго виклики з poll або select повинні чекати прибуття нових даних в умовах навантаженого поллінг(в мікросекундах).

Ця опція дозволяє знизити затримку, але збільшує навантаження на CPU і споживання енергії.

Netpoll: підтримка роботи з мережею в рамках критичних контекстів

Ядро Linux надає спосіб використовувати драйвери пристроїв для відправки і прийому даних на мережевій карті, якщо ядро падає. API для цієї функціональності називається Netpoll. Він використовується різними системами, але особливо kgdb, netconsole.

Netpoll підтримується більшість драйверів. Ваш драйвер повинен реалізувати функцію ndo_poll_controller і прикріпити її до структури struct net_device_ops, що реєструється під час probe.

Коли підсистема мережного пристрою виконує операції з вхідними та вихідними даними, то спочатку перевіряється система Netpoll, щоб визначити, чи не призначений пакет для неї.

Наступний код можна знайти у __netif_receive_skb_core net/dev/core.c:

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, заголовковий файл Netpoll API, 'include/linux/netpoll.h` і цей прекрасний текст.

SO_INCOMING_CPU

Прапор SO_INCOMING_CPU був відсутній в Linux аж до версії 3.19, але він досить корисний, тому я його згадаю.

Для визначення, який CPU обробляє мережеві пакети для конкретного сокета, можна використовувати getsockopt і опцію SO_INCOMING_CPU. Тоді вашу програму зможе використовувати цю інформацію для передачі сокетів тредам, виконує на потрібному CPU. Це допоможе поліпшити локальність даних і частоту попадання в кеш CPU.

Короткий приклад архітектури, коли ця опція корисна, наведено тут: patchwork.ozlabs.org/patch/408257.

Движки DMA

Движок DMA — це апаратна частина, що дозволяє звільняти CPU від великих операцій копіювання, які перекладаються на інше залізо. Так що включення використання движка DMA і запуск коду, що використовує його переваги, повинно привести до зниження навантаження на CPU.

Ядро Linux містить обощенный інтерфейс движка DMA, який може використовуватися авторами драйверів движка. Детальніше про це інтерфейсі можна почитати в документації до ядра.

Ядро підтримує кілька движків DMA, але ми будемо говорити про одне з найпоширеніших — Intel IOAT DMA engine.

Технологія прискорення I/O Intel (intel's I/O Acceleration Technology (IOAT))

Багато сервери містять пакет Intel I/O AT, який вносить у продуктивність ряд змін. Одне з них — використання апаратного движка DMA. Перевірте вихідні дані свого dmesg для ioatdma щоб визначити, завантажений модуль і знайшов він підтримуване обладнання. Движок DMA використовується в ряді місць, але особливо активно — в стеку TCP.

Підтримка движка Intel IOAT була включена в Linux 2.6.18, але в 3.13.11.10 від неї відмовилися через несподіваних багов, повреждавших дані. Користувачі ядер до версії 3.13.11.10 можуть за замовчуванням використовувати модуль ioatdma. Можливо, в майбутніх релізах його пофиксят.

Прямий доступ до кешу (Direct cache access (DCA))

Інша цікава функція, що йде в пакеті Intel I/O AT — Direct Cache Access (DCA).

Вона дозволяє мережевим пристроям (через їх драйвери) поміщати мережеві дані безпосередньо в кеш CPU. Конкретна реалізація залежить від драйвера. У випадку з igb можете подивитися код функції igb_update_dca, а також igb_update_rx_dca. Драйвер igb використовує DCA при запису значень регістрів в мережеву карту.

Щоб використовувати DCA, вам потрібно включити її в BIOS, перевірити, чи завантажений модуль dca і підтримують її ваша мережева карта і драйвер.

Моніторинг движка IOAT DMA

Якщо, незважаючи на ризик пошкодження даних, ви використовуєте модуль ioatdma, то можете моніторити його через деякі записи в sysfs.
Моніторимо загальна кількість розвантажених операцій memcpy DMA-каналі:

$ cat /sys/class/dma/dma0chan0/memcpy_count
123205655

Моніторимо загальна кількість байтів, переданих через DMA-канал:

$ cat /sys/class/dma/dma0chan0/bytes_transferred
131791916307

Налаштування рушія IOAT DMA

Движок IOAT DMA використовується тільки тоді, коли розмір пакета перевищує певний поріг — copybreak. Ця перевірка потрібна тому, що для маленьких копій накладні витрати при встановленні та використанні движка DMA не покривають вигоди від прискорення.

Налаштуємо copybreak для движка DMA:

$ sudo sysctl -w net.ipv4.tcp_dma_copybreak=2048

Значення за замовчуванням-4096.

4. Висновок
Мережевий стек Linux досить складний. Без глибокого розуміння процесів, що відбуваються ви не зможете моніторити або налаштувати його (як і будь-яке інше складне). На просторах інтернету ви можете зустріти приклади sysctl.conf, що містять набори значень, які пропонується скопіювати і вставити на вашому комп'ютері. Це не кращий спосіб оптимізації свого мережевого стека.

Моніторинг мережевого стека вимагає акуратного управління мережевими даними на кожному рівні, починаючи з драйверів. Тоді ви зможете визначати, де відбуваються відкидання і виникають помилки, після чого проводити настройку, зменшуючи ці шкідливі ефекти.

На жаль, легкого шляху тут немає.
Джерело: Хабрахабр

0 коментарів

Тільки зареєстровані та авторизовані користувачі можуть залишати коментарі.