Главная » Ядро Linux » "Зверек другого рода"

0

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

Эти  отличия делают ядро  зверьком  другого рода. Некоторые из старых правил при этом  остаются в силе, а некоторые правила являются полностью новыми. Хотя  часть различий  очевидна (все  знают,  что  ядро  может  делать  все, что  пожелает), другие различия не так  очевидны. Наиболее важные отличия описаны ниже.

• Ядро  не  имеет  доступа  к библиотеке функций языка С.

• Ядро  программируется с использованием компилятора GNU С.

• В ядре  нет такой  защиты памяти, как  в режиме пользователя.

• В ядре  нельзя легко  использовать вычисления с плавающей точкой.

• Ядро  использует стек  небольшого фиксированного размера.

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

• Переносимость очень  важна.

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

Отсутствие библиотеки lib c

В отличие от  обычных пользовательских приложений,  ядро  не  компонуется со стандартной библиотекой функций языка С  (и  ни  с  какой другой  библиотекой такого  же  типа).   Для  этого  есть  несколько причин,  включая некоторые ситуации с дилеммой о курице и яйце, однако первопричина — скорость выполнения  и  объем кода.   Полная библиотека функций языка С,  и даже  только   самая   необходимая ее часть, очень  большая и неэффективная для  ядра.

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

<linux/string.h >  и пользоваться этими функциями.

Заголовочные файлы

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

Отсутствует наиболее  известная  функция  printf() .  Ядро  не  имеет  доступа   к функции  printf() , однако ему доступна функция print k () . Функция  printk( )  копирует  форматированную строку  в буфер  системных сообщений ядра  (kernel  log buffer), который обычно читается с  помощью программы  syslog .  Использование этой функции  аналогично  использованию  printf() :

printk("Hello world! Строка: %s и целое число: %d\n", a_string, an_integer);

Одно  важное  отличие между  print f ()  и print k ()   состоит в том, что  в функции print k ()    можно  использовать флаг  уровня   вывода.  Этот  флаг  используется программой  syslo g для  того, чтобы  определить, нужно  ли  показывать сообщение ядра. Вот  пример использования уровня  вывода:

printk(KERN_ERR "Это была ошибка!\n");

Функция print k ()   будет  использоваться на  протяжении всей  книги. В следующих  главах  приведено больше  информации о функции print k () .

Компилятор GNU С

Как  и  все  "уважающие себя"  ядра  Unix, ядро  Linux  написано на  языке  С.  Может быть, это  покажется неожиданным, но  ядро  Linux  написано не  на  чистом языке  С в стандарте ANSI  С.  Наоборот, где это  возможно, разработчики ядра  используют раз-

личные расширения  языка,  которые доступны с помощью средств компиляции gcc (GNU Compiler Collection — коллекция  компиляторов  GNU, в которой содержится компилятор  С, используемый для компиляции ядра).

Разработчики  ядра используют как расширения  языка С ISO  C991    так и расширения GNU  С. Эти изменения  связывают ядро Linux с компилятором gcc, хотя современные  компиляторы, такие как Imel С, имеют достаточную поддержку возможностей компилятора  gcc для того, чтобы ими тоже можно было компилировать ядро Linux. В ядре не используются какие-либо  особенные  расширения  стандарта С99, и кроме того, поскольку стандарт С99 является официальной редакцией языка С, эти расширения  редко приводят к возникновению ошибок в других частях кода. Более интересные  и, возможно,  менее знакомые отклонения  от стандарта языка ANSI С связаны с расширениями GNU С. Давайте рассмотрим некоторые наиболее интересные расширения, которые могут встретиться в программном  коде ядра.

Функции с подстановкой тела

Компилятор GNU  С поддерживает функции с подстановкой  тела (inline functions). Исполняемый код функции  с подстановкой  тела, как следует из названия,  вставляется во все места программы,  где указан вызов функции.  Это позволяет  избежать дополнительных  затрат на вызов функции  и возврат из функции  (сохранение  и восстановление  регистров)  и потенциально  позволяет повысить уровень оптимизации, так как компилятор  может оптимизировать  код вызывающей и вызываемой функций вместе. Обратной стороной такой подстановки  (ничто в этой жизни не дается даром) является увеличение объема кода, увеличение используемой памяти и уменьше ние эффективности использования процессорного  кэша инструкций.  Разработчики  ядра используют функции  с подстановкой  тела для небольших функций,  критичных ко времени выполнения. Использовать подстановку тела для больших функций,  особенно когда они вызываются больше одного раза или не слишком критичны  ко времени выполнения, не рекомендуется.

