Вперед Назад Содержание


1. Загрузка

1.1 Построение образа ядра Linux

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

Когда пользователь дает команду 'make zImage' или 'make bzImage', результат -- загрузочный образ ядра, записывается как arch/i386/boot/zImage или arch/i386/boot/bzImage соответственно. Вот что происходит в процессе сборки:

  1. Исходные файлы на C и ассемблере компилируются в перемещаемый [relocatable] объектный код в формате ELF (файлы с расширением .o), при этом некоторые файлы, с помощью утилиты ar(1), дополнительно группируются в архивы (с раширением .a)
  2. Созданные на предыдущем шаге, файлы .o и .a объединяются утилитой ld(1) в статически скомпонованный исполняемый файл vmlinux в 32-разрядном формате ELF 80386 с включенной символической информацией.
  3. Далее, посредством nm vmlinux, создается файл System.map, при этом все не относящиеся к делу символы отбрасываются.
  4. Переход в каталог arch/i386/boot.
  5. Текст asm-файла bootsect.S перерабатывается с или без ключа -D__BIG_KERNEL__, в зависимости от конечной цели bzImage или zImage, в bbootsect.s или bootsect.s соответственно.
  6. bbootsect.s ассемблируется и конвертируется в файл формата 'raw binary' с именем bbootsect (bootsect.s ассемблируется в файл bootsect в случае сборки zImage).
  7. Содержимое установщика setup.S (setup.S подключает video.S) преобразуется в bsetup.s для bzImage (setup.s для zImage). Как и в случае с кодом bootsector, различия заключаются в использовании ключа -D__BIG_KERNEL__, при сборке bzImage. Результирующий файл конвертируется в формат 'raw binary' с именем bsetup.
  8. Переход в каталог arch/i386/boot/compressed. Файл /usr/src/linux/vmlinux переводится в файл формата 'raw binary' с именем $tmppiggy и из него удаляются ELF-секции .note и .comment.
  9. gzip -9 < $tmppiggy > $tmppiggy.gz
  10. Связывание $tmppiggy.gz в перемещаемый ELF-формат (ld -r) piggy.o.
  11. Компиляция процедур сжатия данных head.S и misc.c (файлы находятся в каталоге arch/i386/boot/compressed) в объектный ELF формат head.o и misc.o.
  12. Объектные файлы head.o, misc.o и piggy.o объединяются в bvmlinux (или vmlinux при сборке zImage, не путайте этот файл с /usr/src/linux/vmlinux!). Обратите внимание на различие: -Ttext 0x1000, используется для vmlinux, а -Ttext 0x100000?-- для bvmlinux, т.е. bzImage загружается по более высоким адресам памяти.
  13. Преобразование bvmlinux в файл формата 'raw binary' с именем bvmlinux.out, в процессе удаляются ELF секции .note и .comment.
  14. Возврат в каталог arch/i386/boot и, с помощью программы tools/build, bbootsect, bsetup и compressed/bvmlinux.out объединяются в bzImage (справедливо и для zImage, только в именах файлов отсутствует начальный символ 'b'). В конец bootsector записываются такие важные переменные, как setup_sects и root_dev.

Размер загрузочного сектора (bootsector) всегда равен 512 байт. Размер установщика (setup) должен быть не менее чем 4 сектора, и ограничивается сверху размером около 12K - по правилу:

512 + setup_sects * 512 + место_для_стека_bootsector/setup <= 0x4000 байт

Откуда взялось это ограничение станет понятным дальше.

На сегодняшний день верхний предел размера bzImage составляет примерно 2.5M, в случае загрузки через LILO, и 0xFFFF параграфов (0xFFFF0 = 1048560 байт) для загрузки raw-образа, например с дискеты или CD-ROM (El-Torito emulation mode).

Следует помнить, что tools/build выполняет проверку размеров загрузочного сектора, образа ядра и нижней границы установщика (setup), но не проверяет *верхнюю* границу установщика (setup). Следовательно очень легко собрать "битое" ядро, добавив несколько больший размер ".space" в конец setup.S.

1.2 Загрузка: Обзор

