第 1-1 课:启动编排:内核如何把自己装配起来

本课环境

  • 源码:D:\Linux\rk3568_linux_sdk\kernel(Linux 5.10.160,RK3568 / ARM64)
  • 主线文件:init/main.c、include/linux/init.h、include/asm-generic/vmlinux.lds.h
  • 实测产物:System.map(本机已构建,767 个内建 initcall)
  • 阅读:约 30--40 分钟 | 实验:10 分钟,不需要板子
  • 配套速查表:启动编排速查表(三阶段 / 级别 / 行号索引 / 实验命令)

为什么第一课讲启动

不是因为它"简单",而是因为启动顺序就是依赖关系图。

你后面遇到的每一个问题都会回到这条装配线:

  • 驱动为什么没 probe?------因为它注册在 level 6,而依赖的 provider 还没就绪;
  • 为什么启动卡在某个位置?------因为最后执行的那个 initcall 就是"凶手";
  • 为什么要加 initcall_debug?------因为它能把这条链每一步的耗时打出来;
  • 为什么 kmalloc 在某个阶段会崩?------因为那时 mm_init() 还没跑。

一句话 :启动顺序不是实现细节,而是内核各构件之间的依赖契约。掌握它,后面的调度、内存、VFS、网络才有位置感。


本文速览

  • 三阶段模型 :start_kernel → rest_init → kernel_init → 用户态,一句话记住
  • 13 个关键调用:每个都回答"如果挪到后面,谁会失去前提"
  • 一个看似奇怪的顺序:为什么必须先建 init、再建 kthreadd(源码注释直接告诉你答案)
  • 九级 initcall:级别是契约,同级顺序是链接顺序、不是 ABI
  • 本机实测:767 条 initcall 的真实分布 + 四个真实驱动的级别追踪
  • 最易混淆点 :initcall ≠ probe,以及为什么日志里全是 deferred probe pending

一、先建立三阶段地图

内核不是"先加载所有驱动,再启动系统"。它先建立一个刚好能运行的最小环境,再让驱动、文件系统、网络逐级挂接上来。整个过程可以压缩成三个阶段:

text 复制代码
        CPU0 启动 CPU(单核、关中断)
              │
   ┌──────────▼────────────────────────────────────────┐
   │ PHASE A   start_kernel()        init/main.c:845   │
   │   setup_arch → mm_init → sched_init → irq → time  │
   │   local_irq_enable()  ←──── 从"顺序 C 程序"变并发  │
   └──────────┬────────────────────────────────────────┘
              │  arch_call_rest_init()   (:1068)
   ┌──────────▼────────────────────────────────────────┐
   │ PHASE B   rest_init()           init/main.c:669   │
   │   fork PID 1  kernel_init    (:680)               │
   │   fork PID 2  kthreadd       (:692)               │
   │   原执行流 ──────────────→ PID 0,进 idle (:714)   │
   └──────────┬────────────────────────────────────────┘
              │  (PID 1 继续跑)
   ┌──────────▼────────────────────────────────────────┐
   │ PHASE C   kernel_init()         init/main.c:1612  │
   │   wait kthreadd_done        (:1699)               │
   │   do_pre_smp_initcalls()    (:1718)  ← early 级    │
   │   smp_init()                (:1721)               │
   │   do_basic_setup()/do_initcalls() (:1518/:1484)   │
   │   free_initmem()            (:1622)               │
   │   execve("/init")  ──────────────→ 用户态          │
   └───────────────────────────────────────────────────┘

💡 一句话记忆

A 建立地基,B 创建工人,C 让工人按图纸装配,最后把钥匙交给用户。

三个阶段各自的"身份"也不同,这点一定要分清:

阶段 谁在执行 CPU 数量 中断 调度器可用吗
A start_kernel() init_task 的内核栈(还不是真正的"进程调度") 1 关 未建立
B rest_init() 原执行流即将变成 PID 0 1 开 刚建立
C kernel_init() PID 1,仍是内核线程 1 → N(smp_init() 之后) 开 可用

