Шины, устройства и драйверы

Предыдущая  Содержание  Следующая V*D*V

До сих пор мы видели немало низкоуровневой инфраструктуры и относительно мало примеров. Мы постараемся компенсировать это в оставшейся части этой главы, как только мы перейдём на более высокие уровни модели устройства Linux. С этой целью мы вводим новую виртуальную шину, которую мы называем lddbus (* Логичным именем для этой шины, конечно, было бы "sbus", но это название уже было взято настоящей, физической шиной.), и изменяем драйвер scullp для "подключения" к этой шине. Ещё раз, большая часть прочитанного здесь никогда не потребуется многим авторам драйверов. Подробности этого уровня, как правило, обрабатываются на уровне шины, и небольшому числу авторов потребуется добавить новый тип шины. Однако, эта информация является полезной для тех, кому интересно, что происходит внутри PCI, USB и других уровнях или кому требуется сделать изменения на этом уровне.

Шины

Шина является каналом между процессором и одним или несколькими устройствами. Для целей модели устройства, все устройства подключаются через шину, даже если она является внутренней, виртуальной, "инструментальной" шиной. Шины могут подключаться друг в друга - контроллер USB, к примеру, обычно представляет собой PCI устройство. Модель устройства отражает фактические связи между шинами и устройствами, которыми они управляют.

 

В модели устройства Linux шина представлена структурой bus_type, определённой в <linux/device.h>. Эта структура выглядит следующим образом:

 

struct bus_type {

    char *name;

    struct subsystem subsys;

    struct kset drivers;

    struct kset devices;

    int (*match)(struct device *dev, struct device_driver *drv);

    struct device *(*add)(struct device * parent, char * bus_id);

    int (*hotplug) (struct device *dev, char **envp,

                    int num_envp, char *buffer, int buffer_size);

    /* Некоторые поля опущены */

};

 

Поля name является именем этой шины, что-нибудь такое, как pci. Вы можете видеть из структуры, что каждая шина имеет собственную подсистему; однако, эти подсистемы не живут на верхнем уровне в sysfs. Вместо этого, они находятся под шинной подсистемой. Шина содержит два kset-а, представляющих известные драйверы для этой шины и все устройства, подключенные к шине. Затем существует набор методов, которые мы получим в ближайшее время.

Регистрация шины

Как мы уже отмечали, исходник примера включает реализацию виртуальной шины под названием lddbus. Эта шина создаёт свою структуру bus_type следующим образом:

 

struct bus_type ldd_bus_type = {

    .name = "ldd",

    .match = ldd_match,

    .hotplug = ldd_hotplug,

};

 

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

 

Безусловно, новая шина должна быть зарегистрирована в системе с помощью вызова bus_register. Код lddbus делает это таким образом:

 

ret = bus_register(&ldd_bus_type);

if (ret)

    return ret;

 

Этот вызов может, конечно, оказаться неудачным, так что возвращаемое значение всегда должно быть проверено. Если он успешен, новая шинная подсистема была добавлена в систему; это видно в sysfs в /sys/bus и можно начинать добавлять устройства.

 

Если необходимо удалить шину из системы (когда, например, удаляется связанный с ней модуль), должна вызываться bus_unregister:

 

void bus_unregister(struct bus_type *bus);

Методы шины

Есть несколько методов, определённых для структуры bus_type; они позволяют коду шины выступать в качестве посредника между ядром устройств и отдельными драйверами. Методами, определёнными в ядре версии 2.6.10 являются:

 

int (*match)(struct device *device, struct device_driver *driver);

Этот метод вызывается, возможно, несколько раз, каждый раз, когда к этой шины добавляется новое устройство или драйвер. Она должна вернуть ненулевое значение, если данный device может быть обработан данным драйвером. (Мы скоро узнаем детали структур драйвера device и device_). Эта функция должна обрабатываться на уровне шины, потому что именно здесь существует соответствующая логика; основное ядро не может знать, как соответствовать устройствам и драйверам для всех возможных типов шины.

 

int (*hotplug) (struct device *device, char **envp, int num_envp, char *buffer, int buffer_size);

Этот метод позволяет шине добавить переменные в окружение перед генерацией события горячего подключения в пользовательском пространстве. Его параметры такие же, как и для метода hotplug kset-а (описанных в предыдущем разделе "Генерация события горячего подключения").

 

