Наши партнеры

UnixForum





Библиотека сайта rus-linux.net

На главную -> MyLDP -> Электронные книги по ОС Linux
Цилюрик О.И. Модули ядра Linux
Назад Архитектура и вокруг... Вперед

Интерфейсы модуля

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

Взаимодействие модуля с ядром

Для взаимодействия модуля с ядром, ядро (и подгружаемые модули) экспортируют набор имён, которые модуль использует в качестве API ядра (это и есть тот набор вызовов, о котором мы говорили чуть выше, специально в примере показан уже обсуждавшийся вызов sprintf(), о котором мы уже знаем, что он как близнец похож на sprintf() из системной библиотеки GCC, но это совсем другая реализация):

$ awk '/T/ && /print/ { print $0 }' /proc/kallsyms

	...
	c042666a T printk
	...
	c04e5b0a T sprintf
	c04e5b2a T vsprintf
	...
	d087197e T scsi_print_status   [scsi_mod]
	...

Вызовы API ядра осуществляются по прямому абсолютному адресу. Каждому экспортированному ядром или любым модулем именем соотносится адрес, он и используется для связывания при загрузке модуля, использующего это имя. Это основной механизм взаимодействия модуля с ядром.

Список имён ядра находится в файле /proc/kallsyms. Но в этом файле: а). ~85К строчек, и б). далеко не все они доступны модулю для связывания. Для того, чтобы разрешить первую проблему, нам необходимо бегло пользоваться (для фильтрации по самым замысловатым критериям) такими инструментами анализа регулярных выражений, как grep, awk (gawk), sed, perl или им подобными. Ключ ко второй нам даёт информация по утилите nm (анализ символов объектного формата), хотя эта утилита никаким боком и не соотносится непосредственно с программированием для ядра:

$ nm --help

	Usage: nm [option(s)] [file(s)] 
	 List symbols in [file(s)] (a.out by default). 
	...

$ man nm

	...
	    if uppercase, the symbol is global (external).
	...
	    "D" The symbol is in the initialized data section.
	    "R" The symbol is in a read only data section.
	    "T" The symbol is in the text (code) section.
	...

Таким образом, например:

$ cat /proc/kallsyms | grep sys_call

c052476b t proc_sys_call_handler

c07ab3d8 R sys_call_table

- важнейшее имя ядра sys_call_table (таблица системных вызовов) хоть может быть и доступно, но не экспортируется ядром, и недоступно для связывания коду модулей (мы ещё вернёмся к этому вопросу).

Примечание: имя sys_call_table может присутствовать в /proc/kallsyms, а может и нет — я наблюдал 1-е в Fedora 12 (2.6.32) и 2-е в CentOS 5.2 (2.6.18). Это имя вообще экспортировалось ядром до версий ядра 2.5, и могло напрямую быть использовано в коде, но такое состояние дел было признано не безопасным к ядру 2.6.

Относительно API ядра нужно знать следующее:

  1. Эти функции реализованы в ядре, при совпадении многих из них по форме с вызовами стандартной библиотеки С или системными вызовами по форме (например, всё тот же sprintf()) - это совершенно другие функции. Заголовочные файлы для функций пространства пользователя располагаются в /usr/include, а для API ядра — в совершенно другом месте, в каталоге /lib/modules/`uname -r`/build/include, это различие особенно важно.
  2. Разработчики ядра не связаны требованиями совместимости снизу вверх, в отличие от очень жёстких ограничений на пользовательские API, налагаемые стандартом POSIX. Поэтому API ядра достаточно произвольно меняются даже от одной подверсии ядра к другой. Они плохо документированы. Детально изучать их приходится только по исходным кодам Linux.

Коды ошибок

Коды ошибок API ядра в основной массе это те же коды ошибок, прекрасно известные по пространству пользователя, определены они в <asm-generic/errno-base.h> (показано только начало обширной таблицы):

	#define EPERM            1      /* Operation not permitted */ 
	#define ENOENT           2      /* No such file or directory */ 
	#define ESRCH            3      /* No such process */ 
	#define EINTR            4      /* Interrupted system call */ 
	#define EIO              5      /* I/O error */ 
	#define ENXIO            6      /* No such device or address */ 
	#define E2BIG            7      /* Argument list too long */ 
	#define ENOEXEC          8      /* Exec format error */ 
	#define EBADF            9      /* Bad file number */ 
	#define ECHILD          10      /* No child processes */ 
	#define EAGAIN          11      /* Try again */ 
	#define ENOMEM          12      /* Out of memory */ 
	#define EACCES          13      /* Permission denied */ 
	#define EFAULT          14      /* Bad address */ 
	#define ENOTBLK         15      /* Block device required */ 
	#define EBUSY           16      /* Device or resource busy */ 
	...

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

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

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

