WWW.DISSERS.RU

БЕСПЛАТНАЯ ЭЛЕКТРОННАЯ БИБЛИОТЕКА

   Добро пожаловать!

Pages:     || 2 | 3 | 4 | 5 |   ...   | 10 |
-- [ Страница 1 ] --

;

-) РАЗРАБОТЧИКУ Разработка ядра Linux Второе издание NOVELL PRESS Роберт Лав • • • • • • • • • • • • • • • • • • • • • • • • • • • • • • • • • • • • • • • • • • • • • • • • • • • • • • • • • • • • • • • • • • • • • • • • • • • • • • • • • • • • • • • • • • • • Главный инженер по разработке ядра Группа Ximian Desktop, корпорации Novell www.williamspublishing.com Novell Разработка ядра Linux Второе издание Linux Kernel Development Second Edition Robert Love Novell® Novell Press, 800 East 96th Street, Indianapolis, Indiana, 46240 USA •• Разработка ядра Linux Второе издание Роберт Лав Москва • Санкт-Петербург • Киев 2006 ББК 32.973.26-018.2.75 Л13 УДК 681.3.07 Издательский дом "Вильямc" Зав. редакцией С.Н. Тригуб Перевод с английского АA. Судакава По общим вопросам обращайтесь в Издательский дом "Вильямс" по адресу:

info@williamspubiishing.com, http://www.williamspublishing.com 115419, Москва, а/я 783

;

03150, Киев, а/я 152 Лав, Роберт.

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

ISBN 5-8459-1085-4 (рус.) В книге детально рассмотрены основные подсистемы и функции ядер Linux серии 2.6, включая особенности построения, реализации и соответствующие программны интерфейсы. Рассмотренные вопросы включают: планирование выполнения процес сов, управление временем и таймеры ядра, интерфейс системных вызовов, особен ности адресации и управления памятью, страничный кэш, подсистему VFS, механиз мы синхронизации, проблемы переносимости и особенности отладки. Автор книги является разработчиком основных подсистем ядра Linux. Ядро рассматривается как с теоретической, так и с прикладной точек зрения, что может привлечь читателей различными интересами и потребностями.

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

ББК 32.973.26-018.2. Все названия программных продуктов являются зарегистрированными торговыми марками со ответствующих фирм.

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

Aufhorued translation from the English language edition published by Novell Press, Copyright © by Pearson Education, Inc.

All rights reserved. No part of this book shall be reproduced, stored in a retrieval system, or transmi ted by any means, electronic, mechanical, photocopying, recording, or otherwise, without written permi sion from the publisher.

All terms mentioned in this book that are known to be trademarks or service marks have been appro[ rialely capitalized. Novell Press cannot attest to the accuracy of this information. Use of a term in this boo should not be regarded as affecting the validity of any trademark or service mark.

Russian language edition is published by Williams Publishing House according to the Agreement wit R&I Enterprises International, Copyright © ISBN 5-S459-10854 (рус.) © Издательский дом "Вильяме", ISBN 0-Й72-Я2720-1 (англ.) © 2005 by Pearson Education, Inc., Оглавление Предисловие Введение Об авторе От издательства Глава 1. Введение в ядро Linux • Глава 2. Начальные сведения о ядре Linux Глава 3. Управление процессами Глава 4. Планирование выполнения процессов Глава 5. Системные вызовы Глава 6. Прерывания и обработка прерываний Глава 7. Обработка нижних половин и отложенные действия Глава 8. Введение в синхронизацию выполнения кода ядра Глава 9. Средства синхронизации в ядре Глава 10. Таймеры и управление временем Глава 11. Управление памятью Глава 12. Виртуальная файловая система Глава 13. Уровень блочного ввода-вывода Глава 14. Адресное пространство процесса Глава 15. Страничный кэш и обратная запись страниц Глава 16. Модули Глава 17. Объекты kobject и файловая система sysfs Глава 18. Отладка Глава 19. Переносимость Глава 20. Заплаты, разработка и сообщество Приложение А. Связанные списки Приложение Б. Генератор случайных чисел ядра Приложение В. Сложность алгоритмов Приложение Г. Библиография и список литературы Предметный указатель Содержание Предисловие Введение Итак... Версия ядра Читательская аудитория Интернет-ресурс Благодарности ко второму изданию Об авторе От издательства Для читателей Глава 1. Введение в ядро Linux Потом пришел Линус: введение в Linux Обзор операционных систем и ядер Ядро Linux в сравнении с классическими ядрами Unix Версии ядра Linux Сообщество разработчиков ядра Linux Перед тем как начать Глава 2. Начальные сведения о ядре Linux Получение исходного кода ядра Инсталляция исходного кода ядра Использование заплат Дерево исходных кодов ядра Сборка ядра Уменьшение количества выводимых сообщений Параллельная сборка Инсталляция ядра "Зверек другого рода" Отсутствие библиотеки l i bc Компилятор GNU С Отсутствие защиты памяти Нельзя просто использовать вычисления с плавающей точкой Маленький стек фиксированного размера Синхронизация и параллелизм Переносимость — это важно Резюме Глава 3. Управление процессами Дескриптор процесса и структура task structure. 6 Содержание Выделение дескриптора процесса Хранениедескрипторапроцесса Состояние процесса Манипулирование текущим состоянием процесса Контекст процесса Дерево семейства процессов Создание нового процесса Копирование при записи Реализация потоков в ядре Linux Потоки в пространстве ядра Завершение процесса. Удаление дескриптора процесса Дилемма "беспризорного" процесса Резюме Глава 4. Планирование выполнения процессов Стратегия планирования Процессы, ограниченные скоростью ввода-вывода и скоростью процессора Приоритет процесса Квант времени Вытеснение процесса. Стратегия планирования в действии Алгоритм планирования Очереди выполнения Массивы приоритетов Пересчет квантов времени Вычисление приоритетов и квантов времени Переход в приостановленное состояние и возврат к выполнению Балансировка нагрузки Вытеснение и переключение контекста Вытеснение пространства пользователя Вытеснение пространства ядра Режим реального времени Системные вызовы для управления планировщиком Системные вызовы, связанные с управлением стратегией и приоритетом Системные вызовы управления процессорной привязкой Передача процессорного времени В завершение о планировщике Глава 5. Системные вызовы API, POSIX и библиотека С Вызовы syscall Номера системных вызовов. Производительность системных вызовов Обработка системных вызовов Определение необходимого системного вызова Передача параметров Реализация системных вызовов Проверка параметров Контекст системного вызова Окончательные шаги регистрации системного вызова Доступ к системным вызовам из пространства пользователя Почему не нужно создавать системные вызовы В заключение о системных вызовах Глава 6. Прерывания и обработка прерываний Прерывания Обработчики прерываний Верхняя и нижняя половины Регистрация обработчика прерывания Освобождение обработчика прерывания Написание обработчика прерывания Совместно используемые обработчики Настоящий обработчик прерывания Контекст прерывания Реализация системы обработки прерываний Управление прерываниями Запрещение и разрешение прерываний Запрещение определенной линии прерывания Состояние системы обработки прерываний Не нужно прерывать, мы почти закончили! Глава 7. Обработка нижних половин и отложенные действия Нижние половины Когда нужно использовать нижние половины Многообразие нижних половин Механизм отложенных прерываний (softirq) Реализация отложенных прерываний Использование отложенных прерываний Тасклеты Реализация тасклетов Использование тасклетов Демон ksoftirqd Старый механизм ВН Очереди отложенных действий Реализация очередей отложенных действий Использование очередей отложенных действий Старый механизм очередей заданий Какие обработчики нижних половин необходимо использовать Блокировки между обработчиками нижних половин Запрещение обработки нижних половин Внизу обработки нижних половин 8 Содержание Глава 8. Введение в синхронизацию выполнения кода ядра Критические участки и состояние конкуренции за ресурсы Зачем нужна защита Блокировки Откуда берется параллелизм Что требует защиты Взаимоблокировки Конфликт при захвате блокировки и масштабируемость Блокировки в вашем коде Глава 9. Средства синхронизации в ядре Атомарные операции Целочисленные атомарные операции Битовые атомарные операции Спин-бдокировки Другие средства работы со спин-блокировками Спин-блокировки и обработчики нижних половин Спин-блокировки чтения-записи Семафоры Создание и инициализация семафоров Использование семафоров Семафоры чтения-записи. Сравнение спин-блокировок и семафоров Условные переменные BLK: Большая блокировка ядра Секвентные блокировки Средства запрещения преемптивности Барьеры и порядок выполнения Резюмирование по синхронизации Глава 10. Таймеры и управление временем Информация о времени в ядре Частота импульсов таймера: HZ Идеальное значение параметра HZ Переменная jiffies Внутреннее представление переменной j iffies Переполнение переменной jiffies Пространство пользователя и параметр HZ Аппаратные часы и таймеры Часы реального времени. Системный таймер Обработчик прерываний таймера Абсолютное время Таймеры Использование таймеров Состояния конкуренции, связанные с таймерами Содержание Реализациятаймеров Задержка выполнения Задержка с помощью цикла Короткие задержки Функция schedule_timeout () Время вышло Глава 11. Управление памятью Страницы памяти Зоны Получение страниц памяти Получение страниц заполненных нулями Освобождение страниц Функция kmalloc () Флаги gfp_mask Функция kfгее () Функция vmalloc () Уровень слябового распределителя памяти Устройство слябового распределителя памяти Интерфейс слябового распределителя памяти Пример использования слябового распределителя памяти Статическое выделение памяти в стеке Честная игра со стеком Отображение верхней памяти Постоянное отображение Временное отображение Выделение памяти, связанной с определенным процессором Новый интерфейс percpu Работа с данными, связанными с процессорами, на этапе компиляции Работа с данными, связанными с процессорами, на этапе выполнения Когда лучше использовать данные, связанные с процессорами Какой способ выделения памяти необходимо использовать Глава 12. Виртуальная файловая система Общий интерфейс к файловым системам Уровень обобщенной файловой системы Файловые системы Unix Объекты VFS и их структуры данных Другие объекты подсистемы VFS Объект superblock Операции суперблока Объект inode Операции с файловыми индексами Объект dentry Состояние элементов каталога Кэш объектов dentry Операции с элементами каталогов 10 Содержание Объект file Файловые операции Структуры данных, связанные с файловыми системами Структуры данных, связанные с процессом Файловые системы в операционной системе Linux Глава 13. Уровень блочного ввода-вывода Анатомия блочного устройства Буферы и заголовки буферов Структура bio Сравнение старой и новой реализаций Очереди запросов. Запросы Планировщики ввода-вывода Задачи планировщика ввода-вывода Лифтовой алгоритм Линуса Планировщик ввода-вывода с лимитом по времени Прогнозирующий планировщик ввода-вывода Планировщик ввода-вывода с полностью равноправными очередями Планировщик ввода-вывода noop Выбор планировщика ввода-вывода Резюме Глава 14. Адресное пространство процесса Дескриптор памяти Выделение дескриптора памяти Удаление дескриптора памяти Структура mm_struct и потоки пространства ядра Области памяти Флаги областей VMA Операции с областями VMA Списки и деревья областей памяти Области памяти в реальной жизни Работа с областями памяти Функция find_vma () Функция find_vma_prev () Функция find_VMA_intersection () Функции mmap () и do_mmap (): создание интервала адресов Системный вызов mmap () Функции munmap () и do_munmap (): удаление интервала адресов Системный вызов munmap () Таблицы страниц Заключение Глава 15. Страничный кэш и обратная запись страниц Страничный кэш Объект address_space Содержание Базисное дерево Старая хеш-таблица страниц Буферный кэш Демон pdflush Демоны bdflush и kupdated Предотвращение перегруженности: для чего нужны несколько потоков Коротко о главном Глава 16. Модули Модуль "Hello,Worldl" Сборка модулей Использование дерева каталогов исходных кодов ядра Компиляция вне дерева исходных кодов ядра Инсталляция модулей Генерация зависимостей между модулями Загрузка модулей Управление конфигурационными параметрами Параметры модулей Экспортируемые символы Вокруг модулей Глава 17. Объекты kobject и файловая система sysfs Объекты kobject Типы ktype Множества объектов kset Подсистемы Путаница со структурами Управление и манипуляции с объектами kobject Счетчики ссылок Структуры kref Файловая система sysfs Добавление и удаление объектов на файловой системе sysfs Добавление файлов на файловой системе sysfs Уровень событий ядра Кратко об объектах kobj ect и файловой системе sysfs Глава 18. Отладка С чего необходимо начать Дефекты ядра Функция pri ntk () Устойчивость функции pri ntk () Уровни вывода сообщений ядра Буфер сообщений ядра Демоны syslogd и klogd Замечание относительно функции pri nt k () и разработки ядра Сообщения Oops Утилита ksymoops 12 Содержание Функция kallsyms Конфигурационные параметры отладки ядра Отладка атомарных операций 38] Генерация ошибок и выдача информации Магическая клавиша SysRq Сага об отладчике ядра Использование отладчика gdb Отладчик kgdb Отладчик kdb Исследование и тестирование системы Использование идентификатора UID в качестве условия Использование условных переменных Использование статистики Ограничение частоты следования событий при отладке Нахождение исполняемых образов с изменениями приводящими к ошибкам Если ничто не помогает — обратитесь к сообществу Глава 19. Переносимость История переносимости Linux Размер машинного слова и типы данных Скрытые типы данных ' Специальные типы данных Типы с явным указанием размера Знак типа данных char Выравнивание данных Как избежать проблем с выравниванием Выравнивание нестандартных типов данных Заполнение структур Порядок следования байтов История терминов big-endian и little-endian Порядок байтов в ядре Таймер Размер страницы памяти Порядок выполнения операций процессором Многопроцессорность, преемптивность и верхняя память Пару слов о переносимости Глава 20. Заплаты, разработка и сообщество Сообщество Стиль написания исходного кода Отступы Фигурные скобки Длинные строки Имена Функции Комментарии Использование директивы typedef Содержание Использование того, что уже есть Никаких директив ifdef в исходном коде - Инициализация структур Исправление ранее написанного кода Организация команды разработчиков Отправка сообщений об ошибках ' Генерация заплат Представление заплат Заключение Приложение А. Связанные списки Кольцевые связанные списки Перемещение по связанному списку Реализация связанных списков в ядре Linux Структура элемента списка Работа со связанными списками Перемещение по связанным спискам Приложение Б. Генератор случайных чисел ядра Принцип работы и реализация Проблема с загрузкой системы Интерфейсы для ввода энтропии Интерфейсы для вывода энтропии Приложение В. Сложность алгоритмов Алгоритмы Множество О Множество болышого-тета Объединяем все вместе Опасность, связанная со сложностью алгоритмов Приложение Г. Библиография и список литературы Книги по основам построения операционных систем Книги о ядрах Unix Книги о ядрах Linux Книги о ядрах других операционных систем Книги по API Unix Другие работы Web-сайты Предметный указатель 14 Содержание Посвящается Дорис (Doris) и Хялен (Helen) Предисловие связи с тем, что ядро и приложения операционной системы Linux используют ся все более широко, возрастает число разработчиков системного программ В ного обеспечения, желающих заняться разработкой и поддержкой операционной системы Linux. Некоторые из этих инженеров руководствуются исключительно соб ственным интересом, некоторые работают в компаниях, которые занимаются опе рационной системой Linux, некоторые работают на производителей компьютерных аппаратных средств, некоторые заняты в проектах по разработке программного обе спечения на дому.

