Инициализация ядра. Часть 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.

Ссылки

results matching ""

    No results matching ""