19. Структура file: члены и назначение.

advertisement
Оглавление
1. Перечислите и охарактеризуйте средства аппаратной поддержки функций ОС. ................................ 2
2. Механизм виртуальной памяти и его реализация в процессорах фирмы Интел. ................................. 3
3. Реализация механизма системного вызова в ОС. Таблица системных вызовов и методы ее
модификации в ОС LINUX. ........................................................................................................................... 5
4. Понятие процесса и потока: раскройте и охарактеризуйте. ................................................................... 6
5. Многослойная структура ядра: принципы построения современных ОС. ........................................... 7
6. Перечислите и охарактеризуйте основные подсистемы ядра ОС LINUX. ........................................... 9
7. Перечислите и охарактеризуйте основные классы устройств и модулей ядра ОС LINUX. ............. 10
8. Программная структура модулей ядра. Загрузка и выгрузка модулей. Функции init_module и
cleanup_module. ............................................................................................................................................. 12
9. Реализация пользовательского режима и режима ядра в системе LINUX. ......................................... 13
10. Сравните модуль ядра и модуль приложения пользовательского режима: что общего и в чем
разница ........................................................................................................................................................... 15
11. Опишите процесс динамической компоновки модулей ядра с действующим ядром. Утилиты
insmod, modprobe и rmmod. .......................................................................................................................... 16
12. Охарактеризуйте механизм проверки версии модулей ядра. ............................................................. 17
13. Подсчет ссылок на модули ядра. Использование макросов MOD_INC_USE_COUNT,
MOD_DEC_USE_COUNT и MOD_IN_USE. .............................................................................................. 18
14. Драйверы символьных устройств. Старший (major) и младший (minor) номера устройств........... 19
15. Динамическое выделение старших номеров устройств. ..................................................................... 20
16. Регистрация драйвера символьного устройства и удаление драйвера из системы. ......................... 21
17. Перечислите основные виды файлов в ОС LINUX и охарактеризуйте их. ...................................... 22
18. Структура file_operations: основные члены и назначение. Использование расширенного
синтаксиса для ее инициализации. .............................................................................................................. 23
19. Структура file: члены и назначение. .................................................................................................... 25
20. Методы open и release структуры file_operations. ................................................................................ 26
21. Методы read и write структуры file_operations..................................................................................... 27
22. Работа с пользовательским адресным пространством. Функции copy_to_user, copy_from_user,
access_ok, get_user, put_user и др. ................................................................................................................ 28
23. Функции ввода-вывода пользовательского режима и их связь с обработчиками драйвера
устройства. ..................................................................................................................................................... 29
24. Состояние гонки в режиме ядра. Использование семафоров в режиме ядра. .................................. 30
25. Функция управления ioctl: ее описание в структуре file_operations и прототип в режиме ядра. ... 32
26. Генерирование номера команды функции ioctl. Макросы _IOW, _IO, _IOR. .................................. 33
27. Операции блокируемого ввода-вывода. Использование очередей. ................................................... 34
28. Реализация разграничения доступа на уровне драйвера устройства. ................................................ 36
29. Отладка модулей ядра с помощью функции printk. Управление кольцевым буфером сообщений
ядра. ................................................................................................................................................................ 37
30. Выделение и освобождение памяти в режиме ядра. Особенности и отличие от пользовательского
режима............................................................................................................................................................ 38
31. Файловая система /proc. Создание файлов для чтения и файлов чтения/записи. ............................ 39
32. Таймеры ядра. Инициализация таймера, его использование и удаление. ......................................... 42
33. Использование средств tasklet. Особенности и отличие от таймеров ядра....................................... 44
34. Механизм очередей Workqueue. ............................................................................................................ 46
35. Обработка прерываний. Установка и удаление обработчика прерываний. ...................................... 47
36. Назначение линии IRQ обработчику прерывания. .............................................................................. 48
37. Механизм обработки прерываний в ОС LINUX. ................................................................................. 49
38. Верхняя и нижняя половины обработчика прерываний. .................................................................... 50
1
ВОПРОСЫ К ЭКЗАМЕНУ ПО КУРСУ “СИСТЕМНОЕ ПРОГРАММИРОВАНИЕ”
1. Перечислите и охарактеризуйте средства аппаратной
поддержки функций ОС.
Основные средства для аппаратной поддержки:
1) система прерываний,
выставляется как некоторый уровень сигнала после того, как появился определенный уровень;
различные действия в ОС связаны с обработкой различных прерываний
2) управление привилегиями,
существуют функции, позволяющие разделить привилегии,- ядру предоставляется больше
возможностей, пользовательским процессам - меньше; ядро может управлять набором
привилегий, предоставленных отдельному процессу, - т.е. с помощью функций аппаратной
поддержки ядро управляет пользовательским процессом
3) системный таймер,
таймер устанавливает (генерирует) прерывания через заданный интервал; используется для
планирования процессов
4) переключение процессов,
средства переключения процессов: многозадачность за счет того, что на определенный квант
времени процессор «уходит в тень», но при переключении процессов надо сохранять много
информации, которая представляется на аппаратном уровне
5) переключение страниц памяти,
каждый процесс предстает в своем 4-х байтном пространстве: процессор и ОС реализуют
общение с виртуальной памятью
6) ограничение доступа к памяти
реализован совместно со средством переключения страниц; определяет права доступа к
страницам
*) защита памяти, механизм преобразования адресов в системах виртуальной памяти, управление
каналами и периферийными устройствами.
2
2. Механизм виртуальной памяти и его реализация в
процессорах фирмы Интел.
Существуют различные механизмы, позволяющие распределить адресное пространство-управление памятью:
- с внешней памятью
механизмы использования: страничная память / сегментная память / странично-сегментная память
- без внешней памяти
механизмы использования:
с фиксированными разделами (размер, выделяемый в физ.MEM задается при запуске ОС),
с динамическими разделами (первоначально память не разделена),
с перемещаемыми разделами (overlay / swapping / виртуальная память)
Виртуальным называется ресурс, который пользователю или пользовательской программе представляется
обладающим свойствами, которыми он в действительности не обладает. Таким образом, виртуальная
память - это совокупность программно-аппаратных средств, позволяющих пользователям писать программы,
размер которых превосходит имеющуюся оперативную память; для этого виртуальная память решает
следующие задачи, которые выполняются автоматически:



