Точный момент времени на сетевом интерфейсе давно перестал быть прихотью телекоммуникаций: биржевые системы должны доказывать порядок заявок с точностью в наносекунды, сети мобильной связи держат фазу по стандарту G.8275, а распределённые базы используют время для разрешения конфликтов. Проблема в том, что программная метка времени, которую ядро ставит в момент обработки пакета, плывёт на микросекунды и больше: прерывания, очереди, планировщик. Аппаратные метки времени снимают эту неопределённость: сетевой контроллер проставляет время прямо на проводе, при приёме и передаче кадра, и погрешность падает до единиц наносекунд. Регуляторы финансовых рынков требуют привязки событий к единой шкале с точностью до сотен микросекунд, телекоммуникационные профили держат фазу радиочастотных сетей, и в обоих мирах без аппаратной разметки и PTP уже не обойтись. Linux поддерживает весь стек от меток в сокетах до протокола PTP и управления часами контроллера, и управлять им стоит научиться.

Разберём уровни меток, включение аппаратной поддержки, работу с ними в приложении, синхронизацию часов через PTP и практический порядок внедрения.

От программных меток до аппаратных

Метка времени бывает трёх уровней. Программная на приёме ставится ядром в момент прохода пакета, на передаче в момент отправки из сокета, и точность здесь упирается в загрузку машины, типично единицы-десятки микросекунд. Аппаратная на передаче проставляется контроллером в момент фактического ухода кадра на линию, аппаратная на приёме в момент прихода первого бита заголовка, и здесь счёт идёт на наносекунды. Различие между MAC и PHY тоже важно: часть контроллеров метит на уровне MAC, часть умеет метить в PHY, что точнее, потому что ближе к медиа, но требует согласования драйвера с конкретным PHY. Источники разброса программной метки понятны и воспроизводимы: очередь прерываний, мягкие прерывания ядра, вытеснение потоком обработчика, и всё это исчезает из уравнения, как только метку ставит железо в момент физического события.

Для протокола PTP это даёт два режима работы. Двухшаговый режим, классический, отправляет сообщение Sync, затем следом сообщение Follow_Up с меткой времени уходящего Sync, и так умеют почти все контроллеры. Одношаговый режим встраивает метку прямо в поле Sync на проводе, экономя пакет и устраняя задержку между кадрами, но требует явной поддержки со стороны контроллера и включается отдельным значением tx_type. Для приёма фильтры определяют, какие кадры метятся: все подряд, только PTP по UDP или только PTP в формате второго уровня с этерtype 0x88F7, и выбор фильтра экономит ресурсы контроллера на потоках, которым метки не нужны.

Возможности интерфейса и включение через SIOCSHWTSTAMP

Возможности интерфейса показывает ethtool:

ethtool -T eth0

Вывод перечисляет поддерживаемые флаги SOF_TIMESTAMPING, значения tx_type и rx_filter, а также номер аппаратных часов PTP Hardware Clock, по которому находится соответствующее устройство /dev/ptpN. Включение аппаратной разметки делается ioctl SIOCSHWTSTAMP со структурой hwtstamp_config из заголовка linux/net_tstamp.h:

#include <linux/net_tstamp.h>

struct hwtstamp_config cfg = {0};
struct ifreq ifr = {0};

cfg.tx_type = HWTSTAMP_TX_ON;
cfg.rx_filter = HWTSTAMP_FILTER_ALL;
snprintf(ifr.ifr_name, sizeof(ifr.ifr_name), "eth0");
ifr.ifr_data = (char *)&cfg;
ioctl(fd, SIOCSHWTSTAMP, &ifr);

Значение HWTSTAMP_TX_ON включает аппаратные метки на передачу в двухшаговом режиме, HWTSTAMP_TX_ONESTEP_SYNC просит одношаговый, HWTSTAMP_TX_OFF отключает. Фильтр на приёме выбирает класс кадров, от HWTSTAMP_FILTER_NONE до HWTSTAMP_FILTER_ALL и точечных PTP-вариантов для первого и второго уровня. Тот же ioctl читает текущую конфигурацию, если поле flags оставить нулевым, и это удобный способ проверить, что запрос принят именно так, как отправлен. Драйверы трактуют запрос по-разному в деталях: кто-то включает метки для всего устройства, кто-то лишь для заданного фильтра, поэтому после установки стоит перечитать конфигурацию и сверить фактические значения с запрошенными.

