понедельник, 18 октября 2010 г.

3.2. Мажорные и минорные числа

3.2 Старший и младший номера

Символьные устройства доступны через имена в файловой системе. Этими именами называют специальные файлы или файлы устройств или просто узлы дерева файловой системы; обычно они расположены в каталоге /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. Современные ядра Линукс позволяют вешать несколько драйверов на одни и те же старшие номера, но большинство устройств, с которыми вы встретитесь, по-прежнему организованы на принципе один старший номер - один драйвер.

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

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

В ядре для хранения номера устройства, как старшей так и младшей его части, используется тип dev_t (определен в types.h>). Начиная с версии ядра 2.6.0, dev_t представляет из себя 32-битную величину, где 12 битов занимает старший номер, а 20 - младший номер. Конечно, ваш исходный код не должен делать предположений, о внутренней организации номеров устройств, наоборот, должен использовать макросы из файла 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 может снова измениться в будущем; однако, если вы внимательно пишете свои драйверы, эти изменения не будут проблемой.

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

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

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

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

Как и в большинстве функций ядра, функция register_chrdev_region возвращает 0, если выделение диапазона адресов произошло успешно. В случае ошибки будет возвращен отрицательный код ошибки и вы не получите доступа к запрашиваемому дипапзону номеров.

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

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

В этой функции dev является только выходным значением, которое при успешном завершении содержит первый номер выделенного диапазона. firstminor должен иметь значение первого младшего номера для использования; как правило, 0. Параметры count и name аналогичны request_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).

Если неоднократное создание и уничтожение узлов /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;
}

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

Переведено на сайте www.notabenoid.com
http://notabenoid.com/book/11832/38270
Внимание! Этот перевод, возможно, ещё не готов,
так как модераторы установили для него статус
"перевод редактируется"

Комментариев нет:

Отправить комментарий

Примечание. Отправлять комментарии могут только участники этого блога.