Физический и канальный уровни: Ethernet, ARP и MAC-адресация

Углублённое исследование L1/L2 уровней в Linux: от физического кодирования и структуры кадра Ethernet до механизмов разрешения адресов и управления таблицей соседей в ядре.

Физический уровень (L1): от электрических сигналов до битовых потоков в Linux

Физический уровень (L1): от электрических сигналов до битовых потоков в Linux

Ядро Linux ничего не знает о медных проводах, оптоволокне, фотонах или электромагнитных наводках. Для операционной системы сеть начинается с кольцевого буфера в оперативной памяти и аппаратного прерывания. Но если вытащить кабель из коммутатора, в консоли Linux интерфейс мгновенно перейдет в состояние DOWN. Как именно ядро узнает о физическом разрыве линка, если оно работает только с памятью?

Чтобы стать инженером, который не просто «перезапускает сеть», а понимает первопричины потерь пакетов, необходимо спуститься на самый нижний уровень — туда, где аналоговая физика превращается в цифровой битовый поток.

Анатомия сетевой карты: разделение на PHY и MAC

В предыдущем курсе мы разобрали, как MAC-контроллер (Media Access Control) через механизм DMA перекладывает кадры в Ring Buffer оперативной памяти. Однако сам MAC-контроллер не умеет работать с кабелем.

Любая сетевая карта (NIC — Network Interface Card) концептуально разделена на два фундаментальных блока:

  1. PHY (Physical Layer Transceiver) — чип, отвечающий исключительно за физический уровень (L1).
  2. MAC (Media Access Control) — контроллер, отвечающий за канальный уровень (L2) и взаимодействие с шиной PCIe.

PHY-чип — это переводчик. С одной стороны в него «втыкается» кабель (витая пара или оптика). Здесь царит аналоговый хаос: затухания сигнала, перекрестные помехи, изменения напряжения или вспышки света. Задача PHY — демодулировать этот аналоговый сигнал, очистить его от шума с помощью DSP (Digital Signal Processing) и превратить в идеальные нули и единицы.

С другой стороны PHY соединен с MAC-контроллером на печатной плате сетевой карты. Для передачи битов между ними используется шина MII (Media Independent Interface) или ее современные высокоскоростные аналоги (GMII, XGMII).

Но как Linux управляет PHY-чипом, если процессор общается только с MAC-контроллером через PCIe? Для этого существует отдельная управляющая шина — MDIO (Management Data Input/Output).

Когда вы вводите команду в Linux для изменения скорости линка, происходит следующая цепочка:

  1. Утилита из User Space делает системный вызов (через ioctl/netlink).
  2. Драйвер сетевой карты в ядре получает команду.
  3. Драйвер отправляет инструкцию в регистры MAC-контроллера через шину PCIe.
  4. MAC-контроллер по низкоскоростной шине MDIO передает команду в регистры PHY-чипа.
  5. PHY-чип меняет параметры модуляции электрического сигнала на порту.

Модульность L1: трансиверы SFP

В современных серверах и коммутаторах PHY-чип не всегда намертво впаян в плату. Чтобы не выпускать отдельные сетевые карты для медных кабелей, одномодовой и многомодовой оптики, индустрия перешла на модульный стандарт — SFP (Small Form-factor Pluggable).

При использовании SFP сетевая карта предоставляет стандартизированный слот. Сам модуль SFP содержит внутри себя лазер (или медный коннектор) и часть логики PHY-чипа. Это позволяет «на лету» менять среду передачи данных, просто вставив другой модуль, в то время как MAC-контроллер и драйвер Linux остаются неизменными.

Автосогласование (Autonegotiation): как устройства договариваются

Когда вы соединяете два устройства кабелем, они изначально не знают, какую скорость и режим дуплекса поддерживает собеседник. Передача данных на скорости 1 Гбит/с кардинально отличается по физике кодирования от 100 Мбит/с. Если одно устройство начнет вещать на гигабитной скорости, а второе ожидает 100 мегабит, линк просто не поднимется.

Для решения этой проблемы был создан механизм Autonegotiation (автосогласование).

Как только появляется электрический контакт, PHY-чипы начинают обмениваться специальными короткими импульсами — FLP (Fast Link Pulses). Эти импульсы передаются на базовой, самой медленной скорости (совместимой со старым стандартом 10Base-T), которую гарантированно поймет любой чип.

В FLP-импульсах закодировано «резюме» порта:

  • Поддерживаемые скорости (10, 100, 1000 Мбит/с).
  • Поддерживаемый дуплекс (Half Duplex, Full Duplex).
  • Поддержка управления потоком (Flow Control).

Правило максимального знаменателя При автосогласовании устройства сравнивают свои возможности и всегда выбирают максимально возможную скорость и режим Full Duplex, поддерживаемые обоими концами.

Только после того, как согласование успешно завершено, PHY-чипы переключаются на выбранный метод модуляции (например, PAM-5 для гигабита), устанавливают физический линк и сообщают MAC-контроллеру: «Линк поднят на скорости 1000/Full». MAC генерирует аппаратное прерывание, и ядро Linux меняет статус интерфейса на LOWER_UP.

Взгляд из Linux: утилита ethtool

Если утилита ip (которую мы разбирали ранее) управляет L2 и L3 уровнями ядра, то для работы с L1 (опроса PHY-чипа) в Linux используется утилита ethtool.

Посмотрим на типичный вывод команды ethtool eth0:

Settings for eth0:
    Supported ports: [ TP ]
    Supported link modes:   10baseT/Half 10baseT/Full
                            100baseT/Half 100baseT/Full
                            1000baseT/Full
    Supported pause frame use: Symmetric
    Supports auto-negotiation: Yes
    Advertised link modes:  10baseT/Half 10baseT/Full
                            100baseT/Half 100baseT/Full
                            1000baseT/Full
    Advertised auto-negotiation: Yes
    Speed: 1000Mb/s
    Duplex: Full
    Port: Twisted Pair
    PHYAD: 1
    Transceiver: internal
    Auto-negotiation: on
    Link detected: yes

Разберем ключевые строки:

  • Supported link modes: Аппаратные возможности вашего PHY-чипа (что он в принципе умеет).
  • Advertised link modes: Что ваш PHY-чип сообщает соседу в FLP-импульсах прямо сейчас. Вы можете урезать этот список административно, заставив гигабитную карту притворяться 100-мегабитной.
  • Speed / Duplex: Результат автосогласования. Текущие рабочие параметры L1.
  • Auto-negotiation: on: Механизм FLP включен.
  • Link detected: yes: PHY-чип рапортует о наличии несущего сигнала (carrier) от соседа. Именно этот параметр транслируется в флаг LOWER_UP в выводе ip link.

Классическая проблема L1: Duplex Mismatch

Понимание работы автосогласования необходимо для диагностики одной из самых коварных сетевых проблем — Duplex Mismatch (несовпадение дуплекса).

Исторически сложилось правило: если администратор жестко фиксирует скорость и дуплекс на одном конце линка (отключая Autonegotiation), этот порт перестает отправлять FLP-импульсы.

