Старший и младший номера устройств

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

Символьные устройства доступны в файловой системе через имена. Эти имена называются специальными файлами или файлами устройств или просто узлами дерева файловой системы; они обычно находятся в каталоге /dev. Специальные файлы для символьных драйверов идентифицируются по “c” в первом столбце вывода по команде ls -l. Блочные устройства также представлены в /dev, но они идентифицируются по “b”. В центре внимания этой главы символьные устройства, но большая часть следующей информации относится так же и к блочными устройствами.

 

Если вы введёте команду ls -l, то увидите два числа (разделённые запятой) в каждой записи файла устройства перед датой последней модификации файла, где обычно показывается длина. Эти цифры являются старшим и младшим номером устройства для каждого из них. Следующая распечатка показывает нескольких устройств, имеющихся в типичной системе. Их старшие номера: 1, 4, 7 и 10, а младшие: 1, 3, 5, 64, 65 и 129.

 

crw-rw-rw- 1 root root    1,   3 Apr 11  2002 null

crw------- 1 root root   10,   1 Apr 11  2002 psaux

crw------- 1 root root    4,   1 Oct 28 03:04 tty1

crw-rw-rw- 1 root tty     4,  64 Apr 11  2002 ttys0

crw-rw---- 1 root uucp    4,  65 Apr 11  2002 ttyS1

crw--w---- 1 vcsa tty     7,   1 Apr 11  2002 vcs1

crw--w---- 1 vcsa tty     7, 129 Apr 11  2002 vcsa1

crw-rw-rw- 1 root root    1,   5 Apr 11  2002 zero

 

Традиционно, старший номер идентифицирует драйвер, ассоциированный с устройством. Например, и /dev/null и /dev/zero управляются драйвером 1, тогда как виртуальные консоли и последовательные терминалы управляются драйвером 4; аналогично, оба устройства vcs1 и vcsa1 управляются драйвером 7. Современные ядра Linux позволяют нескольким драйверам иметь одинаковые старшие номера, но большинство устройств, которые вы увидите, всё ещё организованы по принципу один-старший-один-драйвер.

 

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

Внутреннее представление номеров устройств

Для хранения номеров устройств, обоих, старшего и младшего, в ядре используется тип dev_t (определённый в <linux/types.h>). Начиная с версии ядра 2.6.0, dev_t является 32-х разрядным, 12 бит отведены для старшего номера и 20 - для младшего. Ваш код, конечно, никогда не должен делать никаких предположений о внутренней организации номеров устройств; наоборот, он должен использовать набор макросов, находящихся в <linux/kdev_t.h>. Для получения старшей или младшей части dev_t используйте:

 

MAJOR(dev_t dev);

MINOR(dev_t dev);

 

Наоборот, если у вас есть старший и младший номера и необходимость превратить их в dev_t, используйте:

 

MKDEV(int major, int minor);

 

Заметим, что ядро версии 2.6 может вместить большое количество устройств, в то время как предыдущие версии ядра были ограничены 255-ю старшими и 255-ю младшими номерами. Предполагается, что такого широкого диапазона будет достаточно в течение довольно продолжительного времени, но компьютерная область достаточно усеяна ошибочными предположениями. Таким образом, вы должны ожидать, что формат dev_t может снова измениться в будущем; однако, если вы внимательно пишете свои драйверы, эти изменения не будут проблемой.

Получение и освобождение номеров устройств

Одним из первых шагов, который необходимо сделать вашему драйверу при установке символьного устройства, является получение одного или нескольких номеров устройств для работы с ними. Необходимой функцией для выполнения этой задачи является register_chrdev_region, которая объявлена в <linux/fs.h>:

 

int register_chrdev_region(dev_t first, unsigned int count, char *name);

 

Здесь first - это начало диапазона номеров устройств, который вы хотели бы выделить. Младшее число first часто 0, но не существует никаких требований на этот счёт. count - запрашиваемое общее число смежных номеров устройств. Заметим, что если число count большое, запрашиваемый диапазон может перекинуться на следующей старший номер, но всё будет работать правильно, если запрашиваемый диапазон чисел доступен. Наконец, name - это имя устройства, которое должно быть связано с этим диапазоном чисел; оно будет отображаться в /proc/devices и sysfs.

 

