Память ошибается. Это не повод для паники, это физический факт. Космические лучи, электромагнитные помехи, деградация ячеек, температурные флуктуации, всё это приводит к тому, что отдельные биты в модулях DRAM периодически принимают неверные значения. Большинство таких событий происходит бесследно. Один перевёрнутый бит в неиспользуемой странице памяти не вызовет ни одной видимой ошибки. Но тот же бит в структуре данных ядра или в пользовательских данных может обернуться чем угодно: от тихого повреждения файла до паники системы.

Именно для контроля над этим процессом в Linux существует подсистема EDAC, Error Detection and Correction. Её цель сформулирована предельно ясно в документации ядра: обнаруживать и сообщать об ошибках, возникающих в аппаратных компонентах системы. EDAC охватывает контроллеры памяти, кэши процессора, шины PCI и другие компоненты, но центр тяжести подсистемы приходится именно на оперативную память. Большинство серверных платформ, многие рабочие станции и часть встраиваемых систем оснащены ECC-памятью, а значит, у ядра есть аппаратный партнёр для совместной охраны целостности данных.

Четыре класса ошибок и их разная судьба в ядре

Прежде чем разбирать устройство подсистемы, полезно понять, с чем именно ей приходится работать. EDAC различает четыре класса событий, и реакция ядра на каждый из них принципиально отличается.

Исправимая ошибка (Correctable Error, CE) означает, что ECC-логика контроллера обнаружила и исправила одиночный перевёрнутый бит. Данные доставлены корректно, операция завершена успешно. Само по себе одно такое событие не опасно. Опасна тенденция: нарастающий счётчик CE на конкретном DIMM сигнализирует о деградирующей микросхеме памяти задолго до того, как она начнёт выдавать неисправимые ошибки.

Неисправимая ошибка (Uncorrectable Error, UE) возникает тогда, когда ECC не способна восстановить данные. Два и более перевёрнутых бита в одном слове для стандартной SECDED-логики уже за пределами исправления. Если система настроена на panic_on_ue, ядро немедленно останавливает работу. Иначе ошибка фиксируется, а пострадавшая страница памяти помечается как отравленная.

Отложенная ошибка (Deferred Error) представляет собой вариант неисправимой, но не срочной. Аппаратура применила отравление данных, то есть пометила проблемные данные специальным сигналом, и система продолжает работу до тех пор, пока отравленные данные не будут прочитаны. В этот момент ядро принимает решение: вызвать панику, убить процесс или просто сообщить об инциденте.

Фатальная ошибка (Fatal Error) является неисправимой ошибкой, от которой система не смогла восстановиться. Это конечное состояние: ядро паникует вне зависимости от настроек.

Архитектура EDAC и структура mem_ctl_info

Подсистема EDAC построена по принципу разделения ядра и драйверов. Ядро EDAC предоставляет абстрактный API и управляет sysfs-представлением. Драйверы, специфичные для конкретных контроллеров памяти, опрашивают аппаратные регистры и сообщают об ошибках через этот API. Ни один из уровней не знает деталей реализации другого.

Центральной структурой данных является mem_ctl_info. Она описывает один контроллер памяти и является непрозрачной для драйверов: только ядро EDAC имеет право изменять её поля напрямую. Драйвер получает указатель на неё, заполняет допустимые поля при инициализации и передаёт ядру через edac_mc_add_mc().

Иерархия данных внутри mem_ctl_info отражает физическую организацию памяти. Контроллер содержит набор chip-select row, сокращённо csrow. Каждый csrow соответствует физическому ряду адресации в подключённых DIMM. Каждый csrow разбит на каналы, channel. Пересечение csrow и channel однозначно указывает на конкретный физический модуль памяти.

Для двухканального контроллера с двумя слотами на канал картина выглядит так:

           Channel 0    Channel 1
           =============================
csrow0  |  DIMM_A0  |  DIMM_B0  |
csrow1  |  DIMM_A0  |  DIMM_B0  |   <- dual-rank DIMM
           =============================
csrow2  |  DIMM_A1  |  DIMM_B1  |
csrow3  |  DIMM_A1  |  DIMM_B1  |   <- dual-rank DIMM
           =============================

Отсутствие csrow1 в sysfs означает, что DIMM_A0 и DIMM_B0 одноранговые. Наличие csrow0 и csrow1 для одной пары слотов указывает на двухранговые модули.

Написание EDAC-драйвера для контроллера памяти

Разработка EDAC-драйвера начинается с аллокации структуры mem_ctl_info через edac_mc_alloc(). Функция принимает описание иерархии слоёв через массив edac_mc_layer, что позволяет единообразно описывать как простые одноканальные контроллеры, так и многоуровневые конфигурации с несколькими сокетами.

#include <linux/edac.h>