Драйвер lddbus имеет очень простую функцию совпадения, которая просто сравнивает имена драйвера и устройства:

 

static int ldd_match(struct device *dev, struct device_driver *driver)

{

    return !strncmp(dev->bus_id, driver->name, strlen(driver->name));

}

 

Когда речь идёт о реальном оборудовании, функция match обычно делает какое-то сравнение между идентификационным номером оборудования, предоставляемым самим устройством, и идентификаторами, поддерживаемыми драйвером.

 

Метод горячего подключения в lddbus выглядит следующим образом:

 

static int ldd_hotplug(struct device *dev, char **envp, int num_envp, char *buffer, int buffer_size)

{

    envp[0] = buffer;

    if (snprintf(buffer, buffer_size, "LDDBUS_VERSION=%s",

                    Version) >= buffer_size)

        return -ENOMEM;

    envp[1] = NULL;

    return 0;

}

 

Здесь мы добавляем в него номер текущей версии исходного кода lddbus, на случай, если кто-нибудь полюбопытствует.

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

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

 

Для работы с каждым устройством, известном шине, используйте:

 

int bus_for_each_dev(struct bus_type *bus, struct device *start,

                     void *data, int (*fn)(struct device *, void *));

 

Эта функция перебирает все устройства на шине, передавая связанную структуру device в fn, вместе со значением, передаваемым как data. Если start является NULL, итерация начинается с первого устройства на шине; в противном случае итерация начинается с первого устройства после start. Если fn возвращает ненулевое значение, итерация останавливается и такое значение является возвращённым из bus_for_each_dev.

 

Существует аналогичная функция для перебора драйверов:

 

int bus_for_each_drv(struct bus_type *bus, struct device_driver *start,

                     void *data, int (*fn)(struct device_driver *, void *));

 

Эта функция работает подобно bus_for_each_dev, за исключением, конечно, что вместо этого она работает с драйверами.

 

Следует отметить, что обе эти функции в течение работы удерживают шинный семафор подсистемы чтения/записи. Так что попытка использовать их обе вместе приведёт к взаимоблокировке, каждая будет пытаться получить один и тот же семафор. Операции, которые изменяют шину (такие, как отмена регистрации устройств), также будут блокироваться. Таким образом, используйте функцию bus_for_each с некоторой осторожностью.

Атрибуты шины

Почти каждый слой в модели устройства Linux предоставляет интерфейс для добавления атрибутов и уровень шины не является исключением. Тип bus_attribute определён в <linux/device.h> следующим образом:

 

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);

};

 

Мы уже видели struct attribute в разделе "Атрибуты по умолчанию". Тип bus_attribute также включает в себя два метода для отображения и установки значения атрибута. Большинство уровней модели устройства над уровнем kobject-а работают таким образом. Для создания во время компиляции и инициализации структуры bus_attribute был предусмотрен удобный макрос:

 

BUS_ATTR(name, mode, show, store);

 

Этот макрос декларирует структуру, создавая её название, предваряя заданное name строкой bus_attr_.

 

Любые атрибуты, принадлежащие шине, должны быть созданы явно с помощью bus_create_file:

 

int bus_create_file(struct bus_type *bus, struct bus_attribute *attr);

 

Атрибуты также могут быть удалены с помощью:

 

void bus_remove_file(struct bus_type *bus, struct bus_attribute *attr);

 

Драйвер lddbus создаёт простой файл атрибута, опять же содержащий номер версии исходника. Метод show и структура bus_attribute создаются следующим образом:

 

static ssize_t show_bus_version(struct bus_type *bus, char *buf)

{

    return snprintf(buf, PAGE_SIZE, "%s\n", Version);

}

static BUS_ATTR(version, S_IRUGO, show_bus_version, NULL);

 

Создание атрибутного файла производится на время загрузки модуля:

 

if (bus_create_file(&ldd_bus_type, &bus_attr_version))

    printk(KERN_NOTICE "Unable to create version attribute\n");

 

Этот вызов создаёт атрибутный файл (/sys/bus/ldd/version), содержащее номер ревизии кода lddbus.

Устройства

На самом низком уровне, каждое устройство в системе Linux представлено экземпляром struct device:

 

struct device {

    struct device *parent;

    struct kobject kobj;

    char bus_id[BUS_ID_SIZE];

