读懂 Linux 内核:八课精华,从一块 RK3568 板子看懂整个内核设计
学内核最怕两件事:一是对着教科书背概念,背完还是不知道板子上为什么是这样;二是一头扎进源码,看了三天
start_kernel,还是画不出一张完整的图。这篇文章走第三条路------拿你手里那块能烧录、能串口登录的 RK3568 当样本,从"一块网卡怎么活起来"这个具体问题切入,顺着它追配置、追启动、追内存、追调度、追中断、追驱动、追调试。
八课走完,你会发现:内核不是一堆零散的子系统,而是一套层层接力、处处取舍的设计哲学。
全景图:八课主线一张表
先把整个框架立起来,后面每一章都是对这张表的展开:
| 层次 | 核心问题 | 关键机制 | 板上验证入口 |
|---|---|---|---|
| 配置与绑定 | 一套源码怎么服务千差万别的板卡? | defconfig → Kconfig → DTS → compatible → probe | /sys/bus/platform/ |
| 启动流程 | 从裸硬件到完整系统,一步到位为什么不行? | U-Boot 交棒 → head.S → start_kernel → kernel_init → PID 1 | dmesg、/proc/cmdline |
| 内存管理 | 物理页怎么分配?连续性与利用率怎么平衡? | memblock → buddy → CMA → slab/vmalloc | /proc/buddyinfo、/sys/kernel/slab/ |
| 调度与频率 | CPU 只有几个,任务那么多,谁先跑?跑多快? | CFS vruntime + 红黑树 → schedutil → DVFS | ftrace sched_switch、scaling_cur_freq |
| 中断与并发 | 硬件来了信号,内核怎么快速响应又不被拖死? | GIC → hardirq → softirq / threaded IRQ / workqueue | /proc/interrupts、/proc/softirqs |
| 驱动模型 | device 和 driver 怎么相遇?失败了怎么办? | bus 匹配 → probe → devres → deferred → runtime PM | /sys/bus/.../uevent、devices_deferred |
| 调试观测 | 出问题了,用什么工具看? | 先体检配置 → ftrace / debugfs / kprobe / proc | /proc/config.gz、/sys/kernel/debug/tracing/ |
一、配置与绑定:内核为什么能跑在千差万别的板子上
很多人以为内核编译就是 make defconfig && make,然后镜像烧进去就能跑。但 Rockchip 的 SDK 告诉你:从源码到板上运行,要经过三层配置。
#mermaid-svg-2chiu3uzJc3scpJY{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-2chiu3uzJc3scpJY .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-2chiu3uzJc3scpJY .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-2chiu3uzJc3scpJY .error-icon{fill:#552222;}#mermaid-svg-2chiu3uzJc3scpJY .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-2chiu3uzJc3scpJY .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-2chiu3uzJc3scpJY .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-2chiu3uzJc3scpJY .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-2chiu3uzJc3scpJY .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-2chiu3uzJc3scpJY .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-2chiu3uzJc3scpJY .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-2chiu3uzJc3scpJY .marker{fill:#333333;stroke:#333333;}#mermaid-svg-2chiu3uzJc3scpJY .marker.cross{stroke:#333333;}#mermaid-svg-2chiu3uzJc3scpJY svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-2chiu3uzJc3scpJY p{margin:0;}#mermaid-svg-2chiu3uzJc3scpJY .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-2chiu3uzJc3scpJY .cluster-label text{fill:#333;}#mermaid-svg-2chiu3uzJc3scpJY .cluster-label span{color:#333;}#mermaid-svg-2chiu3uzJc3scpJY .cluster-label span p{background-color:transparent;}#mermaid-svg-2chiu3uzJc3scpJY .label text,#mermaid-svg-2chiu3uzJc3scpJY span{fill:#333;color:#333;}#mermaid-svg-2chiu3uzJc3scpJY .node rect,#mermaid-svg-2chiu3uzJc3scpJY .node circle,#mermaid-svg-2chiu3uzJc3scpJY .node ellipse,#mermaid-svg-2chiu3uzJc3scpJY .node polygon,#mermaid-svg-2chiu3uzJc3scpJY .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-2chiu3uzJc3scpJY .rough-node .label text,#mermaid-svg-2chiu3uzJc3scpJY .node .label text,#mermaid-svg-2chiu3uzJc3scpJY .image-shape .label,#mermaid-svg-2chiu3uzJc3scpJY .icon-shape .label{text-anchor:middle;}#mermaid-svg-2chiu3uzJc3scpJY .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-2chiu3uzJc3scpJY .rough-node .label,#mermaid-svg-2chiu3uzJc3scpJY .node .label,#mermaid-svg-2chiu3uzJc3scpJY .image-shape .label,#mermaid-svg-2chiu3uzJc3scpJY .icon-shape .label{text-align:center;}#mermaid-svg-2chiu3uzJc3scpJY .node.clickable{cursor:pointer;}#mermaid-svg-2chiu3uzJc3scpJY .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-2chiu3uzJc3scpJY .arrowheadPath{fill:#333333;}#mermaid-svg-2chiu3uzJc3scpJY .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-2chiu3uzJc3scpJY .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-2chiu3uzJc3scpJY .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-2chiu3uzJc3scpJY .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-2chiu3uzJc3scpJY .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-2chiu3uzJc3scpJY .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-2chiu3uzJc3scpJY .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-2chiu3uzJc3scpJY .cluster text{fill:#333;}#mermaid-svg-2chiu3uzJc3scpJY .cluster span{color:#333;}#mermaid-svg-2chiu3uzJc3scpJY div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-2chiu3uzJc3scpJY .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-2chiu3uzJc3scpJY rect.text{fill:none;stroke-width:0;}#mermaid-svg-2chiu3uzJc3scpJY .icon-shape,#mermaid-svg-2chiu3uzJc3scpJY .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-2chiu3uzJc3scpJY .icon-shape p,#mermaid-svg-2chiu3uzJc3scpJY .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-2chiu3uzJc3scpJY .icon-shape .label rect,#mermaid-svg-2chiu3uzJc3scpJY .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-2chiu3uzJc3scpJY .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-2chiu3uzJc3scpJY .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-2chiu3uzJc3scpJY :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 选择DTS+fragment
.config → autoconf.h → vmlinux
compatible 匹配
① 板级清单
SDK层
回答: 构建哪块板?
② 内核配置
代码层
回答: 编进哪些代码?
③ 硬件描述
设备树层
回答: 运行哪些设备?
驱动 probe
设备被认领
三层配置,各管各的事
| 层 | 回答的问题 | 典型文件 | 改了会怎样 |
|---|---|---|---|
| 板级清单 | 这块板怎么构建? | alientek_rk3568_defconfig |
换 DTS、换配置 fragment |
| 内核配置 | 编进哪些代码? | .config → autoconf.h |
加/减驱动、开关子系统 |
| 设备树 | 运行哪些设备? | rk3568.dtsi + 板级 .dts |
启用/禁用外设、改引脚地址 |
一句话记住:内核镜像决定"有哪些代码",设备树决定"这些代码为谁工作"。
跟着 GMAC0 网卡走一遍
打开 rk3568.dtsi,你会看到 GMAC0 的 status = "disabled"------因为这颗 SoC 的 dtsi 描述"芯片具备什么",默认不启用。
板级 DTS 用标签 &gmac0 覆盖,只写"这块板接了什么":
dts
/* SoC dtsi: 能力描述 */
gmac0: ethernet@fe2a0000 {
compatible = "rockchip,rk3568-gmac", "snps,dwmac-4.20a";
reg = <0x0 0xfe2a0000 0x0 0x10000>;
status = "disabled";
};
/* 板级 DTS: 策略覆盖 */
&gmac0 {
phy-mode = "rgmii";
reset-gpio = <&gpio2 RK_PD3 GPIO_ACTIVE_LOW>;
status = "okay"; /* 这块板启用它 */
};
然后呢?设备树被加载后,OF 核心把节点变成 platform_device;platform 总线拿设备侧的 compatible 列表,逐项比对驱动侧的 of_match_table;匹配成功后,driver core 调用 probe。
这就是"被认领"的全过程。
💡 设计洞察:两层驱动
Rockchip 的
dwmac-rk.c是 glue 层 ------只处理这家 SoC 特有的时钟、GRF、电源;Synopsys 的
stmmac是 core 层------处理通用 MAC 逻辑。
compatible的两个字符串正好对应这两层。BSP 的差异通常在 glue 层,通用逻辑来自上游 core。
⚠️ 一个坑:磁盘 .config 不是运行内核的事实
本仓库里 kernel/.config 的修改时间比 autoconf.h、vmlinux 都新;而且 MFD_RK808 在 .config 里是 not set,在 autoconf.h 里却是 1。
判断运行内核带什么,永远看板上的 zcat /proc/config.gz。
二、启动流程:从 U-Boot 交棒到第一个用户进程
上一章我们追的是"设备怎么被认领",但这个动作发生在内核启动的中后期。那最开始呢?U-Boot 跳过来的那一瞬间,到底发生了什么?
arm64 启动协议:一页合同,四个寄存器
U-Boot 只做三件事:把 Image 与 DTB 放进内存,把 DTB 地址放进 x0,跳过去。
c
/* Documentation/arm64/booting.rst */
x0 = physical address of device tree blob (dtb)
x1 = 0 x2 = 0 x3 = 0 // 必须清零
/* The MMU must be off. */
为什么用寄存器传参?因为进入内核的第一刻,栈、堆、MMU 都还不可靠,C 运行时也不存在。寄存器是唯一确定可用的通道。
三段式:内核怎样"长大"
#mermaid-svg-ncgtViapkQEapkn0{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-ncgtViapkQEapkn0 .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-ncgtViapkQEapkn0 .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-ncgtViapkQEapkn0 .error-icon{fill:#552222;}#mermaid-svg-ncgtViapkQEapkn0 .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-ncgtViapkQEapkn0 .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-ncgtViapkQEapkn0 .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-ncgtViapkQEapkn0 .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-ncgtViapkQEapkn0 .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-ncgtViapkQEapkn0 .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-ncgtViapkQEapkn0 .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-ncgtViapkQEapkn0 .marker{fill:#333333;stroke:#333333;}#mermaid-svg-ncgtViapkQEapkn0 .marker.cross{stroke:#333333;}#mermaid-svg-ncgtViapkQEapkn0 svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-ncgtViapkQEapkn0 p{margin:0;}#mermaid-svg-ncgtViapkQEapkn0 .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-ncgtViapkQEapkn0 .cluster-label text{fill:#333;}#mermaid-svg-ncgtViapkQEapkn0 .cluster-label span{color:#333;}#mermaid-svg-ncgtViapkQEapkn0 .cluster-label span p{background-color:transparent;}#mermaid-svg-ncgtViapkQEapkn0 .label text,#mermaid-svg-ncgtViapkQEapkn0 span{fill:#333;color:#333;}#mermaid-svg-ncgtViapkQEapkn0 .node rect,#mermaid-svg-ncgtViapkQEapkn0 .node circle,#mermaid-svg-ncgtViapkQEapkn0 .node ellipse,#mermaid-svg-ncgtViapkQEapkn0 .node polygon,#mermaid-svg-ncgtViapkQEapkn0 .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-ncgtViapkQEapkn0 .rough-node .label text,#mermaid-svg-ncgtViapkQEapkn0 .node .label text,#mermaid-svg-ncgtViapkQEapkn0 .image-shape .label,#mermaid-svg-ncgtViapkQEapkn0 .icon-shape .label{text-anchor:middle;}#mermaid-svg-ncgtViapkQEapkn0 .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-ncgtViapkQEapkn0 .rough-node .label,#mermaid-svg-ncgtViapkQEapkn0 .node .label,#mermaid-svg-ncgtViapkQEapkn0 .image-shape .label,#mermaid-svg-ncgtViapkQEapkn0 .icon-shape .label{text-align:center;}#mermaid-svg-ncgtViapkQEapkn0 .node.clickable{cursor:pointer;}#mermaid-svg-ncgtViapkQEapkn0 .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-ncgtViapkQEapkn0 .arrowheadPath{fill:#333333;}#mermaid-svg-ncgtViapkQEapkn0 .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-ncgtViapkQEapkn0 .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-ncgtViapkQEapkn0 .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-ncgtViapkQEapkn0 .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-ncgtViapkQEapkn0 .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-ncgtViapkQEapkn0 .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-ncgtViapkQEapkn0 .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-ncgtViapkQEapkn0 .cluster text{fill:#333;}#mermaid-svg-ncgtViapkQEapkn0 .cluster span{color:#333;}#mermaid-svg-ncgtViapkQEapkn0 div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-ncgtViapkQEapkn0 .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-ncgtViapkQEapkn0 rect.text{fill:none;stroke-width:0;}#mermaid-svg-ncgtViapkQEapkn0 .icon-shape,#mermaid-svg-ncgtViapkQEapkn0 .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-ncgtViapkQEapkn0 .icon-shape p,#mermaid-svg-ncgtViapkQEapkn0 .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-ncgtViapkQEapkn0 .icon-shape .label rect,#mermaid-svg-ncgtViapkQEapkn0 .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-ncgtViapkQEapkn0 .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-ncgtViapkQEapkn0 .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-ncgtViapkQEapkn0 :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} b start_kernel
arch_call_rest_init
C 世界搭建阶段 · kernel_init
创建驱动与sysfs
执行initcall
挂载根文件系统
exec 用户态init
B 核心内核阶段 · start_kernel
解析DTB与bootargs
初始化内存
初始化调度
初始化中断与时间
A 汇编阶段 · head.S
保存启动参数
建页表
打开MMU
映射DTB
start_kernel 把机器变成内核,kernel_init 把内核变成系统。
六段链路速查表
| # | 函数 | 位置 | 它建立了什么 |
|---|---|---|---|
| 1 | preserve_boot_args |
head.S:125 |
保存 x0--x3,MMU 仍关闭 |
| 2 | __primary_switched |
head.S:417 |
清 BSS、映射 DTB、进入 C |
| 3 | start_kernel |
main.c:845 |
打印 banner、解析 DTB、初始化 mm/sched/irq |
| 4 | rest_init |
main.c:669 |
创建 PID 1 kernel_init 与 PID 2 kthreadd |
| 5 | kernel_init |
main.c:1612 |
driver_init + do_initcalls,驱动登场 |
| 6 | run_init_process |
main.c:1537 |
挂载根文件系统,exec /sbin/init |
initcall 八级秩序表
驱动什么时候注册?设备什么时候创建?全靠 initcall 的八个等级:
| 等级 | 名字 | 典型内容 |
|---|---|---|
| 0--1 | pure / core | 最早的核心设施 |
| 2--3 | postcore / arch | 架构相关;设备树设备在这里被创建 |
| 4--5 | subsys / fs | 子系统、文件系统 |
| 6 | device | 绝大多数平台驱动在这里注册并 probe |
| 7 | late | 最后收尾 |
所以顺序是:先有 device(等级 3),再有 driver(等级 6),匹配成功后才 probe。
🎯 思考题:GMAC 的 probe 日志,出现在 "Run /sbin/init" 之前还是之后?
答案:之前 。因为
do_initcalls()在run_init_process()之前执行。反例:如果它是
=m模块,probe 会等到用户态 modprobe 才发生,日志就跑到 init 之后。同一份源码,y 与 m 改变的不只是"在不在内核里",还有"什么时候初始化"。
三、内存管理:memblock、buddy、slab 的三层接力
启动日志里有一行:
Memory: 2000000K/2048000K available (... 32768K reserved, 16384K cma-reserved)
很多人扫一眼就过去了。但这一行记录了内核在那一刻怎样看内存。
为什么需要三个分配器?
直觉上,内核一上电就该用 buddy 分配页。但 buddy 需要 struct page、zone、free_area 来描述每一页------这些元数据本身也要占内存。
渲染错误: Mermaid 渲染失败: Parse error on line 2: ...mblock · 启动期
一张"可用区间+保留区间"的表
不需要 -----------------------^ Expecting 'SQE', 'DOUBLECIRCLEEND', 'PE', '-)', 'STADIUMEND', 'SUBROUTINEEND', 'PIPE', 'CYLINDEREND', 'DIAMOND_STOP', 'TAGEND', 'TRAPEND', 'INVTRAPEND', 'UNICODE_TEXT', 'TEXT', 'TAGSTART', got 'STR'
💡 设计洞察:用简单启动复杂
描述内存的元数据,本身也需要内存。所以内核分成两段------memblock 只维护区间,先把页表和元数据建起来;buddy 就绪后接管,memblock 功成身退。
理解这个 "bootstrap" 模式,就能理解为什么很多子系统都有"早期版本 + 正式版本"两套接口。
一个"意外":kmalloc(64) 进的不是 kmalloc-64
教材会说:64 字节 → kmalloc-64。在这块 arm64 板上,你会看到它进 kmalloc-128。
为什么?因为 ARCH_DMA_MINALIGN = 128------arm64 要求 kmalloc 返回的缓冲区按 128 字节对齐,否则 DMA 可能读到缓存里的旧数据。
c
/* arch/arm64/include/asm/cache.h:51 */
#define ARCH_DMA_MINALIGN (128)
/* include/linux/slab.h:206 */
#define KMALLOC_MIN_SIZE ARCH_DMA_MINALIGN // 128
#define KMALLOC_SHIFT_LOW ilog2(ARCH_DMA_MINALIGN) // 7
于是启动时 setup_kmalloc_cache_index_table() 把所有小于 128 字节的请求都重定向到索引 7,也就是 kmalloc-128。
| 请求大小(本板实际) | 落入的缓存 |
|---|---|
| 1 -- 128 B | kmalloc-128 |
| 129 -- 256 B | kmalloc-256 |
| 257 -- 512 B | kmalloc-512 |
| 513 B -- 8 KiB | kmalloc-1k ... kmalloc-8k |
| > 8 KiB | kmalloc_large → 直接找 buddy |
代价是内部碎片:kmalloc(64) 实际占用一个 128 字节对象,浪费 50%。换来的是 DMA 安全性和更简单的对齐规则。
CMA:预留连续性,但不让它闲着
这块板的 CMA 是 16 MiB。它的设计非常巧妙------两次动作:
| 阶段 | 时机 | 做什么 |
|---|---|---|
| 预留 | arm64_memblock_init |
把这段标成 memblock reserved,buddy 拿不到它 |
| 激活 | core_initcall(cma_init_reserved_areas) |
把这些页标成 MIGRATE_CMA 并释放进 buddy |
激活后,CMA 页在 buddy 里是"可移动"的。带 __GFP_CMA 的可移动分配可以借用它;设备要连续内存时,再把借出去的页迁移走。
💡 面试可以这样说 :
"CMA 不是永久保留地,而是一块可被可移动内存临时借用的连续区;它的成本从'浪费'转移到了'迁移'。"
四、调度与频率:谁先跑,跑多快
CPU 只有 4 个,任务却有很多。CFS 用"虚拟运行时间"决定下一个跑谁;schedutil 用同一个调度器的负载信号决定 CPU 跑多快。
CFS 的三条规则
① vruntime:按权重"花虚拟时间"
每个 tick,当前任务的实际运行时间会折算成虚拟时间,累加到 vruntime:
c
/* kernel/sched/fair.c:862 */
curr->vruntime += calc_delta_fair(delta_exec, curr);
// 概念上:vruntime += delta_exec × (NICE_0_LOAD / weight)
nice 值先被翻译成权重:
| nice | -1 | 0 | 1 | 5 | 10 | 19 |
|---|---|---|---|---|---|---|
| weight | 1277 | 1024 | 820 | 335 | 110 | 15 |
nice 0 的任务跑 1 ms,vruntime 涨 1 ms;nice 10 的任务跑 1 ms,vruntime 涨约 9.3 ms。所以 CFS 会更少选它------这就是"公平"的实现方式:不是按墙钟时间均分,而是按权重折算后的虚拟时间均分。
② 红黑树最左:O(log n) 找下一个
CFS 的就绪队列是一棵红黑树,键是 vruntime。pick_next_entity() 取最左节点------也就是"虚拟时间最小"的实体。
③ sched_slice:一次该跑多久
CFS 先算一个"调度周期",再按权重分给每个任务:
- 任务数 ≤ 8 时,周期 6 ms
- 超过 8 个,周期拉长为
nr × 0.75 ms,保证每个任务至少跑 0.75 ms
从调度到频率:schedutil
传统 governor 需要自己采样 CPU 占用;schedutil 直接复用调度器的 PELT 统计:
c
/* kernel/sched/cpufreq_schedutil.c */
util_avg = rq->cfs.avg.util_avg; // 0--1024,对应 CPU capacity
next_freq ≈ current_freq × util / (target_load% × max)
本板 target_load = 80,所以当 util 达到 max 时,它会请求当前频率的 1.25 倍,逐步爬到最高档------这就是"留 20% 余量"的含义。
💡 设计洞察:调度器就是 DVFS 的传感器
schedutil 把"谁在跑"和"跑多快"统一到同一个 util 信号上,用一点耦合换掉了重复采样和延迟。
五、中断与并发:GIC → hardirq → 下半部的三段接力
网卡收到一个包,GIC 先把它变成 Linux 中断号;hardirq 只做最少的事,把重活交给下半部。
第一段:从 DTS 到 Linux 中断号
RK3568 用 GICv3。DTS 里写的是:
dts
interrupts = <GIC_SPI 27 IRQ_TYPE_LEVEL_HIGH>;
GIC 驱动的 translate 回调把 GIC_SPI 27 变成硬件中断号 27 + 32 = 59,再通过 irq_domain 映射成 Linux 的 virq。
| 概念 | 例子 | 谁分配 |
|---|---|---|
| DTS 中断描述 | GIC_SPI 27 | 设备树作者 |
| hwirq(硬件号) | 59 | GIC 的 translate 回调 |
| virq(Linux 中断号) | /proc/interrupts 第一列 | irq_domain 动态映射 |
PMIC 的中断链更有意思 :它挂在 GPIO0 上,而 GPIO0 自己又是一个中断控制器------所以要经过两级 irq_domain:rk808 的 hwirq → GPIO0 的 virq → GIC 的 hwirq → 最终的 virq。
第二段:hardirq 只做最少的事
hardirq 运行在中断上下文、IRQ 关闭的状态,不能睡眠、不能等 I2C、不能拿会睡眠的锁。所以它只做"清状态、记账、把重活排出去"。
第三段:下半部的三种选择
| 机制 | 运行上下文 | 能睡眠 | 典型用途 |
|---|---|---|---|
| softirq / NAPI | 软中断(借用当前任务栈) | 否 | 网络收包、块层完成 |
| threaded IRQ | 每个 IRQ 一个内核线程 | 是 | PMIC、I2C/SPI 设备 |
| workqueue | 共享 kworker 池 | 是 | 链接状态、延迟/可睡眠工作 |
为什么不是所有中断都线程化?
线程化让下半部能睡眠,但多了一次唤醒与上下文切换;对高频率的网络收包,这会吃掉吞吐。
💡 设计洞察
Linux 保留 softirq 作为"快而短"的下半部,把"慢而可睡眠"的留给 threaded IRQ / workqueue。
选择标准不是"哪个更先进",而是这段工作能不能在中断上下文里安全、快速地完成。
六、驱动模型:probe 的成功与失败之间
一块网卡从 DTS 节点变成 probe(),中间不是一次函数调用:bus 先匹配,driver core 再绑定;probe 失败时要按顺序清理,返回 -EPROBE_DEFER 时还要排队重试。
三角关系:bus、device、driver
| 角色 | 结构 | 职责 | 用户态入口 |
|---|---|---|---|
| bus | struct bus_type |
维护两个链表,提供 match() | /sys/bus/platform/ |
| device | struct device |
描述"有什么":资源、父节点、PM 状态 | /sys/bus/platform/devices/ |
| driver | struct device_driver |
描述"怎么驱动":probe/remove、匹配表 | /sys/bus/platform/drivers/ |
匹配顺序是固定的:OF compatible → ACPI → id_table → 驱动名。
probe 失败了会怎样?
really_probe() 按顺序做这些事:检查 supplier 依赖 → 绑定 pinctrl → 配置 DMA → 建 sysfs 链接 → 调 probe() → driver_bound()。
任何一步失败都会进入清理路径:
c
/* drivers/base/dd.c:600 */
probe_failed:
device_links_no_driver(dev);
devres_release_all(dev); // 释放所有 devm_* 资源
arch_teardown_dma_ops(dev);
driver_sysfs_remove(dev); // 删掉 driver 符号链接
dev->driver = NULL;
💡 设计洞察:probe 失败不调用 remove
如果
probe()返回错误,driver core 不会 调用驱动的remove(),而是直接devres_release_all()释放所有devm_*资源。这要求驱动在 probe 里尽量使用
devm接口;自己kzalloc的东西必须自己处理,否则会泄漏。
-EPROBE_DEFER:不是失败,是"稍后再试"
如果 probe 依赖的资源(regulator、clock、GPIO、另一个设备)还没就绪,驱动返回 -EPROBE_DEFER。driver core 把设备放进 deferred 列表,等某个驱动成功绑定后触发重试。
板上可以看这张 pending 列表:
bash
cat /sys/kernel/debug/devices_deferred
如果它是空的,说明启动时所有设备都绑定成功;如果某行长期存在,通常意味着 supplier 驱动没编进来、DTS 依赖写错,或者资源名对不上。
runtime PM:设备什么时候"睡着"
driver core 在 probe 前后自动处理 supplier 的引用,驱动自己则用 runtime PM 控制设备空闲时是否休眠。核心是引用计数 + 父子依赖 + autosuspend 延迟共同决定,不是驱动自己写定时器。
七、调试与观测:先体检,再选工具
ftrace、debugfs、kprobe、perf 不是"装上就有"。它们由内核配置决定,而运行内核的配置可能与磁盘上的 .config 不一致。
第一步:体检,不猜
bash
zcat /proc/config.gz | grep -E "CONFIG_(DEBUG_FS|FTRACE|FUNCTION_GRAPH_TRACER|KPROBES|PERF_EVENTS|DYNAMIC_DEBUG|MAGIC_SYSRQ)"
这台仓库里就有个真实的例子:磁盘 .config 说 DEBUG_FS、PERF_EVENTS、MAGIC_SYSRQ 都是 off;但已编译镜像的 autoconf.h 里它们是 on,而 KPROBES 在两边都没有。
同一棵树,磁盘配置和运行镜像可能不是一回事。
工具选择矩阵
| 问题 | 首选 | 备选 / 备注 |
|---|---|---|
| 这个函数被调用了吗?调用链多深? | ftrace function_graph |
kprobe(需 CONFIG_KPROBES) |
| 这个事件发生了多少次?参数是什么? | tracepoint events | perf(需 PERF_EVENTS) |
| 子系统当前状态? | debugfs | /proc、/sys |
| 驱动 probe 为什么失败? | dynamic debug + dmesg | ftrace、devices_deferred |
| 任务卡住/阻塞? | /proc/<pid>/stack |
sysrq w(需 MAGIC_SYSRQ) |
| 内存去哪了? | /proc/slabinfo、memblock |
kmemleak(需开启) |
一次探针式排障:GMAC probe 失败
把前七课串起来,按这个顺序走:
- 先看结果 :
ls -l /sys/bus/platform/devices/fe2a0000.ethernet/driver有没有链接;devices_deferred有没有列出它 - 看日志 :
dmesg | grep -iE "rk_gmac|probe.*failed|deferred" - 看依赖 :DTS 里的
phy-supply、clock、reset-gpio 是否由别的驱动提供;用clk_summary与regulator_summary确认 - 看调用 :
function_graph过滤rk_gmac_probe,确认失败发生在哪一步 - 验证修复 :改 DTS/配置后重编,重复第 1 步------
devices_deferred清空、driver 链接出现、dmesg 无 failed,才算闭环
八、写在最后:内核设计的共通哲学
回头看这八章,你会发现内核的设计处处透着一种共通的智慧:
| 主线 | 核心矛盾 | 解法 | 设计思想 |
|---|---|---|---|
| 配置与绑定 | 一套源码服务千差万别的板卡 | 三层配置分离 + compatible 匹配 | 能力与策略分离 |
| 启动流程 | 从裸硬件到完整系统,一步做不到 | 三段式:汇编→核心→世界 | 用简单启动复杂 |
| 内存管理 | 连续性与利用率的矛盾 | memblock→buddy→CMA 接力 | 预留 + 借用 + 迁移 |
| slab 分配 | 既要 DMA 对齐,又要减少碎片 | per-CPU freelist + size class 对齐 | 为最坏用例买单,用闲置换延迟 |
| 调度与频率 | 公平性与响应速度的平衡 | vruntime + schedutil 统一信号 | 耦合换效率 |
| 中断与并发 | 快速响应与不能睡眠的矛盾 | hardirq + 三种下半部分层 | 快的归快的,慢的归慢的 |
| 驱动模型 | 匹配与失败清理的复杂性 | bus 三角 + devres + deferred | 资源自动生命周期管理 |
| 调试观测 | 工具很多但不一定都开 | 先体检配置,再选工具 | 可观测性也是配置 |
这不是巧合。内核里到处都是这种**"分阶段、做取舍、留接口"**的思路。
📌 学内核的正确姿势
不要只读源码,一定要板上验证。每学一个概念,就去板上敲一条命令,亲眼看到那个数字、那行日志。
内核不是书本里的理论,它是跑在你板子上的真实代码。
而你,只要愿意花 30 分钟追一个具体问题,就能比 90% 只会背"内核五大子系统"的人更懂它。
参考资料(均为本机 SDK 内一手文档):
kernel/Documentation/driver-api/driver-model/binding.rst------ 驱动绑定说明kernel/Documentation/arm64/booting.rst------ arm64 启动协议kernel/Documentation/admin-guide/mm/concepts.rst------ 内存管理概念地图kernel/Documentation/trace/ftrace.rst------ ftrace 完整手册kernel/Documentation/scheduler/sched-design-CFS.rst------ CFS 设计文档docs/cn/RK3566_RK3568/RK3566_RK3568_Linux5.10_SDK_Note.md------ Rockchip 5.10 SDK 发布说明
本文基于 ATK-DLRK3568 开发板 + Rockchip Linux 5.10 BSP 实战整理,所有源码路径与行号均来自真实 SDK。八课主线到此闭环------如果你也在学内核,欢迎拿你手里的板子一起对账。