MyTetra Share
Делитесь знаниями!
5. Общие сведения
Время создания: 20.09.2022 09:44
Текстовые метки: linux, ядро, модуль, программирование, язык, C, Си, пособие, документация
Раздел: Компьютер - Linux - Ядро - Пособие по программированию модулей ядра Linux
Запись: xintrea/mytetra_syncro/master/base/1663656279m6ljpxzdcp/text.html на raw.github.com

5. Общие сведения


▍ 5.1 Начало и завершение модулей

Программа обычно начинается с функции main(), выполняет ряд инструкций, после чего завершается. А вот модули ядра работают несколько иначе. Модуль всегда начинается либо с init_module, либо с функции, которую мы указываем вызовом module_init. У модулей это функция входа, которая сообщает ядру, какую функциональность модуль несёт, и настраивает ядро на выполнение этой функциональности при необходимости. По завершении функции входа, модуль переходит в состояние бездействия, пока ядру не потребуется от него некая функциональность для работы с кодом.

Заканчиваются все модули вызовом либо cleanup_module, либо функции, указываемой вызовом module_exit. У модулей это функция выхода. Она отменяет всё, что до этого сделала функция входа, и отменяет регистрацию всей ранее введённой ей функциональности.

Обе описанные функции входа и выхода должны присутствовать в каждом модуле. А поскольку для их определения существует не один способ, я постараюсь использовать общие термины «функция входа» и «функция выхода», но если вдруг по недосмотру назову их init_module и cleanup_module, то, думаю, вы поймёте, что я имею в виду.

▍ 5.2 Функции, доступные модулям

Программисты используют функции, не требующие постоянного переопределения. Хорошим примером этого является printf(). Это лишь одна из функций, предоставляемых стандартной библиотекой libc. Фактически их определения не попадают в программу до этапа линковки, который гарантирует доступность кода (например, для printf()) и направляет на этот код инструкцию вызова.

Здесь модули ядра тоже отличаются. В примере “Hello world” вы могли заметить, что мы использовали функцию pr_info, но не включали стандартную библиотеку ввода-вывода. Причина в том, что модули – это объектные файлы, чьи символы разрешаются при выполнении insmod или modprobe. Определение для символов поступает из самого ядра. Единственными внешними функциями, которые можно использовать, являются те, что предоставляет ядро. Если вам интересно узнать, какие символы ваше ядро экспортировало, загляните в /proc/kallsyms.

При всём при этом нужно помнить о различии между библиотечными функциями и системными вызовами. Библиотечные функции работают на более высоком уровне, выполняясь полностью в пользовательском пространстве и предоставляя программисту более удобный интерфейс для доступа к функциям, которые и совершают реальную работу – системным вызовам. Эти вызовы, в свою очередь, выполняются в режиме ядра от имени пользователя и предоставляются самим ядром. Библиотечная функция printf() может выглядеть как обобщённая функция вывода, но на деле она лишь форматирует данные в строки и записывает эти строчные данные с помощью низкоуровневого системного вызова write, который затем отправляет их в стандартный вывод.

Хотите увидеть, какие системные вызовы совершает printf()? Легко! Скомпилируйте следующую программу с помощью gcc -Wall -o hello hello.c:

#include <stdio.h>

int main(void)

{

printf("hello");

return 0;

}

Запустите исполняемый файл командой strace ./hello. Впечатлены? Каждая строка, которую вы видите, соответствует системному вызову. strace – это удобная утилита, сообщающая подробности о том, какие системные вызовы совершает программа, включая то, какие аргументы эти вызовы содержат и какие результаты возвращают.

Это невероятно ценный инструмент, позволяющий выяснять, к каким файлам обращается программа. Ближе к концу вы увидите строку вроде write(1, "hello", 5hello). Вот оно – лицо, скрытое за маской printf(). Вы можете быть незнакомы с write(), поскольку большинство людей для файлового ввода-вывода используют библиотечные функции (например, fopen, fputs, fclose). Если так и есть, то рекомендую заглянуть в мануал, man 2 write. Второй раздел в нём посвящён системным вызовам (вроде kill() и read()). Третий раздел описывает библиотечные вызовы (вроде cosh() и random()), с которыми вы наверняка уже более знакомы.