最后一行是很多人的认知盲区:此时的 PID 1 还不是用户态 init ,它是个内核线程,要到 kernel_init() 的最后才 execve 用户态的 /init。


二、阶段 A:先建立"能运行"的最小环境

打开 init/main.c 的 start_kernel()(第 845 行),你会看到一长串调用。不要试图背下来------只问一个问题:

如果把它挪到后面,谁会失去前提?

2.1 十三个关键调用(行号已逐条核对本机源码)

# 调用 行号 建立什么 为什么必须在这个位置
1 setup_arch() 867 机器描述、DTB、命令行、内存布局、CPU 特性 还不知道内存从哪开始、CPU 有什么能力,后面的 mm 和调度无从谈起
2 mm_init() 917 memblock → 伙伴系统、页表、slab 没有页分配器,后续任何 kmalloc、栈扩张都会失败
3 sched_init() 929 运行队列、调度类、init_task 没有运行队列,schedule() 无法理解"下一个任务"
4 workqueue_init_early() 947 早期工作队列 后续大量初始化会创建工作项
5 rcu_init() 949 RCU 读侧临界区 后续代码会大量使用 rcu_read_lock()
6 early_irq_init() init_IRQ() 959 960 中断描述符、IRQ domain、GIC 在开中断之前,必须知道"中断来了交给谁"
7 init_timers() hrtimers_init() softirq_init() timekeeping_init() time_init() 963 964 965 966 968 低分辨率/高精度定时器、软中断、墙上时间、时钟源 一旦开中断,tick 和 hrtimer 可能立刻到期,时间子系统必须先就绪
8 local_irq_enable() 986 打开本 CPU 的中断 从此刻起异步事件开始介入;之前只能靠显式初始化推进
9 fork_init() 1041 task 缓存、最大线程数、PID 基础 为 rest_init() 创建 PID 1/2 做准备
10 vfs_caches_init() 1048 dentry / inode 缓存 PID 1 稍后要挂载 rootfs
11 pagecache_init() 1049 页缓存 文件系统读写的前提
12 proc_root_init() 1052 procfs /proc 要可用
13 arch_call_rest_init() 1068 进入阶段 B 地基完成

2.2 两个真正的"开关"

这张表里只有两行是"性质发生变化",其余都是"能力逐项补齐":

text 复制代码
        mm_init()                local_irq_enable()
            │                             │
  ──────────┼─────────────────────────────┼──────────────▶ 时间
            │                             │
   此前:连一块内存都分配不出来      此前:内核只按代码顺序推进
   此后:第一次拥有通用内存分配      此后:第一次暴露在异步事件下
            │                             │
        "在裸机上跑的 C 程序"  ──────▶  "一个并发系统"
  • mm_init():内核第一次拥有通用内存分配能力;
  • local_irq_enable():内核第一次暴露在异步事件之下。

在这两个开关之前,内核更像"一段在裸机上顺序执行的 C 程序";之后才逐渐变成一个并发系统。

⚠️ 不要误解"顺序"

源码顺序表达的是"后者假设前者已经可用 ",不是"前者一定依赖后者"。

例如 rcu_init() 之后仍可能有 RCU 的延迟初始化。读顺序时找的是假设,不是死记编号。


三、阶段 B:从一条执行流到"进程世界"

start_kernel() 的最后调用 arch_call_rest_init()(1068 行),进入 rest_init()(669 行)。这里是内核第一次造进程。

c 复制代码
/* init/main.c:669 */
rcu_scheduler_starting();

/* 先创建 init,确保它拿到 PID 1 */
pid = kernel_thread(kernel_init, NULL, CLONE_FS);              /* :680 */

/* 再创建 kthreadd,所有内核线程的祖先 */
pid = kernel_thread(kthreadd, NULL, CLONE_FS | CLONE_FILES);   /* :692 */

complete(&kthreadd_done);                                      /* :706 */
schedule_preempt_disabled();                                   /* :712 */
cpu_startup_entry(CPUHP_ONLINE);                               /* :714 */