1. Диагностика из модуля (в системный журнал) printk() :

  • осуществляет вывод в текстовую консоль (не графический терминал!);

  • осуществляет вывод в файл журнала /var/log/messages;
  • содержимое файла журнала можно дополнительно посмотреть командой dmesg;

2. Копирование данных в программном коде между пользовательским адресным пространством и пространством ядра (выполняется только по инициативе модуля). Конечно, это всё те же вызовы из числа API ядра, но эти четыре вызова предназначены для узко утилитарных целей: обмен данными между адресным пространством ядра и пространством пользователя. Вот эти вызовы (мы их получим динамически из таблицы имён ядра, а только после этого посмотрим определения в заголовочных файлах):

$ cat /proc/kallsyms | grep put_user

	c05c634c T __put_user_1 
	c05c6360 T __put_user_2 
	c05c6378 T __put_user_4 
	c05c6390 T __put_user_8 
	...

$ cat /proc/kallsyms | grep get_user

	...
	c05c628c T __get_user_1 
	c05c62a0 T __get_user_2 
	c05c62b8 T __get_user_4 
	...

$ cat /proc/kallsyms | grep copy_to_user

	... 
	c05c6afa T copy_to_user 
	... 

$ cat /proc/kallsyms | grep copy_from_user

	... 
	c04abfeb T iov_iter_copy_from_user
	c04ac053 T iov_iter_copy_from_user_atomic 
	... 
	c05c69e1 T copy_from_user 
	... 

Вызовы put_user() и get_user() - это макросы, которые пытаются определить размер пересылаемой порции данных (1, 2, 4 байта - для get_user(); 1, 2, 4, 8 байт - для put_user()4). Вызовы copy_to_user() и copy_from_user() являются вызовами API ядра для данных произвольного размера, но они просто используют в цикле put_user() и get_user(), соответственно, нужное им число раз. Определения всех этих API можно найти в <asm/uaccess.h>, прототипы имеют вид (для макросов put_user() и get_user() восстановлен вид, как он имел бы для функциональных вызовов - с типизированными параметрами):

	long copy_from_user( void *to, const void __user * from, unsigned long n ); 
	long copy_to_user( void __user *to, const void *from, unsigned long n ); 
	int put_user( void *x, void __user *ptr ); 
	int get_user( void *x, const void __user *ptr );

Каждый из этих вызовов возвращает реально скопированное число байт или код ошибки операции (отрицательное значение). В комментариях утверждается, что передача коротких порций данных через put_user() и get_user() будет осуществляться быстрее.

3. Интерфейс взаимодействия посредством создания символьных имён устройств, вида /dev/XXX. Модуль может обеспечивать поддержку стандартных операций ввода-вывода на устройстве (как символьном, там и блочном). Это основной интерфейс модуля к пользовательскому уровню. Будет детально рассмотрено далее.

4. Взаимодействие через файлы системы /proc (файловая система procfs). Модуль может создавать специфические для него индикативные псевдофайлы в /proc, туда модуль может писать отладочную или диагностическую информацию, или читать оттуда управляющую. Эти файлы в /proc доступны для чтения-записи всеми стандартными командами Linux (в пределах регламента прав доступа, установленных для конкретного файлового имени). Будет детально рассмотрено далее.

5. Взаимодействие через файлы системы /sys (файловая система sysfs). Эта файловая система подобна (по назначению) /proc, но возникла заметно позже, считается, что её функциональность выше, и она во многих качествах будет заменять /proc. Будет детально рассмотрено далее.

6. Взаимодействие модуля со стеком сетевых протоколов (главным образом со стеком протоколов IP, но это не принципиально важно, стек протоколов IP просто намного более развит в Linux, чем других протокольных семейств). Будет детально рассмотрено далее.

4) Почему такая асимметрия я не готов сказать.


Предыдущий раздел: Оглавление Следующий раздел:
Траектория системного вызова   Загрузка модулей