    struct bus_type *bus;

    struct device_driver *driver;

    void *driver_data;

    void (*release)(struct device *dev);

    /* Некоторые поля опущены */

};

 

Есть много других полей struct device, которые представляют интерес только для кода ядра устройства. Эти поля, однако, стоит знать:

 

struct device *parent

Устройство-"родитель" для устройства - то устройство, к которому оно подключено. В большинстве случаев родительским устройством является какая-то шина или хост-контроллер. Если parent является NULL, устройство является устройством верхнего уровня, которое обычно не то, что вы хотите.

 

struct kobject kobj;

Kobject, который представляет это устройство и подсоединяет его в эту иерархию. Заметим, что как правило device->kobj->parent эквивалентен &device-parent->kobj.

 

char bus_id[BUS_ID_SIZE];

Строка, которая однозначно идентифицирует это устройство на этой шине. PCI устройства, например, используют стандартный формат PCI идентификатора, содержащий домен, шину, устройство и функциональные номера.

 

struct bus_type *bus;

Определяет, на каком виде шины находится устройство.

 

struct device_driver *driver;

Драйвер, который управляет этим устройством; мы изучаем struct device_driver в следующем разделе.

 

void *driver_data;

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

 

void (*release)(struct device *dev);

Метод вызывается, когда удаляется последняя ссылка на устройство; он вызывается из метода release встроенного kobject-а. Все структуры устройства, зарегистрированные в ядре, должны иметь метод release, или ядро распечатывает страшные жалобы.

 

Перед тем, как структура устройства может быть зарегистрирована, должны быть установлены, по крайней мере, поля parent, bus_id, bus и release.

Регистрация устройства

Существует обычный набор функций регистрации и её отмены:

 

int device_register(struct device *dev);

void device_unregister(struct device *dev);

 

Мы видели, как код lddbus регистрирует свой тип шины. Тем не менее, настоящая шина является устройством и должна быть зарегистрирована отдельно. Для простоты, модуль lddbus поддерживает только одну виртуальную шину, так что драйвер создаёт свои устройства во время компиляции:

 

static void ldd_bus_release(struct device *dev)

{

    printk(KERN_DEBUG "lddbus release\n");

}

 

struct device ldd_bus = {

    .bus_id = "ldd0",

    .release = ldd_bus_release

};

 

Это шина верхнего уровня, поэтому поля parent и bus остались NULL. У нас есть простой метод release, не делающий ничего, и, как первая (и единственная) шина, она имеет имя ldd0. Это устройство шины регистрируется так:

 

ret = device_register(&ldd_bus);

if (ret)

    printk(KERN_NOTICE "Unable to register ldd0\n");

 

Как только этот вызов будет завершён, в sysfs в каталоге /sys/devices можно увидеть новую шину. Любые устройства, добавленные к этой шине, показываются затем в /sys/devices/ldd0/.

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

Записи устройств в sysfs могут иметь атрибуты. Соответствующей структурой является:

 

struct device_attribute {

    struct attribute attr;

    ssize_t (*show)(struct device *dev, char *buf);

    ssize_t (*store)(struct device *dev, const char *buf, size_t count);

};

 

Эти структуры атрибутов могут быть созданы во время компиляции с помощью этого макроса:

 

DEVICE_ATTR(name, mode, show, store);

 

Результирующая структура называется, предваряя dev_attr_ заданное name. Фактическое управление файлами атрибутов осуществляется обычной парой функций:

 

int device_create_file(struct device *device, struct device_attribute *entry);

void device_remove_file(struct device *dev, struct device_attribute *attr);

 

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

Внедрение структуры устройства

Структура device содержит информацию, которая необходима ядру модели устройства для моделирования системы. Однако, большинство подсистем отслеживают дополнительную информацию о тех устройств, которые они содержат. Как результат, редкие устройства представлены голыми структурами device; вместо этого, эта структура, как и структуры kobject, как правило, встроены в представление устройства более высокого уровня. Если вы посмотрите на определения struct pci_dev или struct usb_device, вы найдете похороненную внутри struct device. Как правило, низкоуровневые драйверы даже не знают об этой struct device, но здесь могут быть исключения.

 

Драйвер lddbus создаёт свой собственный тип устройства (struct ldd_device) и ожидает, что отдельные драйверы устройств зарегистрировали свои устройства, используя этот тип. Он является простой структурой:

 