3.1 为什么先建 init,再建 kthreadd?

看源码 676--679 行的注释,这是全内核最"自解释"的一段注释:

"We need to spawn init first so that it obtains pid 1, however the init task will end up wanting to create kthreads, which, if we schedule it before we create kthreadd, will OOPS."
大意:必须先让 init 拿到 PID 1,但它随后会创建内核线程;如果在 kthreadd 存在之前就调度它,会直接 OOPS。

于是就有了这个看似矛盾的顺序:

text 复制代码
① 创建 kernel_init(拿到 PID 1)
        ↓
② 创建 kthreadd(内核线程的祖先)
        ↓
③ complete(&kthreadd_done)   ← 告诉 PID 1:"kthreadd 已就位"
        ↓
④ PID 1 在 kernel_init_freeable() 里 wait_for_completion(&kthreadd_done)
        ↓
⑤ 确认之后才开始创建内核线程、启动从核

这不是随意安排,而是在解决两个约束的冲突:

  • 约束一:PID 1 必须是 init(用户态的祖先是 PID 1,不能是别的);
  • 约束二:所有内核线程必须由 kthreadd 派生。

3.2 原来的执行流去哪了?

c 复制代码
schedule_preempt_disabled();      /* :712  让出 CPU */
cpu_startup_entry(CPUHP_ONLINE);  /* :714  进入空闲循环 */

它变成 PID 0 / idle。从这一刻起,内核有了"可被调度"的多个任务。

3.3 kernel_init_freeable() 在等什么

kernel_init_freeable()(1694 行)是 PID 1 在装配前的准备清单:

调用 行号 作用
wait_for_completion(&kthreadd_done) 1699 确保 kthreadd 已能接收创建内核线程的请求
workqueue_init() 1713 把早期 workqueue 升级为完整工作队列
do_pre_smp_initcalls() 1718 执行 early_initcall:在 SMP 启动前完成关键初始化
smp_init() 1721 启动其他 CPU 核心
sched_init_smp() 1722 建立 SMP 调度域与负载均衡
do_basic_setup() 1733 初始化驱动模型,执行 0--7 级 initcalls

注意 1718 和 1721 的先后:early 级的 initcall 必须跑在从核被拉起来之前。这就是下面要讲的"early 是特例"的根因。


四、阶段 C:initcall 装配线

do_basic_setup()(1518 行)做两件大事:先 driver_init() 建立驱动模型,再 do_initcalls()(1484 行)按级别执行所有内建初始化函数。

4.1 级别从哪里来?

你在驱动里写的 module_init(foo_init),在内建编译 时会展开成 device_initcall(foo_init),最终把函数指针放进 .initcall6.init 段。链接脚本把这些段按级别排好,内核启动时顺序遍历。

text 复制代码
module_init(foo_init)
        │  内建(=y)
        ▼
__initcall(foo_init)
        ▼
device_initcall(foo_init)          ← level 6
        ▼
__define_initcall(fn, 6)
        ▼
段名:.initcall6.init
        ▼
链接脚本按 __initcall0_start ... __initcall7_start 排列
        ▼
do_initcalls() 逐级遍历 → do_one_initcall()

4.2 本机真实分布(来自 System.map 实测)

这是我在你这棵已经构建好的内核上真跑出来的结果,767 条内建 initcall:

text 复制代码
level        count  examples
------------------------------------------------------------------------------
early           24  trace_init_flags_sys_enter, trace_init_flags_sys_exit, cpu_suspend_init
0 pure           5  ipc_ns_init, init_mmap_min_addr, pci_realloc_setup_params
1 core          57  fpsimd_init, tagged_addr_init, enable_mrs_emulation
2 postcore      37  debug_monitors_init, irq_sysfs_init, dma_atomic_pool_init
3 arch          14  reserve_memblock_reserved_regions, aarch32_alloc_vdso_pages, vdso_init
4 subsys       128  topology_init, uid_cache_init, param_sysfs_init
5 fs            48  create_debug_debugfs_entry, em_debug_init, clocksource_done_booting
6 device       400  register_arm64_panic_block, cpuinfo_regs_init, aarch32_el0_sysfs_init
7 late          54  init_oops_id, sched_init_debug, cpu_latency_qos_init
------------------------------------------------------------------------------
total          767