Опция SO_TIMESTAMPING в приложении и очередь ошибок

Прикладной интерфейс строится вокруг опции сокета SO_TIMESTAMPING. Флаги задают, какие метки нужны: TX_SOFTWARE и RX_SOFTWARE для программных, TX_HARDWARE и RX_HARDWARE для аппаратных, RAW_HARDWARE чтобы получить сырые показания часов контроллера, а не пересчитанные в системное время. Метки приёма приходят обычным способом через recvmsg с управляющими сообщениями, метки передачи из очереди ошибок сокета. Чтение очереди с разбором обоих типов управляющих сообщений выглядит так:

char ctrl[512];
struct msghdr msg = {0};
msg.msg_control = ctrl;
msg.msg_controllen = sizeof(ctrl);
recvmsg(fd, &msg, MSG_ERRQUEUE);

for (struct cmsghdr *cm = CMSG_FIRSTHDR(&msg); cm;
     cm = CMSG_NXTHDR(&msg, cm)) {
  if (cm->cmsg_level == SOL_SOCKET &&
      cm->cmsg_type == SCM_TIMESTAMPING) {
    struct timespec *ts = (struct timespec *)CMSG_DATA(cm);
    process(ts[2]);
  }
  if (cm->cmsg_level == SOL_SOCKET &&
      cm->cmsg_type == SCM_TIMESTAMPING_OPT_ID) {
    uint32_t id = *(uint32_t *)CMSG_DATA(cm);
    match(id);
  }
}
int tsen = SOF_TIMESTAMPING_TX_HARDWARE | SOF_TIMESTAMPING_RX_HARDWARE |
           SOF_TIMESTAMPING_RAW_HARDWARE | SOF_TIMESTAMPING_OPT_ID;
setsockopt(fd, SOL_SOCKET, SO_TIMESTAMPING, &tsen, sizeof(tsen));

int on = 1;
setsockopt(fd, SOL_SOCKET, SO_SELECT_ERR_QUEUE, &on, sizeof(on));

Метку передачи приложение вычитывает recvmsg с флагом MSG_ERRQUEUE, и в управляющем сообщении SCM_TIMESTAMPING лежит массив из трёх timespec: программная на приёме, программная на передаче и сырая аппаратная. Флаг OPT_ID с опцией SO_TIMESTAMPING присваивает пакетам монотонные идентификаторы, которые возвращаются вместе с меткой, и именно они связывают метку с конкретным отправленным пакетом, когда в полёте их сотни. Отдельно стоит помнить, что метка передачи приходит лишь после фактического ухода кадра, и цикл без чтения очереди ошибок копит переполнение.

PTP и синхронизация часов

Метки интерфейса считаются часами контроллера, устройством /dev/ptpN, и их нужно синхронизировать с остальным миром. Протокол PTP тянет драйвер linuxptp: демон ptp4l ведёт переговоры с мастерами домена и подстраивает часы интерфейса, а phc2sys перегоняет показания часов контроллера в системные часы:

ptp4l -i eth0 -m -s
phc2sys -s /dev/ptp0 -c CLOCK_REALTIME -w
pmc -u -b 0 'GET TIME_STATUS_NP'
phc_ctl /dev/ptp0 cmp

Утилита phc_ctl с командой cmp прямо показывает рассогласование между часами интерфейса и системными, что служит быстрой проверкой здоровья связки. Транспорт выбирается ключами: четвёртый ключ означает PTP поверх UDP, второй уровень кадры 0x88F7, а профили вроде телекоммуникационного G.8275.1 задаются в конфигурационном файле, как и выбор между задержкой насквозь и задержкой до соседа, режимы e2e и p2p. Базовые строки конфигурации выглядят так:

[global]
network_transport L2
delay_mechanism P2P
time_stamping hardware

Профиль G.8275.1 предполагает полную поддержку синхронизации по всей цепочке, где коммутаторы работают пограничными часами, и на таких сетях режим до соседа с метками на каждом участке даёт лучшие фазовые показатели, чем сквозной через десятки хопов. Для качественной синхронизации имеют значение детали, о которых легко забыть: демон ntpd или chrony, держащий системные часы по сети, обязан уступить место PTP, иначе два механизма будут бороться за одну стрелку.

Часы контроллера и дополнительные возможности

