Network stack
Коротко
Пакет проходит: Провод -> NIC -> DMA -> Ring Buffer -> Hard IRQ -> Soft IRQ (NAPI) -> Сетевой стек ядра -> Socket Buffer -> Userspace.
Ключевые механизмы: DMA (без участия CPU), прерывания (hard/soft), NAPI (polling вместо interrupt storm), sk_buff (структура пакета в ядре).
Полный путь пакета (RX)
┌─────────────────────────────────────────────────────────────────┐
│ ФИЗИЧЕСКИЙ УРОВЕНЬ │
├─────────────────────────────────────────────────────────────────┤
│ Провод/оптика │
│ ↓ │
│ Трансивер (PHY) - преобразование сигнала │
│ ↓ │
│ MAC (Media Access Control) - проверка FCS, извлечение фрейма │
└─────────────────────────────────────────────────────────────────┘
↓
┌─────────────────────────────────────────────────────────────────┐
│ NIC (Network Interface Card) │
├─────────────────────────────────────────────────────────────────┤
│ 1. Принимает фрейм в hardware buffer │
│ 2. Проверяет checksum (если поддерживается) │
│ 3. DMA: копирует данные в Ring Buffer (RAM) │
│ 4. Обновляет RX descriptor │
│ 5. Генерирует Hardware Interrupt │
└─────────────────────────────────────────────────────────────────┘
↓
┌─────────────────────────────────────────────────────────────────┐
│ HARD IRQ (аппаратное прерывание) │
├─────────────────────────────────────────────────────────────────┤
│ CPU прерывается, выполняет interrupt handler драйвера: │
│ - Отключает прерывания от NIC (чтобы не было interrupt storm) │
│ - Вызывает napi_schedule() - планирует обработку │
│ - Возвращает управление (минимум работы) │
└─────────────────────────────────────────────────────────────────┘
↓
┌─────────────────────────────────────────────────────────────────┐
│ SOFT IRQ / NAPI (отложенная обработка) │
├─────────────────────────────────────────────────────────────────┤
│ ksoftirqd обрабатывает NET_RX_SOFTIRQ: │
│ - net_rx_action() вызывает poll-функцию драйвера │
│ - Драйвер читает пакеты из Ring Buffer (polling, не interrupt) │
│ - Создает sk_buff для каждого пакета │
│ - Передает в napi_gro_receive() или netif_receive_skb() │
│ - Когда Ring Buffer пуст - включает прерывания обратно │
└─────────────────────────────────────────────────────────────────┘
↓
┌─────────────────────────────────────────────────────────────────┐
│ СЕТЕВОЙ СТЕК ЯДРА │
├─────────────────────────────────────────────────────────────────┤
│ netif_receive_skb() │
│ ↓ │
│ Определяет протокол L3 (ETH_P_IP, ETH_P_IPV6...) │
│ ↓ │
│ ip_rcv() - обработка IP │
│ - Проверка заголовка, checksum │
│ - PREROUTING (netfilter/iptables) │
│ - ip_route_input() - lookup маршрута │
│ - INPUT или FORWARD │
│ ↓ │
│ tcp_v4_rcv() - обработка TCP │
│ - Поиск сокета по 4-tuple (src ip:port, dst ip:port) │
│ - TCP state machine (ACK, sequence numbers) │
│ - Добавление в sk_receive_queue сокета │
│ ↓ │
│ sock_def_readable() - будит процесс │
└─────────────────────────────────────────────────────────────────┘
↓
┌─────────────────────────────────────────────────────────────────┐
│ USERSPACE │
├─────────────────────────────────────────────────────────────────┤
│ Процесс разблокируется (epoll_wait/select/read) │
│ ↓ │
│ recv()/read() - системный вызов │
│ ↓ │
│ copy_to_user() - копирование из sk_receive_queue в userspace │
│ ↓ │
│ Данные доступны приложению │
└─────────────────────────────────────────────────────────────────┘Ring Buffer
Кольцевой буфер в RAM. NIC пишет туда через DMA. Драйвер читает оттуда.
Структура:
- RX descriptor ring - массив дескрипторов (указатели на буферы + метаданные)
- Буферы данных - память под сами пакеты
┌───┬───┬───┬───┬───┬───┬───┬───┐
│ 0 │ 1 │ 2 │ 3 │ 4 │ 5 │ 6 │ 7 │ <- дескрипторы
└─┬─┴─┬─┴───┴───┴───┴─┬─┴─┬─┴───┘
│ │ │ │
↓ ↓ ↓ ↓
[buf][buf] [buf][buf] <- буферы с данными
↑ ↑
│ │
HEAD TAIL
(NIC пишет) (драйвер читает)Переполнение (overflow) - пакеты теряются. Счетчик: ethtool -S eth0 | grep rx_missed.
sk_buff (skb)
Главная структура для пакета в ядре. Не содержит данные напрямую - только метаданные и указатели.
struct sk_buff {
/* Указатели на заголовки */
transport_header -> TCP/UDP header
network_header -> IP header
mac_header -> Ethernet header
/* Размеры */
len -> общий размер данных
data_len -> размер в page fragments
/* Указатели на данные */
head -> начало буфера
data -> начало данных
tail -> конец данных
end -> конец буфера
/* Очередь */
next, prev -> двусвязный список
}Hard IRQ vs Soft IRQ
Hard IRQ (top half):
- Вызывается железом немедленно
- Прерывает текущую работу CPU
- Нельзя блокироваться
- Минимум работы: acknowledge + schedule soft irq
- Другие прерывания заблокированы
Soft IRQ (bottom half):
- Выполняется ksoftirqd или после hard irq
- Можно прерывать
- Основная обработка пакетов
- Ограничен по времени и количеству пакетов
NAPI (New API)
Проблема: при высокой нагрузке - interrupt storm (тысячи прерываний в секунду).
Решение NAPI:
- Первое прерывание будит обработчик
- Обработчик отключает прерывания
- Polling: читает пакеты из ring buffer пока есть
- Когда ring buffer пуст - включает прерывания
При низкой нагрузке: 1 прерывание на пакет. При высокой: 1 прерывание на множество пакетов.
Interrupt Coalescing
NIC накапливает несколько пакетов перед генерацией прерывания.
Параметры:
- rx-usecs: ждать N микросекунд
- rx-frames: ждать N пакетов
Компромисс: меньше прерываний = выше throughput, но выше latency.
Hardware Offloads
NIC берет на себя часть работы CPU. Без offloads 100G невозможен - CPU не успеет.
Checksum Offload
- rx-checksumming: NIC проверяет checksum, ядро не пересчитывает
- tx-checksumming: NIC считает checksum при отправке
Segmentation Offload
- TSO (TCP Segmentation Offload): приложение отдает большой chunk (64KB), NIC сам нарезает на MSS-сегменты
- GSO (Generic Segmentation Offload): то же, но в софте (fallback если нет TSO)
- UFO (UDP Fragmentation Offload): аналог для UDP
Receive Offload
- GRO (Generic Receive Offload): склеивает мелкие пакеты одного потока в большие перед передачей в стек
- LRO (Large Receive Offload): аналог на железе (агрессивнее, может ломать routing/bridging)
RSS (Receive Side Scaling)
- NIC хеширует 5-tuple (src/dst IP, src/dst port, protocol)
- Распределяет пакеты по разным RX-очередям
- Каждая очередь - свой CPU
- 100G карта: 32-128 очередей = параллельная обработка
Flow Director / ATR
- NIC запоминает flow и отправляет на тот же CPU где сокет
- Лучше cache locality
| |
Типичный вывод ethtool -k:
rx-checksumming: on
tx-checksumming: on
scatter-gather: on
tcp-segmentation-offload: on
generic-receive-offload: on
large-receive-offload: off # обычно off, ломает forwarding
receive-hashing: on # RSSЧто дает offload:
Без TSO (CPU нарезает):
App: 64KB данных
↓
Kernel: создает ~44 sk_buff по 1460 байт
↓
NIC: отправляет 44 пакетаС TSO (NIC нарезает):
App: 64KB данных
↓
Kernel: создает 1 большой sk_buff
↓
NIC: сам нарезает и отправляет 44 пакетаЭкономия: меньше sk_buff allocations, меньше проходов по стеку.
Команды
| |
Показать размер ring buffer (текущий и максимальный).
| |
Увеличить RX ring buffer до 4096.
| |
Показать настройки interrupt coalescing.
| |
Установить задержку прерывания 100 мкс.
| |
Включить адаптивный coalescing.
| |
Проверить потери пакетов на уровне NIC.
| |
Прерывания по CPU для сетевой карты.
| |
Статистика softirq (столбцы: processed, dropped, time_squeeze).
| |
Счетчики NET_RX_SOFTIRQ и NET_TX_SOFTIRQ.
Примеры
Диагностика потерь пакетов:
| |
Тюнинг для высокой нагрузки:
| |
Тюнинг для низкой latency:
| |