把它画成柱状图,你会立刻看出结构:

text 复制代码
early │ 24
    0 │   5
    1 │  57  ██
    2 │  37  █
    3 │  14
    4 │ 128  ██████
    5 │  48  ██
    6 │ 400  ████████████████████   ← 超过一半
    7 │  54  ██
      └────────────────────────────

观察两点:

  1. level 6 占了 400 个(52%) ------因为绝大多数驱动都用 module_init();
  2. level 3 只有 14 个------架构相关的初始化本来就少。

这张分布本身就是一幅"谁在什么时候报到"的热力图。

4.3 一个容易忽略的细节:同级的"最后三名"

脚本还输出了每一级的最后 3 个条目(真实输出):

text 复制代码
Last 3 entries per level (link order):
  early        arm_enable_runtime_services, efi_earlycon_remap_fb, dummy_timer_register
  0 pure       pci_realloc_setup_params, rockchip_soc_id_init, net_ns_init
  1 core       init_default_flow_dissectors, netlink_proto_init, genl_init
  2 postcore   init_menu, rpmsg_init, rockchip_clk_link_drv_register
  3 arch       dmi_id_init, rockchip_clocks_loader_protect, of_platform_default_populate_init
  4 subsys     rockchip_cpuinfo_init, xz3216_module_init, watchdog_init
  5 fs         rk817_charge_init, populate_rootfs, rockchip_thermal_driver_init
  6 device     sensor_mod_init, sensor_mod_init, sensor_mod_init
  7 late       rkisp1_clr_unready_dev, rkisp_clr_unready_dev, of_platform_sync_state_init

这里藏着两个非常真实的信号:

  • level 6 最后三项全是 sensor_mod_init :因为同一个 sensor_mod_init 函数被多个 sensor 驱动实例引用(宏展开产生了多份函数指针),名字重复完全正常------这也说明不能靠名字判断"哪一个驱动";
  • of_platform_default_populate_init 排在 level 3 的最后:它是"把 DTS 变成 device"的那一步,正好卡在 arch 级的末尾、subsys 级之前------这为后面"驱动在 level 6 才注册"埋下了时间差。

4.4 三条设计规则

规则一:级别是依赖契约。

级别数字越大,可假设的"前置服务"越多。把驱动放早,不会让它更快,只会让它更早暴露在"服务还没准备好"的环境里。

规则二:同一级内部没有稳定顺序。

谁先谁后由链接顺序(Makefile 里对象的排列)决定,不是 ABI。不要通过调整 Makefile 顺序来"修依赖"。

规则三:early 是特例。

它不在 initcall_levels[](init/main.c:1255)数组里,而是由 do_pre_smp_initcalls()(1528 行)

在 __initcall_start 到 __initcall0_start 之间单独遍历,在 smp_init() 之前执行。

实测印证了这一点:

text 复制代码
$ python -I assets/initcall_explorer.py .../System.map --grep rcu_spawn_gp_kthread
  early        tree                 rcu_spawn_gp_kthread

RCU 的宽限期内核线程创建器是 early 级------因为从核一被拉起来就会用到 RCU,而 RCU 的线程必须先在。

⚠️ 模块是另一套规则

对于可加载模块 (.ko),early_initcall / core_initcall / device_initcall 等宏都会退化成 module_init (include/linux/module.h:114--128)。

级别的差异只在"内建进内核镜像"时才有意义;模块是在 insmod 时执行初始化。


五、最容易混淆的点:initcall ≠ probe

很多新手看到"驱动在 level 6 初始化",就以为"probe 也在 level 6 执行"。不是。

initcall 通常只做一件事:把驱动注册到总线。真正的 probe 由驱动核心在"设备与驱动匹配"时调用。