struct ldd_device {

    char *name;

    struct ldd_driver *driver;

    struct device dev;

};

 

#define to_ldd_device(_dev) container_of(_dev, struct ldd_device, dev);

 

Эта структура позволяет драйверу обеспечить актуальное имя для устройства (которое может отличается от его ID шины, сохранённом в структуре device) и указатель на информацию драйвера. Структуры для настоящих устройств обычно также содержат информацию о поставщике, модели устройства, конфигурации устройства, используемых ресурсах и так далее. Хорошие примеры могут быть найдены в struct pci_dev (<linux/pci.h>) или struct usb_device (<linux/usb.h>). Для struct ldd_device также определён удобный макрос (to_ldd_device), чтобы сделать простым преобразование указателей во встроенной структуре device в указатели ldd_device.

 

Интерфейс регистрации, экспортируемый lddbus, выглядит следующим образом:

 

int register_ldd_device(struct ldd_device *ldddev)

{

    ldddev->dev.bus = &ldd_bus_type;

    ldddev->dev.parent = &ldd_bus;

    ldddev->dev.release = ldd_dev_release;

    strncpy(ldddev->dev.bus_id, ldddev->name, BUS_ID_SIZE);

    return device_register(&ldddev->dev);

}

EXPORT_SYMBOL(register_ldd_device);

 

Здесь мы просто заполняем некоторые поля встроенной структуры device (о которых отдельным драйверам не необходимости знать) и регистрируем устройство в драйверном ядре. Если бы мы хотели добавить в устройство зависимые от шины атрибуты, мы могли бы сделать это здесь.

 

Чтобы показать, как используется этот интерфейс, давайте познакомимся с другим примером драйвера, который мы назвали sculld.  Он является ещё одним вариантом драйвера scullp, впервые представленного в Главе 8. Он реализует обычное устройство в области памяти, но sculld также работает с моделью устройства в Linux через интерфейс lddbus.

 

Драйвер sculld добавляет собственный атрибут к своей записи устройства; этот атрибут, названный dev, просто содержит связанный  с ним номер устройства. Этот атрибут может быть использован скриптом загрузки модуля или подсистемой горячего подключения для автоматического создания узлов устройства, когда устройство добавляется в систему. Установка этого атрибута следует обычным моделям:

 

static ssize_t sculld_show_dev(struct device *ddev, char *buf)

{

    struct sculld_dev *dev = ddev->driver_data;

 

    return print_dev_t(buf, dev->cdev.dev);

}

 

static DEVICE_ATTR(dev, S_IRUGO, sculld_show_dev, NULL);

 

Затем, во время инициализации, устройство регистрируется и создается атрибут dev посредством следующей функции:

 

static void sculld_register_dev(struct sculld_dev *dev, int index)

{

    sprintf(dev->devname, "sculld%d", index);

    dev->ldev.name = dev->devname;

    dev->ldev.driver = &sculld_driver;

    dev->ldev.dev.driver_data = dev;

    register_ldd_device(&dev->ldev);

    device_create_file(&dev->ldev.dev, &dev_attr_dev);

}

 

Обратите внимание, что мы используем поле driver_data для сохранения указателя на нашу собственную внутреннюю структуру устройства.

Драйверы устройств

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

 

Драйверы определяются следующей структурой:

 

struct device_driver {

    char *name;

    struct bus_type *bus;

    struct kobject kobj;

    struct list_head devices;

    int (*probe)(struct device *dev);

    int (*remove)(struct device *dev);

    void (*shutdown) (struct device *dev);

};

 

Снова, некоторые поля этой структуры были опущены (смотрите <linux/device.h> для полной информации). Здесь, name является именем драйвера (оно появляется в sysfs), bus является типом шины, с которой работает этот драйвер, kobj является неизбежным kobject, devices является списком всех устройств в настоящее время связанных с этим драйвером, probe является функцией, вызываемой для запроса наличия определённого устройства (и может ли этот драйвер работать с ним), remove вызывается, когда устройство удаляется из системы, и shutdown вызывается во время выключения для перевода устройства в пассивное состояние.

 

Форма функций для работы со структурами device_driver должны теперь выглядеть уже знакомо (так что мы рассматриваем их очень быстро). Функциями регистрации являются:

 

