LogoLogo
  • 简介
  • 引导
    • 从引导加载程序内核
    • 在内核安装代码的第一步
    • 视频模式初始化和转换到保护模式
    • 过渡到 64 位模式
    • 内核解压缩
  • 初始化
    • 内核解压之后的首要步骤
    • 早期的中断和异常控制
    • 在到达内核入口之前最后的准备
    • 内核入口 - start_kernel
    • 体系架构初始化
    • 进一步初始化指定体系架构
    • 最后对指定体系架构初始化
    • 调度器初始化
    • RCU 初始化
    • 初始化结束
  • 中断
    • 中断和中断处理第一部分
    • 深入 Linux 内核中的中断
    • 初步中断处理
    • 中断处理
    • 异常处理的实现
    • 处理不可屏蔽中断
    • 深入外部硬件中断
    • IRQs的非早期初始化
    • Softirq, Tasklets and Workqueues
    • 最后一部分
  • 系统调用
    • 系统调用概念简介
    • Linux 内核如何处理系统调用
    • vsyscall and vDSO
    • Linux 内核如何运行程序
    • open 系统调用的实现
    • Linux 资源限制
  • 定时器和时钟管理
    • 简介
    • 时钟源框架简介
    • The tick broadcast framework and dyntick
    • 定时器介绍
    • Clockevents 框架简介
    • x86 相关的时钟源
    • Linux 内核中与时钟相关的系统调用
  • 同步原语
    • 自旋锁简介
    • 队列自旋锁
    • 信号量
    • 互斥锁
    • 读者/写者信号量
    • 顺序锁
    • RCU
    • Lockdep
  • 内存管理
    • 内存块
    • 固定映射地址和 ioremap
    • kmemcheck
  • 控制组
    • 控制组简介
  • SMP
  • 概念
    • 每个 CPU 的变量
    • CPU 掩码
    • initcall 机制
    • Linux 内核的通知链
  • Linux 内核中的数据结构
    • 双向链表
    • 基数树
    • 位数组
  • 理论
    • 分页
    • ELF 文件格式
    • 內联汇编
    • CPUID
    • MSR
  • Initial ram disk
  • 杂项
    • Linux 内核开发
    • 内核编译方法
    • 链接器
    • 用户空间的程序启动过程
    • 书写并提交你第一个内核补丁
  • 内核数据结构
    • 中断描述符表
  • 有帮助的链接
  • 贡献者
由 GitBook 提供支持
在本页
  • 内核初始化 第七部分
  • 架构相关初始化尾声:最后一程
  • 为 DMA 分配域
  • 稀疏内存初始化
  • vsyscall 映射
  • 获取 SMP 配置
  • setup_arch 的剩余部分
  • 回到 main.c
  • 总结
  • 链接
  1. 初始化

最后对指定体系架构初始化

上一页进一步初始化指定体系架构下一页调度器初始化

最后更新于1个月前

内核初始化 第七部分

架构相关初始化尾声:最后一程

这是 Linux 内核初始化过程的第七部分,主要解析 文件中 setup_arch 函数内部机制。通过可知,setup_arch 函数执行架构相关(本文以 为例)的初始化工作,包括为内核代码/数据/bss 保留内存,的早期扫描, 设备的早期转储等众多操作。如果您已经阅读了前面的,会记得我们是在 setup_real_mode 函数处结束的。接下来,在我们限制 为所有已映射页后,可以看到 中调用了 setup_log_buf 函数。

setup_log_buf 函数用于设置内核循环缓冲区,其长度取决于 CONFIG_LOG_BUF_SHIFT 配置选项。从文档中可知,CONFIG_LOG_BUF_SHIFT 取值范围在 12 到 21 之间。在内部实现中,该缓冲区定义为字符数组:

#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 扩展缓冲区的大小:

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 的:

setup_log_buf(1);

