☰
openEuler内核入门:从用户态到内核态的分层学习路线
2026/10/13 12:50:45 网站建设 项目流程

这一篇聊的是openEuler内核解读系列里的基础知识。前两篇写安装配置、日常运维时,不少读者留言问同一个问题:内核到底从哪学起?我的回答是别一上来就啃源码,先把知识分成层,一层一层往下摸,这是最稳的路线。openEuler内核基于Linux内核,支持x86_64和ARM等主流硬件,所以下面这些内容放到几乎所有Linux环境里都成立。适合刚接触openEuler的运维同学、想转内核开发的工程师,以及那些追容器性能问题追到内核层就断线的人。

很多朋友把内核看成黑盒,觉得离自己很远。实际上你每次执行ls、写文件、配网络,都在和内核交互,只是这些交互被用户态工具包装得太温柔,你感觉不到而已。这篇文章的目标,就是拆掉这层包装,让你亲眼看到用户态和内核态之间的通道、内核模块的加载过程,以及如何用几个基础工具验证内核行为。全程不需要多高深的C语言功底,跟着操作就能跑通。

1. 先理清三块地基:用户态、系统调用和内核模块

在动手写代码、查源码之前,我建议先把最底层且最容易混的三组概念弄清楚。这三块地基不牢,后面看什么都像看天书。

1.1 用户态与内核态:不是两套源码,而是两种特权环境

用户态和内核态的本质区别在于CPU特权级。x86体系里,CPU有ring0到ring3四个优先级,Linux只用ring0和ring3;ARM体系则用EL1和EL0。重点不是记住编号,而是明白内核态能直接访问物理内存、操作硬件寄存器、执行特权指令,用户态程序只能访问自己的虚拟内存空间,运行普通指令。如果普通进程想访问物理内存,会收到段错误,因为内核没给它这个权限。

为什么要这样隔离?用公司来类比,普通员工只能在工位上处理自己的文件,系统管理员能看服务器上所有日志和配置。如果不隔离,任意一个用户程序都能修改内存、硬件,那病毒和崩溃程序分分钟搞瘫整台机器。openEuler跑在服务器上,这类隔离尤其重要。

判断一个程序当前在哪个态,不需要什么高深工具。你在终端执行top或ps -ef,能看到一堆名字带方括号的进程,比如[kworker/0:1]、[rcu_gp]。这些就是内核线程,它们没有用户态地址空间,全程运行在内核态。普通进程如bash,则大部分时间在用户态,只有调用系统调用时才短暂切进内核态。记住这个判断方法,以后排查问题先分清“这是应用业务问题,还是内核层问题”,方向就不容易跑偏。

1.2 系统调用:用户态和内核态之间的收费站

系统调用是用户程序进入内核态的唯一正道,也是我观察内核行为的第一入口。它就像一个收费站,用户程序想读取文件、发网络包、创建进程,都得经过这个窗口把请求递给内核,内核干完活再把结果返回。之所以非要绕一圈,是因为用户态程序没有权限直接操作硬件,必须由内核代理。

系统调用有编号。openEuler的x86_64和ARM64系统调用表存在差异,但像open、read、write、fork、execve这些常用调用是稳定的。查看程序到底做了什么系统调用,用strace最方便。举个简单例子:

strace ls -l /tmp 2>&1 | head -30

你会看到execve、openat、readdir、write等一系列调用。这些调用就是ls命令和内核之间的完整对话记录。如果某次排查发现程序报“FATAL: kernel too old”,通常就是这个程序调用了当前内核不认识的系统调用号。

这里还有一个容易误导新手的点:不是所有C语言函数都会触发系统调用。比如printf先把内容写入用户态缓冲区,等条件满足再调write;getpid在新版glibc里因为结果被缓存,甚至可能一次系统调用都没有。遇到这类情况,不要怀疑strace坏了,而是用户态多了一层封装。理解这层封装,才算真正理解系统调用的边界。

1.3 内核模块:不是所有驱动都编进了内核

传统Linux是宏内核,但内核功能不是一刀切全编进去,而是把很多驱动和扩展功能做成模块,需要时再加载。openEuler装好后,/lib/modules/$(uname -r)目录下放着大量.ko文件,这就是模块本体。查看当前加载了哪些模块用lsmod,查看某个模块详细信息用modinfo,加载和卸载分别用modprobe和rmmod。