Представьте ситуацию: сервер Linux подключен к коммутатору.

  1. На коммутаторе администратор жестко задал: Speed 100, Duplex Full (Autonegotiation = off).
  2. На сервере Linux оставлены настройки по умолчанию (Autonegotiation = on).

Что произойдет на физическом уровне? Сервер Linux отправляет FLP-импульсы, но коммутатор молчит (он жестко сконфигурирован). По стандарту Ethernet, если устройство с включенным Autonegotiation не получает ответов, оно обязано определить скорость по электрической сигнатуре (в данном случае это будет 100 Мбит/с), но обязано упасть в безопасный режим Half Duplex.

В результате линк поднимется (Link detected: yes). Утилита ping даже будет работать. Но как только пойдет реальный трафик, сервер (в Half Duplex) будет считать, что канал разделяемый, и прекращать передачу при любой активности коммутатора, а коммутатор (в Full Duplex) будет слать данные непрерывно. Возникнут массовые коллизии, ошибки FCS на L2 и катастрофическое падение пропускной способности.

Умение читать вывод ethtool и понимать границу между PHY и MAC — это первый шаг к локализации проблем, которые невозможно решить на уровне IP-маршрутизации или TCP-тюнинга. Физический уровень сформировал битовый поток. В следующей главе мы разберем, как MAC-контроллер нарезает этот поток на осмысленные блоки — кадры Ethernet II, и как проверяет их целостность.

Анатомия Ethernet II: преамбула, контрольные суммы и типы кадров

Анатомия Ethernet II: преамбула, контрольные суммы и типы кадров

Битовый поток непрерывен. Когда PHY-чип сетевой карты преобразует аналоговые сигналы из кабеля в нули и единицы, он передает их MAC-контроллеру сплошной чередой. В этом потоке нет пауз, пробелов или маркеров начала строки. Возникает фундаментальная инженерная задача: как MAC-контроллер понимает, где в этом бесконечном шуме начинается осмысленный сетевой кадр?

Преамбула и SFD: пробуждение приемника

Чтобы приемник смог корректно прочитать данные, его внутренние часы (генератор тактовой частоты) должны работать абсолютно синхронно с часами передатчика. Если частота немного «уплывет», приемник прочитает один бит как два или пропустит его.

Для калибровки часов используется Преамбула (Preamble) — последовательность из 7 байт, состоящая из чередующихся единиц и нулей: 10101010.

Когда MAC-контроллер видит эту пульсацию, он подстраивает свой таймер под ритм входящего сигнала. Это процесс физического уровня (L1). Но сама по себе преамбула не говорит, где начинаются данные. Для этого нужен восьмой байт — SFD (Start Frame Delimiter). Его структура почти такая же, но два последних бита — единицы: 10101011.

Два подряд идущих бита 11 в конце SFD — это аппаратный триггер. Как только MAC-контроллер считывает их, он понимает: синхронизация завершена, следующий бит — это первый бит MAC-адреса назначения.

Структура кадра Ethernet II

После того как SFD «открыл дверь», начинается сам кадр канального уровня (L2). В современных сетях стандартом де-факто является формат Ethernet II (или DIX Ethernet).

В отличие от преамбулы, которая нужна только аппаратуре и отбрасывается сразу после прочтения, поля Ethernet II — это структура, с которой работает логика канального уровня.

Кадр состоит из строго определенных полей:

  1. Destination MAC (6 байт) — кому предназначен кадр.
  2. Source MAC (6 байт) — кто отправил кадр.
  3. EtherType (2 байта) — тип вложенного протокола.
  4. Payload (от 46 до 1500 байт) — полезная нагрузка (данные верхних уровней).
  5. FCS (4 байта) — контрольная сумма.

Минимальный размер кадра Ethernet II (без преамбулы) составляет 64 байта, максимальный — 1518 байт. Если полезная нагрузка меньше 46 байт, MAC-контроллер отправителя автоматически добавляет «мусорные» нули (Padding), чтобы «добить» кадр до минимального размера в 64 байта. Это историческое ограничение, необходимое для корректного обнаружения коллизий в старых полудуплексных сетях.

EtherType: демультиплексор канального уровня

MAC-адреса обеспечивают доставку кадра до нужного узла, но когда кадр доставлен, ядру Linux нужно понять, какому модулю сетевого стека отдать полезную нагрузку. За это отвечает поле EtherType.

EtherType — это 16-битное значение, которое работает как указатель на протокол следующего (сетевого) уровня.

Значение (Hex) Протокол в Payload Что делает ядро Linux
0x0800 IPv4 Передает данные функции ip_rcv() для обработки IP-пакета.
0x86DD IPv6 Передает данные модулю IPv6.
0x0806 ARP Передает данные функции arp_rcv() для обновления Neighbor Cache.
0x8100 802.1Q (VLAN) Читает следующие 2 байта как тег VLAN, а истинный EtherType сдвигается дальше.

Если бы поля EtherType не существовало, ядро не смогло бы отличить пакет IPv4 от пакета ARP, так как оба они представлены просто массивом байтов внутри Payload.

FCS: тихий убийца битых кадров

Сетевой кабель подвержен электромагнитным помехам. Любой бит в процессе передачи может перевернуться. Чтобы предотвратить обработку искаженных данных, в конце кадра находится поле FCS (Frame Check Sequence).

Механика работы FCS опирается на алгоритм CRC32 (циклический избыточный код):

  1. MAC-контроллер отправителя берет все биты кадра (от Destination MAC до конца Payload), прогоняет их через математическую функцию и получает 32-битное число. Это число записывается в поле FCS.
  2. MAC-контроллер получателя принимает кадр, берет те же поля и самостоятельно вычисляет CRC32.
  3. Получатель сравнивает свой результат со значением, записанным в поле FCS пришедшего кадра.

Если CRCrxFCStxCRC_{rx} \neq FCS_{tx}, кадр считается поврежденным.

Что происходит дальше — важнейший нюанс для системного администратора. MAC-контроллер сетевой карты молча отбрасывает (drop) такой кадр. Он не запрашивает повторную передачу (Ethernet не гарантирует доставку) и, что самое главное, не передает этот кадр в оперативную память через DMA.

Поскольку поврежденный кадр уничтожается на аппаратном уровне (в самом чипе сетевой карты), операционная система о нем ничего не знает. Вы не увидите его в tcpdump или Wireshark, потому что снифферы работают с копиями пакетов из памяти ядра, куда этот кадр просто не доехал.

Единственный способ в Linux узнать, что кабель «сыплет» ошибками на L1/L2 — посмотреть аппаратную статистику MAC-контроллера через ethtool:

ethtool -S eth0 | grep crc

Если счетчик rx_crc_errors растет, значит, кадры бьются «в полете» (плохой обжим, наводки на кабель, умирающий SFP-модуль).

Межпакетный интервал (IPG)

Когда кадр успешно принят, проверен по FCS и отправлен в память, сетевой карте нужно мгновение, чтобы очистить внутренние буферы и подготовиться к приему следующей преамбулы.

Для этого стандарт предписывает отправителю выдерживать паузу между отправкой кадров — Inter-Packet Gap (IPG). Длина этой паузы эквивалентна времени передачи 12 байт (96 бит). В это время в линии искусственно поддерживается тишина (отсутствие сигнала).