Процесс загрузки во многом зависит от аппаратной платформы, поэтому основное внимание будет уделено платформе IBM PC/IA32. Для сохранения обратной совместимости, firmware-загрузчики загружают операционную систему устаревшим способом. Процесс этот можно разделить на несколько этапов:

  1. BIOS выбирает загрузочное устройство.
  2. BIOS загружает bootsector с загрузочного устройства.
  3. Код bootsector-а загружает установщика, процедуры декомпрессии и сжатый образ ядра.
  4. Ядро декомпрессируется в защищенном режиме (protected mode).
  5. Выполняется низкоуровневый инициализирующий ассемблерный код.
  6. Выполняется высокоуровневый инициализирующий C код.

1.3 Загрузка: BIOS POST

  1. При включении питания запускается тактовый генератор и схема контроля питания устанавливает на шине сигнал #POWERGOOD.
  2. На вывод CPU #RESET подается сигнал (после чего CPU переходит в реальный режим 8086).
  3. %ds=%es=%fs=%gs=%ss=0, %cs=0xFFFF0000,%eip = 0x0000FFF0 (запуск кода Power On Self Test в ROM BIOS).
  4. На время выполнения проверок, прерывания запрещены.
  5. По адресу 0 инициализируется таблица векторов прерываний (IVT, Interrupts Vector Table).
  6. По прерыванию 0x19 вызывается начальный (bootstrap) загрузчик BIOS, регистр %dl содержит 'номер загрузочного устройства'. В результате по физическому адресу 0x7C00 (0x07C0:0000) загружается содержимое первого сектора нулевой дорожки.

1.4 Загрузка: bootsector и setup

Для загрузки ядра Linux можно воспользоваться следующими загрузочными секторами:

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


29 SETUPSECS = 4                /* число секторов установщика по умолчанию */
30 BOOTSEG   = 0x07C0           /* первоначальный адрес загрузочного сектора */
31 INITSEG   = DEF_INITSEG      /* сюда перемещается загрузчик - чтобы не мешал */
32 SETUPSEG  = DEF_SETUPSEG     /* здесь начинается установщик */
33 SYSSEG    = DEF_SYSSEG       /* система загружается по адресу 0x10000 (65536) */
34 SYSSIZE   = DEF_SYSSIZE      /* размер системы: в 16-байтных блоках */


(числа в начале - это номера строк в файле bootsect.S file) Значения DEF_INITSEG, DEF_SETUPSEG, DEF_SYSSEG и DEF_SYSSIZE берутся из файла include/asm/boot.h:


/* Ничего не меняйте, если не уверены в том, что делаете. */
#define DEF_INITSEG     0x9000
#define DEF_SYSSEG      0x1000
#define DEF_SETUPSEG    0x9020
#define DEF_SYSSIZE     0x7F00


Рассмотрим поближе код bootsect.S:


  54          movw    $BOOTSEG, %ax
    55          movw    %ax, %ds
    56          movw    $INITSEG, %ax
    57          movw    %ax, %es
    58          movw    $256, %cx
    59          subw    %si, %si
    60          subw    %di, %di
    61          cld
    62          rep
    63          movsw
    64          ljmp    $INITSEG, $go

    65  # bde - 0xff00 изменено на 0x4000 для работы отладчика с 0x6400 и выше (bde).
    66  # Если мы проверили верхние адреса, то об этом можно не беспокоиться. Кроме того,
    67  # мой BIOS можно сконфигурировать на загрузку таблицы дисков wini в верхнюю память
    68  # вместо таблицы векторов.  Старый стек может "помесить"
    69  # таблицу устройств [drive table].

    70  go:     movw    $0x4000-12, %di         # 0x4000 - произвольное значение >=
    71                                          # длины bootsect + длины
    72                                          # setup + место для стека;
    73                                          # 12 - размер параметров диска.
    74          movw    %ax, %ds                # INITSEG уже в ax и es
    75          movw    %ax, %ss
    76          movw    %di, %sp                # разместим стек по INITSEG:0x4000-12.


Строки 54-63 перемещают код начального загрузчика из адреса 0x7C00 в адрес 0x90000. Для этого:

  1. в регистровую пару %ds:%si заносится значение $BOOTSEG:0 (0x7C0:0 = 0x7C00)
  2. в регистровую пару %es:%di заносится значение $INITSEG:0 (0x9000:0 = 0x90000)
  3. в регистр %cx записывается число 16-битовых слов (256 слов = 512 байт = 1 сектор)
  4. В регистре флагов EFLAGS сбрасывается флаг направления DF (Direction Flag) (копирование с автоинкрементом адресных регистров) (cld)
  5. копируется 512 байт (rep movsw)