размещает данные в запоминающих устройствах разного типа, например, часть программы в оперативной памяти, а
часть на диске;
перемещает по мере необходимости данные между запоминающими устройствами разного типа, например,
подгружает нужную часть программы с диска в оперативную память;
преобразует виртуальные адреса в физические.
Наиболее распространенными реализациями виртуальной памяти является страничное, сегментное и
странично-сегментное распределение памяти, а также свопинг.
Страничное распределение
Виртуальное адресное пространство каждого процесса делится механически на равные части, - виртуальные страницы.
Вся оперативная память машины делится на части такого же размера, - физические страницы. При загрузке процесса
часть его виртуальных страниц помещается в оперативную память, а остальные - на диск. При загрузке ОС создает для
каждого процесса информационную структуру - таблицу страниц, в которой устанавливается соответствие между
номерами виртуальных и физических страниц для страниц, загруженных в оперативную память, или делается отметка,
что виртуальная страница выгружена на диск. Кроме того, в таблице страниц содержится управляющая информация.
При каждом обращении к памяти происходит чтение из таблицы страниц информации о виртуальной странице, к
которой произошло обращение. Если данная виртуальная страница находится в оперативной памяти, то выполняется
преобразование виртуального адреса в физический. Если же нужная виртуальная страница в данный момент выгружена
на диск, то происходит так называемое страничное прерывание. Выполняющийся процесс переводится в состояние
ожидания, и активизируется другой процесс из очереди готовых. Параллельно программа обработки страничного
прерывания находит на диске требуемую виртуальную страницу и пытается загрузить ее в оперативную память. Если в
памяти имеется свободная физическая страница, то загрузка выполняется немедленно, если же свободных страниц нет,
то решается вопрос, какую страницу следует выгрузить из оперативной памяти.
Сегментное распределение
Виртуальное адресное пространство процесса делится на сегменты, размер которых определяется программистом с
учетом смыслового значения содержащейся в них информации. Отдельный сегмент может представлять собой
подпрограмму, массив данных и т.п. Иногда сегментация программы выполняется по умолчанию компилятором. При
загрузке процесса часть сегментов помещается в оперативную память (при этом для каждого из этих сегментов
операционная система подыскивает подходящий участок свободной памяти), а часть сегментов размещается в дисковой
памяти. Сегменты одной программы могут занимать в оперативной памяти несмежные участки. Во время загрузки
система создает таблицу сегментов процесса, в которой для каждого сегмента указывается начальный физический адрес
сегмента в оперативной памяти, размер сегмента, правила доступа и другая информация. Если виртуальные адресные
пространства нескольких процессов включают один и тот же сегмент, то в таблицах сегментов этих процессов делаются
ссылки на один и тот же участок оперативной памяти, в который данный сегмент загружается в единственном
экземпляре.
Странично-сегментное распределение
Виртуальное пространство процесса делится на сегменты, а каждый сегмент в свою очередь делится на виртуальные
страницы, которые нумеруются в пределах сегмента. Оперативная память делится на физические страницы. Загрузка
процесса выполняется операционной системой постранично, при этом часть страниц размещается в оперативной памяти,
а часть на диске. Для каждого сегмента создается своя таблица страниц, структура которой полностью совпадает со
структурой таблицы страниц, используемой при страничном распределении. Для каждого процесса создается таблица
сегментов, в которой указываются адреса таблиц страниц для всех сегментов данного процесса. Адрес таблицы
сегментов загружается в специальный регистр процессора, когда активизируется соответствующий процесс.
Свопинг
3
Процесс перемещается между памятью и диском целиком, то есть в течение некоторого времени
процесс может полностью отсутствовать в оперативной памяти. Существуют различные алгоритмы
выбора процессов на загрузку и выгрузку, а также различные способы выделения оперативной и
дисковой памяти загружаемому процессу.
4
3. Реализация механизма системного вызова в ОС. Таблица
системных вызовов и методы ее модификации в ОС LINUX.
ОС 2х типов: - с централизованными системными вызовами
- с распределенными системными вызовами
Системные вызовы - это набор функций, реализованных в ядре ОС. Любой запрос приложения пользователя
в конечном итоге трансформируется в системный вызов, который выполняет запрашиваемое действие.
Рассмотрим ОС с централиз.сист.вызовами: используется один вектор, к-рый инициирует сист. вызовы (синхр/асинхр)
таблица
syscall
————
————
————
————
————
————
——-
user
process
обработчик
syscall
процедура
Механизм организации системных вызовов:
1) в регистр ядра записывается номер системного вызова (цифра пишется в стек)
2) вызывается функция INTH
3) передача управления в обработчик системных вызовов
4) обработчик syscall проверяет, корректен ли системный вызов (сохраняет контекст текущей
работы usera; обращается к таблице syscall, в которой прописаны адреса функций,
обрабатывающих системные вызовы)
5) вызывается процедура и результат отправляется user`y
void** sys_call_table;
int mkdir(const char* path)
{return 0;}
int (*orig_mkdir)(const char*);
int init_module (void){
sys_call_table=(void**)0xc0331f0;
printk("Hella!\n");
orig_mkdir=sys_call_table[SYS_mkdir];
sys_call_table[SYS_mkdir]=(void*)mkdir;
return 0;
};
sys_read
Используется для чтения из файлов
sys_write
Используется для записи в файлы
sys_open
Используется для создания или открытия файлов
sys_chdir
Используется для изменения текущей директории
sys_ioctl
Используется для работы с устройствами
sys_kill
Используется для отправки сигналов процессам
5
4. Понятие процесса и потока: раскройте и охарактеризуйте.
Процесс – адресное пространство (~рассматривается операционной системой как заявка на
потребление всех видов ресурсов, кроме одного-проц.вр.) - выполняемое приложение, обладающее
собственной памятью, описателями файлов и другими системными ресурсами. С каждым процессом
связано свое адресное пространство. Любой процесс, обращаясь при доступе к ядру, приобретает
привилегии ядра, переходит в режим ядра; а после выполнения процесса переходит обратно в
пользовательский процесс.
Поток – последовательность команд (~ последовательность/список команд, которые должен
выполнять процессор) - код, исполняемый внутри процесса. Процессорное время распределяется
операционной системой между потоками.
Внутри процесса существует хотя бы один поток. Процесс может иметь как один поток, так и
множество параллельно выполняющихся потоков. Все потоки одного и того же процесса используют
одни и те же ресурсы.
Все пространство кода и данных процесса доступно всем его потокам. Несколько потоков могут
обращаться к одним и тем же глобальным переменным или функциям.
Потоками управляет операционная система, и у каждого потока есть свой собственный стек.
6
5. Многослойная структура ядра: принципы построения
современных ОС.
ОС включает:

модули самой ОС

исходные коды

файлы документации

дополнительные программные модули
В общем случае: ядро ОС (основная часть) + вспомогательные модули (дополнительные модули ОС)
ВМ1
ВМ2
ВМn
Я
ВМ3
Ядро: управление виртуальной памятью
Модули ОС: управление функциональными
компонентами ОС, архивация и обработка данных
…
Пользовательский
процесс
Пользовательский
процесс
Процесс
ядра
Безопасность
ОС
реализуется
привилегированности процессов.
за
счет
Любой процесс при доступе обращаясь к ядру,
приобретает привилегии ядра, переходит в режим
ядра; а после выполнения процесса переходит
обратно в пользовательский процесс.
В операционной системе Unix одновременно работает множество процессов, выполняющих самые
разнообразные задачи. Каждый процесс запрашивает у системы различные ресурсы, -принимает и
обрабатывает все эти запросы -- ядро, которое является одним большим испоняемым файлом.
Дополнительные модули и
приложения
Ядро ОС
Аппаратная
среда
Ядро операционной системы должно управлять всеми
устройствами и программами в системе, распределяя
аппаратные ресурсы и запуская программы по мере
надобности.
За управление памятью, управление программным
обеспечением, управление устройствами системы и за
управление файловой системой отвечает главным образом
ядро.
Наиболее часто используется «послойная система» ядра, элементы типовой модели:





7
аппаратная поддержка ОС со стороны микропроцессора
аппаратно-зависимый слой (Hardware Associated Layer) // повторяет «железо»
базовые механизмы ОС // пытается оторваться от «железа»
менеджеры ресурсов
(менеджер процессов, менеджер вв/выв, менеджер вирт.памяти = выгр/загр. страниц)
слой API (слой системных вызовов) //через этот слой происходит переключение режимов
Принципы построения ОС:
1) должна быть написана на языке высокого уровня
2) элементы в «железе» необходимо заменить «абстрактными»
3) необходимо минимизировать число модулей, работающих непосредственно с «железом»
8
6. Перечислите и охарактеризуйте основные подсистемы ядра
ОС LINUX.
Рисунок 1-1. Структурная схема ядра.
Управление процессами
Одной из функций ядра является управление созданием и уничтожением процессов, обеспечение
взаимодействия процессов с внешним миром (ввод/вывод) и обеспечение взаимодействия процессов между
собой (сигналы, каналы -pipes, и IPC-примитивы (interprocess communication)). Также, в функции управления
процессами, входит диспетчеризация процессов, которая управляет разделением времени процессора между
процессами.
Управление памятью
Память компьютера представляет собой очень важный ресурс компьютера, и реализация политики его
разделения существенно влияет на производительность операционной системы. Ядро предоставляет огромные
виртуальные пространства памяти некоторым или всем процессам на фоне ограниченных физических ресурсов.
Различные части ядра взаимодействуют с подсистемой управления памятью через определенный набор
функций, например malloc()/free().
Файловые системы
Unix жестко связан с концепцией файловых систем: почти все в Unix может быть представлено как файл. Ядро
выстраивает структурированную файловую систему из неструктурированного аппаратного слоя, и
результирующая файловая абстракция жестко вплетена во все компоненты системы. В дополнении к этому,
Linux поддерживает множество типов файловых систем, которые различным способом организуют данные на
физических носителях информации. Для примера, дискета может быть отформатирована либо в Linux стандарте,
файловой системе ext2, либо, в распространенных на Windows платформе, форматах FATx.
Управление устройствами
Практически каждая системная операция, в конечном счете, отображается на физическое устройство. За
исключением процессора, памяти и некоторых других элементов, все остальные операции управления
устройствами выполняются кодом, специфичным для адресуемого устройства. Этот код называется драйвером
устройства. Ядро должно включать в себя драйвер каждого устройства, управляемого системой, начиная от
жесткого диска и заканчивая клавиатурой и мышью.
Сетевые службы
Сетевой транспорт должен быть реализован в ядре операционной системы, т.к. большинство сетевых операций
не специфичны для процессов – поступление пакетов является асинхронным событием. Пакеты должны быть
собраны, идентифицированы и диспетчеризованы перед тем, как будут переданы на дальнейшую обработку в
пользовательские процессы. Система должна управлять передачей пакетов данных между программами через
сетевые интерфейсы. Маршрутизация и система распознавания различных классов сетевых адресов также
должны быть реализованы в ядре операционной системы.
9
Одной из замечательных характеристик ядра Linux является способность расширять
функциональность ядра во время его работы. Это означает, что можно добавить требуемые функции
в ядро без перезагрузки операционной системы.
7. Перечислите и охарактеризуйте основные классы устройств и
модулей ядра ОС LINUX.
В Unix устройства подразделяются на три класса (типа). Каждый модуль реализует поддержку одного из этих
классов устройств, и, таким образом, подразделяется на модули символьных, блочных и сетевых устройств.
Такое разделение модулей на различные классы не является жестким. Программист может создать большой
модуль, реализующий различные драйверы в одном куске кода. Однако более верным стилем
программирования является создание различных модулей для каждой новой функциональности, которую они
добавляют, т.к. декомпозиция является ключевым элементом масштабируемости и расширяемости.
Символьные устройства (character devices)
- устройство, которое может быть представлено потоком байт (как файл). Такие драйвера реализуют, по меньшей мере,
системные вызовы open(), close(), read() и write(). Текстовая консоль (/dev/console) и последовательные порты (/dev/ttyS0
и аналогичные) представляют собой примеры символьных устройств. Доступ к символьным устройствам реализуется
через специальные файлы, называемые интерфейсами устройств, которые обычно располагаются в каталоге /dev.
Отличие между символьным устройством и файлом, заключается в том, что открыв обычный файл, вы можете
перемещаться по нему как вперед, так и назад, в то время как символьное устройство представляет собой
последовательный канал данных. Однако, существуют символьные устройства, которые представляются как область
данных, и вы также можете перемещаться по ней как вперед, так и назад, используя функции lseek() и mmap().
Блочные устройства (block devices)
Доступ к блочным устройствам, так же как и к символьным, осуществляется через специальные файлы-интерфейсы
устройств, расположенные, обычно, в каталоге /dev. На блочных устройствах, как правило, размещаются файловые
системы. В большинстве Unix систем, блочные устройства могут быть представлены только как множество блоков.
Размер блока кратен степени двух и часто равен одному килобайту данных. Linux позволяет приложениям читать и
писать в блочные устройства, также как и в символьные. Разница заключается в том, что при обращении к блочному
устройству передается блок данных, а не один байт (символ). Для пользователя, блочные и символьные устройства
неразличимы. Драйвер блочного устройства взаимодействует с ядром через более широкий блочно-ориентированный
интерфейс, но это скрыто от пользователя и приложений, которые взаимодействуют с устройством через файл
интерфейса устройства, расположенного, как правило, в каталоге /dev. Интерфейсы блочных устройств наиболее удобны
для монтирования файловых систем.
Сетевые интерфейсы (network interfaces)
Сетевой интерфейс м.б. реализован на основе аппаратного устройства(обычно) или чисто программно (интерфейс
loopback). На сетевой интерфейс, который управляется сетевой подсистемой ядра, наложены функции приема и передачи
пакетов данных. Разные сетевые службы, например telnet или FTP, ведут передачу данных через одно и тоже сетевое
устройство, которое не знает об индивидуальных потоках, и только передает пакеты данных. При этом и telnet и FTP
являются потоко-ориентированными системами, в то время как сетевой интерфейс не принадлежит к потокоориентированным устройствам, а работает с дискретными данными - пакетами. Вместо чтения и записи, ядро вызывает
функции, относящиеся к передаче пакетов.
Основное различие блочных и символьных устройств состоит в том, что обмен данными с блочным
устройством производится порциями байт — блоками. Они имеют внутренний буфер, благодаря
чему повышается скорость обмена. В большинстве Unix-систем размер одного блока равен 1
килобайту или другому числу, являющемуся степенью числа 2. Символьные же устройства — это
лишь каналы передачи информации, по которым данные следуют последовательно, байт за байтом.
Большинство устройств относятся к классу символьных, поскольку они не ограничены размером
блока и не нуждаются в буферизации. Если первый символ в списке, полученном командой ls-l /dev,
'b', тогда это блочное устройство, если 'c', тогда — символьное.
В ОС Linux представлены и другие классы модулей. Модули каждого класса предоставляют
интерфейс для предоставления определенного типа устройств. Поэтому, можно говорить о модулях
шины USB, последовательного порта и т.д.
10
К наиболее общему, нестандартному классу устройств относятся устройства SCSI. (Small Computer
System Interface, и представляет собой учрежденный стандарт на рынке рабочих станций и high-end
серверов). И хотя любое устройство подсоединенное к шине SCSI представляется файлом
интерфейсом в каталоге /dev как символьное или блочное устройство, внутренняя организация таких
драйверов различна.
11
8. Программная структура модулей ядра. Загрузка и выгрузка
модулей. Функции init_module и cleanup_module.
Основное назначение модулей - расширение функциональности ядра. Код модуля исполняется в
пространстве ядра. Обычно модуль реализует две задачи - одни функции выполняются как часть
системных вызовов, другие - выполняют обработку прерываний.
М1
М2
Я
Регистрируются
функциональные
возможности модуля
Добавить функцию:
1) необходимо прописать в файле name.c
init_module
2) gcc = name.o
3) insmod (insert module) name
Модуль регистрирует себя самого в ядре,
подготавливая его для обслуживания возможных
запросов, и его функция "main" завершает свою работу
сразу же после вызова.
Отключить функцию:
1) необходимо прописать в файле name.c
cleanup_module
2) gcc = name.o
3) insmod (insert module) name
4) …..
5) remmod (remove module) name
Любой модуль ядра должен иметь по меньшей мере хотя бы две функции: функцию инициализации
модуля — init_module(), которую вызывает insmod во время загрузки модуля, и функцию
завершения работы модуля — cleanup_module(), которую вызывает rmmod.
Обычно функция init_module(точка входа) выполняет регистрацию обработчика какого-либо
события или замещает какую-либо функцию в ядре своим кодом (который, как правило, выполнив
некие специфические действия, вызывает оригинальную версию функции в ядре).
Функция cleanup_module(вторая точка входа) является полной противоположностью, она производит
"откат" изменений, сделаных функцией init_module(), что делает выгрузку модуля безопасной.
На рисунке показана схема вызова
функций
и
как
используются
указатели на функции в модуле при
добавлении его в ядро.
Поскольку модуль не связывается ни
с одной из стандартных библиотек,
исходные тексты модуля не должны
подключать обычные заголовочные
файлы. В модулях ядра могут
использоваться только те функции,
которые экспортируются ядром.
Все заголовочные файлы, которые
относятся к ядру, расположены в
каталогах
include/linux
и
include/asm,
внутри
дерева
каталогов с исходными текстами ядра
(как
правило
это
каталог
/usr/src/linux).
12
9. Реализация пользовательского режима и режима ядра в
системе LINUX.
Модуль выполняется в так называемом пространстве ядра (режим ядра), тогда как приложения
работают в пространстве пользовательского процесса (пользовательский режим).
Пользовательский режим - наименее привилегированный режим, поддерживаемый LINUX; он не
имеет прямого доступа к оборудованию и у него ограниченный доступ к памяти. В режиме
"пользователь" запрещено выполнение действий, связанных с управлением ресурсами системы, в
частности, корректировка системных таблиц, управление внешними устройствами, маскирование
прерываний, обработка прерываний. В режиме "пользователь" выполняются оболочка и прикладные
программы.
Режим ядра - привилегированный режим.. Когда процесс запускает системный запрос, он не
передает управление другому процессу, а скорее меняет режим исполнения на режим ядра. В этом
режиме он запускает он запускает защищенный от ошибок код ядра. В режиме "система"
выполняются программы ядра.
При необходимости выполнить привилегированные действия пользовательский процесс обращается с
запросом к ядру в форме так называемого системного вызова. В результате системного вызова управление
передается соответствующей программе ядра. С момента начала выполнения системного вызова процесс
считается системным. Таким образом, один и тот же процесс может находиться в пользовательской и
системной фазах. Эти фазы никогда не выполняются одновременно.
Если процессор имеет более двух уровней привилегий, то используются наинизший и наивысший
(0й). Ядро Unix работает на наивысшем уровне привилегий, обеспечивая управление оборудованием
и процессами пользователя. Аппаратный уровень привилегий процессора определяет возможное
множество инструкций, которые может вызывать исполняемый в данный момент процессором код.
Хотя понятия «режим пользователя» и «режим ядра» часто используются для описания кода, на
самом деле это уровни привилегий, ассоциированные с процессором.
Уровень привилегий накладывает три типа ограничений:
1) возможность выполнения привилегированных команд,
2) запрет обращения к данным с более высоким уровнем привилегий,
3) запрет передачи управления коду с уровнем привилегий, не равным уровню привилегий вызывающего кода.
Когда мы говорим о пространстве ядра и пространстве пользовательского процесса имеются в
виду не только разные уровни привилегий исполняемого кода, но и разные адресные пространства.
Unix передает исполнение из пространства пользовательского процесса в пространство ядра в двух случаях:
1) когда пользовательское приложение выполняет обращение к ядру (системный вызов),
2) во время обслуживания аппаратных прерываний.
Обычно, модуль осуществляет две задачи: некоторые функции модуля исполняются как часть
системных вызовов, а некоторые ответственны за управление прерываниями.
Драйвер будет запущен в режиме работы с ядром, а ядро Linux не имеет средств принудительного
сброса => если драйвер будет долго работать, не давая при этом работать другим программам, компьютер может "зависнуть". Нормальный пользовательский режим с последовательным запуском
не обращается к вашему драйверу.
Процесс работает одновременно и в режиме ядра, и в режиме пользователя. Основная часть процесса запускается в
пользовательском режиме, системные вызовы запускаются в режиме ядра. Стеки, используемые работающими в разных
13
режимах процессами, различны - определенный сегментный стек используется в пользовательском режиме, а режим
ядра использует стек определенной величины.
14
10. Сравните модуль ядра и модуль приложения
пользовательского режима: что общего и в чем разница
Модуль - это код, который может быть загружен или выгружен ядром по мере необходимости.
Модули ядра исполняется на высшем уровне (он еще называется привилегированный режим), где
позволяется выполнение любых действий.
Модули приложения исполняются на самом нижнем уровне (так называемый непривилегированный
режим), где прямой доступ к аппаратуре и памяти регулируется процессором.
Разл: Модули ядра старше, вызываются ядром;
Общ: режимы связаны между собой,-функциональность
Приложение имеет одну точку входа, которая начинает исполняется сразу же после размещения
запущенного приложения в оперативной памяти компьютера. Эта точка входа описывается как
функция main(). Завершение функции main() означает завершение приложения.
Модуль имеет несколько точек входа, исполняемых при установке и удалении модуля из ядра, а
также при обработке поступающих, от пользователя, запросов. Так, точка входа init_module()
исполняется при загрузке модуля в ядро. Функция cleanup_module() исполняется при выгрузке
модуля.
Приложение может вызвать функцию, которая не была объявлена в приложении. На стадиях
статической или динамической линковки определяются адреса таких функций из соответствующих
библиотек.
Одна из особенностей ОС Linux - отсутствие библиотек, которые могут быть слинкованы с
модулями ядра. Модули при загрузке, линкуются в ядро, поэтому все, внешние для этого модуля,
функции должны быть объявлены в заголовочных файлах ядра и присутствовать в ядре. Исходники
модулей никогда не должны включать обычные заголовочные файлы из библиотек
пользовательского пространства. В модулях ядра вы можете использовать только функции, которые
действительно являются частью ядра.
15
11. Опишите процесс динамической компоновки модулей ядра с
действующим ядром. Утилиты insmod, modprobe и rmmod.
После написания программного кода модуля ядра происходит его компиляция.
gcc – c –I/usr/src/linux-$(uname -r)/include mod.c
После компиляции получается объяктный код, в котором не произведена линковка с ядром.
После загрузки модуля с помощью команды insmod модуль связывается с ядром и имеет доступ к
опубликованным (экспортированным) функциям и переменным ядра. Т.е. утилита insmod связывает
все неопределенные символы (вызовы функций и пр.) модуля с символьной таблицей запущенного
ядра, при этом она не изменяет дисковый файл модуля, а загружает слинкованный с ядром объект
модуля в оперативную память. Утилита insmod может принимать некоторые опции командной
строки.
Функциональность утилиты modprobe во многом похожа на утилиту insmod, но при загрузке модуля
проверяет его нижележащие зависимости, и, при необходимости, подгружает необходимые
модули до требуемого заполнения стека модулей. Таким образом, одна команда modprobe может
приводить к нескольким вызовам команды insmod. Можно сказать, что команда modprobe является
интеллектуальной оболочкой над insmod. Можно использовать modprobe вместо insmod везде, за
исключением случаев загрузки собственных модулей из текущего каталога, т.к. modprobe
просматривает только специальные каталоги размещения модулей, и не сможет удовлетворить
возможные зависимости.
Для выгрузки модуля используется утилита rmmod. Выгрузка модуля более простая задача нежели
его загрузка, при которой выполняется его динамическая линковка с ядром. При выгрузке модуля
выполняется системный вызов delete_module(), который либо выполняет вызов функции
cleanup_module() выгружаемого модуля в случае, если его счетчик использования равен нулю, либо
прекращает работу с ошибкой.
16
12. Охарактеризуйте механизм проверки версии модулей ядра.
У каждого модуля ядра есть несколько версий. Направление вывода сообщений ядра с приоритетом
по умолчанию зависит от версии запущенного ядра.
Модули, скомпилированные с одним ядром, могут не загружаться другим ядром, если в ядре
включен механизм проверки версий модулей. В большинстве дистрибутивов ядро собирается с
такой поддержкой. В случае возникновения проблем можно пересобрать ядро без поддержки
механизма контроля версий.
При компиляции модуля специальный макрос из <module.h> определяет номер версии ядра, для
которого производится компиляция, и помещается полученный номер в специальгую секцию
объектного файла модуля. Поэтому при включении его в код модуля при компиляции автоматически
определяется номер ядра. Если номер ядра не определен, то он принимается раным номеру текущего
релиза ядра (например, 2.2 или 2.4).
При выполнении утилиты insmod происходит автоматическая сверка номера ядра, для которого был
скомпилирован модуль и текущей версии ядра. Если они не совпадают, модуль не будет загружен в
ядро.
В случае отказа загрузки модуля по причине несоответствия версий, можно попытаться загрузить
этот модуль передав в строку параметров утилиты insmod ключ -f (force). В этом случае линковка
производится по сигнатурам функций. Если сигнатура функций в новой версии ядра была изменена,
то модуль все ранво не будет загружен.
Если вы хотите скомпилировать ваш модуль для особой версии ядра, вы должны включить
заголовочные файл именно от этой версии ядра.
17
13. Подсчет ссылок на модули ядра. Использование макросов
MOD_INC_USE_COUNT, MOD_DEC_USE_COUNT и MOD_IN_USE.
Сейчас подсчет ссылок происходит автоматически, раньше использовали макросы.
Система содержит счетчик использования каждого модуля для того, чтобы определить возможность
безопасной выгрузки модуля. Системе нужна эта информация, потому что модуль не может быть
выгружен, если он кем-нибудь или чем-нибудь занят – вы не можете удалить драйвер файловой
системы, если эта файловая система примонтирована, или вы не можете выгрузить модуль
символьного устройства, если какой-нибудь процесс использует это устройство. В противном
случае, это может привести к краху системы – segmentation fault или kernel panic.
В современных ядрах, система может предоставить вам автоматический счетчик использования
модуля используя механизм, который мы рассмотрим в следующей главе. Независимо от версии
ядра можно использовать ручное управление данным счетчиком. Так, код, который предполагается
использовать в старых версиях ядра должен использовать модель учета используемости модуля
построенную на следующих трех макросах:
MOD_INC_USE_COUNT
Увеличивает счетчик использования текущего модуля
int silly_open(struct inode *inode, struct file *filp)
{
MOD_INC_USE_COUNT;
return 0;
}
MOD_DEC_USE_COUNT
Уменьшает счетчик использования текущего модуля
MOD_IN_USE
Возвращает истину, если счетчик использования данного модуля равен нулю. Не требуется
проверять MOD_IN_USE в коде функции cleanup_module(), потому, что эта проверка
выполняется автоматически до вызова cleanup_module() в системном вызове
sys_delete_module(), который определен в kernel/module.c.
Эти макросы определены в <linux/module.h>, и они манипулируют специальной внутренней
структурой данных прямой доступ к которой нежелателен. Дело в том, что внутренняя структура и
способ управления этими данными могут меняться от версии к версии, в то время как внешний
интерфейс использования этих макросов остается неизменным.
Корректное управление счетчиком использования модуля критично для стабильности системы. Ядро
может решить автоматически выгрузить неиспользуемый модуль в любое время. Например, в ответ
на некий запрос, код модуля выполняет некоторые действия и при завершении обработки
увеличивает счетчик использования модуля. При обработке запроса к модулю надо вызывать
MOD_INC_USE_COUNT перед выполнением каких либо действий, и MOD_DEC_USE_COUNT
после их выполнения.
Возможны ситуации, в которых, по понятным причинам, вы не сможете выгрузить модуль если
потеряете управление счетчиком его использования. Такая ситуация часто встречается на этапе
разработки модуля. Например, процесс может прерваться при попытке разыменования NULL
указателя, и вы не сможете выгрузить такой модуль, пока не вернете счетчик его использования к
нулю. Одно из возможных решений такой проблемы на этапе отладки модуля заключается в полном
отказе
от
управления
счетчиком
использования
модуля
путем
переопределения
MOD_INC_USE_COUNT и MOD_DEC_USE_COUNT в пустой код.
18
14. Драйверы символьных устройств. Старший (major) и
младший (minor) номера устройств.
Символьные устройства доступны через специальные символьные файлы устройств файловой
системы Unix. Такие файлы часто называются интерфейсами символьных устройств. Обычно, они
располагаются в каталоге /dev. С помощью команды ls -l можно вывести список файлов этого
каталога, (интерфейсы симв. у-тв помечены символом “c”, интерфейсы бл.у-тв с символом “b”). В
строках вывода есть два номера разделенные запятыми перед датой последнего изменения файла, на
месте где обычно располагается размер файла. Эти номера представляют собой старший и младший
номер каждого из устройств. Ниже приведен типичный пример вывода команды ls -l. В этом списке,
старшие номера устройств представлены числами 1, 10, тогда как младшие – числами 3, 1.
crw-rw-rw- 1 root
root
1, 3
Feb 23 1999 null
crw———- 1 root
root
10, 1
Feb 23 1999 psaux
Старший номер (major) определяет драйвер связанный с устройством, т.е. это номер драйвера.
Например, устройства /dev/null используют драйвер с номером 1. Ядро использует старший номер
устройства для диспетчеризации запроса на требуемый драйвер. [Старший номер говорит о том, какой
драйвер используется для обслуживания аппаратного обеспечения. Каждый драйвер имеет свой уникальный старший
номер. Все файлы устройств с одинаковым старшим номером управляются одним и тем же драйвером.]
Младший номер (minor) определяет устройство, и используется драйвером, определенным по
старшему номеру. Никакие другие части ядра не используют младший номер устройства. Наиболее
часто, один драйвер управляет разными устройствами различаемыми кодом драйвера по младшему
номеру. [Младший номер используется драйвером, для различения аппаратных средств, которыми он управляет. Даже
если несколько устройств обслуживаются одним и тем же драйвером, тем не менее каждое из них имеет уникальный
младший номер, поэтому драйвер "видит" их как различные аппаратные устройства.] На практике, различные
младшие номера используются для доступа к различным устройствам управляемым одним
драйвером, или для открытия одного и того же устройства с разными целями.
Добавление нового драйвера в систему означает назначение ему старшего номера.
Старший номер устройства представляет собой небольшое целое число, являющееся индексом в массиве драйверов
символьных устройств. Ядро версии 2.0 поддерживало 128 устройств, ядра версий 2.2 и 2.4 увеличили их число до 256
(резервируя номера 0 и 255 для особого использования в будущем).
Младшие номера (0 до 255), также, представляют собой восьмибитовую величину. Эти номера не передаются в функцию
register_chardev(), потому что они используются только для диспетчеризации устройств в коде драйвера, и системе
безынтересны.
struct file_operation fop={
…
open: drv_open;
… };
int drv_open (struct inode * inodp, struct file &filp)
{ int k=MINOR (inodp ->i_rdev); } //макрос, вызывающий minor
19
number
15. Динамическое выделение старших номеров устройств.
Некоторые старшие номера статически назначаются наиболее общим устройствам. Список таких
устройств - в файле Documentation/devices.txt, расположенном в дереве каталогов источников ядра.
Следует учитывать 1) что многие номера уже назначены, 2) что в системе могут работать
пользовательские драйверы с назначенными старшими номерами.
Можно выбрать старший номер устройства из зарезервированных для экспериментов, но при этом
могут возникнуть проблемы с другими пользовательскими драйверами. Результаты особенно
непредсказуемы после передачи вашего драйвера в пользование другим людям.
Старшие номера устройств в диапазонах от 60 до 63, от 120 до 127, от 240 до 254 зарезервированы
для местного и экспериментального использования. Эти номера не должны быть назначены
реальным устройствам.
Можно запросить динамическое назначение для старших номеров устройств.
Если параметр major установлен в 0, при вызове register_chrdev(), то функция регистрации сама
выбирает свободный номер и возвращает его. Возвращенный, в этом случае номер, всегда
положителен. Отрицательное значение всегда говорит об ошибке исполнения функции. Заметьте,
что интерпретация возвращаемого значения функции register_chardev() несколько отличается при
статическом и динамическом назначении номеров.
int register_chrdev(unsigned int major, const char *name, struct file_operations *fops);
где unsigned int major -- запрашиваемый старший номер устройства, const char *name -- название
устройства, которое будет отображаться в /proc/devices и struct file_operations *fops -- указатель на
таблицу file_operations драйвера. В случае ошибки, функция register_chrdev() возвращает
отрицательное число.
При статическом назначении функция возвращает ноль в случае успеха, в то время, как при
динамическом назначении, функция возвращает назначенный номер устройства, или отрицательный
номер ошибки.
Лучше использовать динамическое назначение старшего номера устройства.
С другой стороны, драйвер станет гораздо более удобным в использовании сторонними пользователями, если он будет
включен в официальное дерево драйверов, и ему будет назначен уникальный статический номер.
Неудобство динамического назначения: нельзя использовать постоянный файл интерфейса
устройства, потому что нет никакой гарантии, что при следующей регистрации, ваш модуль получит
тот же самый старший номер устройства. Таким образом, нельзя обеспечить загрузку драйвера по
требованию. Это представляет проблему для нормального использования драйвера –придется
определять назначенный номер устройства по содержимому файла /proc/devices.
Лучший способ назначения старшего номера устройства заключается в использовании
одновременно динамического и явного назначения. В качестве умолчания можно использовать
динамическое выделение старшего номера устройства, а опционально, использовать явное
назначение номера, либо во время загрузки модуля, либо, даже, во время его компиляции.
20
16. Регистрация драйвера символьного устройства и удаление
драйвера из системы.
Добавление драйвера в систему подразумевает его регистрацию в ядре, т.е. получение старшего
номера в момент инициализации модуля. Получить его можно вызовом функции register_chrdev(),
определенной в <linux/fs.h>:
int register_chrdev(unsigned int major, const char *name, struct file_operations
*fops);
- функция регистрации драйвера устройства. Если старший номер задан нулем, то функция
назначает устройству динамический старший номер. В случае ошибки, функция
register_chrdev() возвращает отрицательное число.
int unregister_chrdev(unsigned int major, const char *name);
- функция отмены регистрации вызываемая при выгрузке драйвера. (Когда модуль
выгружается из системы, старший номер устройства должен быть освобожден). Значение
major и name должны быть такими же, какие были у драйвера во время регистрации.
unsigned int major
const char *name
-- запрашиваемый старший номер устройства,
-- название устройства, которое будет отображаться в /proc/devices
struct file_operations *fops
-- указатель на таблицу file_operations драйвера.
Ядро сравнивает переданное в функцию имя с зарегистрированным для данного номера. Если
значения имени не совпадают, то функция возвращает значение -EINVAL. Ядро, также возвращает EINVAL, если значение старшего номера устройства выходит за допустимый диапазон.
В случае ошибки освобождения старшего номера необходимо загрузить в систему сразу два модуля
– оригинальный модуль содержащий ошибку и модуль специально написанный для корректного
освобождения номера. Если вы не изменили код оригинального модуля, то он загрузится в туже
самую область памяти и строка с именем будет расположена в том же самом месте. Другой
безопасной альтернативой написанию специального модуля исправляющего ошибку является,
конечно же, перезагрузка системы.
21
17. Перечислите основные виды файлов в ОС LINUX и
охарактеризуйте их.
С точки зрения операционной системы файл представляет собой просто поток байтов. Такой подход
позволяет распространить концепцию файла на физические устройства и некоторые другие объекты.
Это позволяет упростить организацию данных и обмен ими, потому что аналогичным образом
осуществляется запись данных в файл, передача их на физические устройства и обмен данными
между процессами. С точки зрения ОС Linux, все подключаемые к компьютеру устройства (жесткие
и съемные диски, терминал, принтер, модем и т. д.), представляются файлами. Если, например, надо
вывести на экран какую-то информацию, то система как бы производит запись в файл /dev/tty01. Во
всех случаях используется один и тот же подход, основанный на идее байтового потока. Поэтому
наряду с обычными файлами и каталогами, файлами с точки зрения Linux являются также:
 файлы физических устройств;
Физические устройства бывают двух типов: символьными (или байт-ориентированными) и блочными (или блокориентированными). Различие между ними состоит в том, как производится считывание и запись информации в эти
устройства. Взаимодействие с символьными устройствами производится посимвольно, в режиме потока байтов. К таким
устройствам относятся, например, терминалы. На блок-ориентированных устройствах информация записывается (и,
соответственно, считывается) блоками. Примером устройств этого типа являются жесткие диски. На диск невозможно
записать или считать с него один байт: обмен с диском производится только блоками.
Взаимодействием с физическими устройствами в Linux управляют драйверы устройств, которые либо встроены в ядро,
либо подключаются к нему как отдельные модули. Для взаимодействия с остальными частями операционной системы
каждый драйвер образует коммуникационный интерфейс, который выглядит как файл. Большинство таких файлов для
различных устройств как бы "заготовлены заранее" и располагаются в каталоге /dev.
 именованные каналы или буферы FIFO (named pipes);
Файлы этого типа служат в основном для того, чтобы организовать обмен данными между разными. Канал — это очень
удобное и широко применяемое средство обмена информацией между процессами. Все, что один процесс помещает в
канал, другой может оттуда прочитать. Если два процесса, обменивающиеся информацией, порождены одним и тем же
родительским процессом (а так чаще всего и происходит), канал может быть неименованным. При этом собственно файл
именованного канала участвует только в инициации обмена данными.
 гнезда (sockets);
Гнезда — это соединения между процессами, которые позволяют им взаимодействовать, не подвергаясь влиянию других
процессов. С точки зрения файловой системы гнезда практически неотличимы от именованных каналов: это просто
метки, позволяющие связать несколько программ. После того как связь установлена, общение программ происходит без
участия файла гнезда: данные передаются ядром ОС непосредственно от одной программы к другой.
 символические ссылки (symlinks).
Жесткая ссылка является еще одним именем для исходного файла. Она прописывается в индексном дескрипторе
исходного файла. После создания жесткой ссылки невозможно различить, где исходное имя файла, а где ссылка. Если вы
удаляете один из этих файлов (точнее одно из этих имен), то файл еще сохраняется на диске (пока у него есть хоть одно
имя-ссылка). Одно из применений жестких ссылок состоит в том, чтобы предотвратить возможность случайного
удаления файла. Особенностью жестких ссылок является то, что они прямо указывают на номер индексного
дескриптора, т.е. такие имена могут указывать только на файлы внутри той же самой файловой системы (на том же
самом носителе, на котором находится каталог, содержащий это имя).
Символические ссылки тоже могут рассматриваться как дополнительные имена файлов, но в то же время они
представляются отдельными файлами - файлами типа символических ссылок. В отличие от жестких ссылок
символические ссылки могут указывать на файлы, расположенные в другой файловой системе, например, на
монтируемом носителе, или даже на другом компьютере. Если исходный файл удален, символическая ссылка не
удаляется, но становится бесполезной. Используйте символические ссылки в тех случаях, когда хотите избежать
путаницы, связанной с применением жестких ссылок.
Создание любой ссылки внешне подобно копированию файла, но фактически как исходное имя файла, так и
ссылка указывают на один и тот же реальный файл на диске. Поэтому, например, если вы внесли изменения в
файл, обратившись к нему под одним именем, вы обнаружите эти изменения и тогда, когда обратитесь к
файлу по имени-ссылке.
22
18. Структура file_operations: основные члены и назначение.
Использование расширенного синтаксиса для ее
инициализации.
Открытое устройство характеризуется структурой file, а ядро использует структуру file_operations
для доступа к функциям драйвера. Структура, определенная в заголовочном файле <linux/fs.h>
представляет собой массив указателей на функции драйвера, обрабатывающие стандартные запросы.
Каждый файл интерфейса связан с собственным набором функций, т.к. для каждого из них
определено поле f_op указывающее на структуру file_operations. Эти механизмы поддерживаются
системными вызовами open(), read() и пр.
Каждое поле этой структуры должно быть указателем на функцию драйвера, выполняющую
определенную операцию по обработке одного из стандартных запросов к драйверу, или иметь
значение NULL если такой запрос не поддерживается драйвером.
Именно через структуру file_operations добавляется новая функциональность в ядро.
Ниже приводится определение структуры, взятое из исходных текстов ядра 2.6.5:
struct file_operations {
loff_t(*llseek) (struct file *, loff_t, int);// loff_t drv_llseek (struct file *filp, loff_t off, int whence)
//симв.устройства представляются как область данных, и можно перемещаться по ней как вперед, так и назад,
ssize_t(*read) (struct file *, char __user *, size_t, loff_t *);
// для получения данных из устройства.// ssize_t drv_read (struct file * filp, char* buff, size_t count, loff_t * pos)
ssize_t(*write) (struct file *, const char __user *, size_t, loff_t *);
// пишет данные в устройство.// ssize_t drv_write (struct file * filp, char* buff, size_t count, loff_t * pos)
int (*ioctl) (struct inode *, struct file *, unsigned int, unsigned long);
// для выполнения различных операций по упр.оборудованием через драйвер //
int (*open) (struct inode *, struct file *);
//для проведения подготовки к дальнейшим операциям с драйвером// int drv_open(struct inode * nod, struct file * filp)
int (*release) (struct inode *, struct file *);
…};
// освобождает память , занятую под указатели filp->private_data в методе open() и выполняет все завершающие действия
при последнем закрытии устройства// int mydrv_release(struct inode * nod, struct file * filp)
struct inode * – Указатель на структуру inode специального файла устройства, доступного для использования
непосредственно пользователем.
char * buff - адрес, куда надо прочесть пользовательскую часть.
size_t size - размер, кот. пользователь хочет прочесть.
loff_t * pos - ссылается на текущую позицию внутри файла.
Драйвер зачастую реализует не все функции, предусмотренные структурой file_operations. Поля
структуры с нереализованными функциями, заполняются "пустыми" указателями — NULL.
Компилятор gcc предоставляет программисту довольно удобный способ заполнения полей
структуры в исходном тексте. Пример подобного заполнения:
struct file_operations fops = {read: device_read,
write: device_write,};
Однако, существует еще один способ заполнения структур, который описывается стандартом C99. Причем
этот способ более предпочтителен. gcc 2.95, который я использую, поддерживает синтаксис C99. Вам так же
следует придерживаться этого синтаксиса, если вы желаете обеспечить переносимость своему драйверу:
23
struct file_operations fops = {.read = device_read, .write = device_write,};
24
19. Структура file: члены и назначение.
Структура file, определенная в <linux/fs.h>, является второй, по важности структурой данных, используемой в
драйверах устройств. (Структура FILE, доступная программам пользовательского пространства, определена
в стандартной библиотеке языка Си, и не видна из кода ядра). В противоположность этому структура file
принадлежит ядру, и не используется в программах пользователя.
Структура file представляет информацию об открытом файле. !это не является спецификой именно драйверов
устройств – каждый открытый в системе файл связан со структурой file в пространстве ядра. Она
создается ядром в ответ на системный вызов open() и передается во все функции работающие с файлом. При
закрытии файла системным вызовом close() код ядра освобождает эту структуру данных. Лежащий на диске
файл представляется в системе структурой inode. При открытии файла, система дополняет информацию о
файле соответствующими логическими структурами.
В источниках ядра, указатель на структуру file обычно называется либо file либо filp (“file pointer”).
Мы будем использовать название filp для этого указателя, для предотвращения двусмысленности.
Таким образом, в нашем коде, название file применяется к структуре, а название filp к указателю на
эту структуру.
Наиболее важные поля структуры file описаны ниже.
loff_t f_pos;
Текущее значение позиции чтения или записи. loff_t представляет собой 64-битное значение. Драйвер
может обратится к этому значению, если потребуется значение текущей позиции в файле, но он, ни в
коем случае не должен изменять это значение. Изменение этого значения должно производиться
только в функциях read() и write(), которые, последним аргументом, получают указатель на этот
элемент.
unsigned int f_flags;
Имеется набор флагов определенных для файла, таких как O_RDONLY, O_NONBLOCK и O_SYNC.
Часто, драйверу может понадобиться проверить флаг запрещения блокировки, в то время остальные
флаги используются значительно реже. В особенности, права на чтение/запись должны проверяться
через f_mode, вместо f_flags. Полный набор флагов определен в заголовочном файле <linux/fcntl.h>.
struct file_operations *f_op;
Операции, связанные с файлом. Ядро назначает этот указатель в коде системного вызова open().
Чтение этого указателя производится ядром при каждом переключении (диспетчеризации) операций.
Значение filp->f_op не сохраняется нигде отдельно для последующих обращений. Это означает, что в
любое время можно изменить операции, связанные с файлом, и они вступят в силу уже при
следующем вызове. Это позволяет реализовывать различное поведение для одного и того же старшего
номера устройства без перегрузки каждого системного вызова.
void *private_data;
Код системного вызова open() устанавливает этот указатель в NULL перед вызовом метода open()
вашего драйвера. Драйвер может использовать это поле по своему собственному усмотрению или не
использовать его вовсе. Можно использовать это поле для указателя на динамически распределенную
область памяти. В этом случае, необходимо освободить эту память в методе release() драйвера, перед
тем, как ядро уничтожит структуру file. Такой указатель очень удобен для передачи информации
между системными вызовами и используется в большинстве наших примеров модулей.
В действительности, структура file имеет еще несколько других полей. Драйвер не заполняет, а
использует структуру file.
25
20. Методы open и release структуры file_operations.
Метод open() может быть использован драйвером для проведения подготовки к дальнейшим
операциям с драйвером. Кроме того, обычно в этом методе увеличивают на 1 счетчик использования
модуля, предотвращая его выгрузку до момента закрытия файла-интерфейса к модулю.
В большинстве драйверов, метод open() выполняет следующие задачи:





Инкрементирование счетчика использования модуля.
Специфичные для устройства проверки
(проверка готовности устройства или выявление какие-нибудь других специфичных проблем).
Инициализация устройства, если оно открывается первый раз.
Определение младшего номера устройства и соотв.корректировка поля f_op, при необходимости.
Распределение и заполнение других структур данных для передачи их через указатель filp->private_data
int mydrv_open( struct inode* node,
struct file* filp )
{
int i = MINOR(node->i_rdev);//0 - если все хорошо
//отрицательное число если ошибка
printk("MINOR ID = %d",i);
return 0;
};
Назначение метода release() противоположно методу open(). Иногда метод носит название
device_close() а не device_release(). Метод должен выполнять следующие задачи:



Освобождение всей памяти занятой под указателем filp->private_data в методе open()
По необходимости, выполняются все завершающие действия при последнем закрытии
устройства
Декрементирование (уменьшение на 1) счетчика использования модуля
int mydrv_release(struct inode *inode, struct file *filp)
{
MOD_DEC_USE_COUNT;
return 0;
}
Обратите внимание, что декрементрирование счетчика использования модуля необходимо, если вы
инкрементируете его в методе open(). Иначе, ядро не сможет выгрузить модуль, счетчик
использования которого не равен нулю.
Не каждый системный вызов close() приводит к вызову метода release().
Метод release() вызывается только в том случае, если ядро действительно освобождает структуру
данных устройства – отсюда и название – release значит “освобождать”. Ядро содержит счетчик
использования структуры file.
Системный вызов close() вызывает метод release() только тогда, когда счетчик использования
структуры file уменьшается до нуля. При этом, структура уничтожается. Взаимодействия между
системным вызовом call() и методом release() гарантируют корректное состояния счетчика
использования модуля.
Завершение приложения автоматически приводит к закрытию связанных с ним файлов. Т.е. для
каждого открытого файла, ядро автоматически вызывает системный close().
26
21. Методы read и write структуры file_operations.
Методы read() и write() выполняют схожие задачи, заключающиеся в копировании данных из, или в
приложение пользователя. Поэтому, их прототипы очень похожи.
Возвращаемое значение {<0 (error) } {=count (ok)} {=0 (nothing)} {>0 и <count (not all)}
ssize_t write(struct file *filp, const char *buff, size_t count, loff_t *offp);
ssize_t read (struct file *filp,
char *buff, size_t count, loff_t *offp);
где
filp - указатель на структуру file,
count - определяет размер передаваемых данных.
buff - указывает на буфер данных чтения/записи.
offp - (указатель на “long offset type”) указывает на смещение от начала данных файла для операций чтения/записи.
Методы возвращают “signed size type.
Главной задачей этих двух методов является передача данных между адресным пространством ядра и
адресным пространством пользовательского процесса.
Код методов read() / write()реализовывает копирование целых сегментов данных в / из адресного пространства
пользователя. Для этого используется функции ядра, которые копируют произвольное количество байт.
Метод read() занимается копированием данных из устройства в адресное пространство пользователя,
используя функцию copy_to_user().
Метод write() копирует данные из пространства пользователя в устройство, используя copy_from_user().
Для каждого системного вызова read() или write() определяется количество передаваемых байт, однако
драйвер может передать меньшее количество данных. Независимо от количества данных передаваемых этими
методами, они должны изменять значение *offp текущей позиции файла, после успешного исполнения
системного вызова. В большинстве случаев аргумент offp представляет собой указатель на filp->f_pos.
Типичная реализация использования аргументов в методе read():
Оба метода, и read(), и write() возвращают отрицательное значение в случае ошибки. Если
возвращаемое значение равно или больше нуля, то это говорит вызывающей программе о количестве
успешно переданных байт. Если ошибка возникла во время передачи, то возвращается количество
переданных байт, либо ошибка, при повторном вызове функции.
27
22. Работа с пользовательским адресным пространством.
Функции copy_to_user, copy_from_user, access_ok, get_user,
put_user и др.
Для копирования данных между адресными пространствами пользователя и ядра используются,
описанные ниже, функции ядра, которые копируют произвольное количество байт.
unsigned long copy_to_user(void *to, const void *from, unsigned long count);
unsigned long copy_from_user(void *to, const void *from, unsigned long count);
Проверка корректности адреса в ядрах до версии 2.2.x реализуется через вызов функции access_ok(),
которая описана в заголовочном файле <asm/uaccess.h>:
int access_ok(int type, const void *addr, unsigned long size);
(возвращает булево значение: 1 – в случае успешной проверки, и 0 – в случае неудачи; если проверка
адреса не удалась, то драйвер обычно возвращает код ошибки –EFAULT). Функция не выполняет
полной проверки для заданного диапазона адресов – определяется только принадлежность заданного
диапазона тому множеству адресов, которые доступны для данного процесса. Таким образом,
функция access_ok() может гарантировать, что заданный диапазон адресов не принадлежит
пространству ядра. Кроме того, необходимо помнить, что в большинстве случаев вы не столкнетесь
с необходимостью прямого вызова access_ok(), т.к. процедуры доступа к памяти, которые будут
описаны позднее, выполняют эту проверку за вас.
После вызова access_ok() драйвер может безопасно выполнять передачу данных из одного адресного
пространства в другое (ядро и пользовательский процесс). В добавлении к функциям
copy_from_user() и copy_to_user(), выполняющим такую передачу данных, программист может
использовать множество функций, которые оптимизированы для наиболее часто используемых
размеров данных – один, два, четыре, или восемь байт (для 64-х разрядной платформы).
Список этих функций, описанных в заголовочном файле <asm/uaccess.h>.
put_user(datum, ptr)
проверяет возможность записи по данному адресу памяти. В случае успешного завершения возвращается 0, и EFAULT в случае ошибки. __put_user() выполняет меньшее количество проверок (не выполняется вызов
access_ok()), но определенные ошибки неправильной адресации могут быть определены данным вызовом. Таким
образом, __put_user() должен быть использован только если регион памяти был уже проверен вызовом
access_ok().
get_user(local, ptr)
для получения одного элемента данных из адресного пространства пользователя. Поведение схоже с макросом
put_user(), но передача данных производится в обратном направлении. Полученное из адресного пространства
пользователя значение сохраняется в локальной переменной local. Возвращаемое макросом значение определяет
успешность выполнения операции.
Если попытка выполнения передачи данных с помощью описанных выше макросов приводит, на
этапе компиляции, к сообщению типа "conversion to non-scalar type requested", то размер
передаваемого аргумента не соответствует размерам обрабатываемым макросом. В этом случае,
необходимо воспользоваться функциями copy_to_user() и copy_from_user().
28
23. Функции ввода-вывода пользовательского режима и их
связь с обработчиками драйвера устройства.
Для выполнения функций в режиме ядра (таких как функций ввода/вывода) программа из
пользовательского режима просто посылает сообщение WinDriver Kernel PlugIn. Это сообщение
вызывает соответствующую функцию в режиме ядра.
пользователь: int fd = open (char const * file_path, int mode[int flags])
ядро: int open(struct inode * inode, struct file * filp)
int close (int fd)
int release (struct inode * inode, struct file * filp)
int read (int fd, char * buff, size)
ssize_t read (struct file * filp, char * buff, int size, loff_t *pos)
int write (int fd, char * buff, size)
ssize_t read (struct file * filp, char * buff, int size, loff_t *pos)
int llseek (int fd, loff_t offset, int mode)
loff_t llseek (struct file * filp, loff_t offset, int mode)
//режимы - от начала файла, от текущего положения указателя, от конца.
int ioctl (int fd, int mode, …)
int ioctl (struct inode * inode, struct file* filp, int cmd, long arg)
Указатели inode и filp представляют собой значения, соответствующие файловому дескриптору fd.
fd был передан пользовательским
передаваемыми в системный вызов.
29
процессом,
и
полностью
совпадают
с
параметрами,
24. Состояние гонки в режиме ядра. Использование семафоров
в режиме ядра.
Предположим, что имеется два процесса A и B, открывшие драйвер mydrv для записи данных. И оба
они пытаются одновременно добавлять данные в устройство. Для обеспечение этой операции
требуется распределение новых квантов. Поэтому, каждый процесс распределяет требуемое
количество памяти и сохраняет эти указатели в квантовом массиве. Результат такой операции
должен вызывать беспокойство. Оба процесса работают с одним и тем же устройством mydrv.
Каждый сохраняет распределенные кванты в одном и том же квантовом массиве. Если сначала
устройство A сохраняет свой указатель, то процесс B перезаписывает этот указатель впоследствии.
Таким образом, память распределенная процессом A будет потеряна.
Такая ситуация носит название race condition (условия состязания, или состязание). Результат
зависит от того, кто начинает первым. Процессы A и B могут быть запущены на разных
процессорах, и их взаимодействие может вызвать такую проблему.
Семафоры представляют собой основной механизм доступа к ресурсам. Простейший вариант –
бинарный семафор=мьютекс (MUTual EXclusion – взаимоисключения). Процессы, использующие
семафоры в режиме мьютесков предотвращают одновременный запуск одного и того же кода, или
доступ к одним и тем же данным.
struct semaphore sem;
Семафоры в Linux определены в заголовочном файле <asm/semaphore.h>. Они описаны в структуре
semaphore, и драйвер обязан использовать эту структуру только через предлагаемый к ней интерфейс.
Перед использованием надо инициализировать семафор, - передачей числового аргумента в функцию
sema_init(). Для приложений, использующих мьютексы (т.е. для приложений, которые должны предотвратить
одновременный доступ к одним и тем же данным), семафоры должны быть проинициализированны
значением 1, которое означает разрешение работы семафора. sema_init (&sem, 1);
Процесс, желающий использовать код защищенный семафором, должен сначала проверить, что
данный код не используется другим процессом. Доступ к семафору: down() и
down_interruptible(&sem).
Эти функции проверяют значение семафора на величину большую нуля. Если это так, то функция
декрементирует значение семафора и завершается. Если значение семафора равно нулю, то функция
засыпает и просыпается через некоторое время, когда некий другой процесс, использующий
семафор, предположительно, освобождает его.
Функция down_interruptible() может быть прервана сигналом, в то время как функция down() не
принимает сигналы к процессу. Сложность использования сигнальных прерываний заключается в
том, что в теле функции down_interruptible() необходимо всегда проверять, была ли функция
прервана. Обычно, функция возвращает 0 в случае успеха, и не ноль в случае неудачного
завершения. Если процесс прерывается, то он не получит семафоров, и, таким образом, нет
необходимости вызывать функцию up(). Поэтому, типичный вызов получения семафора выглядит
следующим образом:
if (down_interruptible (&sem))
return -ERESTARTSYS;
Возвращаемое значение -ERESTARTSYS говорит системе, что операция была прервана сигналом.
Процесс, который получает семафор должен всегда освобождать его впоследствии.
Освобождение семафора: up (&sem);
Этот код увеличивает значение семафора и будит все процессы, которые ожидают доступного
семафора. Во избежании тупиковых ситуаций, получением семафора должны заниматься только
методы драйвера. Внутренние процедуры, должны предполагать, что семафор уже получен. При
30
соблюдении этих условий, доступ к структуре mydrv будет корректным, и не приведет к ситуации
“race conditions”.
31
25. Функция управления ioctl: ее описание в структуре
file_operations и прототип в режиме ядра.
ioctl
- сокращение от Input Output ConTroL.
Любое устройство может иметь свои команды ioctl, которые могут читать (для передачи данных от
процесса ядру), писать (для передачи данных от ядра к процессу), и писать и читать, и ни то ни
другое.
Функция ioctl() драйвера получает аргументы согласно следующему объявлению:
int (*ioctl) (struct inode *inode, struct file *filp, unsigned int cmd, unsigned long arg);
Указатели inode и filp представляют собой значения соответствующие файловому дескриптору fd,
переданному пользовательским процессом, и полностью совпадают с параметрами, передаваемыми в
системный вызов open().
Аргумент cmd передается от пользователя неизменным,
необязательный аргумент arg передается в форме unsigned long, что может соответствовать как
целому значению, так и указателю. Если вызывающая программа не передает третий аргумент, то
значение arg, полученное драйвером имеет неопределенное значение.
-) проверка типов передаваемых аргументов запрещена, поэтому компилятор не сможет
предупредить о возможной ошибке, (до момента исполнения ошибочного кода).
Реализация метода ioctl(), в большинстве случаев строится на основе оператора switch, который
обеспечивает ветвление, согласно значению аргумента cmd. Различные команды имеют различные
числовые значения, которым, обычно, дают символические имена для улучшения читабельности
кода. Символические имена задаются через директиву define препроцессора. Обычно, такие имена
задаются в заголовочных файлах драйвера. Если пользовательская программа, хочет использовать те
же символические имена, то достаточно подключить к ней соответствующий заголовочный файл.
32
26. Генерирование номера команды функции ioctl. Макросы
_IOW, _IO, _IOR.
Номер ioctl содержит комбинацию бит, составляющих старший номер устройства, тип команды и
тип дополнительного параметра. Обычно номер ioctl создается макроопределением (_IO, _IOR,
_IOW или _IOWR, в зависимости от типа) в файле заголовка. Этот заголовочный должен
подключаться директивой #include, к исходным файлам программы, которая использует ioctl для
обмена данными с модулем.
Если предполагается использовать ioctl в собственных модулях, то следует обратиться к файлу
Documentation/ioctl-number.txt с тем, чтобы не "занять" зарегистрированные номера ioctl.
В заголовочном файле определяется использование битовых полей: тип ("магический номер"),
порядковый номер команды, направление передачи и размер аргумента. Файл ioctl-number.txt
содержит список "магических номеров" используемых в ядре. Необходимо выбрать номер
отличающийся от зарезервированных в файле, для избежания перекрытия номеров. Также, в этом
текстовом файле содержится список причин, по которым, данное соглашение о нумерации кодов
команд должно быть использовано.
Конечно же, и приложение, и драйвер должны использовать одно и тоже соглашение о нумерации
кодов команд. Выбрав способ нумерации остается только организовать оператор switch в драйвере.
Новый способ определения номеров команд использует четыре битовых поля, которые имеют
описанное ниже значения. Все новые, используемые здесь, макросимволы определены в
<linux/ioctl.h>.
type
Магическое число. Выбор номера необходимо осуществлять после ознакомления с файлом ioctl-number.txt.
Именно этот выбранный номер, необходимо, в дальнейшем, использовать везде, где он потребуется для вашего
драйвера. Данное поле имеет емкость в восемь бит (_IOC_TYPEBITS).
number Порядковый номер. Емкость – восемь бит (_IOC_NRBITS).
direction
size
Направление передачи данных, если данная команда вызывает пересылку данных. Возможные значения:
_IOC_NONE (нет передачи данных), _IOC_READ, _IOC_WRITE, и _IOC_READ | _IOC_WRITE (данные
передаются в обоих направлениях). Точкой наблюдения передачи данных является приложение. Таким
образом _IOC_READ означает чтение данных из устройства, так, что драйвер должен писать в
пользовательский процесс. Обратите внимание, что это поле представляет собой битовую маску,
поэтому и _IOC_READ и _IOC_WRITE могут быть извлечены используя логическую операцию AND.
Размер передаваемых данных. Емкость этого поля архитектурно-зависима и, сейчас, лежит в диапазоне от 8 до
14 бит. Вы можете определить его значение для вашей архитектуры из макро _IOC_SIZEBITS. Если вы создаете
портируемый драйвер, то значение этого поля не должно превышать 255. Использование данного поля не
является обязательным. Если вы передаете больший размер данных, то вы можете просто игнорировать его.
Скоро, мы увидим пример использования этого поля.
В заголовочном файле <asm/ioctl.h>, который включен в <linux/ioctl.h>, определены следующие
макросы, которые помогут установить номер команды:
_IO(type,nr), _IOR(type,nr,dataitem), _IOW(type,nr,dataitem)
Каждое макро соответсвует одному возможному значению для направления передачи.
Поля type и number передаются как аргументы, а поле size вычисляется как sizeof() над аргументом
dataitem.
Порядковый номер команд не имеет какого-то специфического значения. Он используется только
для различения команд.
33
27. Операции блокируемого ввода-вывода. Использование
очередей.
В реализации отклика на чтение в драйвере может возникнуть проблема, когда, с одной стороны,
данные еще не кончились (т.е. нет конца файла), но, в данный момент, еще не готовы.
В этом случае, необходимо отправить вызывающий процесс в спящее состояние.
Когда процесс должен ждать какого-либо события (ожидание данных, или завершение другого процесса), он
должен быть погружен в спящее состояние. Это означает, что система приостанавливает исполнение
процесса, освобождая процессор для исполнения других процессов. Позже, когда ожидаемое событие
реализуется, процесс будет разбужен и продолжит свою работу.
Существует несколько способов управления переводом процесса в спящее состояние и его
пробуждения. Каждый из этих способов используется в разных контекстах. Однако, все эти способы
используют один и тот же тип данных – очередь ожидания (wait_queue_head_t).
Очередь ожидания – это очередь процессов, ожидающих какого-либо события. Очередь ожидания
описывается и инициализируется следующим образом:
wait_queue_head_t my_queue;
init_waitqueue_head (&my_queue);
Даже если очередь ожидания описана статически (static), т.е. она определена не как автоматическая
переменная или часть динамически распределенной структуры данных, то, по прежнему, возможна
инициализация очереди на этапе компиляции:
DECLARE_WAIT_QUEUE_HEAD (my_queue);
НЕЛЬЗЯ игнорировать процедуру инициализации очереди ожидания. (если не выполнить такую
инициализацию - результат работы с очередью будет непредсказуемым).
После инициализации очереди, ее можно использовать для перевода процесса в спящее состояние:
sleep_on(wait_queue_head_t *queue);
Функция выполняет перевод процесса в спящее состояние. Недостатком использования такого способа
приостановки выполнения процесса является невозможность его прерывания. Т.е. процесс становится
неубиваемым, если ожидаемое им событие не может случиться.
interruptible_sleep_on(wait_queue_head_t *queue);
Прерываемый вариант. Работает также как и sleep_on(), но переведенный в сон процесс может быть прерван
сигналом. Разработчики драйверов использовали такую форму вызова достаточно долгое время до появления,
описанной ниже, альтернативы wait_event_interruptible().
void wait_event(wait_queue_head_t queue, int condition);
int wait_event_interruptible(wait_queue_head_t queue, int condition);
Эти макросы предоставляют наиболее предпочтительный способ перевода процесса в сон до момента
удовлетворения заданного условия. Макросы связывают ожидание события и проверку его возникновения
способом, позволяющим избежать проблемы "race condition" (борьба за ресурсы). Код будет спать до момента
возникновения условия, которое может быть задано любым логическим выражением языка Си, вычисляемым в
true (истина).
Данные макросы расширяются в цикл while, который перевычисляет условие перед каждым повторением тела цикла.
Такое поведение отличается от вызова функции или простого макро, где аргументы вычисляются только в момент
вызова. Последниее макро реализуется как выражение, которое возвращает ноль в случае успеха, и -ERESTARTSYS,
если цикл прерывается сигналом.
Как правило, если одна часть драйвера спит, то существует другая его часть, которая может выполнить
пробуждение, при возникновении определенного события. Драйвер пробуждает спящие куски кода в своем
обработчике прерываний при получении новой порции данных. Функции для пробуждения процесса:
wake_up(wait_queue_head_t *queue);
Данная функция будит все процессы, которые ожидают данной очереди.
wake_up_interruptible(wait_queue_head_t *queue);
Данная функция будит только те процессы, которые находятся в прерываемом сне. Все остальные процессы,
которые заснули по заданной очереди событий, но были переведены в сон с помощью "непрерываемых"
функций, останутся в спящем состоянии.
34
Чтобы быть последовательным, используя interruptible_sleep_on() для перевода в сон, следует
использовать wake_up_interruptible() для пробуждения. Пробуждение процесса может быть
связано не с тем событием, которого он ожидал. Процес может быть разбужет по другим
причинам, например, в результате получения сигнала. Поэтому, спящий код должен
проверять необходимое условие после пробуждения.
35
28. Реализация разграничения доступа на уровне драйвера
устройства.
Ограничение доступа по принципу "Один пользователь за раз"
Следующий способ ограничения доступа, который мы сейчас рассмотрим, позволяют одному пользователю
открывать устройство много раз из разных процессов, но устройство может быть открыто только одним
пользователем за раз. Т.е. до тех пор пока один пользователь использует устройство хотя бы в одном из своих
процессов, любой другой пользователь не сможет получить доступ к этому устройству. Такое устройство
может быть легко протестировано, так как пользователь может читать и писать данные в устройство
одновременно из нескольких процессов. Единственное, нужно понимать, что пользователь несет некоторую
ответственность за целостность данных при организации множественного доступа к устройству. Такое
управление доступом реализуется добавлением специальной проверки в метод open() драйвера. Понятно, что
такая проверка будет выполнена после всех проверок выполняемых файловой системой на основе битов
разрешения доступа к файлу устройства. Именно такая политика доступа используется для устройств tty, но
реализуется не за счет обращения к внешним привелигированным программам.
Такая политика доступа несколько сложнее в реализации, нежели политика single-open устройства. В
этом случае нам необходимо слежение как за счетчиком использования модуля, так и за uid ("user
ID" - идентификатор пользователя) владельца ("owner") устройства. Как уже говорилось раньше,
лучшим местом для хранения такой информации является структура устройства, однако для
минимизации повторяемого кода в наших примерах мы будем использовать для ее хранения
глобальную переменную. Обсуждаемое здесь устройство называется sculluid.
Вызов open() разрешает доступ при первом открытии файла, и запоминает владельца устройства.
Теперь этот пользователь может открыть данное устройство произвольное количество раз, используя
разные процессы для параллельного доступа к этому устройству. В то же самое время, никакой
другой пользователь не сможет открыть данное устройство. Вариант метода open() для данного
случая во многом похож на тот код, который был приведен ранее, для устройства с политикой singleopen, поэтому здесь мы приведем только измененную часть кода, а не весь метод целиком:
spin_lock(&scull_u_lock);
if (scull_u_count &&
(scull_u_owner != current->uid) && /* allow user */
(scull_u_owner != current->euid) && /* allow whoever did su */
!capable(CAP_DAC_OVERRIDE)) { /* still allow root */
spin_unlock(&scull_u_lock);
return -EBUSY; /* -EPERM would confuse the user */
}
if (scull_u_count == 0)
scull_u_owner = current->uid; /* grab it */
scull_u_count++;
spin_unlock(&scull_u_lock);
В качестве кода возврата при невыполнении приведенного условия мы используем значение EBUSY, а не -EPERM. Это подчеркивает тот факт, что пользователь имеет право доступа к
устройству, но на данный момент оно занято. "Permition denied" используется, обычно, как ошибка
при проверке прав доступа к /dev-файлу устройства, в то время как "Device busy" корректно
отображает тот факт, что устройство занято другим пользователем.
Приведенный код проверяет не только идентификатор пользователя (uid), но и эффективный
идентификатор пользователя (euid), что позволяет открывать устройство процессам использующим
требуемую замену идентификатора пользователя. Проверка возможности перекрытия прав доступа к
файлам и каталогам (CAP_DAC_OVERRIDE), дополнительно позволяет ослабить жесткую политику
доступа к устройству.
Код метода close() не показан для этого случая, потому что все, что он делает сводится к
декрементированию счетчика открытия файла.
36
29. Отладка модулей ядра с помощью функции printk.
Управление кольцевым буфером сообщений ядра.
Наиболее общей техникой отладки является печать. В приложениях пользовательского уровня, такая техника
реализуется через вызов функции printf() в определенных местах программы. При отладке кода ядра
используется аналогичная функция printk(), доступная в коде ядра.
Одно из отличий функций printk() и printf() заключается в том, что функция printk() позволяет вам
классифицировать сообщения согласно, назначенным им, приоритетам. Приоритет можно задать через
макроопределение. Например, KERN_INFO один из возможных приоритетов сообщения. Макроопределение
приоритета расширяется до строки, которая соединяется со строкой текста сообщения во время компиляции.
По этой причине, между указанием приоритета и строкой сообщения не ставится запятая. Приведем два
примера вызова функции printk() с отладочным и критическим сообщением.
printk(KERN_DEBUG "Here I am: %s:%i\n", __FILE__, __LINE_&_);
printk(KERN_CRIT "I'm trashed; giving up on %p\n", ptr);
В заголовочном файле <linux/kernel.h> определены восемь возможных уровней приоритета (loglevel).
KERN_EMERG
Используется для “непредвиденных” сообщений, особенно для тех, которые предшествуют краху.
KERN_ALERT
Ситуация, требующая немедленного вмешательства.
KERN_CRIT
Критическая ситуация. Часто связанная с серьезными неисправностями оборудования или программным
проблемам.
KERN_ERR
Используется для отчета об условиях возникшей ошибки. Драйвера устройств часто используют KERN_ERR для
сообщениях о проблемах, связанных с оборудованием.
KERN_WARNING
Сообщение о проблемных ситуациях, которые сами по себе не создают проблем для системы.
KERN_NOTICE
Ситуация не проблемная, но заслуживает внимания. Часто используется в сообщениях системы безопасности.
KERN_INFO
Информационное сообщение. Используется в сообщениях о найденном оборудовании при загрузке драйверов.
KERN_DEBUG
Используется для отладочных сообщений.
Каждая строка (при подстановке макроопределения) представляет целое число в угловых скобках. Диапазон
изменения целого числа от 0 до 7. Причем, чем меньше значение тем выше приоритет. В зависимости от
уровня приоритета, ядро может выводить сообщения на текущую консоль (текстовый терминал), на принтер
или в файл.
Функция printk() пишет сообщение в круговой буфер длиной LOG_BUF_LEN байт, определенный в
kernel/printk.c. Затем, просыпается один из процессов ожидающий сообщение. Т.е. это либо процесс, который
спит в системном вызове syslog, либо процесс, который читает /proc/kmsg. Эти два интерфейса к log-машине
практически эквивалентны, но чтение из /proc/kmsg “съедает” данные из log-буфера, в то время как
системный вызов syslog может опционально вернуть данные в буфер пока они нужны другим процессам. В
общем, чтение файла из /proc проще, поэтому оно используется в klogd по умолчанию.
После остановки klogd видно, что файл в /proc организован в виде очереди FIFO. Причем, читающий процесс
блокируется для ожидания следующих данных. Таким образом, вы не можете прочитать сообщения этим
способом, если klogd уже занят чтением буфера, потому что, иначе, это приведет к проблеме конкуренции
доступа к данным. Если круговой буфер переполняется, то функция printk начинает затирать наиболее старые
данные буфера. Таким образом, старые данные теряются. Эта проблема не существенна, по сравнению с
преимуществами организации кругового буфера. Например, круговой буфер позволяет системе работать
даже, если нет процесса, обрабатывающего этот буфер. Другом преимуществом Linux подхода к реализации
этого механизма заключается в том, что функция printk() может быть вызвана откуда угодно, даже из
обработчика прерываний, без ограничений на количество выводимых данных. Единственным недостатком
является возможность потери данных.
Если процесс klogd запущен, то он извлекает из буфера сообщения ядра и диспетчеризует их согласно
настройкам в файле /etc/syslog.conf. syslogd различает события согласно их принадлежности и приоритету.
Если klogd не запущен, то данные остаются в круговом буфере, либо пока их кто-нибудь не прочитает, либо
пока буфер не переполнится.
37
30. Выделение и освобождение памяти в режиме ядра.
Особенности и отличие от пользовательского режима.
Управление динамической памятью в ядре не имеет принципиальных отличий. Программа может
получить память используя функцию kmalloc() и освободить ее, с помощью kfree().Эти функции
очень похожи на malloc() и free(), за тем исключением, что в функцию kmalloc() передается
дополнительный аргумент – приоритет. Обычно приоритет принимает значения GFP_KERNEL или
GFP_USER. GFP представляет собой акроним от “get free page” - взять свободную страницу.
void *kmalloc(unsigned int size, int priority);
void kfree(void *obj);
Аналоги функций malloc() и free() в пространстве ядра. Используют значение GFP_KERNEL для указания приоритета.
Функция kmalloc() и связанная с ней система управления памятью представляет собой мощный и
достаточно простой инструмент распределения необходимого пространства памяти. Вызов kmalloc()
высокопроизводителен если не оперирует с блоками. Функция не очищает распределенную память в полученном куске остается предыдущий контент. Распределенный регион непрерывен в
физической памяти.
Наиболее часто используемым флагом является флаг GFP_KERNEL, означающий, что
распределение выполняется в интересах процесса запущенного в пространстве ядра. Использование
GFP_KERNEL означает, что kmalloc() может перевести текущий процесс в спящее состояние на
время ожидания требуемой страницы памяти при работе с нижней памятью (low memory situations).
Во время спячки процесса, ядро выполняет соответствующие действия для получения страницы
памяти, т.е. либо сбрасывает дисковые буфера, либо реализует своппинг памяти из
пользовательского процесса. Все флаги определены в заголовочном файле <linux/mm.h>:
индивидуальные флаги префексированы двойным знаком подчеркивания, как __GFP_DMA; флаги,
которые могут использоваться в сочетаниях не имеют такого префикса и иногда называются
приоритетами распределения.
GFP_KERNEL
Обычное распределение памяти в ядре. Может привести процесс в спящее состояние.
GFP_BUFFER
Используется для управления буфером кэша. Этот приоритет позволяет перевести процесс в сон. Он отличается
от GFP_KERNEL тем, что освобождение памяти будет производится сбросом на диск грязных страниц (dirty
pages). Назначение флага заключается в избежании ситуации дэдлока (deadlock - взаимной блокировки) в случае,
если подсистеме ввода/вывода самой потребуется память.
GFP_USER
Используется для распределения памяти для пользовательского процесса. Может перевести процесс в спящее
состояние. Имеет низкий приоритет.
38
31. Файловая система /proc. Создание файлов для чтения и
файлов чтения/записи.
Файловая система /proc представляет собой специальную, программно-реализованную файловую
систему, которая связана с функциями ядра, которые генерируют содержание файла на лету, во
время чтения файла. (например, таким как /proc/modules, который возвращает список загруженных, в
данный момент, модулей).
Драйвера устройств также используют /proc для передачи информации в пространство пользователя.
Файловая система /proc является динамической системой, и ваш модуль может добавлять и удалять
файловый элемент из этой системы во время своей работы. Все модули, которые работают с
файловой системой /proc должны включать заголовочный файл <linux/proc_fs.h>, где определены
соответствующие функции.
Для создания файла в файловой системе /proc, который будет доступен только для чтения,
драйвер должен реализовать функцию, которая будет наполнять файл содержанием во время его
чтения. Когда некоторый процесс читает файл (используя системный вызов read()), запрос достигает
вашего модуля через один из двух возможных интерфейсов, в зависимости от способа регистрации
файла. Рассмотрим интерфейсы чтения.
В обоих случаях, ядро распределяет страницу памяти, размером PAGE_SIZE байт, куда драйвер будет
писать данные, передаваемые в пользовательское пространство. Рекомендуемым интерфейсом
является интерфейс read_proc, но, также, существует старый интерфейс, имеющий название
get_info.
int (*read_proc)(char *page, char **start, off_t offset, int count, int *eof, void *data);
Параметр page представляет собой указатель на буфер, куда вы пишите свои данные. Параметр start
используется функцией для указания размещения данных на странице. Параметры offset и count
(размер буфера) имеют то же значение, что и в реализации метода read(). Аргумент eof указывает на
целое число, которое должно быть установлено драйвером, для сообщения о том, что данные
закончились. Параметр data представляет собой драйверо-зависимый указатель, который может
быть использован для внутренних целей разработчика драйвера.
int (*get_info)(char *page, char **start, off_t offset, int count);
get_info представляет собой старый интерфейс, используемый при чтении из файла файловой
системы /proc. Все аргументы имеют те же самые значения, что и для интерфейса read_proc.
Недостатком этого интерфейса является отсутствие указателя-индикатора eof (“end-of-file”) и
указателя data.
Обе функции должны возвращать количество байт данных действительно размещенных в буфере
страницы. Другими возвращающими параметрами являются параметры *eof и *start. Параметр eof
это простой флаг окончания данных. Более сложным является параметр start.
Главной проблемой обычной реализации пользовательского дополнения в файловую систему /proc, является
использование одной страницы памяти для передачи данных. Это ограничивает размер файла четырьмя
килобайтами (зависит от аппаратной платформы). Аргумент start нужен для поддержки больших файлов
данных, и может быть проигнорирован.
Если функция proc_read() не устанавливает указатель *start (т.е. start равен NULL), то ядро предполагает, что параметр
offset был проигнорирован, и что страница данных целиком занята файлом, который вы хотите возвратить в
пространство пользователя. Если нужно передать файл большего размера, состоящий из кусков, то можно установить
*start равным page, чтобы вызывающая программа знала, что новые данные расположены начиная с начала буфера.
Конечно, надо пропустить первые offset байт данных, которые уже были возвращены в предыдущем вызове.
После реализации функции read_proc() необходимо зарегистрировать элемент в иерархии файловой
системы /proc. Возможны два способа такой регистрации, в зависимости от версии ядра, в котором
вы собираетесь использовать драйвер. Простейший способ заключается в вызове функции
create_proc_read_entry(). Приведем пример регистрации элемента /proc/scullmem для драйвера scull.
create_proc_read_entry("scullmem", -имя файла в /proc
0
/* default mode */, -права доступа к файлу (0 всеобщий на чтение),
39
NULL /* parent dir */,
scull_read_procmem,
NULL /* client data */);
-указатель на родительский каталог для файла
-указатель на функцию read_proc()
- указатель на данные, которые будут переданы
При выгрузке модуля, его элементы в /proc должны быть удалены. Функция remove_proc_entry()
используется для удаления элементов созданных вызовом create_proc_read_entry().
remove_proc_entry("scullmem", NULL /* parent dir */);
Для создания файла в файловой системе /proc, который будет доступен для записи/чтения надо
организовать передачу данных модулю ядра посредством файловой системы /proc.
Поскольку файловая система /proc была написана, главным образом, для того чтобы получать
данные от ядра, она не предусматривает специальных средств для записи данных в файлы.
Структура proc_dir_entry не содержит указатель на функцию-обработчик записи. Поэтому, вместо
того, чтобы писать в /proc напрямую, мы вынуждены будем использовать стандартный для
файловой системы механизм.
Linux предусматривает возможность регистрации файловой системы. Так как каждая файловая
система должна иметь собственные функции, для обработки inode и выполнять файловые операции,
то имеется специальная структура, которая хранит указатели на все необходимые функцииобработчики — struct inode_operations, которая включает указатель на struct file_operations.
Файловая система /proc, всякий раз, когда мы регистрируем новый файл, позволяет указать — какая
struct inode_operations будет использоваться для доступа к нему. В свою очередь, в этой
структуре имеется указатель struct file_operations, а в ней уже находятся указатели на наши
функции-обработчики.
Обратите внимание: стандартные понятия "чтение" и "запись", в ядре имеют противоположный
смысл. Функции чтения используются для записи в файл, в то время как функции записи
используются для чтения из файла. Причина в том, что понятия "чтение" и "запись"
рассматриваются здесь с точки зрения пользователя: если процесс читает что-то из ядра — ядро
должно записать эти данные, если процесс пишет — ядро должно прочитать то, что записано.
Еще один интересный момент — функция module_permission. Она вызывается всякий раз, когда
процесс пытается обратиться к файлу в файловой системе /proc, и принимает решение — разрешить
доступ к файлу или нет. На сегодняшний день, решение принимается только на основе выполняемой
операции и UID процесса, но в принципе возможна и иная организация принятия решения,
например, разрешать ли одновременный доступ к файлу нескольким процессам и пр..
Причина, по которой для копирования данных используются функции put_user и get_user, состоит
в том, что процессы в Linux (по крайней мере в архитектуре Intel) исполняются в изолированных
адресных пространствах, не пересекающихся с адресным пространством ядра. Это означает, что
указатель, не содержит уникальный адрес физической памяти — он хранит логический адрес в
адресном пространстве процесса.
Единственное адресное пространство, доступное процессу — это его собственное адресное
пространство. Практически любой модуль ядра, должен иметь возможность обмена информацией с
пользовательскими процессами. Однако, когда модуль ядра получает указатель на некий буфер, то
адрес этого буфера находится в адресном пространстве процесса. Макрокоманды put_user и
get_user позволяют обращаться к памяти процесса по указанному им адресу.
/* Для передачи данных из пространства ядра в пространство пользователя
* следует использовать put_user. В обратном направлении — get_user.
* Эта функция принимает решение о праве на выполнение операций с файлом
* 0 — разрешено, ненулевое значение — запрещено.
* Операции с файлом: 2-Запись(user ->модуль ядра) и 4-Чтение (модуль ядра -> user) 0-Х
* Эта функция проверяет права доступа к файлу */
static int module_permission(struct inode *inode, int op, struct nameidata *foo)
{ /* Позволим любому читать файл, но писать — только root-у (uid 0) */
if (op == 4 || (op == 2 && current->euid == 0))
return 0;
40
return -EACCES;
/* Если что-то иное — запретить доступ */ }
static struct inode_operations Inode_Ops_4_Our_Proc_File =
{ .permission = module_permission,
/* проверка прав доступа */ };
41
32. Таймеры ядра. Инициализация таймера, его использование и
удаление.
Таймеры используются для планирования исполнения функции (обработчика таймера) в заданное
время. Таким образом, их работа отличается тем, что можно определить время запуска обработчика
таймера. С другой стороны, таймеры ядра похожи на очереди задач в том, что функция
зарегистрированная в таймере ядра выполняется только один раз.
Иногда возникает необходимость выполнения операций отсоединенных от какого-либо контекста
процесса. Например, выключение мотора флоппи-дисковода, или завершение других
долгозавершаемых операций. В этом случае, задержка на ожидание окончания завершения таких
операций не должна сказываться на работе приложений. Использование, для этих целей, очереди
задач было было бы расточительно, потому что задача, запускаемая из очереди, должна
перерегистрировать себя в очереди до тех пор, пока операция не будет завершена.
Таймер много проще в использовании. Вы регистрируете свою функцию единожды, и ядро вызывает
ее при истечении счета таймера. Данная функциональность часто используется в самом ядре, но,
иногда, требуется и в драйверах, как например, в случае управления мотором флоппи-дисковода.
Таймеры ядра организуются в двунаправленный связанный список. Это означает, что вы можете
создавать столько таймеров, сколько захотите. Таймер характеризуется значением таймаута (в
джиффисах), и функцией, вызываемой при истечении таймера. Обработчик таймера принимает
аргумент, который сохраняется в структуре данных, вместе с указателем на сам обработчик.
Структура таймера определена в заголовочном файле <linux/timer.h> и выглядит следующим
образом:
struct timer_list {
struct timer_list *next;
struct timer_list *prev;
unsigned long expires;
unsigned long data;
void (*function)(unsigned
volatile int running;
};
/* never touch this*/
/* never touch this*/
/* значение таймаута в джиффисах*/
/* аргумент для обработчика*/
long); /* handler of the timeout (обработчик)*/
/* added in 2.4; don't */
Значение таймера задается в джиффисах. Таким образом, timer->function() будет запущена тогда,
когда значение джиффисов будет больше или равно заданному значению timer->expires. Таймаут
задается в абсолютных значениях джиффисов, обычно получаемых прибавлением желаемой
задержки к текущему значению.
Как только структура timer_list инициализируется, add_timer() вставляет ее в сортированный список, который
проверяется около 100 раз в секунду. Увеличение частоты системного таймера не увеличит частоту обработки
этого списка. Для работы с таймером используются следующие функции:
void init_timer(struct timer_list *timer);
Эта inline-функция используется для инициализации структуры таймера. В текущей реализации, она обнуляет
указатели prev и next (и флаг running на системах SMP). Для совместимости со следующими версиями ядра,
программистам крайне рекомендуется использовать эту функцию для инициализации таймера, и не
использовать явную инициализацию указателей этой структуры.
void add_timer(struct timer_list *timer);
Данная функция вставляет таймер в глобальный список активных таймеров.
int mod_timer(struct timer_list *timer, unsigned long expires);
Для изменения времени истечения таймера, который уже находится в глобальном списке активных таймеров.
После ее вызова, для данного таймера будет использовано новое значение таймаута.
int del_timer(struct timer_list *timer);
Функция del_timer() удаляет таймер из списка активных таймеров (до его истечения). Если таймер был
действительно установлен в очередь, то del_timer() возвратит 1, иначе - 0. (при истечении таймера, он будет
удален из списка автоматически)
42
При использовании таймера следует иметь ввиду, что таймер истекает в точно назначенное время, даже если процессор
выполняет, в этот момент, системный вызов. Раньше мы предполагали, что когда процесс выполняется в пространстве
ядра, то он не планируется вообще, т.е. не может быть прерван. Однако, таймеры являются специальным случаем, и
выполняют все свои задачи независимо от текущего процесса. И хотя система может оказаться жестко заблокированной
системным вызовом находящимся в ожидании, но и очередь таймера, и таймеры ядра будут продолжать обрабатываться.
43
33. Использование средств tasklet. Особенности и отличие от
таймеров ядра.
Практически перед выпуском ядра 2.4, разработчики добавили новый механизм для запуска отложенных задач ядра.
Этот механизм, называемый такслетами, представляет сейчас наиболее предпочтительный способ исполнения нижних
половинок (botton-half). Кроме того, в данный момент, нижние половинки, сами по себе, реализованы через такслеты.
Во многом, тасклеты похожи на очереди задач. Они предоставляют способ безопасного запуска отложенных
задач, и всегда исполняются в режиме прерывания. Как и очереди задач, такслеты могут быть запущены
только единожды, даже при многократной установке в планировщик, но тасклеты могут быть запущены
параллельно с другими тасклетами на системах SMP. Также, на системах SMP существует гарантия, что
тасклеты будут запущены на том CPU, который впервые планировал их, что обеспечивает лучшее
использование кэша и более высокую производительность.
Каждый тасклет имеет связанную с ним функцию, которыя вызывается тогда, когда тасклет должен быть
выполнен. Жизнь некоторых разработчиков ядра была бы проще, если бы эта функция принимала бы свой
единственный аргумент как экземпляр типа unsigned long. С другой стороны, в некоторых случаях,
предпочтительнее использовать в качестве аргумента указатель. Как бы там не было, но проблемой это не
является, так как преобразование аргумента long в тип указателя является совершенно безопасным и может
быть использовано на всех поддерживаемых платформах. Мы остановимся на этом вопросе в главе 13 "mmap
и DMA". Обсуждаемая тасклет-функция не возвращает значения (т.е. возвращает void). Поддержка тасклетов
предоставляется заголовочным файлом <linux/interrupt.h>. Тасклет может быть объявлен с помощью одного
из следующих макросов:
DECLARE_TASKLET(name, function, data);
Объявляет тасклет с именем name. При исполнении тасклета вызывается функция function, в которую передается
значение data типа unsigned long.
DECLARE_TASKLET_DISABLED(name, function, data);
Так же как и в предыдущем случае объявляет тасклет, но с начальным состоянием "disabled" (запрещено),
означающем, что он может учавствовать в планировке задач, но не может быть исполнен до тех пор, пока не
будет разрешен в последствии.
Пример драйвера jiq, при компиляции для ядра 2.4 реализует поддержку файла /proc/jiqtasklet, который
работает также, как и другие файлы, обрабатываемые драйвером jiq, но использует тасклеты. Мы не
эмулируем тасклеты для старых версий ядра в нашем заголовочном файле sysdep.h. Модуль объявляет свой
тасклет следующим образом:
void jiq_print_tasklet (unsigned long);
DECLARE_TASKLET (jiq_tasklet, jiq_print_tasklet, (unsigned long) &jiq_data);
Когда драйвер хочет диспетчеризовать тасклет для запуска, он вызывает функцию tasklet_schedule():
tasklet_schedule(&jiq_tasklet);
Если тасклет разрешен, то как только он диспетчеризуется, он будет запущен сразу же как только это будет
возможно из соображений безопасности. Тасклеты могут перепланировать сами себя много раз, также как и
очереди задач. Тасклет не должен беспокоиться о запуске своей копии на многопроцессорной системе, так как
в ядре предприняты шаги к тому, чтобы гарантировать запуск каждого из тасклетов только в одном месте.
Также, необходимо понимать, что если ваш драйвер реализует множество тасклетов, то он должен быть готов
к тому, что более чем один из них может исполняться одновременно. В этом случае, для защиты критических
секций кода должен быть использован spinlock. Так как семафоры могут уйти в состояние сна, то они не
могут быть использованы в тасклетах, исполняемых в режиме прерывания.
Типичное содержимое файла /proc/jiqtasklet:
time delta interrupt pid cpu command
45472377
0
1
8904
0
head
Тасклет всегда запускается на одном и том же CPU, даже если вывод файла производится на двупроцессорной системе.
void tasklet_disable(struct tasklet_struct *t);
Функция запрещает данный тасклет. Тасклет может обрабатываться планировщиком по tasklet_schedule(), но его
исполнение будет отложено до тех пор, пока тасклет не будет разрешен.
void tasklet_enable(struct tasklet_struct *t);
Разрешает тасклет, который был предварительно запрещен. При этом, если тасклет был уже спланирован, то он
будет скоро запущен, но не прямо из tasklet_enable().
void tasklet_kill(struct tasklet_struct *t);
Эта функция может быть использована для тасклетов, которые перепланировали сами себя неконтролируемое
число раз. Функция tasklet_kill() удалит тасклеты из любой очереди, в которой он содержится. Для того, чтобы
избежать гонки (race condition) в запущенных, и планирующих себя тасклетах, эта функция ожидает завершения
44
работы тасклета, и только потом извлекает его из очереди. Таким образом, вы можете быть уверены, что
тасклеты не будут прерваны во время исполнения. Однако, если тасклет не является запущенным, и не
перепланирует сам себя, то функция tasklet_kill() может повиснуть. tasklet_kill() не может быть вызвана во время
прерывания.
45
34. Механизм очередей Workqueue.
Необходимый заголовочный файл
#include <linux/workqueue.h> /* очереди задач */
#include <linux/sched.h>
#include <linux/init.h>
/* Взаимодействие с планировщиком */
/* макросы __init и __exit */
Некоторые функции, относящиеся к work_queue доступны только если модуль лицензирован под
GPL - необходимо включить MODULE_LICENSE("GPL");
Очередь задач, создается для того, чтобы поместить в очередь таймера (workqueue.h)
static struct workqueue_struct *my_workqueue;
Функция инициализации
int __init init_module()
{
…;
Создать очередь задач с нашей задачей и поместить ее в очередь таймера
my_workqueue = create_workqueue(MY_WORK_QUEUE_NAME);
queue_delayed_work(my_workqueue, &Task, 100);
…
}
Завершение работы
void __exit cleanup_module()
{
…
flush_workqueue(my_workqueue); /* ждать пока отработает таймер */
destroy_workqueue(my_workqueue);
…
}
46
35. Обработка прерываний. Установка и удаление обработчика
прерываний.
В Linux аппаратные прерывания называются IRQ (сокращенно от Interrupt ReQuests — Запросы на
Прерывание). Имеется два типа IRQ: "короткие" и "длинные". "Короткие" IRQ занимают очень короткий
период времени, в течение которого работа операционной системы будет заблокирована, а так же будет
невозможна обработка других прерываний. "Длинные" IRQ могут занять довольно продолжительное время, в
течение которого могут обрабатываться и другие прерывания (но не прерывания из того же самого
устройства). Поэтому, иногда бывает благоразумным разбить выполнение работы на исполняемую внутри
обработчика прерываний (т.е. подтверждение прерывания, изменение состояния и пр.) и работу, которая
может быть отложена на некоторое время (например постобработка данных, активизация процессов,
ожидающих эти данные и т.п.). Если это возможно, лучше объявлять обработчики прерывания "длинными".
Тебуется заголовочный файл #include <linux/interrupt.h> и <linux/sched.h> :
IRQ нумеруются и каждое аппаратное устройство в системе связывается с номером IRQ . Связывание
номера IRQ с устройством позволяет центральному процессору выяснить, какое устройство
сгенерировало каждое прерывание, и, следовательно, позволяет ему выполнить переход к нужному
обработчику прерывания.
int request_irq(unsigned int irq,
void (*handler)(int, void *, struct pt_regs *),
unsigned long flags,
const char *dev_name,
void *dev_id);
Устанавливается обработчик прерывания вызовом функции request_irq(). Ей передаются номер IRQ,
имя функции-обработчика, флаги, имя для /proc/interrupts и дополнительный параметр для
обработчика прерываний. Флаги могут включать SA_SHIRQ, чтобы указать, что прерывание может
обслуживаться несколькими обработчиками (обычно, по той простой причине, что на одном IRQ может
"сидеть" несколько устройств) и SA_INTERRUPT, чтобы указать, что это "короткое" прерывание. Эта
функция установит обработчик только в том случае, если на заданном IRQ еще нет обработчика прерывания,
или если существующий обработчик зарегистрировал совместную обработку прерывания флагом SA_SHIRQ.
Обработка прерываний .
// Захват прерывания
if (request_irq(IRQ_NUM, irq_handler, 0, DEV_NAME, NULL)){
printk("Kernel: IRQ allocation failed\n");
release_mem_region(MEM_START, MEM_COUNT);
release_region(PORT_START, PORT_COUNT);
return -EBUSY;
}
Удаляется обработчик прерывания вызовом функции free_irq()
void free_irq(unsigned int irq, void *dev_id);
47
36. Назначение линии IRQ обработчику прерывания.
Устанавливается обработчик прерывания вызовом функции request_irq. Ей передаются номер IRQ, имя
функции-обработчика, флаги, имя для /proc/interrupts и дополнительный параметр для обработчика
прерываний. Флаги могут включать SA_SHIRQ, чтобы указать, что прерывание может обслуживаться
несколькими обработчиками (обычно, по той простой причине, что на одном IRQ может "сидеть" несколько
устройств) и SA_INTERRUPT, чтобы указать, что это "короткое" прерывание. Эта функция установит
обработчик только в том случае, если на заданном IRQ еще нет обработчика прерывания, или если
существующий обработчик зарегистрировал совместную обработку прерывания флагом SA_SHIRQ.
48
37. Механизм обработки прерываний в ОС LINUX.
1) прерывание , - выбор нужного обработчика (можно обрабатывать сразу или позже)
2) назначение приоритета
3) выбор обработчика
Применяемый в ОС механизм обработки внутренних и внешних прерываний в основном зависит от
того, какая аппаратная поддержка обработки прерываний обеспечивается конкретной аппаратной
платформой. Но существует соглашение о базовых механизмах прерываний.
Суть механизма состоит в том, что каждому возможному прерыванию процессора (будь то
внутреннее или внешнее прерывание) соответствует некоторый фиксированный адрес физической
оперативной памяти. В тот момент, когда процессору разрешается прерваться по причине наличия
внутренней или внешней заявки на прерывание, происходит аппаратная передача управления на
ячейку физической оперативной памяти с соответствующим адресом - обычно адрес этой ячейки
называется "вектором прерывания" (как правило, заявки на внутреннее прерывание, т.е. заявки,
поступающие непосредственно от процессора, удовлетворяются немедленно).
Дело ОС - разместить в соответствующих ячейках оперативной памяти программный код,
обеспечивающий начальную обработку прерывания и инициирующий полную обработку.
В векторе прерывания, соответствующем внешнему прерыванию, т.е. прерыванию от некоторого
внешнего устройства, содержатся команды, устанавливающие уровень выполнения процессора
(уровень выполнения определяет, на какие внешние прерывания процессор должен реагировать
незамедлительно) и осуществляющие переход на программу полной обработки прерывания в
соответствующем драйвере устройства. Для внутреннего прерывания (например, прерывания по
инициативе программы пользователя при отсутствии в основной памяти нужной страницы
виртуальной памяти, при возникновении исключительной ситуации в программе пользователя и т.д.)
или прерывания от таймера в векторе прерывания содержится переход на соответствующую
программу ядра ОС
49
38. Верхняя и нижняя половины обработчика прерываний.
Иногда бывает благоразумным разбить выполнение работы на исполняемую внутри обработчика
прерываний (т.е. подтверждение прерывания, изменение состояния и пр.) и работу, которая может
быть отложена на некоторое время (например постобработка данных, активизация процессов,
ожидающих эти данные и т.п.).
Bottom halves - это самый старый механизм отложенного исполнения задач ядра и был доступен еще
в Linux 1.x.. В Linux 2.0 появился новый механизм - "очереди задач" ('task queues').
Bottom halves упорядочиваются блокировкой (spinlock) global_bh_lock, т.е. только один bottom half
может быть запущен на любом CPU за раз. Однако, если при попытке запустить обработчик,
global_bh_lock оказывается недоступна, то bottom half планируется на исполнение планировщиком
- таким образом обработка может быть продолжена вместо того, чтобы стоять в цикле ожидания на
global_bh_lock.
Всего может быть зарегистрировано только 32 bottom halves. Функции для работы с ними
экспортируются в модули.
Bottom halves, по сути своей, являются глобальными "блокированными" тасклетами (tasklets), так,
вопрос: "Когда исполняются обработчики bottom half ?", в действительности должен звучать как:
"Когда исполняются тасклеты?". На этот вопрос имеется два ответа:
а) при каждом вызове schedule()
б) каждый раз, при исполнении кода возврата из прерываний/системных вызовов (interrupt/syscall
return path) в entry.S.
50
Download