text 复制代码
STEP 1 · level 3 ── 设备先出现
    of_platform_default_populate_init()   ← 实测就在 level 3 的末尾
        解析设备树 → 每个有 compatible 的节点变成一个 platform_device → 挂到总线
        drivers/of/platform.c:517,545

              ↓  (时间是错开的,驱动还没来)

STEP 2 · level 6 ── 驱动来报到
    dw_mci_rockchip_pltfm_driver_init()
        调用 platform_driver_register() → 把驱动挂到总线
        注意:这一步只是"注册",没有 probe

              ↓  总线发现"设备与驱动匹配"

STEP 3 · 匹配之后 ── probe 才执行
    really_probe()  drivers/base/dd.c:497
        → 调用驱动的 probe()
        → 依赖没就绪?返回 -EPROBE_DEFER,设备回待探测队列,稍后重试

所以:

  • initcall 的级别只保证"注册动作"发生在某个阶段,不保证 probe 的完成时间;
  • 两个同为 level 6 的驱动,谁先 probe 取决于设备/驱动注册顺序、总线匹配和依赖解析;
  • 依赖暂时缺失时,内核用 -EPROBE_DEFER 把设备放回待探测队列,而不是靠调整 initcall 级别。

💡 这就是为什么

你会在 RK3568 的启动日志里看到大量 deferred probe pending:

不是驱动写错了,而是它依赖的时钟、稳压器、pinctrl 还没就绪。内核故意允许"先注册、后探测"。


六、动手实验:在 System.map 上验证

不需要板子,也不需要重新编译。脚本是 assets/initcall_explorer.py,只依赖 Python 标准库。

任务 1:看全貌

bash 复制代码
python -I assets/initcall_explorer.py D:/Linux/rk3568_linux_sdk/kernel/System.map --examples 3

输出就是上面 4.2 节那张表(767 条)。先自己预测一下 :你觉得 level 6 会有多少条?

多数人猜 100 左右,实际是 400------这个差距本身就是一次很好的校准。

任务 2:追踪四个真实驱动

bash 复制代码
python -I assets/initcall_explorer.py .../System.map --grep dw_mmc_rockchip
python -I assets/initcall_explorer.py .../System.map --grep rockchip_pinctrl
python -I assets/initcall_explorer.py .../System.map --grep mmc_core
python -I assets/initcall_explorer.py .../System.map --grep regulator_init

本机实测汇总:

组件 实测级别 init 函数 它为什么必须早/晚
regulator_init 1 core regulator_init 稳压器框架;供电是外设工作的前提
rockchip_pinctrl 2 postcore rockchip_pinctrl_drv_register 引脚复用必须在大量设备 probe 前可用
mmc_core 4 subsys mmc_init 提供 mmc_host、总线与核心 API
dw_mmc_rockchip 6 device dw_mci_rockchip_pltfm_driver_init 具体平台驱动,依赖上面三者

还有一个收尾出现在最后一级:

text 复制代码
  1 core       core                 regulator_init
  4 subsys     gpio_regulator       gpio_regulator_init
  4 subsys     tps6586x_regulator   tps6586x_regulator_init
  7 late       core                 regulator_init_complete

regulator_init(level 1)建框架,regulator_init_complete(level 7)在所有设备注册后收尾------

同一个子系统在装配线的两端各有一个钩子,这是 Linux 里非常常见的模式。

⚠️ 注意:这只说明"注册顺序有保障",不代表 probe 一定成功。

如果板上的某个稳压器驱动也是 level 6 且链接在后面,MMC 驱动的 probe 仍可能先跑并返回 -EPROBE_DEFER。

任务 3:一个"破坏性"思想实验

假设有人为了"让 MMC 驱动更早工作",把 module_init 改成了 core_initcall。会发生什么?

先写下你的预测,再看答案。

参考答案

驱动注册动作会提前到 level 1,但此时 MMC core(level 4)、pinctrl(level 2)等可能还没初始化 。

