mirror of
https://github.com/MintCN/linux-insides-zh.git
synced 2026-02-04 02:54:51 +08:00
* Update Translate status * Finish translate session 2.7 * Update translate status * Update translate 2.7 * Update translate 2.7
488 lines
30 KiB
Markdown
488 lines
30 KiB
Markdown
内核初始化 第七部分
|
||
================================================================================
|
||
|
||
架构相关初始化尾声:最后一程
|
||
================================================================================
|
||
|
||
这是 Linux 内核初始化过程的第七部分,主要解析 [arch/x86/Kernel/setup.c](https://github.com/torvalds/linux/blob/16f73eb02d7e1765ccab3d2018e0bd98eb93d973/arch/x86/kernel/setup.c#L861) 文件中 `setup_arch` 函数内部机制。通过[前文](https://0xax.gitbook.io/linux-insides/summary/initialization)可知,`setup_arch` 函数执行架构相关(本文以 [x86_64](http://en.wikipedia.org/wiki/X86-64) 为例)的初始化工作,包括为内核代码/数据/bss 保留内存,[桌面管理界面](http://en.wikipedia.org/wiki/Desktop_Management_Interface)的早期扫描,[PCI](http://en.wikipedia.org/wiki/PCI) 设备的早期转储等众多操作。如果您已经阅读了前面的[部分](https://0xax.gitbook.io/linux-insides/summary/initialization/linux-initialization-6),会记得我们是在 `setup_real_mode` 函数处结束的。接下来,在我们限制 [memblock](https://0xax.gitbook.io/linux-insides/summary/mm/linux-mm-1) 为所有已映射页后,可以看到 [kernel/printk/printk.c](https://github.com/torvalds/linux/blob/16f73eb02d7e1765ccab3d2018e0bd98eb93d973/kernel/printk/printk.c) 中调用了 `setup_log_buf` 函数。
|
||
|
||
`setup_log_buf` 函数用于设置内核循环缓冲区,其长度取决于 `CONFIG_LOG_BUF_SHIFT` 配置选项。从文档中可知,`CONFIG_LOG_BUF_SHIFT` 取值范围在 `12` 到 `21` 之间。在内部实现中,该缓冲区定义为字符数组:
|
||
|
||
```c
|
||
#define __LOG_BUF_LEN (1 << CONFIG_LOG_BUF_SHIFT)
|
||
static char __log_buf[__LOG_BUF_LEN] __aligned(LOG_ALIGN);
|
||
static char *log_buf = __log_buf;
|
||
```
|
||
|
||
现在我们来看 `setup_log_buf` 函数的实现。它首先会检查当前缓冲区是否为空(因为缓冲区刚刚完成初始化,所以其必然为空),同时还会检查是否为早期初始化阶段。如果内核日志缓冲区的设置不属于早期初始化阶段,则会调用 `log_buf_add_cpu` 函数,该函数会为每个 CPU 扩展缓冲区的大小:
|
||
|
||
```C
|
||
if (log_buf != __log_buf)
|
||
return;
|
||
|
||
if (!early && !new_log_buf_len)
|
||
log_buf_add_cpu();
|
||
```
|
||
|
||
这里我们暂不深入分析 `log_buf_add_cpu` 函数,因为正如 `setup_arch` 中所示,我们是通过以下方式调用 `setup_log_buf` 的:
|
||
|
||
```C
|
||
setup_log_buf(1);
|
||
```
|
||
|
||
其中参数 `1` 表示当前处于早期初始化阶段。接下来,我们会检查 `new_log_buf_len` 变量(该变量表示更新后的内核日志缓冲区长度),并通过 `memblock_virt_alloc` 函数为其分配新的缓冲区空间,否则直接返回。
|
||
|
||
当内核日志缓冲区准备就绪后,下一个执行的是 `reserve_initrd` 函数。您可能还记得,在[内核初始化第四部分](https://0xax.gitbook.io/linux-insides/summary/initialization/linux-initialization-4)中我们已经调用过 `early_reserve_initrd` 函数。现在,由于我们已在 `init_mem_mapping` 函数中重建了直接内存映射,因此需要将[初始 RAM 磁盘](http://en.wikipedia.org/wiki/Initrd)移入直接映射内存区域。`reserve_initrd` 函数首先确定 initrd 的基地址和结束地址,并检查 bootloader 是否提供了 initrd —— 这些操作与我们在 `early_reserve_initrd` 中看到的完全一致。但不同于之前通过调用 `memblock_reserve` 在 memblock 区域中保留空间的做法,这里我们会获取直接内存映射区域的映射大小,并通过以下方式确保 initrd 的大小不超过该区域:
|
||
|
||
```C
|
||
mapped_size = memblock_mem_size(max_pfn_mapped);
|
||
if (ramdisk_size >= (mapped_size>>1))
|
||
panic("initrd too large to handle, "
|
||
"disabling initrd (%lld needed, %lld available)\n",
|
||
ramdisk_size, mapped_size>>1);
|
||
```
|
||
|
||
可以看到,这里我们调用了 `memblock_mem_size` 函数,并将 `max_pfn_mapped` 作为参数传入。其中,`max_pfn_mapped` 存储的是当前直接映射的最高页帧号(Page Frame Number)。若不记得什么是页帧号,这里简单说明:虚拟地址的低 `12` 位表示物理页(页帧)的偏移量。当我们右移虚拟地址的 `12` 位时,将丢弃偏移部分,从而得到页帧号。在 `memblock_mem_size` 函数内部,我们会遍历所有 memblock 的 `mem` 区域(不包括保留区域),计算已映射页面的总大小,并将结果返回给 `mapped_size` 变量(参见上文代码)。获取到直接映射内存的总量后,我们会检查 `initrd` 的大小是否超过已映射的页面范围。如果超出,则直接调用 `panic` 函数终止系统运行,并打印著名的[内核恐慌](http://en.wikipedia.org/wiki/Kernel_panic)信息。
|
||
|
||
接下来,我们会打印关于 `initrd` 大小的信息。通过 `dmesg` 命令的输出可以看到如下结果:
|
||
|
||
```C
|
||
[0.000000] RAMDISK: [mem 0x36d20000-0x37687fff]
|
||
```
|
||
|
||
随后通过 `relocate_initrd` 函数将 `initrd` 重定位到直接映射区域。在 `relocate_initrd` 函数起始处,我们会尝试使用 `memblock_find_in_range` 函数来寻找可用内存区域:
|
||
|
||
```C
|
||
relocated_ramdisk = memblock_find_in_range(0, PFN_PHYS(max_pfn_mapped), area_size, PAGE_SIZE);
|
||
|
||
if (!relocated_ramdisk)
|
||
panic("Cannot find place for new RAMDISK of size %lld\n",
|
||
ramdisk_size);
|
||
```
|
||
|
||
`memblock_find_in_range` 函数会尝试在指定范围内(本例中是从 `0` 到最大已映射物理地址)寻找可用区域,且该区域大小必须等于经过对齐处理的 `initrd` 大小。如果未能找到符合要求的区域,系统将再次调用 `panic` 函数终止运行。若成功找到合适区域,我们将在下一步将 RAM 磁盘重定位至直接映射内存的末端。
|
||
|
||
在 `reserve_initrd` 函数的最后阶段,通过调用以下函数释放原 RAM 磁盘占用的 memblock 内存:
|
||
|
||
```C
|
||
memblock_free(ramdisk_image, ramdisk_end - ramdisk_image);
|
||
```
|
||
|
||
在完成 `initrd` RAM 磁盘镜像的重定位后,接下来执行的是位于 [arch/x86/kernel/vsmp_64.c](https://github.com/torvalds/linux/blob/16f73eb02d7e1765ccab3d2018e0bd98eb93d973/arch/x86/kernel/vsmp_64.c) 的 `vsmp_init` 函数。该函数用于初始化 `ScaleMP vSMP` 架构支持。如先前章节所述,本文不会涉及与 `x86_64` 初始化无关的内容(例如当前的 `ACPI` 等)。因此我们将暂时跳过其具体实现,留待后续讲解并行计算技术时再作探讨。
|
||
|
||
随后调用的是 [arch/x86/kernel/io_delay.c](https://github.com/torvalds/linux/blob/16f73eb02d7e1765ccab3d2018e0bd98eb93d973/arch/x86/kernel/io_delay.c) 中的 `io_delay_init` 函数。该函数允许覆盖默认的 I/O 延迟端口 `0x80`。我们已在[进入保护模式前的最后准备](https://0xax.gitbook.io/linux-insides/summary/booting/linux-bootstrap-3)中接触过 I/O 延迟的概念,现在让我们深入分析 `io_delay_init` 的具体实现:
|
||
|
||
```C
|
||
void __init io_delay_init(void)
|
||
{
|
||
if (!io_delay_override)
|
||
dmi_check_system(io_delay_0xed_port_dmi_table);
|
||
}
|
||
```
|
||
|
||
该函数会检查 `io_delay_override` 变量,若该变量被设置,则覆盖默认的 I/O 延迟端口。我们可以通过向内核命令行传递 `io_delay` 参数来设置 `io_delay_override` 变量。根据 [Documentation/kernel-parameters.txt](https://github.com/torvalds/linux/blob/master/Documentation/admin-guide/kernel-parameters.rst) 文档说明,`io_delay` 选项是:
|
||
|
||
```
|
||
io_delay= [X86] I/O delay method
|
||
0x80
|
||
Standard port 0x80 based delay
|
||
0xed
|
||
Alternate port 0xed based delay (needed on some systems)
|
||
udelay
|
||
Simple two microseconds delay
|
||
none
|
||
No delay
|
||
```
|
||
|
||
我们可以看到,在 [arch/x86/kernel/io_delay.c](https://github.com/torvalds/linux/blob/16f73eb02d7e1765ccab3d2018e0bd98eb93d973/arch/x86/kernel/io_delay.c) 文件中,`io_delay` 命令行参数是通过 `early_param` 宏进行设置的:
|
||
|
||
```C
|
||
early_param("io_delay", io_delay_param);
|
||
```
|
||
|
||
关于 `early_param` 宏的更多细节,您可以在[第六部分](https://0xax.gitbook.io/linux-insides/summary/initialization/linux-initialization-6)中查阅。因此,用于设置 `io_delay_override` 变量的 `io_delay_param` 函数将会在 [do_early_param](https://github.com/torvalds/linux/blob/16f73eb02d7e1765ccab3d2018e0bd98eb93d973/init/main.c#L413) 函数中被调用。`io_delay_param` 函数通过解析 `io_delay` 内核命令行参数,根据传入值设置相应的 `io_delay_type`:
|
||
|
||
```C
|
||
static int __init io_delay_param(char *s)
|
||
{
|
||
if (!s)
|
||
return -EINVAL;
|
||
|
||
if (!strcmp(s, "0x80"))
|
||
io_delay_type = CONFIG_IO_DELAY_TYPE_0X80;
|
||
else if (!strcmp(s, "0xed"))
|
||
io_delay_type = CONFIG_IO_DELAY_TYPE_0XED;
|
||
else if (!strcmp(s, "udelay"))
|
||
io_delay_type = CONFIG_IO_DELAY_TYPE_UDELAY;
|
||
else if (!strcmp(s, "none"))
|
||
io_delay_type = CONFIG_IO_DELAY_TYPE_NONE;
|
||
else
|
||
return -EINVAL;
|
||
|
||
io_delay_override = 1;
|
||
return 0;
|
||
}
|
||
```
|
||
|
||
在 `io_delay_init` 之后,接下来执行的函数依次是 `acpi_boot_table_init`、`early_acpi_boot_init` 和 `initmem_init`。不过正如前文所述,在当前的「Linux 内核初始化流程」章节中,我们将不涉及与 [ACPI](http://en.wikipedia.org/wiki/Advanced_Configuration_and_Power_Interface) 相关的内容。
|
||
|
||
为 DMA 分配域
|
||
--------------------------------------------------------------------------------
|
||
|
||
下一步我们需要通过 `dma_contiguous_reserve` 函数为[直接内存访问(DMA)](http://en.wikipedia.org/wiki/Direct_memory_access)分配专用区域,该函数定义于 [drivers/base/dma-contiguous.c](https://github.com/torvalds/linux/blob/16f73eb02d7e1765ccab3d2018e0bd98eb93d973/drivers/base/dma-contiguous.c)。DMA 是一种特殊工作模式,设备无需 CPU 介入即可直接与内存通信。请注意,我们向 `dma_contiguous_reserve` 函数传递了一个关键参数——`max_pfn_mapped << PAGE_SHIFT`。从该表达式可以理解,此参数表示可保留内存的上限地址(将最大页帧号转换为字节地址)。让我们分析该函数的实现,它从定义以下变量开始:
|
||
|
||
```C
|
||
phys_addr_t selected_size = 0;
|
||
phys_addr_t selected_base = 0;
|
||
phys_addr_t selected_limit = limit;
|
||
bool fixed = false;
|
||
```
|
||
|
||
其中第一个参数表示保留区域的大小(以字节为单位),第二个参数是保留区域的基地址,第三个参数是保留区域的结束地址,最后一个 `fixed` 参数用于指定保留区域的放置方式。当 `fixed` 为 `1` 时,直接通过 `memblock_reserve` 保留内存区域;若为 `0` ,则使用 `kmemleak_alloc` 动态分配空间。在接下来的步骤中,函数会检查 `size_cmdline` 变量。若该变量不等于 `-1`(表示已通过命令行参数指定大小),则将使用来自 `cma` 内核命令行参数的值填充上述所有变量:
|
||
|
||
```C
|
||
if (size_cmdline != -1) {
|
||
...
|
||
...
|
||
...
|
||
}
|
||
```
|
||
|
||
在该源码文件中可以找到如下早期参数的定义:
|
||
|
||
```C
|
||
early_param("cma", early_cma);
|
||
```
|
||
|
||
其中 `cma` 表示:
|
||
|
||
```
|
||
cma=nn[MG]@[start[MG][-end[MG]]]
|
||
[ARM,X86,KNL]
|
||
Sets the size of kernel global memory area for
|
||
contiguous memory allocations and optionally the
|
||
placement constraint by the physical address range of
|
||
memory allocations. A value of 0 disables CMA
|
||
altogether. For more information, see
|
||
include/linux/dma-contiguous.h
|
||
```
|
||
|
||
如果未向内核命令行传递 `cma` 参数,则 `size_cmdline` 将保持默认值 `-1`。此时,系统需要根据以下内核配置选项来计算保留区域的大小:
|
||
|
||
* `CONFIG_CMA_SIZE_SEL_MBYTES` - 该选项表示以兆字节(MB)为单位的默认全局连续内存分配器(CMA)区域大小,其计算公式为 `CMA_SIZE_MBYTES * SZ_1M` 或等价的 `CONFIG_CMA_SIZE_MBYTES * 1M`;
|
||
* `CONFIG_CMA_SIZE_SEL_PERCENTAGE` - 总内存的百分比;
|
||
* `CONFIG_CMA_SIZE_SEL_MIN` - 取较小值;
|
||
* `CONFIG_CMA_SIZE_SEL_MAX` - 取较大值;
|
||
|
||
在计算出保留区域的大小后,系统将通过调用 `dma_contiguous_reserve_area` 函数来实际保留该内存区域。该函数的执行流程首先会调用以下函数:
|
||
|
||
```
|
||
ret = cma_declare_contiguous(base, size, limit, 0, 0, fixed, res_cma);
|
||
```
|
||
|
||
`cma_declare_contiguous` 函数用于从指定的基地址开始保留一块连续的物理内存区域,其大小由参数指定。完成 DMA 区域的内存保留后,接下来会调用 `memblock_find_dma_reserve` 函数——顾名思义,该函数用于统计 DMA 区域中已保留的页框数量。由于 CMA(连续内存分配器)和 DMA 相关实现较为复杂,本文暂不深入探讨所有细节。我们将在后续专门讲解 Linux 内核内存管理的章节中,详细分析连续内存分配器及其内存区域的实现机制。
|
||
|
||
稀疏内存初始化
|
||
--------------------------------------------------------------------------------
|
||
|
||
接下来将调用 `x86_init.paging.pagetable_init` 函数。若您在内核源码中追溯该函数的实现,最终会发现以下宏定义:
|
||
|
||
```C
|
||
#define native_pagetable_init paging_init
|
||
```
|
||
|
||
该宏如您所见,会展开调用 [arch/x86/mm/init_64.c](https://github.com/torvalds/linux/blob/16f73eb02d7e1765ccab3d2018e0bd98eb93d973/arch/x86/mm/init_64.c) 中的 `paging_init` 函数。`paging_init` 函数负责初始化稀疏内存和内存区域大小。首先需要了解什么是内存区域(zone)以及什么是 `Sparsemem`。`Sparsemem` 是 Linux 内核内存管理器中用于在 [NUMA](http://en.wikipedia.org/wiki/Non-uniform_memory_access) 系统中将内存区域划分为不同内存组(memory bank)的特殊基础架构。让我们看看 `paging_init` 函数的实现:
|
||
|
||
```C
|
||
void __init paging_init(void)
|
||
{
|
||
sparse_memory_present_with_active_regions(MAX_NUMNODES);
|
||
sparse_init();
|
||
|
||
node_clear_state(0, N_MEMORY);
|
||
if (N_MEMORY != N_NORMAL_MEMORY)
|
||
node_clear_state(0, N_NORMAL_MEMORY);
|
||
|
||
zone_sizes_init();
|
||
}
|
||
```
|
||
|
||
可以看到,这里调用了 `sparse_memory_present_with_active_regions` 函数,该函数会为每个 NUMA 节点记录内存区域到 `mem_section` 结构数组中,该结构包含指向 `struct page` 数组结构的指针。随后的 `sparse_init` 函数会分配非线性的 `mem_section` 和 `mem_map`。接下来我们会清除可移动内存节点的状态并初始化各内存区域(zone)的大小。每个 NUMA 节点都被划分为多个称为"zone"的部分。因此,来自 [arch/x86/mm/init.c](https://github.com/torvalds/linux/blob/16f73eb02d7e1765ccab3d2018e0bd98eb93d973/arch/x86/mm/init.c) 的 `zone_sizes_init` 函数就是用于初始化这些zone的大小的。
|
||
|
||
再次说明,本部分及后续部分不会完整详细地涵盖这一主题。关于 NUMA 将有专门的章节进行讲解。
|
||
|
||
vsyscall 映射
|
||
--------------------------------------------------------------------------------
|
||
|
||
在 `SparseMem` 初始化之后,下一步是设置 `trampoline_cr4_features`,它必须包含 `cr4` [控制寄存器](http://en.wikipedia.org/wiki/Control_register)的内容。首先我们需要检查当前 CPU 是否支持 `cr4` 寄存器,如果支持,就将其内容保存到 `trampoline_cr4_features` ,这是在实模式下存储 `cr4` 的地方。
|
||
|
||
```C
|
||
if (boot_cpu_data.cpuid_level >= 0) {
|
||
mmu_cr4_features = __read_cr4();
|
||
if (trampoline_cr4_features)
|
||
*trampoline_cr4_features = mmu_cr4_features;
|
||
}
|
||
```
|
||
|
||
接下来您会看到的是 [arch/x86/entry/vsyscall/vsyscall_64.c](https://github.com/torvalds/linux/blob/master/arch/x86/entry/vsyscall/vsyscall_64.c) 的 `map_vsyscall` 函数。这个函数为 [vsyscalls](https://lwn.net/Articles/446528/) 映射内存空间,其功能依赖于内核配置选项 `CONFIG_X86_VSYSCALL_EMULATION`。实际上,`vsyscall` 是一个特殊的段,它提供了对某些系统调用(如 `getcpu` 等)的快速访问。该函数的具体实现如下:
|
||
|
||
```C
|
||
void __init map_vsyscall(void)
|
||
{
|
||
extern char __vsyscall_page;
|
||
unsigned long physaddr_vsyscall = __pa_symbol(&__vsyscall_page);
|
||
|
||
if (vsyscall_mode != NONE)
|
||
__set_fixmap(VSYSCALL_PAGE, physaddr_vsyscall,
|
||
vsyscall_mode == NATIVE
|
||
? PAGE_KERNEL_VSYSCALL
|
||
: PAGE_KERNEL_VVAR);
|
||
|
||
BUILD_BUG_ON((unsigned long)__fix_to_virt(VSYSCALL_PAGE) !=
|
||
(unsigned long)VSYSCALL_ADDR);
|
||
}
|
||
```
|
||
|
||
在 `map_vsyscall` 函数的开头,我们可以看到两个变量的定义。第一个是外部变量 `__vsyscall_page`。作为外部变量,它实际上是在其他源文件中定义的。我们可以在 [arch/x86/entry/vsyscall/vsyscall_emu_64.S](https://github.com/torvalds/linux/blob/master/arch/x86/entry/vsyscall/vsyscall_emu_64.S) 中找到 `__vsyscall_page` 的定义。`__vsyscall_page` 符号指向对齐的 vsyscalls 调用,如 `gettimeofday` 等:
|
||
|
||
```assembly
|
||
.globl __vsyscall_page
|
||
.balign PAGE_SIZE, 0xcc
|
||
.type __vsyscall_page, @object
|
||
__vsyscall_page:
|
||
|
||
mov $__NR_gettimeofday, %rax
|
||
syscall
|
||
ret
|
||
|
||
.balign 1024, 0xcc
|
||
mov $__NR_time, %rax
|
||
syscall
|
||
ret
|
||
...
|
||
...
|
||
...
|
||
```
|
||
|
||
第二个变量是 `physaddr_vsyscall`,它仅存储 `__vsyscall_page` 符号的物理地址。在接下来的步骤中,我们会检查 `vsyscall_mode` 变量,如果它不等于 `NONE`(默认情况下是 `EMULATE` 模式):
|
||
|
||
```C
|
||
static enum { EMULATE, NATIVE, NONE } vsyscall_mode = EMULATE;
|
||
```
|
||
|
||
随后我们会看到调用 `__set_fixmap` 函数,该函数会以相同的参数调用 `native_set_fixmap`:
|
||
|
||
```C
|
||
void native_set_fixmap(enum fixed_addresses idx, unsigned long phys, pgprot_t flags)
|
||
{
|
||
__native_set_fixmap(idx, pfn_pte(phys >> PAGE_SHIFT, flags));
|
||
}
|
||
|
||
void __native_set_fixmap(enum fixed_addresses idx, pte_t pte)
|
||
{
|
||
unsigned long address = __fix_to_virt(idx);
|
||
|
||
if (idx >= __end_of_fixed_addresses) {
|
||
BUG();
|
||
return;
|
||
}
|
||
set_pte_vaddr(address, pte);
|
||
fixmaps_set++;
|
||
}
|
||
```
|
||
|
||
这里我们可以看到 `native_set_fixmap` 根据给定的物理地址(在我们的例子中是 `__vsyscall_page` 符号的物理地址)生成页表项的值,并调用内部函数 `__native_set_fixmap`。这个内部函数获取给定 `fixed_addresses` 索引(在我们的例子中是 `VSYSCALL_PAGE`)的虚拟地址,并检查给定的索引不超过 fix-mapped 地址的结束范围。之后我们通过调用 `set_pte_vaddr` 函数设置页表项,并增加 fix-mapped 地址的计数。在 `map_vsyscall` 的最后,我们检查 `VSYSCALL_PAGE`(这是 `fixed_addresses` 中的第一个索引)的虚拟地址不超过 `VSYSCALL_ADDR`,即 `-10UL << 20` 或 `ffffffffff600000`,这是通过 `BUILD_BUG_ON` 宏实现的。
|
||
|
||
```C
|
||
BUILD_BUG_ON((unsigned long)__fix_to_virt(VSYSCALL_PAGE) !=
|
||
(unsigned long)VSYSCALL_ADDR);
|
||
```
|
||
|
||
现在,`vsyscall` 区域已被置于固定映射(fix-mapped)地址区域。关于 `map_vsyscall` 的内容就是这些。如果您对固定映射地址不熟悉,可以参考[《固定映射地址与 ioremap》](https://0xax.gitbook.io/linux-insides/summary/mm/linux-mm-2)一文。我们将在后续关于 `vsyscalls` 和 `vdso` 的章节中更详细地探讨 `vsyscalls` 的实现机制。
|
||
|
||
获取 SMP 配置
|
||
--------------------------------------------------------------------------------
|
||
|
||
您可能还记得我们在前一部分中是如何搜索 [SMP](http://en.wikipedia.org/wiki/Symmetric_multiprocessing) 配置的。现在,如果找到了 SMP 配置,我们需要获取它。为此,我们检查在 `smp_scan_config` 函数中设置的 `smp_found_config` 变量(关于该函数请参阅前一部分),并调用 `get_smp_config` 函数:
|
||
|
||
```C
|
||
if (smp_found_config)
|
||
get_smp_config();
|
||
```
|
||
|
||
`get_smp_config` 展开为 `x86_init.mpparse.default_get_smp_config` 函数,该函数定义于 [arch/x86/kernel/mpparse.c](https://github.com/torvalds/linux/blob/16f73eb02d7e1765ccab3d2018e0bd98eb93d973/arch/x86/kernel/mpparse.c)。这个函数定义了指向多处理器浮点指针结构 `mpf_intel` 的指针(您可以在前文[第六部分](https://0xax.gitbook.io/linux-insides/summary/initialization/linux-initialization-6)中阅读相关内容),并执行以下检查:
|
||
|
||
```C
|
||
struct mpf_intel *mpf = mpf_found;
|
||
|
||
if (!mpf)
|
||
return;
|
||
|
||
if (acpi_lapic && early)
|
||
return;
|
||
```
|
||
|
||
这里我们可以看到,如果在 `smp_scan_config` 函数中找到了多处理器配置就继续执行,否则直接返回函数。接下来的检查是 `acpi_lapic` 和 `early` 标志。完成这些检查后,我们开始读取 SMP 配置。读取完成后,下一步是调用 `prefill_possible_map` 函数,该函数会预先填充可能的 CPU 的 `cpumask`(更多关于此的内容可参阅 [cpumasks 简介](https://0xax.gitbook.io/linux-insides/summary/concepts/linux-cpu-2))。
|
||
|
||
setup_arch 的剩余部分
|
||
--------------------------------------------------------------------------------
|
||
|
||
现在我们已经接近 `setup_arch` 函数的尾声。虽然剩余部分也很重要,但本部分不会详细讨论这些内容。我们将简要浏览这些函数,因为它们主要涉及 `NUMA`、`SMP`、`ACPI` 和 `APIC` 等非通用内核特性。首先调用的是 `init_apic_mappings` 函数,它负责设置本地 [APIC](http://en.wikipedia.org/wiki/Advanced_Programmable_Interrupt_Controller) 的地址。接着是 `x86_io_apic_ops.init` 函数,用于初始化 I/O APIC(关于 APIC 的完整细节将在中断和异常处理章节介绍)。随后通过 `x86_init.resources.reserve_resources` 调用保留标准 I/O 资源(如 `DMA`、`TIMER`、`FPU` 等)。然后是初始化机器检查异常的 `mcheck_init` 函数,最后是注册 [jiffy](http://en.wikipedia.org/wiki/Jiffy_%28time%29) 的 `register_refined_jiffies`(内核定时器将有专门章节讨论)。
|
||
|
||
至此,我们完成了对 `setup_arch` 这个庞大函数的分析。虽然如我多次提到的,我们尚未涵盖该函数的全部细节,但不必担心。在后续不同章节中,我们还会多次回顾这个函数,以理解各种平台相关部分是如何初始化的。
|
||
|
||
现在我们可以从 `setup_arch` 返回到 `start_kernel` 函数继续分析了。
|
||
|
||
回到 main.c
|
||
================================================================================
|
||
|
||
如前所述,我们已经完成了对 `setup_arch` 函数的分析,现在可以回到 [init/main.c](https://github.com/torvalds/linux/blob/16f73eb02d7e1765ccab3d2018e0bd98eb93d973/init/main.c) 中的 `start_kernel` 函数。您可能记得或已经注意到,`start_kernel` 函数与 `setup_arch` 一样庞大,因此接下来的几个部分将专门学习这个函数。
|
||
|
||
在 `setup_arch` 之后,我们可以看到 `mm_init_cpumask` 函数的调用。这个函数将 [cpumask](https://0xax.gitbook.io/linux-insides/summary/concepts/linux-cpu-2) 指针设置到内存描述符的 `cpumask` 中。让我们看看它的实现:
|
||
|
||
```C
|
||
static inline void mm_init_cpumask(struct mm_struct *mm)
|
||
{
|
||
#ifdef CONFIG_CPUMASK_OFFSTACK
|
||
mm->cpu_vm_mask_var = &mm->cpumask_allocation;
|
||
#endif
|
||
cpumask_clear(mm->cpu_vm_mask_var);
|
||
}
|
||
```
|
||
|
||
如您所见,在 [init/main.c](https://github.com/torvalds/linux/blob/16f73eb02d7e1765ccab3d2018e0bd98eb93d973/init/main.c) 中我们将 init 进程的内存描述符传递给 `mm_init_cpumask` 函数,并根据 `CONFIG_CPUMASK_OFFSTACK` 配置选项来决定是否清除 [TLB](http://en.wikipedia.org/wiki/Translation_lookaside_buffer) 并切换 `cpumask`。
|
||
|
||
在接下来的步骤中,我们会看到以下函数的调用:
|
||
|
||
```C
|
||
setup_command_line(command_line);
|
||
```
|
||
|
||
该函数接收内核命令行指针,并分配两个缓冲区来存储命令行。我们需要两个缓冲区,因为一个用于将来引用和访问命令行,另一个用于参数解析。我们将为以下缓冲区分配空间:
|
||
|
||
* `saved_command_line` - 将保存启动命令行;
|
||
* `initcall_command_line` - 将保存启动命令行,将在 `do_initcall_level` 中使用;
|
||
* `static_command_line` - 将保存用于参数解析的命令行。
|
||
|
||
我们将使用 `memblock_virt_alloc` 函数分配空间。这个函数调用 `memblock_virt_alloc_try_nid`,如果 [slab](http://en.wikipedia.org/wiki/Slab_allocation) 不可用,则用 `memblock_reserve` 分配启动内存块,否则使用 `kzalloc_node`(更多相关内容将在Linux内存管理章节介绍)。`memblock_virt_alloc` 使用 `BOOTMEM_LOW_LIMIT`(值为 `PAGE_OFFSET + 0x1000000` 的物理地址)和 `BOOTMEM_ALLOC_ACCESSIBLE`(等于当前 `memblock.current_limit` 的值)作为内存区域的最小地址和最大地址。
|
||
|
||
让我们看看 `setup_command_line` 的实现:
|
||
|
||
```C
|
||
static void __init setup_command_line(char *command_line)
|
||
{
|
||
saved_command_line =
|
||
memblock_virt_alloc(strlen(boot_command_line) + 1, 0);
|
||
initcall_command_line =
|
||
memblock_virt_alloc(strlen(boot_command_line) + 1, 0);
|
||
static_command_line = memblock_virt_alloc(strlen(command_line) + 1, 0);
|
||
strcpy(saved_command_line, boot_command_line);
|
||
strcpy(static_command_line, command_line);
|
||
}
|
||
```
|
||
|
||
这里我们为三个缓冲区分配了空间,这些缓冲区将存储用于不同目的的内核命令行(如上所述)。完成空间分配后,我们将 `boot_command_line` 存入 `saved_command_line`,并将来自 `setup_arch` 的 `command_line`(内核命令行)存入 `static_command_line`。
|
||
|
||
在 `setup_command_line` 之后的下一个函数是 `setup_nr_cpu_ids`。该函数根据 `cpu_possible_mask` 的最后一位来设置 `nr_cpu_ids`(CPU 的数量)。关于此概念的更多细节,您可以阅读描述 [cpumasks](https://0xax.gitbook.io/linux-insides/summary/concepts/linux-cpu-2) 概念的章节。让我们看看它的实现:
|
||
|
||
```C
|
||
void __init setup_nr_cpu_ids(void)
|
||
{
|
||
nr_cpu_ids = find_last_bit(cpumask_bits(cpu_possible_mask),NR_CPUS) + 1;
|
||
}
|
||
```
|
||
|
||
这里 `nr_cpu_ids` 表示实际可用的 CPU 数量,而 `NR_CPUS` 表示在配置时可设置的最大 CPU 数量:
|
||
|
||

|
||
|
||
实际上我们需要调用这个函数,因为 `NR_CPUS` 可能会大于您计算机中实际的 CPU 数量。这里我们可以看到调用了 `find_last_bit` 函数并传递了两个参数:
|
||
|
||
- `cpu_possible_mask` 位图;
|
||
- CPU 的最大数量。
|
||
|
||
在 `setup_arch` 中,我们可以找到 `prefill_possible_map` 函数的调用,它计算实际 CPU 数量并写入 `cpu_possible_mask`。`find_last_bit` 函数接收地址和最大搜索范围作为参数,返回第一个置位(1)的位号。我们传入了 `cpu_possible_mask` 位图和 CPU 的最大数量。
|
||
|
||
首先,`find_last_bit` 函数将给定的 `unsigned long` 地址分割成[字](http://en.wikipedia.org/wiki/Word_%28computer_architecture%29):
|
||
|
||
```C
|
||
words = size / BITS_PER_LONG;
|
||
```
|
||
|
||
其中在 `x86_64` 架构上,`BITS_PER_LONG` 的值为 `64`。当我们获得搜索数据给定大小中的字数后,需要通过以下检查确认给定大小是否包含不完整的字:
|
||
|
||
```C
|
||
if (size & (BITS_PER_LONG-1)) {
|
||
tmp = (addr[words] & (~0UL >> (BITS_PER_LONG
|
||
- (size & (BITS_PER_LONG-1)))));
|
||
if (tmp)
|
||
goto found;
|
||
}
|
||
```
|
||
|
||
如果存在不完整的字,我们将对最后一个字进行掩码处理并检查它。如果最后一个字不为零,则表明当前字至少包含一个置位。此时程序将跳转到 `found` 标签处继续执行:
|
||
|
||
```C
|
||
found:
|
||
return words * BITS_PER_LONG + __fls(tmp);
|
||
```
|
||
|
||
这里您可以看到 `__fls` 函数,它通过 `bsr`(Bit Scan Reverse)指令的帮助返回给定字中最后一个置位的位号:
|
||
|
||
```C
|
||
static inline unsigned long __fls(unsigned long word)
|
||
{
|
||
asm("bsr %1,%0"
|
||
: "=r" (word)
|
||
: "rm" (word));
|
||
return word;
|
||
}
|
||
```
|
||
|
||
`bsr` 指令会扫描给定的操作数以查找第一个置位。如果最后一个字不是部分字,我们将遍历给定地址中的所有字,尝试找到第一个置位:
|
||
|
||
```C
|
||
while (words) {
|
||
tmp = addr[--words];
|
||
if (tmp) {
|
||
found:
|
||
return words * BITS_PER_LONG + __fls(tmp);
|
||
}
|
||
}
|
||
```
|
||
|
||
这里我们将最后一个字存入 `tmp` 变量,并检查 `tmp` 是否包含至少一个置位。如果找到置位,就返回该位的编号。如果所有字都不包含置位,则直接返回给定的搜索范围大小:
|
||
|
||
```C
|
||
return size;
|
||
```
|
||
|
||
完成这些操作后,`nr_cpu_ids` 将包含正确的可用 CPU 数量。
|
||
|
||
至此架构相关初始化部分分析完毕。
|
||
|
||
总结
|
||
================================================================================
|
||
|
||
这是关于 Linux 内核初始化过程的第七部分的结尾。在本次分析中,我们最终完成了对 `setup_arch` 函数的研究,并返回到 `start_kernel` 函数。在下一部分中,我们将继续学习 `start_kernel` 中的通用内核代码,沿着内核启动路径深入,直到第一个 `init` 进程的创建。
|
||
|
||
如果您有任何疑问或建议,欢迎在评论区留言,或通过 [Twitter](https://twitter.com/0xAX) 与我联系。
|
||
|
||
**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 PR to [linux-insides](https://github.com/0xAX/linux-insides).**
|
||
|
||
链接
|
||
================================================================================
|
||
|
||
* [Desktop Management Interface](http://en.wikipedia.org/wiki/Desktop_Management_Interface)
|
||
* [x86_64](http://en.wikipedia.org/wiki/X86-64)
|
||
* [initrd](http://en.wikipedia.org/wiki/Initrd)
|
||
* [Kernel panic](http://en.wikipedia.org/wiki/Kernel_panic)
|
||
* [Documentation/kernel-parameters.txt](https://github.com/torvalds/linux/blob/master/Documentation/admin-guide/kernel-parameters.rst)
|
||
* [ACPI](http://en.wikipedia.org/wiki/Advanced_Configuration_and_Power_Interface)
|
||
* [Direct memory access](http://en.wikipedia.org/wiki/Direct_memory_access)
|
||
* [NUMA](http://en.wikipedia.org/wiki/Non-uniform_memory_access)
|
||
* [Control register](http://en.wikipedia.org/wiki/Control_register)
|
||
* [vsyscalls](https://lwn.net/Articles/446528/)
|
||
* [SMP](http://en.wikipedia.org/wiki/Symmetric_multiprocessing)
|
||
* [jiffy](http://en.wikipedia.org/wiki/Jiffy_%28time%29)
|
||
* [Previous part](https://0xax.gitbook.io/linux-insides/summary/initialization/linux-initialization-6) |