nos_
ENRU

Десять мегабит на гигабитном линке: как мы поднимали I210

I210 на AX41 показывал линк, включённый приёмник и 255 дескрипторов — и не принимал ничего: MAC защёлкнул 10 Мбит/с, а PHY работал на 1000.

9 мин чтения
ЖелезоСетьДрайверыОтладкаПроизводительностьRust
Read in English

Один чип в эмуляторе, другой в стойке

AX41 — второй сервер Hetzner, на котором работает nos: AMD Ryzen 5 3600, без последовательного порта, без IPMI и с единственной сетевой картой — Intel I210 (8086:1533). Значит, поднимаемый драйвер был заодно и единственным каналом, через который машина могла хоть что-то сообщить (подробнее о машине).

drivers/igb мы сознательно писали против модели 82576 в QEMU (f773b8a): у двух чипов общая карта регистров, которую Intel публикует, поэтому «драйвер, который можно отлаживать под gdb с захватом пакетов, — тот же самый, которому придётся подняться на железе, где единственная консоль — поднимаемая им сетевая карта». Первую проблему поймал как раз QEMU. Со стороны MAC автосогласование никто не запускает — о нём надо попросить PHY, а пока оно не завершилось, приёмник выбрасывает каждый кадр, и единственный признак этого — одно трассировочное событие:

e1000x_rx_can_recv_disabled  link_up: 0, rx_enabled 1, pci_master 1

I210 приготовил уроков побольше, а gdb на AX41 нет.

PHY с двумя хозяевами

На 82576 драйвер — единственный мастер на шине MDIO. На I210 на той же шине сидит прошивка подсистемы управления. Обе стороны захватывают PHY через SW_FW_SYNC, а этот регистр сам защищён аппаратным мьютексом в SWSM, так что каждый захват — это две блокировки одна в другой: взять мьютекс, выставить бит, отпустить мьютекс (c80c1db). Если захват пропустить, чистой ошибки не будет: два мастера на одной шине MDIO дают чтения, которые выглядят как данные.

/// Claim a resource in SW_FW_SYNC, waiting for firmware to let go of it.
fn swfw_acquire(regs: &io::MmioRegion, mask: u32) -> bool {
    let fwmask = mask << SWFW_FW_SHIFT;

    for _ in 0..200 {
        if !hw_semaphore_get(regs) {
            return false;
        }

        let sync = regs.read32(SW_FW_SYNC);
        if sync & (fwmask | mask) == 0 {
            regs.write32(SW_FW_SYNC, sync | mask);
            hw_semaphore_put(regs);
            return true;
        }

        /* Held by the other side: drop the mutex so it can make progress. */
        hw_semaphore_put(regs);
        udelay(5000);
    }

    false
}

Любое ожидание в драйвере ограничено, поэтому прошивка, которая так и не отпустит PHY, обойдётся неудавшейся инициализацией и строкой в логе, а не повисшей загрузкой (92af233). То же ревью искало, как первая загрузка без консоли может сломаться молча. Драйвер отображал только 64 КиБ из 512-килобайтного BAR, и блок NVM по адресу 0x12010 оставался недоступен. А ещё он не ждал перезагрузки NVM после сброса, так что MAC-адрес мог прочитаться как нулевой, драйвер бы его отверг — и карта просто не появилась бы в системе. В том же коммите есть признание, на которое ответил следующий коммит в igb: «ничего из этого не сверено с даташитом I210, которого у меня нет».

Логу загрузки тоже нужен был выход, раз поднималась сама сетевая карта. В тот же день появился disklog: он пишет каждую строку трассы в сырой раздел, а скрипт под Ubuntu вычитывает лог после следующей загрузки (6996fd4; об этой машине подробнее — в отдельной статье).

Три исправления из даташита

Даташит оказался публичным — документ Intel 333016, 870 страниц, — «и его чтение за полдня разрешило то, в чём на железе ошиблись догадки» (702753e). Счёт на I210 к тому моменту был такой: 434 миллиона прерываний, 144 миллиона проходов опроса и ни одного принятого кадра.

Шторм. EICR, расширенный регистр причин прерывания, сбрасывается чтением, только когда GPIE.Multiple_MSIX равен нулю. Драйвер этот бит выставляет, так что чтение оставляло причину на месте, и вектор срабатывал снова, как только его заново разрешали:

        let eicr = regs.read32(EICR);
        if eicr & EICR_VECTOR0 == 0 {
            return;
        }

        /* Clear it by writing the bits back. EICR is documented as cleared
         * on read only when GPIE.Multiple_MSIX is zero, and this driver sets
         * that bit -- so a read alone leaves the cause standing and the
         * interrupt re-asserts the moment it is armed again. Measured on an
         * I210 before this line existed: 434 million interrupts and 144
         * million poll rounds, with not one frame received. (333016 rev 3.7,
         * section 8.8.3.) */
        regs.write32(EICR, eicr);