Здесь умышленно не используется инструкция rep movsd (обратите внимание на директиву - .code16).

В строке 64 выполняется переход на метку go:, в только что созданную копию загрузчика, т.е. в сегмент 0x9000. Эта, и следующие три инструкции (строки 64-76) переустанавливают регистр сегмента стека и регистр указателя стека на $INITSEG:0x4000-0xC, т.е. %ss = $INITSEG (0x9000) и %sp = 0x3FF4 (0x4000-0xC). Это и есть то самое ограничение на размер setup, которое упоминалось ранее (см. Построение образа ядра Linux).

Для того, чтобы разрешить считывание сразу нескольких секторов (multi-sector reads), в строках 77-103 исправляются некоторые значения в таблице параметров для первого диска :


  77   # Часто в BIOS по умолчанию в таблицы параметров диска не признают
    78  # чтение по несколько секторов кроме максимального числа, указанного
    79  # по умолчанию в таблице параметров дискеты - что может иногда равняться
    80  # 7 секторам.
    81  #
    82  # Поскольку чтение по одному сектору отпадает (слишком медленно),
    83  # необходимо позаботиться о создании в ОЗУ новой таблицы параметров
    84  # (для первого диска).  Мы установим максимальное число секторов
    85  # равным 36 - максимум, с которым мы столкнемся на ED 2.88.
    86  #
    87  # Много - не мало.  А мало - плохо.
    88  #
    89  # Сегменты устанавливаются так: ds = es = ss = cs - INITSEG, fs = 0,
    90  # а gs не используется.


    91          movw    %cx, %fs                # запись 0 в fs
    92          movw    $0x78, %bx              # в fs:bx адрес таблицы
    93          pushw   %ds
    94          ldsw    %fs:(%bx), %si          # из адреса ds:si
    95          movb    $6, %cl                 # копируется 12 байт
    96          pushw   %di                     # di = 0x4000-12.
    97          rep                             # инструкция cld не нужна - выполнена в строке 66
    98          movsw
    99          popw    %di
   100          popw    %ds
   101          movb    $36, 0x4(%di)           # записывается число секторов
   102          movw    %di, %fs:(%bx)
   103          movw    %es, %fs:2(%bx)


Контроллер НГМД переводится в исходное состояние функцией 0 прерывания 0x13 в BIOS (reset FDC) и секторы установщика загружаются непосредственно после загрузчика, т.е. в физические адреса, начиная с 0x90200 ($INITSEG:0x200), с помощью функции 2 прерывания 0x13 BIOS (read sector(s)). Смотри строки 107-124:


 107  load_setup:
   108          xorb    %ah, %ah                # переинициализация FDC
   109          xorb    %dl, %dl
   110          int     $0x13
   111          xorw    %dx, %dx                # диск 0, головка 0
   112          movb    $0x02, %cl              # сектор 2, дорожка 0
   113          movw    $0x0200, %bx            # адрес в INITSEG = 512
   114          movb    $0x02, %ah              # функция 2, "read sector(s)"
   115          movb    setup_sects, %al        # (все под головкой 0, на дорожке 0)
   116          int     $0x13                   # читать
   117          jnc     ok_load_setup           # получилось - продолжить

   118          pushw   %ax                     # запись кода ошибки
   119          call    print_nl
   120          movw    %sp, %bp
   121          call    print_hex
   122          popw    %ax
   123          jmp     load_setup

   124  ok_load_setup:


Если загрузка по каким-либо причинам не прошла (плохая дискета или дискета была вынута в момент загрузки), то выдается сообщение об ошибке и производится переход на бесконечный цикл. Цикл будет повторяться до тех пор, пока не произойдет успешная загрузка, либо пока машина не будет перезагружена.

Если загрузка setup_sects секторов кода установщика прошла благополучно, то производится переход на метку ok_load_setup:.

