Когда разработчик пишет драйвер и хочет предоставить пользовательскому пространству возможность читать статус устройства или менять параметры его работы, он неизбежно приходит к одному месту в файловой системе: /sys. Именно здесь ядро Linux материализует свою внутреннюю модель устройств в виде привычных файлов и каталогов. Каждый такой файл является атрибутом, и за его созданием, чтением и записью стоит чёткая архитектура, которую полезно знать до того, как атрибут перестаёт появляться в sysfs или udev перестаёт его замечать.

Атрибуты в Linux представляют свойства объектов, доступные для чтения, записи или обоих операций из пространства пользователя. Каждая структура данных, включающая kobject, способна предоставлять собственные атрибуты. Они отображают внутренние данные ядра прямо в файлы sysfs, и этот мост между ядром и пользовательским пространством работает без единого нестандартного системного вызова.

Базовая структура атрибута и три уровня иерархии

Иерархия атрибутов в модели устройств Linux разделена на три уровня: атрибуты шины, атрибуты устройства и атрибуты драйвера. Каждый уровень имеет собственную структуру и собственные макросы объявления, но все они строятся на одном фундаменте, базовой структуре struct attribute:

struct attribute {
    const char  *name;
    umode_t      mode;
};

Поверх этой базовой структуры каждый уровень добавляет функции обратного вызова для чтения и записи. Для атрибутов шины используется struct bus_attribute:

struct bus_attribute {
    struct attribute    attr;
    ssize_t (*show)(struct bus_type *bus, char *buf);
    ssize_t (*store)(struct bus_type *bus,
                     const char *buf, size_t count);
};

Функция show вызывается при чтении файла атрибута из пространства пользователя. Функция store вызывается при записи. Параметр buf в обоих случаях указывает на страницу памяти размером PAGE_SIZE. Функция show обязана вернуть количество записанных байт, функция store обязана вернуть количество потреблённых байт из входного буфера. Нарушение этих соглашений приводит к некорректному поведению при чтении через cat или при записи через echo.

Объявить атрибут шины вручную можно через инициализатор __ATTR, но куда чище использовать готовые макросы:

/* Атрибут только для чтения */
static BUS_ATTR_RO(version);

/* Атрибут для чтения и записи */
static BUS_ATTR_RW(scan_interval);

/* Атрибут только для записи */
static BUS_ATTR_WO(rescan);

Каждый из этих макросов разворачивается в объявление struct bus_attribute с именем bus_attr_<name> и ожидает наличия функций с именами <name>_show и <name>_store в том же файле.

Реализация функций show и store

За каждым макросом объявления стоят две функции, и именно в них сосредоточена вся реальная логика атрибута. Функция show формирует строку для передачи в пространство пользователя, функция store разбирает входную строку и применяет изменение.

Простой атрибут, читающий версию протокола шины:

static ssize_t version_show(struct bus_type *bus, char *buf)
{
    return sysfs_emit(buf, "%d.%d\n",
                      MY_BUS_VERSION_MAJOR,
                      MY_BUS_VERSION_MINOR);
}

static BUS_ATTR_RO(version);

Функция sysfs_emit() предпочтительнее sprintf() начиная с ядра 5.10. Она автоматически учитывает размер буфера PAGE_SIZE и защищает от переполнения. Суффикс \n в строке формата обязателен: без него cat выводит значение без перевода строки, что нарушает ожидания скриптов и утилит.

Для атрибута с записью важно корректно разобрать входную строку и проверить диапазон значений:

static ssize_t scan_interval_show(struct bus_type *bus, char *buf)
{
    return sysfs_emit(buf, "%u\n", my_bus_scan_interval_ms);
}

static ssize_t scan_interval_store(struct bus_type *bus,
                                   const char *buf, size_t count)
{
    unsigned int val;
    int ret;

    ret = kstrtouint(buf, 10, &val);
    if (ret)
        return ret;

    if (val < 10 || val > 60000)
        return -EINVAL;

    my_bus_scan_interval_ms = val;
    return count;
}