其中参数 1 表示当前处于早期初始化阶段。接下来,我们会检查 new_log_buf_len 变量(该变量表示更新后的内核日志缓冲区长度),并通过 memblock_virt_alloc 函数为其分配新的缓冲区空间,否则直接返回。

当内核日志缓冲区准备就绪后,下一个执行的是 reserve_initrd 函数。您可能还记得,在中我们已经调用过 early_reserve_initrd 函数。现在,由于我们已在 init_mem_mapping 函数中重建了直接内存映射,因此需要将移入直接映射内存区域。reserve_initrd 函数首先确定 initrd 的基地址和结束地址,并检查 bootloader 是否提供了 initrd —— 这些操作与我们在 early_reserve_initrd 中看到的完全一致。但不同于之前通过调用 memblock_reserve 在 memblock 区域中保留空间的做法,这里我们会获取直接内存映射区域的映射大小,并通过以下方式确保 initrd 的大小不超过该区域:

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);

接下来,我们会打印关于 initrd 大小的信息。通过 dmesg 命令的输出可以看到如下结果:

[0.000000] RAMDISK: [mem 0x36d20000-0x37687fff]

随后通过 relocate_initrd 函数将 initrd 重定位到直接映射区域。在 relocate_initrd 函数起始处,我们会尝试使用 memblock_find_in_range 函数来寻找可用内存区域:

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 内存:

memblock_free(ramdisk_image, ramdisk_end - ramdisk_image);
void __init io_delay_init(void)
{
    if (!io_delay_override)
        dmi_check_system(io_delay_0xed_port_dmi_table);
}
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
early_param("io_delay", io_delay_param);
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;
}

为 DMA 分配域

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 内核命令行参数的值填充上述所有变量:

if (size_cmdline != -1) {
   ...
   ...
   ...
}

在该源码文件中可以找到如下早期参数的定义:

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 函数。若您在内核源码中追溯该函数的实现,最终会发现以下宏定义:

#define native_pagetable_init        paging_init
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();
}

再次说明,本部分及后续部分不会完整详细地涵盖这一主题。关于 NUMA 将有专门的章节进行讲解。

vsyscall 映射

if (boot_cpu_data.cpuid_level >= 0) {
    mmu_cr4_features = __read_cr4();
	if (trampoline_cr4_features)
	    *trampoline_cr4_features = mmu_cr4_features;
}
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);
}
	.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 模式):

static enum { EMULATE, NATIVE, NONE } vsyscall_mode = EMULATE;

随后我们会看到调用 __set_fixmap 函数,该函数会以相同的参数调用 native_set_fixmap:

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 宏实现的。

BUILD_BUG_ON((unsigned long)__fix_to_virt(VSYSCALL_PAGE) !=
                     (unsigned long)VSYSCALL_ADDR);

获取 SMP 配置

if (smp_found_config)
	get_smp_config();
struct mpf_intel *mpf = mpf_found;

if (!mpf)
    return;

if (acpi_lapic && early)
   return;

setup_arch 的剩余部分

至此,我们完成了对 setup_arch 这个庞大函数的分析。虽然如我多次提到的,我们尚未涵盖该函数的全部细节,但不必担心。在后续不同章节中,我们还会多次回顾这个函数,以理解各种平台相关部分是如何初始化的。

现在我们可以从 setup_arch 返回到 start_kernel 函数继续分析了。

回到 main.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);
}

在接下来的步骤中,我们会看到以下函数的调用:

setup_command_line(command_line);

该函数接收内核命令行指针,并分配两个缓冲区来存储命令行。我们需要两个缓冲区,因为一个用于将来引用和访问命令行,另一个用于参数解析。我们将为以下缓冲区分配空间:

  • saved_command_line - 将保存启动命令行;

  • initcall_command_line - 将保存启动命令行,将在 do_initcall_level 中使用;

  • static_command_line - 将保存用于参数解析的命令行。

让我们看看 setup_command_line 的实现:

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。

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 的最大数量。