static int my_mc_probe(struct platform_device *pdev)
{
    struct mem_ctl_info    *mci;
    struct edac_mc_layer    layers[2];
    struct my_mc_pvt       *pvt;
    struct dimm_info       *dimm;

    /* Описываем иерархию: 2 csrow x 2 channel */
    layers[0].type       = EDAC_MC_LAYER_CHIP_SELECT;
    layers[0].size       = 2;     /* количество csrow */
    layers[0].is_virt_csrow = true;

    layers[1].type       = EDAC_MC_LAYER_CHANNEL;
    layers[1].size       = 2;     /* количество каналов */
    layers[1].is_virt_csrow = false;

    /* Аллоцируем mci с приватными данными драйвера */
    mci = edac_mc_alloc(0, ARRAY_SIZE(layers), layers,
                        sizeof(struct my_mc_pvt));
    if (!mci)
        return -ENOMEM;

    pvt = mci->pvt_info;
    pvt->base = devm_platform_ioremap_resource(pdev, 0);
    if (IS_ERR(pvt->base)) {
        edac_mc_free(mci);
        return PTR_ERR(pvt->base);
    }

    /* Заполняем метаданные контроллера */
    mci->pdev        = &pdev->dev;
    mci->mtype_cap   = MEM_FLAG_DDR4;
    mci->edac_ctl_cap = EDAC_FLAG_SECDED;
    mci->edac_cap    = EDAC_FLAG_SECDED;
    mci->mod_name    = "my_mc_edac";
    mci->ctl_name    = dev_name(&pdev->dev);
    mci->scrub_mode  = SCRUB_HW_SRC;

    /* Описываем физический DIMM в слоте [0][0] */
    dimm = edac_get_dimm(mci, 0, 0, 0);
    dimm->nr_pages  = (512 * 1024 * 1024) >> PAGE_SHIFT; /* 512 МБ */
    dimm->grain     = 8;         /* гранулярность отчёта об ошибке */
    dimm->dtype     = DEV_X8;
    dimm->mtype     = MEM_DDR4;
    dimm->edac_mode = EDAC_SECDED;
    snprintf(dimm->label, sizeof(dimm->label), "DIMM_A0");

    platform_set_drvdata(pdev, mci);

    /* Регистрируем в ядре EDAC */
    if (edac_mc_add_mc(mci)) {
        edac_mc_free(mci);
        return -ENODEV;
    }

    return 0;
}

После успешного edac_mc_add_mc() контроллер появляется в sysfs и готов принимать отчёты об ошибках.

Отчёт об ошибке через edac_mc_handle_error

Когда аппаратный регистр состояния сигнализирует об ошибке, драйвер читает его и вызывает edac_mc_handle_error(). Именно этот вызов переводит аппаратное событие в запись в sysfs, сообщение в dmesg и при необходимости в панику системы.

static irqreturn_t my_mc_irq_handler(int irq, void *dev_id)
{
    struct mem_ctl_info *mci = dev_id;
    struct my_mc_pvt    *pvt = mci->pvt_info;
    u32  status, syndrome, addr;
    int  csrow, channel;
    enum hw_event_mc_err_type err_type;

    status   = readl(pvt->base + MC_STATUS_REG);
    syndrome = readl(pvt->base + MC_SYNDROME_REG);
    addr     = readl(pvt->base + MC_ADDR_REG);

    if (!(status & MC_ERR_FLAG))
        return IRQ_NONE;

    /* Определяем тип ошибки */
    err_type = (status & MC_UE_FLAG) ? HW_EVENT_ERR_UNCORRECTED
                                     : HW_EVENT_ERR_CORRECTED;

    /* Определяем физическое расположение */
    csrow   = (addr >> MC_CSROW_SHIFT) & MC_CSROW_MASK;
    channel = (addr >> MC_CHAN_SHIFT)  & MC_CHAN_MASK;

    edac_mc_handle_error(
        err_type,
        mci,
        1,                          /* error_count */
        addr >> PAGE_SHIFT,         /* page_frame_number */
        addr & ~PAGE_MASK,          /* offset_in_page */
        syndrome,
        csrow,                      /* top_layer (csrow) */
        channel,                    /* mid_layer (channel) */
        -1,                         /* low_layer (не используется) */
        "ECC error detected",
        ""
    );

    /* Сбрасываем флаг ошибки */
    writel(status, pvt->base + MC_STATUS_REG);

    return IRQ_HANDLED;
}

Параметр error_count позволяет сообщить о нескольких одинаковых ошибках за один вызов, что актуально для контроллеров с регистром переполнения, накапливающим ошибки между опросами.

Интерфейс sysfs и что читать при диагностике

После регистрации контроллера EDAC создаёт иерархию файлов в /sys/devices/system/edac/mc/. Каждый контроллер получает собственный каталог mc0, mc1 и так далее. Внутри расположены каталоги csrowX и файлы с счётчиками ошибок.

Базовая диагностика начинается с чтения счётчиков на уровне контроллера:

# Общее количество исправимых ошибок на mc0
cat /sys/devices/system/edac/mc/mc0/ce_count

# Общее количество неисправимых ошибок
cat /sys/devices/system/edac/mc/mc0/ue_count

# Имя драйвера контроллера
cat /sys/devices/system/edac/mc/mc0/mc_name