Таким образом, полный цикл передачи одного кадра на физическом уровне выглядит так: Преамбула (7) + SFD (1) + Ethernet II (64–1518) + IPG (12).

Успешно принятый и проверенный кадр лишается преамбулы, SFD и FCS. В оперативную память сервера через DMA копируется только «чистый» Ethernet II (от D-MAC до конца Payload). Именно в этот момент ядро Linux выделяет для него структуру sk_buff, и начинается программная жизнь пакета, управляемая логикой MAC-адресации.

MAC-адресация и логика работы коммутации: Unicast, Broadcast и Multicast

MAC-адресация и логика работы коммутации: Unicast, Broadcast и Multicast

Кадр Ethernet II успешно пролетел по медному кабелю, преамбула синхронизировала частоты, а контрольная сумма FCS совпала — данные не повреждены. Теперь MAC-контроллер сетевой карты держит в буфере от 64 до 1518 байт. Возникает вопрос: должен ли он передать этот кадр дальше в память ядра (через DMA), или его нужно немедленно отбросить? Ответ кроется в первых шести байтах кадра — MAC-адресе назначения.

Анатомия MAC-адреса: больше, чем просто идентификатор

Мы привыкли воспринимать MAC-адрес как плоскую строку из 12 шестнадцатеричных символов, например b8:27:eb:14:88:a1. Однако аппаратура смотрит на него иначе. MAC-адрес состоит из 48 бит, которые строго разделены на логические блоки.

Первые 24 бита называются OUI (Organizationally Unique Identifier). Это идентификатор вендора, который выдается производителю комитетом IEEE. Оставшиеся 24 бита — UAA (Universally Administered Address), уникальный серийный номер, который производитель назначает конкретной сетевой карте.

Но самая важная магия происходит в самом первом байте, который передается в сеть. В нем есть два служебных бита, определяющих фундаментальную логику работы L2-уровня:

  1. U/L бит (Universal/Local) — второй младший бит первого байта. Если он равен 00, адрес глобально уникален (зашит на заводе). Если 11 — адрес назначен локально (например, администратор изменил MAC через ip link set eth0 address ...).
  2. I/G бит (Individual/Group) — самый младший бит первого байта. Это главный рубильник канального уровня. Если он равен 00, кадр адресован одному конкретному устройству (Unicast). Если 11 — кадр адресован группе устройств (Multicast или Broadcast).

Именно из-за I/G бита все широковещательные адреса (ff:ff:ff:ff:ff:ff) и адреса групповой рассылки всегда имеют нечетный первый байт (например, 01, 33 или ff).

Аппаратная фильтрация: как сетевая карта бережет CPU

Представьте сервер в дата-центре, подключенный к общему сегменту сети. Каждую секунду мимо него пролетают тысячи чужих кадров. Если бы сетевая карта передавала каждый кадр в ядро Linux (генерируя прерывания или заставляя NAPI поллить Ring Buffer), процессор был бы парализован обработкой чужого мусора.

Поэтому MAC-контроллер выполняет строгую аппаратную фильтрацию. Он пропускает кадр в Ring Buffer только в трех случаях:

  1. Unicast-совпадение: D-MAC кадра точно совпадает с MAC-адресом самой сетевой карты.
  2. Broadcast: D-MAC равен ff:ff:ff:ff:ff:ff. Такие кадры обязаны принимать все узлы в сегменте (например, для работы ARP).
  3. Multicast-подписка: D-MAC является групповым, и ядро Linux ранее явно попросило сетевую карту «слушать» этот конкретный адрес.

Если кадр не попадает ни под одно из этих условий, MAC-контроллер молча уничтожает его прямо в кремнии. Ядро Linux даже не узнает, что кадр существовал.

Исключение: Promiscuous Mode Когда вы запускаете tcpdump или Wireshark, утилита через системный вызов переводит сетевую карту в «неразборчивый режим» (Promiscuous mode, в выводе ip link появляется флаг PROMISC). В этом режиме аппаратный фильтр отключается, и MAC-контроллер начинает сливать в DMA абсолютно все кадры, которые физически долетают до порта.

Прозрачная коммутация (Transparent Bridging)

Сетевая карта решает, принимать ли кадр. Но как кадр находит путь к нужной сетевой карте? За это отвечают коммутаторы (свитчи).

Коммутатор работает по алгоритму Transparent Bridging. «Прозрачным» он называется потому, что конечные узлы (серверы, ПК) даже не подозревают о существовании коммутатора — они просто отправляют кадр в кабель, думая, что общаются напрямую.

Внутри коммутатора есть CAM-таблица (Content-Addressable Memory) — аппаратная хеш-таблица, хранящая соответствие «MAC-адрес \rightarrow Порт». Логика работы коммутатора при получении кадра разбивается на два независимых процесса: обучение и пересылка.

Шаг 1: Обучение (Learning)

Коммутатор всегда смотрит на Source MAC (адрес отправителя). Если этого адреса нет в CAM-таблице, коммутатор записывает его туда, привязывая к порту, на котором был получен кадр. Если адрес уже есть, но порт изменился — запись обновляется. Каждая запись имеет таймер жизни (обычно 300 секунд).

Шаг 2: Пересылка (Forwarding / Flooding)

Затем коммутатор смотрит на Destination MAC (адрес получателя).

  • Если это Known Unicast (адрес есть в CAM-таблице), кадр отправляется только в тот порт, к которому привязан этот MAC.
  • Если это Unknown Unicast (адреса нет в таблице), Broadcast или Multicast, коммутатор выполняет Flooding (лавинную рассылку) — копирует кадр и отправляет его во все порты, кроме того, из которого кадр пришел.

Multicast на стыке L2 и L3

С Unicast и Broadcast всё прозрачно. Но Multicast заслуживает отдельного внимания, так как именно здесь канальный уровень (L2) тесно переплетается с сетевым (L3).

Широковещательный трафик (Broadcast) крайне неэффективен: он заставляет каждый узел в сети прервать работу CPU, чтобы ядро посмотрело на пакет и, скорее всего, отбросило его. Групповая рассылка (Multicast) решает эту проблему: трафик получают только те, кто на него подписался.

Протоколы маршрутизации (OSPF), обнаружения сервисов (Bonjour/mDNS) и потоковое видео используют IP-адреса класса D (от 224.0.0.0224.0.0.0 до 239.255.255.255239.255.255.255). Но коммутаторы L2 ничего не знают про IP-адреса. Им нужен MAC-адрес.

Чтобы не создавать сложных таблиц трансляции, инженеры придумали алгоритм жесткого отображения IPv4 Multicast в MAC Multicast:

  1. Зарезервирован специальный префикс MAC-адреса: 01:00:5e.
  2. В этот префикс копируются младшие 23 бита из Multicast IP-адреса.

Например, протокол OSPF использует IP-адрес 224.0.0.5224.0.0.5. В двоичном виде младшие 23 бита этого адреса — это просто число 55. Добавляем их к префиксу 01:00:5e и получаем MAC-адрес 01:00:5e:00:00:05.