模块为什么存在?直接原因有两个:一是减少内存占用,按需加载驱动;二是开发调试方便,不用每次改驱动都重新编译整个内核。但代价也很明显,模块运行在内核态,写错代码可以直接让系统崩溃,所以自编译模块在openEuler上经常会遇到签名校验问题。虚拟机实验时,建议先关闭SecureBoot,否则加载模块时报Required key not available,不是你的代码错了,是安全机制拦住了。

以后看到docker、网络过滤、文件系统扩展,其实都是在和内核模块打交道。docker依赖的overlayfs、namespace、cgroup,很多就是模块或内核配置项。所以基础概念不是死知识,它们会在后面的实战章节反复出现。

2. openEuler内核版本与源码结构:从哪一层开始读代码

地基概念清楚了,下一步是知道openEuler内核代码在哪、长什么样。很多人在源码面前迷路,是因为不知道代码目录本身就是按层组织的。

2.1 内核版本选择:不要把发行版版本和内核版本搞混

openEuler是发行版,它有自己的版本号,比如20.03 LTS、22.03 LTS,但这和内核版本不是一回事。openEuler 20.03 LTS的内核基线是4.19,22.03 LTS的内核基线是5.10,对应关系以官网发布说明为准。生产环境一般选LTS版本,因为维护周期长、更新稳定;非生产环境或开发板想用新特性,可以追较新的openEuler社区版本。

查看当前运行内核版本用uname -r。这个命令在安装内核开发包时特别重要,因为模块编译内核头文件必须和运行内核严格一致。很多人一上来执行yum install kernel-devel,装完发现版本不匹配,编译出的KO根本加载不了。稳妥做法是:

yum install -y kernel-devel-$(uname -r)

配置网络、安装docker、排查性能,也都应该先看内核版本。docker的overlayfs和namespace功能依赖内核配置,如果升级了内核,最好重新验证一下容器功能,别让“昨天还能跑,今天突然报错”的问题找上你。

2.2 内核源码目录:按硬件、核心、子系统、驱动分层的活教材

openEuler提供了源码包,可以yum install kernel-source,或者去镜像站下载kernel.src.rpm再解压。注意,如果你只是想编译内核模块,装kernel-devel就够了,它包含编译所需的头文件和Makefile,不需要完整源码。

源码目录本身就是一张“分层递进”的地图。我常用下面这张表给新人指路:

目录对应层级常见子模块
arch/x86, arch/arm64硬件抽象层boot、entry、syscalls
kernel/核心算法层sched、time、irq
mm/内存管理层page_alloc、slub、oom_kill
fs/文件系统层ext4、overlayfs、proc
net/网络协议栈层ipv4、netfilter、sock
drivers/设备驱动层net、scsi、gpu

读源码不建议从头按顺序读。我一般推荐先看arch/x86/entry/common.c理解系统调用入口,再看fs/read_write.c追踪一次read调用如何从虚存层到具体文件系统,最后跳到对应驱动。比如你执行cat /proc/stat,数据来源其实是fs/proc/stat.c,一层一层拨开,最终会看到CPU时间统计打印。这个过程比干背目录有意思多了,也符合“分层递进”的主线。

2.3 内核符号表:程序员视角的“内核GPS”

内核符号表记录内核里所有函数、变量的符号,以及它们被放在哪个地址。在openEuler上查看:

cat /proc/kallsyms | head

你会看到类似ffffffff8e0a0000 T _stext的输出。T表示该符号是文本段函数,D是数据段,d是局部变量。符号表最大的用途是排查模块加载失败:当内核报Unknown symbol,去/proc/kallsyms里查这个符号是否被导出,如果找不到,说明依赖的函数没有EXPORT_SYMBOL,或者对应的模块没加载。

内核配置是符号表的“总开关”,存在/boot/config-$(uname -r)文件里。grep CONFIG_MODULES /boot/config-$(uname -r)能看到模块是否开启,grep CONFIG_X86能看到CPU特性。如果想确认某个驱动是不是编进去了,先查config再查模块目录,比盲目改配置高效得多。

这里有个安全细节:新内核把部分符号地址限制为0,普通用户看/proc/kallsyms全是零,需要root权限才能看到真实地址。这是出于安全考虑,不要试图关闭它,生产环境开着就好。

3. 从用户态观察内核:不写代码也能感知分层

内核看着神秘,其实openEuler把大量内部状态暴露在了虚拟文件系统里。这一层不需要写内核代码,就能实践分层学习方法。

3.1 /proc和/sys:内核给你开的窗口

