Ubuntu 驱动开发入门:编写一个简单的 "Hello World" 内核模块
在 Ubuntu 上开发驱动程序意味着要编写 Linux 内核模块。下面我将带你创建一个最简单的 "Hello World" 内核模块,并演示如何编译、加载和卸载它。
1. 准备工作
首先安装必要的开发工具和内核头文件:
sudo apt update
sudo apt install build-essential linux-headers-$(uname -r)
2. 编写最简单的内核模块
创建一个名为 hello_world.c
的文件:
#include <linux/init.h> // 包含模块初始化和清理函数的宏
#include <linux/module.h> // 包含内核模块相关的函数和宏
#include <linux/kernel.h> // 包含 printk 等内核函数
// 模块信息(可选但推荐)
MODULE_LICENSE("GPL"); // 许可证
MODULE_AUTHOR("Your Name"); // 作者
MODULE_DESCRIPTION("A simple Hello World Linux kernel module"); // 描述
MODULE_VERSION("0.1"); // 版本
// 模块初始化函数
static int __init hello_init(void)
{
printk(KERN_INFO "Hello, World from the Linux kernel!\n");
return 0; // 返回 0 表示成功
}
// 模块清理函数
static void __exit hello_exit(void)
{
printk(KERN_INFO "Goodbye, World from the Linux kernel!\n");
}
// 注册模块的初始化和清理函数
module_init(hello_init);
module_exit(hello_exit);
3. 创建 Makefile
在同一目录下创建 Makefile
文件:
obj-m := hello_world.o
KDIR := /lib/modules/$(shell uname -r)/build
PWD := $(shell pwd)
all:
$(MAKE) -C $(KDIR) M=$(PWD) modules
clean:
$(MAKE) -C $(KDIR) M=$(PWD) clean
4. 编译模块
在终端中运行:
make
如果一切顺利,你会看到生成了几个文件,其中最重要的是 hello_world.ko
(内核对象文件)。
5. 加载和测试模块
加载模块:
sudo insmod hello_world.ko
检查模块是否加载:
lsmod | grep hello_world
查看内核日志输出:
dmesg | tail -n 2
你应该能看到类似这样的输出:
[timestamp] Hello, World from the Linux kernel!
卸载模块:
sudo rmmod hello_world
再次检查内核日志:
dmesg | tail -n 2
现在应该能看到:
[timestamp] Goodbye, World from the Linux kernel!
6. 进阶说明
-
printk 和日志级别:
-
KERN_INFO
是日志级别,其他级别包括KERN_DEBUG
,KERN_WARNING
,KERN_ERR
等 -
查看所有内核消息:
dmesg
或journalctl -k
-
-
模块参数:
可以添加可配置参数:static char *name = "World"; module_param(name, charp, 0644); MODULE_PARM_DESC(name, "The name to greet"); static int __init hello_init(void) { printk(KERN_INFO "Hello, %s from the Linux kernel!\n", name); return 0; }
然后加载时指定参数:
sudo insmod hello_world.ko name="Linux"
-
安全性:
-
内核模块运行在内核空间,错误会导致系统崩溃
-
开发时建议在虚拟机中进行
-
-
调试:
-
使用
printk
是最简单的调试方式 -
更高级的调试可以使用
kgdb
或kprobes
-
7. 清理
完成后可以清理编译生成的文件:
make clean
下一步
如果你想进一步学习 Linux 驱动开发,可以:
-
学习字符设备驱动(创建
/dev
下的设备文件) -
研究
file_operations
结构体实现读写操作 -
探索平台设备驱动和设备树(用于嵌入式开发)
这个简单的 "Hello World" 模块展示了最基本的 Linux 内核模块结构,是进入 Linux 驱动开发世界的第一步。
这个 Makefile 是用于编译 Linux 内核模块的标准模板。我来逐行解释它的含义和作用:
1. obj-m := hello_world.o
-
作用:告诉内核构建系统要编译哪些模块。
-
解释:
-
obj-m
表示要编译成可加载内核模块(.ko
文件)。 -
hello_world.o
是目标文件名(对应你的.c
文件)。 -
构建系统会自动将
hello_world.o
链接成hello_world.ko
。
-
2. KDIR := /lib/modules/$(shell uname -r)/build
-
作用:指定当前运行的内核的构建目录。
-
解释:
-
$(shell uname -r)
执行 shell 命令获取当前内核版本(如5.15.0-76-generic
)。 -
/lib/modules/$(uname -r)/build
是标准的内核头文件和构建系统路径。 -
这是内核模块编译时必须指向的目录,因为它包含内核的配置和头文件。
-
3. PWD := $(shell pwd)
-
作用:获取当前工作目录的绝对路径。
-
解释:
-
$(shell pwd)
执行 shell 的pwd
命令,返回当前目录路径。 -
用于告诉内核构建系统在哪里查找你的模块源代码。
-
4. all:
目标
all:
$(MAKE) -C $(KDIR) M=$(PWD) modules
-
作用:编译模块的主要指令。
-
解释:
-
$(MAKE)
:调用make
工具。 -
-C $(KDIR)
:切换到内核构建目录(/lib/modules/.../build
)。 -
M=$(PWD)
:告诉内核构建系统模块源代码的位置(当前目录)。 -
modules
:内核构建系统的目标,表示编译外部模块。 -
实际效果:
内核的构建系统会读取当前目录的代码,使用内核的配置和编译器设置来生成.ko
文件。
-
5. clean:
目标
clean:
$(MAKE) -C $(KDIR) M=$(PWD) clean
-
作用:清理编译生成的文件。
-
解释:
-
clean
:内核构建系统的目标,表示清理生成的文件。 -
会删除
.ko
、.o
、.mod.c
等中间文件。
-
关键点总结
部分 | 作用 |
---|---|
obj-m |
指定要编译的模块目标(.o 文件)。 |
KDIR |
指向当前运行内核的构建目录(包含内核头文件和配置)。 |
M=$(PWD) |
告诉内核构建系统模块源代码的位置。 |
$(MAKE) -C ... |
切换到内核目录并调用内核的构建系统处理当前目录的模块代码。 |
modules /clean |
内核构建系统的目标,分别表示编译模块和清理生成文件。 |
为什么需要这样的 Makefile?
-
依赖内核构建系统:
Linux 内核模块必须与当前运行的内核严格匹配(版本、配置、编译器选项等)。直接使用内核的构建系统可以确保兼容性。 -
简化编译过程:
手动指定所有编译选项(如-I
路径、宏定义等)非常复杂,而内核的构建系统自动处理这些细节。 -
标准化的模块构建:
这是内核官方推荐的模块编译方式,所有 Linux 驱动开发者都使用类似的 Makefile。
实际执行流程示例
-
当你在终端运行
make
时:-
Makefile 调用
$(MAKE) -C /lib/modules/$(uname -r)/build M=$(pwd) modules
。 -
内核构建系统读取当前目录的代码,生成
hello_world.ko
。
-
-
运行
make clean
时:-
删除所有生成的文件,只保留源代码。
-
常见问题
Q1: 如果遇到 Cannot find /lib/modules/.../build
错误?
-
原因:未安装当前内核的头文件。
-
解决:
sudo apt install linux-headers-$(uname -r)
Q2: 能否直接使用 gcc
编译内核模块?
-
不能。内核模块需要特殊的内核头文件、编译器选项和链接方式,必须通过内核构建系统完成。
Q3: M=$(PWD)
的作用是什么?
-
它向内核构建系统传递你的模块源代码路径,否则构建系统不知道在哪里找你的
.c
文件。
这个 Makefile 是 Linux 内核模块开发的标准模板,理解它的工作原理对驱动开发至关重要!