Как и в большинстве функций ядра, возвращаемое значение register_chrdev_region будет 0, если выделение была успешно выполнено. В случае ошибки будет возвращён отрицательный код ошибки и вы не получите доступ к запрашиваемому региону. register_chrdev_region работает хорошо, если вы знаете заранее, какие именно номера устройств вы хотите. Однако, часто вы не будете знать, какие старшие номера устройств будут использоваться; есть постоянные усилия в рамках сообщества разработчиков ядра Linux перейти к использованию динамически выделяемых номеров устройств. Ядро будет счастливо выделить старший номер для вас "на лету", но вы должны запрашивать это распределение, используя другую функцию:

 

int alloc_chrdev_region(dev_t *dev, unsigned int firstminor, unsigned int count, char *name);

 

В этой функции dev является только выходным значением, которое при успешном завершении содержит первый номер выделенного диапазона. firstminor должен иметь значение первого младшего номера для использования; как правило, 0. Параметры count и name аналогичны register_chrdev_region.

 

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

 

void unregister_chrdev_region(dev_t first, unsigned int count);

 

Обычное место для вызова unregister_chrdev_region будет в функции очистки вашего модуля.

 

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

Динамическое выделение старших номеров

Некоторые старшие номера устройств для наиболее распространённых устройств выделены статически. Перечень этих устройств можно найти в Documentation/devices.txt в дереве исходных текстов ядра. Однако шансы, что статический номер уже был назначен перед использованием нового драйвера, малы и новые номера не назначаются (видимо, не будет назначен, если всё-таки совпадёт?). Так что, автор драйвера, у вас есть выбор: вы можете просто выбрать номер, который кажется неиспользованным, или вы можете определить старшие номера динамическим способом. Выбор номера может работать; пока вы единственный пользователь вашего драйвера; если ваш драйвер распространяется более широко, случайно выбранный старший номер будет приводить к конфликтам и неприятностям.

 

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

 

Недостатком динамического назначения является то, что вы не сможете создать узлы устройств заранее, так как старший номер, выделяемый для вашего модуля, будет меняться. Вряд ли это проблема для нормального использования драйвера, потому что после того, как номер был назначен, вы можете прочитать его из /proc/devices. (* Еще большая информация об устройстве обычно может быть получена из sysfs, обычно смонтированной в /sys в системах, базирующихся на ядре 2.6. Обучить scull экспортировать информацию через sysfs выходит за рамки данной главы, однако, мы вернёмся к этой теме в Главе 14.)

 

Следовательно, чтобы загрузить драйвер, использующий динамический старший номер, вызов insmod может быть заменён простым скриптом, который после вызова insmod читает /proc/devices в целью создания специального файла (ов).

 

Типичный файл /proc/devices выглядит следующим образом:

 

Символьные устройства:

1 mem

2 pty

3 ttyp

4 ttyS

6 lp

7 vcs

10 misc

13 input

14 sound

21 sg

180 usb

 

Блочные устройства:

2 fd

8 sd

11 sr

65 sd

66 sd

 

Следовательно, чтобы извлечь информацию из /proc/devices для создания файлов в каталоге /dev, скрипт загрузки модуля, которому был присвоен динамический номер, может быть написан с использованием такого инструмента, как awk.

 

Следующий скрипт, scull_load, является частью дистрибутива scull. Пользователь драйвера, который распространяется в виде модуля, может вызывать такой сценарий из системного файла rc.local или запускать его вручную каждый раз, когда модуль становится необходим.

 

#!/bin/sh

module="scull"

device="scull"

mode="664"

 

# вызвать insmod со всеми полученными параметрами

# и использовать имя пути, так как новые modutils не просматривают . по умолчанию

/sbin/insmod ./$module.ko $* || exit 1

 

# удалить давно ненужные узлы

rm -f /dev/${device}[0-3]

 

major=$(awk "\$2==\"$module\" {print \$1}" /proc/devices)

 

mknod /dev/${device}0 c $major 0

mknod /dev/${device}1 c $major 1

mknod /dev/${device}2 c $major 2