Однако все они сталкиваются с общей проблемой: кривая затрат на изучение ядра становится все длиннее и круче. Система становится все более сложной и, кро ме того, очень большой по объему. Годы проходят, и нынешние члены команды раз работчиков ядра приобретают все более широкие и глубокие знания, что увеличива ет разрыв между ними и разработчиками-новичками.

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

Один из возможных подходов к решению данной проблемы — ясность исходного кода: удобные интерфейсы, четкая структура, следование принципу "Делать мало, но делать хорошо" и т.д. Такое решение предложено Линусом Торвальдсом (Linus Torvalds).

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

Именно печатное слово лучше всего подходит для стартовой точки такого пони мания.

Вклад Роберта Лава (Robert Love) состоит в предоставлении возможности, благо даря которой опытные разработчики смогут получить полную информацию о том, какие задачи должны выполнять различные подсистемы ядра и каким образом пред полагается выполнение этих задач. Этой информации должно быть достаточно для многих людей: для любопытных, для разработчиков прикладного программного обе спечения, для тех, кто хочет ознакомиться с устройством ядра, и т.д.

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

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

Пользуйтесь книгой Роберта и учитесь по ней! Может быть, и вы решите сделать следующий шаг и вступить в сообщество разработчиков ядра, куда мы вас и пригла шаем. Людей ценят по важности их дел, поэтому, помогая развитию операционной системы Linux, знайте, что ваша работа— небольшая, но непосредственная помощь десяткам или даже сотням миллионов людей.

Эндрю Мортон (Andrew Morton) Open Source Development Labs 16 - Введение Введение Когда я сделал первую попытку превратить свой опыт работы с ядром Linux в текст книги, понял, что не знаю, как двигаться дальше. Не хотелось просто писать еще одну книгу о ядре операционной системы. Конечно, на эту тему не так уж и много книг, но все же я хотел сделать что-то такое, благодаря чему моя книга была бы особенной. Как достичь этой цели? Я не могу успокоиться, пока не сделаю что нибудь особенное, лучшее в своем роде.

Наконец я решил, что смогу предложить достаточно уникальный подход к дан ной теме. Моя работа — изучение и разработка ядра операционной системы. Мое увлечение — изучение и разработка ядра операционной системы. Моя любовь — ядро операционной системы. Конечно, за многие годы я успел собрать много интересных анекдотов и полезных советов. С моим опытом я смог бы написать книгу о том, как нужно разрабатывать программный код ядра и как этого делать не нужно. Прежде всего, эта книга об устройстве и практической реализации ядра операционной си стемы Linux. В ней информация представлена так, чтобы получить достаточно зна ний для решения реальных практических задач и чтобы эти задачи решать правиль но. Я человек прагматичный, и книга имеет практический уклон. Она должна быть полезной, интересной и легко читаться.

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

Это то, что касалось еще первого издания книги. Однако время идет и снова приходится возвращаться к рассмотренным вопросам. В этом издании представле но несколько больше информации по сравнению с первым: материал серьезно пере смотрен и доработан, появились новые разделы и главы. С момента выхода перво го издания в ядро были внесены изменения. Однако, что более важно, сообщество разработчиков ядра Linux приняло решение1 в ближайшем будущем не начинать разработку серии ядра 2.7. Было решено заняться стабилизацией серии ядра 2.6.

Стабилизация включает в себя много моментов, тем не менее есть один важный, ко торый касается данной книги, — книга, которая посвящена ядру серии 2.6, остается актуальной. Если изменения происходят не слишком быстро, то существует большой шанс, что "моментальный снимок" ядра останется актуальным и в будущем. В конце концов, книга сможет вырасти и стать канонической документацией по ядру. Я на деюсь, что именно такая книга и находится у вас в руках.

Как бы там ни было, книга написана, и я надеюсь, что она вам понравится.

Это решение было принято на саммите разработчиков ядра Linux (Linux Kernel Development Summit), который состоялся летом 2004 года в г. Оттава, Канада.

Введение Итак...

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

Очень важно, чтобы вы читали программный код. Доступность открытого исхо дного кода операционной системы Linux — это подарок, который встречается очень редко. Однако недостаточно только читать исходный код. Необходимо взяться за дело серьезно и изменять этот программный код. Находите ошибки и исправляйте их! Улучшайте драйверы для своего аппаратного обеспечения! Находите слабые ме ста и закрывайте их! У вас все получится, если вы будете сами писать программный код.

Версия ядра Эта книга посвящена ядрам Linux серии 2.6 и базируется на версии ядра 2.6.10.

Ядро — это "движущийся объект", и никакая книга не в состоянии передать динами ку во все моменты времени. Тем не менее базовые внутренние структуры ядра уже сформировались, и основные усилия по представлению материала были направлены на то, чтобы этот материал можно было использовать и в будущем.

Читательская аудитория Эта книга предназначена для разработчиков программного обеспечения, кото рые заинтересованы в понимании ядра операционной системы Linux. Тем не менее это не построчные комментарии исходного кода ядра. Это также не руководство по разработке драйверов и не справочник по программному интерфейсу (API) ядра (кстати, формализованного API ядра Linux никогда не было). Целью книги являет ся предоставление достаточной информации об устройстве и реализации ядра для того, чтобы подготовленный программист смог начать разработку программного кода. Разработка ядра может быть увлекательным и полезным занятием, и я хочу ознакомить читателя с этой сферой деятельности по возможности быстро. В книге обсуждаются как вопросы теории, так и практические приложения, она обращена к людям, которые интересуются и тем, и другим. Я всегда придерживался мнения, что для понимания практических приложений необходима теория, тем не менее я счи таю, что эта книга не сильно углубляется в оба этих направления. Я надеюсь, что, независимо от мотиваций необходимости понимания ядра операционной системы Linux, эта книга сможет объяснить особенности устройства и реализации в доста точной степени.

Таким образом, данная книга освещает как использование основных подсистем ядра, так и особенности их устройства и реализации. Я думаю, что эти вопросы важ ны и достойны обсуждения. Хороший пример — глава 7, "Обработка нижних половин и отложенные действия", посвященная обработчикам нижних половин (bottom half).

18 Введение В этой главе рассказывается о принципах работы и об особенностях реализации механизмов обработки нижних половин (эта часть может быть интересна разработ чикам основных механизмов ядра), а также о том, как на практике использовать экс портируемый интерфейс ядра для реализации собственных обработчиков bottom ha lf (это может быть интересно для разработчиков драйверов устройств). На самом деле мне кажется, что обе эти стороны обсуждения будут интересны для всех групп разработчиков. Разработчик основных механизмов ядра, который, конечно, должен понимать принципы работы внутренних частей ядра, должен также понимать и то, как интерфейсы ядра будут использоваться на практике. В то же самое время разра ботчик драйверов устройств получит большую пользу от хорошего понимания того, что стоит за этим интерфейсом.

Все это сродни изучению программного интерфейса некоторой библиотеки наря ду с изучением того, как эта библиотека реализована. На первый взгляд, разработчик прикладных программ должен понимать лишь интерфейс (API). И действительно, интерфейсы часто предлагают рассматривать в виде черного ящика. Разработчик библиотеки, наоборот, обычно интересуется лишь принципом работы и реализации функций библиотеки. Я уверен, что обе группы разработчиков должны потратить некоторое время на изучение другой стороны предмета. Разработчик программ, который хорошо понимает операционную систему, сможет значительно лучше эту операционную систему использовать. Аналогично разработчик библиотеки должен иметь хотя бы малое представление о том, что происходит в реальной жизни, и, в частности, о тех программах, в которых будет использоваться его библиотека.

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

Предполагается, что читатель знаком с языком программирования С и операци онной системой Linux. Некоторые знания принципов построения операционных си стем также желательны. Я старался объяснять все понятия, однако в случае проблем в списке литературы можно найти несколько отличных книг, которые посвящены основам построения операционных систем.

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

;

я буду очень рад оказать помощь.

Интернет-ресурс Автор поддерживает Интернет-сайт http: //tech9.net/rml/kernel_book/, со держащий информацию о данной книге, включая ошибки, расширенные и исправ ленные разделы, а также информацию о будущих изданиях. Всем читателям реко мендуется посетить этот сайт.

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

В первую очередь, я хотел бы высказать благодарность моему редактору Скотту Мейерсу (Scott Meyers) за руководство, благодаря которому второе издание книги превратилось из идеи в конечный продукт. Мне снова было очень приятно работать с Джоржем Недеффом (Georg Nedeff), производственным редактором, ко торый во всем обеспечивал порядок. Особая благодарность литературному редакто ру Марго Кэтс (Margo Catts). Мы можем только желать, чтобы наше владение ядром было так же совершенно, как ее владение печатным словом.