/proc是procfs,/sys是sysfs,它们都是虚拟文件系统,内容由内核动态生成,不占磁盘空间。/proc/meminfo看物理内存使用,/proc/cpuinfo看CPU特性,/proc/slabinfo看内核对象缓存,/sys/kernel/mm看内存子系统参数。

配置网络时遇到的/proc/sys/net/ipv4/ip_forward,实际上就是内核网络层功能的开关。改sysctl -w net.ipv4.ip_forward=1只对当前生效,要持久化需要在/etc/sysctl.d/下写配置。这也是openEuler多种网络配置方式里最常见的一种路径。理解了“改sysctl就是改内核参数”,你配置网络时就不会把路由转发问题当成应用层面的问题了。

我的建议是,把/proc和/sys当作“内核状态仪表盘”。排查问题前先在这两个目录里看一圈,能快速缩小范围,还能避免乱改系统参数。切记不要随手往/proc/sys里写值,很多参数改哪怕一点点,都可能引起系统行为改变。

3.2 内核环形缓冲:printk的终点,dmesg的起点

内核自己不打日志文件,而是往一块固定大小的环形缓冲区里写日志。缓冲区写满后,新日志会覆盖旧日志,这就是大家搜索过的“内核缓冲”的真相。openEuler上查看这块缓冲区用dmesg,实时跟踪用dmesg -w,想看历史启动日志则用journalctl -k。

为什么内核要用环形缓冲?因为启动早期文件系统还没挂载,写不了日志文件,而且如果每次printk都刷磁盘,IO开销会把系统拖垮。环形缓冲的毛病也明显:日志会被覆盖。调试自己的内核模块时,如果printk打太频繁,很可能等你去看的时候,关键日志已经被冲掉了。

所以我的习惯是:出现问题后第一件事先dmesg -T > /tmp/dmesg.log留底,再慢慢看滚动输出。如果你想要更大的缓冲,可以在内核启动参数里加log_buf_len=8M,或者调整kernel.printk参数控制日志级别。这些参数看着不起眼,真到现场排障的时候能救命。

3.3 strace、perf和eBPF:沿着分层逐渐往下钻

观察内核行为不能只靠看文件,还得有工具。我建议按这个顺序学三样东西:

  • strace:跟踪系统调用,停在用户态和内核态的边界上。
  • perf:利用内核性能事件子系统,采样内核函数热点。
  • eBPF:在内核虚拟机中跑受限程序,动态观测内核事件。

openEuler仓库自带这些工具,装起来很方便。示例:

strace -c ls

会统计ls执行过程中的系统调用次数;再看perf top -K,能显示CPU在内核里的热点函数。这些命令背后,其实就是一层比一层深的内核视野。初学者能从strace看到边界,再到perf看到函数级热点,最后用bpftrace写出“统计某文件系统函数调用次数”的小脚本,这条递进路线非常平滑。

要提醒的是,perf和eBPF能力越强,权限要求也越高。普通用户可能因为perf_event_paranoid限制看不了完整数据,容器环境还需要CAP_PERFMON等权限。遇到权限报错时,先看安全配置合理不合理,别急着关保护。

4. 动手写一个最小内核模块:把前几层的知识串起来

说了这么多基础概念,不如亲手做一个小实验。这个实验能在几分钟内让你走完用户态→系统调用→内核态的完整链路。

4.1 实验环境搭建:用虚拟机隔离“炸内核”的风险

强烈不建议在实体机上做内核模块实验,因为你永远不知道一个printk循环会把系统卡成什么样。我用的是VirtualBox,内存给2GB,安装openEuler最小系统。做完记得打快照。

进入系统后安装编译环境:

yum install -y gcc make kernel-devel uname -r ls -d /usr/src/kernels/$(uname -r)

重点还是那个坑:kernel-devel版本必须匹配uname -r输出。如果不一致,指定版本号安装。另外,虚拟机固件里建议关闭SecureBoot,否则后面加载自编译模块会遇到签名错误,影响实验连续性。

4.2 编写hello_module模块和Makefile

新建一个目录,放两个文件。第一个是hello_module.c:

#include <linux/init.h> #include <linux/module.h> #include <linux/kernel.h> static int __init hello_init(void) { printk(KERN_INFO "[hello] module loaded\n"); return 0; } static void __exit hello_exit(void) { printk(KERN_INFO "[hello] module unloaded\n"); } module_init(hello_init); module_exit(hello_exit); MODULE_LICENSE("GPL"); MODULE_AUTHOR("your name");