mknod /dev/${device}3 c $major 3

 

# назначьте соответствующую группу/разрешения, и измените группу.

# не все дистрибутивы имеют "staff", некоторые вместо этого используют "wheel".

group="staff"

grep -q '^staff:' /etc/group || group="wheel"

 

chgrp $group /dev/${device}[0-3]

chmod $mode /dev/${device}[0-3]

 

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

 

Последние несколько строчек скрипта могут показаться неясными: зачем менять группы и режим работы устройства? Причина в том, что скрипт должен запускаться суперпользователем (superuser), так что вновь созданные специальные файлы принадлежат root-у. По умолчанию биты разрешения установлены так, что только root имеет доступ на запись, остальные могут получать доступ на чтение. Как правило, узлы устройств требуют иной политики доступа, так что тем или иным путём права доступа должны быть изменены.

 

Установки в нашем скрипте предоставляют доступ группе пользователей, но ваши потребности могут отличаться. В разделе "Контроль доступа к файлу устройства" в Главе 6 код sculluid демонстрирует, как драйвер может реализовать свой собственный вид авторизации для доступа к устройству.

 

Для очистки каталога /dev и удаления модуля так же доступен скрипт scull_unload.

 

В качестве альтернативы использования пары скриптов для загрузки и выгрузки вы могли бы написать скрипт инициализации, готовый для размещения в каталоге вашего дистрибутива, используемого для таких скриптов. (* Linux Standard Base указывает, что скрипты инициализации должны быть размещены в /etc/init.d, но некоторые дистрибутивы всё ещё размещают их в другом месте. Кроме того, если ваш скрипт будет работать во время загрузки, вам необходимо сделать ссылку на него из соответствующей директории уровня загрузки (run-level) (то есть .../rc3.d).) Как часть исходников scull, мы предлагаем достаточно полный и настраиваемый пример скрипта инициализации, названного scull.init; он принимает обычные аргументы: старт (start), стоп (stop) и перезагрузка (restart) и выполняет роль как scull_load, так и scull_unload.

 

Если неоднократное создание и уничтожение узлов /dev выглядит как излишество, есть полезный обходной путь. Если вы загружаете и выгружаете только один драйвер, после первого создания специальных файлов вашим скриптом вы можете просто использовать rmmod и insmod: динамические номера не случайны (не рандомизированы) (* Хотя некоторые разработчики ядра угрожали сделать это в будущем.) и вы можете рассчитывать, что такие же номера выбираются каждый раз, если вы не загружаете какие-то другие (динамические) модули. Избегание больших скриптов полезно в процессе разработки. Но очевидно, что этот трюк не масштабируется более чем на один драйвер за раз.

 

Лучшим способом присвоения старших номеров, на наш взгляд, является использование по умолчанию динамического присвоения, оставив себя возможность указать старший номер во время загрузки или даже во время компиляции. scull выполняет работу таким образом; он использует глобальную переменную scull_major, чтобы сохранить выбранный номер (есть также scull_minor для младшего номера). Переменная инициализируется SCULL_MAJOR, определённым в scull.h. Значение по умолчанию SCULL_MAJOR в распространяемых исходниках равно 0, что означает "использовать динамическое определение". Пользователь может принять значение по умолчанию или выбрать специфичный старший номер либо изменив макрос перед компиляцией, либо указав значение для scull_major в командной строки insmod. Наконец, с помощью скрипта scull_load пользователь может передать аргументы insmod в командной строке scull_load. (* Инициализационный скрипт scull.init не принимает параметры драйвера в командной строке, но он поддерживает конфигурационный файл, потому что разработан для автоматического использования во время запуска и выключения.)

 

Для получения старшего номера в исходниках scull используется такой код:

 

if (scull_major) {

    dev = MKDEV(scull_major, scull_minor);

    result = register_chrdev_region(dev, scull_nr_devs, "scull");

} else {

    result = alloc_chrdev_region(&dev, scull_minor, scull_nr_devs, "scull");

    scull_major = MAJOR(dev);

}

if (result < 0) {

    printk(KERN_WARNING "scull: can't get major %d\n", scull_major);

    return result;

}

 

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

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