Когда Linux запускает OSPF-демон, ядро программирует аппаратный фильтр сетевой карты: «теперь мы слушаем не только свой Unicast, но и Multicast 01:00:5e:00:00:05». Коммутатор, получив такой кадр, по умолчанию сделает Flooding (если не настроен IGMP Snooping, но это тема продвинутого администрирования). Кадр долетит до всех, но сетевые карты тех серверов, где OSPF не запущен, аппаратно отбросят его, сэкономив ресурсы процессора.

Итог

Мы разобрали, как кадр перемещается внутри одного L2-сегмента. Коммутаторы строят карту сети на лету, а сетевые карты аппаратно фильтруют шум.

Но здесь возникает концептуальный разрыв. Приложение в User Space (например, curl или ping) оперирует только IP-адресами. Когда ядро Linux формирует пакет для отправки на соседний сервер, оно знает его IP-адрес, но для инкапсуляции в Ethernet II ему жизненно необходим D-MAC. Как ядро узнает, какой MAC-адрес соответствует нужному IP-адресу? Для этого существует протокол ARP, к глубокому разбору которого мы переходим на следующем шаге.

Протокол ARP: механизм связывания L2 и L3 уровней

Протокол ARP: механизм связывания L2 и L3 уровней

Ядро Linux сформировало ICMP-эхо запрос, обернуло его в заголовок IPv4 с адресом назначения 192.168.1.5 и готово передать структуру sk_buff драйверу сетевой карты. Но процесс замирает. Чтобы данные физически покинули сервер, IP-пакет должен быть инкапсулирован в кадр Ethernet II. Ядро знает IP-адрес получателя, но коммутаторы не умеют читать IP-заголовки — им нужен Destination MAC. Кадра нет, потому что ядро не знает, какой физический адрес вписать в заголовок L2.

Здесь возникает фундаментальный разрыв между логической адресацией (L3) и физической доставкой (L2). IP-адреса иерархичны и могут меняться, MAC-адреса плоские и вшиты в оборудование. Чтобы сетевой стек мог отправить пакет в локальный сегмент, ему нужен надежный переводчик.

Эту задачу решает ARP (Address Resolution Protocol) — протокол разрешения адресов.

Механизм Request и Reply: спросить всех, ответит один

Когда ядру нужно узнать MAC-адрес узла в локальной сети, оно приостанавливает отправку исходного IP-пакета (помещая его в очередь) и инициирует процедуру ARP.

Процесс состоит из двух шагов:

  1. ARP Request (Запрос). Узел-отправитель формирует специальное сообщение с вопросом: «У кого IP-адрес 192.168.1.5? Сообщите свой MAC-адрес узлу 192.168.1.10». Поскольку отправитель не знает MAC-адрес получателя, он использует широковещательный Destination MAC ff:ff:ff:ff:ff:ff. Коммутатор, получив такой кадр, выполняет Flooding — рассылает его во все порты. Запрос получают абсолютно все устройства в сегменте L2.
  2. ARP Reply (Ответ). Каждое устройство принимает широковещательный кадр и передает его своему ядру. Сетевой стек читает ARP-запрос. Если искомый IP-адрес не совпадает с адресом интерфейса, устройство молча отбрасывает пакет. Но узел с IP 192.168.1.5 узнает себя. Он формирует ответ: «Это я, мой MAC-адрес — 00:1A:2B:3C:4D:5E». Этот ответ отправляется уже как Unicast — строго на MAC-адрес отправителя запроса.

Получив ARP Reply, ядро отправителя извлекает MAC-адрес, вписывает его в ожидающий отправки заголовок Ethernet II и наконец передает исходный IP-пакет на сетевую карту.

Структура ARP: протокол без IP-заголовка

Важнейшая особенность ARP заключается в том, что он не использует IP-адресацию для своей доставки.

Хотя ARP оперирует IP-адресами внутри своего сообщения, сам пакет не имеет заголовка IPv4. Он инкапсулируется напрямую в кадр Ethernet II. Поле EtherType в L2-заголовке принимает значение 0x0806, что сигнализирует ядру получателя: «Внутри не IP-пакет, передай данные обработчику ARP».

Внутри самого ARP-сообщения находятся ключевые поля, которые делают магию связывания:

  • Hardware Type (HTYPE) и Protocol Type (PTYPE): указывают типы связываемых адресов. Для наших сетей это Ethernet (1) и IPv4 (0x0800).
  • Hardware Length (HLEN) и Protocol Length (PLEN): длины адресов (6 байт для MAC и 4 байта для IPv4).
  • Opcode: тип операции. 1 означает Request, 2 — Reply.
  • Sender MAC / Sender IP: кто спрашивает (или кто отвечает).
  • Target MAC / Target IP: кого ищут. В ARP Request поле Target MAC заполнено нулями 00:00:00:00:00:00, так как оно еще неизвестно.

Маршрутизация: почему мы не ищем MAC-адрес Google

Алгоритм ARP Request/Reply работает только в пределах одного широковещательного домена (L2-сегмента). Маршрутизаторы по умолчанию не пропускают широковещательные кадры ff:ff:ff:ff:ff:ff через себя.

Что произойдет, если вы выполните команду ping 8.8.8.8? Отправит ли Linux ARP-запрос «У кого IP 8.8.8.8?» в локальную сеть?

Нет. Перед формированием любого пакета ядро обращается к таблице маршрутизации.

  1. Алгоритм Longest Prefix Match определяет, что адрес 8.8.8.8 не принадлежит ни одной из локальных подсетей сервера.
  2. Ядро находит маршрут по умолчанию (Default Gateway), например, 192.168.1.1.
  3. Ядро понимает: чтобы пакет достиг 8.8.8.8, его нужно физически передать шлюзу 192.168.1.1.
  4. Linux генерирует ARP-запрос для IP-адреса шлюза (192.168.1.1), а не для 8.8.8.8.

В итоге сформированный кадр будет иметь Destination MAC шлюза, но внутри IP-заголовка Destination IP останется 8.8.8.8. Шлюз примет этот кадр аппаратно (MAC совпал), распакует до L3, увидит чужой IP-адрес и смаршрутизирует пакет дальше.

Gratuitous ARP: непрошеные ответы

Существует особый вид ARP-сообщений, который ломает привычную логику «запрос-ответ». Это Gratuitous ARP (GARP) — «безвозмездный» или непрошеный ARP.

В GARP-сообщении поля Sender IP и Target IP совпадают, а отправляется оно широковещательно. Узел буквально кричит на всю сеть: «Я владею адресом 192.168.1.10, и вот мой MAC!», хотя его об этом никто не спрашивал.

Зачем это нужно?

  1. Обнаружение конфликтов IP-адресов. При поднятии сетевого интерфейса ОС отправляет GARP. Если кто-то в сети ответит на него, значит, этот IP уже занят другим устройством, и ОС выдаст предупреждение "IP address conflict".
  2. Обеспечение отказоустойчивости (HA). Представьте кластер из двух маршрутизаторов (VRRP или Keepalived), делящих один виртуальный IP (VIP). Если мастер-узел падает, резервный берет VIP на себя. Но коммутаторы и соседние серверы все еще помнят старый MAC-адрес упавшего мастера. Новый мастер немедленно рассылает GARP. Получив его, все устройства обновляют свои ARP-кеши, а коммутаторы перестраивают CAM-таблицы, направляя трафик в новый порт.

