Второе издание
Скачать 3.09 Mb.
|
Уменьшение количества выводимых сообщений Для того чтобы уменьшить шум, связанный с сообщениями, которые выдаются во время сборки, но в то же время видеть предупреждения и сообщения об ошибках, можно использовать такую хитрость, как перенаправление стандартного вывода ко- манды make (1): make > "имя_некоторого_файла" Если вдруг окажется необходимым просмотреть выводимые сообщения, можно воспользоваться соответствующим файлом. Но обычно, если предупреждения или сообщения об ошибках выводятся на экран, в этом нет необходимости. На самом деле я выполняю следующую команду make > /dev/null, что позволяет совсем избавиться от ненужных сообщений. Параллельная сборка Программа make (1) предоставляет возможность разбить процесс сборки на не- сколько заданий. Каждое из этих заданий выполняется отдельно от остальных и параллельно с остальными, существенно ускоряя процесс сборки на многопроцес- сорных системах. Это также позволяет более оптимально использовать процессор, Поскольку время компиляции большого дерева исходного кода также включает вре- мя ожидания завершения ввода-вывода (время, в течение которого процесс ждет за- вершения операций ввода-вывода). По умолчанию утилита make (1) запускает только одну задачу, поскольку часто файлы сборки содержат некорректную информацию о зависимостях. При непра- вильной информации о зависимостях несколько заданий могут начать "наступать друг другу на ноги", что приведет к ошибкам компиляции. Конечно же, в файле сборки ядра таких ошибок нет. Для компиляции ядра с использованием параллель- ной сборки необходимо выполнить следующую команду. $ make -jn где n — количество заданий, которые необходимо запустить. Начальные сведения о ядре Linux 37 Обычно запускается один или два процесса на процессор. Например, на двухпро- цессорной машине можно использовать следующий запуск. $ make -j4 Используя такие отличные утилиты, как d i s t c c (1) и c c a c h e ( l ) , можно еще бо- лее существенно уменьшить время компиляции ядра. Инсталляция ядра После того как ядро собрано, его необходимо инсталлировать. Процесс инстал- ляции существенно зависит от платформы и типа системного загрузчика. Для того чтобы узнать, в какой каталог должен быть скопирован образ ядра и как установить его для загрузки, необходимо обратиться к руководству по используемому системно- му загрузчику. На случай если новое ядро будет иметь проблемы с работоспособнос- тью, всегда следует сохранить одну или две копии старых ядер, которые гарантиро- ванно работоспособны! Например, для платформы x86, при использовании системного загрузчика grub можно скопировать загружаемый образ ядра из файла a r c h / i 3 8 6 / b o o t / b z l m a g e в каталог /boot и отредактировать файл / e t c / g r u b / g r u b . c o n f для указания записи, которая соответствует новому ядру. В системах, где для загрузки используется загруз- чик LILO, необходимо соответственно отредактировать файл / e t c / l i l o . c o n f и за- пустить утилиту l i l o (8). Инсталляция модулей ядра автоматизирована и не зависит от аппаратной плат- формы. Просто нужно запустить следующую команду с правами пользователя root. $ make modules_install В процессе компиляции в корневом каталоге дерева исходного кода ядра также создается файл System.map. В этом файле содержится таблица соответствия симво- лов ядра их начальным адресам в памяти. Эта таблица используется при отладке для перевода адресов памяти в имена функций и переменных. "Зверек другого рода" Ядро имеет некоторые отличия в сравнении с обычными пользовательскими при- ложениями, эти отличия хотя и не обязательно приводят к серьезным усложнениям при программировании, но все же создают специфические проблемы при разработ- ке ядра. Эти отличия делают ядро зверьком другого рода. Некоторые из старых правил при этом остаются в силе, а некоторые правила являются полностью новыми. Хотя часть различий очевидна (все знают, что ядро может делать все, что пожелает), другие различия не так очевидны. Наиболее важные отличия описаны ниже. • Ядро не имеет доступа к библиотеке функций языка С. • Ядро программируется с использованием компилятора GNU С. • В ядре нет такой защиты памяти, как в режиме пользователя. • В ядре нельзя легко использовать вычисления с плавающей точкой. • Ядро использует стек небольшого фиксированного размера. 38 Глава 2 • Поскольку в ядре используются асинхронные прерывания, ядро является пре- емптивным и в ядре имеется поддержка SMP, то в ядре необходимо учитывать наличие параллелизма и использовать синхронизацию. • Переносимость очень важна. Давайте рассмотрим более детально все эти проблемы, так как все разработчики ядра должны постоянно помнить о них. Отсутствие библиотеки l i b c В отличие от обычных пользовательских приложений, ядро не компонуется со стандартной библиотекой функций языка С (и ни с какой другой библиотекой та- кого же типа). Для этого есть несколько причин, включая некоторые ситуации с дилеммой о курице и яйце, однако первопричина — скорость выполнения и объем кода. Полная библиотека функций языка С, и даже только самая необходимая ее часть, очень большая и неэффективная для ядра. При этом не нужно расстраиваться, так как многие из функций библиотеки язы- ка С реализованы в ядре. Например, обычные функции работы со строками опи- саны в файле l i b / s t r i n g . с . Необходимо лишь подключить заголовочный файл < l i n u x / s t r i n g . h > и пользоваться этими функциями. Заголовочные файлы Заметим, что упомянутые заголовочные файлы и заголовочные файлы, которые будут упоми- наться далее в этой книге, принадлежат дереву исходного кода ядра. В файлах исходного кода ядра нельзя подключать заголовочные файлы извне этого дерева каталогов, так же как и нельзя использовать внешние библиотеки, Отсутствует наиболее известная функция p r i n t f ( ) . Ядро не имеет доступа к функции p r i n t f ( ) , однако ему доступна функция p r i n t k (). Функция p r i n t k ( ) ко- пирует форматированную строку в буфер системных сообщений ядра (kernel log buf- fer), который обычно читается с помощью программы syslog. Использование этой функции аналогично использованию p r i n t f ( ) : printk("Hello world! Строка: %s и целое число: %d\n", a_string, an_integer); Одно важное отличие между p r i n t f () и p r i n t k () состоит в том, что в функции p r i n t k () можно использовать флаг уровня вывода. Этот флаг используется про- граммой s y s l o g для того, чтобы определить, нужно ли показывать сообщение ядра. Вот пример использования уровня вывода: printk(KERN_ERR "Это была ошибка!\n"); Функция p r i n t k () будет использоваться на протяжении всей книги. В следую- щих главах приведено больше информации о функции p r i n t k (). Компилятор GNU С Как и все "уважающие себя" ядра Unix, ядро Linux написано на языке С. Может быть, это покажется неожиданным, но ядро Linux написано не на чистом языке С в стандарте ANSI С. Наоборот, где это возможно, разработчики ядра используют раз- Начальные сведения о ядре Linux 39 I l l личные расширения языка, которые доступны с помощью средств компиляции gcc (GNU Compiler Collection — коллекция компиляторов GNU, в которой содержится компилятор С, используемый для компиляции ядра). Разработчики ядра используют как расширения языка С ISO C99 1 так и расши- рения GNU С. Эти изменения связывают ядро Linux с компилятором gcc, хотя со- временные компиляторы, такие как Imel С, имеют достаточную поддержку возмож- ностей компилятора gcc для того, чтобы ими тоже можно было компилировать ядро Linux. В ядре не используются какие-либо особенные расширения стандарта С99, и кроме того, поскольку стандарт С99 является официальной редакцией языка С, эти расширения редко приводят к возникновению ошибок в других частях кода. Более интересные и, возможно, менее знакомые отклонения от стандарта языка ANSI С связаны с расширениями GNU С. Давайте рассмотрим некоторые наиболее интерес- ные расширения, которые могут встретиться в программном коде ядра. Функции с подстановкой тела Компилятор GNU С поддерживает функции с подстановкой тела (inline functions). Исполняемый код функции с подстановкой тела, как следует из названия, вставля- ется во все места программы, где указан вызов функции. Это позволяет избежать дополнительных затрат на вызов функции и возврат из функции (сохранение и вос- становление регистров) и потенциально позволяет повысить уровень оптимизации, так как компилятор может оптимизировать код вызывающей и вызываемой функций вместе. Обратной стороной такой подстановки (ничто в этой жизни не дается да- ром) является увеличение объема кода, увеличение используемой памяти и уменьше- ние эффективности использования процессорного кэша инструкций. Разработчики ядра используют функции с подстановкой тела для небольших функций, критичных ко времени выполнения. Использовать подстановку тела для больших функций, осо- бенно когда они вызываются больше одного раза или не слишком критичны ко вре- мени выполнения, не рекомендуется. Функции с подстановкой тела объявляются с помощью ключевых слов s t a t i c и inline в декларации функции. Например, static inline void dog(unsigned long tail_size); Декларация функции должна быть описана перед любым ее вызовом, иначе под- становка тела не будет произведена. Стандартный прием — это размещение функций с подстановкой тела в заголовочных файлах. Поскольку функция объявляется как статическая (static), экземпляр функции без подстановки тела не создается. Если функция с подстановкой тела используется только в одном файле, то она может быть размещена в верхней части этого файла. В ядре использованию функций с подстановкой тела следует отдавать преимуще- ство по сравнению с использованием сложных макросов. 1 Стандарт ISO C99 — это последняя основная версия редакции стандарта ISO С. Редакция С99 со- держит многочисленные улучшения предыдущей основной редакции этого стандарта. Стандарт ISO C99 вводит поименную инициализацию полей структур и тип complex. 40 Глава 2 Встроенный ассемблер Компилятор gcc С позволяет встраивать инструкции языка ассемблера в обыч- ные функции языка С. Эта возможность, конечно, должна использоваться только в тех частях ядра, которые уникальны для определенной аппаратной платформы. Для встраивания ассемблерного кода используется директива компилятора asm(). Ядро Limix написано на смеси языков ассемблера и С. Язык ассемблера использу- ется в низкоуровневых подсистемах и на участках кода, где нужна большая скорость выполнения. Большая часть коду ядра написана на языке программирования С. Аннотация ветвлений Компилятор gnu С имеет встроенные директивы, позволяющие оптимизировать различные ветви условных операторов, которые наиболее или наименее вероятны. Компилятор использует эти директивы для соответственной оптимизации кода. В ядре эти директивы заключаются в макросы l i k e l y ( ) и u n l i k e l y ( ) , которые легко использовать. Например, если используется оператор if следующего вида: if (foo) { / * . . * / } то для того, чтобы отметить этот путь выполнения как маловероятный, необходимо указать: /* предполагается, что значение переменной foo равно нулю ..*/ if (unllkely(ffoo)) { /*..*/ } И наоборот, чтобы отметить этот путь выполнения как наиболее вероятный /* предполагается, что значение переменной foo не равно нулю ..*/ if (likely(foo)) { / * . . * / } Эти директивы необходимо использовать только в случае, когда направление вет- вления с большой вероятностью известно априори или когда необходима оптимиза- ция какой-либо части кода за счет другой части. Важно помнить, что эти директивы дают увеличение производительности, когда направление ветвления предсказано правильно, однако приводят к потере производительности при неправильном пред- сказании. Наиболее часто директивы u n l i k e l y () и l i k e l y () используются для проверки ошибок. Отсутствие защиты памяти Когда прикладная программа предпринимает незаконную попытку обращения к памяти, ядро может перехватить эту ошибку и аварийно завершить соответствующий процесс. Если ядро предпринимает попытку некорректного обращения к памяти, то результаты могут быть менее контролируемы. Нарушение правил доступа к памяти в режиме ядра приводит к ошибке oops, которая является наиболее часто встреча- Начальные сведения о ядре Linux 41 ющейся ошибкой ядра. Не стоит говорить, что нельзя обращаться к запрещенным областям памяти, разыменовывать указатели со значением NULL и так далее, однако в ядре ставки значительно выше! Кроме того, память ядра не использует замещение страниц. Поэтому каждый байт памяти, который использован в ядре, — это еще один байт доступной физической па- мяти. Это необходимо помнить всякий раз, когда добавляются новые функции ядра. Нельзя просто использовать вычисления с плавающей точкой Когда пользовательская программа использует вычисления с плавающей точкой, ядро управляет переходом из режима работы с целыми числами в режим работы с плавающей точкой. Операции, которые ядро должно выполнить для использования инструкций работы с плавающей точкой, зависят от аппаратной платформы. В отличие от режима задачи, в режиме ядра нет такой роскоши, как прямое ис- пользование вычислений с плавающей точкой. Активизация режима вычислений с плавающей точкой в режиме ядра требует сохранения и восстановления регистров устройства поддержки вычислений с плавающей точкой вручную, кроме прочих ру- тинных операций. Если коротко, то можно посоветовать: не нужно этого делать; ника- ких вычислений с плавающей точкой в режиме ядра. Маленький стек фиксированного размера Пользовательские программы могут "отдохнуть" вместе со своими тоннами ста- тически выделяемых переменных в стеке, включая структуры большого размера и многоэлементные массивы. Такое поведение является законным в режиме задачи, так как область стека пользовательских программ может динамически увеличиваться в размере (разработчики, которые писали программы под старые и не очень интел- лектуальные операционные системы, как, например, DOS, могут вспомнить то вре- мя, когда даже стек пользовательских программ имел фиксированный размер). Стек, доступный в режиме ядра, не является ни большим, ни динамически изме- няемым, он мал по объему и имеет фиксированный размер. Размер стека зависит от аппаратной платформы. Для платформы х86 размер стека может быть сконфигури- рован на этапе компиляции и быть равным 4 или 8 Кбайт. Исторически так сложи- лось, что размер стека ядра равен двум страницам памяти, что соответствует 8 Кбайт для 32-разрядных аппаратных платформ и 16 Кбайт — для 64-разрядных. Этот размер фиксирован. Каждый процесс получает свою область стека. Более подробное обсуждение использования стека в режиме ядра смотрите в сле- дующих главах. Синхронизация и параллелизм Ядро подвержено состояниям конкуренции за ресурсы (race condition). В отли- чие от однопоточной пользовательской программы, ряд свойств ядра позволяет осу- ществлять параллельные обращения к ресурсам общего доступа, и поэтому требуется выполнять синхронизацию для предотвращения состояний конкуренции за ресурсы. В частности, возможны следующие ситуации. 42 Глава 2 • Ядро Linux поддерживает многопроцессорную обработку. Поэтому, без соот- ветствующей защиты, код ядра может выполняться на одном, двух или боль- шем количестве процессоров и при этом одновременно обращаться к одному ресурсу. • Прерывания возникают асинхронно по отношению к исполняемому коду. Поэтому, без соответствующей защиты, прерывания могут возникнуть во время обращения к ресурсу общего доступа, и обработчик прерывания может тоже обратиться к этому же ресурсу. • Ядро Linux является преемптивным. Поэтому, без соответствующей защиты, исполняемый код ядра может быть вытеснен в пользу другого кода ядра, кото- рый тоже может обращаться к некоторому общему ресурсу. Стандартное решение для предотвращения состояния конкуренции за ресурсы (состояния гонок) — это использование спин-блокировок и семафоров. Более полное обсуждение вопросов синхронизации и параллелизма приведено в следующих главах. Переносимость — это важно При разработке пользовательских программ переносимость не всегда является це лью, однако операционная система Linux является переносимой и должна оставать- ся такой. Это означает, что платформо-независимый код, написанный на языке С, должен компилироваться без ошибок и правильно выполняться на большом количе- стве систем. Несколько правил, такие как не создавать зависимости от порядка следования байтов, обеспечивать возможность использования кода для 64-битовых систем, не привязываться к размеру страницы памяти или машинного слова и другие— имеют большое значение. Эти вопросы более подробно освещаются в одной из следующих глав. Резюме Да, ядро— это действительно нечто иное: отсутствует защита памяти, нет про- веренной библиотеки функций языка С, маленький стек, большое дерево исходного кода. Ядро Linux играет по своим правилам и занимается серьезными вещами. Тем не менее, ядро— это всего лишь программа; оно, по сути, не сильно отличается от других обычных программ. Не нужно его бояться. Понимание того, что ядро не так уж страшно, как кажется, может стать первым шагом к пониманию того, что все имеет свой смысл. Однако чтобы достичь этой уто- пии, необходимо стараться, читать исходный код, изменять его и не падать духом. Вводный материал, который был представлен в первой главе, и базовые момен- ты, которые описаны в текущей, надеюсь, станут хорошим фундаментом для тех знаний, которые будут получены при прочтении всей книги. В следующих разделах будут рассмотрены конкретные подсистемы ядра и принципы их работы. Начальные сведения о ядре Linux 43 3 Управление процессами П роцесс - одно из самых важных абстрактных понятий в Unix-подобных опе- рационных системах 1 . По сути, процесс— это программа, т.е. объектный код, хранящийся на каком-либо носителе информации и находящийся в состоянии ис- полнения. Однако процесс — это не только исполняемый программный код, кото- рый для операционной системы Unix часто называется text section (сегмент текста или сегмент кода). Процессы также включают в себя сегмент данных (data section), со- держащий глобальные переменные; набор ресурсов, таких как открытые файлы и ожидающие на обработку сигналы; адресное пространство и один или более потоков выполнения. Процесс — это живой результат выполнения программного кода. Потоки выполнения, которые часто для сокращения называют просто потоками (thread), представляют собой объекты, выполняющие определенные операции вну- три процесса. Каждый поток включает в себя уникальный счетчик команд (program counter), стек выполнения и набор регистров процессора. Ядро планирует выполне- ние отдельных потоков, а не процессов. В традиционных Unix-подобных операцион- ных системах каждый процесс содержал только один поток. Однако в современных системах многопоточные программы используются очень широко. Как будет пока- зано далее, в операционной системе Linux используется уникальная реализация по- токов — между процессами и потоками нет никакой разницы. Поток в операционной системе Linux — это специальный тип процесса. В современных операционных системах процессы предусматривают наличие двух виртуальных ресурсов: виртуального процессора и виртуальной памяти. Виртуальный процессор создает для процесса иллюзию, что этот процесс монопольно использу- ет всю компьютерную систему, за исключением, может быть, только того, что фи- зическим процессором совместно пользуются десятки других процессов. В главе 4, "Планирование выполнения процессов", эта виртуализация обсуждается более под- робно. Виртуальная память предоставляет процессу иллюзию того, что он один рас- полагает всей памятью компьютерной системы. Виртуальной памяти посвящена гла- ва 11, "Управление памятью". Потоки совместно используют одну и ту же виртуальную память, хотя каждый поток получает свой виртуальный процессор. Другая абстракция — это файл. Следует подчеркнуть, что сама по себе программа процессом не является; про- цесс — это выполняющаяся программа плюс набор соответствующих ресурсов. Конечно, может существовать два процесса, которые исполняют одну и ту же про- грамму. В действительности может даже существовать два или больше процессов, которые совместно используют одни и те же ресурсы, такие как открытые файлы, или адресное пространство. Процесс начинает свое существование с момента соз- дания, что впрочем не удивительно. В операционной системе Linux такое создание выполняется с помощью системного вызова fork() (буквально, ветвление или вил- ка), который создает новый процесс путем полного копирования уже существую- щего. Процесс, который вызвал системную функцию fork (), называется порожда- ющим (родительским, pannt), новый процесс именуют порожденным (дочерний, child). Родительский процесс после этого продолжает выполнение, а порожденный процесс начинает выполняться с места возврата из системного вызова. Часто после развет- вления в одном из процессов желательно выполнить какую-нибудь другую програм- му. Семейство функций exec*() позволяет создать новое адресное пространство и загрузить в него новую программу. В современных ядрах Linux функция fork() ре- ализована через системный вызов clone(), который будет рассмотрен в следующем разделе. Выход из программы осуществляется с помощью системного вызова e x i t ( ) . Эта функция завершает процесс и освобождает все занятые им ресурсы. Родительский процесс может запросить о состоянии порожденных им процессов с помощью си- стемного вызова wait4() 2 , который заставляет один процесс ожидать завершения другого. Когда процесс завершается, он переходит в специальное состояние зомби (zombie), которое используется для представления завершенного процесса до того мо- мента, пока порождающий его процесс не вызовет системную функцию wait() или waitpid(). Иное название для процесса — задание или задача (task). О процессах в ядре опе- рационной системы Linux говорят как о задачах. В этой книге оба понятия взаимо- заменяемы, хотя по возможности для представления работающей программы в ядре будет использоваться термин задача, а для представления в режиме пользователя — термин процесс. |