wenbo2005.online

学习记录 / Linux

Linux 内核学习第一课

Linux2026-08-11

作者:huangwenbo
环境:Ubuntu 20.04 LTS,内核 5.15.0-139-generic
目标:编写、编译、加载并卸载第一个 Linux 内核模块


1. 本课学习目标

完成本课后,你应该能够:

  • 理解用户空间程序与内核模块的区别
  • 编写一个最简单的内核模块 hello.c
  • 使用 Kbuild 系统编译出 hello.ko
  • 用 insmod / rmmod 加载和卸载模块
  • 用 dmesg 查看内核日志输出
  • 独立排查常见编译与加载错误

2. 核心概念

2.1 用户程序 vs 内核模块

对比项 用户空间程序 内核模块
入口函数 main() module_init()
退出方式 return / exit() module_exit()
输出方式 printf → 终端 printk → 内核日志
编译产物 可执行文件 .ko(Kernel Object)
运行位置 用户空间 内核空间

2.2 什么是内核模块

内核模块是可以动态加载到内核中的代码,不需要重新编译整个内核。

典型流程:

hello.c  →  make  →  hello.ko  →  insmod  →  内核执行 hello_init()
                                              →  dmesg 看到输出
                         rmmod  →  内核执行 hello_exit()

2.3 out-of-tree 模块

我们这种在 kernel 源码树外部单独编译的模块,叫做 out-of-tree module(树外模块)。

加载时可能出现如下警告(开发阶段正常,可忽略):

hello: loading out-of-tree module taints kernel.
hello: module verification failed: signature and/or required key missing - tainting kernel

含义:

  • out-of-tree:模块不在官方内核源码树内
  • taints kernel:内核被标记为"不干净",便于后续 bug 排查
  • signature missing:模块未签名,Ubuntu 默认要求签名验证

查看内核污点状态:

cat /proc/sys/kernel/tainted

3. 安装环境

3.1 获取当前 Ubuntu 内核版本

uname -r

示例输出:5.15.0-139-generic。后续安装内核头文件、编译模块都要用到这个版本号。

3.2 安装编译工具

sudo apt update
sudo apt install build-essential linux-headers-$(uname -r)

build-essential 是一个开发者工具包,包含 GCC、MAKE 等常用编译工具。
linux-headers-$(uname -r) 是当前内核版本对应的头文件,内核模块编译必须依赖它。

3.3 验证安装是否成功

上一步安装成功后,可以验证:

gcc -v
make -v

也可以查看内核头文件目录是否存在:

ls /lib/modules/$(uname -r)/build

最后一行应能看到内核头文件目录,例如 /usr/src/linux-headers-5.15.0-139-generic。


4. 项目结构

hello/
├── hello.c       # 内核模块源码
└── Makefile      # 编译脚本(注意大写 M)

5. 源码讲解:hello.c

#include <linux/init.h>
#include <linux/module.h>
#include <linux/kernel.h>

// 模块加载时执行
static int __init hello_init(void)
{
    printk("Hello, World! 内核模块加载成功\n");
    return 0;
}

// 模块卸载时执行
static void __exit hello_exit(void)
{
    printk("Hello, World! 内核模块卸载成功\n");
}

module_init(hello_init);   // 绑定初始化函数
module_exit(hello_exit);   // 绑定卸载函数

MODULE_LICENSE("GPL");
MODULE_AUTHOR("huangwenbo");
MODULE_DESCRIPTION("第一个内核模块");
MODULE_VERSION("0.1");

5.1 关键语法说明

语法 作用
#include <linux/...> 内核头文件,不能用 <stdio.h>
static int __init 初始化函数,加载后内存可被释放
static void __exit 卸载函数
printk() 内核打印,输出到环形缓冲区,不是终端
module_init() 注册模块入口
module_exit() 注册模块出口
MODULE_LICENSE("GPL") 声明 GPL 许可证,才能使用 GPL 内核符号
return 0 返回 0 表示加载成功;非 0 表示失败

5.2 关于 printk 日志级别

printk 支持不同日志级别,常用宏:

宏 含义
KERN_EMERG 紧急
KERN_ERR 错误
KERN_WARNING 警告
KERN_INFO 信息(常用)
KERN_DEBUG 调试

更简洁的写法:pr_info("...")、pr_err("...") 等。


6. Makefile 讲解

.PHONY: all clean

obj-m += hello.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

6.1 各行含义

行 含义
obj-m += hello.o 将 hello.c 编译为可加载模块(m = module)
obj-y 静态编入内核(本课不用)
make -C .../build 进入内核编译系统目录
M=$(PWD) 指定外部模块源码所在目录
modules 编译模块目标
clean 清理编译产物

6.2 文件名必须是 Makefile

Linux 下文件名区分大小写,这一点很容易踩坑:

普通 make

  • makefile 或 Makefile 都行

内核模块编译(Kbuild)

  • 必须是 Makefile(大写 M)
场景 认哪个文件名
普通 make makefile 或 Makefile 均可
内核 Kbuild 必须是 Makefile(大写 M)