Вы даже можете писать модули на замену системных вызовов ядра, чем мы вскоре и займёмся. Взломщики зачастую используют подобные приёмы для бэкдоров или троянов, но вы можете создавать собственные модули из более доброжелательных побуждений, например, чтобы ядро писало “Tee hee, that tickles!” (Хи-хи, щекотно!) каждый раз, когда кто-то пытается удалить в системе файл.

▍ 5.3 Пользовательское пространство и пространство ядра

Ядро (по своей сути), регулирует доступ к ресурсам, будь то видеокарта, жёсткий диск или память. При этом программы зачастую соперничают за право использовать один и тот же ресурс. Как только я сохранил документ, updatedb начала обновлять локальную базу данных. Мой сеанс VIM и updatedb используют жёсткий диск конкурентно. Ядру необходимо сохранять во всём этом порядок, а не давать пользователям доступ к ресурсам в любой момент, когда им вздумается.

В связи с этим ЦПУ может работать в нескольких режимах. Каждый режим даёт определённый уровень свободы действий в системе. В архитектуре 80386 есть 4 таких режима, называемых кольцами защиты. В Unix используется только два таких кольца: внутреннее (кольцо 0, также известное как «режим супервизора», в котором допустимы все действия) и внешнее, называемое «режим пользователя».

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

▍ 5.4 Пространство имён

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

При написании кода ядра даже малейший модуль будет залинкован со всем ядром, так что это определённо важно. Проще всего в таком случае объявлять все переменные статическими и использовать для символов грамотные префиксы. По соглашению все префиксы в ядре пишутся в нижнем регистре. Если же вы не хотите объявлять что-либо статично, то другой вариант – объявить таблицу символов и зарегистрировать её с помощью ядра. Чуть позже мы об этом поговорим.

В файле /proc/kallsyms хранятся все символы, о которых ядро знает, и которые, благодаря этому, являются доступными для модулей, поскольку находятся в едином пространстве кода ядра.

▍ 5.5 Кодовое пространство

Управление памятью является очень сложной темой, и большая часть книги Understanding The Linux Kernel издательства O’Reilly посвящено именно ей. Мы не ставим задачу стать экспертами в этой области, но для того, чтобы даже задуматься над написанием реальных модулей нам необходимо знать пару фактов.

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

Эта память начинается с 0х00000000 и простирается до необходимых значений. Поскольку область памяти для любых двух процессов не пересекается, все процессы, которые могут обращаться к адресу памяти, скажем 0xbffff978, будут обращаться к разным областям реальной физической памяти. Они будут обращаться к индексу 0xbffff978, указывающему на некое смещение в области памяти, выделенной конкретно для этого процесса. В большинстве случаев процесс вроде нашей программы “Hello World” не может получить доступ к пространству другого процесса, хотя для этого есть определённые способы, о которых мы поговорим позже.

У ядра также есть собственная область памяти. Поскольку модуль является кодом, который может внедряться в ядро и извлекаться из него (в противоположность полуавтономному объекту), он использует кодовое пространство ядра, не имея собственного. Следовательно, если ваш модуль допускает ошибку сегментации, то и с ядром происходит то же самое. И если вы начнёте производить запись поверх данных в результате ошибки смещения на единицу, то происходить это будет поверх данных (или кода) ядра. На деле это даже хуже, чем звучит, так что будьте очень осторожны.

Кстати, хочу отметить, что описанное выше касается любой операционной системы, использующей монолитное ядро. Это не совсем то же, что «встраивание всех модулей в ядро», хотя суть аналогична. Существует такое понятие, как микроядра, которые имеют модули, получающие собственное кодовое пространство. Примерами таких микроядер являются GNU Hurd и Zircon.

▍ 5.6 Драйверы устройств

Одним из классов модулей являются драйверы устройств, которые предоставляют функциональность для оборудования вроде последовательных портов. В Unix каждый элемент оборудования представлен файлом устройства, расположенным в /dev и предоставляющим средства для связи с этим оборудованием. Драйвер устройства обеспечивает связь со стороны пользовательской программы. Например, драйвер звуковой карты es1370.ko может подключать файл устройства /dev/sound к звуковой карте Ensoniq IS1370. В результате программа в пользовательском пространстве, например, mp3blaster, может использовать /dev/sound, даже не зная, какая именно звуковая карта установлена.