Широковещательные запросы — дорогая операция. Если бы ядру приходилось делать ARP Request перед отправкой каждого пакета, сеть бы задохнулась от служебного трафика, а задержки (latency) выросли бы многократно. Поэтому ядро сохраняет полученные связки IP-MAC в специальную таблицу в оперативной памяти — Neighbor Cache (часто называемую ARP-кешем). О том, как Linux управляет этой таблицей, какие состояния проходит каждая запись и как их диагностировать с помощью ip neigh, мы детально поговорим на следующем шаге.

Состояния ARP-записей в ядре: разбор жизненного цикла Neighbor Cache

Состояния ARP-записей в ядре: разбор жизненного цикла Neighbor Cache

Вы пингуете соседний сервер, протокол ARP успешно разрешает его IP в MAC-адрес, и трафик начинает идти. В этот момент вы физически выдергиваете сетевой кабель из целевого сервера. Как быстро ядро Linux на отправляющей машине поймет, что MAC-адрес больше недоступен, и перестанет слать туда кадры? Ответ неочевиден: ядро будет продолжать инкапсулировать пакеты с уже неактуальным MAC-адресом еще несколько минут, прежде чем окончательно сдастся.

Причина такого поведения кроется в том, что таблица соседей (Neighbor Cache) в Linux — это не просто статичный словарь «IP = MAC». Это сложный конечный автомат, реализующий механизм NUD (Neighbor Unreachability Detection). Его главная задача — балансировать между мгновенной реакцией на сбои и защитой сети от широковещательных ARP-штормов.

Рождение записи: от пустоты к INCOMPLETE

Когда приложение (например, curl) отправляет пакет на локальный IP-адрес, сетевой стек спускает его до уровня L2. Функция ядра ip_finish_output2 проверяет Neighbor Cache. Если записи для нужного IP нет, ядро не отбрасывает пакет приложения. Оно ставит его в специальную очередь (по умолчанию до 3 пакетов) и создает новую запись в состоянии INCOMPLETE.

В этом состоянии у ядра еще нет MAC-адреса. Оно отправляет широковещательный ARP Request (EtherType 0x0806), чтобы его узнать.

Состояние INCOMPLETE означает: «Я знаю, что мне нужно отправить данные на этот IP, я уже спросил MAC-адрес у сети, но ответа пока не получил. Пакеты ждут в очереди».

Если целевой узел не отвечает, ядро сделает несколько попыток (обычно N=3N = 3), после чего запись перейдет в состояние ошибки, а пакеты из очереди будут удалены. Но если ARP Reply приходит, ядро извлекает из него MAC-адрес, применяет его к ожидающим пакетам из очереди и переводит запись в состояние REACHABLE.

Золотое время: REACHABLE

Состояние REACHABLE — это идеальный сценарий. Ядро точно знает, что связка IP и MAC актуальна. Любой пакет, адресованный этому IP, немедленно инкапсулируется в кадр Ethernet II с известным MAC-адресом назначения и отправляется в Ring Buffer сетевой карты без каких-либо дополнительных проверок.

Однако запись не может оставаться в этом состоянии вечно. Сеть динамична: сервер могут перезагрузить, сетевую карту — заменить, а IP-адрес — передать другому узлу через VRRP. Поэтому у состояния REACHABLE есть таймер (Base Reachable Time), который по умолчанию варьируется от 15 до 45 секунд.

Каждый раз, когда ядро получает двустороннее подтверждение связи (например, ответный пакет от этого MAC-адреса), таймер сбрасывается. Если же время истекает, а подтверждений не было, запись деградирует.

Оптимистичная деградация: STALE и DELAY

Самое частое заблуждение сетевых инженеров — считать, что устаревшая запись удаляется из кэша. На самом деле, по истечении таймера REACHABLE запись переходит в состояние STALE (устаревшая).

В состоянии STALE ядро рассуждает так: «Я давно не получал подтверждений от этого узла. Возможно, он мертв. Но удалять MAC-адрес прямо сейчас невыгодно — если приложение вдруг решит отправить туда данные, мне придется снова слать широковещательный ARP Request и задерживать пакет».

Поэтому запись в STALE висит в ядре пассивно. Если трафика нет, она может находиться там часами, пока ее не вычистит сборщик мусора (Garbage Collector).

Но что произойдет, если приложение отправит пакет на IP-адрес, запись которого находится в STALE?

  1. Ядро немедленно отправляет пакет, используя старый MAC-адрес из кэша. Никаких задержек для приложения нет.
  2. Сразу после отправки данных запись переводится в состояние DELAY.

Состояние DELAY — это короткая пауза (по умолчанию 5 секунд). Ядро дает шанс протоколам верхнего уровня (например, TCP) подтвердить, что узел жив. Если в течение этих 5 секунд придет TCP ACK от целевого сервера, ядро поймет: «Ага, данные дошли и вернулись, значит MAC-адрес все еще верный!». Запись триумфально возвращается в REACHABLE без единого ARP-запроса.

Активная проверка и смерть: PROBE и FAILED

Если таймер DELAY истек, а подтверждения от верхних уровней не поступило, ядро берет инициативу в свои руки. Запись переходит в состояние PROBE.

В состоянии PROBE ядро активно проверяет доступность соседа. Но, в отличие от INCOMPLETE, оно отправляет не широковещательный, а одноадресный (Unicast) ARP Request на тот MAC-адрес, который сохранен в кэше. Это критически важное отличие: Unicast-запросы не нагружают CPU всех остальных узлов в широковещательном домене.

Ядро отправляет заданное количество Unicast-проб (обычно N=3N = 3).

  • Если приходит ARP Reply — запись обновляется и возвращается в REACHABLE.
  • Если ответов нет — запись переходит в терминальное состояние FAILED.

В состоянии FAILED ядро окончательно признает, что узел недоступен. Любые новые пакеты к этому IP-адресу будут отбрасываться с ошибкой Destination Host Unreachable. Запись помечается как мертвая и вскоре удаляется сборщиком мусора, освобождая память.

Синтез: таймлайн Neighbor Cache

Чтобы увидеть систему в динамике, проследим непрерывный сценарий.

  1. t=0: Вы запускаете ping 192.168.1.10. Запись создается в INCOMPLETE. Летит Broadcast ARP.
  2. t=1: Приходит ответ. Запись переходит в REACHABLE. Пинги идут без задержек.
  3. t=35: Вы останавливаете пинг. Таймер REACHABLE истекает. Запись переходит в STALE.
  4. t=600: Вы снова запускаете ping. Первый пакет уходит мгновенно по старому MAC. Запись переходит в DELAY.
  5. t=605: Ответного пинга нет (кабель выдернули). Запись переходит в PROBE. Ядро шлет Unicast ARP.
  6. t=608: После трех неудачных проб запись падает в FAILED. Пинг выдает ошибку.

Понимание этого конечного автомата объясняет множество сетевых аномалий. Например, почему при миграции виртуальной машины на другой хост без отправки GARP (Gratuitous ARP) старые клиенты продолжают слать трафик в «черную дыру» — их кэши находятся в состоянии STALE или REACHABLE, и они свято верят, что старый MAC-адрес все еще актуален.

