06 сем Семинар 2. Загружаемые модули ядра. - chrislvt/OS GitHub Wiki

Загружаемые модули ядра

В Linux драйверы могут писаться как загружаемые модули ядра, но конечно драйверы это особые программы. Их задача управлять внешними устройствами и как говорят специалисты, драйверы сам никто не пишет. Структура драйверов строго определена в любой операционной системе. Операционная система вся построена на структурах. Unix/Linux написаны на Си.

Драйвер описывается определённой структурой, например struct usb_driver. Там перечисленны точки входа в драйвер. Точка входа - это место с которого начинает выполняться программа. Две главные точки входа драйвера: probe и disconnect. Как правило в драйвере устройства есть один обработчик прерывания.

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

Простейший загружаемый модуль ядра имеет следующий вид

static int __init md_init( void )
{
printk( "Module md loaded!\n" );
return 0;
}

static void __exit md_exit( void )
{
printk( "Module unloaded!\n" );
}

module_init( md_init );
module_exit( md_exit );

Как мы видим, нет инклудов. module_init, module_exit - два макроса, в которые мы передаём функции. Названия функций передаются в макросы. Эти макросы и делают основную работу, они предназначены для того чтобы сделать загружаемый модуль ядра частью ядра.

Функция printk похожа на printf, но это ядрёная функция и пишет она в лог-файл. В отличие от функции printf которая выводит информацию на терминал, функция printk выводит информацию в журнал. В ядре нельзя использовать функции библиотек Си, т.е. функции пользовательских библиотек. В ядре используются свои функции. Функция printk выводит информацию в /var/log/message. Существует несколько способов вывести это информацию на консоль, в частности это может быть команда (dmesq).

#include <linux/module.h>
#include<linux/kernel.h>
#include<linux/sched.h>

static int __init md_init(void)
{
    struct task_struct *task = &init_task;
    do
   {
       printk(KERN_INFO " _%s-%d %s-%d \n", task-> comm, task-> pid, task->parent->comm, ...);  
   } while (( task = next_task(task)) != &init_task);
   printk(KERN_INFO " %s-%d\n", current->comm, ...);
   return 0;
}

Если мы выводим информацию о процессах, то естесственно мы используем дескриптор процесса. Дескриптор процесса - это task_struct. Вот мы объявляем указатель на struct task_struct. Рязанова всегда предлагает студентам распечатать эту структуру, этой распечаткой можно будет воспользоваться на экзамене взяв её ссобой.

После KERN_INFO запятая не ставится!

Кроме использования непосредственно указателя task в системе есть так называемый символ current. Он определяет текущий процесс. Для его использования не надо писать никаких указателей.

Уровень протоколирования

KERN_INFO - уровень протоколирования сообщения. Система поддерживает восемь уровней протоколирования, показанных в таблице 1. Все константы определены в файле linux/kernel.h

Таблица из методички

image

Чем больше значение, тем меньше приоритет.

Стоит обратить внимание на то, что после уровня протоколирования не стоит запятая, так как она является частью строки форматирования, это сделано в системе для экономии памяти стека при вызове функции.

Библиотеки

Если мы пишем специальные программы, которые назвываются загружаемые модули ядра, то должны использовать специальные библиотеки.

#include <linux/module.h>

#include<linux/kernel.h> содержит уровни протоколирования

#include<linux/sched.h> для обращения к структуре task_struct

Относительно функций (API) ядра, нужно знать следующее:

  1. Функции реализованы в ядре и при совпадении многих из них по форме с вызовами функций стандартных библиотек Си или с системными вызовами по форме - это совершенно другие функции. Заголовочные файлы функции пространства пользователя находятся в user/include, функции ядра находятся в /lib/modules/'uname_r'/build/include

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

Функция printk выводит информацию в /var/log/message.

Существует несколько способов вывести это информацию на консоль, в частности это может быть команда (dmesg).

Кроме того необходимо четко понимать, что компьютеры имеют SMP - архитектуру. Это многопроцессорная архитектура, которая предполагает наличие в системе равноправных процессоров которые работают с общей памятью. Как только появилась эта архитектура ядро было переписано. Обязательно надо смотреть для какой версии ядра написан материал, так как заменяются функции (например proc). Естесственно, базовая теоретическая основа не меняется. Меняются названия функций.