# Время с момента сброса счётчиков (в секундах)
cat /sys/devices/system/edac/mc/mc0/seconds_since_reset

Детальная локализация ошибки до конкретного DIMM выполняется через счётчики на уровне csrow:

# Ошибки на csrow0 по каждому каналу
cat /sys/devices/system/edac/mc/mc0/csrow0/ch0_ce_count
cat /sys/devices/system/edac/mc/mc0/csrow0/ch1_ce_count

# Метка DIMM, шёлкографией нанесённая на плату
cat /sys/devices/system/edac/mc/mc0/csrow0/ch0_dimm_label

# Тип памяти в слоте
cat /sys/devices/system/edac/mc/mc0/csrow0/mem_type

Утилита edac-util из пакета edac-utils предоставляет более удобный сводный вывод:

# Краткий отчёт по всем контроллерам
edac-util -s

# Статус загруженных EDAC-драйверов
edac-util --status

# Полный отчёт с нулевыми счётчиками
edac-util -r full

# Только ненулевые значения
edac-util -q -r full

Пример вывода при обнаруженной исправимой ошибке:

mc0: 3 Correctable Errors
mc0:csrow2:ch0: 3 Correctable Errors
DIMM Label: "DIMM_A1"

Эта запись говорит конкретно: три CE зафиксированы на модуле, установленном в слот DIMM_A1. Пора проверить этот модуль на стенде.

Инъекция ошибок для тестирования и верификации

Написать EDAC-драйвер и убедиться, что он корректно реагирует на ошибки, не так просто без возможности эти ошибки воспроизвести намеренно. Часть платформ предоставляет аппаратный механизм инъекции ошибок через специальные регистры.

На платформах Xilinx ZynqMP, например, инъекция доступна непосредственно через sysfs:

# Включить генерацию исправимой ошибки
echo "CE" > /sys/devices/system/edac/mc/mc0/inject_data_poison

# Задать физический адрес для инъекции
echo 0x7EE0EEE0 > /sys/devices/system/edac/mc/mc0/inject_data_error

# Записать данные по указанному адресу (триггер инъекции)
devmem 0x7EE0EEE0 32 0x12345678

# Прочитать адрес обратно (EDAC зафиксирует CE)
devmem 0x7EE0EEE0

После чтения в dmesg появится запись вида:

EDAC MC0: 1 CE Bit Position: 77 Data: 0x00000000
on mc#0csrow#0channel#0 (csrow:0 channel:0 page:0x0
offset:0x0 grain:1 syndrome:0x0)

Для платформ без аппаратной инъекции можно протестировать цепочку отчётности через прямой вызов edac_mc_handle_error() из тестового модуля ядра. Этот подход не имитирует реальную аппаратную ошибку, но проверяет весь путь: от API до записи в sysfs и до вывода в dmesg.

Конфигурация ядра и режим опроса против прерываний

Включение EDAC в конфигурации ядра требует нескольких опций. Базовый набор:

Device Drivers --->
  EDAC (Error Detection And Correction) reporting --->
    [*] EDAC (Error Detection And Correction) reporting
    [*]   Main Memory EDAC reporting
    < >   AMD64 EDAC support      # для AMD платформ
    < >   Intel E7xxx EDAC support
    < >   Intel i7 Core EDAC support

Для встраиваемых платформ с Synopsys DDR-контроллером (Xilinx Zynq, ZynqMP):

[*] Synopsys DDR Memory Controller     (CONFIG_EDAC_SYNOPSYS)

EDAC поддерживает два режима получения данных об ошибках: опрос и прерывания. В режиме опроса ядро читает регистры состояния контроллера по таймеру раз в секунду. Этот режим работает для любого контроллера, но его точность ограничена периодом опроса: несколько ошибок, произошедших между двумя опросами, могут слиться в одну запись в журнале.

Режим прерываний точнее: каждая ошибка фиксируется в момент возникновения. Но он требует, чтобы контроллер поддерживал генерацию прерываний по ошибке, что есть далеко не у всех. Драйвер выбирает режим при инициализации и сообщает о нём через поле edac_check, которое ядро EDAC использует в своём polling-цикле.

Проверить, в каком режиме работает конкретный контроллер:

cat /sys/devices/system/edac/mc/mc0/mc_name
# Вывод содержит "(POLLED)" или "(INTERRUPT)"

# Посмотреть интервал опроса (в миллисекундах)
cat /sys/module/edac_core/parameters/edac_mc_poll_msec
# 1000

# Изменить интервал опроса
echo 500 > /sys/module/edac_core/parameters/edac_mc_poll_msec

Тихо нарастающий счётчик CE на одном DIMM сложнее в обращении, чем громкая неисправимая ошибка. Первая требует внимания и анализа тренда, вторая кричит сама. EDAC предоставляет инструмент для обоих случаев, и разница между системой, которая предупреждает об умирающем модуле памяти за несколько недель до отказа, и системой, которая просто падает в неожиданный момент, нередко сводится к тому, включена ли эта подсистема и читаются ли её данные.