Каждый раз, когда Linux обнаруживает новое устройство, где-то в недрах ядра начинается тихий, но очень конкретный процесс. Система перебирает список зарегистрированных драйверов в поисках подходящего кандидата. Когда кандидат найден, вызывается функция probe(), и если она возвращает ноль, устройство и драйвер считаются связанными. С этого момента железо живёт. Без этого момента оно просто молчит.

Процесс привязки выглядит обманчиво просто. На практике за ним стоит целая архитектура: четыре механизма сопоставления, отложенная привязка при недоступных зависимостях, ручное управление через sysfs и тонкости асинхронного probe. Знание этой архитектуры отделяет разработчика, который понимает, почему драйвер не привязался, от того, кто просто перезагружает систему в надежде, что само пройдёт.

Как шина становится посредником между устройством и драйвером

В модели устройств Linux между устройством и драйвером всегда стоит шина. Именно шина хранит два списка: список устройств и список драйверов. Когда в систему добавляется новое устройство, шина обходит список драйверов в поисках совпадения. Когда регистрируется новый драйвер, шина обходит список устройств в поисках тех, которые ещё не имеют хозяина.

Реальные шины вроде PCI или USB умеют перечислять подключённые устройства самостоятельно. Периферийные блоки SoC на это неспособны, поэтому для них создана виртуальная шина, platform bus. Она работает по тем же правилам, только вместо аппаратного перечисления устройства регистрируются через Device Tree или через статические описания в коде платформы.

Логика сопоставления для платформенных устройств реализована в функции platform_match(). Она проверяет четыре условия строго по приоритету. Сначала проверяется driver_override, если он задан для устройства, привязка допускается только к драйверу с точно совпадающим именем. Затем проверяется таблица OF-совместимости. Следом идёт ACPI-совпадение. И только в самом конце проверяется простое совпадение строк имён:

static int platform_match(struct device *dev, struct device_driver *drv)
{
    struct platform_device *pdev = to_platform_device(dev);
    struct platform_driver *pdrv = to_platform_driver(drv);

    if (pdev->driver_override)
        return !strcmp(pdev->driver_override, drv->name);

    if (of_driver_match_device(dev, drv))
        return 1;

    if (acpi_driver_match_device(dev, drv))
        return 1;

    if (pdrv->id_table)
        return platform_match_id(pdrv->id_table, pdev) != NULL;

    return (strcmp(pdev->name, drv->name) == 0);
}

Понимание этого порядка помогает мгновенно диагностировать отсутствующую привязку. Если of_match_table в драйвере не объявлена или строка compatible не совпадает с тем, что написано в Device Tree, шина даже не дойдёт до проверки имён.

OF-сопоставление через compatible и таблица of_match_table

OF-сопоставление, от Open Firmware, является приоритетным механизмом для платформенных и большинства других устройств в современных системах на базе Device Tree. Оно строится на строке compatible в узле Device Tree с одной стороны и таблице of_device_id в драйвере с другой.

Строка compatible следует соглашению "производитель,устройство". Это не просто рекомендация: без имени производителя возникают конфликты между несовместимыми устройствами от разных вендоров с одинаковым именем блока. Хорошая практика выглядит так:

static const struct of_device_id my_driver_of_match[] = {
    { .compatible = "vendor,my-uart-v2", .data = &uart_v2_config },
    { .compatible = "vendor,my-uart-v1", .data = &uart_v1_config },
    { /* sentinel */ }
};
MODULE_DEVICE_TABLE(of, my_driver_of_match);

Пустой элемент-страж в конце массива обязателен: именно по нему ядро понимает, что таблица закончилась. Поле .data позволяет передавать в probe() конфигурацию, специфичную для версии устройства. Внутри probe() её получают так:

static int my_driver_probe(struct platform_device *pdev)
{
    const struct of_device_id *match;
    const struct my_config *cfg;

    match = of_match_device(my_driver_of_match, &pdev->dev);
    if (!match)
        return -ENODEV;

    cfg = match->data;
    /* Используем cfg->fifo_size, cfg->has_dma и другие поля */
    return 0;
}

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

Макрос MODULE_DEVICE_TABLE не только делает таблицу видимой через /sys/module, но и позволяет утилите depmod встроить информацию о поддерживаемых устройствах в файл modules.alias. Именно благодаря этому udev автоматически загружает нужный модуль при появлении устройства, без каких-либо ручных настроек.

Таблица platform_device_id для устройств без Device Tree

Не каждая система использует Device Tree. Старые платформы, некоторые x86-системы и унаследованные BSP описывают устройства статически в коде. Для таких случаев существует сопоставление через id_table структуры platform_driver.

platform_device_id содержит два поля: имя устройства и driver_data, в котором обычно хранится указатель на конфигурацию, приведённый к kernel_ulong_t:

static const struct platform_device_id my_id_table[] = {
    { "my-device-v1", (kernel_ulong_t)&config_v1 },
    { "my-device-v2", (kernel_ulong_t)&config_v2 },
    { }
};
MODULE_DEVICE_TABLE(platform, my_id_table);

static struct platform_driver my_driver = {
    .probe      = my_driver_probe,
    .remove     = my_driver_remove,
    .id_table   = my_id_table,
    .driver     = {
        .name           = "my-device",
        .of_match_table = my_driver_of_match,
    },
};

Если id_table задана, то поле .driver.name используется только для отображения в sysfs, а не для сопоставления. Если id_table отсутствует, совпадение проверяется именно по .driver.name. Это нередко вызывает путаницу: разработчик задаёт id_table, меняет имя в ней, но не понимает, почему сопоставление по имени перестало работать.

Внутри probe() указатель на совпавший элемент таблицы доступен через поле id_entry структуры platform_device:

static int my_driver_probe(struct platform_device *pdev)
{
    const struct platform_device_id *id = pdev->id_entry;
    const struct my_config *cfg = (struct my_config *)id->driver_data;
    dev_info(&pdev->dev, "Probing %s\n", id->name);
    return 0;
}

Отложенная привязка и механизм EPROBE_DEFER

Встраиваемые системы редко инициализируются в предсказуемом порядке. Драйвер дисплея нуждается в готовом регуляторе питания. Регулятор питания нуждается в готовом I2C-контроллере. I2C-контроллер нуждается в готовом контроллере тактирования. Если тактовый драйвер ещё не завершил свой probe(), все зависящие от него драйверы получают ошибку при попытке запросить тактовый сигнал.

Именно для этой ситуации существует механизм отложенной привязки. Если probe() возвращает -EPROBE_DEFER, ядро помещает устройство в список отложенной привязки и повторяет попытку позже, каждый раз когда какой-либо другой драйвер успешно завершает свой probe(). Логика проста: только что привязавшийся драйвер мог зарегистрировать именно тот ресурс, которого не хватало.

static int my_driver_probe(struct platform_device *pdev)
{
    struct clk *clk;

    clk = devm_clk_get(&pdev->dev, "bus_clk");
    if (IS_ERR(clk)) {
        if (PTR_ERR(clk) == -EPROBE_DEFER)
            return -EPROBE_DEFER;  /* Попробуем позже */
        return PTR_ERR(clk);       /* Настоящая ошибка */
    }
    return 0;
}

Важное ограничение: -EPROBE_DEFER нельзя возвращать после того, как уже были зарегистрированы дочерние устройства. Если дочернее устройство было создано, а затем probe() вернул -EPROBE_DEFER, это может привести к бесконечному циклу вызовов probe() для того же драйвера.

Диагностировать зависшие в отложенном состоянии устройства помогает debugfs:

cat /sys/kernel/debug/devices_deferred
# Пример вывода:
# spi0.0   spi: supplier backlight not ready