Отдельное спасибо техническим редакторам этого издания Адаму Белею (Adam Belay), Мартину Пулу (Martin Pool) и Крису Ривере (Chris Rivera). Их знания и исправления помогли сделать эту книгу неизмеримо лучше. Если, несмотря на их неоценимые усилия, все же остались ошибки, то это вина автора. Такое же большое спасибо Заку Брауну (Zak Brown), который приложил огромные усилия к техническо му редактированию первого издания.

Многие разработчики ядра отвечали на вопросы, предоставляли поддержку или просто писали программный код, интересный настолько, что по нему можно было бы написать отдельную книгу. Среди них Андреа Аркангели (Andrea Arcangely), Алан Кокс (Alan Сох), Грег Кроах-Хартман (Greg Kroah-Hartman), Даниэл Филлипс (Daniel Phillips), Дэвид Миллер (David Miller), Патрик Мочел (Patrick Mochel), Эндрю Мортон (Andrew Morton), Звене Мвейкамбо (Zwane Mwaikambo), Ник Пиггин (Nick Piggin) и Линус Торвальдс (Linus Torvalds). Особое спасибо тайному сообще ству ядра (хотя никакого тайного сообщества нет).

Я хочу выразить свою любовь и признательность многим людям. Среди них Пол Амичи (Paul Amichi), Кейт Бэрбег (Keith Barbag), Дейв Эггерс (Dave Eggers), Ричард Эриксон (Richard Erickson), Нат Фридман {Nat Friedman), Дастин Холл (Dostin Hall), Джойс Хокинс (Joyce Hawkins), Мигуэль де Иказа (Miguel de Icaza), Джимми Крел (Jimmy Krehl), Дорис Лав (Doris Love), Джонатан Лав (Jonathan Love), Патрик ЛеКлер (Patrick LeClair), Линда Лав (Linda Love), Рэнди О'Дауд (Randy O'Dowd), Сальваторэ Рибаудо (Salvatore Ribaudo) и его чудесная мама, Крис Ривера (Chris Rivera), Джой Шау (Joey Shaw), Джэрэми ВанДорен (Jeremy VanDoren) и его семья, Стив Вейсберг (Steve Weisberg) и Хелен Винснант (Helen Whinsnant).

И в заключение, спасибо за все моим родителям.

Желаю большого хакерского счастья!

Роберт Лав, г. Кембридж, штат, Массачусетс.

20. Введение Об авторе Роберт Лав (Robert Love) использует операционную систему Linux с первых дней ее существования. Он является страстным активистом сообществ разработ чиков ядра и GNOME. Сейчас Роберт работает главным инженером по разработке ядра группы разработчиков Ximian Desktop компании Novell. До этого он работал инженером по разработке ядра компании Мота Vista Software.

Проекты по разработке ядра, которыми занимался автор, включают планиров щик выполнения процессов, преемптивное (вытесняемое) ядро (preemptive kernel), уровень событий ядра, улучшение поддержки виртуальной памяти (VM), улучшение поддержки многопроцессорного оборудования. Роберт является автором утилит schedutils и менеджера томов GNOME. Роберт Лав читает лекции и пишет статьи по основам построения ядра операционной системы и получает приглашения редак тировать статьи в издании Linux Journal.

Автор получил степень бакалавра по математике и вычислительной технике в университете штата Флорида. Хотя Роберт и родился в южной Флориде, своим до мом он считает Кембридж, штат Массачусетс. Роберт увлекается футболом, фотогра фией и любит готовить.

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

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

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

E-mail: info@williamspublishing.com WWW: http://www.williamspublishing.com Информация для писем из:

России: 115419, Москва, а/я Украины: 03150, Киев, а/я Для читателей Более подробную информацию об этой и других книгах издательства Sams Publishing можно получить на Интернет-сайте www. nowellpress.com. Для поиска информации о книгах введите в поисковое поле код ISBN (без соединительных чер точек) или название книги.

Введение в ядро Linux аже после трех десятилетий использования операционная система (ОС) Unix все еще считается одной из самых мощных и элегантных среди всех Д существующих операционных систем. Со времени создания операционной систе мы Unix в 1969 году, это детище Денниса Ритчи (Dennis Ritchie) и Кена Томпсона (Ken Thompson) стало легендарным творением, системой, принцип работы которой выдержал испытание временем и имя которой оказалось почти незапятнанным.

Операционная система Unix выросла из Multics — многопользовательской опера ционной системы, проект по созданию которой потерпел неудачу в корпорации Bell Laboratories. По прекращении проекта Multics, сотрудники центра Bell Laboratories Computer Sciences Research Center прекратили работу и так и не создали дееспособ ной диалоговой операционной системы. Летом 1969 года программисты корпорации Bell Labs разработали проект файловой системы, которая в конце концов была вклю чена в операционную систему Unix. Томпсон осуществил реализацию операционной системы для реально не используемой платформы PDP-7. В 1971 году операционная система Unix была перенесена на платформу PDP-11, а в 1973 году переписана с ис пользованием языка программирования С, что было беспрецедентным шагом в то время, но этот шаг стал основой для будущей переносимости. Первая версия опера ционной системы Unix, которая использовалась вне стен Bell Labs, называлась Unix System версии 6, ее обычно называют V6.

Другие компании перенесли операционную систему Unix на новые типы машин.

Версии, полученные в результате переноса, содержали улучшения, которые позже привели к появлению нескольких разновидностей этой операционной системы, В 1977 году корпорация Bell Labs выпустила комбинацию этих вариантов в виде одной операционной системы Unix System III, а в 1982 году корпорация AT&T пред ставила версию System V1.

Простота устройства операционной системы Unix, а также тот факт, что эта система распространялась вместе со своим исходным кодом, привели к тому, что дальнейшие разработки начали проводиться в других организациях. Наиболее важ ным среди таких разработчиков был Калифорнийский университет в городе Беркли (University of California at Berkeley).

Как насчет версии System IV? Ходят слухи, чтo это внутренняя экспериментальная версия.

Варианты операционной системы Unix из Беркли именовались Berkeley Software Distributions (BSD). Первая версия операционной системы Unix, разработанная в Беркли в 1981 году, называлась 3BSD. Следом за ней появились выпуски серии 4BSD:

4.0BSD, 4.1BSD, 4.2BSD и 4.3BSD. В этих версиях операционной системы Unix была добавлена виртуальная память, замещение страниц по требованию (demand paging) и стек протоколов TCP/IP. Последней официальной версией ОС Unix из Беркли была 4.4BSD, выпущенная в 1993 году, которая содержала переписанную систему управления виртуальной памятью. Сейчас разработка линии BSD продолжается в операционных системах Darwin, Dragonfly BSD, FreeBSD, NetBSD и OpenBSD.

В 1980-1990-х годах многие компании, разработчики рабочих станций и серве ров, предложили свои коммерческие версии операционной системы Unix. Эти опе рационные системы обычно базировались на реализациях AT&T или Беркли и под держивали дополнительные профессиональные возможности, которые обеспечивала соответствующая аппаратная платформа. Среди таких систем были Tru64 компании Digital, HP-UX компании Hewlett Packard, AIX компании IBM, DYNIX/ptx компании Sequent, IRIX компании SGI, Solaris компании Sun.

Первоначальное элегантное устройство операционной системы Unix в соедине нии с многолетними нововведениями и улучшениями, которые за ними последо вали, сделали систему Unix мощной, устойчивой и стабильной. Очень небольшое количество характеристик ОС Unix ответственны за ее устойчивость. Во-первых, операционная система Unix проста: в то время как в некоторых операционных си стемах реалилованы тысячи системных вызовов и эти системы имеют недостаточно ясное назначение, Unix-подобные операционные системы обычно имеют только не сколько сотен системных вызовов и достаточно четкий дизайн. Во-вторых, в опера ционной системе Unix все представляется в виде файлов. Такая особенность позволяет упростить работу с данными и устройствами, а также обеспечить это посредством простых системных вызовов: open (), read (), write (), i oct l () и close (). В-тре тьи, ядро и системные утилиты Операционной системы Unix написаны на языке программирования С - это свойство делает Unix удивительно переносимой и до ступной для широкого круга разработчиков операционной системой.

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

Сегодня Unix — современная операционная система, которая поддерживает мно гозадачность, многопоточность, виртуальную память, замещение страниц по требо ванию, библиотеки совместного использования, загружаемые по требованию, и сеть TCP/IP. Многие варианты операционной системы Unix поддерживают масштабиро вание до сотен процессоров, в то время как другие варианты ОС Unix работают на миниатюрных устройствах в качестве встраиваемых систем. Хотя разработка Unix больше не является исследовательским проектом, все же продолжаются разработки (с целью получить дополнительные преимущества) с использованием возможностей Да, конечно, не все, но многое представлено в виде файла. В современных операционных систе мах, таких как Plan9 (наследник Unix), практически все представляется в виде файлом.

24 Глава операционной системы Unix, которая при этом остается практичной операционной системой общего назначения.

Операционная система Unix обязана своим успехом простоте и элегантности по строения. В основе ее сегодняшней мощности лежат давние идеи Денниса Ритчи, Кена Томпсона и других разработчиков, обеспечившие возможность операционной системе Unix бескомпромиссно развиваться.

Потом пришел Линус: введение в Linux Операционная система Linux была разработана Линусом Торвальдсом (Linus Torvalds) в 1991 году как операционная система для компьютеров, работающих на новом в то время микропроцессоре Intel 80386. Тогда Линус Торвальдс был студен том университета в Хельсинки и был крайне возмущен отсутствием мощной и в то же время свободно доступной Unix-подобной операционной системы. Операционная система DOS, продукт корпорации Microsoft, была для Торвальдса полезна только лишь, чтобы поиграть в игрушку "Принц Персии", и не для чего больше. Линус поль зовался операционной системой Minix, недорогой Unix-подобной операционной си стемой, которая была создана в качестве учебного пособия. В этой операционной системе ему не нравилось отсутствие возможности легко вносить и распространять изменения исходного кода (это запрещалось лицензией ОС Minix), а также техниче ские решения, которые использовал автор ОС Minix.

Поставленный перед такой проблемой, Линус решил написать свою операци онную систему. Начал он с написания простого эмулятора терминала, который он подключал к большим Unix-системам в университете. Его эмулятор терминала по степенно рос, развивался и улучшался. Постепенно у Линуса появилась еще не со всем зрелая, но полноценная Unix-система. В 1991 голу он опубликовал в Интернет ее первую версию.

По некоторым неясным причинам, использование операционной системы Linux и количество ее пользователей начали стремительно расти. Более важным для успе ха Linux стало то, что эта операционная система привлекла многих разработчиков, которые начали изменять, исправлять и улучшать код. Благодаря соответствующему лицензионному соглашению, ОС Linux быстро стала совместным проектом, который разрабатывается многими людьми.

Сейчас Linux — это развитая операционная система, работающая на аппаратных платформах AMD х86-64, ARM, Compaq Alpha, CRIS, DEC VAX, H8/300, Hitachi SuperH, HP PA-RISC, IBM S/390, Incel IA-64, MIPS, Motorola 68000, PowerPC, SPARC, UltraSPARC и v850. Она работает в различных системах, как размером с часы, так и на больших супер-компьютерных кластерах. Сегодня коммерческий интерес к опера ционной системе Linux достаточно высок. Как новые корпорации, ориентирующиеся исключительно на Linux {Monta Vista или Red Hal), так и старые (IBM, Novell) пред лагают решения на основе этой ОС для встраиваемых систем, десктопов и серверов.

Операционная система Linux является клоном Unix, по ОС Linux— это не Unix. Хотя в ОС Linux позаимствовано много идей от Unix, в Linux реализован API ОС Unix (как это определено в стандарте POSIX и спецификации Single Unix Specification), все же система Linux не является производной от исходного кода Unix, как это имеет место для других Unix-систем, Там, где это желательно, были сделаны отклонения от пути, по которому шли другие разработчики, однако это не Введение в ядро Linux подрывает основные принципы построения операционной системы Unix и не нару шает программные интерфейсы.

Одна из наиболее интересных особенностей операционной системы Linux — то, что это не коммерческий продукт

;

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