Рассмотрим некоторые файлы устройств. Ниже приведены их примеры, которые представляют первые три раздела на ведущем HDD:

$ ls -l /dev/hda[1-3]

brw-rw---- 1 root disk 3, 1 Jul 5 2000 /dev/hda1

brw-rw---- 1 root disk 3, 2 Jul 5 2000 /dev/hda2

brw-rw---- 1 root disk 3, 3 Jul 5 2000 /dev/hda3

Обратите внимание на числа, отделённые запятой. Первое называется старшим (major) номером устройства, а второе младшим (minor). Старший номер сообщает, какой драйвер используется для доступа к оборудованию. Каждому драйверу присваивается уникальный старший номер. Все файлы устройств с одинаковым старшим номером управляются одним драйвером. Выше мы видим в качестве таких номеров три 3, поскольку всеми этими устройствами управляет один драйвер.

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

Устройства делятся на два типа: блочные и символьные. Отличие между ними в том, что блочные имеют буфер для запросов, благодаря чему могут выбирать наилучший порядок, в котором на эти запросы отвечать. Это важно в случае устройств хранения, когда получается быстрее считывать/записывать близкорасположенные сектора, нежели те, что удалены друг от друга. Ещё одним отличием является то, что блочные устройства могут получать вход и возвращать выход только блоками (чей размер может отличаться в зависимости от устройства), а символьным дозволено использовать любое необходимое им количество байтов. Большинство устройств являются именно символьными, поскольку им не требуется подобная буферизация, и они не работают с фиксированным размером блоков.

Понять, для какого устройства используется файл устройства – блочного или символьного – можно по первому символу вывода команды ls -l. Если это b, значит — устройство блочное, а если c, значит — символьное. Приведённые выше устройства все являются блочными, а вот несколько символьных (последовательные порты):

crw-rw---- 1 root dial 4, 64 Feb 18 23:34 /dev/ttyS0

crw-r----- 1 root dial 4, 65 Nov 17 10:26 /dev/ttyS1

crw-rw---- 1 root dial 4, 66 Jul 5 2000 /dev/ttyS2

crw-rw---- 1 root dial 4, 67 Jul 5 2000 /dev/ttyS3

Если хотите увидеть, какие устройствам были присвоены старшие номера, можете заглянуть в Documentation/admin-guide/devices.txt.

При установке системы все эти файлы устройств создавались командой mknod. Для создания нового символьного устройства под именем coffee со старшим/младшим номерам 12/2 просто выполните mknod /dev/coffee c 12 2. Вам не обязательно помещать файлы устройств в /dev, но того требует соглашение. Линукс размещает эти файлы в /dev, и вам стоит делать так же. Однако при создании файла устройства для тестирования вполне допустимо разместить его в рабочем каталоге, где вы компилируете модуль ядра. Только не забудьте перенести его в нужное место, когда закончите написание драйвера.

Напоследок хочу дополнительно прояснить момент, который может быть неочевиден из пояснения выше. Когда происходит обращение к файлу устройства, ядро по его старшему номеру определяет, какой драйвер нужно использовать для обработки этого обращения. То есть ядру не обязательно использовать, или даже знать, младший номер. Этот номер интересует лишь драйвер устройства, который использует его для различения отдельных компонент оборудования.

Кстати, когда я говорю «оборудование», то подразумеваю несколько более абстрактное понятие, нежели какая-нибудь PCI-карта, которую вы держите в руках. Взгляните на эти два файла устройств:

$ ls -l /dev/sda /dev/sdb

brw-rw---- 1 root disk 8, 0 Jan 3 09:02 /dev/sda

brw-rw---- 1 root disk 8, 16 Jan 3 09:02 /dev/sdb

Теперь, глядя на них, вы можете сходу понять, что они являются блочными устройствами и обрабатываются одним драйвером. Иногда два файла устройств с одним старшим, но разными младшими номерами на деле могут представлять один и тот же компонент оборудования. Так что имейте в виду, что слово «оборудование» в этом пособии может иметь весьма абстрактное значение.


 
MyTetra Share v.0.59
Яндекс индекс цитирования