Далее производится загрузка сжатого образа ядра в физические адреса начиная с 0x10000, чтобы не затереть firmware-данные в нижних адресах памяти (0-64K). После загрузки ядра управление передается в точку $SETUPSEG:0 (arch/i386/boot/setup.S). Поскольку обращений к BIOS больше не будет, данные в нижней памяти уже не нужны, поэтому образ ядра перемещается из 0x10000 в 0x1000 (физические адреса, конечно). И наконец, установщик setup.S завершает свою работу, переводя процессор в защищенный режим и передает управление по адресу 0x1000 где находится точка входа в сжатое ядро, т.е. arch/386/boot/compressed/{head.S,misc.c}. Здесь производится установка стека и вызывается decompress_kernel(), которая декомпрессирует ядро в адреса, начиная с 0x100000, после чего управление передается туда.

Следует отметить, что старые загрузчики (старые версии LILO) в состоянии загружать только первые 4 сектора установщика (setup), это объясняет присутствие кода, "догружающего" остальные сектора в случае необходимости. Кроме того, установщик содержит код, обрабатывающий различные комбинации типов/версий загрузчиков и zImage/bzImage.

Теперь рассмотрим хитрость, позволяющую загрузчику выполнить загрузку "больших" ядер, известных под именем "bzImage". Установщик загружается как обычно, в адреса с 0x90200, а ядро, с помощью специальной вспомогательной процедуры, вызывающей BIOS для перемещения данных из нижней памяти в верхнюю, загружается кусками по 64К. Эта процедура определена в setup.S как bootsect_helper, а вызывается она из bootsect.S как bootsect_kludge. Метка bootsect_kludge, определенная в setup.S, содержит значение сегмента установщика и смещение bootsect_helper в нем же, так что для передачи управления загрузчик должен использовать инструкцию lcall (межсегментный вызов). Почему эта процедура помещена в setup.S? Причина банальна - в bootsect.S просто больше нет места (строго говоря это не совсем так, поскольку в bootsect.S свободно примерно 4 байта и по меньшей мере еще 1 байт, но вполне очевидно, что этого недостаточно) Эта процедура использует функцию прерывания BIOS 0x15 (ax=0x8700) для перемещения в верхнюю память и переустанавливает %es так, что он всегда указывает на 0x10000. Это гарантирует, что bootsect.S не исчерпает нижнюю память при считывании данных с диска.

1.5 LILO в качестве загрузчика.

Специализированные загрузчики (например LILO) имеют ряд преимуществ перед чисто Linux-овым загрузчиком (bootsector):

  1. Возможность выбора загрузки одного из нескольких ядер Linux или одной из нескольких ОС.
  2. Возможность передачи параметров загрузки в ядро (существует патч BCP который добавляет такую же возможность и к чистому bootsector+setup).
  3. Возможность загружать большие ядра (bzImage) - до 2.5M (против обычного 1M).

Старые версии LILO ( версии 17 и более ранние) не в состоянии загрузить ядро bzImage. Более новые версии (не старше 2-3 лет) используют ту же методику, что и bootsect+setup, для перемещения данных из нижней в верхнюю память посредством функций BIOS. Отдельные разработчики (особенно Peter Anvin) выступают за отказ от поддержки ядер zImage. Тем не менее, поддержка zImage остается в основном из-за (согласно Alan Cox) существования некоторых BIOS-ов, которые не могут грузить ядра bzImage, в то время как zImage грузятся ими без проблем.

В заключение, LILO передает управление в setup.S и далее загрузка продолжается как обычно.

1.6 Высокоуровневая инициализация

Под "высокоуровневой инициализацией" следует понимать действия, непосредственно не связанные с начальной загрузкой, даже не смотря на то, что часть кода, выполняющая ее, написана на ассемблере, а именно в файле arch/i386/kernel/head.S, который является началом декомпрессированного ядра. При инициализации выполняются следующие действия:

  1. Устанавливаются сегментные регистры (%ds = %es = %fs = %gs = __KERNEL_DS = 0x18).
  2. Инициализируются таблицы страниц.
  3. Разрешается листание страниц, установкой бита PG в %cr0.
  4. Обнуляется BSS (для SMP (мультипроцессорных систем (прим. перев.)), это действие выполняет только первый CPU).
  5. Копируются первые 2k bootup параметров (kernel commandline).
  6. Проверяется тип CPU, используя EFLAGS и, если возможно, cpuid, позволяющие обнаружить процессор 386 и выше.
  7. Первый CPU вызывает start_kernel(), все остальные - arch/i386/kernel/smpboot.c:initialize_secondary(), если переменная ready=1, которая только переустанавливает esp/eip.