Карта Realtek преподала тот же урок днём раньше, только с другой стороны (о потерянном прерывании RTL8125): подтверждай явно и не доверяй сбросу при чтении, определения которого не читал.

Анонс возможностей. Драйвер просил PHY провести согласование, не сказав, что анонсировать. При включённом автосогласовании биты скорости и дуплекса в BMCR игнорируются, а партнёр видит только регистры анонса. В итоге гигабитный порт и гигабитный PHY договорились о 10BASE-T full duplex. Теперь ANAR и регистр управления 1000BASE-T записываются до перезапуска согласования.

Адрес PHY. То ревью начало читать его из MDICNFG, и смысла в этом не было: это поле адресует внешний PHY, а на медной версии любое обращение через MDIC всё равно уходит во встроенный (раздел 3.7.2.2).

На следующей загрузке шторма уже не было — 30 прерываний и 10 проходов опроса, — и стало видно, что он заслонял настоящую проблему (23f1e03). Линк по-прежнему поднимался на 10 Мбит/с, приём по-прежнему не работал, а новый анонс ничего не изменил. В этом раунде PHY стали сбрасывать перед настройкой, потому что прошивка может оставить его на ненулевой странице регистров, где записи анонса попадут совсем не туда. А ещё появился инструмент: igbdump теперь печатает, что анонсировал PHY, что ответил партнёр, и оба регистра 1000BASE-T, причём BMSR читает дважды, потому что бит линка в нём защёлкивается в нуле.

Защёлка

Следующий дамп выглядел безупречно (44ce111): линк есть, приёмник включён, очередь включена, 255 дескрипторов выставлено, фильтр правильный — а указатель головы ни разу не сдвинулся. Новые строки показали, в чём дело: обе стороны анонсировали 1000BASE-T full duplex без ошибки master/slave, а MAC сообщал, что линк работает на 10 Мбит/с.

Даташит объясняет это одной фразой про автоопределение скорости: скорость «настраивается только один раз, после того как PHY выставит сигнал LINK» (в оригинале: «is configured only once after the LINK signal is asserted by the PHY»). Это защёлка, которая срабатывает один раз. PHY, выставивший линк до конца согласования и потом договорившийся о более высокой скорости, оставляет MAC на той, что тот увидел первой. MAC, тактируемый под 10 Мбит/с, против PHY на 1000 не принимает ничего, и при этом каждый регистр, который придёт драйверу в голову проверить, выглядит здоровым.

Поэтому теперь драйвер сам вычисляет, о чём договорилось согласование, выставляет под него CTRL.SPEED и CTRL.FD и сбрасывает ASDE. Результат выводится из анонсов обеих сторон в том порядке, в каком их разрешает 802.3:

fn resolve_link(regs: &io::MmioRegion, phy_addr: u32) -> Option<(u32, bool)> {
    let bmsr = phy_read(regs, phy_addr, PHY_BMSR)?;
    if bmsr & BMSR_ANEGCOMPLETE == 0 {
        return None;
    }

    let ours = phy_read(regs, phy_addr, PHY_ANAR)?;
    let theirs = phy_read(regs, phy_addr, PHY_ANLPAR)?;
    let gctl = phy_read(regs, phy_addr, PHY_GCTL)?;
    let gstat = phy_read(regs, phy_addr, PHY_GSTAT)?;

    /* In the order 802.3 resolves them: fastest common ability wins, full
     * duplex ahead of half at the same speed. */
    if gctl & GCTL_1000_FULL != 0 && gstat & GSTAT_PARTNER_1000_FULL != 0 {
        return Some((CTRL_SPEED_1000, true));
    }
    // ... 1000 half, then 100 and 10, full duplex ahead of half ...

    None
}

Очевидное поле STATUS.ASDV не используется: даташит называет его диагностическим. Первая версия исправления ему доверяла — и в QEMU, где поле не реализовано и читается нулём, драйвер тут же сбросил работающий гигабитный линк до 10 Мбит/с. Анонсы в эту сторону ошибиться не могут. Работа выполняется в опросе приёма, а не в прерывании о смене линка: чтение PHY — это опрос MDIC в течение миллисекунд, а на I210 ещё и после захвата семафора у прошивки. Когда драйвер всё-таки переключает скорость принудительно, он об этом пишет: igb: MAC was latched at speed {} duplex {}, link resolved to {} duplex {}; forced. В QEMU, где MAC защёлкнулся правильно, он не делает ничего, и коммит считает это половиной проверки: «случай, когда этот код не должен ничего делать, так же важен, как случай, когда он должен действовать».