第二个是Makefile,注意第二行和第四行行首必须是Tab,不是空格:

obj-m += hello_module.o all: make -C /lib/modules/$(shell uname -r)/build M=$(PWD) modules clean: make -C /lib/modules/$(shell uname -r)/build M=$(PWD) clean

这里obj-m += hello_module.o告诉kbuild要把这个文件编译成模块;MODULE_LICENSE("GPL")不是摆设,很多内核符号只对GPL模块导出,不写它以后会踩“Unknown symbol”的坑。

4.3 加载、卸载并观察系统调用链

编译成功后,按顺序执行:

make insmod hello_module.ko lsmod | grep hello rmmod hello_module dmesg | tail -20

如果一切顺利,dmesg最后几行会看到[hello] module loaded和[hello] module unloaded。这个实验虽然简单,但已经完整见证了模块从用户态通过init_module系统调用进入内核态,注册初始化和退出函数的过程。

还可以用strace验证:

strace -e init_module,delete_module insmod hello_module.ko

输出里能看到init_module(3, {......}),这就把第一章讲的系统调用和内核模块串起来了。如果你遇到Invalid module format,先执行modinfo hello_module.ko | grep vermagic,再对比uname -r,八成是内核头文件版本不一致。

报错信息常见原因处理办法
Invalid module formatvermagic不匹配重装匹配的kernel-devel并重新make
Unknown symbol依赖的模块或符号不在先加载依赖,或查看符号表确认
Required key not availableSecureBoot签名拦截实验机关闭SecureBoot,或用官方签名流程

5. 常见内核问题与排查套路:从新手到熟练工的必经之路

实验做完,再分享几个我实际踩过的坑。这些内容不算高深,但确实能帮你少走弯路。

5.1 模块加载失败:先看dmesg,再看版本

模块加载失败时,insmod往往只给一句很短的话,真正的错误都在内核日志里。比如insmod: ERROR: could not insert module,后面可能跟着Unknown symbol或Invalid module format。正确姿势是先执行dmesg -T | tail,看内核具体抱怨什么。

Unknown symbol的处理思路是去/proc/kallsyms里查符号是否导出。如果在符号表里搜不到,说明该符号所在的模块没加载,或者这个函数根本没EXPORT_SYMBOL。如果符号表里存在但照样报错,那可能是模块与当前内核的vermagic不一致,modinfo一眼就能看出来。按这个顺序排查,能解决八成加载问题。

5.2 内核升级引发的驱动与模块问题

openEuler升级内核后,第三方驱动模块经常会“失联”。原因很简单:/lib/modules下多了新内核目录,但新目录里没有你单独编译的那些KO,旧内核的模块不可能加载进新内核。普通RPM软件不受影响,但驱动、内核扩展都必须重编。

数据库、GPU卡、特殊网卡这类场景尤其明显。我在生产环境的原则是:升级内核前先做快照或备份,升级后第一时间验证存储和网络设备驱动是否正常。如果想锁住内核版本,openEuler可以用yum versionlock kernel防止内核自动跨大版本。别等升级完发生问题才开始后悔。

5.3 printk刷屏与日志丢失:环形缓冲的隐藏问题

写内核模块时,我感觉最简单的“翻车”方式就是无节制printk。你在循环里每分钟打几百条日志,系统负载噌噌上涨,控制台刷屏,而且环形缓冲很快被填满,之前的关键信息全被冲掉。我自己的教训是,调试时不要用死循环打印,也不要打无级别printk,每次打印之前先想清楚这条日志将来要不要看。

临时调低内核日志级别可以用:

sysctl kernel.printk="3 4 1 7"

需要保留更多历史日志,则在启动参数里加log_buf_len=8M。真到了内核日志把系统拖到近乎假死的地步,能做的也只有冷静保存日志、重启复现、然后修复代码。

这个系列走到第三篇,我特别想强调“分层递进”不只是学习顺序,也是排错顺序。遇到任何内核相关的问题,先确定它发生在用户态、系统调用层、内核核心层还是驱动层,然后逐层观察:strace看边界,perf看热点,dmesg看日志,/proc和/sys看状态。按这个顺序排查,方向大多不会错。我当年写第一个内核模块时,因为在虚拟机里忽略了vermagic,折腾了半小时才猛然发现kernel-devel版本不对。如果这篇文章能帮后面的朋友少踩几个这样的基础坑,那这一篇就没白写。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询