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 模块只是理解生命周期;真实模块会在初始化阶段注册字符设备和采集缓冲区,在退出阶段注销设备并释放资源。


Logo

openEuler 是由开放原子开源基金会孵化的全场景开源操作系统项目,面向数字基础设施四大核心场景(服务器、云计算、边缘计算、嵌入式),全面支持 ARM、x86、RISC-V、loongArch、PowerPC、SW-64 等多样性计算架构

更多推荐