Когда разработчик пишет драйвер и хочет предоставить пользовательскому пространству возможность читать статус устройства или менять параметры его работы, он неизбежно приходит к одному месту в файловой системе: /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().