Linux 内核开发入门:从环境配置到 Hello World 实战

前言:为什么是内核模块?

对于从事高性能网络开发(如 3A 系统、网关)的工程师来说,用户态(User Space)的优化往往有极限。当我们需要极致的性能时,我们就必须踏入**内核态(Kernel Space)**的领地。

Linux 内核模块(Loadable Kernel Module, LKM)是进入这一领域的敲门砖。它允许我们在不重新编译整个操作系统内核的情况下,动态地向内核添加功能。

本文将带你从零开始编写一个"Hello World"内核模块,并着重解决老旧内核版本无法下载开发依赖这一常见痛点。


第一部分:环境准备(最关键的一步)

编写内核模块不同于写普通的 C 语言程序。普通的 C 程序依赖 stdio.h 等标准库,而内核模块依赖当前运行内核的头文件(Kernel Headers)

1. 检查环境

编译内核模块需要内核构建目录,通常位于 /lib/modules/$(uname -r)/build

你可以通过以下命令检查:

bash 复制代码
ls -l /lib/modules/$(uname -r)/build

如果显示 No such file or directory,说明你缺少内核开发包,必须安装。

2. 常规安装方法

如果是较新的系统,直接使用包管理器即可:

bash 复制代码
# Debian/Ubuntu
sudo apt-get update
sudo apt-get install linux-headers-$(uname -r)

# CentOS/RedHat
sudo yum install kernel-devel-$(uname -r)

3. 进阶故障排除:当 apt-get 找不到包时

很多时候(特别是云服务器或长期未重启的机器),我们会遇到以下报错:

E: Unable to locate package linux-headers-6.1.0-31-amd64

原因 :你运行的内核版本(如 6.1.0-31)较旧,而官方源(Repo)已经更新到了新版本(如 6.1.0-32),旧版本的头文件包被下架了。

解决方案:去 Debian Snapshot 仓库"捡漏"

我们需要手动下载对应的 .deb 包并安装。

步骤 A:下载依赖包

我们需要两个包:

  1. Common 包:包含通用的头文件。
  2. Arch 包:包含特定架构(如 amd64)的头文件。

前往 Debian Snapshot 或使用 wget 直接下载(注意:版本号需与 uname -r 严格匹配,以下以 6.1.0-31 为例):

bash 复制代码
# 创建临时目录
mkdir -p ~/kheaders && cd ~/kheaders

# 1. 下载 Common 包 (请根据实际情况替换链接)
wget https://snapshot.debian.org/archive/debian/20250207T205318Z/pool/main/l/linux/linux-headers-6.1.0-31-common_6.1.128-1_all.deb

# 2. 下载架构专用包 (amd64)
wget https://snapshot.debian.org/archive/debian/20250207T205318Z/pool/main/l/linux/linux-headers-6.1.0-31-amd64_6.1.128-1_amd64.deb

由于我的系统使用wget直接下载不到对应的deb包,所有手动进入对应的发行版本链接进行下载的。


步骤 B:智能安装(使用 apt install ./xxx.deb)

技巧 :不要直接用 dpkg -i 安装,因为它处理不了依赖问题(比如可能缺 gcckbuild)。我们要用 apt 来安装本地文件,它会自动补齐缺少的依赖。

bash 复制代码
# ./ 表示安装当前目录下的文件
sudo apt install ./linux-headers-*.deb

安装完成后,再次检查 /lib/modules/$(uname -r)/build,应该能看到蓝色的软链接,环境准备完毕!


第二部分:编写代码

创建一个工作目录 my_hello,我们需要两个文件。

1. 源代码:hello.c

内核编程与用户态编程有几个显著区别:

  • 没有 main() :使用初始化函数 module_init
  • 没有 printf() :使用内核日志函数 printk()
  • 必须清理 :必须提供 module_exit 函数,否则模块卸载后内存泄漏。
c 复制代码
#include <linux/module.h>   // 包含内核模块机制的定义
#include <linux/kernel.h>   // 包含 printk 定义
#include <linux/init.h>     // 包含 __init, __exit 宏

// 必须声明开源协议,否则加载时内核会报 "tainted" (污染) 警告,甚至拒绝加载
MODULE_LICENSE("GPL");
MODULE_AUTHOR("DevTeam");
MODULE_DESCRIPTION("A simple Hello World Kernel Module");

// 1. 模块加载函数 (入口)
// __init 宏告诉内核:这个函数只在初始化时用一次,用完可以回收内存
static int __init hello_init(void)
{
    // KERN_INFO 是日志级别
    printk(KERN_INFO "Hello, Kernel! 模块加载成功,开始工作。\n");
    return 0; // 返回 0 表示成功,非 0 表示失败
}

// 2. 模块卸载函数 (出口)
// __exit 宏表示该代码仅用于卸载阶段
static void __exit hello_exit(void)
{
    printk(KERN_INFO "Goodbye, Kernel! 模块卸载完毕,下班回家。\n");
}

// 3. 注册宏
module_init(hello_init);
module_exit(hello_exit);

2. 编译脚本:Makefile

Linux 内核使用 Kbuild 构建系统。我们需要编写一个特殊的 Makefile。

