0. Linux内核喂饭版教程
Part 2:第一个 hello.ko 最小内核模块
这一节目标很简单:
看懂一个最小 Linux 内核模块:加载时执行什么,卸载时执行什么。
资料里的核心点是:内核模块是 .ko 文件,通过 insmod 动态加载进内核,通过 rmmod 卸载;模块没有 main,而是靠 module_init / module_exit 注册入口和出口。
1. 最小模块代码
先看完整代码:
#include <linux/module.h>
#include <linux/kernel.h>
static int __init hello_init(void)
{
printk("hello module init\n");
return 0;
}
static void __exit hello_exit(void)
{
printk("hello module exit\n");
}
MODULE_LICENSE("GPL");
module_init(hello_init);
module_exit(hello_exit);
你现在不要纠结怎么编译,Makefile 下一节讲。
这一节只看:代码结构和加载流程。
2. 这段代码在干什么
它表达的是:
模块被 insmod 加载
↓
内核调用 hello_init()
↓
printk 打印 "hello module init"
模块被 rmmod 卸载
↓
内核调用 hello_exit()
↓
printk 打印 "hello module exit"
所以这个模块本身没有业务功能,只是验证:
模块能不能被加载
模块加载入口是否执行
模块卸载出口是否执行
printk 日志是否能看到
3. 逐行解释
3.1 #include <linux/module.h>
#include <linux/module.h>
这是写内核模块必须要用的头文件之一。
它提供这些东西:
module_init
module_exit
MODULE_LICENSE
module_param
EXPORT_SYMBOL
也就是:
模块入口注册
模块出口注册
模块许可证
模块参数
符号导出
目前你只需要记住:
linux/module.h提供内核模块机制相关的宏和接口。
3.2 #include <linux/kernel.h>
#include <linux/kernel.h>
它提供一些内核基础函数,比如:
printk
日志级别
内核常用宏
用户态打印用:
printf("hello\n");
内核态打印用:
printk("hello\n");
注意:
内核模块不能随便用
printf,因为printf是用户态 C 库函数,内核没有 glibc 运行环境。
3.3 hello_init
static int __init hello_init(void)
{
printk("hello module init\n");
return 0;
}
这是模块加载函数。
当你执行:
sudo insmod hello.ko
内核会调用它。
返回值含义:
| 返回值 | 含义 |
|---|---|
0 | 加载成功 |
| 非 0 | 加载失败 |
所以:
return 0;
表示这个模块初始化成功。
3.4 __init
static int __init hello_init(void)
__init 是一个内核标记。
它告诉内核:
这个函数只在模块初始化阶段使用。
对静态编进内核的代码来说,初始化完成后,__init 标记的代码段可能被释放,节省内存。
对动态模块来说,先按速成记:
__init表示这是初始化函数相关代码。
不用深挖链接脚本和 section,面试第一轮不需要。
3.5 hello_exit
static void __exit hello_exit(void)
{
printk("hello module exit\n");
}
这是模块卸载函数。
当你执行:
sudo rmmod hello
内核会调用它。
它负责释放资源。
比如以后写字符设备时:
init 里注册字符设备
exit 里注销字符设备
init 里申请内存
exit 里释放内存
init 里创建 /dev 节点相关结构
exit 里清理
所以你要形成一个习惯:
内核模块里,
init申请什么,exit就要释放什么。
3.6 __exit
static void __exit hello_exit(void)
__exit 表示这是模块退出函数相关代码。
第一阶段只记:
__exit标记卸载阶段执行的函数。
3.7 MODULE_LICENSE("GPL")
MODULE_LICENSE("GPL");
这是声明模块许可证。
不写会有 warning,而且内核会认为这个模块可能污染内核状态,也就是常说的 taint。
还有一个面试点:
Linux 内核中有些符号只导出给 GPL 模块使用,所以许可证会影响模块能不能使用某些内核符号。
速成记法:
MODULE_LICENSE("GPL")
↓
声明模块许可证
↓
避免 warning
↓
影响 GPL-only 内核符号访问
3.8 module_init
module_init(hello_init);
这句最关键。
它告诉内核:
模块加载时,调用
hello_init。
所以内核模块没有:
int main()
而是靠:
module_init(hello_init);
注册入口。
你可以把它类比成:
用户态程序入口:main()
内核模块加载入口:module_init 注册的函数
但注意只是帮助理解,机制不一样。
3.9 module_exit
module_exit(hello_exit);
它告诉内核:
模块卸载时,调用
hello_exit。
所以完整生命周期是:
insmod hello.ko
↓
hello_init()
rmmod hello
↓
hello_exit()
4. 加载时完整调用链
执行:
sudo insmod hello.ko
内核内部可以粗略理解成:
用户执行 insmod
↓
insmod 读取 hello.ko
↓
通过系统调用请求内核加载模块
↓
内核校验模块格式 / 版本 / 符号
↓
把模块代码放进内核地址空间
↓
调用 module_init 注册的 hello_init()
↓
printk 写入内核日志缓冲区
你查看日志:
dmesg
能看到:
hello module init
5. 卸载时完整调用链
执行:
sudo rmmod hello
内核内部粗略是:
用户执行 rmmod
↓
内核检查模块是否正在被使用
↓
如果可以卸载
↓
调用 module_exit 注册的 hello_exit()
↓
printk 写入内核日志
↓
释放模块占用的内核资源
查看:
dmesg
能看到:
hello module exit
6. 这段代码为什么不能用 gcc hello.c
普通 C 程序:
gcc hello.c -o hello
./hello
内核模块不行。
原因是:
普通程序链接 glibc
运行在用户态
入口是 main
内核模块依赖 Linux 内核头文件和 Kbuild
运行在内核态
入口是 module_init 注册的函数
所以内核模块要用内核构建系统编译。
下一节你会看到类似:
obj-m += hello.o
以及:
make -C /lib/modules/$(uname -r)/build M=$(pwd) modules
现在先只记:
.ko不是普通可执行文件,不能./hello.ko,只能让内核用insmod加载。
7. 这和性能监控项目怎么对应
hello.ko 只是玩具模块。
你的项目里真正的模块不是打印 hello,而是:
module_init()
↓
注册字符设备
↓
初始化 CPU / softirq 采集缓冲区
↓
启动定时采集逻辑
↓
允许用户态 Worker 通过 open / mmap 读取数据
卸载时:
module_exit()
↓
停止采集
↓
注销字符设备
↓
释放缓冲区
↓
清理内核资源
所以项目里的内核模块生命周期可以这样讲:
insmod cpu_stat_collector.ko
↓
模块初始化
↓
注册 /dev/xxx 字符设备
↓
采集 CPU / softirq 累计值
↓
Worker open + mmap 读取
↓
Worker 做差分
↓
MetricCollector 聚合
↓
MonitorInfo 上报 Manager
项目本身就是 Worker 通过 /proc、字符设备、mmap、eBPF map 等方式采集指标,再经 MetricCollector、gRPC、Manager、HostManager、MySQL 形成完整链路。
8. 这一节你要背的最小版本
最小内核模块由四部分组成:
1. 头文件:
#include <linux/module.h>
#include <linux/kernel.h>
2. 加载函数:
hello_init()
模块 insmod 时执行。
3. 卸载函数:
hello_exit()
模块 rmmod 时执行。
4. 注册宏:
module_init(hello_init)
module_exit(hello_exit)
内核模块没有 main 函数。
内核打印用 printk。
printk 日志用 dmesg 查看。
9. 面试版回答
面试官问:一个最小 Linux 内核模块由什么组成?
你可以答:
一个最小 Linux 内核模块通常包括模块头文件、初始化函数、退出函数、许可证声明,以及
module_init/module_exit两个注册宏。模块没有main函数,加载时内核会调用module_init注册的初始化函数,卸载时调用module_exit注册的退出函数。内核态打印不能用用户态的printf,一般用printk,再通过dmesg查看日志。在我的性能监控项目里,hello 模块只是理解生命周期;真实模块会在初始化阶段注册字符设备和采集缓冲区,在退出阶段注销设备并释放资源。
openEuler 是由开放原子开源基金会孵化的全场景开源操作系统项目,面向数字基础设施四大核心场景(服务器、云计算、边缘计算、嵌入式),全面支持 ARM、x86、RISC-V、loongArch、PowerPC、SW-64 等多样性计算架构
更多推荐

所有评论(0)