В следующей статье мы разберем, как управлять этими таймерами и лимитами через параметры ядра (sysctl) и утилиту ip neigh, чтобы адаптировать поведение NUD под высоконагруженные среды.

Управление таблицей соседей: глубокая работа с ip neigh и параметрами ядра

Управление таблицей соседей: глубокая работа с ip neigh и параметрами ядра

В логах высоконагруженного Linux-маршрутизатора или узла Kubernetes с тысячами контейнеров однажды может появиться строка: IPv4: neighbor table overflow. В этот момент ядро начинает молча отбрасывать пакеты, предназначенные для новых IP-адресов, хотя с маршрутизацией (L3) и физическим линком (L1) всё в порядке. Проблема кроется в механизмах защиты памяти: ядро строго лимитирует размер таблицы соседей (Neighbor Cache), чтобы избежать исчерпания оперативной памяти при сканировании подсети злоумышленником.

Сборщик мусора и лимиты таблицы

За очистку устаревших связок IP-MAC отвечает системный Garbage Collector (GC). Его поведение регулируется тремя порогами (thresholds), которые определяют агрессивность удаления старых записей.

Эти параметры живут в пространстве sysctl по пути net.ipv4.neigh.default.

  1. gc_thresh1 (по умолчанию 128) — минимальный размер таблицы. Пока количество записей меньше этого значения, сборщик мусора вообще не запускается. Ядро позволяет таблице свободно расти.
  2. gc_thresh2 (по умолчанию 512) — мягкий лимит. Если количество записей превышает этот порог более 5 секунд, ядро запускает принудительную очистку, пытаясь удалить записи в состоянии STALE, чтобы вернуть размер таблицы в норму.
  3. gc_thresh3 (по умолчанию 1024) — жесткий лимит. Если таблица достигает этого размера, ядро синхронно (немедленно) запускает самую агрессивную очистку. Если очистить нечего (все 1024 записи находятся в активном состоянии REACHABLE), ядро блокирует создание новых записей и выдает ошибку neighbor table overflow.

Дефолтные значения (128/512/1024) были установлены десятилетия назад и отлично подходят для десктопов или небольших серверов. Однако для ядра, выступающего шлюзом для сети /16 (65 тысяч адресов), жесткий лимит в 1024 записи приведет к катастрофе: трафик к 1025-му хосту будет отброшен, так как ядро не сможет выделить память под новую ARP-запись.

Таймеры NUD: как ядро отмеряет время жизни

Автомат NUD (Neighbor Unreachability Detection) управляет переходами записей между состояниями. Время, которое запись проводит в каждом состоянии, жестко не зафиксировано — ядро использует рандомизацию для предотвращения сетевых штормов.

Базовое время нахождения в состоянии REACHABLE задается параметром base_reachable_time_ms (по умолчанию 30000 мс, то есть 30 секунд). Но реальное время жизни каждой конкретной записи вычисляется по формуле:

Tact=Tbase×kT_{act} = T_{base} \times k

Где TactT_{act} — фактическое время до перехода в STALE, TbaseT_{base} — базовое время, а kk — случайный коэффициент в диапазоне от 0.50.5 до 1.51.5.

Если бы рандомизации не было, при одновременном включении 500 серверов в стойке их ARP-записи на коммутаторе устаревали бы ровно в одну и ту же миллисекунду. Это вызвало бы лавину одновременных ARP-запросов (ARP Storm), перегружающих CPU маршрутизатора. Благодаря коэффициенту kk, записи «протухают» равномерно в интервале от 15 до 45 секунд.

Когда запись переходит в состояние STALE, в игру вступает параметр gc_stale_time (по умолчанию 60 секунд). Это частота, с которой Garbage Collector проверяет устаревшие записи. Важный нюанс: запись не удаляется ровно через 60 секунд. Этот таймер лишь указывает сборщику мусора интервал проверок. Если таблица не переполнена (мы ниже gc_thresh1), запись в состоянии STALE может висеть в памяти часами, пока ядро не решит освободить память.

Управление через ip neigh: статика против динамики

Утилита ip neigh позволяет не только просматривать состояния автомата NUD, но и вмешиваться в его работу.

Просмотр текущей таблицы: ip neigh show dev eth0

Очистка всех динамических записей на интерфейсе (полезно при массовой смене IP-адресов в сети): ip neigh flush dev eth0

Иногда автомату NUD нельзя доверять. Например, при подключении к балансировщикам нагрузки (Load Balancers) с кластерным MAC-адресом или при защите от ARP-спуфинга (когда злоумышленник подменяет ARP-ответы). В таких случаях администратор жестко привязывает IP к MAC-адресу:

ip neigh add 192.168.1.100 lladdr aa:bb:cc:dd:ee:ff dev eth0 nud permanent

Ключевое слово permanent переводит запись в особое терминальное состояние.

Состояние PERMANENT полностью исключает запись из логики NUD и Garbage Collector. Ядро никогда не пометит её как STALE, никогда не отправит Unicast ARP-запрос (PROBE) для проверки доступности, и сборщик мусора обойдет её стороной даже при превышении gc_thresh3.

Практический кейс: тюнинг для высоконагруженного узла

Представим сервер, который выполняет роль гипервизора для 5000 виртуальных машин в едином L2-сегменте. Дефолтный лимит gc_thresh3 равен 1024. При старте 1025-й виртуалки гипервизор перестанет с ней общаться.

Чтобы адаптировать сетевой стек под такие нагрузки, необходимо изменить параметры ядра через sysctl. Логика настройки требует, чтобы пороги всегда шли по возрастанию.

Увеличиваем лимиты таблицы: sysctl -w net.ipv4.neigh.default.gc_thresh1=4096 sysctl -w net.ipv4.neigh.default.gc_thresh2=8192 sysctl -w net.ipv4.neigh.default.gc_thresh3=16384

Теперь ядро сможет хранить до 16 тысяч соседей. Однако увеличение таблицы означает больший расход оперативной памяти. Каждая запись sk_buff и структура соседа занимают место. Чтобы память не простаивала зря, можно заставить Garbage Collector работать агрессивнее, уменьшив время проверки STALE-записей:

sysctl -w net.ipv4.neigh.default.gc_stale_time=30

Эти изменения применяются на лету. Ядро мгновенно пересчитывает лимиты, и ошибка neighbor table overflow исчезает, позволяя гипервизору беспрепятственно формировать Ethernet-кадры для всех 5000 виртуальных машин.

L2-проблемы и диагностика: коллизии, ошибки FCS и переполнение буферов NIC

L2-проблемы и диагностика: коллизии, ошибки FCS и переполнение буферов NIC

Утилита ping показывает 5% потерь пакетов до соседнего сервера в той же стойке. Вы запускаете tcpdump, чтобы поймать проблемный трафик, анализируете дампы, но не видите в них ничего подозрительного — пакеты просто исчезают в никуда. Этот классический сценарий ставит в тупик многих инженеров. Разгадка кроется в том, что tcpdump работает на уровне ядра ОС, а ваши пакеты умирают раньше, так и не превратившись в программный код.

