Инициализация ядра. Часть 3.
Последние приготовления перед точкой входа в ядро
Это третья часть серии Инициализация ядра. В предыдущей части мы увидели начальную обработку прерываний и исключений и продолжим погружение в процесс инициализации ядра Linux в текущей части. Наша следующая точка - "точка входа в ядро" - функция start_kernel
из файла init/main.c. Да, технически это не точка входа в ядро, а начало кода ядра, который не зависит от определённой архитектуры. Но прежде чем мы вызовем функцию start_kernel
, мы должны совершить некоторые приготовления. Давайте продолжим.
Снова boot_params
В предыдущей части мы остановились на настройке таблицы векторов прерываний и её загрузки в регистр IDTR
. На следующем шаге мы можем видеть вызов функции copy_bootdata
:
copy_bootdata(__va(real_mode_data));
Эта функция принимает один аргумент - виртуальный адрес real_mode_data
. Вы должны помнить, что мы передали адрес структуры boot_params
из arch/x86/include/uapi/asm/bootparam.h в функцию x86_64_start_kernel
как первый параметр в 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:
#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
extern struct boot_params boot_params;
Давайте посмотрим на реализацию copy_boot_data
:
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
:
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
, который представляет собой всего лишь массив байтов:
extern char __initdata boot_command_line[];
После этого мы имеем скопированную командную строку ядра и структуру boot_params
. На следующем шаге происходит вызов функции load_ucode_bsp
, которая загружает процессорный микрокод, его мы здесь не увидим.
После загрузки микрокода мы можем видеть проверку функции console_loglevel
и early_printk
, которая печатает строку Kernel Alive
. Но вы никогда не увидите этот вывод, потому что early_printk
еще не инициализирован. Это небольшая ошибка в ядре, и я (0xAX, автор оригинальной книги - Прим. пер.) отправил патч - коммит, чтобы исправить её.
Перемещение по страницам инициализации
На следующем шаге, когда мы скопировали структуру boot_params
, нам нужно перейти от начальных таблиц страниц к таблицам страниц для процесса инициализации. Мы уже настроили начальные таблицы страниц, вы можете прочитать об этом в предыдущей части и сбросили это всё функцией reset_early_page_tables
(вы тоже можете прочитать об этом в предыдущей части) и сохранили только отображение страниц ядра. После этого мы вызываем функцию clear_page
:
clear_page(init_level4_pgt);
с аргументом init_level4_pgt
, который определён в файле arch/x86/kernel/head_64.S и выглядит следующим образом:
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. Давайте взглянем на неё:
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:
#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
:
init_level4_pgt[511] = early_top_pgt[511];
Вы должны помнить, что мы очистили все записи early_top_pgt
функцией reset_early_page_table
и сохранили только отображение ядра.
Последний шаг в функции x86_64_start_kernel
заключается в вызове функции x86_64_start_reservations
:
x86_64_start_reservations(real_mode_data);
с аргументов real_mode_data
. Функция x86_64_start_reservations
определена в том же файле исходного кода что и x86_64_start_kernel
:
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
:
if (!boot_params.hdr.version)
copy_bootdata(__va(real_mode_data));
и если он равен нулю то снова вызывается функция copy_bootdata
с виртуальным адресом real_mode_data
.
В следующем шаге мы видим вызов функции reserve_ebda_region
, определённой в файле arch/x86/kernel/head.c. Эта функция резервирует блок памяти для EBDA
или Extended BIOS Data Area
. Extended BIOS Data Area
расположена в верхних адресах основной области памяти (conventional memory) и содержит данные о портах, параметрах диска и т.д.
Давайте посмотрим на функцию reserve_ebda_region
. Он начинается с проверки, включена ли паравиртуализация или нет:
if (paravirt_enabled())
return;
если паравиртуализация включена, мы выходим из функции reserve_ebda_region
, потому что EBDA
отсутствует. На следующем шаге нам нужно получить конец нижней области памяти:
lowmem = *(unsigned short *)__va(BIOS_LOWMEM_KILOBYTES);
lowmem <<= 10;
Мы получаем виртуальный адрес нижней области памяти BIOS в килобайтах и преобразуем его в байты, сдвигая его на 10 (другими словами умножаем на 1024). После этого нам нужно получить адрес EBDA
:
ebda_addr = get_bios_ebda();
Функция get_bios_ebda
определена в файле arch/x86/include/asm/bios_ebda.h:
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
- это то же самое:
static inline void *phys_to_virt(phys_addr_t address)
{
return __va(address);
}
только с одним отличием: мы передаем аргумент phys_addr_t
, который зависит от CONFIG_PHYS_ADDR_T_64BIT
:
#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
:
if (ebda_addr < INSANE_CUTOFF)
ebda_addr = LOWMEM_CAP;
if (lowmem < INSANE_CUTOFF)
lowmem = LOWMEM_CAP;
где INSANE_CUTOFF
:
#define INSANE_CUTOFF 0x20000U
или 128 килобайт. На последнем шаге мы получаем нижнюю часть нижней области памяти и EBDA
и вызываем функцию memblock_reserve
, которая резервирует область памяти для EBDA
между нижней областью памяти и одномегабайтной меткой:
lowmem = min(lowmem, ebda_addr);
lowmem = min(lowmem, LOWMEM_CAP);
memblock_reserve(lowmem, 0x100000 - lowmem);
функция memblock_reserve
определена в mm/block.c и принимает два аргумента:
- базовый физический адрес;
- размер области памяти.
и резервирует область памяти для заданного базового адреса и размера. memblock_reserve
- первая функция в этой книге из фреймворка менеджера памяти ядра Linux. Мы скоро рассмотрим менеджер памяти, но пока что посмотрим на его реализацию.
Первое знакомство с фреймворком менеджера памяти ядра Linux
В предыдущем абзаце мы остановились на вызове функции memblock_reserve
и, как я уже сказал, это первая функция из фреймворка менеджера памяти. Давайте попробуем понять, как это работает. memblock_reserve
просто вызывает функцию:
memblock_reserve_region(base, size, MAX_NUMNODES, 0);
и передаёт ей 4 аргумента:
- физический базовый адрес области памяти;
- размер области памяти;
- максимально число NUMA-узлов;
- флаги.
В начале тела функции memblock_reserve_region
мы можем видеть определение структуры memblock_type
:
struct memblock_type *_rgn = &memblock.reserved;
которая представляет тип блока памяти:
struct memblock_type {
unsigned long cnt;
unsigned long max;
phys_addr_t total_size;
struct memblock_region *regions;
};
Поскольку нам необходимо зарезервировать блок памяти для EBDA
, тип текущей области памяти зарезервирован так же, где и структура memblock
:
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
- глобальная переменная:
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
:
#define __initdata_memblock __meminitdata
где __meminit_data
:
#define __meminitdata __section(.meminit.data)
Из этого можно сделать вывод, что все блоки памяти будут в секции .meminit.data
. После того как мы определили _rgn
, мы печатаем информацию об этом с помощью макроса memblock_dbg
. Вы можете включить его, передав memblock = debug
в командную строку ядра.
После печати строк отладки следует вызов функции memblock_add_range
:
memblock_add_range(_rgn, base, size, nid, flags);
которая добавляет новую область блока памяти в секцию .meminit.data
. Поскольку мы не инициализируем _rgn
и он содержит &memblock.reserved
, мы просто заполняем переданный _rgn
базовым адресом EBDA
, размером этой области и флагами:
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
:
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:
#define MAX_NUMNODES (1 << NODES_SHIFT)
где NODES_SHIFT
зависит от параметра конфигурации CONFIG_NODES_SHIFT
:
#ifdef CONFIG_NODES_SHIFT
#define NODES_SHIFT CONFIG_NODES_SHIFT
#else
#define NODES_SHIFT 0
#endif
Функция memblock_set_region_node
просто заполняет поле nid
из memblock_region
заданным значением:
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.
Мы закончили все приготовления! Последним шагом в функции x86_64_start_reservations
является вызов функции start_kernel
:
start_kernel()
расположенной в init/main.c.
Заключение
Это конец третей части инициализации ядра Linux. В следующей части мы увидим первые шаги инициализации в точке входа в ядро - start_kernel
. Это будет первый шаг, прежде чем мы увидим запуск первого процесса init
.
От переводчика: пожалуйста, имейте в виду, что английский - не мой родной язык, и я очень извиняюсь за возможные неудобства. Если вы найдёте какие-либо ошибки или неточности в переводе, пожалуйста, пришлите pull request в linux-insides-ru.