Главная страница
Навигация по странице:

  • Отсутствие библиотеки l i b c

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

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

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

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

  • Синхронизация и параллелизм

  • Переносимость — это важно

  • Второе издание


    Скачать 3.09 Mb.
    НазваниеВторое издание
    Дата08.09.2019
    Размер3.09 Mb.
    Формат файлаpdf
    Имя файлаLav_Robert_Razrabotka_yadra_Linux_Litmir.net_264560_original_254.pdf
    ТипДокументы
    #86226
    страница5 из 53
    1   2   3   4   5   6   7   8   9   ...   53
    Уменьшение количества выводимых сообщений
    Для того чтобы уменьшить шум, связанный с сообщениями, которые выдаются во время сборки, но в то же время видеть предупреждения и сообщения об ошибках,
    можно использовать такую хитрость, как перенаправление стандартного вывода ко- манды make (1):
    make > "имя_некоторого_файла"
    Если вдруг окажется необходимым просмотреть выводимые сообщения, можно воспользоваться соответствующим файлом. Но обычно, если предупреждения или сообщения об ошибках выводятся на экран, в этом нет необходимости.
    На самом деле я выполняю следующую команду make > /dev/null,
    что позволяет совсем избавиться от ненужных сообщений.
    Параллельная сборка
    Программа make (1) предоставляет возможность разбить процесс сборки на не- сколько заданий. Каждое из этих заданий выполняется отдельно от остальных и параллельно с остальными, существенно ускоряя процесс сборки на многопроцес- сорных системах. Это также позволяет более оптимально использовать процессор,
    Поскольку время компиляции большого дерева исходного кода также включает вре- мя ожидания завершения ввода-вывода (время, в течение которого процесс ждет за- вершения операций ввода-вывода).
    По умолчанию утилита make (1) запускает только одну задачу, поскольку часто файлы сборки содержат некорректную информацию о зависимостях. При непра- вильной информации о зависимостях несколько заданий могут начать "наступать друг другу на ноги", что приведет к ошибкам компиляции. Конечно же, в файле сборки ядра таких ошибок нет. Для компиляции ядра с использованием параллель- ной сборки необходимо выполнить следующую команду.
    $ make -jn где n — количество заданий, которые необходимо запустить.
    Начальные сведения о ядре Linux 37

    Обычно запускается один или два процесса на процессор. Например, на двухпро- цессорной машине можно использовать следующий запуск.
    $ make -j4
    Используя такие отличные утилиты, как d i s t c c (1) и c c a c h e ( l ) , можно еще бо- лее существенно уменьшить время компиляции ядра.
    Инсталляция ядра
    После того как ядро собрано, его необходимо инсталлировать. Процесс инстал- ляции существенно зависит от платформы и типа системного загрузчика. Для того чтобы узнать, в какой каталог должен быть скопирован образ ядра и как установить его для загрузки, необходимо обратиться к руководству по используемому системно- му загрузчику. На случай если новое ядро будет иметь проблемы с работоспособнос- тью, всегда следует сохранить одну или две копии старых ядер, которые гарантиро- ванно работоспособны!
    Например, для платформы x86, при использовании системного загрузчика grub можно скопировать загружаемый образ ядра из файла a r c h / i 3 8 6 / b o o t / b z l m a g e в каталог /boot и отредактировать файл / e t c / g r u b / g r u b . c o n f для указания записи,
    которая соответствует новому ядру. В системах, где для загрузки используется загруз- чик LILO, необходимо соответственно отредактировать файл / e t c / l i l o . c o n f и за- пустить утилиту l i l o (8).
    Инсталляция модулей ядра автоматизирована и не зависит от аппаратной плат- формы. Просто нужно запустить следующую команду с правами пользователя root.
    $ make modules_install
    В процессе компиляции в корневом каталоге дерева исходного кода ядра также создается файл System.map. В этом файле содержится таблица соответствия симво- лов ядра их начальным адресам в памяти. Эта таблица используется при отладке для перевода адресов памяти в имена функций и переменных.
    "Зверек другого рода"
    Ядро имеет некоторые отличия в сравнении с обычными пользовательскими при- ложениями, эти отличия хотя и не обязательно приводят к серьезным усложнениям при программировании, но все же создают специфические проблемы при разработ- ке ядра.
    Эти отличия делают ядро зверьком другого рода. Некоторые из старых правил при этом остаются в силе, а некоторые правила являются полностью новыми. Хотя часть различий очевидна (все знают, что ядро может делать все, что пожелает), другие различия не так очевидны. Наиболее важные отличия описаны ниже.
    • Ядро не имеет доступа к библиотеке функций языка С.
    • Ядро программируется с использованием компилятора GNU С.
    • В ядре нет такой защиты памяти, как в режиме пользователя.
    • В ядре нельзя легко использовать вычисления с плавающей точкой.
    • Ядро использует стек небольшого фиксированного размера.
    38 Глава 2

    • Поскольку в ядре используются асинхронные прерывания, ядро является пре- емптивным и в ядре имеется поддержка SMP, то в ядре необходимо учитывать наличие параллелизма и использовать синхронизацию.
    • Переносимость очень важна.
    Давайте рассмотрим более детально все эти проблемы, так как все разработчики ядра должны постоянно помнить о них.
    Отсутствие библиотеки l i b c
    В отличие от обычных пользовательских приложений, ядро не компонуется со стандартной библиотекой функций языка С (и ни с какой другой библиотекой та- кого же типа). Для этого есть несколько причин, включая некоторые ситуации с дилеммой о курице и яйце, однако первопричина — скорость выполнения и объем кода. Полная библиотека функций языка С, и даже только самая необходимая ее часть, очень большая и неэффективная для ядра.
    При этом не нужно расстраиваться, так как многие из функций библиотеки язы- ка С реализованы в ядре. Например, обычные функции работы со строками опи- саны в файле l i b / s t r i n g . с . Необходимо лишь подключить заголовочный файл
    < l i n u x / s t r i n g . h > и пользоваться этими функциями.
    Заголовочные файлы
    Заметим, что упомянутые заголовочные файлы и заголовочные файлы, которые будут упоми- наться далее в этой книге, принадлежат дереву исходного кода ядра. В файлах исходного кода ядра нельзя подключать заголовочные файлы извне этого дерева каталогов, так же как и нельзя использовать внешние библиотеки,
    Отсутствует наиболее известная функция p r i n t f ( ) . Ядро не имеет доступа к функции p r i n t f ( ) , однако ему доступна функция p r i n t k (). Функция p r i n t k ( ) ко- пирует форматированную строку в буфер системных сообщений ядра (kernel log buf- fer), который обычно читается с помощью программы syslog. Использование этой функции аналогично использованию p r i n t f ( ) :
    printk("Hello world! Строка: %s и целое число: %d\n", a_string, an_integer);
    Одно важное отличие между p r i n t f () и p r i n t k () состоит в том, что в функции p r i n t k () можно использовать флаг уровня вывода. Этот флаг используется про- граммой s y s l o g для того, чтобы определить, нужно ли показывать сообщение ядра.
    Вот пример использования уровня вывода:
    printk(KERN_ERR "Это была ошибка!\n");
    Функция p r i n t k () будет использоваться на протяжении всей книги. В следую- щих главах приведено больше информации о функции p r i n t k ().
    Компилятор GNU С
    Как и все "уважающие себя" ядра Unix, ядро Linux написано на языке С. Может быть, это покажется неожиданным, но ядро Linux написано не на чистом языке С в стандарте ANSI С. Наоборот, где это возможно, разработчики ядра используют раз-
    Начальные сведения о ядре Linux 39

    I l l личные расширения языка, которые доступны с помощью средств компиляции gcc
    (GNU Compiler Collection — коллекция компиляторов GNU, в которой содержится компилятор С, используемый для компиляции ядра).
    Разработчики ядра используют как расширения языка С ISO C99 1
    так и расши- рения GNU С. Эти изменения связывают ядро Linux с компилятором gcc, хотя со- временные компиляторы, такие как Imel С, имеют достаточную поддержку возмож- ностей компилятора gcc для того, чтобы ими тоже можно было компилировать ядро
    Linux. В ядре не используются какие-либо особенные расширения стандарта С99, и кроме того, поскольку стандарт С99 является официальной редакцией языка С, эти расширения редко приводят к возникновению ошибок в других частях кода. Более интересные и, возможно, менее знакомые отклонения от стандарта языка ANSI С
    связаны с расширениями GNU С. Давайте рассмотрим некоторые наиболее интерес- ные расширения, которые могут встретиться в программном коде ядра.
    Функции с подстановкой тела
    Компилятор GNU С поддерживает функции с подстановкой тела (inline functions).
    Исполняемый код функции с подстановкой тела, как следует из названия, вставля- ется во все места программы, где указан вызов функции. Это позволяет избежать дополнительных затрат на вызов функции и возврат из функции (сохранение и вос- становление регистров) и потенциально позволяет повысить уровень оптимизации,
    так как компилятор может оптимизировать код вызывающей и вызываемой функций вместе. Обратной стороной такой подстановки (ничто в этой жизни не дается да- ром) является увеличение объема кода, увеличение используемой памяти и уменьше- ние эффективности использования процессорного кэша инструкций. Разработчики ядра используют функции с подстановкой тела для небольших функций, критичных ко времени выполнения. Использовать подстановку тела для больших функций, осо- бенно когда они вызываются больше одного раза или не слишком критичны ко вре- мени выполнения, не рекомендуется.
    Функции с подстановкой тела объявляются с помощью ключевых слов s t a t i c и inline в декларации функции. Например,
    static inline void dog(unsigned long tail_size);
    Декларация функции должна быть описана перед любым ее вызовом, иначе под- становка тела не будет произведена. Стандартный прием — это размещение функций с подстановкой тела в заголовочных файлах. Поскольку функция объявляется как статическая (static), экземпляр функции без подстановки тела не создается. Если функция с подстановкой тела используется только в одном файле, то она может быть размещена в верхней части этого файла.
    В ядре использованию функций с подстановкой тела следует отдавать преимуще- ство по сравнению с использованием сложных макросов.
    1
    Стандарт ISO C99 — это последняя основная версия редакции стандарта ISO С. Редакция С99 со- держит многочисленные улучшения предыдущей основной редакции этого стандарта. Стандарт
    ISO C99 вводит поименную инициализацию полей структур и тип complex.
    40 Глава 2

    Встроенный ассемблер
    Компилятор gcc С позволяет встраивать инструкции языка ассемблера в обыч- ные функции языка С. Эта возможность, конечно, должна использоваться только в тех частях ядра, которые уникальны для определенной аппаратной платформы.
    Для встраивания ассемблерного кода используется директива компилятора asm().
    Ядро Limix написано на смеси языков ассемблера и С. Язык ассемблера использу- ется в низкоуровневых подсистемах и на участках кода, где нужна большая скорость выполнения. Большая часть коду ядра написана на языке программирования С.
    Аннотация ветвлений
    Компилятор gnu С имеет встроенные директивы, позволяющие оптимизировать различные ветви условных операторов, которые наиболее или наименее вероятны.
    Компилятор использует эти директивы для соответственной оптимизации кода.
    В ядре эти директивы заключаются в макросы l i k e l y ( ) и u n l i k e l y ( ) , которые легко использовать. Например, если используется оператор if следующего вида:
    if (foo) {
    / * . . * /
    }
    то для того, чтобы отметить этот путь выполнения как маловероятный, необходимо указать:
    /* предполагается, что значение переменной foo равно нулю ..*/
    if (unllkely(ffoo)) {
    /*..*/
    }
    И наоборот, чтобы отметить этот путь выполнения как наиболее вероятный
    /* предполагается, что значение переменной foo не равно нулю ..*/
    if (likely(foo)) {
    / * . . * /
    }
    Эти директивы необходимо использовать только в случае, когда направление вет- вления с большой вероятностью известно априори или когда необходима оптимиза- ция какой-либо части кода за счет другой части. Важно помнить, что эти директивы дают увеличение производительности, когда направление ветвления предсказано правильно, однако приводят к потере производительности при неправильном пред- сказании. Наиболее часто директивы u n l i k e l y () и l i k e l y () используются для проверки ошибок.
    Отсутствие защиты памяти
    Когда прикладная программа предпринимает незаконную попытку обращения к памяти, ядро может перехватить эту ошибку и аварийно завершить соответствующий процесс. Если ядро предпринимает попытку некорректного обращения к памяти, то результаты могут быть менее контролируемы. Нарушение правил доступа к памяти в режиме ядра приводит к ошибке oops, которая является наиболее часто встреча-
    Начальные сведения о ядре Linux 41
    ющейся ошибкой ядра. Не стоит говорить, что нельзя обращаться к запрещенным областям памяти, разыменовывать указатели со значением NULL и так далее, однако в ядре ставки значительно выше!
    Кроме того, память ядра не использует замещение страниц. Поэтому каждый байт памяти, который использован в ядре, — это еще один байт доступной физической па- мяти. Это необходимо помнить всякий раз, когда добавляются новые функции ядра.
    Нельзя просто использовать вычисления
    с плавающей точкой
    Когда пользовательская программа использует вычисления с плавающей точкой,
    ядро управляет переходом из режима работы с целыми числами в режим работы с плавающей точкой. Операции, которые ядро должно выполнить для использования инструкций работы с плавающей точкой, зависят от аппаратной платформы.
    В отличие от режима задачи, в режиме ядра нет такой роскоши, как прямое ис- пользование вычислений с плавающей точкой. Активизация режима вычислений с плавающей точкой в режиме ядра требует сохранения и восстановления регистров устройства поддержки вычислений с плавающей точкой вручную, кроме прочих ру- тинных операций. Если коротко, то можно посоветовать: не нужно этого делать; ника- ких вычислений с плавающей точкой в режиме ядра.
    Маленький стек фиксированного размера
    Пользовательские программы могут "отдохнуть" вместе со своими тоннами ста- тически выделяемых переменных в стеке, включая структуры большого размера и многоэлементные массивы. Такое поведение является законным в режиме задачи,
    так как область стека пользовательских программ может динамически увеличиваться в размере (разработчики, которые писали программы под старые и не очень интел- лектуальные операционные системы, как, например, DOS, могут вспомнить то вре- мя, когда даже стек пользовательских программ имел фиксированный размер).
    Стек, доступный в режиме ядра, не является ни большим, ни динамически изме- няемым, он мал по объему и имеет фиксированный размер. Размер стека зависит от аппаратной платформы. Для платформы х86 размер стека может быть сконфигури- рован на этапе компиляции и быть равным 4 или 8 Кбайт. Исторически так сложи- лось, что размер стека ядра равен двум страницам памяти, что соответствует 8 Кбайт для 32-разрядных аппаратных платформ и 16 Кбайт — для 64-разрядных. Этот размер фиксирован. Каждый процесс получает свою область стека.
    Более подробное обсуждение использования стека в режиме ядра смотрите в сле- дующих главах.
    Синхронизация и параллелизм
    Ядро подвержено состояниям конкуренции за ресурсы (race condition). В отли- чие от однопоточной пользовательской программы, ряд свойств ядра позволяет осу- ществлять параллельные обращения к ресурсам общего доступа, и поэтому требуется выполнять синхронизацию для предотвращения состояний конкуренции за ресурсы.
    В частности, возможны следующие ситуации.
    42 Глава 2

    • Ядро Linux поддерживает многопроцессорную обработку. Поэтому, без соот- ветствующей защиты, код ядра может выполняться на одном, двух или боль- шем количестве процессоров и при этом одновременно обращаться к одному ресурсу.
    • Прерывания возникают асинхронно по отношению к исполняемому коду.
    Поэтому, без соответствующей защиты, прерывания могут возникнуть во время обращения к ресурсу общего доступа, и обработчик прерывания может тоже обратиться к этому же ресурсу.
    • Ядро Linux является преемптивным. Поэтому, без соответствующей защиты,
    исполняемый код ядра может быть вытеснен в пользу другого кода ядра, кото- рый тоже может обращаться к некоторому общему ресурсу.
    Стандартное решение для предотвращения состояния конкуренции за ресурсы
    (состояния гонок) — это использование спин-блокировок и семафоров.
    Более полное обсуждение вопросов синхронизации и параллелизма приведено в следующих главах.
    Переносимость — это важно
    При разработке пользовательских программ переносимость не всегда является це лью, однако операционная система Linux является переносимой и должна оставать- ся такой. Это означает, что платформо-независимый код, написанный на языке С,
    должен компилироваться без ошибок и правильно выполняться на большом количе- стве систем.
    Несколько правил, такие как не создавать зависимости от порядка следования байтов, обеспечивать возможность использования кода для 64-битовых систем, не привязываться к размеру страницы памяти или машинного слова и другие— имеют большое значение. Эти вопросы более подробно освещаются в одной из следующих глав.
    Резюме
    Да, ядро— это действительно нечто иное: отсутствует защита памяти, нет про- веренной библиотеки функций языка С, маленький стек, большое дерево исходного кода. Ядро Linux играет по своим правилам и занимается серьезными вещами. Тем не менее, ядро— это всего лишь программа; оно, по сути, не сильно отличается от других обычных программ. Не нужно его бояться.
    Понимание того, что ядро не так уж страшно, как кажется, может стать первым шагом к пониманию того, что все имеет свой смысл. Однако чтобы достичь этой уто- пии, необходимо стараться, читать исходный код, изменять его и не падать духом.
    Вводный материал, который был представлен в первой главе, и базовые момен- ты, которые описаны в текущей, надеюсь, станут хорошим фундаментом для тех знаний, которые будут получены при прочтении всей книги. В следующих разделах будут рассмотрены конкретные подсистемы ядра и принципы их работы.
    Начальные сведения о ядре Linux
    43

    3
    Управление
    процессами
    П
    роцесс - одно из самых важных абстрактных понятий в Unix-подобных опе- рационных системах
    1
    . По сути, процесс— это программа, т.е. объектный код,
    хранящийся на каком-либо носителе информации и находящийся в состоянии ис- полнения. Однако процесс — это не только исполняемый программный код, кото- рый для операционной системы Unix часто называется text section (сегмент текста
    или сегмент кода). Процессы также включают в себя сегмент данных (data section), со- держащий глобальные переменные; набор ресурсов, таких как открытые файлы и ожидающие на обработку сигналы; адресное пространство и один или более потоков
    выполнения. Процесс — это живой результат выполнения программного кода.
    Потоки выполнения, которые часто для сокращения называют просто потоками
    (thread), представляют собой объекты, выполняющие определенные операции вну- три процесса. Каждый поток включает в себя уникальный счетчик команд (program counter), стек выполнения и набор регистров процессора. Ядро планирует выполне- ние отдельных потоков, а не процессов. В традиционных Unix-подобных операцион- ных системах каждый процесс содержал только один поток. Однако в современных системах многопоточные программы используются очень широко. Как будет пока- зано далее, в операционной системе Linux используется уникальная реализация по- токов — между процессами и потоками нет никакой разницы. Поток в операционной системе Linux — это специальный тип процесса.
    В современных операционных системах процессы предусматривают наличие двух виртуальных ресурсов: виртуального процессора и виртуальной памяти. Виртуальный процессор создает для процесса иллюзию, что этот процесс монопольно использу- ет всю компьютерную систему, за исключением, может быть, только того, что фи- зическим процессором совместно пользуются десятки других процессов. В главе 4,
    "Планирование выполнения процессов", эта виртуализация обсуждается более под- робно. Виртуальная память предоставляет процессу иллюзию того, что он один рас- полагает всей памятью компьютерной системы. Виртуальной памяти посвящена гла- ва 11, "Управление памятью". Потоки совместно используют одну и ту же виртуальную память, хотя каждый поток получает свой виртуальный процессор.
    Другая абстракция — это файл.

    Следует подчеркнуть, что сама по себе программа процессом не является; про- цесс — это выполняющаяся программа плюс набор соответствующих ресурсов.
    Конечно, может существовать два процесса, которые исполняют одну и ту же про- грамму. В действительности может даже существовать два или больше процессов,
    которые совместно используют одни и те же ресурсы, такие как открытые файлы,
    или адресное пространство. Процесс начинает свое существование с момента соз- дания, что впрочем не удивительно. В операционной системе Linux такое создание выполняется с помощью системного вызова fork() (буквально, ветвление или вил- ка), который создает новый процесс путем полного копирования уже существую- щего. Процесс, который вызвал системную функцию fork (), называется порожда-
    ющим (родительским, pannt), новый процесс именуют порожденным (дочерний, child).
    Родительский процесс после этого продолжает выполнение, а порожденный процесс начинает выполняться с места возврата из системного вызова. Часто после развет- вления в одном из процессов желательно выполнить какую-нибудь другую програм- му. Семейство функций exec*() позволяет создать новое адресное пространство и загрузить в него новую программу. В современных ядрах Linux функция fork() ре- ализована через системный вызов clone(), который будет рассмотрен в следующем разделе.
    Выход из программы осуществляется с помощью системного вызова e x i t ( ) . Эта функция завершает процесс и освобождает все занятые им ресурсы. Родительский процесс может запросить о состоянии порожденных им процессов с помощью си- стемного вызова wait4()
    2
    , который заставляет один процесс ожидать завершения другого. Когда процесс завершается, он переходит в специальное состояние зомби
    (zombie), которое используется для представления завершенного процесса до того мо- мента, пока порождающий его процесс не вызовет системную функцию wait() или waitpid().
    Иное название для процесса — задание или задача (task). О процессах в ядре опе- рационной системы Linux говорят как о задачах. В этой книге оба понятия взаимо- заменяемы, хотя по возможности для представления работающей программы в ядре будет использоваться термин задача, а для представления в режиме пользователя —
    термин процесс.
    1   2   3   4   5   6   7   8   9   ...   53


    написать администратору сайта