如果注册路径只依赖驱动核心/总线,注册本身可能成功;但设备匹配后的 probe() 会因缺少时钟、稳压器、引脚或 MMC 核心服务而失败,通常返回 -EPROBE_DEFER,设备进入待探测队列。

更糟的是:level 1 仍在单核、部分基础设施未完成的阶段,驱动注册路径一旦调用尚未初始化的 API,可能直接崩溃。

正确做法不是改级别,而是用 -EPROBE_DEFER 表达依赖。

任务 4(可选,需要板子):打开 initcall_debug

在内核命令行加入 initcall_debug,启动后执行:

bash 复制代码
dmesg | grep -i initcall | head -50

你会看到每个 initcall 的调用与耗时。本机 init/main.c:13 定义了 DEBUG,

所以即使 CONFIG_DEBUG_KERNEL 未开启,这个参数也可用。

这一步的价值:把"静态的段分布"变成"动态的时间线"------你就能回答"哪个驱动最慢、为什么慢"。

实验检查点

  • 我能说出 level 6 为什么有 400 个 initcall
  • 我能解释 mmc_core(level 4)与 dw_mmc_rockchip(level 6)的级别差异
  • 我能区分"驱动注册"和"probe 执行"
  • 我能解释为什么同级 initcall 的顺序不可依赖
  • 我能说出 early 级为什么必须跑在 smp_init() 之前

七、检索练习

先不要翻回上文。凭记忆作答。

Q1. initcall 分级最核心的设计意图是什么?

A. 按字母顺序排列初始化函数 | B. 减少内核镜像的静态体积 | C. 表达各子系统间的依赖次序 | D. 让全部驱动并行完成初始化

Q2. 内建驱动的 module_init() 等价于哪一级?

A. core_initcall | B. subsys_initcall | C. late_initcall | D. device_initcall

Q3. 一个 level 6 的 platform driver 注册后,probe 何时执行?

A. 注册时立即同步执行 | B. 匹配到设备后由驱动核心执行 | C. 固定在 late 阶段执行 | D. 由链接顺序决定 probe 时刻

Q4. early_initcall 为什么单独在 do_pre_smp_initcalls() 执行?

A. 在 SMP 启动前完成关键初始化 | B. 为了减少 init 段的内存占用 | C. 为了提前加载所有可加载模块 | D. 为了启动第一个用户态进程

Q5. 同一 initcall 级别内部的执行顺序由什么决定?

A. 函数名的字母顺序 | B. 函数指针的地址大小 | C. 运行时随机打乱 | D. 链接顺序,且非稳定 ABI


答案与解析

Q1 → C。 级别是一个"依赖契约":数字越大,可假设的前置服务越多。它不是排序优化,也不是体积优化。

Q2 → D。 内建时 module_init(x) → __initcall(x) → device_initcall(x)(level 6)。可加载模块则在 insmod 时执行 init_module,级别差异消失。

Q3 → B。 initcall 只负责"注册驱动"。真正的 probe 发生在总线匹配到设备之后;资源未就绪时返回 -EPROBE_DEFER,稍后重试。

Q4 → A。 early_initcall 位于 __initcall_start 到 __initcall0_start 之间,由 do_pre_smp_initcalls()(init/main.c:1528)在 smp_init() 之前执行,用于 RCU、软中断等必须先就绪的基础设施。

Q5 → D。 同一级内部由链接顺序(Makefile 中对象的排列)决定,不是稳定 ABI。需要顺序保证时,应选择正确的级别或用 -EPROBE_DEFER,而不是依赖链接顺序。


八、四个常见误区

误区 真相
"initcall 越早越好。" 越早意味着可假设的服务越少。早注册不会让驱动更快工作,只会更早遇到依赖缺失。
"module_init 一定在启动时执行。" 只有内建 进内核镜像时才在启动时执行;.ko 是在 insmod 时执行。
"level 6 注册完,probe 就完成了。" 注册与探测是两件事。 probe 由驱动核心在匹配后调用,还可能被延迟。
"同级顺序可以依赖。" 同级顺序由链接顺序决定,不是稳定接口。依赖必须用级别或 -EPROBE_DEFER 表达。

