Книга «Модули ядра Linux»
На сайте rus-linux.net опубликован проект книги О.И.Цилюрика «Модули ядра Linux». Книга посвящена программированию модулей ядра Linux и рассчитана на опытных разработчиков системного программного обеспечения. Предполагается, что читатель может и не иметь богатого опыта в программировании именно для ядра Linux, или даже вообще в программировании для этой системы, но имеет какой-то опыт в системном программировании для других операционных систем, что послужит базой для построения аналогий. Даже если чтение книги и не подвигнет читателя к написанию собственных компонент ядра (что совершенно не обязательно), то, по крайней мере, поможет более точному пониманию тех процессов, которые происходят в ядре. На примерах дан обстоятельный обзор возможностей в программировании модулей ядра, этого набора примеров достаточно, чтобы начать писать свой собственный драйвер-модуль Linux, дальше наращивая его функциональность. Предназначено для программистов-разработчиков, ведущих реальные проекты. Конструктивные замечания по тексту можно направлять автору на адрес olej at front dot ru.
Уже третья книга по ядру, за относительно короткий промежуток времени. Что не может не радовать.
> Уже третья книга по ядру
А можно дать ссылки на 2 другие из последних, которые вы имели в виду?
Ори Померанц «Ядро Linux. Программирование модулей» + книга pdf Руководство по программированию модулей ядра Linux
Энциклопедия разработчика модулей ядра Linux
Имена всех изделий и программ здесь используются только для целей идентификации. Торговые марки изготовителя и/или зарегистрированные марки изготовителя принадлежат их владельцам. Я не делаю никакого требования монопольного использования или общей ассоциации с изделиями, программами и компаниями, которые обладают ими.
Итак, Вы хотите писать модули для ядра. Вы знаете C, вы написали ряд нормальных программ, выполяемых как процессы, и теперь Вы хотите добраться туда, где происходит реальное действие, туда, где один ошибочный указатель может стереть вашу файловую систему или привести к перезагрузке.
Хорошо, добро пожаловать в клуб. Я однажды имел такой указатель, который стер мне важный каталог под DOS, и я не вижу, почему Linux должна быть более безопасной.
Предупреждение: я написал это и проверил программу под версиями 2.0.35 и 2.2.3 ядра, запущенного на Pentium. Главным образом это должно работать на других CPU и на других версиях ядра, по крайней мере версий 2.0.x или 2.2.x, но я не могу обещать что-нибудь. Одна исключительная ситуация: глава 11, которая не должна работать на архитектуре не x86.
Кто должен читать это
Этот документ для тех, кто хочет писать модули ядра. Хотя я буду касаться в нескольких местах того, как многие задачи выполнены в ядре, это не моя цель. Имеется достаточно много хороших источников, авторы которых проделали работу лучшую чем та, которую я мог бы сделать.
Этот документ также для людей, которые знают как писать модули ядра, но еще не адаптировались к версии 2.2. Если Вы такой человек, я предлагаю, Вам прочитать приложение A, чтобы увидеть все различия, с которыми я столкнулся при модифицировании примеров. Список не всесторонний, но я думаю, что он покрывает большинство базисных функциональных возможностей и его будет достаточно для начала.
Ядро имеет большое количество программирования, и я полагаю, что программисты должны читать по крайней мере некоторые его исходные файлы и понимать их. Сказав это, я также верю в значение игры с системой сначала и выяснением вопросов позже. Когда я узнаю новый язык программирования, я не начинаюсь с чтения библиотечного кода, а пишу маленькую программу `hello, world’. Я не вижу, почему начинающий разбираться с ядром должен быть действовать иначе.
Я нахожу приятным поместить так много шуток в мою документацию, насколько возможно. Я пишу это, потому что я наслаждаюсь этим, и я считаю, что Вы читаете это по той же самой причине. Если Вы хотите получить только суть, игнорируйте весь нормальный текст и читайте исходники. Я обещаю помещать все важные детали в замечаниях.
Новое в версии 1.0.1
1. Раздел изменений, 0.3.
2. Как найти младший номер устройства, 2.
3. Введено объяснение различия между символом и файлами устройства, 2
4. Makefile’ы для модулей ядра, 1.1.
5. Симметричная многопроцессорность, 12.
6. Глава `Плохие идеи’, 13.
Новое в версии 1.1.0
1. Поддержка версии 2.2 ядра во всех главах.
2. Исходный код примеров для разных версий ядра, 2.1.
3. Разница между версиями 2.0 и 2.2, A.
4. Модули ядра в нескольких файлах исходников, 1.2.
5. Предложение, как избежать беспорядка с системными вызовами при выдаче команды rmmod, 7.
Я хотел бы благодарить Weiss за многие полезные идеи и обсуждения, также как и за поиск ошибок внутри этого документа перед публикацией. Конечно, любые оставшиеся ошибки только моя вина.
TEX скелет для этой книги был бесстыдно захвачен из руководства `Linux Installation and Getting Started’, где работа в TEX была выполнена Matt Welsh.
Моя благодарность Linus Torvalds, Richard Stallman и всем другим людям, кто сделали возможным для меня выполнить операционную систему высокого качества на моем компьютере и получить ее исходный текст, как нечто само собой разумеющееся (да, право, зачем я говорю это?).
Благодарности к версии 1.0.1
Я не могу внести в список каждого, кто послал по e-mail мне сообщение, и если я не вписал именно Вас, я приношу извинения заранее. Следующие люди были особенно полезны:
• Frodo Looijaard из Нидерландов За сервер с кучей информации и полезных советов по ядрам версий 2.1.x.
• Stephen Judd из Новой Зеландии За правку орфографии.
• Magnus Ahltorp из Швеции За исправления, касательно разницы между символьными и блочными устройствами.
Благодарности к версии 1.1.0
• Emmanuel Papirakis из Квебека, Канада за перенос всех примеров в версию 2.2 ядра.
• Frodo Looijaard из Нидерландов за сообщение как создать многофайловый модуль ядра (1.2).
Конечно, любые оставшиеся ошибки мои собственные, и если Вы думаете, что они делают книгу непригодной, требуйте полного возврата денег, которые Вы заплатили за книгу.
Традиционно все учебники программирования начинаются с программы «Hello, world!». Я не знаю, что случается с людьми, которые порывают с этой традицией, и думаю, что безопаснее не выяснять.
Модуль ядра (в дальнейшем просто модуль для краткости) должен иметь по крайней мере две функции: init_module, которая вызывается, когда модуль вставляется в ядро и cleanup_module, которая вызывается, когда он удаляется. Обычно init_module регистрирует драйвер для каких-либо действий с ядром или заменяет одну из ядерных функций собственным кодом (обычно код делает что-то и затем вызывает первоначальную функцию). Функция cleanup_module, как предполагается, отменяет все, что сделано init_module, так что модуль может быть выгружен безопасно.
* Copyright (C) 1998 by Ori Pomerantz
* «Hello, world» — версия для модуля ядра.
/* The necessary header files */
/* Standard in kernel modules */
#include
/* We’re doing kernel work */
#include
/* Specifically, a module */
/* Deal with CONFIG_MODVERSIONS */
/* Initialize the module */
printk(«Hello, world — this is the kernel speaking\n»);
/* If we return a non zero value, it means that
* init_module failed and the kernel module
/* Cleanup — undid whatever init_module did */
printk(«Short is the life of a kernel module\n»);
Makefile’ы для модулей ядра
Модуль не яявляется независимой программой, а представляет собой объектный файл, который будет прилинкован к ядру во время выполнения. В результате, они должны компилироваться с опцией -c. Все модули должны компилироваться с некоторыми определенными символами.
• __KERNEL__ — Этот символ сообщает файлам заголовка, что этот код будет выполнен в ядерном режиме (нравится мне такое определение), а не как часть процесса пользователя.
• MODULE — Этот символ сообщает файлам заголовка, что надо дать соответствующие определения для модуля.
• LINUX — Технически это не необходимо. Однако, если Вы когда-либо захотите написать серьезный модуль, который компилируется на больше чем одной операционной системе, вы будете счастливы, что Вы сделали данное определение. Это позволит Вам делать условную трансляцию частей, которые являются OS-зависимыми.
Имеются другие символы, которые должны быть включены или наоборот выключены в зависимости от параметров с которыми компилировалось ядро. Если вы не уверены, как ядро компилировалось, посмотрите в /usr/include/linux/config.h
• __SMP__ — Симметричная многопроцессорная обработка. Этот символ должен быть определен, если Вы компилируете модуль для ядра, которое было скомпилировано с опцией «Поддержка SMP» (даже если работать оно будет на однопроцессорной машине). Если Вы используете симметричную многопроцессорную обработку, имеются другие хитрости, которые Вы должны предусмотреть (см. главу 12).
• CONFIG_MODVERSIONS — Если CONFIG_MODVERSIONS разрешен, Вы должны иметь определить его при компиляции модуля и включить /usr/include/linux/modversions.h. Это может быть также выполнено кодом непосредственно.
# Makefile для базисного ядерного модуля
MODCFLAGS := -Wall -DMODULE -D__KERNEL__ -DLINUX
hello.o: hello.c /usr/include/linux/version.h
$(CC) $(MODCFLAGS) -c hello.c
echo insmod hello.o to turn it on
echo rmmod hello to turn if off
echo X and kernel programming do not mix.
echo Do the insmod and rmmod from outside X
Так, теперь единственное, что надо сделать, это выполнить su, чтобы зайти как root (Вы не компилировали модуль как root, не так ли? 1 ) Теперь скомандуйте insmod hello и rmmod hello. Когда Вы даете эти команды, обратите внимание на Ваш новый модуль в /proc/modules.
Между прочим, причина, почему Makefile предостерегает против выполнения из X в том, что когда ядро имеет сообщение, чтобы печатать его с помощью printk, оно посылает его на консоль. Когда Вы не используете X, оно придет на терминал, который вы используете (тот, который Вы выбрали Alt-F ) и Вы его увидите. Когда Вы используете X, имеются две возможности. Или Вы имеете консоль открытой с xterm -C, тогда вывод будет послан туда, или Вы консоль не видите, тогда вывод будет идти на терминал 7 — тот, который «захвачен» X.
Если в ядре происходит ошибка, у Вас больше шансов получить из ядра отладочные сообщения, если Вы работаете в текстовой консоли, чем если Вы работаете в X. Вне X вывод printk идет непосредственно с ядра на консоль. В X printk идет на процесс режима пользователя (xterm -C). Когда этот процесс получает время CPU, предполагается послать дааные X процессу. Затем, когда X сервер получает время, сообщение отобразится, но нестабильное ядро обычно означает, что система собирается разрушиться или перезагружаться, так что Вы не успеете получить сообщения об ошибках, которые могли бы объяснить Вам, что именно пошло неправильно. Так что, никаких иксов!
Модули ядра из нескольких файлов
Иногда имеет смысл разделить модуль на несколько файлов. В этом случае Вы должны делать следующее:
1. Во всех исходных файлах добавьте строку #define __NO_VERSION__. Это важно, потому что module.h обычно включает определение kernel_version, глобальная переменная версии ядра для которой компилируется модуль. Если Вы нуждаетесь в version.h, Вы должны включить его непосредственно, потому что module.h не будет делать этого после указания __NO_VERSION__.
2. Скомпилируйте все исходные файлы как обычно.
3. Объедините все объектные файлы в один. Под x86 это делается командой:
ld -m elf_i386 -r -o .o .o .o.
Пример такого модуля:
* Copyright (C) 1999 by Ori Pomerantz
* «Hello, world» — the kernel module version.
* This file includes just the start routine
/* The necessary header files */
/* Standard in kernel modules */
#include
/* We’re doing kernel work */
#include
/* Specifically, a module */
/* Deal with CONFIG_MODVERSIONS */
/* Initialize the module */
printk(«Hello, world — this is the kernel speaking\n»);
/* If we return a non zero value, it means that
* init_module failed and the kernel module
* Copyright (C) 1999 by Ori Pomerantz
* «Hello, world» — the kernel module version. This
* file includes just the stop routine.
/* The necessary header files */
/* Standard in kernel modules */
#include
/* We’re doing kernel work */
#define __NO_VERSION__ /* This isn’t «the» file of the kernel module */
#include
/* Specifically, a module */
#include
/* Not included by module.h because of the __NO_VERSION__ */
/* Deal with CONFIG_MODVERSIONS */
/* Cleanup — undid whatever init_module did */
printk(«Short is the life of a kernel module\n»);
# Makefile for a multifile kernel module
MODCFLAGS := -Wall -DMODULE -D__KERNEL__ -DLINUX
hello.o: start.o stop.o
ld -m elf_i386 -r -o hello.o start.o stop.o
start.o: start.c /usr/include/linux/version.h
$(CC) $(MODCFLAGS) -c start.c
stop.o: stop.c /usr/include/linux/version.h
$(CC) $(MODCFLAGS) -c stop.c
Файлы символьных устройств
Имеются два главных пути для общения модуля разговаривать с процессами. Первый идет через файлы устройства (подобно файлам в каталоге /dev), другой должен использовать файловую систему proc. Поскольку одной из главных причин написания модуля ядра, является поддержка некоего аппаратного устройства, мы начнем с файлов устройства.
Первоначальная цель файлов устройства состоит в том, чтобы позволить процессам связываться с драйверами устройства в ядре, и через них с физическими устройствами (модемы, терминалы, и т.д.).
Каждый драйвер устройства, который является ответственным за некоторый тип аппаратных средств, имеет собственный главный номер. Список драйверов и их главных номеров доступен в /proc/devices. Каждое физическое устройство, управляемое драйвером устройства имеет малый номер. Каталог /dev включает специальный файл, названный файлом устройства, для каждого из тех устройств, которые реально установлены в системе.
Например, если Вы даете команду ls -l /dev/hd[ab]*, вы увидите все IDE разделы жесткого диска, которые могли бы быть связаны с машиной. Обратите внимание, что все из них используют тот же самый главный номер, 3, но малые номера у каждого свои! Оговорка: Считается, что вы используете архитектуру PC. Я не знаю ничего относительно файлов устройств Linux на других архитектурах .
Когда система была установлена, все файлы устройств были созданы командой mknod. Не имеется никакой технической причины, по которой они должны быть в каталоге /dev, это только полезное соглашение. При создании файла устройства для целей тестирования, как с упражнением здесь, вероятно имело бы смысл поместить его в каталог, где Вы компилируете модуль.
Устройства разделены на два типа: символьные и блочные. Различие в том, что блочные имеют буфер для запросов, так что они могут выбирать в каком порядке им отвечать. Это важно в случае устройств памяти, где скорее понадобится читать или писать сектора, которые ближе друг к другу, чем те, которые находятся далеко. Другое различие: блочные устройства могут принимать ввод и возвращать вывод только в блоках (чей размер может измениться согласно устройству), в то время как символьные устройства могут использовать столько байтов, сколько нужно. Большинство устройств в мире символьно, потому что они не нуждаются в этом типе буферизации и не работают с фиксированным размером блока. Вы можете узнать, является ли устройство блочным или символьным, рассматривая первый символ в выводе ls -l. Если это «b», значит устройство блочное, а если «c», то символьное.
Этот модуль разделен на две отдельных части: часть модуля, которая регистрирует устройство и часть драйвера устройства. init_module вызывает module_register_chrdev, чтобы добавить драйвер устройства к символьной таблице драйверов устройств ядра. Этот вызов также возвращает главный номер, который нужно использовать для драйвера. Функция cleanup_module вычеркивает из списка устройство.
Это (регистрация и отмена регистрации) основные функциональные возможности этих двух функций. Действия в ядре не выполняются по собственной инициативе, подобно процессам, а вызываются процессами через системные вызовы или аппаратными устройствами через прерывания или другими частями ядра (просто вызывая специфические функции). В результате, когда Вы добавляете код к ядру, вы регистрируете его как драйвер для некоторого типа события, и когда Вы удаляете его, вы отменяете регистрацию.
Драйвер устройства выполняет четыре действия (функции), которые вызываются, когда кто-то пробует делать что-либо с файлом устройства, который имеет наш главный номер. Ядро знает, что вызвать их надо через структуру file_operations, Fops, который был дан, когда устройство было зарегистрировано, включает указатели на те четыре функции, которые данное устройство выполняет.
Еще мы должны помнить, что мы не можем позволять модулю выгружаться командой rmmod всякий раз, когда root захочет его выгрузить. Причина в том что, если файл устройства открыт процессом, и мы удаляем модуль, то использование файла вызвало бы обращение к точке памяти где располагалась соответствующая функция. Если мы удачливы, никакой другой код не был загружен туда, и мы получим уродливое сообщение об ошибках. Если мы неудачливы (обычно так и бывает), другой модуль был загружен в то же самое место, что означает переход в середину другой функции внутри ядра. Результаты этого невозможно предсказывать, но они не могут быть положительны.
Обычно, когда Вы не хотите выполнять что-либо, Вы возвращаете код ошибки (отрицательное число) из функции, которая делает данное действие. С cleanup_module такой фокус не пройдет: если cleanup_module вызван, модуль завершился. Однако, имеется счетчик использований, который считает, сколько других модулей используют этот модуль, названный номером ссылки (последний номер строки в /proc/modules). Если это число не нулевое, rmmod будет терпеть неудачу. Счетчик модульных ссылок доступен в переменной mod_use_count_. Так как имеются макрокоманды, определенные для обработки этой переменной (MOD_INC_USE_COUNT и MOD_DEC_USE_COUNT), мы предпочитаем использовать их, а не mod_use_count_ непосредственно, так что мы будем в безопасности, если реализация изменится в будущем.
* Copyright (C) 1998-1999 by Ori Pomerantz
* Create a character device (read only)
/* The necessary header files */
/* Standard in kernel modules */
#include
/* We’re doing kernel work */
#include
/* Specifically, a module */
/* Deal with CONFIG_MODVERSIONS */
/* For character devices */
#include
/* The character device definitions are here */
#include
/* A wrapper which does next to nothing at present, but may help for compatibility with future versions of Linux */
/* In 2.2.3 /usr/include/linux/version.h includes a macro for this, but 2.0.35 doesn’t — so I add it here if necessary. */
/* Conditional compilation. LINUX_VERSION_CODE is the code (as per KERNEL_VERSION) of this version. */
#if LINUX_VERSION_CODE > KERNEL_VERSION(2,2,0)
/* The name for our device, as it will appear in /proc/devices */
#define DEVICE_NAME «char_dev»
/* The maximum length of the message from the device */
/* Is the device open right now? Used to prevent concurent access into the same device */
static int Device_Open = 0;
/* The message the device will give when asked */
static char Message[BUF_LEN];
/* How far did the process reading the message get? Useful if the message is larger than the size of the buffer we get to fill in device_read. */
static char *Message_Ptr;
/* This function is called whenever a process attempts to open the device file */
static int device_open(struct inode *inode, struct file *file) <
static int counter = 0;
printk(«device_open(%p,%p)\n», inode, file);
/* This is how you get the minor device number in case you have more than one physical device using the driver. */
printk(«Device: %d.%d\n», inode->i_rdev >> 8, inode->i_rdev & 0xFF);
/* We don’t want to talk to two processes at the same time */
if (Device_Open) return -EBUSY;
/* If this was a process, we would have had to be
* In the case of processes, the danger would be
* that one process might have check Device_Open
* and then be replaced by the schedualer by another
* process which runs this function. Then, when the
* first process was back on the CPU, it would assume
* the device is still not open.
* However, Linux guarantees that a process won’t be
* replaced while it is running in kernel context.
* In the case of SMP, one CPU might increment
* Device_Open while another CPU is here, right after
* the check. However, in version 2.0 of the
* kernel this is not a problem because there’s a lock
* to guarantee only one CPU will be kernel module at
* the same time. This is bad in terms of
* performance, so version 2.2 changed it.
* Unfortunately, I don’t have access to an SMP box
* to check how it works with SMP. */
/* Initialize the message. */
sprintf(Message, «If I told you once, I told you %d times — %s», counter++, «Hello, world\n»);
/* The only reason we’re allowed to do this sprintf
* is because the maximum length of the message
* (assuming 32 bit integers — up to 10 digits
* with the minus sign) is less than BUF_LEN, which
* is 80. BE CAREFUL NOT TO OVERFLOW BUFFERS,
* ESPECIALLY IN THE KERNEL. */
/* Make sure that the module isn’t removed while
* the file is open by incrementing the usage count
* (the number of opened references to the module, if
* it’s not zero rmmod will fail)
/* This function is called when a process closes the
* device file. It doesn’t have a return value in
* version 2.0.x because it can’t fail (you must ALWAYS
* be able to close a device). In version 2.2.x it is
* allowed to fail — but we won’t let it. */
#if LINUX_VERSION_CODE >= KERNEL_VERSION(2,2,0)
static int device_release(struct inode *inode, struct file *file)
static void device_release(struct inode *inode, struct file *file)
printk(«device_release(%p,%p)\n», inode, file);
/* We’re now ready for our next caller */
/* Decrement the usage count, otherwise once you opened the file you’ll never get rid of the module. */
#if LINUX_VERSION_CODE >= KERNEL_VERSION(2,2,0)
/* This function is called whenever a process which
* have already opened the device file attempts to
#if LINUX_VERSION_CODE >= KERNEL_VERSION(2,2,0)
static ssize_t device_read(struct file *file,
char *buffer, /* The buffer to fill with data */
size_t length, /* The length of the buffer */
loff_t *offset) /* Our offset in the file */
static int device_read(struct inode *inode, struct file *file,
char *buffer, /* The buffer to fill with the data */
int length) /* The length of the buffer (mustn’t write beyond that!) */
/* Number of bytes actually written to the buffer */
/* If we’re at the end of the message, return 0 (which signifies end of file) */
if (*Message_Ptr == 0) return 0;
/* Actually put the data into the buffer */
while (length && *Message_Ptr) <
/* Because the buffer is in the user data segment,
* not the kernel data segment, assignment wouldn’t
* work. Instead, we have to use put_user which
* copies data from the kernel data segment to the
printk(«Read %d bytes, %d left\n», bytes_read, length);
/* Read functions are supposed to return the number of bytes actually inserted into the buffer */
/* This function is called when somebody tries to write
* into our device file — unsupported in this example. */
#if LINUX_VERSION_CODE >= KERNEL_VERSION(2,2,0)
static ssize_t device_write(struct file *file,
const char *buffer, /* The buffer */
size_t length, /* The length of the buffer */
loff_t *offset) /* Our offset in the file */
static int device_write(struct inode *inode, struct file *file, const char *buffer, int length)
/* The major device number for the device. This is
* global (well, static, which in this context is global
* within this file) because it has to be accessible * both for registration and for release. */
/* This structure will hold the functions to be
* called when a process does something to the device
* we created. Since a pointer to this structure is
* kept in the devices table, it can’t be local to
* init_module. NULL is for unimplemented functions. */
struct file_operations Fops = <
#if LINUX_VERSION_CODE >= KERNEL_VERSION(2,2,0)
device_release /* a.k.a. close */
/* Initialize the module — Register the character device */
/* Register the character device (atleast try) */
Major = module_register_chrdev(0, DEVICE_NAME, &Fops);
/* Negative values signify an error */
if (Major c %d \n», Major);
printk(«You can try different minor numbers %s», «and see what happens.\n»);
/* Cleanup — unregister the appropriate file from /proc */
/* Unregister the device */
ret = module_unregister_chrdev(Major, DEVICE_NAME);
/* If there’s an error, report it */
if (ret Исходники для разных версий ядра Files
Системные вызовы, которые являются главным интерфейсом ядра, для процессов выглядят одинаково, независимо от версии. Новый системный вызов может быть добавлен, но старые обычно будут вести себя точно так, как и раньше. Это необходимо для обратной совместимости новая версия ядра, как предполагается, не разрывает регулярные процессы. В большинстве случаев, файлы устройства также останутся теми же самыми. С другой стороны, внутренние интерфейсы ядра могут изменяться между версиями.
Версии ядра Linux разделены между устойчивыми версиями (n. .m) и версии разработки (n. .m). Версии разработки включают все новые идеи, включая те, которые будут считаться ошибкой или повторно выполнены в следующей версии. В результате Вы не можете доверять интерфейсу в том плане, что он останется тем же самым в версиях разработки. В устойчивых версиях мы можем ожидать, что интерфейс останется тем же самым независимо от версии исправления ошибок (число m).
Эта версия MPG включает поддержку для версии 2.0.x и версии ядра Linux. Так как имеются различия между ними, требуется условная трансляция в зависимости от версии. Способ сделать это сводится к тому, чтобы использовать макрокоманду LINUX_VERSION_CODE. В версии a.b.c ядра значение этой макрокоманды было бы 2 16 a +2 8 b +c . Чтобы получать значение для конкретной версии, мы можем использовать макрокоманду KERNEL_VERSION. Так как этот макрос не определен в 2.0.35, мы определяем его сами в случае необходимости.
Файловая система /proc
В Linux имеется дополнительный механизм для ядра и ядерных модулей, чтобы они могли послать информацию процессам: файловая система /proc. Первоначально разработанная для свободного доступа к информации относительно процессов, она теперь используется каждым кусочком ядра, который может что-либо сообщить, например, /proc/modules, который имеет список модулей и /proc/meminfo, который имеет статистику использования памяти.
Метод использования файловой системы /proc очень похож на работу с драйверами устройства: Вы создаете структуру со всей информацией, необходимой для /proc файла, включая указатели на любые функции драйвера (в нашем случае имеется только один, вызываемый когда кто-то пытается читать из /proc файла). Затем init_module регистрирует структуру и cleanup_module отменяет регистрацию.
Причина по которой мы используем proc_register_dynamic 2 в том, что мы не хотим определять inode номер, используемый для нашего файла заранее, но позволяем ядру определять его, чтобы предотвратить столкновения. В нормальных файловых системах размещенных на диске, а не только в памяти (как /proc) inode является указателем на то место, в котором на диске размещен индексный узел файла (кратко, inode). Inode содержит информацию относительно файла, например разрешения файла, вместе с указателем на то место, где могут быть найдены данные файла.
Поскольку никаких наших функций не вызывается когда файл открывается или закрывается, некуда поместить MOD_INC_USE_COUNT и MOD_DEC_USE_COUNT в этом модуле, и если файл открыт и затем модуль удален, не имеется никакого способа избежать проблем. В следующей главе мы рассмотрим более тяжелый в реализации, но более гибкий путь имеющий дело с файлами из /proc, который позволит нам защититься от этой проблемы.
/* procfs.c — create a «file» in /proc
* Copyright (C) 1998-1999 by Ori Pomerantz
/* The necessary header files */
/* Standard in kernel modules */
#include
/* We’re doing kernel work */
#include
/* Specifically, a module */
/* Deal with CONFIG_MODVERSIONS */
/* Necessary because we use the proc fs */
/* In 2.2.3 /usr/include/linux/version.h includes a
* macro for this, but 2.0.35 doesn’t — so I add it
#define KERNEL_VERSION(a,b,c) ((a)*65536+(b)*256+(c))
/* Put data into the proc fs file.
1. The buffer where the data is to be inserted, if you decide to use it.
2. A pointer to a pointer to characters. This is useful if you don’t want to use the buffer allocated by the kernel.
3. The current position in the file.
4. The size of the buffer in the first argument.
5. Zero (for future use?).
Usage and Return Value
If you use your own buffer, like I do, put its location in the second argument and return the number of bytes used in the buffer.
A return value of zero means you have no further information at this time (end of file). A negative return value is an error condition.
For More Information
The way I discovered what to do with this function wasn’t by reading documentation, but by reading the code which used it. I just looked to see what uses the get_info field of proc_dir_entry struct (I used a combination of find and grep, if you’re interested), and I saw that it is used in /fs/proc/array.c.
If something is unknown about the kernel, this is usually the way to go. In Linux we have the great advantage of having the kernel source code for free — use it.
int procfile_read(char *buffer, char **buffer_location, off_t offset, int buffer_length, int zero) <
int len; /* The number of bytes actually used */
/* This is static so it will still be in memory when we leave this function */
static char my_buffer[80];
static int count = 1;
/* We give all of our information in one go, so if the
* user asks us if we have more information the
* answer should always be no.
* This is important because the standard read
* function from the library would continue to issue
* the read system call until the kernel replies
* that it has no more information, or until its * buffer is filled. */
if (offset > 0) return 0;
/* Fill the buffer and get its length */
len = sprintf(my_buffer, «For the %d%s time, go away!\n», count,
(count % 100 > 10 && count % 100 KERNEL_VERSION(2,2,0)
/* In version 2.2, proc_register assign a dynamic
* inode number automatically if it is zero in the
* structure, so there’s no more need for
/* proc_root is the root directory for the proc fs (/proc). This is where we want our file to be located. */
/* Cleanup — unregister our file from /proc */
Использование /proc для ввода
Пока мы имеем два способа генерировать вывод из модулей: мы можем зарегистрировать драйвер устройства и создать mknod файл устройства, или мы можем создать /proc файл. Это позволяет модулю сообщать нам что-нибудь. Единственная проблема в том, что не имеется никакого пути для нас, чтобы возразить. Первый путем, которым мы пошлем ввод модулям, будет запись обратно в файл в системе /proc.
Поскольку файловая система /proc была написана главным образом, чтобы позволить ядру сообщать ситуацию процессам, нет никаких специальных условий для ввода. Структура proc_dir_entry не включает указатель на функцию ввода. Вместо этого, чтобы писать в /proc, мы должны использовать стандартный механизм файловой системы.
В Linux имеется стандартный механизм для регистрации файловой системы. Так как каждая файловая система должна иметь собственные функции, чтобы обработать inode и выполнять файловые операции 3 , имеется специальная структура, чтобы хранить указатели на все нужные функции, struct inode_operations, который включает указатель на struct file_operations. В /proc всякий раз, когда мы регистрируем новый файл, нам позволяют определить, который struct inode_operations будет использоваться для доступа к этому файлу. Это механизм, который мы используем, struct inode_operations, который включает указатель на struct file_operations, который включает указатели на наши функции module_input и module_output.
Важно обратить внимание что стандартные роли чтения и записи, обращены в ядре. Функции чтения используются для вывода, в то время как функции записи используются для ввода. Причина для этого в том, что чтение и запись относятся к точке зрения пользователя: если процесс читает что-то из ядра, ядро должно вывести это, а если процесс пишет что-то в ядро, оно получает это как ввод.
Другая интересная точка здесь это функция module_permission. Эта функция вызывается всякий раз, когда процесс пробует делать что-либо с файлами системы /proc, и может решить позволить доступ или нет. Сейчас это только основано на операции и uid текущиего пользователя, но в принципе такое разрешение может выдаваться исходя из других параметров, например, что другие процессы делают с тем же самым файлом, времени дня или последнего ввода, который мы получили.
Причина для put_user и get_user в том, что память в Linux (под архитектурой Intel, это может быть различно под некоторыми другими процессорами) сегментирована. Это означает, что указатель, отдельно не ссылается на уникальное место в памяти, а только на место в сегменте памяти, и Вы должны знать, который сегмент памяти нужен. Имеется один сегмент памяти для ядра и по одному на каждый из процессов.
Единственный сегмент памяти, доступный для процесса, это его собственный, так для нормальных программ, нет никакой потребности волноваться относительно сегментов. Когда Вы пишете модуль, обычно Вы хотите обращаться к сегменту памяти ядра, который обрабатывается системой автоматически. Однако, когда содержание буфера памяти должно быть передано между процессом в настоящее время имеющим управление и ядром, функция получает указатель на буфер памяти, который находится в своем сегменте процесса. Макрокоманды put_user и get_user позволяют Вам обращаться к нужной Вам памяти.
/* procfs.c — create a «file» in /proc, which allows
* both input and output.
/* Copyright (C) 1998-1999 by Ori Pomerantz */
/* The necessary header files */
/* Standard in kernel modules */
#include
/* We’re doing kernel work */
#include
/* Specifically, a module */
/* Deal with CONFIG_MODVERSIONS */
/* Necessary because we use proc fs */
/* In 2.2.3 /usr/include/linux/version.h includes a
* macro for this, but 2.0.35 doesn’t — so I add it
#define KERNEL_VERSION(a,b,c) ((a)*65536+(b)*256+(c))
#if LINUX_VERSION_CODE >= KERNEL_VERSION(2,2,0)
/* Here we keep the last message received, to prove
* that we can process our input */
#define MESSAGE_LENGTH 80
static char Message[MESSAGE_LENGTH];
/* Since we use the file operations struct, we can’t
* use the special proc output provisions — we have to
* use a standard read function, which is this function */
#if LINUX_VERSION_CODE >= KERNEL_VERSION(2,2,0)
static ssize_t module_output(
struct file *file, /* The file read */
char *buf, /* The buffer to put data to (in the user segment) */
size_t len, /* The length of the buffer */
loff_t *offset) /* Offset in the file — ignore */
static int module_output(
struct inode *inode, /* The inode read */
struct file *file, /* The file read */
char *buf, /* The buffer to put data to (in the user segment) */
int len) /* The length of the buffer */
static int finished = 0;
/* We return 0 to indicate end of file, that we have
* no more information. Otherwise, processes will
* continue to read from us in an endless loop. */
/* We use put_user to copy the string from the kernel’s
* memory segment to the memory segment of the process
* that called us. get_user, BTW, is
* used for the reverse. */
sprintf(message, «Last input:%s», Message);
for(i=0; i = KERNEL_VERSION(2,2,0)
static ssize_t module_input(
struct file *file, /* The file itself */
const char *buf, /* The buffer with input */
size_t length, /* The buffer’s length */
loff_t *offset) /* offset to file — ignore */
static int module_input(
struct inode *inode, /* The file’s inode */
struct file *file, /* The file itself */
const char *buf, /* The buffer with the input */
int length) /* The buffer’s length */
/* Put the input into Message, where module_output will later be able to use it */
for (i=0; i = KERNEL_VERSION(2,2,0)
/* In version 2.2 the semantics of get_user changed,
* it not longer returns a character, but expects a
* variable to fill up as its first argument and a
* user segment pointer to fill it from as the its second.
* The reason for this change is that the version 2.2
* get_user can also read an short or an int. The way
* it knows the type of the variable it should read
* is by using sizeof, and for that it needs the
Message[i] = ‘\0’; /* we want a standard, zero terminated string */
/* We need to return the number of input characters used */
/* This function decides whether to allow an operation
* (return zero) or not allow it (return a non-zero
* which indicates why it is not allowed).
* The operation can be one of the following values:
* 0 — Execute (run the «file» — meaningless in our case)
* 2 — Write (input to the kernel module)
* 4 — Read (output from the kernel module)
* This is the real function that checks file
* permissions. The permissions returned by ls -l are
* for reference only, and can be overridden here. */
static int module_permission(struct inode *inode, int op) <
/* We allow everybody to read from our module, but only root (uid 0) may write to it */
/* If it’s anything else, access is denied */
/* The file is opened — we don’t really care about
* that, but it does mean we need to increment the
* module’s reference count. */
int module_open(struct inode *inode, struct file *file) <
/* The file is closed — again, interesting only because of the reference count. */
#if LINUX_VERSION_CODE >= KERNEL_VERSION(2,2,0)
int module_close(struct inode *inode, struct file *file)
void module_close(struct inode *inode, struct file *file)
#if LINUX_VERSION_CODE >= KERNEL_VERSION(2,2,0)
/* Structures to register as the /proc file, with
* pointers to all the relevant functions. ********** */
/* File operations for our proc file. This is where we
* place pointers to all the functions called when
* somebody tries to do something to our file. NULL
* means we don’t want to deal with something. */
static struct file_operations File_Ops_4_Our_Proc_File = <
module_output, /* «read» from the file */
module_input, /* «write» to the file */
module_open, /* Somebody opened the file */
#if LINUX_VERSION_CODE >= KERNEL_VERSION(2,2,0)
NULL, /* flush, added here in version 2.2 */
module_close, /* Somebody closed the file */
/* etc. etc. etc. (they are all given in
* /usr/include/linux/fs.h). Since we don’t put
* anything here, the system will keep the default
* data, which in Unix is zeros (NULLs when taken as pointers). */
/* Inode operations for our proc file. We need it so
* we’ll have some place to specify the file operations
* structure we want to use, and the function we use for
* permissions. It’s also possible to specify functions
* to be called for anything else which could be done to
* an inode (although we don’t bother, we just put NULL). */
static struct inode_operations Inode_Ops_4_Our_Proc_File = <
module_permission /* check for permissions */
static struct proc_dir_entry Our_Proc_File = <
0, /* Inode number — ignore, it will be filled by proc_register[_dynamic] */
7, /* Length of the file name */
«rw_test», /* The file name */
S_IFREG | S_IRUGO | S_IWUSR,
/* File mode — this is a regular file which
* can be read by its owner, its group, and everybody
* else. Also, its owner can write to it.
* Actually, this field is just for reference, it’s
* module_permission that does the actual check. It
* could use this field, but in our implementation it
* doesn’t, for simplicity. */
1, /* Number of links (directories where the file is referenced) */
0, 0, /* The uid and gid for the file — we give it to root */
80, /* The size of the file reported by ls. */
/* A pointer to the inode structure for
* the file, if we need it. In our case we
* do, because we need a write function. */
/* The read function for the file. Irrelevant,
* because we put it in the inode structure above */
/* Module initialization and cleanup ******************* */
/* Initialize the module — register the proc file */
/* Success if proc_register[_dynamic] is a success, failure otherwise */
#if LINUX_VERSION_CODE >= KERNEL_VERSION(2,2,0)
/* In version 2.2, proc_register assign a dynamic
* inode number automatically if it is zero in the
* structure, so there’s no more need for
/* Cleanup — unregister our file from /proc */
Работа с файлами устройств (запись и IOCTL)
Файлы устройства представляют физические устройства. Многие физические устройства используются для вывода и для ввода, так должен иметься некоторый механизм для драйверов устройства в ядре, чтобы послать вывод устройству из процесса. Это выполняется, открывая файл устройства для вывода и записывая в него точно так же, как в обычный файл. В следующем примере, это выполнено функцией device_write.
Этого не всегда достаточно. Вообразите, что Вы имеете последовательный порт, связанный с модемом (даже если Вы имеете внутренний модем, он выглядит с точки зрения CPU как последовательный порт, связанный с модемом, так что Вы не должны слишком сдерживать Ваше воображение). Естественное решение использовать файл устройства, чтобы передавать модему команды модема или данные, которые будут посланы через телефонную линию и читать из модема ответы для команд или данные, полученные через телефонную линию. Однако, это оставляет открытым вопрос о том что делать, когда Вы должны работать с последовательным портом непосредственно, например настроить скорость обмена данными с портом.
Ответ в Unix должен использовать специальную функцию, названную ioctl (сокращение от i nput o utput c ont rol ). Каждое устройство может иметь собственные команды ioctl, которые могут читать ioctl (для передачи данных от процесса ядру), записывать ioctl (чтобы возвратить информацию процессу) 4 , выполнять оба действия или ни одно из них. Функция ioctl вызывается с тремя параметрами: описатель файла соответствующий файлу устройства, ioctl номер, и параметра, который имеет тип long, так что Вы можете использовать приведение, чтобы передать что-нибудь. 5
Ioctl номер кодирует главный номер устройства, тип ioctl команды и тип параметра. Этот ioctl номер обычно создается макрообращением (_IO, _IOR, _IOW или _IOWR: в зависимости от типа) в файле заголовка. Этот файл заголовка должен быть присоединен командой #include программой, которая использует ioctl и модулем (так что они могут генерировать соответствующие ioctl). В примере ниже, файл заголовка chardev.h и программа, которая использует это ioctl.c.
Если Вы хотите использовать ioctl в ваших собственных модулях, самое лучшее получить официальное ioctl назначение, так, если Вы случайно получаете ioctl кого-то другого вы будете знать, что что-то неправильно. Для большего количества информации, проконсультируйтесь в файле `Documentation/ioctl-number.txt’ дерева исходников ядра.
* Create an input/output character device
/* Copyright (C) 1998-99 by Ori Pomerantz */
/* The necessary header files */
/* Standard in kernel modules */
#include
/* We’re doing kernel work */
#include
/* Specifically, a module */
/* Deal with CONFIG_MODVERSIONS */
/* For character devices */
/* The character device definitions are here */
/* A wrapper which does next to nothing at
* at present, but may help for compatibility
* with future versions of Linux */
/* Our own ioctl numbers */
/* In 2.2.3 /usr/include/linux/version.h includes a
* macro for this, but 2.0.35 doesn’t — so I add it
#define KERNEL_VERSION(a,b,c) ((a)*65536+(b)*256+(c))
#if LINUX_VERSION_CODE >= KERNEL_VERSION(2,2,0)
/* The name for our device, as it will appear in /proc/devices */
#define DEVICE_NAME «char_dev»
/* The maximum length of the message for the device */
/* Is the device open right now? Used to prevent concurent access into the same device */
static int Device_Open = 0;
/* The message the device will give when asked */
static char Message[BUF_LEN];
/* How far did the process reading the message get?
* Useful if the message is larger than the size of the
* buffer we get to fill in device_read. */
static char *Message_Ptr;
/* This function is called whenever a process attempts to open the device file */
static int device_open(struct inode *inode, struct file *file) <
/* We don’t want to talk to two processes at the same time */
if (Device_Open) return -EBUSY;
/* If this was a process, we would have had to be
* more careful here, because one process might have
* checked Device_Open right before the other one
* tried to increment it. However, we’re in the
* kernel, so we’re protected against context switches.
* This is NOT the right attitude to take, because we
* might be running on an SMP box, but we’ll deal with
* SMP in a later chapter. */
/* Initialize the message */
/* This function is called when a process closes the
* device file. It doesn’t have a return value because
* it cannot fail. Regardless of what else happens, you
* should always be able to close a device (in 2.0, a 2.2
* device file could be impossible to close). */
#if LINUX_VERSION_CODE >= KERNEL_VERSION(2,2,0)
static int device_release(struct inode *inode, struct file *file)
static void device_release(struct inode *inode, struct file *file)
printk(«device_release(%p,%p)\n», inode, file);
/* We’re now ready for our next caller */
#if LINUX_VERSION_CODE >= KERNEL_VERSION(2,2,0)
/* This function is called whenever a process which
* has already opened the device file attempts to read from it. */
#if LINUX_VERSION_CODE >= KERNEL_VERSION(2,2,0)
static ssize_t device_read(
char *buffer, /* The buffer to fill with the data */
size_t length, /* The length of the buffer */
loff_t *offset) /* offset to the file */
static int device_read(
struct inode *inode, struct file *file,
char *buffer, /* The buffer to fill with the data */
int length) /* The length of the buffer (mustn’t write beyond that!) */
/* Number of bytes actually written to the buffer */
printk(«device_read(%p,%p,%d)\n», file, buffer, length);
/* If we’re at the end of the message, return 0 (which signifies end of file) */
if (*Message_Ptr == 0) return 0;
/* Actually put the data into the buffer */
while (length && *Message_Ptr) <
/* Because the buffer is in the user data segment,
* not the kernel data segment, assignment wouldn’t
* work. Instead, we have to use put_user which
* copies data from the kernel data segment to the
printk(«Read %d bytes, %d left\n», bytes_read, length);
/* Read functions are supposed to return the number
* of bytes actually inserted into the buffer */
/* This function is called when somebody tries to write into our device file. */
#if LINUX_VERSION_CODE >= KERNEL_VERSION(2,2,0)
static ssize_t device_write(struct file *file, const char *buffer, size_t length, loff_t *offset)
static int device_write(struct inode *inode, struct file *file, const char *buffer, int length)
printk(«device_write(%p,%s,%d)», file, buffer, length);
for(i=0; i = KERNEL_VERSION(2,2,0)
/* Again, return the number of input characters used */
/* This function is called whenever a process tries to
* do an ioctl on our device file. We get two extra
* parameters (additional to the inode and file
* structures, which all device functions get): the number
* of the ioctl called and the parameter given to the ioctl function.
* If the ioctl is write or read/write (meaning output
* is returned to the calling process), the ioctl call
* returns the output of this function. */
int device_ioctl(struct inode *inode, struct file *file,
unsigned int ioctl_num, /* The number of the ioctl */
unsigned long ioctl_param) /* The parameter to it */
#if LINUX_VERSION_CODE >= KERNEL_VERSION(2,2,0)
/* Switch according to the ioctl called */
/* Receive a pointer to a message (in user space)
* and set that to be the device’s message. */
/* Get the parameter given to ioctl by the process */
/* Find the length of the message */
#if LINUX_VERSION_CODE >= KERNEL_VERSION(2,2,0)
for (i=0; ch && i = KERNEL_VERSION(2,2,0)
device_write(file, (char*)ioctl_param, i, 0);
device_write(inode, file, (char*)ioctl_param, i);
/* Give the current message to the calling
* process — the parameter we got is a pointer, fill it. */
#if LINUX_VERSION_CODE >= KERNEL_VERSION(2,2,0)
i = device_read(file, (char*)ioctl_param, 99, 0);
i = device_read(inode, file, (char*)ioctl_param, 99);
/* Warning — we assume here the buffer length is
* 100. If it’s less than that we might overflow
* the buffer, causing the process to core dump.
* The reason we only allow up to 99 characters is
* that the NULL which terminates the string also needs room. */
/* Put a zero at the end of the buffer, so it will be properly terminated */
/* This ioctl is both input (ioctl_param) and
* output (the return value of this function) */
/* This structure will hold the functions to be called
* when a process does something to the device we
* created. Since a pointer to this structure is kept in
* the devices table, it can’t be local to
* init_module. NULL is for unimplemented functions. */
struct file_operations Fops = <
#if LINUX_VERSION_CODE >= KERNEL_VERSION(2,2,0)
device_release /* a.k.a. close */
/* Initialize the module — Register the character device */
/* Register the character device (atleast try) */
ret_val = module_register_chrdev(MAJOR_NUM, DEVICE_NAME, &Fops);
/* Negative values signify an error */
if (ret_val chardev.h
/* chardev.h — the header file with the ioctl definitions.
* The declarations here have to be in a header file,
* because they need to be known both to the kernel
* module (in chardev.c) and the process calling ioctl (ioctl.c)
/* The major device number. We can’t rely on dynamic
* registration any more, because ioctls need to know it. */
#define MAJOR_NUM 100
/* Set the message of the device driver */
#define IOCTL_SET_MSG _IOR(MAJOR_NUM, 0, char *)
/* _IOR means that we’re creating an ioctl command
* number for passing information from a user process
* to the kernel module.
* The first arguments, MAJOR_NUM, is the major device
* The second argument is the number of the command
* (there could be several with different meanings).
* The third argument is the type we want to get from
* the process to the kernel. */
/* Get the message of the device driver */
#define IOCTL_GET_MSG _IOR(MAJOR_NUM, 1, char *)
/* This IOCTL is used for output, to get the message
* of the device driver. However, we still need the
* buffer to place the message in to be input,
* as it is allocated by the process. */
/* Get the n’th byte of the message */
#define IOCTL_GET_NTH_BYTE _IOWR(MAJOR_NUM, 2, int)
/* The IOCTL is used for both input and output. It
* receives from the user a number, n, and returns Message[n]. */
/* The name of the device file */
#define DEVICE_FILE_NAME «char_dev»
/* ioctl.c — the process to use ioctl’s to control the
* Until now we could have used cat for input and
* output. But now we need to do ioctl’s, which require
* writing our own process. */
/* Copyright (C) 1998 by Ori Pomerantz */
/* device specifics, such as ioctl numbers and the major device file. */
/* Functions for the ioctl calls */
ioctl_set_msg(int file_desc, char *message) <
ret_val = ioctl(file_desc, IOCTL_SET_MSG, message);
if (ret_val Загрузочные параметры
Во многих из предыдущих примеров, мы жестко задавали какие-либо параметры в модуле. Это идет против правил Unix и Linux, философия которых такова, что программа должна быть гибкой, чтобы пользователь мог ее настраивать.
Способ сообщить программе или модулю что-либо до запуска это параметры командной строки. В случае ядерных модулей, мы не получаем argc и argv. Вместо этого, мы получаем кое-что лучше. Мы можем определять глобальные переменные в модуле, и insmod заполнит их для нас.
В этом ядерном модуле мы определяем две таких переменных: str1 и str2. Все, что Вы должны делать это скомпилировать модуль и затем выполнить insmod str1=xxx str2=yyy. При вызове init_module str1 укажет на строку `xxx’ и str2 на строку `yyy’.
В версии 2.0 не имеется никакого контроля соответствия типов аргументов 6 . Если первый символ str1 или str2 является цифрой, ядро заполнит переменную значением целого числа, а не указателем на строку. В реальной ситуации Вы должны проверять это.
С другой стороны, в версии 2.2 Вы используете макрокоманду MACRO_PARM , чтобы сообщить insmod, что Вы ожидаете параметры, их имена и их типы . Это решает проблему типа и позволяет модулям получать строки, которые начинаются с цифры.
* Receive command line parameters at module installation
/* Copyright (C) 1998-99 by Ori Pomerantz */
/* The necessary header files */
/* Standard in kernel modules */
#include
/* We’re doing kernel work */
#include
/* Specifically, a module */
/* Deal with CONFIG_MODVERSIONS */
#include /* I need NULL */
/* In 2.2.3 /usr/include/linux/version.h includes a
* macro for this, but 2.0.35 doesn’t — so I add it
#define KERNEL_VERSION(a,b,c) ((a)*65536+(b)*256+(c))
* Parameter names are now (2.2) handled in a macro.
* The kernel doesn’t resolve the symbol names
* like it seems to have once did.
* To pass parameters to a module, you have to use a macro
* defined in include/linux/modules.h (line 176).
* The macro takes two parameters. The parameter’s name and
* it’s type. The type is a letter in double quotes.
* For example, «i» should be an integer and «s» should
#if LINUX_VERSION_CODE >= KERNEL_VERSION(2,2,0)
/* Initialize the module — show the parameters */
if (str1 == NULL || str2 == NULL) <
printk(«Next time, do insmod param str1= «);
> else printk(«Strings:%s and %s\n», str1, str2);
#if LINUX_VERSION_CODE >= KERNEL_VERSION(2,2,0)
printk(«If you try to insmod this module twice,»);
printk(«it first), you might get the wrong»);
printk(«‘symbol for parameters str1 not found’.\n»);
Пока что все программы, которые мы сделали должны были использовать хорошо определенные механизмы ядра, чтобы регистрировать /proc файлы и драйверы устройства. Это прекрасно, если Вы хотите делать что-то уже предусмотренное программистами ядра, например писать драйвер устройства. Но что, если Вы хотите сделать что-то необычное, изменить поведение системы некоторым способом?
Это как раз то место, где программирование ядра становится опасным. При написании примера ниже, я уничтожил системный вызов open. Это подразумевало, что я не могу открывать любые файлы, я не могу выполнять любые программы, и я не могу закрыть систему командой shutdown. Я должен выключить питание, чтобы ее остановить. К счастью, никакие файлы не были уничтожены. Чтобы гарантировать, что Вы также не будете терять файлы, пожалуйста выполните sync прежде чем Вы отдадите команды insmod и rmmod.
Забудьте про /proc файлы и файлы устройств. Они только малые детали. Реальный процесс связи с ядром, используемый всеми процессами, это системные вызовы. Когда процесс запрашивает обслуживание из ядра (типа открытия файла, запуска нового процесса или запроса большего количества памяти), используется этот механизм. Если Вы хотите изменить поведение ядра интересными способами, это как раз подходящее место. Между прочим, если Вы хотите видеть какие системные вызовы использованы программой, выполните: strace .
Вообще, процесс не способен обратиться к ядру. Он не может обращаться к памяти ядра и не может вызывать функции ядра. Аппаратные средства CPU предписывают такое положение дел (недаром это называется `protected mode’ (защищенный режим)). Системные вызовы исключение из этого общего правила. Процесс заполняет регистры соответствующими значениями и затем вызывает специальную команду, которая переходит к предварительно определенному месту в ядре (конечно, оно читается процессами пользователя, но не перезаписывается ими). Под Intel CPUs, это выполнено посредством прерывания 0x80. Аппаратные средства знают, что, как только Вы переходите к этому месту, Вы больше не работаете в ограниченном режиме пользователя. Вместо этого Вы работаете как ядро операционной системы, и следовательно вам позволено делать все, что Вы хотите сделать.
Место в ядре, к которому процесс может переходить, названо system_call. Процедура, которая там находится, проверяет номер системного вызова, который сообщает ядру чего именно хочет процесс. Затем, она просматривает таблицу системных вызовов (sys_call_table), чтобы найти адрес функции ядра, которую надо вызвать. Затем вызывается нужная функция, и после того, как она возвращает значение, делается несколько проверок системы. Затем результат возвращается обратно процессу (или другому процессу, если процесс завершился). Если Вы хотите посмотреть код, который все это делает, он находится в исходном файле arch/ /kernel/entry.S, после строки ENTRY(system_call).
Так, если мы хотим изменить работу некоторого системного вызова, то первое, что мы должны сделать, это написать нашу собственную функцию, чтобы она выполняла соответствующие действия (обычно, добавляя немного нашего собственного кода, и затем вызывая первоначальную функцию), затем изменить указатель в sys_call_table, чтобы указать на нашу функцию. Поскольку мы можем быть удалены позже и не хотим оставлять систему в непостоянном состоянии, это важно для cleanup_module, чтобы восстановить таблицу в ее первоначальном состоянии.
Исходный текст, приводимый здесь, является примером такого модуля. Мы хотим «шпионить» за некоторым пользователем, и посылать через printk сообщение всякий раз, когда данный пользователь открывает файл. Мы заменяем системный вызов, открытия файла нашей собственной функцией, названной our_sys_open. Эта функция проверяет uid (user id) текущего процесса, и если он равен uid, за которым мы шпионим, вызывает printk, чтобы отобразить имя файла, который будет открыт. Затем вызывает оригинал функции open с теми же самыми параметрами, фактически открывает файл.
Функция init_module меняет соответствующее место в sys_call_table и сохраняет первоначальный указатель в переменной. Функция cleanup_module использует эту переменную, чтобы восстановить все назад к норме. Этот подход опасен, из-за возможности существования двух модулей, меняющих один и тот же системный вызов. Вообразите, что мы имеем два модуля, А и B. Системный вызов open модуля А назовем A_open и такой же вызов модуля B назовем B_open. Теперь, когда вставленный в ядро системный вызов заменен на A_open, который вызовет оригинал sys_open, когда сделает все, что ему нужно. Затем, B будет вставлен в ядро, и заменит системный вызов на B_open, который вызовет то, что как он думает, является первоначальным системным вызовом, а на самом деле является A_open.
Теперь, если B удален первым, все будет хорошо: это просто восстановит системный вызов на A_open, который вызывает оригинал. Однако, если удален А, и затем удален B, система разрушится. Удаление А восстановит системный вызов к оригиналу, sys_open, вырезая B из цикла. Затем, когда B удален, он восстановит системный вызов к тому, что он считает оригиналом, На самом деле вызов будет направлен на A_open, который больше не в памяти. На первый взгляд кажется, что мы могли бы решать эту специфическую проблему, проверяя, если системный вызов равен нашей функции open и если так, не менять значение этого вызова (так, чтобы B не изменил системный вызов, когда удаляется), но это вызовет еще худшую проблему. Когда А удаляется, он видит, что системный вызов был изменен на B_open так, чтобы он больше не указывал на A_open, так что он не будет восстанавливать указатель на sys_open прежде, чем будет удалено из памяти. К сожалению, B_open будет все еще пробовать вызывать A_open, который больше не в памяти, так что даже без удаления B система все равно рухнет.
Я вижу два способа предотвратить эту проблему. Первое: восстановить обращение к первоначальному значению sys_open. К сожалению, sys_open не является частью таблицы ядра системы в /proc/ksyms, так что мы не можем обращаться к нему. Другое решение состоит в том, чтобы использовать счетчик ссылки, чтобы предотвратить выгрузку модуля. Это хорошо для обычных модулей, но плохо для «образовательных» модулей.
* System call «stealing» sample
/* Copyright (C) 1998-99 by Ori Pomerantz */