В частности, ядро Linux выпускается под лицензией GNU General Public License (GPL) версии 2.0. В результате каждый имеет право загружать исходный код и вно сить в него любые изменения. Единственная оговорка — любое распространение внесенных вами изменений должно производиться на тех же условиях, которыми пользовались вы при получении исходного кода, включая доступность самого исхо дного программного кода4.

Операционная система Linux предоставляет много возможностей для многих людей. Основными частями системы являются ядро, библиотека функций языка С, компилятор, набор инструментов, основные системные утилиты, такие как про грамма для входа в систему (login) и обработчик команд пользователя (shell). В опе рационную систему Linux может быть включена современная реализация системы X Windows, включая полно-функциональную среду офисных приложений (desktop environment), такую как, например, GNOME. Для ОС Linux существуют тысячи сво бодных и коммерческих программ. В этой книге под понятием Linux, в основном, имеется в виду ядро Linux. Там, где это может привести к неопределенностям, будет указано, что имеется в виду под понятием Linux — вся система или только ядро.

Строго говоря, термин Linux относится только к ядру.

Обзор операционных систем и ядер Из-за неуклонного роста возможностей и не очень качественного построения не которых современных операционных систем, понятие операционной системы стало несколько неопределенным. Многие пользователи считают, что то, что они видят на экране, — и есть операционная система. Обычно, и в этой книге тоже, под операци онной системой понимается часть компьютерной системы, которая отвечает за основ ные функции использования и администрирования. Это включает в себя ядро и драй веры устройств, системный загрузчик (boot loader), командный процессор и другие интерфейсы пользователя, а также базовую файловую систему и системные утилиты.

В общем, только необходимые компоненты. Термин система обозначает операционную систему и все пользовательские программы, которые работают под ее управлением.

Конечно, основной темой этой книги будет ядро операционной системы.

Интерфейс пользователя — это внешняя часть операционной системы, а ядро — вну Для тех, кому интересно, дискуссии по поводу отличия свободного кода от открытого доступна в Интернет по адресам http://www.fsf.org и http://www.opensource.org.

Вероятно, вам нужно прочесть лицензию GNU GPL, если вы еще не читали ее. В файле COPYING, в исходном коде ядра, есть копия этой лицензии. В Интернет лицензия доступна по адресу http://www.fsf.org.

26 Глава тренняя. В своей основе ядро — это программное обеспечение, которое предоставля ет базовые функции для всех остальных частей операционной системы, занимается управлением аппаратурой и распределяет системные ресурсы. Ядро часто называют основной частью (core) или контроллером операционной системы. Типичные компо ненты ядра — обработчики прерываний, которые обслуживают запросы на преры вания, планировщик, который распределяет процессорное время между многими процессами, система управления памятью, которая управляет адресным простран ством процессов, и системные службы, такие как сетевая подсистема и подсистема межпроцессного взаимодействия. В современных системах с устройствами управле ния защищенной памятью ядро обычно занимает привилегированное положение по отношению к пользовательским программам. Это включает доступ ко всем областям защищенной памяти и полный доступ к аппаратному обеспечению. Состояние систе мы, в котором находится ядро, и область памяти, в которой находится ядро, вместе называются пространством ядра (или режимом ядра, kernel-space). Соответственно, пользовательские программы выполняются в пространствах задач (пользовательский режим, режим задач, user-space). Пользовательским программам доступно лишь не которое подмножество машинных ресурсов, они не могут выполнять некоторые системные функции, напрямую обращаться к аппаратуре и делать другие недозво ленные вещи. При выполнении программного кода ядра система находится в про странстве (режиме) ядра, в отличие от нормального выполнения пользовательских программ, которое происходит в режиме задачи.

Прикладные программы, работающие в системе, взаимодействуют с ядром с по мощью интерфейса системных вызовов (system call) (рис. 1.1). Прикладная программа обычно вызывает функции различных библиотек, например библиотеки функций язы ка С, которые, в свою очередь, обращаются к интерфейсу системных вызовов для того, чтобы отдать приказ ядру выполнить определенные действия от их имени.

Некоторые библиотечные вызовы предоставляют функции, для которых отсутствует системный вызов, и поэтому обращение к ядру — это только один этап в более слож ной функции. Давайте рассмотрим всем известную функцию pri ntf (). Эта функции обеспечивает форматирование и буферизацию данных и лишь после этого один раз обращается к системному вызову write () для вывода данных на консоль. Некоторые библиотечные функции соответствуют функциям ядра один к одному. Например, биб лиотечная функция open () не делает ничего, кроме выполнения системного вызова open (). В то же время некоторые библиотечные функции, как, например, strcpy (), надо полагать, вообще не используют обращения к ядру. Когда прикладная программа выполняет системный вызов, то говорят, что ядро выполняет работу от имени приклад ной программы. Более того, говорят, что прикладная программа выполняет системный вызов в пространстве ядра, а ядро выполняется в контексте процесса. Такой тип взаи модействия, когда прикладная программа входит в ядро через интерфейс системных вызовов, является фундаментальным способом выполнения задач.

В функции ядра входит также управление системным аппаратным обеспечением.

Практически все платформы, включая те, на которых работает операционная си стема Linux, используют прерывания (interrupt). Когда аппаратному устройству необ ходимо как-то взаимодействовать с системой, оно генерирует прерывание, которое прерывает работу ядра в асинхронном режиме5.

Иными словами, заранее неизвестно, в какой момент времени это событие произойдет и в каком состоянии будет система в этот момент времени. - Прим*. перев.

Введение в ядро Linux Прикладная Прикладная Прикладная программа 1 программа программа Пространство задачи Интерфейс системных вызовов Пространство ядра Подсистемы ядра Драйверы устройств Аппаратное обеспечение Рис. 1.1. Взаимодействие между прикладными программами, ядром и аппа ратным обеспечением..

Обычно каждому типу прерываний соответствует номер. Ядро использует номер прерывания для выполнения специального обработчика прерывания (interrupt han dler), который обрабатывает прерывание и отправляет на него ответ. Например, при вводе символа с клавиатуры, контроллер клавиатуры генерирует прерывание, чтобы дать знать системе, что в буфере клавиатуры есть новые данные. Ядро определяет номер прерывания, которое пришло в систему и выполняет соответствующий обра ботчик прерывания. Обработчик прерывания обрабатывает данные, поступившие с клавиатуры, и даст знать контроллеру клавиатуры, что ядро готово для приема но вых данных. Для обеспечения синхронизации выполнения ядро обычно может за прещать прерывания: или все прерывания, или только прерывание с определенным номером. Во многих операционных системах обработчики прерываний не выполня ются в контексте процессов. Они выполняются в специальном контексте прерывания (interrup context), который не связан ни с одним процессом. Этот специальный кон текст существует то только для того, чтобы дать обработчику прерывания возможность быстро отреагировать на прерывание и закончить работу.

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

• Работа от имени определенного процесса в режиме ядра в контексте процесса.

28 Глава • Работа по обработке прерывания в режиме ядра в контексте прерывания, не связанном с процессами.

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

Ядро Linux в сравнении с классическими ядрами Unix Благодаря общему происхождению и одинаковому API, современные ядра Unix имеют некоторые общие характерные черты. За небольшими исключениями ядра Unix представляют собой монолитные статические бинарные файлы. Это значит, что они существуют в виде больших исполняемых образов, которые выполняются один раз и используют одну копию адресного пространства. Для работы операци онной системы Unix обычно требуется система с контроллером управления стра ничной адресацией памяти (memory management unit)

;

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

Сравнение решений на основе монолитного ядра и микроядра Операционные системы, в соответствии с особенностями построения, можно разделить на две большие группы: с монолитным ядром и с микроядром. (Есть еще третий тип — экзоядро, кото рое пока еще используется, в основном, только в исследовательских операционных системах, но уже начинает пробивать дорогу в большой мир.) Монолитное ядро является самым простым, и до 1980-х годов все ядра строились именно та ким образом. Монолитное ядро реализовано в виде одного большого процесса, который вы полняется в одном адресном пространстве, Такие ядра обычно хранятся на диске в виде одного большого статического бинарного файла. Все службы ядра существуют и выполняются в одном большом адресном пространстве ядра. Взаимодействия в ядре выполняются очень просто, по тому что все, что выполняется в режиме ядра, — выполняется в одном адресном пространстве.

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

Сторонники такой модели обычно указывают на простоту и высокую производительность моно литных ядер.

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

Механизм межпроцессного взаимодействия (Inter Process Comrrmnication, IPC) встраивается в систему, и различные серверы взаимодействуют между собой и обращаются к "службам" друг друга путем отправки сообщений через механизм IPC. Разделение серверов позволяет предот вратить возможность выхода из строя одного сервера при выходе из строя другого.

Кроме того, модульность системы позволяет одному серверу вытеснять из памяти другого.

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

Введение в ядро Linux В современных операционных системах с микроядром, большинство серверов выполняется в пространстве ядра, чтобы избавиться от накладных расходов, связанных с переключением контекста, кроме того, это дает потенциальную возможность прямого вызова функций. Ядро операционной системы Windows NT, а также ядро Mach (на котором базируется часть опера ционной системы Mac OS X) - это примеры микроядер. В последних версиях как Windows NT, так и Mac OS X все серверы выполняются только в пространстве ядра, что является отходом от первоначальной концепции микроядра.

Ядро ОС Linux монолитное, т.е. оно выполняется в одном адресном пространстве, в режиме ядра. Тем не менее ядро Linux позаимствовало некоторые хорошие свойства микроядерной модели: в нем используется преемптивное ядро, поддерживаются потоки пространства ядра и возможность динамической загрузки в ядро внешних бинарных файлов (модулей ядра). Ядро Linux не использует никаких функций микроядерной модели, которые приводят к снижению производительности: все выполняется в режиме ядра с непосредственным вызовом функций, вместо передачи сообщений. Следовательно, операционная система Linux — модульная, много поточная, а выполнение самого ядра можно планировать.

Прагматизм снова победил.

По мере того как Линус и другие разработчики вносили свой вклад в ядро Linux, они принимали решения о том, как развивать ОС Linux без пренебрежения корнями, связанными с Unix (и, что более важно, без пренебрежения API ОС Unix). Поскольку операционная система Linux не базируется на какой-либо версии ОС Unix, Линус и компания имели возможность найти и выбрать наилучшее решение для любой проб лемы и даже со временем изобрести новые решения! Ниже приводится анализ харак теристик ядра Linux, которые отличают его от других разновидностей Unix.

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

• Ядро Linux поддерживает симметричную многопроцессорную обработку (SMP).

Хотя большинство коммерческих вариантов операционной системы Unix сей час поддерживает SMP, большинство традиционных реализаций ОС Unix такой поддержки не имеет.

• Ядро Linux является преемптивным. В отличие от традиционных вариантов ОС Unix, ядро Linux в состоянии вытеснить выполняющееся задание, даже если это задание работает в режиме ядра. Среди коммерческих реализаций ОС Unix преемптивное ядро имеют только операционные системы Solaris и IRIX.

• В ядре Linux используется интересный подход для поддержки многопоточно сти (threads): потоки ни чем не отличаются от обычных процессов. С точки зрения ядра все процессы одинаковы, просто некоторые из них имеют общие ресурсы.

• В ядре Linux отсутствуют некоторые функции ОС Unix, которые считаются плохо реализованными, как, например, поддержка интерфейса STREAMS, или отвечают "глупым" стандартам.

• Ядро Linux является полностью открытым во всех смыслах этого слова. Набор функций, реализованных в ядре Linux, — это результат свободной и откры той модели разработки операционной системы Linux. Если какая-либо функ ция ядра считается маловажной или некачественной, то разработчики ядра 30 Глава не обязаны ее реализовать. В противоположность этому, внесение изменений при разработке ядра Linux занимает "элитарную" позицию: изменения должны решать определенную практическую задачу, должны быть логичными и иметь понятную четкую реализацию. Следовательно, функции некоторых современ ных вариантов ОС Unix, такие как память ядра со страничной реализацией, не были реализованы. Несмотря на имеющиеся различия, Linux является опера ционной системой со строгим наследованием традиций ОС Unix.