错误示例:文件名为 makefile(小写),内核编译时会报:

Makefile: 没有那个文件或目录

修复方法:

mv makefile Makefile

7. 编译与运行

7.1 完整操作流程

# 1. 进入项目目录
cd ~/Desktop/workspace/hello

# 2. 编译模块
make

# 3. 确认产物
ls hello.ko

# 4. 加载模块(注意是 .ko,不是 .ok)
sudo insmod hello.ko

# 5. 查看内核日志
dmesg | tail

# 6. 确认模块已加载
lsmod | grep hello

# 7. 查看模块信息
modinfo hello.ko

# 8. 卸载模块(不带 .ko 后缀)
sudo rmmod hello

# 9. 再次查看日志
dmesg | tail

7.2 成功时的 dmesg 输出示例

[ 6474.545115] hello: loading out-of-tree module taints kernel.
[ 6474.545328] hello: module verification failed: signature and/or required key missing - tainting kernel
[ 6474.546871] Hello, World! 内核模块加载成功
[ 6570.448375] Hello, World! 内核模块卸载成功

前两行是警告,后两行是我们的模块输出,说明加载/卸载成功。

7.3 模块加载 / 卸载 / 日志命令

sudo insmod hello.ko      # 加载 hello 模块
lsmod | grep hello        # 查看已经加载的模块,并过滤出 hello
sudo rmmod hello          # 卸载 hello 模块
dmesg | tail              # 看内核日志最后 10 行
sudo dmesg -c             # 清空内核日志,需要权限

注意:加载时用 hello.ko(带后缀),卸载时用 hello(不带后缀)。

7.4 常用命令速查

命令 作用
make 编译,生成 hello.ko
make clean 删除 .ko、.o 等编译产物
sudo insmod hello.ko 加载 hello 模块
lsmod \| grep hello 查看已经加载的模块,并过滤出 hello
sudo rmmod hello 卸载 hello 模块
dmesg \| tail 看内核日志最后 10 行
sudo dmesg -c 清空内核日志,需要权限
modinfo hello.ko 查看模块元信息
dmesg -w 实时跟踪内核日志
cat /proc/modules 另一种查看已加载模块的方式

8. 常见错误与排查

8.1 Makefile 找不到

现象:

/home/.../hello/Makefile: 没有那个文件或目录

原因: 文件名是 makefile(小写),内核 Kbuild 只认 Makefile。

解决:

mv makefile Makefile

8.2 insmod 报 No such file or directory

现象:

insmod: ERROR: could not load module hello.ko: No such file or directory

常见原因:

  1. 文件名打错:hello.ok 应为 hello.ko
  2. 未编译或已清理:执行过 make clean,.ko 已被删除
  3. 不在正确目录:当前目录没有 hello.ko

解决:

ls hello.ko    # 确认文件存在
make           # 若不存在则重新编译
sudo insmod hello.ko

8.3 make clean 后无法 insmod

原因: make clean 会删除 hello.ko,必须先重新 make。

正确流程:

改代码 → make → insmod → dmesg → rmmod

若执行了 make clean,需要重新 make 再 insmod。


8.4 模块已加载,再次 insmod 失败

现象:

insmod: ERROR: could not insert module hello.ko: File exists

原因: 模块已经在内核中。

解决:

sudo rmmod hello
sudo insmod hello.ko

8.5 dmesg 看不到输出

原因: printk 输出在内核环形缓冲区,不会显示在普通终端。

解决: 使用 dmesg | tail 或 dmesg -w 查看。


9. 本课知识小结

┌─────────────────────────────────────────────────┐
│                  用户空间                        │
│   main.c  →  gcc  →  可执行文件  →  ./a.out      │
│   printf 输出到终端                              │
└─────────────────────────────────────────────────┘

┌─────────────────────────────────────────────────┐
│                  内核空间                        │
│   hello.c  →  make  →  hello.ko  →  insmod      │
│   printk 输出到 dmesg                            │
└─────────────────────────────────────────────────┘

记住三个关键点:

  1. 不能用 printf、malloc 等用户空间函数
  2. Makefile 必须大写 M
  3. 加载用 .ko,卸载不带后缀

10. 下一课预告

建议学习顺序:

  1. 模块参数(module_param)—— 加载时传入变量
  2. 字符设备驱动 —— 实现 /dev/mydev,用户程序通过 read/write 与内核通信
  3. proc / sysfs —— 通过 /proc 或 /sys 与内核交互

11. 参考资源


12. 练习

  1. 修改 hello_init 中的打印内容,重新编译并加载,确认 dmesg 输出已更新
  2. 执行 modinfo hello.ko,找到 author、description、version 字段
  3. 故意把 insmod hello.ok 打错,观察报错,再改回正确命令
  4. 执行 make clean 后直接 insmod,观察报错,再 make 后重新加载
  5. 查资料了解 __init 和 __exit 宏的作用

第一课完成。恭喜迈出 Linux 内核开发的第一步!