После этого I210 заработал: DHCP получает адрес, ARP и TCP ходят, а UDP-шелл и netconsole доступны через интернет.

Потом — нагрузка

Остальное нашёл поток мелких UDP-датаграмм в 64-байтных кадрах на netload с машины в том же дата-центре — и нашёл методом, отработанным на RTL8125: читать чип, а не только драйвер.

Драйвер сообщал «dropped 0», но кадр, который MAC выбросил ещё до того, как потянулся за дескриптором, в счётчики драйвера не попадает. Поэтому igbdump научился показывать собственную статистику чипа (a937267). За 30 секунд MAC снял с провода 18 392 028 пакетов — 613 тысяч в секунду — и признал годными все, кроме семи. Драйвер собрал 5 815 846, а счётчики переполнения FIFO и нехватки буферов стояли на нуле (f66bae0). Разрыв объяснил счётчик отбросов очереди, RQDPC. В следующем окне в очередь попали 5 067 156 пакетов, а 13 311 963 были отброшены из-за нехватки дескриптора — в сумме ровно число годных. При этом указатели кольца утверждали, что чипу принадлежат все дескрипторы до единого: rdh 101, rdt 100, clean 101, use 100 (412f2d0).

Верно было и то и другое: чипу не хватало дескрипторов на самом кристалле. RXDCTL — это управление предвыборкой дескрипторов, и бит включения делит регистр с тремя порогами. Запись одного бита обнуляла их, а с PTHRESH, равным нулю, чип никогда не делал предвыборку. Теперь пороги записываются явно, а не сохраняются: кремний сбрасывает их в 12/10/1, а QEMU — в ноль, «а исправление, корректность которого зависит от того, на чём из двух оно запущено, — не исправление».

Через неделю под эхо-флудом примерно в 577 тысяч датаграмм в секунду профайлер показал, что треть процессорного времени приёма уходит на два чтения регистров, EICR и ICR: они нужны были на каждом прерывании, чтобы отличить пакет от смены линка (1c292cf). Очередь получила собственный вектор MSI-X, обработчик которого не читает ничего: причину сбрасывает сам чип (EIAC), он же маскирует вектор (EIAM при GPIE.EIAME), а смена линка переехала на второй вектор. Ревью этого изменения ещё до запуска на AX41 поймало шторм, ждавший своего часа: на запасной раскладке с одним вектором новый обработчик оставлял бы EICR неочищенным — снова те же 434 миллиона (f5da442). А первый флуд после каждой загрузки терял 755 105 кадров из 2,06 миллиона за две секунды: кольцо заполнялось из кучи ещё до появления пула кадров, и освобождение каждого такого кадра стоило TLB shootdown с ожиданием всех остальных CPU. Теперь пул поднимается первым (e02dfe3).

Что получилось в итоге (8b64625): эхо на 578–581 тысяче датаграмм в секунду отвечает на каждую, sink на 615 тысячах принимает каждую, dropped-no-descriptor остаётся нулём — и всё это на одном CPU. Потолок теперь у отправителя, чья сетевая карта с единственной очередью сильно не дотягивает до 1,49 миллиона в секунду, которые 1 GbE несёт при таком размере кадра.

Чего не мог показать эмулятор

82576 в QEMU по-прежнему единственное место, где драйвер igb работает вне стойки Hetzner, поэтому его гейт — tcp-test.py --nic igb и netload-test.py --nic igb, и запускаются они вручную, потому что в QEMU у CI этого устройства нет (о тестировании). Только ни одна из ошибок выше там бы не проявилась. Эмулируемый MAC защёлкивает правильную скорость, ASDV читается нулём, на шине MDIO нет прошивки, а приём работает даже с нулевыми порогами предвыборки. К тому же маска прерываний в модели есть только по старым адресам e1000, так что вектор смены линка под QEMU не срабатывал ни разу (7868565). Отлаживаемым настоящий чип сделал дамп, который печатает взгляд PHY рядом со взглядом MAC, а счётчики чипа — рядом со счётчиками драйвера (исходник), — и читали его на машине, где единственной консолью была та самая отлаживаемая сетевая карта.