九、本课速查

9.1 关键源码索引

位置 内容
init/main.c:845 start_kernel()
init/main.c:669 rest_init()
init/main.c:680 / 692 创建 PID 1 / PID 2
init/main.c:1612 kernel_init()
init/main.c:1694 kernel_init_freeable()
init/main.c:1255 initcall_levels[]
init/main.c:1484 do_initcalls()
init/main.c:1518 do_basic_setup()
init/main.c:1528 do_pre_smp_initcalls()
include/linux/init.h:268 early_initcall
include/linux/init.h:295 __initcall → device_initcall
include/asm-generic/vmlinux.lds.h:964 INIT_CALLS_LEVEL(段排列)
include/linux/module.h:114--128 内建 vs 模块的宏退化
drivers/of/platform.c:517,545 DTS → platform_device
drivers/base/dd.c:497 really_probe()

9.2 术语

术语 含义
initcall 启动时由内核按级别遍历调用的初始化函数指针
early_initcall 在 SMP 启动前由 do_pre_smp_initcalls() 单独执行的一级
PID 0 / 1 / 2 idle / kernel_init / kthreadd
initmem __init 段;initcalls 完成后被释放
probe 设备与驱动匹配后,由驱动核心调用的设备初始化回调
deferred probe probe 返回 -EPROBE_DEFER 后,设备进入待探测队列、稍后重试
platform_device 由 DTS/平台代码描述的、不依赖具体总线的设备
device_initcall level 6;内建 module_init 的落点

结语

启动顺序不是实现细节,而是内核的依赖契约。

你以后遇到的 probe 顺序、-EPROBE_DEFER、initcall_debug,都是这张契约的可见表面。

而这一课最重要的收获不是记住那 13 行调用,而是学会那个提问方式:

"如果把这一行挪到后面,谁会失去前提?"

用这个问题去读任何一段内核初始化代码,你都能自己重建出顺序背后的依赖图。

下一篇:第 02 课《进程与调度:CPU 空出来时,内核选谁?》

把本课的"PID 1/2"展开成一个真正的进程生命周期------task_struct、copy_process()、五个 sched_class、__schedule() 与 CFS 的 vruntime,并回答一个问题:下一个运行的 task,凭什么被选中?


本课所有行号均已在本机 D:\Linux\rk3568_linux_sdk\kernel(Linux 5.10.160)源码上逐条核对;initcall 数量与级别来自本机 System.map 实测(767 条内建 initcall,不含 .ko)。level 5 的范围包含 rootfs_initcall 子级(本机 2 个,例如 populate_rootfs),统计时合并计入。

相关推荐
wacsii7 小时前
【Linux基础开发工具(五)】Git版本控制器从入门到实战:核心操作三板斧与新手避坑指南
linux·运维·服务器
水饺编程7 小时前
第2章:Linux 基础,解压缩【.tar.gz】压缩包
linux·c语言·汇编
luoqice7 小时前
telnet登录linux实时查看xx.log日志
linux
技术小事8 小时前
Surface 装 Ubuntu 双系统
linux·运维·ubuntu
额额额对了8 小时前
【项目实战】从IMX6ULL环境监测到N647 AI视觉加速
linux·开发语言·嵌入式硬件
dingdingfish8 小时前
TLPI 第49章 读书笔记:Memory Mapping
linux·mmap·munmap·tlpi·msync
EVEN-09139 小时前
Linux-进程(2)
linux·运维·服务器
u0109053599 小时前
(永久免费)树莓派 / 香橙派等开发板,安装神卓互联巴比达内网穿透安装教程(Linux ARM 版 V9.4.1)
linux·运维·arm开发
Linux_cy9 小时前
服务器管理方案选型:面板 / SSH 客户端 / 桌面型客户端怎么选
java·linux·运维·服务器·ssh·可视化·gmssh