static BUS_ATTR_RW(scan_interval);

Функция kstrtouint() безопаснее sscanf(): она строго проверяет формат, отвергает переполнения и возвращает ошибку при наличии лишних символов после числа. Для подписанных целых используют kstrtoint(), для 64-битных значений kstrtou64() и kstrtos64().

Регистрация атрибутов через группы

Исторически атрибуты шины регистрировались по одному через bus_create_file(). Этот подход работает, но требует симметричного ручного удаления каждого атрибута через bus_remove_file() при выгрузке модуля. Пропустить один вызов bus_remove_file() означает оставить в sysfs висячий файл, обращение к которому после выгрузки модуля вызывает обращение к освобождённой памяти.

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

static struct bus_attribute my_bus_attrs[] = {
    __ATTR_RO(version),
    __ATTR_RW(scan_interval),
    __ATTR(rescan, 0200, NULL, rescan_store),
    __ATTR_NULL,
};

static const struct attribute_group my_bus_group = {
    .attrs = (struct attribute **)my_bus_attrs,
};

static const struct attribute_group *my_bus_groups[] = {
    &my_bus_group,
    NULL,
};

Массив заканчивается элементом __ATTR_NULL для атрибутов шины и NULL для групп. Оба признака конца обязательны. Затем группы передаются в структуру bus_type через поле bus_groups:

static struct bus_type my_bus_type = {
    .name       = "mybus",
    .match      = my_bus_match,
    .probe      = my_bus_probe,
    .bus_groups = my_bus_groups,
};

При вызове bus_register() ядро автоматически создаёт все файлы атрибутов. При вызове bus_unregister() они удаляются без единой дополнительной строки кода. Атрибуты шины появляются в каталоге /sys/bus/mybus/, тогда как атрибуты устройств размещаются в каталогах самих устройств, а атрибуты драйверов в подкаталоге drivers.

Атрибуты устройств и атрибуты драйверов

Атрибуты устройств объявляются через DEVICE_ATTR и регистрируются на конкретном объекте устройства. Их функции show и store получают указатель на struct device, что позволяет обращаться к данным конкретного экземпляра через dev_get_drvdata():

static ssize_t status_show(struct device *dev,
                            struct device_attribute *attr,
                            char *buf)
{
    struct my_priv *priv = dev_get_drvdata(dev);
    return sysfs_emit(buf, "%s\n",
                      priv->active ? "active" : "idle");
}

static ssize_t reset_store(struct device *dev,
                            struct device_attribute *attr,
                            const char *buf, size_t count)
{
    struct my_priv *priv = dev_get_drvdata(dev);
    my_device_reset(priv);
    return count;
}

static DEVICE_ATTR_RO(status);
static DEVICE_ATTR_WO(reset);

static struct attribute *my_dev_attrs[] = {
    &dev_attr_status.attr,
    &dev_attr_reset.attr,
    NULL,
};

static const struct attribute_group my_dev_group = {
    .attrs = my_dev_attrs,
};

Регистрация группы устройства в probe() выполняется через devm_device_add_group(). Это managed-вариант, который автоматически удаляет все атрибуты группы при отсоединении устройства:

static int my_driver_probe(struct platform_device *pdev)
{
    struct my_priv *priv;
    int ret;

    priv = devm_kzalloc(&pdev->dev, sizeof(*priv), GFP_KERNEL);
    if (!priv)
        return -ENOMEM;

    platform_set_drvdata(pdev, priv);

    ret = devm_device_add_group(&pdev->dev, &my_dev_group);
    if (ret)
        return ret;

    return 0;
}

Атрибуты драйвера работают аналогично атрибутам шины, но размещаются в каталоге драйвера внутри /sys/bus/<bus>/drivers/<driver>/. Их объявляют через DRIVER_ATTR_RO, DRIVER_ATTR_RW и регистрируют через поле drv_groups структуры device_driver.