Функция init/main.c:start_kernel() написана на C и выполняет следующие действия:

  1. Выполняется глобальная блокировка (необходимая для того, чтобы через процесс инициализации проходил только один CPU)
  2. Выполняются платформо-зависимые настройки (анализируется раскладка памяти, копируется командная строка и пр.).
  3. Вывод "баннера" ядра, который содержит версию, компилятор, использованные при сборке, и пр., в кольцевой буфер для сообщений. Текст "баннера" задается в переменной linux_banner, определенной в init/version.c. Текст этот можно вывести на экран командой cat /proc/version.
  4. Инициализация ловушек.
  5. Инициализация аппаратных прерываний (irqs).
  6. Инициализация данных для планировщика.
  7. Инициализация данных хранения времени.
  8. Инициализация подсистемы программных прерываний (softirq).
  9. Разбор параметров командной строки.
  10. Инициализация консоли.
  11. Если ядро было скомпилировано с поддержкой загружаемых модулей, инициализируется подсистема динамической загрузки модулей.
  12. Инициализируются профилирующие буферы, если командная строка содержит указание "profile=".
  13. kmem_cache_init(), начало инициализации менеджера памяти.
  14. Разрешаются прерывания.
  15. Подсчет значения BogoMips для данного CPU.
  16. Вызывается mem_init() которая подсчитывает max_mapnr, totalram_pages и high_memory и выводит строку "Memory: ...".
  17. kmem_cache_sizes_init(), завершение инициализации менеджера памяти.
  18. Инициализация структур данных для procfs.
  19. fork_init(), создает uid_cache, инициализируется max_threads исходя из объема доступной памяти и конфигурируется RLIMIT_NPROC для init_task как max_threads/2.
  20. Создаются различные кэши для VFS, VM, кэш буфера и пр..
  21. Инициализируется подсистема IPC, если имеется поддержка System V IPC. Обратите внимание, что для System V shm, это включает монтирование внутреннего (in-kernel) экземпляра файловой системы shmfs.
  22. Создается и инициализируется специальный кэш, если поддержка квот (quota) включена.
  23. Выполняется платформо-зависимая "проверка ошибок" ("check for bugs") и, если это возможно, активируется обработка ошибок процессора/шины/проч. Сравнение различных архитектур показывает, что "ia64 не имеет ошибок" а "ia32 имеет несколько дефектов", хороший пример - "дефект f00f" который проверен только для ядра, собранного под процессор ниже, чем 686.
  24. Устанавливается флаг, указывающий на то, что планировщик должен быть вызван "при первой возможности" и создается поток ядра init(), который выполняет execute_command, если она имеется среди параметров командной строки в виде "init=", или пытается запустить /sbin/init, /etc/init, /bin/init, /bin/sh в указанном порядке; если не удается ни один из запусков то ядро "впадает в панику" с "предложением" задать параметр "init=".
  25. Переход в фоновый поток с pid=0.

Здесь важно обратить внимание на то, что задача init() вызывает функцию do_basic_setup(), которая в свою очередь вызывает do_initcalls() для поочередного (в цикле) вызова функций, зарегистрированных макросом __initcall или module_init() Эти функции либо являются независимыми друг от друга, либо их взаимозависимость должна быть учтена при задании порядка связывания в Makefile - ах. Это означает, что порядок вызова функций инициализации зависит от положения каталогов в дереве и структуры Makefile - ов. Иногда порядок вызова функций инициализации очень важен. Представим себе две подсистемы: А и Б, причем Б существенным образом зависит от того как была проинициализирована подсистема А. Если А скомпилирована как статическая часть ядра, а Б как подгружаемый модуль, то вызов функции инициализации подсистемы Б будет гарантированно произведен после инициализации подсистемы А. Если А - модуль, то и Б так же должна быть модулем, тогда проблем не будет. Но что произойдет, если и А, и Б скомпилировать с ядром статически? Порядок, в котором они будут вызываться (иницализироваться) зависит от смещения относительно точки .initcall.init ELF секции в образе ядра (грубо говоря - от порядка вызова макроса __initcall или module_init() прим. перев.). Rogier Wolff предложил ввести понятие "приоритетной" инфраструктуры, посредством которой модули могли бы задавать компоновщику порядок связывания, но пока отсутствуют заплаты, которые реализовали бы это качество достаточно изящным способом, чтобы быть включенным в ядро. А посему необходимо следить за порядком компоновки. Если А и Б (см. пример выше) скомпилированы статически и работают корректно, то и при каждой последующей пересборке ядра они будут работать, если порядок следования их в Makefile не изменяется. Если же они не функционируют, то стоит изменить порядок следования объектных файлов.