Версии ядра Linux Ядро Linux поставляется в двух вариантах-: стабильном (stable) и разрабатывае мом (development). Версии стабильного ядра - это выпуски продукции промыш ленного уровня, которая готова для широкого использования. Новые стабильные версии ядра обычно выпускаются для исправления ошибок и для предоставления новых драйверов устройств. Разрабатываемые версии ядра, наоборот, подвержены быстрым изменениям. По мере того как разработчики экспериментируют с новыми решениями, часто вносятся радикальные изменения в ядро.

Ядра Linux стабильных и разрабатываемых версий можно отличить друг от друга с помощью простой схемы присваивания имен (рис. 1.2.). Три числа, которые раз деляются точкой, определяют версию ядра. Первое число - значение старшей (ma jor) версии, второе - значение младшей (minor), третье число - значение редакции (выпуска, revision). Значение младшей версии также определяет, является ли ядро стабильным или разрабатываемым

;

если это значение четное, то ядро стабильное, а если нечетное, то разрабатываемое. Так, например, версия 2.6.0 определяет стабиль ное ядро. Ядро имеет старшую версию 2, младшую версию 6 и редакцию 0. Первые два числа также определяют "серию ядер", в данном случае серия ядер — 2.6.

Младшая версия равна (это стабильное ядро) Старшая версия равна 2 Номер выпуска равен О 2.6. Рис. 1.2. Соглашение о присваивании имен ядрам Разработка ядра соответствует различным фазам. Вначале разработчики ядра ра ботают над новыми функциями, что напоминает хаос. Через определенное время ядро оказывается сформировавшимся, и в конце концов объявляется замораживание функций.

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

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

Введение в ядро Linux Все это не правда По крайней мере - не совсем. Приведенное только что описание процесса разработки ядра технически правильное. Раньше процесс происходил именно так, как описано. Тем не менее летом 2004 года на ежегодном саммите для приглашенных разработчиков ядра Linux было при нято решение продолжить разработку серии 2.6 ядра Linux и в ближайшем будущем не пере ходить на серию разрабатываемого ядра 2.7. Такое решение было принято потому, что ядро 2. получилось хорошим

;

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

Кроме того, и, возможно, это главное — существующая система поддержки, которая обеспечи вается Линусом Тораальдсом и Эндрю Мортоном, работает чрезвычайно хорошо. Разработчики ядра уверены, что процесс разработки может продолжаться таким образом, что серия ядер 2. будет оставаться стабильной и в ней будут появляться новые возможности. Время рассудит, но уже сейчас результаты выглядят хорошо.

Эта книга базируется на ядрах стабильной серии 2.6.

Сообщество разработчиков ядра Linux Когда вы начинаете разрабатывать код ядра Linux, вы становитесь частью гло бального сообщества разработчиков ядра Linux. Главный форум этого сообщества — список рассылки разработчиков ядра Linux (linux-kernel mailing list). Информация по по воду подписки на этот форум доступна по адресу http: //vger. kernel. org. Следует заметить, что это достаточно перегруженный сообщения список рассылки (количе ство сообщений порядка 300 в день) и что другие читатели этого списка (разработ чики ядра, включая Линуса) не очень склонны заниматься ерундой. Однако этот спи сок рассылки может оказать неоценимую помощь в процессе разработки

;

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

В последних главах приведен обзор процесса разработки ядра и более полное описание того, как успешно принимать участие в деятельности сообщества разра ботчиков ядра.

Перед тем как начать Эта книга посвящена ядру Linux: как оно работает, почему оно работает и чему следует уделить внимание. Далее будут описаны принципы работы и реализация основных подсистем ядра, а также интерфейсы и программная семантика. Эта книга касается практических вопросов, и в ней используется подход на основании золотой серединки указанных выше направлений. Такой интересный подход в сочетании с анекдотами из личной практики автора и советами по хакерским приемам позволяет быть уверенным в том, что книга станет хорошим стартом.

Я надеюсь, что у читателей есть доступ к системе Linux и дереву исходного кода ядра. В идеале предполагается, что читатель— это пользователь операционной си стемы Linux, который уже "копался" в исходном программном коде, но все же нуж дается в некоторой помощи для того, чтобы все связать воедино. В принципе, чита тель может и не быть пользователем Linux, но хочет разобраться в устройстве ядра из чистого любопытства. Тем не менее, для того чтобы самому научиться писать программы — исходный код незаменим. Исходный программный код свободно досту пен— пользуйтесь им!

Удачи!

32 Глава Начальные сведения о ядре Linux этой главе будут рассмотрены основные вопросы, связанные с ядром Linux: где получить исходный код, как его компилировать и как инсталлировать новое В ядро. После этого рассмотрим некоторые допущения, связанные с ядром Linux, от личия между ядром и пользовательскими программам, а также общие методы, кото рые используются в ядре.

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

.

Получение исходного кода ядра Исходный программный код последней версии ядра всегда доступен как в виде полного архива в формате tar (tarball), так и виде инкрементной заплаты по адресу http://www.kernel.org.

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

Инсталляция исходного кода ядра Архив исходного кода ядра в формате tar распространяется в сжатых форматах GNU zip (gzip) и bzip2. Формат bzip2 наиболее предпочтителен, так как обеспечи вает больший коэффициент сжатия по сравнению с форматом gzip. Архив ядра в формате bzip2 имеет имя linux-x.у.z.tar.bz2, где х, у, z — это номер соответ ствующей версии исходного кода ядра. После загрузки исходного кода его можно декомпрессировать очень просто. Если tar-архив сжат с помощью GNU zip, то необ ходимо выполнить следующую команду.

$ tar xvzf linux-x.у.z.tar.gz Если сжатие выполнено с помощью bzip2, то команда должна иметь следующий вид.

$ tar xvjf linux-x.у.z.tar.bz Обе эти команды позволяют декомпрессировать и развернуть дерево исходных кодов ядра в каталог с именем linux-x.y.z.

Где лучше инсталлировать и изменять исходный код Исходный код ядра обычно инсталлируется в каталог /usr /sr c/l i nux. Заметим, что это де рево исходного кода нельзя использовать для разработок. Версия ядра, с которой была ском пилирована ваша библиотека С, часто связывается с этим деревом каталогов. Кроме того, чтобы вносить изменения в ядро, не обязательно иметь права пользователя root, вместо этого лучше работать в вашем домашнем каталоге и использовать права пользователя root только для инсталляции ядра. Даже при инсталляции нового ядра каталог / usr / sr c/ l i nux лучше оставлять без изменений.

Использование заплат В сообществе разработчиков ядра Linux заплаты (patch) — это основной язык об щения. Вы будете распространять ваши изменения исходного кода ядра в виде заплат и получать изменения кода от других разработчиков тоже в виде заплат. При дан ном рассмотрении наиболее важными являются инкрементные заплаты (incremental patch), которые позволяют перейти от одной версии ядра к другой. Вместо того что бы загружать большой архив ядра, можно просто применить инкрементную заплату и перейти от имеющейся версии к следующей. Это позволяет сэкономить время и пропускную способность каналов связи. Для того чтобы применить инкрементную заплату, находясь в каталоге дерева исходных кодов ядра, нужно просто выполнить следующую команду.

$ patch -p1 <../patch-х.у.z Обычно заплата для перехода на некоторую версию ядра должна применяться к предыдущей версии ядра.

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

Дерево исходных кодов ядра Дерево исходных кодов ядра содержит ряд каталогов, большинство из которых также содержит подкаталоги. Каталоги, которые находятся в корне дерева исходных кодов, и их описание приведены в табл. 2.1.

Некоторые файлы, которые находятся в корне дерева исходных кодов, также заслуживают внимания. Файл COPYING — это лицензия ядра (GNU GPL v2). Файл CREDITS — это список разработчиков, которые внесли большой вклад в разработку ядра. Файл MAINTAINERS — список людей, которые занимаются поддержкой подси стем и драйверов ядра. И наконец, Makefile — это основной сборочный файл ядра.

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

34 Глава Таблица 2.1. Каталоги в корне дерева исходных кодов ядра Каталог Описание arch Специфичный для аппаратной платформы исходный код crypto Криптографический API Documentation Документация исходного кода ядра drivers Драйверы устройств fs Подсистема VFS и отдельные файловые системы include Заголовочные файлы ядра init Загрузка и инициалиэация ядра ipc Код межпроцессного взаимодействия kernel Основные подсистемы, такие как планировщик lib Вспомогательные подпрограммы mm Подсистема управления памятью и поддержка виртуальной памяти net Сетевая подсистема scripts Сценарии компиляции ядра security Модуль безопасности Linux sound Звуковая подсистема usr Начальный код пространства пользователя (initramfs) Так как доступен исходный код ядра Linux, то, это означает, что есть возможность сконфигурировать ядро перед компиляцией. Есть возможность скомпилировать под держку только необходимых драйверов и функций. Конфигурация ядра— необходи мый этап перед тем, как его компилировать. Поскольку в ядре бесчисленное количе ство функций и вариантов поддерживаемого аппаратного обеспечения, возможностей по конфигурации, мягко говоря, много. Конфигурация управляется с помощью опций конфигурации в виде CONFIG_FEATURE. Например, поддержка симметричной мно гопроцессорной обработки (Symmetric multiprocessing, SMP) устанавливается с по мощью опции CONFIG SMP. Если этот параметр установлен, то поддержка функций SMP включена. Если этот параметр не установлен, то функции поддержки SMP от ключены. Все конфигурационные параметры хранятся в файле.config в корневом каталоге дерева исходного кода ядра и устанавливаются одной из конфигурацион ных программ, например, с помощью команды make xconfig. Конфигурационные параметры используются как для определения того, какие файлы должны быть ском пилированы во время сборки ядра, так и для управления процессом компиляции че рез директивы препроцессора.

Конфигурационные переменные бывают двух видов: логические (boolean) и пере менные с тремя состояниями (instate). Логические переменные могут принимать значения yes и по. Такие переменные конфигурации ядра, как CONFIG_PREEMPT, обычно являются логическими. Конфигурационная переменная с тремя состояния ми может принимать значения yes, no и module. Значение module отвечает кон фигурационному параметру, который установлен, но соответствующий код должен компилироваться как модуль (т.е. как отдельный объект, который загружается дина мически). Драйверы устройств обычно представляются конфигурационными пере менными с тремя состояниями.

Начальные сведения о ядре Linux Конфигурационные параметры могут иметь целочисленный, или строковый, тип.

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

Ядра, которые включаются в поставки ОС Linux такими производителями, как Novell и Redhat, компилируются как часть дистрибутива. В таких ядрах обычно име ется большой набор различных функций и практически полный набор всех драй веров устройств в виде загружаемых модулей. Это позволяет получить хорошее базовое ядро и поддержку широкого диапазона оборудования. К сожалению, как раз работчикам ядра, вам потребуется компилировать свои ядра и самим разбираться, какие модули включать, а какие нет.

В ядре поддерживается несколько инструментов, которые позволяют выполнять конфигурацию. Наиболее простой инструмент — это текстовая утилита командной строки:

• make config Эта утилита просматривает все параметры один за другим и интерактивно запра шивает у пользователя, какое значение соответствующего параметра установить — yes, no или module {для переменной с тремя состояниями). Эта операция требует длительного времени, и если у вас не почасовая оплата, то лучше использовать утили ту на основе интерфейса ncurses:

make menuconfig или графическую утилиту на основе системы X11:

make xconfig или еще более удобную графическую утилиту, основанную на библиотеке gtk+ make gconfig Эти утилиты позволяют разделить все параметры по категориям, таким как Processor Features (Свойства процессора) и Network Devices (Сетевые устройства).

Пользователи могут перемещаться по категориям и, конечно, изменять значения конфигурационных параметров. Команда $ make defconfig позволяет создать конфигурационный файл, который будет содержать параметры, используемые по умолчанию для текущей аппаратной платформы. Хотя эти параме тры и достаточно общие (ходят слухи, что для аппаратной платформы i386 исполь зуется конфигурация Линуса), они являются хорошей стартовой точкой, если вы никогда перед этим не занимались конфигурацией ядра. Чтобы все сделать быстро, необходимо выполнить эту команду, а потом проверить, включена ли поддержка всех нужных аппаратных устройств.

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

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