Мы уже разобрали, как формируются идеальные кадры Ethernet и как работают механизмы MAC и ARP. Но физический мир несовершенен: кабели деградируют, электромагнитные наводки искажают биты, а коммутаторы отправляют данные быстрее, чем сервер может их переварить. В этой статье мы спустимся на аппаратный уровень сетевой карты (NIC) и научимся читать метрики, которые показывают, где именно ломается сеть.

Призраки прошлого: почему мы всё ещё видим коллизии

В классическом Ethernet на коаксиальном кабеле или с использованием хабов среда передачи была общей. Устройства использовали алгоритм CSMA/CD (Carrier Sense Multiple Access with Collision Detection): перед отправкой слушали эфир, а если два узла начинали передачу одновременно, происходила коллизия — сигналы накладывались друг на друга, превращаясь в электрический мусор.

Современные сети строятся на коммутаторах, где каждый линк — это выделенная линия точка-точка (Full-Duplex). Передача (TX) и прием (RX) идут по разным парам проводов или на разных длинах волн. Коллизии физически невозможны.

Почему же в выводе ip -s link счетчик collisions иногда растет?

Причина кроется в логической ошибке — Duplex Mismatch, механику которой мы затрагивали при разборе автосогласования. Если на порту коммутатора жестко задан режим Full-Duplex, а сервер остался в режиме Autonegotiation, сервер не получит FLP-импульсов и откатится в безопасный режим Half-Duplex.

В этом состоянии возникает асимметрия восприятия:

  1. Коммутатор (Full-Duplex) считает, что может отправлять и принимать данные одновременно.
  2. Сервер (Half-Duplex) считает среду полудуплексной. Если он начинает передачу кадра, а в этот момент коммутатор присылает ему встречный кадр, MAC-контроллер сервера фиксирует одновременную активность на линиях RX и TX.
  3. Сервер интерпретирует это как коллизию, прерывает свою передачу, генерирует jam-сигнал (чтобы уведомить «остальных») и увеличивает счетчик collisions.

Если коллизия фиксируется после передачи первых 64 байт кадра, она называется Late Collision (поздняя коллизия). В нормально работающей полудуплексной сети поздних коллизий быть не должно — они однозначно указывают на Duplex Mismatch или превышение максимально допустимой длины кабеля.

Невидимые потери: аппаратное отбрасывание по FCS

Если коллизии — это проблема логики дуплекса, то ошибки контрольной суммы — это проблема физической среды.

Как мы помним, каждый кадр Ethernet II завершается 4-байтным полем FCS (Frame Check Sequence), содержащим хеш CRC32. Если кабель пережат, коннектор RJ-45 окислился, оптика загрязнена или рядом работает мощный источник электромагнитного излучения, один или несколько битов в кадре могут инвертироваться (0 станет 1, или наоборот).

Когда такой кадр попадает в MAC-контроллер сетевой карты, происходит следующее:

  1. MAC-контроллер на лету вычисляет CRC32 для полученной последовательности битов.
  2. Сравнивает результат с полем FCS в конце кадра.
  3. Значения не совпадают. MAC-контроллер аппаратно отбрасывает (drop) этот кадр.

Кадр уничтожается прямо в кремнии сетевой карты. Он не копируется в оперативную память (RAM) через DMA. Ядро Linux о нем ничего не узнает. Утилита tcpdump, которая перехватывает пакеты на уровне драйвера или подсистемы netfilter, ничего не покажет. Вы просто увидите потерю в прикладном софте.

Чтобы увидеть эти потери, нужно обратиться напрямую к регистрам сетевой карты через драйвер. Для этого используется утилита ethtool с флагом -S (Statistics):

ethtool -S eth0 | grep -i crc

Вывод покажет счетчик rx_crc_errors. Если он растет в реальном времени — проблема гарантированно на физическом уровне (L1). Помимо CRC-ошибок, в эту же категорию аппаратных проблем попадают:

  • Runts (rx_frame_errors) — кадры размером менее минимальных 64 байт. Часто возникают как осколки после коллизий.
  • Giants / Jabbers (rx_length_errors) — кадры, превышающие разрешенный MTU (например, пришел Jumbo Frame на 9000 байт, а порт настроен на 1500).
  • Alignment errors — кадры, длина которых не кратна целому числу байт (встречается крайне редко на современном оборудовании).

Когда железо не справляется: переполнение буферов NIC

Представьте, что с физикой всё идеально: кабель цел, дуплекс совпадает, FCS сходится. Но пакеты всё равно теряются, и tcpdump их снова не видит. Мы переходим от проблем целостности к проблемам производительности.

Сетевая карта общается с оперативной памятью через Ring Buffer (кольцевой буфер) — цикличную структуру в RAM, куда NIC через механизм DMA складывает полученные кадры. Драйвер сетевой карты выделяет этот буфер при инициализации интерфейса.

Процесс выглядит как гонка двух процессов:

  1. Producer (Сетевая карта): принимает кадры из кабеля и записывает их в ячейки Ring Buffer.
  2. Consumer (CPU / SoftIRQ): ядро Linux (через механизм NAPI) забирает кадры из Ring Buffer для дальнейшей обработки стеком TCP/IP, освобождая ячейки.

Если на сервер обрушивается микроберст (microburst) — кратковременный, но очень плотный всплеск трафика, — сетевая карта начинает заполнять Ring Buffer быстрее, чем процессор успевает его опустошать.

Когда Ring Buffer заполняется на 100%, сетевой карте больше некуда складывать новые, абсолютно целые и валидные кадры. У нее есть крошечный внутренний FIFO-буфер в самом чипе (обычно несколько килобайт), но он переполняется за микросекунды. После этого NIC вынуждена отбрасывать входящие кадры.

Это событие называется Overrun или Missed. В статистике интерфейса оно отражается в разных счетчиках (в зависимости от драйвера):

ethtool -S eth0 | egrep 'missed|overrun|drop|fifo'

Типичные счетчики:

  • rx_missed_errors — кадры отброшены из-за нехватки места в Ring Buffer.
  • rx_fifo_errors — переполнение внутреннего аппаратного буфера карты.

Как бороться с переполнением буферов

Если вы фиксируете рост rx_missed_errors, у вас есть два основных пути на уровне драйвера:

  1. Увеличить размер Ring Buffer. Проверить текущие и максимальные значения можно командой ethtool -g eth0 (флаг строчной g). Изменить — ethtool -G eth0 rx 4096 (заглавная G). Это даст процессору больше времени на реакцию при микроберстах. Минус — увеличение потребления RAM и потенциальный рост задержки (bufferbloat).
  2. Настроить Interrupt Coalescing (объединение прерываний). Команда ethtool -C eth0 позволяет указать сетевой карте не дергать процессор прерыванием на каждый пакет, а подождать, например, 50 микросекунд или накопить 10 пакетов. Это снижает накладные расходы CPU на контекстные переключения, позволяя ему эффективнее разгребать Ring Buffer.

Синтез: алгоритм локализации L2-потерь

Теперь у нас есть полная картина того, что происходит на границе кабеля и операционной системы. Когда вы сталкиваетесь с потерями на сервере, алгоритм диагностики должен строиться от общего к частному.

