news 2026/10/4 8:56:00

Linux 内核初始化收尾:从 start_kernel 尾声到第一个 init 进程(linux-insides-zh 初始化章节终篇)

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Linux 内核初始化收尾:从 start_kernel 尾声到第一个 init 进程(linux-insides-zh 初始化章节终篇)
  • 文档
  • 教程
  • 操作系统

【免费下载链接】linux-insides-zh

Linux 内核揭秘

项目地址:https://gitcode.com/gh_mirrors/li/linux-insides-zh
点击查看免费下载

本篇文章是《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(); #endif

init_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_MASK

fork_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 内核揭秘

项目地址:https://gitcode.com/gh_mirrors/li/linux-insides-zh
点击查看免费下载

相关推荐

上一篇:NetworkX 2.2 版本解析:统一的随机数治理、GraphView 重构与新算法全览
下一篇:5个核心功能让你轻松掌握stella_vslam视觉SLAM框架

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/4 8:55:21

为什么AI也要算命考试?MingLi-Bench八字命理评测基准深度解析

为什么AI也要算命考试&#xff1f;MingLi-Bench八字命理评测基准深度解析 【免费下载链接】MingLi-Bench A benchmark for evaluating LLMs on Chinese traditional fortune telling — Bazi (八字) and Ziwei Doushu (紫微斗数). 项目地址: https://gitcode.com/gh_mirrors/…

作者头像 李华
网站建设 2026/10/4 8:53:32

架构不是堆层次:判断该不该加一层的实用标准

一次评审会上&#xff0c;年轻同事指着一份设计文档问我&#xff1a;“这个Manager层&#xff0c;是不是有点多余了&#xff1f;我数了一下&#xff0c;一个查询从Controller进来&#xff0c;要经过Service、Manager、Handler&#xff0c;最后才到Mapper&#xff0c;每一层代码…

作者头像 李华
网站建设 2026/10/4 8:47:45

工业数据存储不掉电:PIC18搭配SPI MRAM的实战方案

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/10/4 8:47:00

Python量化回测平台对比:聚宽、米筐、优矿和掘金怎样保存实验

Python量化回测平台可以比较聚宽、米筐、优矿和掘金量化。四款候选都需要代码、数据、参数和输出&#xff0c;但网页项目、本地产品与开发终端的保存方式不同。比较时不直接看收益高低&#xff0c;而是检查三个月后能否用同一份资料重建实验。最小实验包包含环境版本、策略代码…

作者头像 李华
网站建设 2026/10/4 8:44:53

OpenShell:整合PowerShell与WSL的Windows终端增效实战

说实话&#xff0c;我一开始看到“OpenShell”这个名字&#xff0c;以为又是一个 Windows 终端的换肤工具。毕竟这年头&#xff0c;给终端加个背景图、调个透明度&#xff0c;就能自称“生产力神器”的项目太多了。但真正装完、配置好、用了两周之后&#xff0c;我想说&#xf…

作者头像 李华
网站建设 2026/10/4 8:44:25

金融机构接连入驻WorkBuddy,争的不是多一个Skill,是下一个高频入口

自腾讯9月初发布WorkBuddy金融版&#xff0c;面向金融机构推出AI智能工作台后&#xff0c;券商陆续入驻WorkBuddy&#xff0c;角力下一个流量入口。继腾讯发布WorkBuddy金融版后&#xff0c;广发证券、东方财富、兴业证券、中信建投相继入驻WorkBuddy。四家机构分别从对外投研专…

作者头像 李华