Завантаження

Побудова образу ядра Linux

У цьому розділі пояснено послідовність компілювання ядра Linux та результати отримані на кожному етапі. Процес побудови образу ядра залежить від архітектури комп'ютера, тому хотілося б наголосити, що розглядатиметься лише побудова ядра Linux/x86.

Коли користувач набирає «make zImage» або «make bzImage», одержаний образ ядра, здатного до завантаження, записується у файлі arch/i386/boot/zImage або arch/i386/boot/bzImage відповідно. Ось як відбувається побудова ядра:

  1. Вихідні файли на C та асемблері компілюються у файли в об'єктному пересувному форматі ELF (.o), а деякі з них логічно групуються у архіви (.a) за допомогою ar(1).
  2. Вищезгадані файли .a та .o за допомогою ld(1) зв'язуються у vmlinux, який є статично зв'язаним 32-ох бітним виконавчим файлом у форматі ELF LSB 80386.
  3. nm vmlinux створює файл System.map і всі зайві символи видаляються.
  4. Перехід у директорію arch/i386/boot.
  5. Асемблерний код сектора завантаження перетворюється у файл bbootsect.s чи bootsect.s з чи без D__BIG_KERNEL__, залежно від того, чи цільовим файлом є bzImage чи zImage відповідно.
  6. Асемблюється файл bbootsect.s та перетворюється у двійкову форму, яка називається bbootsect (для bzImage) або асемблюється bootsect.s та перетворюється у bootsect (для zImage).
  7. Код програми підготовки до роботи setup.S (setup.S містить video.S) перетворюється у bsetup.s (для bzImage) або setup.s - у bsetup.s (для zImage). Так само як і у випадку з кодом сектора завантаження, різниця полягає у наявності D__BIG_KERNEL__ у bzImage. Після цього результат конвертується у двійкову форму, яка називається bsetup.
  8. Перехід у директорію arch/i386/boot/compressed і конвертація файлу /usr/src/linux/vmlinux у $tmppiggy (ім'я тимчасового файлу) у двійковому форматі, з видаленням ELF секцій .note та .comment.
  9. gzip -9 < $tmppiggy > $tmppiggy.gz.
  10. Компонування $tmppiggy.gz у пересувний ELF-код piggy.o (ld -r).
  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 у bvmlinux.out в двійковій формі з видаленням ELF-секцій .note та .comment.
  14. Повернення до директорії arch/i386/boot та конкатенація файлів bbootsect, bsetup та compressed/bvmlinux.out у bzImage (для zImage зітріть зайву літеру 'b') за допомогою програми tools/build. При цьому у кінці сектора початкового завантаження буде записано такі важливі змінні, як setup_sects і root_dev.

Розмір сектора початкового завантаження завжди дорівнює 512 байтам. Розмір програми підготовки до роботи повинен бути більшим за 4 сектори, але меншим 12 кілобайт. Загалом правило таке:

0x4000 байт >= 512 + setup_sects * 512 + місце для стеку під час виконання bootsector/setup.

Пізніше ми побачимо причину такого обмеження.

Верхня межа на розмір bzImage, отриманого на даному етапі, дорівнює приблизно 2,5 мегабайт при завантаженні з LILO та 0xFFFF параграфів (0xFFFF0 = 1048560 байт) для завантаження з готового образу, тобто з флопі-диску чи CD-ROM (в режимі емуляції El-Torito).

Зверніть увагу, що хоча програма tools/build перевіряє розмір сектора завантаження, образу ядра та нижньої межі розміру програми підготовки до роботи, вона не перевіряє верхню межу зазначеного розміру програми підготовки до роботи. Тому легко збудувати «розбите» ядро лише додавши занадто великий розмір «.space» у кінці setup.S.

Завантаження: загальний огляд

Особливості процесу завантаження залежать від конкретної архітектури, тому ми зосередимо увагу на архітектурі IBM PC/IA32. З причин застарілої розробки та зворотньої сумісності вбудоване прогрмне забезпечення компютера PC завантажує операційну систему у застарілий спосіб. Цей процес можна поділити на наступні логічні кроки:

  1. BIOS вибирає завантажувальний пристрій, з якого відбуватиметься завантаження.
  2. BIOS зчитує у память сектор завантаження з вибраного завантажувального пристрою.
  3. Сектор завантаження завантажує програму підготовки до роботи, підпрограми декомпресії та скомпресований образ ядра.
  4. Відбувається декомпресія ядра у захищеному режимі.
  5. Низькорівнева ініціалізація асемблерним кодом.
  6. Високорівнева ініціалізація С-кодом.

Завантаження: BIOS POST

  1. Подача живлення запускає тактовий генератор і встановлює сигнал #POWERGOOD на системній шині.
  2. Подається сигнал на вивід #RESET центрального процесора (зараз процесор працює в реальному режимі 8086).
  3. Регістри процесора свтановлюються у наступні стани: DS=ES=FS=GS=SS=0, CS=0xFFFF0000, EIP=0x0000FFF0 (код режиму POST (power-on self test), що зберігається у ROM BIOS).
  4. Відключаються переривання і здійснюються всі перевірки тесту POST.
  5. Ініціалізується таблиця векторів переривань на адресі 0.
  6. Викликається BIOS-функція початкового самозавантаження за допомогою переривання INT 19h, при чому DL містить «номер диску» пристрою завантаження, з якого відбуватиметься завантаження. Ця функція завантажує доріжку 0 сектора 1 за фізичною адресою 0x7C00 (0x07C0:0000).

Завантаження: завантажувальний сектор і підготовка до роботи.

Можливі такі варіанти boot-сектора для завантаження ядра Linux:

  • завантажувальний сектор Linux (arch/i386/boot/bootsect.S);
  • завантажувальний сектор LILO (чи іншої програми завантаження);
  • відсутній завантажувальний сектор (loadlin, тощо);

Розгляньмо детально завантажувальний сектор 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. Значення 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, щоб застосувати налагоджувач (debugger)
66  # за  адресою 0x6400 і вище (bde).  Нам не треба турбуватися про це, якщо  ми
67  # перевірили верхні адреси пам'яті.  Також мою BIOS можна сконфігурувати на завантаження таблиці дисків
68  # wini у верхню пам'ять, а не у таблицю векторів.
69  # Старий стек міг розладнати таблицю дисків.
70  go:     movw    $0x4000-12, %di   # 0x4000 є довільною величиною >=
71                                    # довжина boot-сектора + довжина програми
72                                    # підготовки до роботи + місце для стеку;
73                                    # 12 - розмір параметрів диску.
74          movw    %ax, %ds          # AX та ES уже містять INITSEG
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. Встановлюється кількість 16-бітових слів у CX (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). Звідси й обмеження на розмір програми підготовки до роботи, згадане вище (див. Побудова образу ядра Linux).

Рядки 77-103 виправляють таблицю параметрів диску для першого диску, щоб дозволити одночасне зчитування багатьох секторів:

 77  # Багато таблиць параметрів дисків у BIOS за замовчуванням не допускають
 78  # багатосекторні зчитування, які перевищують максимальну кількість секторів, зазаначену
 79  # у таблицях параметрів дискети за замовчуванням - у деяких випадках це може
 80  # означати 7 секторів.
 81  #
 82  # Оскільки односекторні зчитування є повільними і тому не підходять,
 83  # необхідно потурбуватись про це і створити нову таблицю параметрів
 84  # (для першого диску) в оперативній памяті. Ми встановимо лічильник максимальної
 85  # кількості секторів на 36 - найбільшу кількість секторів, яку можна знайти на дискетах
 86  # типу ED 2.88.
 87  # Багато не зашкодить. Нестача - зашкодить.
 88  #
 89  # Сегменти мають наступні значення: DS=ES=SS=CS=INITSEG, FS=0,
 90  # а GS не використовується.
 91          movw    %cx, %fs                # встановити FS у 0
 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 переривання BIOS INT 13h (повернути у вихідний стан контролер гнучкого диска) і сектори програми підготовки до роботи завантажуються одразу після сектора завантаження, тобто за фізичною адресою 0x90200 ($INITSEG:0x200), застосовуючи службову функцію 2 переривання BIOS INT 13h (читати сектор(и)). Це відбувається протягом виконання рядків 107-124:

107  load_setup:
108          xorb    %ah, %ah                # встановити контролер гнучкого диску у вихідний  стан
109          xorb    %dl, %dl
110          int     $0x13  
111          xorw    %dx, %dx                # диск 0, головка 0
112          movb    $0x02, %cl              # сектор 2, доріжка 0
113          movw    $0x0200, %bx            # адреса = 512, у INITSEG
114          movb    $0x02, %ah              # функція 2, "читати сектор(и)"
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. Це робиться для того, щоб зберегти області даних вбудованого програмного забезпечення у низькоадресній пам'яті (0-64K). Після того, як ядро завантажиться, виконується перехід до $SETUPSEG:0 (arch/i386/boot/setup.S). Як тільки ці дані стають непотрібними (наприклад, більше не здійснюються звертання до BIOS) вони перезаписуються перенесенням цілого (стисненого) образу ядра з 0x10000 to 0x1000 (фізичні адреси, звичайно). Це здійснює setup.S, який готує все до захишеного режиму і переходить до адреси 0x1000, де знаходиться початок зкомпресованого ядра, тобто arch/386/boot/compressed/{head.S,misc.c}. Ця програма підготовляє стек і здійснює виклик decompress_kernel(), що декомпресує ядро на адреси починаючи з 0x100000 і передає йому керування.

Варто зауважити, що старі програми завантаження (старі версії LILO) могли завантажити лише перші 4 сектори програми підготовки до роботи, тому у програмі підготовки до роботи є код для завантаження решти програми при потребі. Також код програми підготовки до роботи повинен передбачати різні комбінації типів і версій програми завантаження та zImage/bzImage, що робить його надзвичайно складним.

Давайте розглянемо частину коду сектора завантаження яка дозволяє завантажувати велике ядро, відоме також як «bzImage». Сектори програми підготовки до роботи розташовуються зазвичай за адресою 0x90200, але ядро завантажується порціями по 64 кілобайти із застосуванням спеціальної допоміжної підпрограми яка викликає BIOS щоб перенести дані з низькоадресної пам'яті до високоадресної. До цієї допоміжної підпрограми звертається bootsect_kludge у bootsect.S і вона визначена як bootsect_helper у setup.S. Мітка bootsect_kludge у setup.S містить значення сегменту програми підготовки до роботи та зміщення коду bootsect_helper у ньому, так, щоб сектор завантаження міг використати команду lcall щоб перейти до нього (міжсегментний перехід). Причиною того, що ця мітка знаходиться у setup.S є те, що у bootsect.S просто немає вільного місця (що не зовсім точно: у bootsect.S є приблизно 4 незайняті байти, і щонайменше 1 незайнятий байт у bootsect.S, але цього явно не достатньо). Ця процедура використовує переривання BIOS INT 15h (ax=0x8700) для переміщення до високоадресної пам'яті і встановлює ES у 0x10000. Це не дає коду bootsect.S вичерпати низькоадресну пам'ять при копіюванні даних з диску.

Використання LILO в якості програми початкового завантаження

Застосування спеціалізованої програми завантаження (LILO) має ряд переваг над простим boot-сектором Linux:

  1. Можливість вибирати завантаження з багатьох ядер Linux або навіть з багатьох операційних систем.
  2. Можливість передавати ядру параметри командного рядка (існує виправлення, що називається BCP, яке додає цю можливість до простого сектора завантаження з програмою підготовки до роботи).
  3. Можливість завантажувати значно більші ядра bzImage — аж до 2,5 мегабайти (проти одного мегабайта).

Старі версії LILO (1.7 та раніші) не могли завантажувати ядра bzImage. Новіші версії (станом на кілька років тому) застосовують таку саму техніку як сектор завантаження + програма підготовки до роботи для перенесення даних з низькоадресної пам'яті до високоадресної за допомогою засобів BIOS. Дехто (особливо Пітер Енвін (Peter Anvin)) стверджують, що підтримку zImage слід усунути. Кажуть (Алан Кокс (Alan Cox)), що головною причиною для цього є деякі явно несправні BIOS'и, які унеможливлюють завантаження ядер bzImage, тоді як ядра zImage завантажуються нормально.

На останок LILO передає керування до setup.S і все йде за планом.

Ініціалізація високого рівня

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

  1. Ініціалізація сегментних даних (DS = ES = FS = GS = __KERNEL_DS = 0x18).
  2. Ініціалізація таблиці сторінок.
  3. Дозвіл на посторінкове звертання до пам'яті шляхом встановлення біту PG у %CR0.
  4. Обнулення блоку, що починається з символу (у багатопроцесорних системах цю операцію виконує тільки перший процесор).
  5. Копіювання перших 2-ох кілобайт параметрів запуску (командний рядок ядра).
  6. Перевірка типу процесора за допомогою EFLAGS та, по можливості, CPUID, що визначає процесор і386 та вище.
  7. Перший процесор викликає start_kernel(), а усі інші — arch/i386/kernel/smpboot.c:initialize_secondary() якщо ready=1, що лише перезавантажує ESP/EIP і не повертає керування.

Функцію init/main.c:start_kernel() написано на мові C і здійснює вона наступне:

  1. Глобальне блокування ядра (воно необхідне для того, щоб тільки один процесор виконував ініціалізацію).
  2. Виконання машинно-залежної підготовки до роботи (аналіз розміщення пам'яті, повторне копіювання командного рядка завантаження, тощо).
  3. Вивід «заголовку» ядра Linux, що містить версію, компілятор, використаний для його побудови і т.п., у кільцевий буфер ядра, призначений для повідомлень. Його отримують із змінної linux_banner, визначеної у init/version.c, що є аналогічна рядку, який видає cat /proc/version.
  4. Ініціалізація переривань.
  5. Ініціалізація запитів на переривання IRQ.
  6. Ініціалізація даних, необхідних для програми планування.
  7. Ініціалізація даних про час.
  8. Ініціалізація підсистеми програмних запитів на переривання IRQ.
  9. Аналіз опцій командного рядка завантаження.
  10. Ініціалізація консолі.
  11. Якщо при компіляції в ядро було вбудовано підтримку модулів, то ініціалізуються засоби динамічного завантаження модулів.
  12. Якщо у командному рядку було подано"profile=", то ініціалізуються буфери профілювання.
  13. Виконання kmem_cache_init() та ініціалізаціярозподілювача (менеджера) пам'яті.
  14. Дозвіл на переривання.
  15. Визначення величини BogoMips для даного процесора.
  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. Якщо скомпільовано підтримку міжпроцесних зв'язків для Unix System V Release 4, то ініціалізується підсистема міжпроцесних зв'язків. Зауважте що для System V shm це включає монтування внутрішнього (внутрішньо-ядерного) екземпляра файлової системи shmfs.
  22. Створення та ініціалізація спеціального кешу пам'яті ядра, якщо в ядро включено підтримку квот.
  23. Виконання машинно-залежної перевірки помилок і, якщо можливо, активація засобів обходу помилок процесора/шини/тощо. Порівняння різних архітектур показує що в IA64 "немає помилок", у IA32 "досить низька кількість помилок"; хорошим прикладом є "помилка F00F", яка перевіряється і обходиться відповідним чином лише за умови, що ядро зкомпільовано для процесора нижче I686.
  24. Встановлюється прапор, який сигналізує що системний планувальник повинен бути активізований при «наступній нагоді» і створюється підпроцес ядра init(), що виконує execute_command, якщо команду подано через параметр завантаження «init=», або намагається виконати /sbin/init, /etc/init, /bin/init, /bin/sh у поданому порядку; якщо це все не дає результату — вмикається режим panic з «пропозицією» використати параметр «init=».
  25. Вхід у цикл очікування, який є незавантаженим підпроцесом з ідентифікатором pid=0.

Тут важливо зауважити, що підпроцес ядра init() викликає do_basic_setup(), який в свою чергу викликає do_initcalls(), що проходить по списку функцій, зареєстрованих за допомогою макросу __initcall або module_init() та запускає їх. Ці функції або не залежать одна від одної, або їхні залежності було усунуто вручну шляхом задання порядку підключення у Makefile-ах. Це означає, що залежно від позиції директорій у деревах і структурі Makefile-ів, порядок запуску ініціалізації функцій може змінюватись. Деколи це важливо. Уявіть собі дві підсистеми А і В, де В залежить від певної ініціалізації, здійснюваної A. Якщо А скомпільовано статично, а В є модулем, то є гарантія активізації точки входу в В після того, як А підготує необхідне середовище. Якщо А є модулем, тоді В також обов'язково є модулем і жодних проблем не виникає. А що, коли і А, і В статично приєднані до ядра? Порядок, у якому вони запускаються, залежить від відносного зміщення точок входу у ELF-секції .initcall.init образу ядра. Роджер Вульф (Rogier Wolff) запропонував впровадити ієрархічну «пріоритетну» інфраструктуру, за допомогою якої модулі могли б повідомляти редактор зв'язків, у якому порядку (відносному) їх слід компонувати, але й дотепер немає виправлень, що здійснювали б це у спосіб, достатньо елегантний, щоб їх можна було вставити в ядро. Отже, переконайтеся, що ваш порядок компоновки є правильним. Якщо у поданому вище прикладі А і В працюють добре після того, як їх одного разу скомпілювали статично, то вони працюватимуть завжди, за умови, що вони послідовно подані у тому ж Makefile. Якщо вони не працюють, змініть порядок перечислення їхніх об'єктних файлів.

Також вартою уваги є здатність Лінукса виконувати «альтернативну init-програму» за допомогою передачі «init=» у командному рядку завантаження. Це корисно при відновленні випадково перезаписаного файлу /sbin/init або при налагодженні сценаріїв ініціалізації (rc) та /etc/inittab вручну, виконуючи їх по одному.

SMP завантаження на платфомі x86

У багатопроцесорних системах базовий процесор проходить через нормальну послідовність - завантажувальний сектор, програма підготовки до роботи і т. д., поки не досягне start_kernel(), а тоді переходить до smp_init() та src/i386/kernel/smpboot.c:smp_boot_cpus(). smp_boot_cpus() проходить у циклі (до NR_CPUS) і викликає do_boot_cpu() для кожного apicid. Завдання do_boot_cpu() полягає у створенні (fork_by_hand) циклу очікування (idle task) для цільвого процесора і записі в чітко визначених місцях, зазначених у специфікації Intel MP (0x467/0x469) EIP стартовго коду, що знаходиться у trampoline.S. Потім він генерує STARTUP IPI для цільового процесора, змушуючи даний прикріплений процесор виконати код із trampoline.S.

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

Стартовий код просто встановлює регістр BX у стан 1, входить у захищений режим та переходить до startup_32, який є головним входом до arch/i386/kernel/head.S. Тепер прикріплений процесор починає виконувати head.S і, виявивши що він не є базовим процесором, пропускає код очистки блоку, що починається з символу, а тоді входить у initialize_secondary(), що просто переходить у режим очікування для даного процесора — пригадайте, що init_tasks[cpu] уже було проініціалізовано виконанням do_boot_cpu(cpu) на базовому процесорі.

Зауважте, що init_task можна розділювати, але кожен процес очікування повинен мати свій власний сегмент стану завдання (TSS). Саме тому init_tss[NR_CPUS] є масивом.

Вивільнення даних та коду ініціалізації

Коли ініціалізація операційної системи завершується, більша частина коду та структур даних стають непотрібними. Більшість операційних систем (BSD, FreeBSD,тощо) не можуть позбутися цієї непотрібної інформації, марнуючи таким чином дорогоцінну фізичну пам'ять ядра. Виправданням цьому (див. книгу «4.4BSD» Маккасіка (McKusick)) є те, що «відповідний код розповсюджено по різним підсистемам і тому його не можна вивільнити». Linux, звичайно, не може послуговуватися такими виправданнями, оскільки у Linux «якщо щось є принципово можливим, то це вже реалізували або хтось уже працює над цим».

Отже, як було сказано раніше, ядро Лінукса можна скомпілювати лише як двійковий ELF-файл, і причину (або однією з причин) цього буде пояснено пізніше. Для вивільнення коду/даних ініціалізації в Linux наявні два макроси:

  1. __init — для коду ініціалізації;
  2. __initdata — для даних.

Вони прирівнюються до специфікаторів атрибутів gcc (також відомі як «маґія 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(), яка вивільняє всі сторінки пам'яті між addresses __init_begin та __init_end. На типовій системі (моїй робочій станції) це призводить до вивільнення близько 260 кілобайт пам'яті.

Функції, зареєстровані за допомогою module_init() розміщено у секції .initcall.init, яка також вивільняється у випадку статичної компіляції. Сучасною тенденцією при розробці підсистеми (не обов'язково модуля) у Лінуксі є забезпечення вхідних точок init/exit з найперших етапів розробки, щоб у майбутньому необхідні підсистеми можна було виконати при потребі у вигляді модулів. Прикладом цього є pipefs, див. fs/pipe.c. Навіть якщо дана підсистема ніколи не стане модулем, наприклад bdflush (див. fs/buffer.c), хорошим тоном вважається використання макросу module_init() до функції ініціалізації, за умови, що немає значення, коли насправді функція викликається.

Існують ще два макроси, які працюють подібним чином: __exit та __exitdata, але вони більше пов'язані з підтримкою модулів і, отже, будуть пояснені пізніше.

Обробка командного рядка Linux

Давайте пригадаємо, що відбувається при передачі командного рядка ядру під час завантаження:

  1. LILO (або дублюючий командний процесор (BCP)) приймає командний рядок за допомогою BIOS-служб клавіатури і зберігає його у точно визначеному місці у фізичній пам'яті, так само як і сигнал, який підвтерджує, що там знаходиться придатний до виконання командний рядок.
  2. arch/i386/kernel/head.S копіює перші 2 кілобайти рядка у нульову сторінку. Зверніть увагу, що поточна версія (21) LILO обрубує командний рядок до 79 байт. Це нетривіальна помилка у LILO (коли задіяно підтримку EBDA (розширених зон даних BIOS)) і Вернер (Werner) пообіцяв виправити її незабаром. Якщо справді треба передавати командні рядки довші за 79 байт, тоді можна використати дублюючий сомандний процесор або жорстко запрограмувати ваш командний рядок у функції arch/i386/kernel/setup.c:parse_mem_cmdline().
  3. arch/i386/kernel/setup.c:parse_mem_cmdline() (яку викликає setup_arch(), викликана в свою чергу з start_kernel()) копіює 256 байт з нульової сторінки у збережений командний рядок, який відображається у /proc/cmdline. Та сама процедура обробляє опцію «mem=», якщо така є, і виконує відповідні регулювання параметрів віртуальної пам'яті.
  4. Командний рядок передається у parse_options() (викликається з start_kernel()), що обробляє певні «внутрішньоядерні» параметри (на даний час «init=» та середовище/аргументи для init) та передає кожне слово у checksetup().
  5. checksetup() проходить через код у ELF-секції файлу .setup.init та активізує кожну функцію, передаючи їй слово, якщо підходить. Зауважте, що використовуючи значення 0, повернене з функції, зареєстрованої за допомогою __setup(), можна передати ті самі «змінна=значення» більш ніж одній функції зі «значенням», некоректним для однієї, і коректним для іншої. Джеф Гарзік (Jeff Garzik) коментує: «хакери, які роблять це, отримують ляпаса :)» Чому? Тому що це, безсумнівно, залежить від порядку компонування в ld, тобто одне ядро, скомпоноване в певному порядку активізуватиме функцію А перед функцією В, а в іншому ядрі порядок буде зворотнім, і результат залежатиме від порядку.

Отже, як написати код, який обробляє командний рядок при завантаженні? Для цого використовують макрос __setup(), визначений у include/linux/init.h:

/*
 * Використовується для підготовки до роботи параметрів командного рядка ядра
 */
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 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() нічого не виконує для модулів, отже код, який хоче обробити командний рядок завантаження і може бути або модулем, або статично скомпонованим, повинен ініціалізувати свою функцію синтаксичного аналізу вручну у процедурі ініціалізації модуля. Це також означає, що можна написати код, який обробляє параметри якщо його скомпільвано як модуль, а не статично, або навпаки.