Кроме сказанного, в модулях всегда указывается информация о модуле. В методичке возможная информация о модуле, макросы перечислены. Обязательной информацией является информация о лицензии. Она может быть после инклудов, она может быть после макросов init и exit. Это MODULE_LICENSE, например

MODULE_LICENSE("GPL"); 

Ещё обычно указывают автора, но это не обязательно.

Начиная с версии 2.6 поменялось расширение. Откомпиленные модули получают расширение *.ko . Расширение *.o заменено на расширение *.ko .


Вторая часть лр

Необходимо взять эти модули у Цилюрика, есть ссылка в материале на коды.

Файл md1.c


#include <linux/init.h> 
#include <linux/module.h> 
#include "md.h" 
MODULE_LICENSE( "GPL" ); 
MODULE_AUTHOR( "Oleg Tsiliuric <[email protected]>" ); 
char* md1_data = "Привет мир!"; 
extern char* md1_proc( void ) { 
   return md1_data; 
} 
static char* md1_local( void ) { 
   return md1_data; 
} 
extern char* md1_noexport( void ) { 
   return md1_data; 
} 
EXPORT_SYMBOL( md1_data ); 
EXPORT_SYMBOL( md1_proc ); 
static int __init md_init( void ) { 
   printk( "+ module md1 start!\n" ); 
   return 0; 
} 
static void __exit md_exit( void ) { 
   printk( "+ module md1 unloaded!\n" ); 
} 
module_init( md_init ); 
module_exit( md_exit );

файл md.h


extern char* md1_data; 
extern char* md1_proc( void );

В данном примере рассматривается взаимодействие модулей ядра. Т.е. модули ядра могут взаимодействовать, значит данные объявленные и описанные в одном модуле могут быть доступны в другом модуле. Тут присутствует знакомое слово EXTERN. Здесь кроме того что переменная объявлена как внешняя есть ещё макрос EXPORT. md1_proc объявлена в md.h, но еще раз использует модификатор extern. Это не является ошибкой.

Про + необходимо прочитать.

На что нужно обратить внимание:

md1_data объявлена как extern, md1_proc объявлена как extern и они export. Но есть еще две функции: local которая не extern и no_export которая extern и не export. В модуле md2.c происходит обращение к этим данным. Так как данные объявлены как внешние и экспортируются, то никакой ошибки в таком модуле возникнуть не может. Дополнительно необходимо проделать следующие упражнения: попробовать из md2.c обратиться к no_export и можно обратиться к local.

В модулях md2.c и md3.c вызываются эти данные. md3.c точно такой же как md2.c, но в нём return (-1). То есть возврат ошибки. Понятно что вопрос отладки и загрузки загружаемого модуля ядра является очень серьезным. После того как функция загружена в ядро она становится частью ядра. Поэтому если функция возвращает ошибку, то она в ядро не загружается. Об этом выводится соответствующее сообщение которое необходимо показать. Если убрать return (-1) и поставить return(0), то md3.c загрузится так же успешно как md2.c и md1.c, но обратите внимание на то, что у Цирюлика всё это описано в его материале шаг за шагом. Обратим внимание на второй шаг.

Модуль md2, использующий экспортируемое имя, связывается с этим именем по прямому(Рязановой это слово не нравится!!!! не использовать!!! А ЧТО ЕСТЬ КРИВОЙ??!?!?) абсолютному адресу. Как следствие этого, любые изменения (новая сборка), вносимые в ядро или экспортирующие модули, делают собранный модуль непригодным для использования. Именно поэтому бессмысленно предоставлять модуль в собранном виде — он должен собираться только на месте использования.

Абсолютный адрес - адрес байта памяти.

В силу этого имеет значение порядок загрузки модулей. Модули должны загружаться специальной командой, это команда insmod(от слова insert). Модули должны загружаться в правильном порядке: сначала загружается md1, потом md2 итд. Так как md2 должен получить абсолютные адреса экспортируемых данных. Если загрузим в обратном порядке, то md2 загрузить ничего не сможет. Будет выдана ошибка и это ошибку необходимо показать.