)
文档教程操作系统【免费下载链接】linux-insides-zhLinux 内核揭秘项目地址https://gitcode.com/gh_mirrors/li/linux-insides-zh点击查看免费下载本篇文章是《Linux 内核揭秘》linux-insides-zh中内核初始化流程章节Initialization/README.md的最终篇承接 Initialization/linux-initialization-9.mdRCU 初始化之后完整讲解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_initinit_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_createvm_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_initbuffer_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_NORMAL4GB 以上全部 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_initkernel_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检查并挂载 initrdif (sys_access((const char __user *) ramdisk_execute_command, 0) ! 0) { ramdisk_execute_command NULL; prepare_namespace(); }释放初始化内存并进入 SYSTEM_RUNNINGkernel_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_ARGS2] { 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_initPID 1与kthreaddPID 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-zhLinux 内核揭秘项目地址https://gitcode.com/gh_mirrors/li/linux-insides-zh点击查看免费下载相关推荐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 内核揭秘》「内文档教程操作系统上一篇NetworkX 2.2 版本解析统一的随机数治理、GraphView 重构与新算法全览下一篇5个核心功能让你轻松掌握stella_vslam视觉SLAM框架创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考