Этот интерфейс показывает список устройств, которые не смогли завершить привязку, и причину отложенной привязки прямо в строке вывода. До его появления разработчики были вынуждены прочёсывать весь вывод dmesg в поисках причины, почему драйвер не привязался.

Ручное управление привязкой через sysfs

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

Каждый драйвер в sysfs имеет атрибуты bind и unbind. Чтобы отвязать устройство от драйвера, достаточно записать его bus-идентификатор в файл unbind. Устройство немедленно прекращает обслуживаться этим драйвером:

# Просмотр текущей привязки
ls -la /sys/bus/platform/devices/e0004000.uart/driver
# e0004000.uart -> ../../../../bus/platform/drivers/my-uart

# Отвязать устройство от драйвера
echo -n "e0004000.uart" > /sys/bus/platform/drivers/my-uart/unbind

# Привязать к другому драйверу вручную
echo -n "e0004000.uart" > /sys/bus/platform/drivers/alt-uart/bind

Более тонкий инструмент управления -- атрибут driver_override. Он позволяет задать конкретный драйвер, к которому должно быть привязано устройство, игнорируя автоматическое сопоставление:

# Принудительно привязать к определённому драйверу
echo "my-uart" > /sys/bus/platform/devices/e0004000.uart/driver_override

# Запустить повторное сопоставление
echo -n "e0004000.uart" > /sys/bus/platform/drivers_probe

# Сбросить override и вернуться к автоматическому сопоставлению
echo "" > /sys/bus/platform/devices/e0004000.uart/driver_override

Проверить результат привязки проще всего через вывод sysfs или через dmesg:

# Список всех привязанных устройств платформы
ls /sys/bus/platform/drivers/my-uart/

# Проверка через dmesg
dmesg | grep "my-uart"
# [   2.341] my-uart e0004000.uart: Initialized at phys 0xe0004000

Асинхронный probe и его влияние на время загрузки

По умолчанию probe() каждого драйвера вызывается синхронно. Это означает, что медленные инициализации задерживают весь процесс загрузки последовательно. Современное ядро предоставляет механизм асинхронного probe, который позволяет запускать probe() разных драйверов параллельно.

Конечная цель -- перевести всё ядро на асинхронный probe по умолчанию. Пока это не произошло, аннотация PROBE_PREFER_ASYNCHRONOUS является временной мерой для ускорения загрузки.

Включить асинхронный probe для конкретного драйвера можно через поле probe_type:

static struct platform_driver my_driver = {
    .probe      = my_driver_probe,
    .remove     = my_driver_remove,
    .driver     = {
        .name           = "my-device",
        .of_match_table = my_driver_of_match,
        .probe_type     = PROBE_PREFER_ASYNCHRONOUS,
    },
};

Это поле сигнализирует ядру: данный драйвер готов к параллельному запуску. При этом разработчик берёт на себя ответственность за то, что probe() не имеет скрытых зависимостей от порядка инициализации. Если такие зависимости есть, асинхронный режим породит трудноуловимые гонки состояний.

После успешного завершения probe() устройство регистрируется в классе, к которому принадлежит. Ядро создаёт символические ссылки: в каталоге шины на каталог устройства, в каталоге драйвера на каталог устройства, и в каталоге класса на физическое расположение устройства в дереве sysfs. Именно эта сеть симлинков делает модель устройств Linux навигируемой: из любой точки sysfs можно пройти к связанному драйверу, классу или физическому узлу.

Привязка устройства к драйверу выглядит как простая операция только на уровне концепции. Под капотом это последовательность строго организованных шагов: сопоставление через несколько механизмов по приоритету, вызов probe(), который может потребовать повтора из-за зависимостей, регистрация в классе и выстраивание символических ссылок. Знание каждого из этих шагов превращает отладку несработавшей привязки из гадания по dmesg в систематический анализ с конкретными инструментами на каждом уровне.