make В отличие от предыдущих серий ядер, в версии 2.6 больше нет необходимости выполнять команду make dep перед сборкой ядра, так как создание дерева зависи мостей выполняется автоматически. Также не нужно указывать цель сборки, напри мер bzlmage, как это было необходимо для более ранних версий. Правило, записан ное в файле с именем Makefile, которое используется по умолчанию, в состоянии обработать все!

Уменьшение количества выводимых сообщений Для того чтобы уменьшить шум, связанный с сообщениями, которые выдаются во время сборки, но в то же время видеть предупреждения и сообщения об ошибках, можно использовать такую хитрость, как перенаправление стандартного вывода ко манды make (1):

make > "имя_некоторого_файла" Если вдруг окажется необходимым просмотреть выводимые сообщения, можно воспользоваться соответствующим файлом. Но обычно, если предупреждения или сообщения об ошибках выводятся на экран, в этом нет необходимости.

На самом деле я выполняю следующую команду make > /dev/null, что позволяет совсем избавиться от ненужных сообщений.

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

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

$ make -jn где n — количество заданий, которые необходимо запустить.

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

$ make -j Используя такие отличные утилиты, как di stcc (1) и ccache(l ), можно еще бо лее существенно уменьшить время компиляции ядра.

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

Например, для платформы x86, при использовании системного загрузчика grub можно скопировать загружаемый образ ядра из файла arch/i386/boot/bzlmage в каталог /boot и отредактировать файл /etc/grub/grub.conf для указания записи, которая соответствует новому ядру. В системах, где для загрузки используется загруз чик LILO, необходимо соответственно отредактировать файл /etc/l i l o. conf и за пустить утилиту l i l o (8).

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

$ make modules_install В процессе компиляции в корневом каталоге дерева исходного кода ядра также создается файл System.map. В этом файле содержится таблица соответствия симво лов ядра их начальным адресам в памяти. Эта таблица используется при отладке для перевода адресов памяти в имена функций и переменных.

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

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

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

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

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

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

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

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

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

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

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

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

Заголовочные файлы Заметим, что упомянутые заголовочные файлы и заголовочные файлы, которые будут упоми наться далее в этой книге, принадлежат дереву исходного кода ядра. В файлах исходного кода ядра нельзя подключать заголовочные файлы извне этого дерева каталогов, так же как и нельзя использовать внешние библиотеки, Отсутствует наиболее известная функция pr i nt f ( ). Ядро не имеет доступа к функции pr i nt f (), однако ему доступна функция pri nt k (). Функция pri ntk() ко пирует форматированную строку в буфер системных сообщений ядра (kernel log buf fer), который обычно читается с помощью программы syslog. Использование этой функции аналогично использованию pr i nt f ():

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

;

Одно важное отличие между pri nt f () и pri nt k () состоит в том, что в функции pr i nt k () можно использовать флаг уровня вывода. Этот флаг используется про граммой syslog для того, чтобы определить, нужно ли показывать сообщение ядра.

Вот пример использования уровня вывода:

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

;

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

Компилятор GNU С Как и все "уважающие себя" ядра Unix, ядро Linux написано на языке С. Может быть, это покажется неожиданным, но ядро Linux написано не на чистом языке С в стандарте ANSI С. Наоборот, где это возможно, разработчики ядра используют раз Начальные сведения о ядре Linux личные расширения языка, которые доступны с помощью средств компиляции gcc (GNU Compiler Collection — коллекция компиляторов GNU, в которой содержится компилятор С, используемый для компиляции ядра).

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

Функции с подстановкой тела Компилятор GNU С поддерживает функции с подстановкой тела (inline functions).

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

Функции с подстановкой тела объявляются с помощью ключевых слов stati c и inline в декларации функции. Например, static inline void dog(unsigned long tail_size)

;

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

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

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

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

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

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

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

Компилятор использует эти директивы для соответственной оптимизации кода.

В ядре эти директивы заключаются в макросы l i kel y() и unl i kel y(), которые легко использовать. Например, если используется оператор if следующего вида:

if (foo) { /*..*/ } то для того, чтобы отметить этот путь выполнения как маловероятный, необходимо указать:

/* предполагается, что значение переменной foo равно нулю..*/ if (unllkely(ffoo)) { /*..*/ } И наоборот, чтобы отметить этот путь выполнения как наиболее вероятный /* предполагается, что значение переменной foo не равно нулю..*/ if (likely(foo)) { /*..*/ } Эти директивы необходимо использовать только в случае, когда направление вет вления с большой вероятностью известно априори или когда необходима оптимиза ция какой-либо части кода за счет другой части. Важно помнить, что эти директивы дают увеличение производительности, когда направление ветвления предсказано правильно, однако приводят к потере производительности при неправильном пред сказании. Наиболее часто директивы unl i kel y () и l i kel y () используются для проверки ошибок.

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

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

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

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

;

ника ких вычислений с плавающей точкой в режиме ядра.

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

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

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

Синхронизация и параллелизм Ядро подвержено состояниям конкуренции за ресурсы (race condition). В отли чие от однопоточной пользовательской программы, ряд свойств ядра позволяет осу ществлять параллельные обращения к ресурсам общего доступа, и поэтому требуется выполнять синхронизацию для предотвращения состояний конкуренции за ресурсы.

В частности, возможны следующие ситуации.

42 Глава • Ядро Linux поддерживает многопроцессорную обработку. Поэтому, без соот ветствующей защиты, код ядра может выполняться на одном, двух или боль шем количестве процессоров и при этом одновременно обращаться к одному ресурсу.

• Прерывания возникают асинхронно по отношению к исполняемому коду.

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

• Ядро Linux является преемптивным. Поэтому, без соответствующей защиты, исполняемый код ядра может быть вытеснен в пользу другого кода ядра, кото рый тоже может обращаться к некоторому общему ресурсу.

Стандартное решение для предотвращения состояния конкуренции за ресурсы (состояния гонок) — это использование спин-блокировок и семафоров.

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

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

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

Резюме Да, ядро— это действительно нечто иное: отсутствует защита памяти, нет про веренной библиотеки функций языка С, маленький стек, большое дерево исходного кода. Ядро Linux играет по своим правилам и занимается серьезными вещами. Тем не менее, ядро— это всего лишь программа

;

оно, по сути, не сильно отличается от других обычных программ. Не нужно его бояться.

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

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

Начальные сведения о ядре Linux Управление процессами роцесс - одно из самых важных абстрактных понятий в Unix-подобных опе рационных системах1. По сути, процесс— это программа, т.е. объектный код, П хранящийся на каком-либо носителе информации и находящийся в состоянии ис полнения. Однако процесс — это не только исполняемый программный код, кото рый для операционной системы Unix часто называется text section (сегмент текста или сегмент кода). Процессы также включают в себя сегмент данных (data section), со держащий глобальные переменные

;

набор ресурсов, таких как открытые файлы и ожидающие на обработку сигналы

;

адресное пространство и один или более потоков выполнения. Процесс — это живой результат выполнения программного кода.

Потоки выполнения, которые часто для сокращения называют просто потоками (thread), представляют собой объекты, выполняющие определенные операции вну три процесса. Каждый поток включает в себя уникальный счетчик команд (program counter), стек выполнения и набор регистров процессора. Ядро планирует выполне ние отдельных потоков, а не процессов. В традиционных Unix-подобных операцион ных системах каждый процесс содержал только один поток. Однако в современных системах многопоточные программы используются очень широко. Как будет пока зано далее, в операционной системе Linux используется уникальная реализация по токов — между процессами и потоками нет никакой разницы. Поток в операционной системе Linux — это специальный тип процесса.

В современных операционных системах процессы предусматривают наличие двух виртуальных ресурсов: виртуального процессора и виртуальной памяти. Виртуальный процессор создает для процесса иллюзию, что этот процесс монопольно использу ет всю компьютерную систему, за исключением, может быть, только того, что фи зическим процессором совместно пользуются десятки других процессов. В главе 4, "Планирование выполнения процессов", эта виртуализация обсуждается более под робно. Виртуальная память предоставляет процессу иллюзию того, что он один рас полагает всей памятью компьютерной системы. Виртуальной памяти посвящена гла ва 11, "Управление памятью". Потоки совместно используют одну и ту же виртуальную память, хотя каждый поток получает свой виртуальный процессор.

Другая абстракция — это файл.

Следует подчеркнуть, что сама по себе программа процессом не является

;

про цесс — это выполняющаяся программа плюс набор соответствующих ресурсов.

Конечно, может существовать два процесса, которые исполняют одну и ту же про грамму. В действительности может даже существовать два или больше процессов, которые совместно используют одни и те же ресурсы, такие как открытые файлы, или адресное пространство. Процесс начинает свое существование с момента соз дания, что впрочем не удивительно. В операционной системе Linux такое создание выполняется с помощью системного вызова fork() (буквально, ветвление или вил ка), который создает новый процесс путем полного копирования уже существую щего. Процесс, который вызвал системную функцию fork (), называется порожда ющим (родительским, pannt), новый процесс именуют порожденным (дочерний, child).

Родительский процесс после этого продолжает выполнение, а порожденный процесс начинает выполняться с места возврата из системного вызова. Часто после развет вления в одном из процессов желательно выполнить какую-нибудь другую програм му. Семейство функций exec*() позволяет создать новое адресное пространство и загрузить в него новую программу. В современных ядрах Linux функция fork() ре ализована через системный вызов clone(), который будет рассмотрен в следующем разделе.

Выход из программы осуществляется с помощью системного вызова exit(). Эта функция завершает процесс и освобождает все занятые им ресурсы. Родительский процесс может запросить о состоянии порожденных им процессов с помощью си стемного вызова wait4()2, который заставляет один процесс ожидать завершения другого. Когда процесс завершается, он переходит в специальное состояние зомби (zombie), которое используется для представления завершенного процесса до того мо мента, пока порождающий его процесс не вызовет системную функцию wait() или waitpid().

Иное название для процесса — задание или задача (task). О процессах в ядре опе рационной системы Linux говорят как о задачах. В этой книге оба понятия взаимо заменяемы, хотя по возможности для представления работающей программы в ядре будет использоваться термин задача, а для представления в режиме пользователя — термин процесс.

Дескриптор процесса и структура task structure Ядро хранит информацию о всех процессах в двухсвязном списке, который на зывается task list3 (список задач). Каждый элемент этого списка является дескрипто ром процесса и имеет тип структуры struct task_struct, которая описана в файле include/linux/sched.h. Дескриптор процесса содержит всю информацию об определенном процессе.

В ядре реализован системный вызов wai t 4(). В операционной системе Linux через библиотеку функций языка С доступны функции wai t (), wai t pi d(), wai t3() и wai t 4(). Все эти функции возвращают информацию о состоянии завершившегося процесса, хотя в несколько разной семан тике.

Иногда в литературе по построению операционных систем этот список называется task array (массив задач). Поскольку в ядре Linux используется связанный список, а не статический массив, его называют task l i s t.

i 46 Глава Структура task_struct — достаточно большая структура данных размером по рядка 1,7 Кбайт на 32-разрядной машине. Однако этот размер не такой уж большой, учитывая, что в данной структуре содержится вся информация о процессе, которая необходима ядру. Дескриптор процесса содержит данные, которые описывают вы полняющуюся программу, — открытые файлы, адресное пространство процесса, ожи дающие на обработку сигналы, состояние процесса и многое другое (рис. 3.1).

Выделение дескриптора процесса Память для структуры task_struct выделяется с помощью подсистемы выделе ния памяти, которая называется слябовый распределитель (slab allocator), для возмож ности повторного использования объектов и раскрашивания кэша (cache coloring) (см. главу 11, "Управление памятью"). В ядрах до серии 2.6 структура task_struct хранилась в конце стека ядра каждого процесса. Это позволяет для аппаратных плат форм, у которых достаточно мало регистров процессора (как, например, платформа х86), вычислять местоположение дескриптора процесса, только зная значение ре гистра указателя стека (stack pointer), без использования дополнительных регистров для хранения самого адреса этого местоположения. Так как теперь дескриптор про цесса создается с помощью слябового распределителя, была введена новая структура thread_info, которая хранится в области дна стека (для платформ, у которых стек растет в сторону уменьшения значения адреса памяти) или в области вершины стека (для платформ, у которых стек растет в сторону увеличения значения адреса памя ти)4 (рис. 3.2.).