words = size / BITS_PER_LONG;

其中在 x86_64 架构上,BITS_PER_LONG 的值为 64。当我们获得搜索数据给定大小中的字数后,需要通过以下检查确认给定大小是否包含不完整的字:

if (size & (BITS_PER_LONG-1)) {
         tmp = (addr[words] & (~0UL >> (BITS_PER_LONG
                                 - (size & (BITS_PER_LONG-1)))));
         if (tmp)
                 goto found;
}

如果存在不完整的字,我们将对最后一个字进行掩码处理并检查它。如果最后一个字不为零,则表明当前字至少包含一个置位。此时程序将跳转到 found 标签处继续执行:

found:
    return words * BITS_PER_LONG + __fls(tmp);

这里您可以看到 __fls 函数,它通过 bsr(Bit Scan Reverse)指令的帮助返回给定字中最后一个置位的位号:

static inline unsigned long __fls(unsigned long word)
{
        asm("bsr %1,%0"
            : "=r" (word)
            : "rm" (word));
        return word;
}

bsr 指令会扫描给定的操作数以查找第一个置位。如果最后一个字不是部分字,我们将遍历给定地址中的所有字,尝试找到第一个置位:

while (words) {
    tmp = addr[--words];
    if (tmp) {
found:
        return words * BITS_PER_LONG + __fls(tmp);
    }
}

这里我们将最后一个字存入 tmp 变量,并检查 tmp 是否包含至少一个置位。如果找到置位,就返回该位的编号。如果所有字都不包含置位,则直接返回给定的搜索范围大小:

return size;

完成这些操作后,nr_cpu_ids 将包含正确的可用 CPU 数量。

至此架构相关初始化部分分析完毕。

总结

这是关于 Linux 内核初始化过程的第七部分的结尾。在本次分析中,我们最终完成了对 setup_arch 函数的研究,并返回到 start_kernel 函数。在下一部分中,我们将继续学习 start_kernel 中的通用内核代码,沿着内核启动路径深入,直到第一个 init 进程的创建。

链接

可以看到,这里我们调用了 memblock_mem_size 函数,并将 max_pfn_mapped 作为参数传入。其中,max_pfn_mapped 存储的是当前直接映射的最高页帧号(Page Frame Number)。若不记得什么是页帧号,这里简单说明:虚拟地址的低 12 位表示物理页(页帧)的偏移量。当我们右移虚拟地址的 12 位时,将丢弃偏移部分,从而得到页帧号。在 memblock_mem_size 函数内部,我们会遍历所有 memblock 的 mem 区域(不包括保留区域),计算已映射页面的总大小,并将结果返回给 mapped_size 变量(参见上文代码)。获取到直接映射内存的总量后,我们会检查 initrd 的大小是否超过已映射的页面范围。如果超出,则直接调用 panic 函数终止系统运行,并打印著名的信息。

在完成 initrd RAM 磁盘镜像的重定位后,接下来执行的是位于 的 vsmp_init 函数。该函数用于初始化 ScaleMP vSMP 架构支持。如先前章节所述,本文不会涉及与 x86_64 初始化无关的内容(例如当前的 ACPI 等)。因此我们将暂时跳过其具体实现,留待后续讲解并行计算技术时再作探讨。

随后调用的是 中的 io_delay_init 函数。该函数允许覆盖默认的 I/O 延迟端口 0x80。我们已在中接触过 I/O 延迟的概念,现在让我们深入分析 io_delay_init 的具体实现:

该函数会检查 io_delay_override 变量,若该变量被设置,则覆盖默认的 I/O 延迟端口。我们可以通过向内核命令行传递 io_delay 参数来设置 io_delay_override 变量。根据 文档说明,io_delay 选项是:

我们可以看到,在 文件中,io_delay 命令行参数是通过 early_param 宏进行设置的:

关于 early_param 宏的更多细节,您可以在中查阅。因此,用于设置 io_delay_override 变量的 io_delay_param 函数将会在 函数中被调用。io_delay_param 函数通过解析 io_delay 内核命令行参数,根据传入值设置相应的 io_delay_type:

在 io_delay_init 之后,接下来执行的函数依次是 acpi_boot_table_init、early_acpi_boot_init 和 initmem_init。不过正如前文所述,在当前的「Linux 内核初始化流程」章节中,我们将不涉及与 相关的内容。

下一步我们需要通过 dma_contiguous_reserve 函数为分配专用区域,该函数定义于 。DMA 是一种特殊工作模式,设备无需 CPU 介入即可直接与内存通信。请注意,我们向 dma_contiguous_reserve 函数传递了一个关键参数——max_pfn_mapped << PAGE_SHIFT。从该表达式可以理解,此参数表示可保留内存的上限地址(将最大页帧号转换为字节地址)。让我们分析该函数的实现,它从定义以下变量开始:

该宏如您所见,会展开调用 中的 paging_init 函数。paging_init 函数负责初始化稀疏内存和内存区域大小。首先需要了解什么是内存区域(zone)以及什么是 Sparsemem。Sparsemem 是 Linux 内核内存管理器中用于在 系统中将内存区域划分为不同内存组(memory bank)的特殊基础架构。让我们看看 paging_init 函数的实现:

可以看到,这里调用了 sparse_memory_present_with_active_regions 函数,该函数会为每个 NUMA 节点记录内存区域到 mem_section 结构数组中,该结构包含指向 struct page 数组结构的指针。随后的 sparse_init 函数会分配非线性的 mem_section 和 mem_map。接下来我们会清除可移动内存节点的状态并初始化各内存区域(zone)的大小。每个 NUMA 节点都被划分为多个称为"zone"的部分。因此,来自 的 zone_sizes_init 函数就是用于初始化这些zone的大小的。

在 SparseMem 初始化之后,下一步是设置 trampoline_cr4_features,它必须包含 cr4 的内容。首先我们需要检查当前 CPU 是否支持 cr4 寄存器,如果支持,就将其内容保存到 trampoline_cr4_features ,这是在实模式下存储 cr4 的地方。

接下来您会看到的是 的 map_vsyscall 函数。这个函数为 映射内存空间,其功能依赖于内核配置选项 CONFIG_X86_VSYSCALL_EMULATION。实际上,vsyscall 是一个特殊的段,它提供了对某些系统调用(如 getcpu 等)的快速访问。该函数的具体实现如下:

在 map_vsyscall 函数的开头,我们可以看到两个变量的定义。第一个是外部变量 __vsyscall_page。作为外部变量,它实际上是在其他源文件中定义的。我们可以在 中找到 __vsyscall_page 的定义。__vsyscall_page 符号指向对齐的 vsyscalls 调用,如 gettimeofday 等:

现在,vsyscall 区域已被置于固定映射(fix-mapped)地址区域。关于 map_vsyscall 的内容就是这些。如果您对固定映射地址不熟悉,可以参考一文。我们将在后续关于 vsyscalls 和 vdso 的章节中更详细地探讨 vsyscalls 的实现机制。

您可能还记得我们在前一部分中是如何搜索 配置的。现在,如果找到了 SMP 配置,我们需要获取它。为此,我们检查在 smp_scan_config 函数中设置的 smp_found_config 变量(关于该函数请参阅前一部分),并调用 get_smp_config 函数:

get_smp_config 展开为 x86_init.mpparse.default_get_smp_config 函数,该函数定义于 。这个函数定义了指向多处理器浮点指针结构 mpf_intel 的指针(您可以在前文中阅读相关内容),并执行以下检查:

这里我们可以看到,如果在 smp_scan_config 函数中找到了多处理器配置就继续执行,否则直接返回函数。接下来的检查是 acpi_lapic 和 early 标志。完成这些检查后,我们开始读取 SMP 配置。读取完成后,下一步是调用 prefill_possible_map 函数,该函数会预先填充可能的 CPU 的 cpumask(更多关于此的内容可参阅 )。

