Отличия между модулями ядра и приложениями

Предыдущая  Содержание  Следующая V*D*V

Прежде чем идти дальше, следует подчеркнуть разнообразные отличия между модулями ядра и приложениями.

 

Хотя большинство малых и средних приложений выполняют от начала до конца одну задачу, каждый модуль ядра просто регистрирует себя для того, чтобы обслуживать в будущем запросы, и его функция инициализации немедленно прекращается. Иными словами, задача функции инициализации модуля заключается в подготовке функций модуля для последующего вызова; это как будто модуль сказал: "Вот я и вот что я могу делать". Функция выхода модуля (hello_exit в примере) вызывается только непосредственно перед выгрузкой модуля. Она сообщает ядру: "Меня больше нет; не просите меня сделать что-нибудь ещё". Такой подход к программированию подобен программируемой обработке событий, но пока не все приложения управляются событиями, как модули ядра. Другое сильное отличие между событийно-управляемым приложением и кодом ядра в функции выхода: в то время как приложение, которое прекращает работу, может быть ленивым при высвобождении ресурсов или избегать очистки всего, функция выхода модуля должна тщательно отменить все изменения, сделанные функцией инициализации, или эти куски останутся вокруг до перезагрузки системы.

 

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

 

Как программист, вы знаете, что приложение может вызывать функции, которые не определены: стадия линковки разрешает (определяет) внешние ссылки, используя соответствующие библиотечные функции. printf является одной из таких вызываемых функций и определена в libc. Модуль, с другой стороны, связан только с ядром и может вызывать только те функции, которые экспортированы ядром, нет библиотек для установления связи. Например, функция printk, использованная ранее в hello.c, является версией printf, определённой в ядре и экспортированной для модулей. Она ведёт себя аналогично оригинальной функции с небольшими отличиями, главным из которых является отсутствие поддержки плавающей точки. Рисунок 2-1 показывает, как используются в модуле вызовы функций и указатели на функции, чтобы добавить ядру новую функциональность.

 

Рисунок 2-1. Связи модуля в ядре

Рисунок 2-1. Связи модуля в ядре

 

Файлы исходников никогда не должны подключать обычные заголовочные файлы, потому что нет библиотеки, связанной с модулями, <stdarg.h> и очень специальные ситуации будут только исключениями. Только функции, которые фактически являются частью самого ядра, могут быть использованы в модулях ядра. Всё относящееся к ядру объявлено в заголовках, находящихся в дереве исходных текстов ядра, которое вы установили и настроили; наиболее часто используемые заголовки живут в include/linux и include/asm, но есть и другие подкаталоги в папке include для содержания материалов, связанных со специфичными подсистемами ядра.

 

Роль каждого отдельного заголовка ядра объясняется в книге по мере того, как каждый из них становится необходим.

 

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

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

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

 

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

 

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

 

Мы обычно упоминаем о режимах исполнения, как о пространстве ядра и пространстве пользователя. Эти термины включают в себя не только различные уровни привилегий, присущие двум режимам, но также тот факт, что каждый режим может также иметь своё собственное отображение памяти, своё собственное адресное пространство.

 

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

 

Ролью модуля является расширение функциональности ядра; код модулей выполняется в пространстве ядра. Обычно драйвер выполняет обе задачи, изложенные ранее: некоторые функции в модуле выполняются как часть системных вызовов, а некоторые из них отвечают за обработку прерываний.

Конкуренция в ядре

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

 

Есть несколько источников конкуренции в программировании ядра. Естественно, система Linux запускает множество процессов и более чем один из них может пытаться использовать драйвер в то же самое время. Большинство устройств способны вызвать прерывание процессора; обработчики прерываний запускаются асинхронно и могут быть вызваны в то же время, когда ваш драйвер пытается сделать что-то другое. Несколько программных абстракций (например, таймеры ядра, описанные в Главе 7) тоже работают асинхронно. И конечно, Linux может работать на симметричных многопроцессорных (SMP) системах, в результате чего ваш драйвер может выполняться одновременно на более чем одном процессоре. Наконец, в версии 2.6 код ядра был сделан вытесняемым; это изменение вызывает даже на однопроцессорных системах многие из тех же вопросов конкуренции, что и на многопроцессорных системах.

 

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

 

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

Текущий процесс

Хотя модули ядра не выполняются последовательно, как приложения, большинство действий, выполняемых ядром, делаются от имени определённого процесса. Код ядра может обратиться к текущему процессу через глобальный объект current, определённый в <asm/current.h>, который даёт указатель на структуру task_struct, определённую в <linux/sched.h>. Указатель current ссылается на процесс, который выполняется в настоящее время. Во время выполнения системного вызова, например, open или read, текущим процессом является тот, который сделал вызов. Код ядра может получать процессо-зависимую информацию используя current, если это необходимо сделать. Пример этой техники приводится в Главе 6.

 

На самом деле current не является подлинно глобальной переменной. Необходимость поддержки SMP систем вынудила разработчиков ядра разработать механизм, который ищет текущий процесс на соответствующем процессоре. Этот механизм должен быть также быстрым, поскольку ссылки на current происходят часто. Результатом является архитектурно-зависимый механизм, который, как правило, скрывает указатель на структуру task_struct на стеке ядра. Детали реализации остаются скрытыми для других подсистем ядра и драйвер устройства может только подключить <linux/sched.h> и сослаться на текущий процесс. Например, следующая команда печатает идентификатор процесса и название команды текущего процесса через доступ к соответствующим полям в структуре task_struct:

 

printk(KERN_INFO "The process is \"%s\" (pid %i)\n",

                current->comm, current->pid);

 

Название команды сохраняется в current->comm и является базовым именем файла программы (обрезается до 15 символов, если это необходимо), которая в настоящее время выполняется текущим процессом.

Несколько дополнительных деталей

Программирование ядра во многих отношениях отличается от программирования пользовательского пространства. Мы будем узнавать об этом, когда будем доходить до них в течение этой книги, но есть несколько основных вопросов, которые, хотя и не требуют своего отдельного раздела, заслуживают упоминания. Итак, вы углубляетесь в ядро и должны иметь в виду следующие соображения. Приложения размещаются в виртуальной памяти с очень большой областью стека. Стек, естественно, используется для хранения истории вызовов функций и всех автоматических переменных, создаваемых активной в данный момент функцией. Ядро, напротив, имеет очень маленький стек, он может быть так же мал, как одна 4096 байтовая страница. Ваши функции должны делить стек со всей цепочкой вызовов пространства ядра. Таким образом, никогда не является хорошей идеей объявление больших автоматических переменных; если вам необходимы более крупные структуры, вы должны выделять им память динамически во время вызова.

 

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

 

Код ядра не может делать арифметики с плавающей точкой. Разрешение плавающей точки требовало бы, чтобы ядро сохраняло и восстанавливало состояние процессора с плавающей точкой при каждом входе и выходе в пространстве ядра, по крайней мере, на некоторых архитектурах. Учитывая, что в действительности нет необходимости в плавающей точке в коде ядра, не стоит иметь дополнительных накладных расходов.

Предыдущая  Содержание  Следующая