Files
linux-insides-zh/Initialization/linux-initialization-7.md
chenhr56 76f7403760 Update Translate status (#298)
* Update Translate status

* Finish translate session 2.7

* Update translate status

* Update translate 2.7

* Update translate 2.7
2025-03-31 19:31:34 +08:00

488 lines
30 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
内核初始化 第七部分
================================================================================
架构相关初始化尾声:最后一程
================================================================================
这是 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 数量:
![CONFIG_NR_CPUS](images/CONFIG_NR_CPUS.png)
实际上我们需要调用这个函数,因为 `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)