|
|
Инициализация ядра. Часть 3.
|
|
|
================================================================================
|
|
|
|
|
|
Последние приготовления перед точкой входа в ядро
|
|
|
--------------------------------------------------------------------------------
|
|
|
|
|
|
Это третья часть серии Инициализация ядра. В предыдущей [части](linux-initialization-2.md) мы увидели начальную обработку прерываний и исключений и продолжим погружение в процесс инициализации ядра Linux в текущей части. Наша следующая точка - "точка входа в ядро" - функция `start_kernel` из файла [init/main.c](https://github.com/torvalds/linux/blob/16f73eb02d7e1765ccab3d2018e0bd98eb93d973/init/main.c). Да, технически это не точка входа в ядро, а начало кода ядра, который не зависит от определённой архитектуры. Но прежде чем мы вызовем функцию `start_kernel`, мы должны совершить некоторые приготовления. Давайте продолжим.
|
|
|
|
|
|
Снова boot_params
|
|
|
--------------------------------------------------------------------------------
|
|
|
|
|
|
В предыдущей части мы остановились на настройке таблицы векторов прерываний и её загрузки в регистр `IDTR`. На следующем шаге мы можем видеть вызов функции `copy_bootdata`:
|
|
|
|
|
|
```C
|
|
|
copy_bootdata(__va(real_mode_data));
|
|
|
```
|
|
|
|
|
|
Эта функция принимает один аргумент - виртуальный адрес `real_mode_data`. Вы должны помнить, что мы передали адрес структуры `boot_params` из [arch/x86/include/uapi/asm/bootparam.h](https://github.com/torvalds/linux/blob/16f73eb02d7e1765ccab3d2018e0bd98eb93d973/arch/x86/include/uapi/asm/bootparam.h#L114) в функцию `x86_64_start_kernel` как первый параметр в [arch/x86/kernel/head_64.S](https://github.com/torvalds/linux/blob/16f73eb02d7e1765ccab3d2018e0bd98eb93d973/arch/x86/kernel/head_64.S):
|
|
|
|
|
|
```
|
|
|
/* rsi is pointer to real mode structure with interesting info.
|
|
|
pass it to C */
|
|
|
movq %rsi, %rdi
|
|
|
```
|
|
|
|
|
|
Взглянем на макрос `__va`. Этот макрос определён в [init/main.c](https://github.com/torvalds/linux/blob/16f73eb02d7e1765ccab3d2018e0bd98eb93d973/init/main.c):
|
|
|
|
|
|
```C
|
|
|
#define __va(x) ((void *)((unsigned long)(x)+PAGE_OFFSET))
|
|
|
```
|
|
|
|
|
|
где `PAGE_OFFSET` это `__PAGE_OFFSET` (`0xffff880000000000` и базовый виртуальный адрес прямого отображения всей физической памяти). Таким образом, мы получаем виртуальный адрес структуры `boot_params` и передаём его функции `copy_bootdata`, в которой мы копируем `real_mod_data` в ` boot_params`, объявленный в файле [arch/x86/include/asm/setup.h](https://github.com/torvalds/linux/blob/16f73eb02d7e1765ccab3d2018e0bd98eb93d973/arch/x86/include/asm/setup.h)
|
|
|
|
|
|
```C
|
|
|
extern struct boot_params boot_params;
|
|
|
```
|
|
|
|
|
|
Давайте посмотрим на реализацию `copy_boot_data`:
|
|
|
|
|
|
```C
|
|
|
static void __init copy_bootdata(char *real_mode_data)
|
|
|
{
|
|
|
char * command_line;
|
|
|
unsigned long cmd_line_ptr;
|
|
|
|
|
|
memcpy(&boot_params, real_mode_data, sizeof boot_params);
|
|
|
sanitize_boot_params(&boot_params);
|
|
|
cmd_line_ptr = get_cmd_line_ptr();
|
|
|
if (cmd_line_ptr) {
|
|
|
command_line = __va(cmd_line_ptr);
|
|
|
memcpy(boot_command_line, command_line, COMMAND_LINE_SIZE);
|
|
|
}
|
|
|
}
|
|
|
```
|
|
|
|
|
|
Прежде всего, обратите внимание на то, что эта функция объявлена с префиксом `__init`. Это означает, что эта функция будет использоваться только во время инициализации и используемая память будет освобождена.
|
|
|
|
|
|
Мы можем видеть объявление двух переменных для командной строки ядра и копирование `real_mode_data` в `boot_params` функцией `memcpy`. Далее следует вызов функции `sanitize_boot_params`, которая заполняет некоторые поля структуры `boot_params`, такие как `ext_ramdisk_image` и т.д, если загрузчики не инициализировал неизвестные поля в `boot_params` нулём. После этого мы получаем адрес командной строки вызовом функции `get_cmd_line_ptr`:
|
|
|
|
|
|
```C
|
|
|
unsigned long cmd_line_ptr = boot_params.hdr.cmd_line_ptr;
|
|
|
cmd_line_ptr |= (u64)boot_params.ext_cmd_line_ptr << 32;
|
|
|
return cmd_line_ptr;
|
|
|
```
|
|
|
|
|
|
который получает 64-битный адрес командной строки из заголовочного файла загрузки ядра и возвращает его. На последнем шаге мы проверяем `cmd_line_ptr`, получаем его виртуальный адрес и копируем его в `boot_command_line`, который представляет собой всего лишь массив байтов:
|
|
|
|
|
|
```C
|
|
|
extern char __initdata boot_command_line[];
|
|
|
```
|
|
|
|
|
|
После этого мы имеем скопированную командную строку ядра и структуру `boot_params`. На следующем шаге происходит вызов функции `load_ucode_bsp`, которая загружает процессорный микрокод, его мы здесь не увидим.
|
|
|
|
|
|
После загрузки микрокода мы можем видеть проверку функции `console_loglevel` и `early_printk`, которая печатает строку `Kernel Alive`. Но вы никогда не увидите этот вывод, потому что `early_printk` еще не инициализирован. Это небольшая ошибка в ядре, и я (*[0xAX](https://github.com/0xAX), автор оригинальной книги - Прим. пер.*) отправил патч - [коммит](http://git.kernel.org/cgit/linux/kernel/git/tip/tip.git/commit/?id=91d8f0416f3989e248d3a3d3efb821eda10a85d2), чтобы исправить её.
|
|
|
|
|
|
Перемещение по страницам инициализации
|
|
|
--------------------------------------------------------------------------------
|
|
|
|
|
|
На следующем шаге, когда мы скопировали структуру `boot_params`, нам нужно перейти от начальных таблиц страниц к таблицам страниц для процесса инициализации. Мы уже настроили начальные таблицы страниц, вы можете прочитать об этом в предыдущей [части](linux-initialization-1.md) и сбросили это всё функцией `reset_early_page_tables` (вы тоже можете прочитать об этом в предыдущей части) и сохранили только отображение страниц ядра. После этого мы вызываем функцию `clear_page`:
|
|
|
|
|
|
```C
|
|
|
clear_page(init_level4_pgt);
|
|
|
```
|
|
|
|
|
|
с аргументом `init_level4_pgt`, который определён в файле [arch/x86/kernel/head_64.S](https://github.com/torvalds/linux/blob/16f73eb02d7e1765ccab3d2018e0bd98eb93d973/arch/x86/kernel/head_64.S) и выглядит следующим образом:
|
|
|
|
|
|
```assembly
|
|
|
NEXT_PAGE(init_level4_pgt)
|
|
|
.quad level3_ident_pgt - __START_KERNEL_map + _KERNPG_TABLE
|
|
|
.org init_level4_pgt + L4_PAGE_OFFSET*8, 0
|
|
|
.quad level3_ident_pgt - __START_KERNEL_map + _KERNPG_TABLE
|
|
|
.org init_level4_pgt + L4_START_KERNEL*8, 0
|
|
|
.quad level3_kernel_pgt - __START_KERNEL_map + _PAGE_TABLE
|
|
|
```
|
|
|
|
|
|
Он отображает первые 2 гигабайта и 512 мегабайта для кода ядра, данных и bss. Функция `clear_page` определена в [arch/x86/lib/clear_page_64.S](https://github.com/torvalds/linux/blob/16f73eb02d7e1765ccab3d2018e0bd98eb93d973/arch/x86/lib/clear_page_64.S). Давайте взглянем на неё:
|
|
|
|
|
|
```assembly
|
|
|
ENTRY(clear_page)
|
|
|
CFI_STARTPROC
|
|
|
xorl %eax,%eax
|
|
|
movl $4096/64,%ecx
|
|
|
.p2align 4
|
|
|
.Lloop:
|
|
|
decl %ecx
|
|
|
#define PUT(x) movq %rax,x*8(%rdi)
|
|
|
movq %rax,(%rdi)
|
|
|
PUT(1)
|
|
|
PUT(2)
|
|
|
PUT(3)
|
|
|
PUT(4)
|
|
|
PUT(5)
|
|
|
PUT(6)
|
|
|
PUT(7)
|
|
|
leaq 64(%rdi),%rdi
|
|
|
jnz .Lloop
|
|
|
nop
|
|
|
ret
|
|
|
CFI_ENDPROC
|
|
|
.Lclear_page_end:
|
|
|
ENDPROC(clear_page)
|
|
|
```
|
|
|
|
|
|
Как вы можете понять из имени функции, она очищает или заполняет нулями таблицы страниц. Прежде всего обратите внимание, что эта функция начинается с макросов `CFI_STARTPROC` и `CFI_ENDPROC`, которые раскрываются до директив сборки GNU:
|
|
|
|
|
|
```C
|
|
|
#define CFI_STARTPROC .cfi_startproc
|
|
|
#define CFI_ENDPROC .cfi_endproc
|
|
|
```
|
|
|
|
|
|
и используются для отладки. После макроса `CFI_STARTPROC` мы обнуляем регистр `eax` и помещаем 64 в `ecx` (это будет счётчик). Далее мы видим цикл, который начинается с метки `.Lloop` и декремента `ecx`. После этого мы помещаем нуль из регистра `rax` в `rdi`, который теперь содержит базовый адрес `init_level4_pgt` и выполняем ту же процедуру семь раз, но каждый раз перемещаем смещение `rdi` на 8. После этого первые 64 байта `init_level4_pgt` будут заполнены нулями. На следующем шаге мы снова помещаем адрес `init_level4_pgt` со смещением 64 байта в `rdi` и повторяем все операции до тех пор, пока `ecx` не будет равен нулю. В итоге мы получим `init_level4_pgt`, заполненный нулями.
|
|
|
|
|
|
После заполнения нулями `init_level4_pgt`, мы помещаем последнюю запись в `init_level4_pgt`:
|
|
|
|
|
|
```C
|
|
|
init_level4_pgt[511] = early_top_pgt[511];
|
|
|
```
|
|
|
|
|
|
Вы должны помнить, что мы очистили все записи `early_top_pgt` функцией `reset_early_page_table` и сохранили только отображение ядра.
|
|
|
|
|
|
Последний шаг в функции `x86_64_start_kernel` заключается в вызове функции `x86_64_start_reservations`:
|
|
|
|
|
|
```C
|
|
|
x86_64_start_reservations(real_mode_data);
|
|
|
```
|
|
|
|
|
|
с аргументов `real_mode_data`. Функция `x86_64_start_reservations` определена в том же файле исходного кода что и `x86_64_start_kernel`:
|
|
|
|
|
|
```C
|
|
|
void __init x86_64_start_reservations(char *real_mode_data)
|
|
|
{
|
|
|
if (!boot_params.hdr.version)
|
|
|
copy_bootdata(__va(real_mode_data));
|
|
|
|
|
|
reserve_ebda_region();
|
|
|
|
|
|
start_kernel();
|
|
|
}
|
|
|
```
|
|
|
|
|
|
Это последняя функция перед входом в точку ядра - `start_kernel`. Давайте посмотрим, что он делает и как это работает.
|
|
|
|
|
|
Последний шаг перед точкой входа в ядро
|
|
|
--------------------------------------------------------------------------------
|
|
|
|
|
|
В первую очередь мы видим проверку `boot_params.hdr.version` в функции `x86_64_start_reservations`:
|
|
|
|
|
|
```C
|
|
|
if (!boot_params.hdr.version)
|
|
|
copy_bootdata(__va(real_mode_data));
|
|
|
```
|
|
|
|
|
|
и если он равен нулю то снова вызывается функция `copy_bootdata` с виртуальным адресом `real_mode_data`.
|
|
|
|
|
|
В следующем шаге мы видим вызов функции `reserve_ebda_region`, определённой в файле [arch/x86/kernel/head.c](https://github.com/torvalds/linux/blob/16f73eb02d7e1765ccab3d2018e0bd98eb93d973/arch/x86/kernel/head.c). Эта функция резервирует блок памяти для `EBDA` или `Extended BIOS Data Area`. `Extended BIOS Data Area` расположена в верхних адресах основной области памяти (conventional memory) и содержит данные о портах, параметрах диска и т.д.
|
|
|
|
|
|
Давайте посмотрим на функцию `reserve_ebda_region`. Он начинается с проверки, включена ли паравиртуализация или нет:
|
|
|
|
|
|
```C
|
|
|
if (paravirt_enabled())
|
|
|
return;
|
|
|
```
|
|
|
|
|
|
если паравиртуализация включена, мы выходим из функции `reserve_ebda_region`, потому что `EBDA` отсутствует. На следующем шаге нам нужно получить конец нижней области памяти:
|
|
|
|
|
|
```C
|
|
|
lowmem = *(unsigned short *)__va(BIOS_LOWMEM_KILOBYTES);
|
|
|
lowmem <<= 10;
|
|
|
```
|
|
|
|
|
|
Мы получаем виртуальный адрес нижней области памяти BIOS в килобайтах и преобразуем его в байты, сдвигая его на 10 (другими словами умножаем на 1024). После этого нам нужно получить адрес `EBDA`:
|
|
|
|
|
|
```C
|
|
|
ebda_addr = get_bios_ebda();
|
|
|
```
|
|
|
|
|
|
Функция `get_bios_ebda` определена в файле [arch/x86/include/asm/bios_ebda.h](https://github.com/torvalds/linux/blob/16f73eb02d7e1765ccab3d2018e0bd98eb93d973/arch/x86/include/asm/bios_ebda.h):
|
|
|
|
|
|
```C
|
|
|
static inline unsigned int get_bios_ebda(void)
|
|
|
{
|
|
|
unsigned int address = *(unsigned short *)phys_to_virt(0x40E);
|
|
|
address <<= 4;
|
|
|
return address;
|
|
|
}
|
|
|
```
|
|
|
|
|
|
Давайте попробуем понять, как это работает. Мы видим преобразование физического адреса `0x40E` в виртуальный, где `0x0040: 0x000e` - это сегмент, который содержит базовый адрес `EBDA`. Не беспокойтесь о том, что мы используем функцию `phys_to_virt` для преобразования физического адреса в виртуальный. Вы можете заметить, что ранее мы использовали макрос `__va`, но `phys_to_virt` - это то же самое:
|
|
|
|
|
|
```C
|
|
|
static inline void *phys_to_virt(phys_addr_t address)
|
|
|
{
|
|
|
return __va(address);
|
|
|
}
|
|
|
```
|
|
|
|
|
|
только с одним отличием: мы передаем аргумент `phys_addr_t`, который зависит от `CONFIG_PHYS_ADDR_T_64BIT`:
|
|
|
|
|
|
```C
|
|
|
#ifdef CONFIG_PHYS_ADDR_T_64BIT
|
|
|
typedef u64 phys_addr_t;
|
|
|
#else
|
|
|
typedef u32 phys_addr_t;
|
|
|
#endif
|
|
|
```
|
|
|
|
|
|
Мы получили виртуальный адрес сегмента, в котором хранится базовый адрес `EBDA`. Мы сдвигаем его на 4 и возвращаем как результат. После этого переменная `ebda_addr` содержит базовый адрес `EBDA`.
|
|
|
|
|
|
На следующем шаге мы проверяем, что адрес `EBDA` и нижняя область памяти не меньше, чем значение макроса `INSANE_CUTOFF`:
|
|
|
|
|
|
```C
|
|
|
if (ebda_addr < INSANE_CUTOFF)
|
|
|
ebda_addr = LOWMEM_CAP;
|
|
|
|
|
|
if (lowmem < INSANE_CUTOFF)
|
|
|
lowmem = LOWMEM_CAP;
|
|
|
```
|
|
|
|
|
|
где `INSANE_CUTOFF`:
|
|
|
|
|
|
```C
|
|
|
#define INSANE_CUTOFF 0x20000U
|
|
|
```
|
|
|
|
|
|
или 128 килобайт. На последнем шаге мы получаем нижнюю часть нижней области памяти и `EBDA` и вызываем функцию `memblock_reserve`, которая резервирует область памяти для `EBDA` между нижней областью памяти и одномегабайтной меткой:
|
|
|
|
|
|
```C
|
|
|
lowmem = min(lowmem, ebda_addr);
|
|
|
lowmem = min(lowmem, LOWMEM_CAP);
|
|
|
memblock_reserve(lowmem, 0x100000 - lowmem);
|
|
|
```
|
|
|
|
|
|
функция `memblock_reserve` определена в [mm/block.c](https://github.com/torvalds/linux/blob/16f73eb02d7e1765ccab3d2018e0bd98eb93d973/mm/block.c) и принимает два аргумента:
|
|
|
|
|
|
* базовый физический адрес;
|
|
|
* размер области памяти.
|
|
|
|
|
|
и резервирует область памяти для заданного базового адреса и размера. `memblock_reserve` - первая функция в этой книге из фреймворка менеджера памяти ядра Linux. Мы скоро рассмотрим менеджер памяти, но пока что посмотрим на его реализацию.
|
|
|
|
|
|
Первое знакомство с фреймворком менеджера памяти ядра Linux
|
|
|
--------------------------------------------------------------------------------
|
|
|
|
|
|
В предыдущем абзаце мы остановились на вызове функции `memblock_reserve` и, как я уже сказал, это первая функция из фреймворка менеджера памяти. Давайте попробуем понять, как это работает. `memblock_reserve` просто вызывает функцию:
|
|
|
|
|
|
```C
|
|
|
memblock_reserve_region(base, size, MAX_NUMNODES, 0);
|
|
|
```
|
|
|
|
|
|
и передаёт ей 4 аргумента:
|
|
|
|
|
|
* физический базовый адрес области памяти;
|
|
|
* размер области памяти;
|
|
|
* максимально число NUMA-узлов;
|
|
|
* флаги.
|
|
|
|
|
|
В начале тела функции `memblock_reserve_region` мы можем видеть определение структуры `memblock_type`:
|
|
|
|
|
|
```C
|
|
|
struct memblock_type *_rgn = &memblock.reserved;
|
|
|
```
|
|
|
|
|
|
которая представляет тип блока памяти:
|
|
|
|
|
|
```C
|
|
|
struct memblock_type {
|
|
|
unsigned long cnt;
|
|
|
unsigned long max;
|
|
|
phys_addr_t total_size;
|
|
|
struct memblock_region *regions;
|
|
|
};
|
|
|
```
|
|
|
|
|
|
Поскольку нам необходимо зарезервировать блок памяти для `EBDA`, тип текущей области памяти зарезервирован так же, где и структура `memblock`:
|
|
|
|
|
|
```C
|
|
|
struct memblock {
|
|
|
bool bottom_up;
|
|
|
phys_addr_t current_limit;
|
|
|
struct memblock_type memory;
|
|
|
struct memblock_type reserved;
|
|
|
#ifdef CONFIG_HAVE_MEMBLOCK_PHYS_MAP
|
|
|
struct memblock_type physmem;
|
|
|
#endif
|
|
|
};
|
|
|
```
|
|
|
|
|
|
и описывает общий блок памяти. Мы инициализируем `_rgn` адресом `memblock.reserved`. `memblock` - глобальная переменная:
|
|
|
|
|
|
```C
|
|
|
struct memblock memblock __initdata_memblock = {
|
|
|
.memory.regions = memblock_memory_init_regions,
|
|
|
.memory.cnt = 1,
|
|
|
.memory.max = INIT_MEMBLOCK_REGIONS,
|
|
|
.reserved.regions = memblock_reserved_init_regions,
|
|
|
.reserved.cnt = 1,
|
|
|
.reserved.max = INIT_MEMBLOCK_REGIONS,
|
|
|
#ifdef CONFIG_HAVE_MEMBLOCK_PHYS_MAP
|
|
|
.physmem.regions = memblock_physmem_init_regions,
|
|
|
.physmem.cnt = 1,
|
|
|
.physmem.max = INIT_PHYSMEM_REGIONS,
|
|
|
#endif
|
|
|
.bottom_up = false,
|
|
|
.current_limit = MEMBLOCK_ALLOC_ANYWHERE,
|
|
|
};
|
|
|
```
|
|
|
|
|
|
Мы не будем погружаться в детали этой переменной, но мы увидим все подробности об этом в частях о менеджере памяти. Просто отметьте, что переменная `memblock` определена с помощью` __initdata_memblock`:
|
|
|
|
|
|
```C
|
|
|
#define __initdata_memblock __meminitdata
|
|
|
```
|
|
|
|
|
|
где `__meminit_data`:
|
|
|
|
|
|
```C
|
|
|
#define __meminitdata __section(.meminit.data)
|
|
|
```
|
|
|
|
|
|
Из этого можно сделать вывод, что все блоки памяти будут в секции `.meminit.data`. После того как мы определили `_rgn`, мы печатаем информацию об этом с помощью макроса `memblock_dbg`. Вы можете включить его, передав `memblock = debug` в командную строку ядра.
|
|
|
|
|
|
После печати строк отладки следует вызов функции `memblock_add_range`:
|
|
|
|
|
|
```C
|
|
|
memblock_add_range(_rgn, base, size, nid, flags);
|
|
|
```
|
|
|
|
|
|
которая добавляет новую область блока памяти в секцию `.meminit.data`. Поскольку мы не инициализируем `_rgn` и он содержит `&memblock.reserved`, мы просто заполняем переданный `_rgn` базовым адресом `EBDA`, размером этой области и флагами:
|
|
|
|
|
|
```C
|
|
|
if (type->regions[0].size == 0) {
|
|
|
WARN_ON(type->cnt != 1 || type->total_size);
|
|
|
type->regions[0].base = base;
|
|
|
type->regions[0].size = size;
|
|
|
type->regions[0].flags = flags;
|
|
|
memblock_set_region_node(&type->regions[0], nid);
|
|
|
type->total_size = size;
|
|
|
return 0;
|
|
|
}
|
|
|
```
|
|
|
|
|
|
После заполнения нашей области памяти мы видим вызов функции `memblock_set_region_node` с двумя аргументами:
|
|
|
|
|
|
* адрес заполненной области памяти;
|
|
|
* id NUMA-узла.
|
|
|
|
|
|
где наши области памяти представлены структурой `memblock_region`:
|
|
|
|
|
|
```C
|
|
|
struct memblock_region {
|
|
|
phys_addr_t base;
|
|
|
phys_addr_t size;
|
|
|
unsigned long flags;
|
|
|
#ifdef CONFIG_HAVE_MEMBLOCK_NODE_MAP
|
|
|
int nid;
|
|
|
#endif
|
|
|
};
|
|
|
```
|
|
|
|
|
|
Id NUMA-узла зависит от макроса `MAX_NUMNODES`, определённого в файле [include/linux/numa.h](https://github.com/torvalds/linux/blob/16f73eb02d7e1765ccab3d2018e0bd98eb93d973/include/linux/numa.h):
|
|
|
|
|
|
```C
|
|
|
#define MAX_NUMNODES (1 << NODES_SHIFT)
|
|
|
```
|
|
|
|
|
|
где `NODES_SHIFT` зависит от параметра конфигурации `CONFIG_NODES_SHIFT`:
|
|
|
|
|
|
```C
|
|
|
#ifdef CONFIG_NODES_SHIFT
|
|
|
#define NODES_SHIFT CONFIG_NODES_SHIFT
|
|
|
#else
|
|
|
#define NODES_SHIFT 0
|
|
|
#endif
|
|
|
```
|
|
|
|
|
|
Функция `memblick_set_region_node` просто заполняет поле `nid` из `memblock_region` заданным значением:
|
|
|
|
|
|
```C
|
|
|
static inline void memblock_set_region_node(struct memblock_region *r, int nid)
|
|
|
{
|
|
|
r->nid = nid;
|
|
|
}
|
|
|
```
|
|
|
|
|
|
После этого у нас будет первый зарезервированный `memblock` для `EBDA` в секции `.meminit.data`. Функция `reserve_ebda_region` завершила работу над этим шагом, и мы можем вернуться в [arch/x86/kernel/head64.c](https://github.com/torvalds/linux/blob/16f73eb02d7e1765ccab3d2018e0bd98eb93d973/arch/x86/kernel/head64.c).
|
|
|
|
|
|
Мы закончили все приготовления! Последним шагом в функции `x86_64_start_reservations` является вызов функции `start_kernel`:
|
|
|
|
|
|
```C
|
|
|
start_kernel()
|
|
|
```
|
|
|
|
|
|
расположенной в [init/main.c](https://github.com/torvalds/linux/blob/16f73eb02d7e1765ccab3d2018e0bd98eb93d973/init/main.c).
|
|
|
|
|
|
Заключение
|
|
|
--------------------------------------------------------------------------------
|
|
|
|
|
|
Это конец третей части инициализации ядра Linux. В следующей части мы увидим первые шаги инициализации в точке входа в ядро - `start_kernel`. Это будет первый шаг, прежде чем мы увидим запуск первого процесса `init`.
|
|
|
|
|
|
**От переводчика: пожалуйста, имейте в виду, что английский - не мой родной язык, и я очень извиняюсь за возможные неудобства. Если вы найдёте какие-либо ошибки или неточности в переводе, пожалуйста, пришлите pull request в [linux-insides-ru](https://github.com/proninyaroslav/linux-insides-ru).**
|
|
|
|
|
|
Ссылки
|
|
|
--------------------------------------------------------------------------------
|
|
|
|
|
|
* [BIOS data area](http://stanislavs.org/helppc/bios_data_area.html)
|
|
|
* [Что такое Extended BIOS Data Area](http://www.kryslix.com/nsfaq/Q.6.html)
|
|
|
* [Предыдущая часть](linux-initialization-2.md)
|