Еще одна замечательная особенность Linux - это возможность запуска "альтернативной программы инициализации", если ядру передается командная строка "init=". Эта особенность может применяться для перекрытия /sbin/init или для отладки скриптов инициализации (rc) и /etc/inittab вручную, запуская их по одному за раз

1.7 SMP Загрузка на x86

В случае SMP (многопроцессорной системы), первичный процессор проходит обычную последовательность - bootsector, setup и т.д., пока не встретится вызов функции start_kernel(), в которой стоит вызов функции smp_init(), откуда вызывается arch/i386/kernel/smpboot.c:smp_boot_cpus(). Функция smp_boot_cpus() в цикле (от 0 до NR_CPUS) вызывает do_boot_cpu() для каждого apicid. Функция do_boot_cpu() создает (т.е. fork_by_hand) фоновую задачу для указанного CPU и записывает, согласно спецификации Intel MP (в 0x467/0x469) трамплин-код, содержащийся в trampoline.S. Затем генерирует STARTUP IPI, заставляя вторичный процессор выполнить код из trampoline.S.

Ведущий процессор создает трамплин-код для каждого процессора в нижней памяти. Ведомый процессор, при исполнении "трамплина", записывает "магическое число", чтобы известить ведущий процессор, что код исполнен. Требование, по размещению трамплин-кода в нижней памяти, обусловлено спецификацией Intel MP.

Трамплин-код просто записывает 1 в %bx, переводит процессор в защищенный режим и передает управление на метку startup_32, которая является точкой входа в arch/i386/kernel/head.S.

При исполнении кода head.S, ведомый CPU обнаруживает, что он не является ведущим, перепрыгивает через очистку BSS и входит в initialize_secondary() которая переходит в фоновую задачу для данного CPU - минуя вызов init_tasks[cpu], поскольку она уже была проинициирована ведущим процессором при исполнении do_boot_cpu(cpu).

Характерно, что код init_task может использоваться совместно, но каждая фоновая задача должна иметь свой собственный TSS. Именно поэтому init_tss[NR_CPUS] является массивом.

1.8 Освобождение памяти после инициализации

После выполнения инициализации операционной системы, значительная часть кода и данных становится ненужной. Некоторые системы (BSD, FreeBSD и пр.) не освобождают память, занятую этой ненужной информацией. В оправдание этому приводится (см. книгу McKusick-а по 4.4BSD): "данный код располагается среди других подсистем и поэтому нет никакой возможности избавиться от него". В Linux, конечно же такое оправдание невозможно, потому что в Linux "если что-то возможно в принципе, то это либо уже реализовано, либо над этим кто-то работает".

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

Макросы подсчитывают размер этих секций в спецификаторах аттрибутов gcc, и определены в include/linux/init.h:


#ifndef MODULE
#define __init        __attribute__ ((__section__ (".text.init")))
#define __initdata    __attribute__ ((__section__ (".data.init")))
#else
#define __init
#define __initdata
#endif


Что означает - если код скомпилирован статически (т.е. литерал MODULE не определен), то он размещается в ELF-секции .text.init, которая объявлена в карте компоновки arch/i386/vmlinux.lds. В противном случае (т.е. когда компилируется модуль) макрос ничего не делает.

Таким образом, в процессе загрузки, поток ядра "init" (функция init/main.c:init()) вызывает функцию free_initmem(), которая и освобождает все страницы памяти между адресами __init_begin и __init_end.

На типичной системе (на моей рабочей станции) это дает примерно 260K памяти.

