/* Создаем неблокирующий сокет */
ioctl(sock, FIONBIO, &on);
}
Глава 28
Программирование ядра
Из главы 7 вы узнали, что драйверы устройств в Linux выполнены в виде модулей ядра, и познакомились с пакетом module-init-tools (он же modutils для ядер 2.4), содержащим утилиты для выполнения основных операций над модулями ядра. В этой главе я покажу, как создать собственный модуль, позволяющий расширить возможности ядра операционной системы.
Что будет делать ваш модуль, зависит от вас — это может быть драйвер устройства или просто небольшой модуль, дополняющий ядро нужной вам функцией.
Для начала напишем каркас модуля на языке С. Этот каркас можно будет скомпилировать, но в результате получится модуль, который не делает ничего. Он просто послужит вам основой для написания настоящих, серьезных модулей.
Листинг 28.1. Каркас модуля ядра Linux (module1.с)
#define MODULE
#define __KERNEL__
#include <linux/module.h>
int init_module() {
return 0;
}
void cleanup_module() {}
Теперь разберемся, что означает каждая строчка кода нашего будущего модуля. Первые две строчки делают обыкновенную программу модулем ядра Linux. Это директивы препроцессора cpp, обязательные для каждого модуля. Если вы не укажете их, компилятор сгенерирует совсем не тот код, которого мы от него ожидали.
Третья строка подключает заголовочный файл module.h, в котором находятся все необходимые для создания модуля определения.
Функция init_module() вызывается при загрузке модуля ядром. Если загрузка модуля прошла успешно, функция возвращает 0, в противном случае она должна возвратить любое другое значение — код ошибки.
Функция cleanup_module() вызывается при удалении модуля. Тип возвращаемого ею значения не определен, поэтому проверить, успешно ли произошло удаление, сама функция не позволяет.
Названия функций init_module() и cleanup_module() необязательны — вы можете назвать их по-другому, но для этого вам нужно использовать функции module_init() и module_exit(), которые определены в заголовочном файле init.h.
Переделаем наш шаблон так, чтобы он не использовал стандартные имена функций init_module() и cleanup_module():
Листинг 28.2. Шаблон модуля с переименованием стандартных функций (module2.c)
#define MODULE
#define __KERNEL__
#include <linux/module.h> // для модуля.
#include <linux/init.h> // module_init() и module_exit()
int start() {
return 0;
}
void stop() {}
module_init(start);
module_exit(stop);
Функциям module_init() и module_exit() нужно передать имена функций, которые будут вызваны при инициализации и удалении модуля соответственно. Зачем это нужно? Так, для общего развития — чтобы вы знали, для чего используются эти функции. Ваш модуль не станет работать лучше, если вы переименуете стандартные функции инициализации и удаления.
Помните, для чего используется программа modinfo? Да, для получения информации о модуле. Давайте добавим эту информацию в наш модуль.
Листинг 28.3. Информация о модуле (module3.c)
#define MODULE
#define __KERNEL__
#include <linux/module.h>
MODULE_AUTHOR("Denis Kolisnichenko [email protected]");
MODULE_DESCRIPTION("Linux kernel module");
int init_module() {
return 0;
}
void cleanup_module() {
return 0;
}
Макросы MODULE_AUTHOR и MODULE_DESCRIPTION определены в заголовочном файле module.h, поэтому никаких дополнительных заголовочных файлов подключать не нужно.
При необходимости модуль может выводить на консоль сообщения, например, о невозможности инициализации устройства. Выводимые модулем сообщения будут запротоколированы службой протоколирования ядра — демоном klogd. Выводить сообщения нужно не с помощью функции printf(), а функцией printk(). В этом случае сообщения не только окажутся на системной консоли, но и будут запротоколированы в файле /var/log/messages.
Сейчас мы напишем модуль, который будет выводить сообщения при загрузке и при удалении.
Листинг 28.4. Использование функции printk() (module.с)
#define MODULE
#define __KERNEL__
#include <linux/module.h>
#include <linux/kernel.h> // printk
MODULE_AUTHOR{"Denis Kolisnichenko [email protected]");
MODULE_DESCRIPTION("Linux kernel module");
int init_module() {
printk("My module: Starting...n");
return 0;
}
void cleanup_module() {
printk("My module: Stopping...n");
return 0;
}
Компилировать мы будем файл module.c. Для этого понадобится установленный компилятор gcc, заголовочные файлы и исходные тексты ядра. Если вы дочитали книгу до этой главы, то у вас уже должны быть установлены пакеты:
1. cpp — препроцессор cpp;
2. binutils — набор различных утилит (as, gprof, ld);
3. glibc-kerheaders — заголовочные файлы ядра;
4. glibc-devel — вспомогательные файлы для разработки приложений с использованием стандартной библиотеки С;
5. gcc — компилятор gcc.
Осталось установить пакет kernel-source — исходные тексты ядра Linux. Кроме того, нужно убедиться, что ваше ядро поддерживает динамически загружаемые модули (п. 20.3.2.3). Если опция Enable loadable module support выключена, ее нужно включить, сохранить файл конфигурации ядра и перекомпилировать ядро.
Компилятор gcc нужно вызвать со множеством опций, поэтому для облегчения себе работы мы напишем make-файл (п. 21.2):
Листинг 28.5. Makefile для сборки модуля
CC=gcc
PATH=/usr/include /usr/src/linux-2.4/include
MODFLAGS:= -O3 -Wall -DLINUX -D__KERNEL__ -I$(PATH)
module.o: module.с
$(CC) $(MODFLAGS) -c module.с
Опции компилятора означают следующее:
♦ O3: будет использован третий уровень оптимизации (что это такое, вы узнаете в справочной системе gcc: man gcc);
♦ Wall: включаем все предупреждения;
♦ DLINUX: генерируем код для Linux;
♦ I$(РАТН): определяем путь поиска заголовочных файлов. По умолчанию компилятор ищет файлы заголовков в каталоге /usr/include, но там может и не быть нужных файлов. Например, для дистрибутива ALT Linux (ядро 2.4.21) файлы заголовков находятся в каталоге /usr/include/linux-2.4.21rel-std-up.
Поместите make-файл в тот же каталог, где находится module.c, и выполните команду make. После ее выполнения вы получите файл module.o, который будет находиться в том же каталоге.
# insmod module.o
Вы увидите сообщение My module: Starting... Это же сообщение будет записано в файл протокола /var/log/messages.
28.3. Работа с устройствами
Мы только что написали модуль ядра, который можно установить, удалить, который выводит сообщения, но ничего полезного он не делает. Усложним нашу задачу: напишем модуль, управляющий некоторым устройством /dev/device. Данного устройства в нашей системе нет, поэтому нам его нужно создать, но этим мы займемся чуть позже.
Перед тем, как приступить к написанию драйвера устройства, нужно поговорить о самих устройствах. Устройства бывают трех типов:
♦ Символьные: чтение и запись устройства выполняются посимвольно. Примером такого устройства может послужить последовательный порт.
♦ Блочные: чтение и запись устройства выполняются блоками, как правило, по 512 или 1024 байта. Самый яркий пример блочного устройства — жесткий диск.
♦ Сетевые: файлов этих устройств вы не найдете в каталоге /dev, поскольку использование файловой системы, то есть работа с этими устройствами как с файлами, неэффективно. Пример — сетевая карта (ethX).
Существуют устройства, которые нельзя отнести ни к одному типу, поскольку они не принимают и не передают данные. Пример: RTC (Real Time Clock).
Чтобы сделать устройство доступным для системы и пользовательских программ, нужно его зарегистрировать. В заголовочном файле fs.h определены функции для регистрации символьных и блочных устройств. Все они начинаются с префикса register. Наше устройство будет символьным, поэтому для его регистрации мы будем использовать функцию register_chrdev. Вот ее прототип:
extern int register_chrdev(unsigned int, const char *,
struct file_operations *);
Первый аргумент — это старший номер (major number) устройства, определяющий его тип. Если старший номер равен 0, то функция возвратит свободный старший номер для устройства нашего вида (символьное устройство).
Чтобы понять, для чего нужен старший номер, вспомним каталог /dev, содержащий файлы устройств. Файл /dev/tty1 — это терминал с номером 1. Старший номер определяет тип устройства — терминал (tty), а младший — его номер в системе (1). Человеку проще работать не с номерами. а с символьными именами устройств, поэтому каждому старшему номеру соответствует символьное обозначение.