Устройство /dev/ptpN живёт по своему API: ioctls PTP_SYS_OFFSET измеряют рассогласование между часами контроллера и системными сериями парных чтений, PTP_EXTTS_REQUEST подписывается на внешние метки времени с входного контакта, PTP_PEROUT_REQUEST настраивает периодический выход, из которого проще всего получается сигнал PPS для сравнения с GPS-приёмником или осциллографом. Управление живёт и в sysfs: файл pps_enable в каталоге /sys/class/ptp/ptp0 включает импульсы в секунду, а счётчики внешних меток читаются оттуда же. Контроллеры свежих поколений ведут отдельные часы на каждый порт и умеют независимо настраивать выходы, так что один сервер сравнивает две независимые трассы времени, не смешивая показания.

Свежие ядра добавили виртуальные часы: атрибут n_vclocks у физического PHC создаёт дочерние часы, которые тянутся за физическими с индивидуальным смещением, и это придумано для виртуализации, где каждой виртуальной машине достаются собственные виртуальные часы поверх одних физических. Плюс интерфейсное поле привязки выводов позволяет назначать физические контакты контроллера функциям, когда на плате есть доступные выводы, и связка внешних меток с периодическим выходом превращает сервер в измерительный прибор.

Практический порядок внедрения на боевом узле

Путь от нуля до наносекундного порядка на узле умещается в шесть шагов:

  1. проверить возможности через ethtool -T и найти часы PTP Hardware Clock в /dev/ptp;
  2. включить аппаратные метки на интерфейсе либо приложением через SIOCSHWTSTAMP, либо конфигурацией ptp4l;
  3. поднять ptp4l на нужном профиле и транспорте, выбрав режим мастера или подписчика;
  4. связать часы контроллера с системными через phc2sys и убрать конкурентов из мира NTP;
  5. выверить задержки приложения на метках SOF_TIMESTAMPING с OPT_ID и убедиться, что хвост лежит в бюджете;
  6. закрепить мониторинг рассогласования через phc_ctl и события ptp4l в системном мониторинге.

Ключевой пункт пятый: метки дают данные, но бюджет задержки остаётся на приложении, и связка идентификаторов с логами запросов превращает метки в измеримый инструмент, а не в красивую цифру. На пути меток из контроллера в приложение остаётся путь прерывания, и его тоже укорачивают: привязка прерываний очереди к ядрам NUMA-узла интерфейса и опция сокета SO_BUSY_POLL, позволяющая приложению опрашивать очередь без сна, сокращают последний отрезок от провода до структуры timespec.

Типичные ошибки и диагностика

Первая классика, приложение просит аппаратные метки, а интерфейс остался с HWTSTAMP_TX_OFF, и молча получает программные фолбэки, потому что SOF_TIMESTAMPING не проверяет, что контроллер действительно метит. Проверка проста: сырые аппаратные метки должны отличаться от программных на характерную величину, и если они совпадают с точностью до микросекунды, метки аппаратными не являются. Вторая классика, спор NTP и PTP за системные часы, о которой речь выше, и её след виден в скачках рассогласования на графиках phc_ctl.

Диагностика опирается на журналы ptp4l, где смещение от мастера видно построчно, на pmc для опроса состояния портов и на счётчики интерфейса: у контроллеров с аппаратными метками есть счётчики пропущенных меток и ошибок PTP, читаемые через ethtool. Отдельный пласт погрешностей живёт в медиа: асимметрия прямого и обратного пути в оптике вносит систематическое смещение, которое в аккуратных инсталляциях компенсируют настройками задержки, и на длинных путях через разнотипную оптоволоконную трассу вклад асимметрии становится заметным на шкале наносекунд. На уровне железа полезно помнить о PHY: если метки заявлены в PHY, а драйвер работает с MAC, включение соответствующего флага в прошивке или драйвере меняет качество разметки радикально. И напоследок дисциплина версий: поддержка PTP в драйверах появлялась поколениями, mlx5 добавлял виртуальные часы после выпуска 5.19, и возможности контроллера стоит проверять не по памяти, а по фактическому выводу ethtool на установленном ядре.

В сумме аппаратные метки времени превращают сеть из источника неопределённости в измерительный прибор: приложение знает момент ухода и прихода кадра с точностью контроллера, а PTP привязывает эти моменты к общей шкале времени. Настроенная система держит наносекундный порядок на узле и микросекундный по ферме, и разница между так и почти так здесь измеряется не эмоциями, а разборами инцидентов, которых больше не случается.