Сначала мы смотрим высокоуровневую статистику через ip -s link show eth0:

  • Поле RX errors — это сумма всех поврежденных кадров (FCS, длина).
  • Поле RX dropped — кадры, отброшенные уже внутри ядра (например, из-за нехватки памяти для структур sk_buff или правил фаервола L2).
  • Поле RX overruns — те самые переполнения Ring Buffer.
  • Поле collisions — индикатор проблем с дуплексом.

Если мы видим рост RX errors или RX overruns, мы спускаемся на уровень драйвера через ethtool -S eth0 и ищем конкретную причину:

  • Растет rx_crc_errors → меняем патч-корд, проверяем SFP-модуль, ищем заломы оптики. Проблема аппаратная.
  • Растет rx_missed_errors → увеличиваем Ring Buffer (ethtool -G), проверяем, не перегружено ли ядро (SoftIRQ). Проблема в производительности.

Кадры, которые успешно миновали физические искажения, не столкнулись с коллизиями и поместились в Ring Buffer, наконец-то готовы стать частью программного мира операционной системы.

Синтез: путь кадра от сетевой карты до формирования sk_buff

Синтез: путь кадра от сетевой карты до формирования sk_buff

На линке 10 Гбит/с при минимальном размере кадра в 64 байта сетевая карта принимает около 14,8 миллионов пакетов в секунду. Это значит, что на обработку одного кадра у системы есть всего 67 наносекунд. Если бы центральный процессор побайтово читал каждый кадр из сетевой карты, проверял контрольные суммы и копировал данные в память ядра, сервер не смог бы заниматься ничем, кроме обслуживания сети. Чтобы этого избежать, физический уровень, канальный уровень и ядро Linux работают как единый конвейер, где самые тяжелые задачи делегируются аппаратному обеспечению.

Проследим этот путь от электрического импульса до структуры, готовой к маршрутизации на сетевом уровне.

Аппаратный фильтр: отбраковка на лету

Когда сигнал поступает на порт, в дело вступает чип PHY. Он преобразует аналоговые колебания среды в цифровой битовый поток. Первое, что видит PHY — это преамбула и байт SFD. Они используются исключительно для синхронизации тактовых частот и определения начала кадра. Как только синхронизация завершена, PHY отрезает эти 8 байт — в оперативной памяти они никогда не появятся.

Далее битовый поток передается в MAC-контроллер сетевой карты. MAC выполняет две критически важные проверки, не привлекая ресурсы CPU.

Сначала MAC-контроллер читает первые 6 байт кадра (Destination MAC) и сверяет их со своими аппаратными фильтрами. Кадр будет обрабатываться дальше только в четырёх случаях:

  1. D-MAC точно совпадает с собственным MAC-адресом интерфейса (Unicast).
  2. D-MAC является широковещательным (Broadcast, ff:ff:ff:ff:ff:ff).
  3. D-MAC является многоадресным (Multicast), и ОС ранее сообщила сетевой карте, что подписана на эту группу (аппаратный Multicast-фильтр).
  4. Сетевая карта переведена в Promiscuous mode — аппаратный фильтр отключен, пропускается всё.

Одновременно с приемом данных MAC-контроллер на лету вычисляет контрольную сумму (алгоритм CRC32). Когда кадр принят полностью, результат сравнивается с последними 4 байтами — полем FCS. Если суммы не совпадают (кадр повредился при передаче), он молча уничтожается. Ядро ОС даже не узнает о том, что этот кадр существовал.

Если кадр прошел обе проверки, MAC-контроллер отрезает поле FCS (оно больше не нужно) и готовится передать полезную нагрузку в оперативную память.

Прямой доступ к памяти и Ring Buffer

MAC-контроллер использует механизм DMA (Direct Memory Access), чтобы записать оставшиеся данные кадра (начиная с D-MAC и заканчивая концом Payload) напрямую в оперативную память сервера.

Для этого используется RX Ring Buffer. Драйвер сетевой карты заранее выделяет в RAM кольцевой массив дескрипторов. Каждый дескриптор содержит физический адрес пустой области памяти, куда можно записать кадр.

Сетевая карта берет первый доступный дескриптор, по шине PCIe отправляет данные кадра по указанному адресу в RAM, а затем помечает дескриптор как «заполненный», записывая в него фактический размер принятого кадра. Процессор в этом перемещении данных не участвует.

Пробуждение ядра и рождение sk_buff

Когда кадр (или пачка кадров) оказывается в оперативной памяти, сетевая карта отправляет процессору аппаратное прерывание (Hard IRQ).

Драйвер сетевой карты перехватывает прерывание, немедленно маскирует его на контроллере прерываний (чтобы временно отключить новые Hard IRQ от этой карты и избежать interrupt storm) и планирует выполнение программного прерывания — SoftIRQ NET_RX_SOFTIRQ через механизм NAPI.

В контексте SoftIRQ ядро начинает опрашивать (poll) Ring Buffer. Драйвер видит заполненный дескриптор и инициирует создание главной сетевой структуры ядра Linux — sk_buff.

Здесь проявляется архитектура zero-copy. Ядро выделяет память только для самой структуры метаданных sk_buff (около 200-300 байт). Сами данные кадра (Ethernet-заголовок, IP-пакет, TCP-сегмент и полезная нагрузка), которые DMA записал в RAM, не копируются. Драйвер просто нацеливает указатели skb->head и skb->data на тот адрес в оперативной памяти, куда сетевая карта положила сырой кадр.

Разбор канального уровня: eth_type_trans

На этом этапе у ядра есть структура sk_buff, указывающая на сырой набор байт, начинающийся с Destination MAC. Чтобы передать пакет протоколам верхнего уровня (IP, ARP), ядро должно понять, что это за пакет и кому он адресован логически.

Драйвер вызывает функцию eth_type_trans(). Она выполняет три ключевые операции:

  1. Установка указателя MAC-заголовка. Ядро фиксирует текущее положение указателя data в переменной mac_header. Теперь ядро всегда знает, где в памяти начинается L2-заголовок.
  2. Классификация пакета (skb->pkt_type). Функция анализирует D-MAC и устанавливает флаг типа пакета:
    • PACKET_HOST — кадр адресован конкретно этому узлу (Unicast).
    • PACKET_BROADCAST — широковещательный кадр.
    • PACKET_MULTICAST — многоадресный кадр.
    • PACKET_OTHERHOST — кадр адресован чужому MAC-адресу, но был принят, так как интерфейс работает в Promiscuous mode. (Именно по этому флагу сетевой стек позже отбросит чужой пакет, если он не предназначен для маршрутизации или сниффера вроде tcpdump).
  3. Определение протокола верхнего уровня. Функция читает поле EtherType (13-й и 14-й байты кадра). Если там 0x0800, функция вернет идентификатор протокола IPv4. Если 0x0806 — ARP.

После этого eth_type_trans() сдвигает указатель skb->data на 14 байт вперед (размер Ethernet-заголовка).

С этого момента канальный уровень логически «снят». Данные не удалены из памяти, но для всех последующих функций ядра пакет начинается с IP-заголовка или структуры ARP. Сформированный и размеченный sk_buff передается в функцию netif_receive_skb(), которая отправляет пакет на сетевой уровень (L3) в зависимости от прочитанного EtherType.