⚠️ 注意:Makefile 中的缩进必须使用 Tab 键,不能用空格!

makefile 复制代码
# obj-m 表示我们要把 hello.o 编译成模块 (module)
obj-m += hello.o

# 指向我们刚才辛苦配置好的内核构建目录
KDIR := /lib/modules/$(shell uname -r)/build

# 获取当前目录
PWD := $(shell pwd)

# 默认编译目标
all:
	# -C 切换到内核目录,借用其环境
	# M= 回到当前目录编译模块
	make -C $(KDIR) M=$(PWD) modules

# 清理目标
clean:
	make -C $(KDIR) M=$(PWD) clean

第三部分:编译与验证

1. 编译

在目录下直接运行 make

bash 复制代码
root@server:~/my_hello# make
make -C /lib/modules/6.1.0-31-amd64/build M=/root/my_hello modules
make[1]: Entering directory '/usr/src/linux-headers-6.1.0-31-amd64'
  CC [M]  /root/my_hello/hello.o
  MODPOST /root/my_hello/Module.symvers
  CC [M]  /root/my_hello/hello.mod.o
  LD [M]  /root/my_hello/hello.ko
make[1]: Leaving directory '/usr/src/linux-headers-6.1.0-31-amd64'

如果没有报错,目录下会生成一个 hello.ko 文件。这就是编译好的内核模块。


第四部分:加载模块的两种姿势

姿势一:insmod (适合开发测试)

insmod (Install Module) 是最原始的加载方式,它需要指定 .ko 文件的具体路径。它不解决依赖,指哪打哪。

操作步骤:

  1. 加载模块

    bash 复制代码
    sudo insmod hello.ko
  2. 查看日志
    内核日志不会直接打印在终端,需要查看 Ring Buffer。

    bash 复制代码
    dmesg | tail

    输出:[ ... ] Hello, Kernel! 模块加载成功,开始工作。

  3. 卸载模块

    bash 复制代码
    sudo rmmod hello

    (再次查看 dmesg 会看到 "Goodbye..." 的日志)

关于 "Tainted Kernel" 警告

如果 dmesg 出现 loading out-of-tree module taints kernel,请不用惊慌。这只是内核在发牢骚:"你加载了一个非官方发布的模块,出了问题我不负责。" 只要没报 Error,模块就是正常运行的。


姿势二:modprobe (适合生产部署)

如果你尝试运行 modprobe hello,你会发现报错:

modprobe: FATAL: Module hello not found in directory ...

这是因为 modprobe 是通过读取系统的模块索引数据库 来工作的,它不知道你的临时目录里有个 hello.ko

如何让 modprobe 生效?

  1. 归档:将模块复制到系统的标准模块库中。

    bash 复制代码
    sudo mkdir -p /lib/modules/$(uname -r)/kernel/drivers/misc/
    sudo cp hello.ko /lib/modules/$(uname -r)/kernel/drivers/misc/
  2. 更新索引:告诉系统重新扫描一遍所有模块。

    bash 复制代码
    sudo depmod -a
  3. 加载

    现在,你可以在任何目录下直接加载它了,且不需要加 .ko 后缀。

    bash 复制代码
    sudo modprobe hello

modprobe 的优势

  • 自动解决依赖 :如果模块 A 依赖模块 B,modprobe A 会自动先把 B 加载进来。
  • 无需路径:不需要知道文件具体在哪。

总结

通过本文,我们完成了一次完整的内核模块开发流程:

  1. 环境 :通过下载 Snapshot 的 deb 包并用 apt 安装,解决了老旧内核缺失头文件的问题。
  2. 代码 :编写了标准的 module_init/module_exit 结构。
  3. 编译 :利用 Kbuild 系统和 Makefile 生成了 .ko 文件。
  4. 运行 :区分了 insmod(开发用)和 modprobe(生产用)的区别。
相关推荐
乖乖是干饭王2 小时前
Linux 内核 Kbuild 中的 ld 调用机制
linux·c·makefile
Trouvaille ~2 小时前
【Linux】理解“一切皆文件“与缓冲区机制:Linux文件系统的设计哲学
linux·运维·服务器·操作系统·进程·文件·缓冲区
ITKEY_2 小时前
archlinux 通过wpa_supplicant 连接wifi固定ip设置方法
linux·固定ip
小五传输2 小时前
隔离网闸的作用是什么?新型网闸如何构筑“数字护城河”?
大数据·运维·安全
算力魔方AIPC2 小时前
使用 Docker 一键部署 PaddleOCR-VL: 新手保姆级教程
运维·docker·容器
Evan芙3 小时前
nginx核心配置总结,并实现nginx多虚拟主机
运维·数据库·nginx
FIT2CLOUD飞致云3 小时前
操作教程丨通过1Panel快速安装Zabbix,搭建企业级监控系统
运维·服务器·开源·zabbix·监控·1panel
幸存者letp3 小时前
Python 常用方法分类大全
linux·服务器·python
知识分享小能手3 小时前
Ubuntu入门学习教程,从入门到精通,Linux操作系统概述(1)
linux·学习·ubuntu