struct task struct struct task_struct struct task_struct struct task struct unsigned long state

;

int prio

;

unsigned long policy

;

struct task_struct *parent

;

struct list_head tasks

;

pid_t pid

;

Дескриптор процесса Список задач (task list) Рис. З.1. Дескриптор процесса и список задач Причиной создания структуры thread_info было не только наличие аппаратных платформ, обед ненных регистрами процессора, но и то, что положение этой структуры позволяет достаточно просто рассчитыпать смешения адресов для значений ее нолей при использовании языка ассем блера.

Управление процессами Стек ядра процесса Наибольшее значение адреса памяти Начало стека Указатель стека Структура struct thread_infо current_thread_infо() Наименьшее значение адреса Структура thread_infо содержит указатель на дескриптор процесса Структура struct task_struct процесса Рис 3.2. Дескриптор процесса и стек ядра Структура struct thread_info для платформы х86 определена в файле в следующем виде.

struct thread_info { struct task_struct *task

;

struct exec_domain *exec_domain

;

unsigned long flags

;

unsigned long status

;

u32 cpu

;

s32 preempt_count

;

mm_segment_t addr_limit

;

struct restart_block restart_block

;

unsigned long previous_esp

;

u8 supervisorytack[0]

;

}

;

Для каждой задачи ее структура thread_info хранится в конце стека ядра этой задачи. Элемент структуры thread_info с именем task является указателем на структуру task_struct этой задачи.

Хранение дескриптора процесса Система идентифицирует процессы с помощью уникального значения, которое называется идентификатором процесса (process identification, PID). Идентификатор PID — это целое число, представленное с помощью скрытого типа pid_t5, который обыч но соответствует знаковому целому— int.

Скрытый тип (opaque type) — это тип данных, физическое представление которого неизвестно или не существенно.

48 Глава Однако, для обратной совместимости со старыми версиями ОС Unix и Linux мак симальное значение этого параметра по умолчанию составляет всего лишь (что соответствует типу данных short int). Ядро хранит значение данного парамет ра в поле pid дескриптора процесса.

Это максимальное значение является важным, потому что оно определяет мак симальное количество процессов, которые одновременно могут существовать в си стеме. Хотя значения 32768 и достаточно для офисного компьютера, для больших серверов может потребоваться значительно больше процессов. Чем меньше это зна чение, тем скорее нумерация процессов будет начинаться сначала, что приводит к нарушению полезного свойства: больший номер процесса соответствует процессу, который запустился позже. Если есть желание нарушить в системе обратную совме стимость со старыми приложениями, то администратор может увеличить это макси мальное значение во время работы системы с помощью записи его в файл /ргос/ sys/kernel/pid_max.

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

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

Для платформы х86 значение параметра current вычисляется путем маскирования 13 младших бит указателя стека для получения адреса структуры thread_infо. Это мо жет быть сделано с помощью функции current_thread_info (). Соответствующий код на языке ассемблера показан ниже.

movl $-8192, %eax andl %esp, %eax Окончательно значение параметра current получается путем разыменования значения поля task полученной структуры thread_info:

current_thread_info()->task

;

Для контраста можно сравнить такой подход с используемым на платформе PowerPC (современный процессор на основе RISC-архитектуры фирмы IBM), для которого значение переменной current хранится в регистре процессора r2. На платформе РРС такой подход можно использовать, так как, в отличие от платформы х8б, здесь регистры процессора доступны в изобилии. Так как доступ к дескриптору процесса — это очень частая и важная операция, разработчики ядра для платформы РРС сочли правильным пожертвовать одним регистром для этой цели.

Управление процессами Состояние процесса Поле s t at e дескриптора процесса описывает текущее состояние процесса (рис. 3-3). Каждый процесс в системе гарантированно находится в одном из пяти различных состояний.

Существующий процесс вызывает TASK_ZOMBIE функцию fork() (процесс завершен) Планировщик отправляет задачу и создает новый на выполнение: функция schedule () процесс вызывает функцию concext_switch () Задача Задача завершается разветвляется через do exit() TASK_RUNNING TASK_RUNNING (готов, но пока (выполняется] не выполняется) Задача вытесняется более приоритетной задачей Событие произошло, задача Задача находится TASK_INTЕRRUPTIBLE возобновляет выполнение в приостановленном и помещается обратно в очередь состоянии в очереди ожиданий TASK_UNINTERRUPTTВLE готовых к выполнению задач на определенное событие (задача ожидает) Рис. 3.3. Диаграмма состояний процесса Эти состояния представляются значением одного из пяти возможных флагов, описанных ниже.

• TASK_RUNNING— процесс готов к выполнению (runnable). Иными словами, либо процесс выполняется в данный момент, либо находится в одной из оче редей процессов, ожидающих на выполнение (эти очереди, runqueue, обсуж даются в главе 4. "Планирование выполнения процессов").

• TASK_INTERRUPTIBLE — процесс приостановлен (находится в состоянии ожидания, sleeping), т.е. заблокирован в ожидании выполнения некоторого 50 Глава условия. Когда это условие выполнится, ядро переведет процесс в состояние TASKRUNNING. Процесс также возобновляет выполнение (wake up) преждевре менно при получении им сигнала.

• TASK_UNNTERRUPTIBLE - аналогично TASK_INTERRUPTIBLE, за исключени ем того, что процесс не возобновляет выполнение при получении сигнала.

Используется в случае, когда процесс должен ожидать беспрерывно или когда ожидается, что некоторое событие может возникать достаточно часто. Так как задача в этом состоянии не отвечает на сигналы, TASK_UNINTERRUPTIBLE ис пользуется менее часто, чем TASK_INTERRUPTIBLE6.

• TASK_ZOMBIE — процесс завершен, однако порождающий его процесс еще не вызвал системный вызов wait4 (). Дескриптор такого процесса должен оста ваться доступным на случай, если родительскому процессу потребуется доступ к этому дескриптору. Когда родительский процесс вызывает функцию wait4 (), то такой дескриптор освобождается.

• TASK_STOPPED — выполнение процесса остановлено. Задача не выполняется и не имеет право выполняться. Такое может случиться, если задача получает ка кой-либо из сигналов SIGSTOP, SIGTSTP, SIGTTIN или SIGTTOU, а также если сигнал приходит в тот момент, когда процесс находится в состоянии отладки.

Манипулирование текущим состоянием процесса Исполняемому коду ядра часто необходимо изменять состояние процесса.

Наиболее предпочтительно для Этого использовать функцию set_task state(task, state)

;

/* установить задание 'task' в состояние 'state' */ которая устанавливает указанное состояние для указанной задачи. Если применимо, то эта функция также пытается применить барьер памяти (memory barrier), чтобы га рантировать доступность установленного состояния для всех процессоров (необхо димо только для SMP-систем). В других случаях это эквивалентно выражению:

task->state = state

;

Вызов set current state (state) является синонимом к вызову set_task_ state(current, state).

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

Именно из-за этого появляются наводящие ужас "неубиваемые" процессы, для которых команда ps (1) показывает значение состояния, равное D, Так как процесс не отвечает на сигналы, ему нель зя послать сигнал SIGKILL. Более того, завершать такой процесс было бы неразумно, так как этот процесс, скорее всего, выполняет какую-либо важную операцию и может удерживать семафор.

Управление процессами С этого момента говорят, что ядро "выполняется от имени процесса" и делает это в контексте процесса. В контексте процесса макрос current является действитель ным1. При выходе из режима ядра процесс продолжает выполнение в пространстве пользователя, если в это время не появляется готовый к выполнению более приори тетный процесс. В таком случае активизируется планировщик, который выбирает для выполнения более приоритетный процесс.

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

Дерево семейства процессов В операционной системе Linux существует четкая иерархия процессов. Все про цессы являются потомками процесса init, значение идентификатора PID для кото рого равно 1. Ядро запускает процесс i ni t на последнем шаге процедуры загрузки системы. Процесс i ni t, в свою очередь, читает системные файлы сценариев началь ной загрузки (initscripts) и выполняет другие программы, что в конце концов заверша ет процедуру загрузки системы.

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

Процессы, которые порождены одним и тем же родительским процессом, назы ваются родственными (siblings). Информация о взаимосвязи между процессами хра нится в дескрипторе процесса. Каждая структура task_struct содержит указатель на структуру task_struct родительского процесса, который называется parent, эта структура также имеет список порожденных процессов, который называется children. Следовательно, если известен текущий процесс (current), то для него можно определить дескриптор родительского процесса с помощью выражения:

struct task_struct *task = current->parent

;

Аналогично можно выполнить цикл по процессам, порожденным от текущего процесса, с помощью кода:

struct task_struct *task

;

struct list_head *list

;

list_for_each (list, scurrent->children) { task = list_entry(list, struct task_struct, sibling)

;

/* переменная task теперь указывает на один из процессов, порожденных текущим процессом */ } Дескриптор процесса i ni t — это статически выделенная структура данных с име нем i ni t t ask. Хороший пример использования связей между всеми процессами — это приведенный ниже код, который всегда выполняется успешно.

Отличным от контекста процесса является контекст прерывания, описанный в главе 6, "Прерыва ния и обработка прерываний". В контексте прерывания система работает не от имени процесса, а выполняет обработчик прерывания. С обработчиком прерывании не связан ни один процесс, поэтому и контекст процесса отсутствует.

52 Глава struct task_struct *task for (task = current

;

task ! = $init_task

;

task = task->parent) /* переменная task теперь указывает на процесс init */ Конечно, проходя по иерархии процессов, можно перейти от одного процесса системы к другому. Иногда, однако, желательно выполнить цикл по всем процессам системы. Такая задача решается очень просто, так как список задач — это двухсвяз ный список. Для того чтобы получить указатель на следующее задание из этого спи ска, имея действительный указатель на дескриптор какого-либо процесса, можно ис пользовать показанный ниже код:

list_entry(task->tasks.next, struct task_struct, tasks) Получение указателя на предыдущее задание работает аналогично.

list_entry (task->tasks.prev, struct task_struct, tasks) Дна указанных выше выражения доступны также в виде макросов next_task (task) (получить следующую задачу), prev_task (task) (получить предыдущую задачу).

Наконец, макрос for_each_process (task) позволяет выполнить цикл по всему списку задач. На каждом шаге цикла переменная task указывает на следующую за дачу из списка:

struct task_struct *task

;