现在我们已经接近 setup_arch 函数的尾声。虽然剩余部分也很重要,但本部分不会详细讨论这些内容。我们将简要浏览这些函数,因为它们主要涉及 NUMA、SMP、ACPI 和 APIC 等非通用内核特性。首先调用的是 init_apic_mappings 函数,它负责设置本地 的地址。接着是 x86_io_apic_ops.init 函数,用于初始化 I/O APIC(关于 APIC 的完整细节将在中断和异常处理章节介绍)。随后通过 x86_init.resources.reserve_resources 调用保留标准 I/O 资源(如 DMA、TIMER、FPU 等)。然后是初始化机器检查异常的 mcheck_init 函数,最后是注册 的 register_refined_jiffies(内核定时器将有专门章节讨论)。

如前所述,我们已经完成了对 setup_arch 函数的分析,现在可以回到 中的 start_kernel 函数。您可能记得或已经注意到,start_kernel 函数与 setup_arch 一样庞大,因此接下来的几个部分将专门学习这个函数。

在 setup_arch 之后,我们可以看到 mm_init_cpumask 函数的调用。这个函数将 指针设置到内存描述符的 cpumask 中。让我们看看它的实现:

如您所见,在 中我们将 init 进程的内存描述符传递给 mm_init_cpumask 函数,并根据 CONFIG_CPUMASK_OFFSTACK 配置选项来决定是否清除 并切换 cpumask。

我们将使用 memblock_virt_alloc 函数分配空间。这个函数调用 memblock_virt_alloc_try_nid,如果 不可用,则用 memblock_reserve 分配启动内存块,否则使用 kzalloc_node(更多相关内容将在Linux内存管理章节介绍)。memblock_virt_alloc 使用 BOOTMEM_LOW_LIMIT(值为 PAGE_OFFSET + 0x1000000 的物理地址)和 BOOTMEM_ALLOC_ACCESSIBLE(等于当前 memblock.current_limit 的值)作为内存区域的最小地址和最大地址。

在 setup_command_line 之后的下一个函数是 setup_nr_cpu_ids。该函数根据 cpu_possible_mask 的最后一位来设置 nr_cpu_ids(CPU 的数量)。关于此概念的更多细节,您可以阅读描述 概念的章节。让我们看看它的实现:

首先,find_last_bit 函数将给定的 unsigned long 地址分割成:

如果您有任何疑问或建议,欢迎在评论区留言,或通过 与我联系。

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 .

arch/x86/Kernel/setup.c
前文
x86_64
桌面管理界面
PCI
部分
memblock
kernel/printk/printk.c
内核初始化第四部分
初始 RAM 磁盘
内核恐慌
arch/x86/kernel/vsmp_64.c
arch/x86/kernel/io_delay.c
进入保护模式前的最后准备
Documentation/kernel-parameters.txt
arch/x86/kernel/io_delay.c
第六部分
do_early_param
ACPI
直接内存访问(DMA)
drivers/base/dma-contiguous.c
arch/x86/mm/init_64.c
NUMA
arch/x86/mm/init.c
控制寄存器
arch/x86/entry/vsyscall/vsyscall_64.c
vsyscalls
arch/x86/entry/vsyscall/vsyscall_emu_64.S
《固定映射地址与 ioremap》
SMP
arch/x86/kernel/mpparse.c
第六部分
cpumasks 简介
APIC
jiffy
init/main.c
cpumask
init/main.c
TLB
slab
cpumasks
字
Twitter
linux-insides
Desktop Management Interface
x86_64
initrd
Kernel panic
Documentation/kernel-parameters.txt
ACPI
Direct memory access
NUMA
Control register
vsyscalls
SMP
jiffy
Previous part
CONFIG_NR_CPUS