第1课:基于正点原子RK3568开发板读懂Linux内核

读懂 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 失败

把前七课串起来,按这个顺序走:

  1. 先看结果 :ls -l /sys/bus/platform/devices/fe2a0000.ethernet/driver 有没有链接;devices_deferred 有没有列出它
  2. 看日志 :dmesg | grep -iE "rk_gmac|probe.*failed|deferred"
  3. 看依赖 :DTS 里的 phy-supply、clock、reset-gpio 是否由别的驱动提供;用 clk_summary 与 regulator_summary 确认
  4. 看调用 :function_graph 过滤 rk_gmac_probe,确认失败发生在哪一步
  5. 验证修复 :改 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。八课主线到此闭环------如果你也在学内核,欢迎拿你手里的板子一起对账。

相关推荐
GlobalInfo1 小时前
全球及中国机器人现场部署服务行业研究报告(2026版):市场现状、行业前景与占有率分析
大数据·运维·人工智能
Android系统攻城狮1 小时前
Linux FFmpeg深度解析之avformat_open_input调用流程与实战(一)
linux·运维·ffmpeg·车载音视频·ffmpeg音视频·音视频进阶手册
分布式存储与RustFS2 小时前
把 LSM 引擎直接架在对象存储上:slatedb 能不能跑在 RustFS 上?
运维·云原生·开源·对象存储·分布式存储·s3·性能基准
深圳恒讯2 小时前
国外云服务器部署企业官网,怎样确定配置、IP 与后续扩容方案?
运维·服务器·tcp/ip
java_logo2 小时前
Docker 部署 Excalidraw:轻松搭建手绘风格白板
运维·docker·容器·excalidraw·白板·轩辕镜像
吹什么轩2 小时前
linux系统复习:Ext 文件系统(一个文件在磁盘上到底长什么样)
linux·运维·服务器
吹什么轩2 小时前
linux系统复习:全景串联
linux·运维·服务器
m0_547486662 小时前
《Ubuntu Linux操作系统与实验教程》全套PPT课件2026
linux·ubuntu·操作系统
Elastic 中国社区官方博客2 小时前
5 分钟内将 56 页扫描文档转化为带引用的答案:面向搜索无法读取的文档的检索增强生成流水线
大数据·运维·人工智能·elasticsearch·搜索引擎·信息可视化·全文检索