Код, регистрирующийся через module_init(), размещается в секции .initcall.init, которая так же освобождается. Текущая тенденция в Linux - при проектировании подсистем (не обязательно модулей) закладывать точки входа/выхода на самых ранних стадиях с тем, чтобы в будущем, рассматриваемая подсистема, могла быть модулем. Например: pipefs, см. fs/pipe.c. Даже если подсистема никогда не будет модулем напрмер bdflush (см. fs/buffer.c), все равно считается хорошим тоном использовать макрос module_init() вместо прямого вызова функции инициализации, при условии, что не имеет значения когда эта функция будет вызвана.

Имеются еще две макрокоманды, работающие подобным образом. Называются они __exit и __exitdata, но они более тесно связаны с поддержкой модулей, и поэтому будет описаны ниже.

1.9 Разбор командной строки

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

  1. LILO (или BCP) воспринимает командную строку через сервис клавиатуры BIOS-а, и размещает ее в физической памяти.
  2. Код arch/i386/kernel/head.S копирует первые 2k в нулевую страницу (zeropage). Примечательно, что текущая версия LILO (21) ограничивает размер командной строки 79-ю символами. Это не просто ошибка в LILO (в случае включенной поддержки EBDA(LARGE_EBDA (Extended BIOS Data Area) --необходима для некоторых современных мультипроцессорных систем. Заставляет LILO загружаться в нижние адреса памяти, с целью оставить как можно больше пространства для EBDA, но ограничивает максимальный размер для "малых" ядер - т.е. "Image" и "zImage" прим. перев. )). Werner пообещал убрать это ограничение в ближайшее время. Если действительно необходимо передать ядру командную строку длиной более 79 символов, то можно использовать в качестве загрузчика BCP или подправить размер командной строки в функции arch/i386/kernel/setup.c:parse_mem_cmdline().
  3. arch/i386/kernel/setup.c:parse_mem_cmdline() (вызывается из setup_arch(), которая в свою очередь вызывается из start_kernel()), копирует 256 байт из нулевой страницы в saved_command_line, которая отображается в /proc/cmdline. Эта же функция обрабатывает опцию "mem=", если она присутствует в командной строке, и выполняет соответствующие корректировки параметра VM.
  4. далее, командная строка передается в parse_options() (вызывается из start_kernel()), где обрабатываются некоторые "in-kernel" параметры (в настоящее время "init=" и параметры для init) и каждый параметр передается в checksetup().
  5. checksetup() проходит через код в ELF-секции .setup.init и вызывает каждую функцию, передавая ей полученное слово. Обратите внимание, что если функция, зарегистрированная через __setup(), возвращает 0, то становится возможной передача одного и того же "variable=value" нескольким функциям. Одни из них воспринимают параметр как ошибочный, другие -как правильный. Jeff Garzik говорит по этом у поводу: "hackers who do that get spanked :)" (не уверен в точности перевода, но тем не менее "программисты, работающие с ядром, иногда получают щелчок по носу". прим. перев.). Почему? Все зависит от порядка компоновки ядра, т.е. в одном случае functionA вызывается перед functionB, порядок может быть изменен с точностью до наоборот, результат зависит от порядка следования вызовов.

Для написания кода, обрабатывающего командную строку, следует использовать макрос __setup(), определенный в include/linux/init.h:


/*
 * Used for kernel command line parameter setup
 */
struct kernel_param {
        const char *str;
        int (*setup_func)(char *);
};

extern struct kernel_param __setup_start, __setup_end;

#ifndef MODULE
#define __setup(str, fn) \
   static char __setup_str_##fn[] __initdata = str; \
   static struct kernel_param __setup_##fn __initsetup = \
   { __setup_str_##fn, fn }

#else
#define __setup(str,func) /* nothing */
endif


Ниже приводится типичный пример, при написании собственного кода (пример взят из реального кода драйвера BusLogic HBA drivers/scsi/BusLogic.c):


static int __init
BusLogic_Setup(char *str)
{
        int ints[3];

        (void)get_options(str, ARRAY_SIZE(ints), ints);

        if (ints[0] != 0) {
                BusLogic_Error("BusLogic: Obsolete Command Line Entry "
                                "Format Ignored\n", NULL);
                return 0;
        }
        if (str == NULL || *str == '\0')
                return 0;
        return BusLogic_ParseDriverOptions(str);
}

__setup("BusLogic=", BusLogic_Setup);


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


Вперед Назад Содержание