Двоичные атрибуты и условная видимость

Строковые атрибуты покрывают большинство задач. Но иногда нужно передать между ядром и пространством пользователя произвольные двоичные данные: образ прошивки, калибровочную таблицу, дамп регистров. Для этого существуют двоичные атрибуты, struct bin_attribute:

static ssize_t calib_read(struct file *filp,
                           struct kobject *kobj,
                           struct bin_attribute *attr,
                           char *buf, loff_t off, size_t count)
{
    struct device *dev = kobj_to_dev(kobj);
    struct my_priv *priv = dev_get_drvdata(dev);

    return memory_read_from_buffer(buf, count, &off,
                                   priv->calib_data,
                                   sizeof(priv->calib_data));
}

static BIN_ATTR_RO(calib, sizeof(struct my_calib_table));

Размер двоичного атрибута фиксируется при объявлении и отображается в файловой системе как размер файла. Пространство пользователя может читать содержимое через read() с произвольным смещением, что удобно при работе с большими буферами через dd.

Условная видимость атрибутов решается через поле .is_visible структуры attribute_group. Это функция, которую ядро вызывает при создании атрибутов: если она возвращает ноль для конкретного атрибута, файл не создаётся. Это удобно, когда одна и та же группа атрибутов используется для нескольких версий устройства, часть из которых не поддерживает определённые возможности:

static umode_t my_attrs_visible(struct kobject *kobj,
                                 struct attribute *attr, int n)
{
    struct device *dev = kobj_to_dev(kobj);
    struct my_priv *priv = dev_get_drvdata(dev);

    if (attr == &dev_attr_dma_status.attr && !priv->has_dma)
        return 0;

    return attr->mode;
}

static const struct attribute_group my_dev_group = {
    .attrs      = my_dev_attrs,
    .is_visible = my_attrs_visible,
};

Диагностика и работа с атрибутами из командной строки

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

Типичные операции с атрибутами шины:

# Прочитать версию протокола шины
cat /sys/bus/mybus/version

# Прочитать текущий интервал сканирования
cat /sys/bus/mybus/scan_interval

# Установить новый интервал
echo 500 > /sys/bus/mybus/scan_interval

# Проверить статус конкретного устройства
cat /sys/bus/mybus/devices/mydevice0/status

# Сбросить устройство
echo 1 > /sys/bus/mybus/devices/mydevice0/reset

Для автоматизации реакции на изменения атрибутов udev использует uevent-правила. Когда устройство появляется в системе, udev читает атрибуты из sysfs для принятия решений: какой модуль загрузить, какие права назначить файлу устройства, какой сервис уведомить. Корректно оформленные атрибуты с именами, следующими соглашениям ядра, считываются udev без дополнительных настроек.

Проверить, какие атрибуты реально присутствуют для устройства и каковы их текущие права доступа, позволяет команда:

ls -la /sys/bus/mybus/devices/mydevice0/

Если атрибут не появился в sysfs после регистрации, первое место для диагностики это dmesg. Ядро печатает предупреждение при конфликте имён атрибутов в одной группе и при попытке зарегистрировать атрибут с нулевыми правами доступа. Второе место для проверки это убедиться, что группа действительно передана в правильное поле структуры: bus_groups для шины, dev_groups для устройства, drv_groups для драйвера. Перепутать их легко, а симптом один и тот же: атрибут есть в коде, но в sysfs его нет.

Атрибуты шин и устройств решают задачу, которая иначе потребовала бы либо кастомного /proc-интерфейса, либо специального ioctl. Унифицированный механизм sysfs делает поведение драйвера предсказуемым для инструментов мониторинга, систем управления конфигурацией и пользовательских скриптов. Именно поэтому правильно реализованные атрибуты считаются частью хорошего тона в разработке драйверов Linux наравне с корректным управлением ресурсами и обработкой ошибок в probe().