Функции  с подстановкой  тела объявляются  с помощью ключевых слов stati c и inlin e в декларации функции.  Например,

static inline void dog(unsigned long tail_size);

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

В ядре использованию  функций  с подстановкой  тела следует отдавать преимущество по сравнению с использованием  сложных макросов.

1    Стандарт ISO C99 — это последняя  основная  версия редакции  стандарта ISO С. Редакция  С99 содержит многочисленные улучшения  предыдущей  основной  редакции  этого  стандарта.  Стандарт ISO  C99 вводит поименную  инициализацию полей  структур и тип  complex.

Il l

Встроенный ассемблер

Компилятор gcc  С  позволяет встраивать инструкции  языка ассемблера в  обычные  функции языка С.  Эта  возможность, конечно, должна  использоваться только  в тех частях  ядра, которые уникальны для  определенной аппаратной платформы.

Для   встраивани я  ассемблерного  кода   используется  директив а  компилятора asm() .

Ядро  Limix  написано на смеси  языков ассемблера и С. Язык  ассемблера используется  в низкоуровневых  подсистемах и  на участках  кода, где  нужна  большая скорость выполнения. Большая часть  коду ядра  написана на языке  программирования С.

Аннотация ветвлений

Компилятор gnu  С  имеет  встроенные директивы, позволяющие оптимизировать различные ветви  условных операторов,  которые наиболее или  наименее вероятны. Компилятор  использует эти  директивы для  соответственной оптимизации  кода. В ядре  эти  директивы  заключаются  в  макросы  likely( )   и  unlikely() , которые легко  использовать. Например,  если  используется оператор  if  следующего вида:

if (foo) {

/*..*/

}

то для  того, чтобы  отметить этот  путь  выполнения как  маловероятный, необходимо указать:

/* предполагается, что значение переменной foo равно нулю ..*/

if (unllkely(ffoo)) {

/*..*/

}

И  наоборот, чтобы  отметить этот  путь  выполнения как  наиболее вероятный

/* предполагается, что значение переменной foo не равно нулю ..*/

if (likely(foo)) {

/*..*/

}

Эти  директивы необходимо использовать только  в случае, когда  направление ветвления с большой вероятностью известно априори или  когда  необходима оптимизация  какой-либо части  кода  за счет другой  части.  Важно  помнить, что эти  директивы дают  увеличение производительности,  когда  направление  ветвления  предсказано правильно, однако приводят к потере  производительности при  неправильном предсказании.  Наиболее часто   директивы  unlikel y  ()  и  likel y ()   используются для проверки ошибок.

Отсутствие защиты памяти

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

ющейся  ошибкой  ядра.  Не стоит  говорить,  что нельзя  обращаться  к запрещенным областям  памяти,  разыменовывать  указатели  со значением  NULL  и так далее,  однако в ядре ставки значительно  выше!

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

Нельзя просто использовать вычисления с плавающей точкой

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

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

Маленький стек фиксированного размера

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

Стек,  доступный  в режиме ядра,  не является  ни большим,  ни динамически  изменяемым,  он мал по объему и имеет фиксированный размер.  Размер  стека зависит  от аппаратной  платформы.  Для  платформы  х86 размер  стека  может  быть сконфигурирован на этапе компиляции и быть равным  4 или  8 Кбайт.  Исторически  так сложилось,  что размер стека ядра равен двум страницам  памяти,  что соответствует 8 Кбайт для 32-разрядных  аппаратных  платформ  и  16 Кбайт — для 64-разрядных.  Этот размер фиксирован.  Каждый  процесс  получает свою область стека.

Более подробное  обсуждение использования стека  в режиме ядра смотрите в следующих  главах.

Источник: Лав,  Роберт. Разработка ядра  Linux, 2-е  издание. : Пер.  с англ.  — М.  : ООО  «И.Д.  Вильяме» 2006. — 448 с. : ил. — Парал. тит. англ.

По теме:

  • Комментарии