for_each_process(task) { /* просто печатается имя команды и идентификатор PID для каждой задачи */ printk("%s[%d]\n", task->comm, task->pid)

;

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

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

Создание нового процесса В операционной системе Unix создание процессов происходит уникальным об разом. В большинстве операционных систем для создания процессов используется метод порождения процессов (spawn). При этом создается новый процесс в новом адресном пространстве, в которое считывается исполняемый файл, и после этого начинается исполнение процесса. В ОС Unix используется другой подход, а именно разбиение указанных выше операций на две функции: fork () и exec ()8.

Под exec() будем понимать любую функцию из семейства exec*(). В ядре реализован системный вызов execve(), на основе которого реализованы библиотечные функции execl p(), execle(), execv() и execvp().

Управление процессами В начале с помощью функции fork() создается порожденный процесс, который является копией текущего задания. Порожденный процесс отличается от родитель ского только значением идентификатора PID (который является уникальным в си стеме), значением параметра PPID (идентификатор PID родительского процесса, который устанавливается в значение PID порождающего процесса), некоторыми ре сурсами, такими как ожидающие на обработку сигналы (которые не наследуются), а также статистикой использования ресурсов - Вторая функция — exec () — загружает исполняемый файл в адресное пространство процесса и начинает исполнять его.

Комбинация функций fork () и exec () аналогична той одной функции создания процесса, которую предоставляет большинство операционных систем.

Копирование при записи Традиционно при выполнении функции fork() делался дубликат всех ресурсов родительского процесса и передавался порожденному. Такой подход достаточно наивный и неэффективный. В операционной системе Linux вызов fork () реализо ван с использованием механизма копирования при записи (copy-on-write) страниц памяти.

Технология копирования при записи (copy-on-write, COW) позволяет отложить или вообще предотвратить копирование данных. Вместо создания дубликата адресного пространства процесса родительский и порожденный процессы могут совместно ис пользовать одну и ту же копию адресного пространства. Однако при этом данные помечаются особым образом, и если вдруг один из процессов начинает изменять данные, то создается дубликат данных, и каждый процесс получает уникальную ко пию данных. Следовательно, дубликаты ресурсов создаются только тогда, когда в эти ресурсы осуществляется запись, а до того момента они используются совместно в режиме только для чтения (read-only). Такая техника позволяет задержать копи рование каждой страницы памяти до того момента, пока в эту страницу памяти не будет осуществляться запись. В случае, если в страницы памяти никогда не делается запись, как, например, при вызове функции exec () сразу после вызова fork (), то эти страницы никогда и не копируются. Единственные накладные расходы, которые вносит вызов функции fork (), — это копирование таблиц страниц родительского процесса и создание дескриптора порожденного процесса. Данная оптимизация пре дотвращает ненужное копирование большого количества данных (размер адресного пространства часто может быть более 10 Мбайт), так как процесс после разветвле ния в большинстве случаев сразу же начинает выполнять новый исполняемый образ.

Эта оптимизация очень важна, потому чти идеология операционной системы Unix предусматривает быстрое выполнение процессов.

Функция fork () В операционной системе Linux функция fork () реализована через системный вызов cl one (). Этот системный вызов может принимать в качестве аргументов набор флагов, определяющих, какие ресурсы должны быть общими (если вообще должны) у родительского и порожденного процессов. Далее в разделе "Реализация потоков в ядре Linux" об этих флагах рассказано более подробно. Библиотечные вызовы fork(),vfork() и cl oned вызывают системную функцию clone () с соответствующими флагами. В свою очередь системный вызов clone () вызывает функцию ядра do_fork ().

54 Глава Основную массу работы по разветвлению процесса выполняет функция do_f ork (), которая определена в файле kernel/fork.с. Эта функция, в свою очередь, вызыва ет функцию copy_pracess () и запускает новый процесс на выполнение. Ниже опи сана та интересная работа, которую выполняет функция copy_process ().

• Вызывается функция dup_task_struct (), которая создает стек ядра, струк туры thread_info и task_struct для нового процесса, причем все значения указанных структур данных идентичны для порождающего и порожденного процессов. На этом этапе дескрипторы родительского и порожденного про цессов идентичны.

• Проверяется, не произойдет ли при создании нового процесса переполнение лимита на количество процессов для данного пользователя.

• Теперь необходимо сделать порожденный процесс отличным от родительско го. При этом различные поля дескриптора порожденного процесса очищаются или устанавливаются в начальные значения. Большое количество данных де скриптора процесса является совместно используемым.

• Далее состояние порожденного процесса устанавливается в значение TASK_ UNINTERRUPTIBLE, чтобы гарантировать, что порожденный процесс не будет выполняться.

• Из функции copy_process () вызывается функция copy_f lags (), которая об новляет значение поля flags структуры task struct. При этом сбрасывается флаг PF_SUPERPRIV, который определяет, имеет ли процесс права суперполь зователя. Флаг PF_FORKNOEXEC, который указывает на то, что процесс не вы звал функцию exec (), — устанавливается.

• Вызывается функция get_pid (), которая назначает новое значение иденти фикатора PID для новой задачи.

• В зависимости от значений флагов, переданных в функцию clone (), осущест вляется копирование или совместное использование открытых файлов, инфор мации о файловой системе, обработчиков сигналов, адресного пространства процесса и пространства имен (namespace). Обычно эти ресурсы совместно ис пользуются потоками одного процесса. В противном случае они будут уникаль ными и будут копироваться на этом этапе.

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

• Наконец, происходит окончательная зачистка структур данных и возвращается указатель на новый порожденный процесс.

Далее происходит возврат в функцию do_fork (). Если возврат из функции copy_process () происходит успешно, то новый порожденный процесс возобновля ет выполнение. Порожденный процесс намеренно запускается на выполнение рань ше родительского9.

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

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

Функция vfork () Системный вызов vfork () позволяет получить тот же эффект, что и системный вызов fork (), за исключением того, что записи таблиц страниц родительского про цесса не копируются. Вместо этого порожденный процесс запускается как отдель ный поток в адресном пространстве родительского процесса и родительский про цесс блокируется до того момента, пока порожденный процесс не вызовет функцию exec () или не завершится. Порожденному процессу запрещена запись в адресное пространство. Такая оптимизация была желанной в старые времена 3BSD, когда реализация системного вызова fork () не базировалась на технике копирования страниц памяти при записи. Сегодня, при использовании техники копирования страниц памяти при записи и запуске порожденного процесса перед родительским, единственное преимущество вызова vfork () — это отсутствие копирования таблиц страниц родительского процесса. Если когда-нибудь в операционной системе Linux будет реализовано копирование полей таблиц страниц при записи10, то вообще не останется никаких преимуществ. Поскольку семантика функции vfork () достаточ но ненадежна (что, например, будет, если вызов exec () завершится неудачно?), то было бы здорово, если бы системный вызов vfork () умер медленной и мучитель ной смертью. Вполне можно реализовать системный вызов vfork () через обычный вызов fork (), что действительно имело место в ядрах Linux до версии 2.2.

Сейчас системный вызов vfork () реализован через специальный флаг в систем ном вызове clone (), как показано ниже.

• При выполнении функции copy_process () поле vfork_done структуры task_struct устанавливается в значение NULL.

• При выполнении функции do_fvork (), если соответствующий флаг установ лен, поле vfork_done устанавливается в ненулевое значение (начинает указы вать на определенный адрес).

• После того как порожденный процесс в первый раз запущен, родительский процесс, вместо того чтобы возвратиться из функции copy_process () к вы полнению, начинает ожидать, пока порожденный процесс не подаст ему сиг нал через указатель vfork_done.

• При выполнении порожденным процессом функции mm_release () (которая вызывается, когда задание заканчивает работу со своим адресным простран ством), если значение поля vfork_done не равно NULL, родительский про цесс получает указанный выше сигнал.

• При возврате в функцию do_fork() родительский процесс возобновляет вы полнение и выходит из этой функции.

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

56 Глава Если все прошло так, как запланировано, то теперь порожденный процесс выпол няется в новом адресном пространстве, а родительский процесс — в первоначальном адресном пространстве. Накладные расходы меньше, но реализация не очень при влекательна.

Реализация потоков в ядре Linux Многопоточность — это популярная сегодня программная абстракция. Она обе спечивает выполнение нескольких потоков в совместно используемом адресном про странстве памяти. Потоки также могут совместно использовать открытые файлы и другие ресурсы. Многопоточность используется для параллельного программирования (concurrent programming), что на многопроцессорных системах обеспечивает истинный параллелизм.

Реализация потоков в операционной системе Linux уникальна. Для ядра Linux не существует отдельной концепции потоков. В ядре Linux потоки реализованы так же, как и обычные процессы. В ОС Linux нет никакой особенной семантики для планирования выполнения потоков или каких-либо особенных структур данных для представления потоков. Поток— это просто процесс, который использует не которые ресурсы совместно с другими процессами. Каждый поток имеет структуру t ask_st ruct и представляется для ядра обычным процессом (который совместно использует ресурсы, такие как адресное пространство, с другими процессами).

В этом смысле Linux отличается от других операционных систем, таких как Microsoft Windows или Sun Solaris, которые имеют явные средства поддержки пото ков в ядре (в этих системах иногда потоки называются процессами с быстрым пере ключением контекста, lightweight process). Название "процесс с быстрым переключени ем контекста" показывает разницу между философией Linux и других операционных систем. Для остальных операционных систем потоки— это абстракция, которая обеспечивает облегченные, более быстрые для исполнения сущности, чем обычные тяжелые процессы. Для операционной системы Linux потоки — это просто способ совместного использования ресурсов несколькими процессами (которые и так име ют достаточно малое время переключения контекста)11.

Допустим, у нас есть процесс, состоящий из четырех потоков. В операционных системах с явной поддержкой потоков должен существовать дескриптор процесса, который далее указывает на четыре потока. Дескриптор процесса описывает со вместно используемые ресурсы, такие как адресное пространство и открытые фай лы. Потоки описываются ресурсами, которые принадлежат только им. В ОС Linux, наоборот, существует просто четыре процесса и, соответственно, четыре обычные структуры task_struct. Четыре процесса построены так, чтобы совместно исполь зовать определенные ресурсы.

Потоки создаются так же, как и обычные задания, за исключением того, что в системный вызов clone () передаются флаги с указанием, какие ресурсы должны ис пользоваться совместно:

Clone (CLONE_VM | CLONE_FS | CLONE_FILES | CLONE_SIGHAND, 0)

;

Как пример можно привести тесты по измерению времени создания процессов (и даже потоков) и операционной системе Linux по сравнению с другими операционными системами. Результаты очень хорошие.

Управление процессами - Результат выполнения показанного кода будет таким же, как и при выполнении обычного вызова fork (), за исключением того, что адресное пространство, ресур сы файловой системы, дескрипторы файлов и обработчики сигналов останутся об щими. Другими словами, новая задача, так же как и родительский процесс, — обыч ные потоки. В отличие от этого, обычный вызов fork () может быть реализован следующим образом:

clone (SIGCHLD, 0)

;

а вызов vfork () в таком виде:

clone (CLONE_VFORK | CLONE_VM | SIGCHLD, 0)

;

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

Таблица 3.1. Флаги системного вызова cl one () Флаг Описание CLONE_FILES Родительский и порожденный процессы совместно используют открытые файлы CLONE_FS Родительский и порожденный процессы совместно используют информацию о файловой системе CLONE_IDLETASK Установить значение PID в нуль (используется только для холостых (idle) задач) CLONE_NEWNS Создать новое пространство имен для порожденной задачи CLONE_PARENT Родительский процесс вызывающего процесса становится родитель ским и для порожденного CLONE_PTRACE Продолжить трассировку и для порожденного процесса CLONE_SETTID Возвратить значение идентификатора TID в пространство пользовател?

CLONE_SETTLS Для порожденного процесса создать новую область локальных дан ных потока (thread local storage, TLS) CLONE_SIGHAND У порожденного и родительского процессов будут общие обработчи ки сигналов CLONE_SYSVSEM У родительского и порожденного процессов будет общая семантика обработки флага SEM_UNDO ДЛЯ семафоров System V CLONE_THREAD Родительский и порожденный процессы будут принадлежать одной группе потоков CLONE_VFOK Использовать vfork (): родительский процесс будет находиться а приостановленном состоянии, пока порожденный процесс не воз обновит его работу CLONE_ONTRACED Запретить родительскому процессу использование флага CLONE_PTRACE для порожденного процесса CLONE_3T0P Запустить процесс в состоянии TASK_STOPPED CLONE_CHILD_CLEARTID Очистить идентификатор TID для порожденного процесса CLONE_CHILD_SETTID Установить идентификатор TID для порожденного процесса CLONE_PARENT_SETTID Установить идентификатор TID для родительского процесса CLONE_VM У порожденного и родительского процессов будет общее адресное пространство 58 Глава Потоки в пространстве ядра Часто в ядре полезно выполнить некоторые операции в фоновом режиме. В ядре такая возможность реализована с помощью потоков пространства ядра (kernel thread) — обычных процессов, которые выполняются исключительно в пространстве ядра.

Pages:     || 2 | 3 | 4 | 5 |   ...   | 10 |



© 2011 www.dissers.ru - «Бесплатная электронная библиотека»

Материалы этого сайта размещены для ознакомления, все права принадлежат их авторам.
Если Вы не согласны с тем, что Ваш материал размещён на этом сайте, пожалуйста, напишите нам, мы в течении 1-2 рабочих дней удалим его.