- 文档
- 教程
- 操作系统
【免费下载链接】linux-insides-zh
Linux 内核揭秘
本篇文章是《Linux 内核揭秘》(linux-insides-zh)中"内核初始化流程"章节(Initialization/README.md)的最终篇,承接 Initialization/linux-initialization-9.md(RCU 初始化)之后,完整讲解start_kernel的收尾工作、rest_init创建内核线程的过程,以及内核如何最终启动第一个用户态init进程。读完本文,你将掌握内核对象缓存(SLAB)的初始化链路、kernel_thread/completion 同步机制、idle进程的角色,以及rdinit=、init=等内核命令行参数对首个用户态程序选择的影响,从而把"内核如何从开机走到运行第一个进程"的整条主线串起来。
start_kernel 的收尾工作:从 espfix 到各类内核缓存
上一部分我们停在acpi_early_init函数处。紧随其后,start_kernel(位于内核源码init/main.c)开始做一系列与体系结构和内存管理对象缓存相关的收尾工作。
防止 esp 寄存器泄漏:init_espfix_bsp
#ifdef CONFIG_X86_ESPFIX64 init_espfix_bsp(); #endifinit_espfix_bsp定义于arch/x86/kernel/espfix_64.c,其职责是防止在返回 16 位栈时泄漏esp寄存器的31:16位。首先,它把espfix页上级目录(PUD)装入内核页目录:
pgd_p = &init_level4_pgt[pgd_index(ESPFIX_BASE_ADDR)]; pgd_populate(&init_mm, pgd_p, (pud_t *)espfix_pud_page);其中ESPFIX_BASE_ADDR的定义如下:
#define PGDIR_SHIFT 39 #define ESPFIX_PGD_ENTRY _AC(-2, UL) #define ESPFIX_BASE_ADDR (ESPFIX_PGD_ENTRY << PGDIR_SHIFT)在Documentation/x86/x86_64/mm中,这一段虚拟地址空间被描述为:
... unused hole ... ffffff0000000000 - ffffff7fffffffff (=39 bits) %esp fixup stacks ... unused hole ...页全局目录填充完espfixPUD 之后,接着调用init_espfix_random(为espfix页生成随机位置)与init_espfix_ap(为当前 CPU 启用espfix)。
内核对象缓存池的建立:thread_info_cache_init 与 cred_init
init_espfix_bsp完成之后,轮到thread_info_cache_init(定义于kernel/fork.c)。它在THREAD_SIZE小于PAGE_SIZE时,为thread_info分配缓存:
# if THREAD_SIZE >= PAGE_SIZE ... void thread_info_cache_init(void) { thread_info_cache = kmem_cache_create("thread_info", THREAD_SIZE, THREAD_SIZE, 0, NULL); BUG_ON(thread_info_cache == NULL); } ... # endif这里PAGE_SIZE为(_AC(1,UL) << PAGE_SHIFT),即 4096 字节;而在 x86_64 上THREAD_SIZE为(PAGE_SIZE << THREAD_SIZE_ORDER),即 16384 字节,所以这段代码在 x86_64 上实际不会编译执行。
下一个函数是cred_init(定义于kernel/cred.c),它为进程凭据(uid、gid等)分配缓存:
void __init cred_init(void) { cred_jar = kmem_cache_create("cred_jar", sizeof(struct cred), 0, SLAB_HWCACHE_ALIGN|SLAB_PANIC, NULL); }关于凭据的更多细节可参考内核源码Documentation/security/credentials.rst。
fork_init:为 task_struct 与信号资源做准备
接下来是fork_init(定义于kernel/fork.c),它为task_struct分配缓存。先看它的开头部分:
#ifndef CONFIG_ARCH_TASK_STRUCT_ALLOCATOR #ifndef ARCH_MIN_TASKALIGN #define ARCH_MIN_TASKALIGN L1_CACHE_BYTES #endif task_struct_cachep = kmem_cache_create("task_struct", sizeof(struct task_struct), ARCH_MIN_TASKALIGN, SLAB_PANIC | SLAB_NOTRACK, NULL); #endif这段代码依赖CONFIG_ARCH_TASK_STRUCT_ALLOCATOR配置项,它表示给定架构是否提供alloc_task_struct函数。由于 x86_64 没有alloc_task_struct,这段代码在 x86_64 上不会被编译执行。
随后fork_init调用arch_task_cache_init完成架构相关的缓存初始化:
void arch_task_cache_init(void) { task_xstate_cachep = kmem_cache_create("task_xstate", xstate_size, __alignof__(union thread_xstate), SLAB_PANIC | SLAB_NOTRACK, NULL); setup_xstate_comp(); }它分配了表示 FPU 状态的task_xstate缓存,并通过setup_xstate_comp设置 xsave 区域中所有扩展状态的大小与偏移。之后用set_max_threads(MAX_THREADS)计算默认的最大线程数:
#define FUTEX_TID_MASK 0x3fffffff #define MAX_THREADS FUTEX_TID_MASKfork_init最后初始化信号处理器相关资源限制:
init_task.signal->rlim[RLIMIT_NPROC].rlim_cur = max_threads/2; init_task.signal->rlim[RLIMIT_NPROC].rlim_max = max_threads/2; init_task.signal->rlim[RLIMIT_SIGPENDING] = init_task.signal->rlim[RLIMIT_NPROC];init_task是task_struct的实例,其signal字段类型为struct signal_struct。前两行设置了资源限制(resource limits)的当前值与最大值,rlim是资源控制限制,由以下结构表示:
struct rlimit { __kernel_ulong_t rlim_cur; __kernel_ulong_t rlim_max; };这里涉及的资源是RLIMIT_NPROC(用户可拥有的最大进程数)与RLIMIT_SIGPENDING(最大挂起信号数)。你可以在自己的系统上用下面的命令验证(详见仓库中 SysCall/linux-syscall-6.md 关于 Linux 资源限制的介绍):
cat /proc/self/limits输出中的 "Max processes" 与 "Max pending signals" 两项即对应上述两个限制。
进程内存与文件系统相关 SLAB 缓存初始化
proc_caches_init:内存描述符等缓存
fork_init之后的proc_caches_init(定义于kernel/fork.c)为内存描述符(mm_struct)等结构分配 SLAB 缓存。开头部分通过kmem_cache_create依次创建:
sighand_cachep—— 管理已安装信号处理器的信息;signal_cachep—— 管理进程信号描述符信息;files_cachep—— 管理已打开文件的信息;fs_cachep—— 管理文件系统信息。
随后为mm_struct分配缓存:
mm_cachep = kmem_cache_create("mm_struct", sizeof(struct mm_struct), ARCH_MIN_MMSTRUCT_ALIGN, SLAB_HWCACHE_ALIGN|SLAB_PANIC|SLAB_NOTRACK, NULL);接着为内核管理虚拟内存空间时使用的vm_area_struct分配缓存。注意这里用的是KMEM_CACHE宏而非kmem_cache_create:
vm_area_cachep = KMEM_CACHE(vm_area_struct, SLAB_PANIC);KMEM_CACHE宏定义于内核源码include/linux/slab.h,展开后就是一次kmem_cache_create调用:
#define KMEM_CACHE(__struct, __flags) kmem_cache_create(#__struct,\ sizeof(struct __struct), __alignof__(struct __struct),\ (__flags), NULL)它与kmem_cache_create的区别在于使用__alignof__运算符,按给定结构体的对齐要求对齐 SLAB,而kmem_cache_create使用传入的数值。此后还调用mmap_init(初始化虚拟内存区域 SLAB)和nsproxy_cache_init(为命名空间分配 SLAB)。
buffer_init:buffer_head 与脏页比例
下一个函数是buffer_init(定义于fs/buffer.c),为管理缓冲区的struct buffer_head(定义于include/linux/buffer_head.h)分配缓存,并计算内存中缓冲区的最大数量:
nrpages = (nr_free_buffer_pages() * 10) / 100; max_buffer_heads = nrpages * (PAGE_SIZE / sizeof(struct buffer_head));这个值相当于 x86_64 上ZONE_NORMAL(4GB 以上全部 RAM)的 10%。
接下来是vfs_caches_init,为各种 VFS(虚拟文件系统)缓存分配 SLAB 缓存和哈希表。第 8 部分(Initialization/linux-initialization-8.md)中见过的vfs_caches_init_early已初始化了dcache(目录缓存)与 inode 缓存,而vfs_caches_init则进行 dcache、inode 缓存、私有数据缓存、挂载点哈希表等的后期初始化。
随后是signals_init(定义于kernel/signal.c),为表示实时信号队列的sigqueue结构分配缓存;再往后是page_writeback_init,初始化脏页(dirty pages)的比率。每个底层页条目都包含dirty位,用于指示该页在装入内存后是否被写回。
proc_root_init:创建 /proc 文件系统根
完成上述缓存准备后,需要为 proc 文件系统创建根节点,这一步由proc_root_init(定义于fs/proc/root.c)完成。函数开头先为 inode 分配缓存,并向系统注册新的文件系统:
err = register_filesystem(&proc_fs_type); if (err) return;注册成功后调用proc_self_init(定义于fs/proc/self.c),为self目录分配 inode 编号(/proc/self指向访问/proc文件系统的进程)。接着是proc_setup_thread_self,设置包含当前线程信息的/proc/thread-self目录,再通过以下调用创建/proc/self/mounts符号链接:
proc_symlink("mounts", NULL, "self/mounts");随后根据不同的内核配置选项创建一系列目录:
#ifdef CONFIG_SYSVIPC proc_mkdir("sysvipc", NULL); #endif proc_mkdir("fs", NULL); proc_mkdir("driver", NULL); proc_mkdir("fs/nfsd", NULL); #if defined(CONFIG_SUN_OPENPROMFS) || defined(CONFIG_SUN_OPENPROMFS_MODULE) proc_mkdir("openprom", NULL); #endif proc_mkdir("bus", NULL); ... if (!proc_mkdir("tty", NULL)) return; proc_mkdir("tty/ldisc", NULL); ...proc_root_init最后调用proc_sys_init,创建/proc/sys目录并初始化 Sysctl 子系统。
到这里start_kernel主体结束。原文档也坦率说明,start_kernel中还调用了taskstats_init_early(向用户空间导出每任务统计)、delayacct_init(初始化每任务延迟记账)、key_init与security_init(安全相关)、check_bugs(修复架构相关 bug)、ftrace_init(初始化 ftrace)、cgroup_init(初始化 cgroup 子系统剩余部分)等函数,它们依赖不同内核配置、属于不同主题,会在后续章节中分别展开(例如 Concepts/linux-cpu-3.md 讲述的 initcall 机制、Cgroups/README.md 讲述的 cgroup 等)。
start_kernel 之后的第一件事:rest_init
走出start_kernel后,最后的调用是rest_init(同样定义于init/main.c)。它的开头是两个函数:
rcu_scheduler_starting(); smpboot_thread_init();rcu_scheduler_starting激活 RCU 调度器;smpboot_thread_init注册smpboot_thread_notifierCPU 通知器(详见内核源码Documentation/cpu-hotplug.txt)。紧接着创建两个内核线程:
kernel_thread(kernel_init, NULL, CLONE_FS); pid = kernel_thread(kthreadd, NULL, CLONE_FS | CLONE_FILES);kernel_thread(定义于kernel/fork.c)接收三个参数:新线程要执行的函数、传给该函数的参数、以及标志位。从实现上看,kernel_thread最终会调用clone系统调用。通过CLONE_FS标志,父子线程共享文件系统相关信息;两个调用分别创建了PID = 1的init进程与PID = 2的kthreadd。内核线程与用户线程的区别在于它运行在内核态。
kthreadd是一个特殊的内核线程,专门负责帮助内核的各个部分创建其他内核线程。可以用ps命令观察到它:
$ ps -ef | grep kthreadd root 2 0 0 Jan11 ? 00:00:00 [kthreadd]用 completion 保证 kthreadd 就绪
创建完两个内核线程后,rest_init继续执行:
rcu_read_lock(); kthreadd_task = find_task_by_pid_ns(pid, &init_pid_ns); rcu_read_unlock();rcu_read_lock/rcu_read_unlock标记一个 RCU 读侧临界区的起止,用来保护find_task_by_pid_ns——该函数按给定 pid 返回task_struct指针。这里取到的是刚才创建kthreadd时返回的PID = 2对应的task_struct。随后调用:
complete(&kthreadd_done);kthreadd_done的定义如下:
static __initdata DECLARE_COMPLETION(kthreadd_done);DECLARE_COMPLETION宏展开为对completion结构(定义于内核源码include/linux/completion.h)的定义。completion 是一种代码同步机制,为"必须等待某个进程达到某个点或特定状态"的线程提供无竞争(race-free)的解决方案。使用 completion 分三步:第一步定义completion结构(即上面的DECLARE_COMPLETION);第二步调用wait_for_completion,调用它的线程将挂起等待其他线程调用complete;第三步调用complete唤醒等待者。kernel_init_freeable开头就有对应的等待:
wait_for_completion(&kthreadd_done);也就是说,kernel_init_freeable要等kthreadd线程被设置好之后才会继续执行。
引导 CPU 成为 idle 进程
kthreadd就绪后,rest_init中还有三个关键调用:
init_idle_bootup_task(current); schedule_preempt_disabled(); cpu_startup_entry(CPUHP_ONLINE);init_idle_bootup_task(定义于kernel/sched/core.c)为当前进程设置调度类——此时是idle类:
void init_idle_bootup_task(struct task_struct *idle) { idle->sched_class = &idle_sched_class; }idle调度类优先级很低,只有当处理器无事可做时才会运行这类任务。schedule_preempt_disabled在idle任务中禁用抢占。第三个函数cpu_startup_entry(定义于kernel/sched/idle.c)调用cpu_idle_loop——它以PID = 0在后台工作,主要目的是消耗空闲的 CPU 周期:当没有进程可运行时,它便开始工作,不断检查是否有活跃任务可以切换:
static void cpu_idle_loop(void) { ... while (1) { while (!need_resched()) { ... } ... } }至此,start_kernel通过rest_init派生了init进程(kernel_init函数),而它自己则变成了idle进程。
第一个用户态进程的诞生:kernel_init
kernel_init_freeable:完成剩余的系统准备
kernel_init的执行从kernel_init_freeable开始,它首先要等kthreadd完成设置:
wait_for_completion(&kthreadd_done);之后它依次完成:把gfp_allowed_mask设为__GFP_BITS_MASK(表示系统已开始运行);用set_mems_allowed把所有 CPU 和 NUMA 节点设置为允许集合;用set_cpus_allowed_ptr允许init进程在任何 CPU 上运行;为cad(即 Ctrl-Alt-Delete)设置 pid;调用smp_prepare_cpus准备引导其他 CPU;调用do_pre_smp_initcalls执行早期的 initcall;调用smp_init初始化 SMP;调用lockup_detector_init初始化 lockup 检测器;调用sched_init_smp初始化调度器。
do_basic_setup:开始"真正的工作"
随后调用do_basic_setup。在此之前内核已经完成了基本初始化,正如内核源码注释所说:
Now we can finally start doing some real work..do_basic_setup会:把 cpuset 重新初始化为活跃 CPU;初始化khelper(用于从内核向用户空间发出调用的内核线程);初始化 tmpfs;初始化驱动子系统;启用用户态辅助 workqueue;并对各等级 initcall 做后期调用。关于 initcall 的完整机制——八个等级(early、core、postcore、arch、subsys、fs、device、late)、__define_initcall宏的段布局、do_initcalls/do_one_initcall的执行与黑名单过滤——可阅读仓库中 Concepts/linux-cpu-3.md 的详细讲解。
do_basic_setup之后,打开初始控制台并把文件描述符 0 到 2 复制两次:
if (sys_open((const char __user *) "/dev/console", O_RDWR, 0) < 0) pr_err("Warning: unable to open an initial console.\n"); (void) sys_dup(0); (void) sys_dup(0);这里用到sys_open与sys_dup两个系统调用。之后检查rdinit=内核命令行参数,若未传入则设置默认的 ramdisk 路径:
if (!ramdisk_execute_command) ramdisk_execute_command = "/init";再检查用户对 ramdisk 的权限,并调用prepare_namespace(定义于init/do_mounts.c)检查并挂载 initrd:
if (sys_access((const char __user *) ramdisk_execute_command, 0) != 0) { ramdisk_execute_command = NULL; prepare_namespace(); }释放初始化内存并进入 SYSTEM_RUNNING
kernel_init_freeable至此结束,控制权交回kernel_init。接下来调用async_synchronize_full等待所有异步函数调用完成,再调用free_initmem释放位于__init_begin与__init_end之间的初始化代码所占用的全部内存。随后用mark_rodata_ro保护.rodata段,并把系统状态从SYSTEM_BOOTING更新为:
system_state = SYSTEM_RUNNING;然后尝试运行init进程:
if (ramdisk_execute_command) { ret = run_init_process(ramdisk_execute_command); if (!ret) return 0; pr_err("Failed to execute %s (error %d)\n", ramdisk_execute_command, ret); }ramdisk_execute_command是在kernel_init_freeable中设置的,其值为rdinit=命令行参数或默认的/init。run_init_process填充argv_init数组的第一个元素:
static const char *argv_init[MAX_INIT_ARGS+2] = { "init", NULL, };然后调用do_execve(定义于内核源码include/linux/sched.h)执行指定文件名和参数的程序:
argv_init[0] = init_filename; return do_execve(getname_kernel(init_filename), (const char __user *const __user *)argv_init, (const char __user *const __user *)envp_init);如果未传入rdinit=,内核接着检查execute_command(即init=内核命令行参数的值):
if (execute_command) { ret = run_init_process(execute_command); if (!ret) return 0; panic("Requested init %s failed (error %d).", execute_command, ret); }若init=也未传入,内核依次尝试下列可执行文件:
if (!try_to_run_init_process("/sbin/init") || !try_to_run_init_process("/etc/init") || !try_to_run_init_process("/bin/init") || !try_to_run_init_process("/bin/sh")) return 0;全部失败则以内核 panic 告终:
panic("No working init found. Try passing init= option to kernel. " "See Linux Documentation/init.txt for guidance.");下图展示了一个真实的启动命令行示例,其中通过rdinit=/sbin/init显式指定了第一个用户态进程(来自仓库 Initialization/images/kernel_command_line.png),直观印证了本文所述"内核按命令行参数依次寻找并执行 init 程序"的完整链路:
小结
至此,Linux 内核初始化流程走完了最后一段:start_kernel依次完成 espfix、thread_info_cache、cred、task_struct、mm_struct、vm_area_struct、buffer_head、proc文件系统根等初始化工作,随后进入rest_init,创建kernel_init(PID 1)与kthreadd(PID 2)两个内核线程,并借助 completion 机制确保kthreadd就绪,引导 CPU 自身则成为idle进程。最后kernel_init通过kernel_init_freeable完成剩余系统准备与do_basic_setup,释放初始化内存、进入SYSTEM_RUNNING,并按照rdinit=、init=参数及/sbin/init等默认路径的优先级启动第一个用户态进程。
正如原文档所说,这一章从第一个与架构无关的函数start_kernel开始,一直讲到系统中第一个init进程的启动。调度器、中断、异常处理等子系统在此过程中被有意跳过,它们会在本仓库的后续章节(如 Interrupts/README.md、SyncPrim/README.md、Timers/README.md 等)中逐一深入展开。
- 文档
- 教程
- 操作系统
【免费下载链接】linux-insides-zh
Linux 内核揭秘
相关推荐
Linux 内核初始化(十):start_kernel 收尾与第一个 init 进程的启动全流程解析
Linux 内核初始化(十):start_kernel 收尾与第一个 init 进程的启动全流程解析 导读 本文是 linux insides 仓库 Kerne
文档教程操作系统Linux 内核初始化(七):setup_arch 收尾、initrd 重定位与 start_kernel 通用初始化
Linux 内核初始化(七):setup_arch 收尾、initrd 重定位与 start_kernel 通用初始化 导读 本文是「Linux 内核初始化」系
Linux 内核揭秘:初始化第七部分 —— `setup_arch` 架构相关初始化尾声与 `start_kernel` 回归
Linux 内核揭秘:初始化第七部分 —— setup_arch 架构相关初始化尾声与 start_kernel 回归 导读 本文是《Linux 内核揭秘》「内
文档教程操作系统
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考