Инициализация ядра. Часть 4.
Если вы читали предыдущую часть — Последние приготовления перед точкой входа в ядро, вы можете помнить, что мы завершили все действия по предварительной инициализации и остановились прямо перед вызовом функции start_kernel из init/main.c. start_kernel это точка входа общего и независимого от архитектуры кода ядра, хотя мы будем возвращаться в папку arch/ много раз. Если вы заглянете внутрь функции start_kernel , то увидите, что эта функция очень большая. На данный момент она содержит около 86 вызовов функций. Да, она очень большая и, конечно, эта часть не будет охватывать все процессы, которые происходят в этой функции. В текущей части мы только начнем это делать. Эта часть и все последующие, которые будут описаны в главе Процесс инициализации ядра, охватят её.
Основная цель start_kernel — завершить процесс инициализации ядра и запустить первый процесс init . Перед запуском первого процесса start_kernel должен сделать много вещей, такие как: включить блокировщик валидатора, инициализировать идентификатор процессора, включить начальную подсистему cgroups, настроить области для каждого CPU, инициализировать различные кэши в vfs, инициализировать менеджер памяти, rcu, vmalloc, планировщик, IRQ, ACPI и многое другое. Только после этих шагов мы увидим запуск первого процесса init в последней части этой главы. Так много кода ядра ждет нас, давайте начнем.
ПРИМЕЧАНИЕ: Все части этой большой главы Процесс инициализации ядра Linux не будут касаться отладки. Для этого будет отдельная глава.
Немного об атрибутах функции
Как я писал выше, функция start_kernel определена в init/main.c. Эта функция определена с атрибутом __init и, как вы уже знаете из других частей, все функции, которые определены с этим атрибутом, необходимы во время инициализации ядра.
После завершения процесса инициализации, ядро осободит эти секции вызовом функции free_initmem . Также обратите внимание, что __init определена двумя атрибутами: __cold и notrace . Цель первого атрибута — отметить, что функция используется редко, и компилятор должен оптимизировать размер этой функции. Второй атрибут определён следующий образом:
где no_instrument_function говорит компилятору не генерировать вызовы функции профилирования.
В определении функции start_kernel вы также можете увидеть атрибут __visible , который раскрывается в следующее выражение:
где externally_visible сообщает компилятору, что кто-то использует эту функцию или переменную, чтобы предотвратить маркировку этой функции/переменной как unusable . Вы можете найти определение этого и других макро-атрибутов в include/linux/init.h.
Первые шаги в start_kernel
В начале start_kernel вы можете увидеть определение этих двух переменных:
Первая представляет собой указатель на командную строку ядра, а вторая будет содержать результат функции parse_args , которая анализирует входную строку с параметрами в форме name = value , ищет конкретные ключевые слова и вызывает верные обработчики. Мы не будем сейчас вдаваться в детали, связанные с этими двумя переменными, но увидим это в следующих частях. На следующем шаге мы видим вызов функции set_task_stack_end_magic . Эта функция берет адрес init_task и устанавливает для нее STACK_END_MAGIC ( 0x57AC6E9D ). init_task представляет собой начальную структуру задачи:
где task_struct хранит всю информацию о процессе. Я не буду объяснять эту структуру в данной книге, потому что она очень большая. Вы можете найти её определение в include/linux/sched.h. На данный момент task_struct содержит более 100 полей! Хотя вы не увидите объяснения task_struct в этой книге, мы будем использовать её очень часто, поскольку это фундаментальная структура, которая описывает процесс в ядре Linux. Я буду описывать значение полей этой структуры по мере того как мы будем встречать их на практике.
Вы можете видеть определение init_task и она инициализирована макросом INIT_TASK . Этот макрос взят из include/linux/init_task.h и просто заполняет init_task значениями для первого процесса. Например, он устанавливает:
- начальное состояние процесса в ноль или runnable . Runnable процесс — это процесс, который ожидает запуска на CPU;
- начальные флаги процесса — PF_KTHREAD , что означает поток ядра;
- список выполняемых задач;
- адресное пространство процесса;
- начальный стек процесса в &init_thread_info , который является init_thread_union.thread_info , и initthread_union имеет тип thread_union , который содержит thread_info и стек процесса:
Каждый процесс имеет свой собственный стек и он составляет 16 килобайт или 4 страницы в x86_64 . Мы можем заметить, что он определён как массив unsigned long . Следующее поле thread_union — это структура thread_info , которая занимает 52 байта:
thread_info содержит специфичную для архитектуры информацию о потоке. Мы знаем, что в x86_64 стек уменьшается и в нашем случае thread_union.thread_info размещена в нижней части стека. Таким образом, стек процесса составляет 16 килобайт и thread_info находится внизу. Оставшийся размер потока будет составлять 16 килобайт — 62 байта = 16332 байта . Обратите внимание, что thread_union представлен как union, а не как структура, это означает, что thread_info и стек совместно используют одно и то же пространство памяти.
Схематически это можно представить следующим образом:
Таким образом, макрос INIT_TASK заполняет эти поля в task_struct , а также многие другие. Как я уже писал выше, я не буду описывать все поля и значения в макросе INIT_TASK , но скоро мы их увидим.
Теперь вернёмся к функции set_task_stack_end_magic . Эта функция определена в kernel/fork.c и устанавливает стековый индикатор («канарейка») в стек процесса init для предотвращения его переполнения.
Его реализация проста. set_task_stack_end_magic получает конец стека для заданной task_struct с помощью функции end_of_stack . Ранее (теперь для всех архитектур, кроме x86_64 ) стек был расположен в структуре thread_info . Таким образом, конец стека процессов зависит от параметра конфигурации CONFIG_STACK_GROWSUP . Как мы знаем, в x86_64 стек растёт вниз. Таким образом, конец стека процесса будет следующим:
где task_thread_info просто возвращает стек, который мы заполнили с помощью мароса INIT_TASK :
Начиная с релиза ядра Linux v4.9-rc1 структура thread_info может содержать только флаги, а указатель стека находится в структуре task_struct , которая представляет поток в ядре Linux. Это зависит от параметра конфигурации ядра CONFIG_THREAD_INFO_IN_TASK , который по умолчанию включен для x86_64 . Вы можете быть убедиться в этом, если загляните в файл конфигурации сборки init/main.c :
Поэтому мы можем просто получить конец стека потока из заданной структуры task_struct :
Когда мы получили конец стека init процесса, мы записываем туда STACK_END_MAGIC . После того, как «канарейка» установлена, мы можем проверить это следующим образом:
Следующая функция после set_task_stack_end_magic — smp_setup_processor_id . Эта функция имеет пустое тело для x86_64 :
так как она реализована только для некоторых архитектур, таких как s390 и arm64.
Следующая функция в start_kernel — это debug_objects_early_init . Реализация данной функции почти такая же, как у lockdep_init , но в отличии от неё заполняет хеши для отладки объектов. Как я писал выше в этой главе мы не увидим объяснения этой и других функций, предназначенных для отладки.
После функции debug_object_early_init мы можем видеть вызов функции boot_init_stack_canary , которая заполняет task_struct-> canary значением «канарейки» для опции gcc -fstack-protector . Эта опция зависит от параметра конфигурации CONFIG_CC_STACKPROTECTOR и, если этот параметр отключён, функция boot_init_stack_canary ничего не делает, в противном случае она генерирует случайные числа на основе пула энтропии и TSC:
После того как мы получили случайное число, мы заполняем поле stack_canary в task_struct :
и запишите это значение в верхнюю часть стека IRQ:
Опять же, здесь мы не будем вдаваться в подробности, мы расскажем об этом в части о IRQ. Когда «канарейка» установлена, мы отключаем локальные и начальные загрузочные IRQ и регистрируем загрузочный CPU в картах CPU. Мы отключаем локальные IRQ (прерывания для текущего процессора) с помощью макроса local_irq_disable , который раскрывается в вызов функции arch_local_irq_disable из include/linux/percpu-defs.h:
Где native_irq_disable — это инструкция cli для x86_64 . Поскольку прерывания отключены, мы можем зарегистрировать текущий CPU с заданным идентификатором в битовой карте CPU.
Первая активация процессора
The current function from the start_kernel is boot_cpu_init . This function initializes various CPU masks for the bootstrap processor. First of all it gets the bootstrap processor id with a call to:
For now it is just zero. If the CONFIG_DEBUG_PREEMPT configuration option is disabled, smp_processor_id just expands to the call of raw_smp_processor_id which expands to the:
this_cpu_read as many other function like this ( this_cpu_write , this_cpu_add and etc. ) defined in the include/linux/percpu-defs.h and presents this_cpu operation. These operations provide a way of optimizing access to the per-cpu variables which are associated with the current processor. In our case it is this_cpu_read :
Remember that we have passed cpu_number as pcp to the this_cpu_read from the raw_smp_processor_id . Now let’s look at the __pcpu_size_call_return implementation:
Yes, it looks a little strange but it’s easy. First of all we can see the definition of the pscr_ret__ variable with the int type. Why int? Ok, variable is common_cpu and it was declared as per-cpu int variable:
In the next step we call __verify_pcpu_ptr with the address of cpu_number . __veryf_pcpu_ptr used to verify that the given parameter is a per-cpu pointer. After that we set pscr_ret__ value which depends on the size of the variable. Our common_cpu variable is int , so it 4 bytes in size. It means that we will get this_cpu_read_4(common_cpu) in pscr_ret__ . In the end of the __pcpu_size_call_return we just call it. this_cpu_read_4 is a macro:
which calls percpu_from_op and pass mov instruction and per-cpu variable there. percpu_from_op will expand to the inline assembly call:
Let’s try to understand how it works and what it does. The gs segment register contains the base of per-cpu area. Here we just copy common_cpu which is in memory to the pfo_ret__ with the movl instruction. Or with another words:
As we didn’t setup per-cpu area, we have only one — for the current running CPU, we will get zero as a result of the smp_processor_id .
As we got the current processor id, boot_cpu_init sets the given CPU online, active, present and possible with the:
All of these functions use the concept — cpumask . cpu_possible is a set of CPU ID’s which can be plugged in at any time during the life of that system boot. cpu_present represents which CPUs are currently plugged in. cpu_online represents subset of the cpu_present and indicates CPUs which are available for scheduling. These masks depend on the CONFIG_HOTPLUG_CPU configuration option and if this option is disabled possible == present and active == online . Implementation of the all of these functions are very similar. Every function checks the second parameter. If it is true , it calls cpumask_set_cpu or cpumask_clear_cpu otherwise.
For example let’s look at set_cpu_possible . As we passed true as the second parameter, the:
will be called. First of all let’s try to understand the to_cpumask macro. This macro casts a bitmap to a struct cpumask * . CPU masks provide a bitmap suitable for representing the set of CPU’s in a system, one bit position per CPU number. CPU mask presented by the cpumask structure:
which is just bitmap declared with the DECLARE_BITMAP macro:
As we can see from its definition, the DECLARE_BITMAP macro expands to the array of unsigned long . Now let’s look at how the to_cpumask macro is implemented:
I don’t know about you, but it looked really weird for me at the first time. We can see a ternary operator here which is true every time, but why the __check_is_bitmap here? It’s simple, let’s look at it:
Yeah, it just returns 1 every time. Actually we need in it here only for one purpose: at compile time it checks that the given bitmap is a bitmap, or in other words it checks that the given bitmap has a type of unsigned long * . So we just pass cpu_possible_bits to the to_cpumask macro for converting the array of unsigned long to the struct cpumask * . Now we can call cpumask_set_cpu function with the cpu — 0 and struct cpumask *cpu_possible_bits . This function makes only one call of the set_bit function which sets the given cpu in the cpumask. All of these set_cpu_* functions work on the same principle.
If you’re not sure that this set_cpu_* operations and cpumask are not clear for you, don’t worry about it. You can get more info by reading the special part about it — cpumask or documentation.
As we activated the bootstrap processor, it’s time to go to the next function in the start_kernel. Now it is page_address_init , but this function does nothing in our case, because it executes only when all RAM can’t be mapped directly.
Print linux banner
The next call is pr_notice :
as you can see it just expands to the printk call. At this moment we use pr_notice to print the Linux banner:
which is just the kernel version with some additional parameters:
Architecture-dependent parts of initialization
The next step is architecture-specific initialization. The Linux kernel does it with the call of the setup_arch function. This is a very big function like start_kernel and we do not have time to consider all of its implementation in this part. Here we’ll only start to do it and continue in the next part. As it is architecture-specific , we need to go again to the arch/ directory. The setup_arch function defined in the arch/x86/kernel/setup.c source code file and takes only one argument — address of the kernel command line.
This function starts from the reserving memory block for the kernel _text and _data which starts from the _text symbol (you can remember it from the arch/x86/kernel/head_64.S) and ends before __bss_stop . We are using memblock for the reserving of memory block:
You can read about memblock in the Linux kernel memory management Part 1.. As you can remember memblock_reserve function takes two parameters:
- base physical address of a memory block;
- size of a memory block.
We can get the base physical address of the _text symbol with the __pa_symbol macro:
First of all it calls __phys_reloc_hide macro on the given parameter. The __phys_reloc_hide macro does nothing for x86_64 and just returns the given parameter. Implementation of the __phys_addr_symbol macro is easy. It just subtracts the symbol address from the base address of the kernel text mapping base virtual address (you can remember that it is __START_KERNEL_map ) and adds phys_base which is the base address of _text :
After we got the physical address of the _text symbol, memblock_reserve can reserve a memory block from the _text to the __bss_stop — _text .
Reserve memory for initrd
In the next step after we reserved place for the kernel text and data is reserving place for the initrd. We will not see details about initrd in this post, you just may know that it is temporary root file system stored in memory and used by the kernel during its startup. The early_reserve_initrd function does all work. First of all this function gets the base address of the ram disk, its size and the end address with:
All of these parameters are taken from boot_params . If you have read the chapter about Linux Kernel Booting Process, you must remember that we filled the boot_params structure during boot time. The kernel setup header contains a couple of fields which describes ramdisk, for example:
So we can get all the information that interests us from boot_params . For example let’s look at get_ramdisk_image :
Here we get the address of the ramdisk from the boot_params and shift left it on 32 . We need to do it because as you can read in the Documentation/x86/zero-page.txt:
So after shifting it on 32, we’re getting a 64-bit address in ramdisk_image and we return it. get_ramdisk_size works on the same principle as get_ramdisk_image , but it used ext_ramdisk_size instead of ext_ramdisk_image . After we got ramdisk’s size, base address and end address, we check that bootloader provided ramdisk with the:
and reserve memory block with the calculated addresses for the initial ramdisk in the end:
Conclusion
It is the end of the fourth part about the Linux kernel initialization process. We started to dive in the kernel generic code from the start_kernel function in this part and stopped on the architecture-specific initialization in the setup_arch . In the next part we will continue with architecture-dependent initialization steps.
If you have any questions or suggestions write me a comment or ping me at twitter.
Please note that English is not my first language, And I am really sorry for any inconvenience. If you find any mistakes please send me a PR to linux-insides.