int driver_register(struct device_driver *drv);

void driver_unregister(struct device_driver *drv);

 

Существует обычная структура атрибута:

 

struct driver_attribute {

    struct attribute attr;

    ssize_t (*show)(struct device_driver *drv, char *buf);

    ssize_t (*store)(struct device_driver *drv, const char *buf, size_t count);

};

DRIVER_ATTR(name, mode, show, store);

 

И атрибутные файлы создаются обычным образом:

 

int driver_create_file(struct device_driver *drv, struct driver_attribute *attr);

void driver_remove_file(struct device_driver *drv, struct driver_attribute *attr);

 

Структура bus_type содержит поле (drv_attrs), которое указывает на набор атрибутов по умолчанию, создаваемых для всех драйверов, связанных с этой шиной.

Внедрение структуры драйвера

Как и в случае с большинством структур драйверного ядра, структура device_driver обычно внедрена в высокоуровневую, зависимую от шины структуру. Подсистема lddbus никогда не пойдёт против такой тенденции, поэтому она определяет свою собственную структуру ldd_driver:

 

struct ldd_driver {

    char *version;

    struct module *module;

    struct device_driver driver;

    struct driver_attribute version_attr;

};

 

#define to_ldd_driver(drv) container_of(drv, struct ldd_driver, driver);

 

Здесь мы требуем, чтобы каждый драйвер предоставлял свою текущую версию программного обеспечения, а lddbus экспортирует эту строку версии для каждого драйвера о котором она знает. Шинно-зависимой функцией регистрации драйвера является:

 

int register_ldd_driver(struct ldd_driver *driver)

{

    int ret;

    driver->driver.bus = &ldd_bus_type;

    ret = driver_register(&driver->driver);

    if (ret)

        return ret;

    driver->version_attr.attr.name = "version";

    driver->version_attr.attr.owner = driver->module;

    driver->version_attr.attr.mode = S_IRUGO;

    driver->version_attr.show = show_version;

    driver->version_attr.store = NULL;

    return driver_create_file(&driver->driver, &driver->version_attr);

}

 

Первая половина функции просто регистрирует в ядре низкоуровневую структуру device_driver; остальная устанавливает атрибут version. Так как этот атрибут создаётся во время выполнения, мы не можем использовать макрос DRIVER_ATTR; вместо этого структура driver_attribute должна быть заполнена вручную. Обратите внимание, что мы установили владельцем атрибута модуль драйвера, а не модуль lddbus; причину этого можно увидеть в реализации функции show для этого атрибута:

 

static ssize_t show_version(struct device_driver *driver, char *buf)

{

    struct ldd_driver *ldriver = to_ldd_driver(driver);

 

    sprintf(buf, "%s\n", ldriver->version);

    return strlen(buf);

}

 

Можно подумать, что владельцем атрибута должен быть модуль lddbus, поскольку функция, которая реализует данный атрибут, определяется здесь. Однако, эта функция работает со структурой ldd_driver, созданной (и принадлежащей) самим драйвером. Если бы эта структура ушла в то время, как процесс в пользовательском пространстве попытался прочитать номер версии, всё могло бы поломаться. Назначение модуля драйвера в качестве владельца атрибута предохраняет модулю от выгрузки, пока пользовательское пространство удерживает файл атрибута открытым. Так как каждый модуль драйвера создаёт ссылку на модуль lddbus, мы можем быть уверены, что lddbus не будет выгружена в неподходящее время.

 

Для полноты, sculld создаёт свою структуру ldd_driver следующим образом:

 

static struct ldd_driver sculld_driver = {

    .version = "$Revision: 1.1 $",

    .module = THIS_MODULE,

    .driver = {

        .name = "sculld",

    },

};

 

В систему её добавляет простой вызов register_ldd_driver. После завершения инициализации информацию драйвера можно увидеть в sysfs:

 

$ tree /sys/bus/ldd/drivers

/sys/bus/ldd/drivers

`-- sculld

    |-- sculld0 -> ../../../../devices/ldd0/sculld0

    |-- sculld1 -> ../../../../devices/ldd0/sculld1

    |-- sculld2 -> ../../../../devices/ldd0/sculld2

    |-- sculld3 -> ../../../../devices/ldd0/sculld3

    `-- version

 

Предыдущая  Содержание  Следующая