Модули ядра в сравнении с приложениями
Прежде чем двигаться дальше, необходимо понять различия между модулями ядра и приложениями.
В то время как большинство приложений выполняют одну задачу от начала до конца, каждый модуль просто регистрирует себя в порядке выполнения будущего запроса и инициируют функции которые непосредственно завершают его (запрос). Другими словами, задачу функции инициализации модулей — которая подготовляет более поздний вызов функций модулей, можно объяснить так: модуль говорит «Привет, я здесь, вот что я могу делать.» Функция выхода модуля (hello_exit в примере), вызывается только для того чтобы выгрузить модуль. Она говорит ядру: «Меня здесь больше нет, не больше не просите меня ни о чем.» Этот вид подхода в программировании похож на программирование «случайного управления», но пока не все приложения «случайного управления», как все и каждый модуль ядра. Другое важное отличие между приложениями «случайного управления» и кодом ядра это функция выхода: пока приложение, которое завершает свою работу лениво высвобождает ресурсы или избегает отключать все сразу, функция выхода (exit function) модуля должна внимательно отменить все что создала функция инициализации (init fuction) иначе куски кода будут оставаться в ядре до тех пор пока система не перегрузится.
Между прочим, возможность выгружать модули станет для вас одним из самых ценных преимуществ модульности, т.к. она помогает сократить время разработки. Вы сможете тестировать новые версии вашего драйвера не ожидая каждый раз длинного процесса перезагрузки.
Как программист вы знаете что приложения могут вызывать функции не определяя их: связанная стадия вызова внешних ссылок использует соответствующую библиотеку функций. Prinf одна из таких вызываемых функций и определена в libc. Модуль, с другой стороны, связан только с ядром, и только функции могут вызвать его (модуль) из ядра; здесь нет библиотек к которым нужно обращается(?!!). Функция printk, которую мы использовали недавно в файле hello.c , для примера, версия определенной printf без ядра и экспортирована в модуль. Она ведет себя подобно настоящей функции, с некоторыми маловажными различиями, основой её недостаток, то что она не поддерживает числа с плавающей точкой.
Рисунок 2-1 показывает как вызовы функции и указатели используются в модулях и добавляют новую функциональность работающему ядру.
Рисунок 2-1. Связь модулей в ядре
Так как нет библиотек связанных с модулями, исходные файлы как правило ни когда не включают заголовки файлов, и в очень редких случаях в качестве исключения. Только функции которые в настоящее время являются частью ядра могут быть использованы в модулях ядра. Все что вы брали и сконфигурировали связанно с ядром и определено в заголовке который можно найти в исходном дереве ядра, большинство относящихся к делу заголовков находятся в директориях include/linux и include/asm, но другие поддиректории директории include добавлены в содержании материала связанны с особенными подсистемами ядра.
Роль каждого отдельного заголовка ядра которые объясняются на протяжении всей книги тогда когда это необходимо.
Другое важное различие между программированием ядра и приложений состоит в том, как среда реагирует на ошибки: в то время как ошибка сегментации в процессе разработки приложения безопасна и всегда может быть использован отладчик для обнаружения проблемы в исходном коде, ошибка в ядре в как минимум уничтожит текущий процесс, если не всю систему целиком. Как отслеживать ошибки ядра будет рассмотренно главе 4.
2.3.1. Пространство пользователя и пространство ядра.
Модуль работает в пространстве ядра, в то время как приложение - в пространстве пользователя. Этот принцип является основополагающим в теории операционных систем.
Роль операционной системы на практике — предоставить программам согласующийся вид аппаратного обеспечения компьютера. В дополнение, операционная система должна считать независимые операции программ и защищать от несанкционированного доступа к ресурсам. Эта не очевидная задача возможна только если ЦрУ навязывает защиту программного обеспечения системы от приложений.
Каждый современный процессор позволяет навязывать такой режим работы. Избранный подход осуществляет различные операционные модальности (или уровни) внутри ЦрУ. Уровни играют разную роль и некоторые операции не разрешены более низким уровням, код программы может переключаться с одного уровня на другой только через ограниченное число входов. Юникс системы разработаны таким образом, чтобы дать расширение этих возможностей аппаратного обеспечения, используя два таких уровня. В настоящее время все процессоры поддерживают два уровня защиты а некоторые такие как семейство х86, имеют в распоряжении ещё больше уровней, когда отдельные уровни отсутствуют, тогда используются более высокие и более низкие уровни. В Юниксах ядро выполняется на самом высоком уровне (оно называется ещё режим суперпользователя), на этом уровне разрешено все, пока приложения выполняются на самом низком уровне ( также называется режим пользователя), процессор регулирует прямой доступ к аппаратному обеспечению и несанкционированный доступ к памяти.
Обычно мы обращаемся за выполнением режима как пользовательского пространства, так и пространства пользователя. Эти термины окружены не только различными привилегированными уровнями, свойственный двум режимам, но также факт то что каждый режим, также может иметь в распределении собственной памяти собственное пространство адресов
Юникс переносит выполнение из пространства пользователя в пространство ядра каждый раз, когда приложение делает системный вызов или приостанавливается из-за аппаратного прерывания. Код ядра, выполняя системный вызов, работает в контексте процесса и способен обеспечивать доступ к данным, находящимся в адресном пространстве процессов. С другой стороны, код, который управляет прерываниями, является асинхронным по отношению к процессам и не связан с каким-либо конкретным процессом.
Роль модулей - расширение функциональности ядра; модульный код выполняется в пространстве ядра. Обычно драйвер выполняет обе задачи, описанные ранее: некоторые функции в модуле выполняются как часть системных вызовов, а некоторые из них отвечают за обработку прерываний.
2.3.2 Параллелизм ядра
В одном программирование ядра значительно отличается от обычного программирования приложений - это в подходе к параллелизму. Большинство приложений, за исключением многопоточных, обычно работает последовательно, от начала до конца, совершенно не беспокоясь о том, какие изменения происходят в их окружении. Код ядра работает не в таком простом мире, даже самый простой модуль ядра должен быть написан с учетом того, что многие события могут произойти во время его работы.
Вот несколько источников параллелизма в программировании ядра. Естественно, Линукс работает с многочисленными процессами, более одного из которых могут попытаться одновременно использовать ваш драйвер. Большинство устройств поддерживают прерывание процессора; обработчики прерываний запускаются асинхронно и могут быть вызваны в тот момент, когда ваш драйвер уже что-то делает. Некоторые программные абстракции (такие как таймеры ядра, описанные в главе 7) также выполняются асинхронно. Кроме того, конечно, Линукс может работать на симметричных многопроцессорных (SMP) системах, в результате чего ваш драйвер может быть запущен параллельно более чем на одном процессоре. И наконец, код ядра версии 2.6 сделан вытесняемым, что делает даже однопроцессорные системы сравнимыми с многопроцессорными в вопросах параллелизма.
В результате, код ядра Линукс, включая код драйвера, должен быть reentrantit (?), должна быть возможность запускать его одновременно более, чем в одном контексте. Структуры данных должны быть тщательно спроектированы с таким расчетом, чтобы хранить множество потоков, запущенных отдельно, и код должен обеспечивать доступ к общим данным таким образом, чтобы предотвратить их повреждение. Написание кода, который обладал бы параллелизмом и позволял бы избежать состояний гонки (ситуаций, когда порядок выполнения частей кода влияет на его поведение) требует глубоких размышлений и может быть весьма сложным. Правильное управление параллелизмом требует написания корректного кода ядра; вот почему каждый пример драйвера в этой книге написан с расчетом на параллелизм. Используемые методы объясняются по мере их рассмотрения; этому вопросу также посвящена глава 5, в ней также описаны доступные примитивы ядра для управления параллелизмом.
Распространенная ошибка программистов, пишущих драйверы, состоит в предположении о том, что параллелизм - не проблема, пока отдельный сегмент кода не заснет (или не будет блокирован). Даже в предыдущих версиях ядра (которые не были основными), это предположение не работало для многопроцессорных систем. В версии 2.6 код ядра вообще не может рассчитывать на то, что он будет монопольно владеть процессором во время выполнения его определенного участка. Если вы не будете писать код в расчете на параллелизм, то вы получите код, порождающий катастрофические сбои, который чрезвычайно тяжело будет отладить.
2.3.3. Текущий процесс
Хотя модули ядра не выполняются последовательно как приложения, большинство действий ядра выполняются от имени отдельного процесса. Код ядра может ссылаться на текущий процесс через доступную глобальную переменную current, описанную в файле current.h>, которая является указателем на структуру struct task_struct, содержащуюся в файле sched.h>. Текущий указатель ссылается на процесс, который выполняется в настоящее время. Во время выполнения системного вызова, такого как open или read, текущим считается процесс, который инициировал системный вызов. Если в этом есть необходимость, код ядра может использовать информацию процесса при помощи переменной current. Пример использования этого метода представлен в главе 6.
На самом деле current не является по-настоящему глобальной переменной. Необходимость поддержки SMP заставила разработчиков ядра создать механизм, находящий текущий процесс на соответствующем процессоре. Этому механизму было также необходимо быть быстрым, поскольку ссылки на current возникают часто. В результате появился архитекурно-зависимый механизм, который обычно скрывает указатель на структуру task_struct в стеке ядра. Детали этой реализации остаются скрытыми даже от других подсистем ядра и в драйверы устройств можно просто включить sched.h> и ссылаться на текущий процесс. Например, следующий оператор выводит идентификатор и имя команды текущего процесса, беря их из соответствующих полей структуры task_struct:
printk(KERN_INFO "The process is \"%s\" (pid %i)\n", current->comm, current->pid);
Имя команды, хранящееся в
current->comm
, представляет собой базовое имя файла программы (при необходимости урезанное до 15 символов), которая выполняется текущим процессом.2.3.4 Ещё несколько подробностей
Программирование ядра во многом отличается от программирования в пространстве пользователя. Мы будем указывать на эти различия по мере того, как будем с ними встречаться, но есть несколько принципиальных вопросов, не требующих отдельного раздела, которые стоит упомянуть. По мере углубления в ядро, следующие моменты нужно иметь в виду.
Приложения полагаются на виртуальную память с очень большой областью для стека. Стек используется для хранения истории вызовов функций и всех автоматических переменных, созданных текущей функцией. У ядра же, наоборот, стек очень маленький и может иметь размер одной 4096-байтной страницы. Функции делят этот стек со всей цепочкой вызовов в пространстве ядра. Поэтому создание больших автоматических переменных является плохой идеей -- если вам нужны большие структуры, следует выделять для них память динамически во время вызова.
В API ядра часто встречаются имена функций, начинающиеся с двойного подчеркивания (__). Функции, отмеченные таким образом, обычно являются низкоуровневыми компонентами интерфейса и должны использоваться с большой осторожностью. По сути, двойное подчеркивание говорит программисту: "Если ты вызываешь эту функцию, будь уверен, что ты знаешь, что делаешь".
Код ядра не может выполнять операций с плавающей точкой. Для этого ядру было бы нужно сохранять и восстанавливать состояние математического со-процессора при каждом входе в пространство ядра и выходе из него, как минимум на некоторых архитектурах. С учётом того, что, на самом деле, в коде ядра нет необходимости в операциях с плавающей точкой, дополнительные накладные расходы не оправданы.
Переведено на сайте www.notabenoid.com
http://notabenoid.com/book/11832/38202
Внимание! Этот перевод, возможно, ещё не готов,
так как модераторы установили для него статус
"перевод редактируется"
Комментариев нет:
Отправить комментарий
Примечание. Отправлять комментарии могут только участники этого блога.