在学习编程的旅程中,无论是初涉C语言还是探索其他编程语言,我们往往以一句经典的 “ Hello, World! ” 作为起点,以此叩开代码世界的大门。这一简单而富有仪式感的传统,不仅象征着程序员与计算机之间的第一次对话,更承载了无数开发者对技术的热爱与追求。同样地,在驱动程序编程的领域,我们也可以延续这一传统,以 “ Hello, World! ” 作为我们的首个驱动程序,开启通往底层系统开发的奇妙之旅。
接下来,我们将着手编写第一个驱动程序——一个名为 “ Hello, World! ” 的简单驱动模块。这不仅是我们迈向驱动开发的第一步,更是理解Linux内核机制的重要起点。通过这段代码,我们将初步领略驱动程序的基本结构、加载与卸载流程,以及与内核交互的方式。准备好了吗?让我们一起开始这段充满挑战与乐趣的驱动开发之旅吧!
一、内核的基本框架
Linux 内核模块(驱动)开发,最基本的框架由三部分组成:
加载函数(Module Entry Point)
- 这是模块/驱动的初始化入口函数。
- 作用是在驱动被加载(insmod)时完成资源的分配、硬件的初始化或数据结构的建立等准备工作。
- 通常用
static int __init xxx_init(void)定义,并通过module_init()宏注册。 - 加载失败时可通过返回非零值让模块加载中断。
卸载函数(Module Exit Point)
- 当驱动被卸载(rmmod)时自动调用。
- 作用是做资源释放、注销设备和数据结构清理,确保不会造成内存泄漏或系统不稳定。
- 用
static void __exit xxx_exit(void)定义,并通过module_exit()宏注册。
许可证声明(License Declaration)
- 使用
MODULE_LICENSE("GPL v2")或类似宏声明许可证。 - 作用是表明你的模块遵循哪种开源协议,告知内核你的代码符合开源要求。
- 如果不声明,内核会做安全处理,并标记模块为“不受信任”(tainted),也影响某些功能调用。
- 使用
任何一个最简单、最基础的 Linux 内核驱动模块,都必须有:
- 明确的加载与卸载流程(开闭原则,保障资源安全)
- 清晰的法律许可声明(保障合规与内核兼容)
其他补充(根据实际需求可添加):
- 模块参数:允许模块加载时传递参数,提高灵活性
- 导出符号:实现不同模块/驱动间的功能共享
- 作者和描述信息:便于维护和模块识别
二、驱动文件夹准备
我们在自己的工作目录中创建一个 01_helloworld/ 文件夹,用来存放我们的驱动源码和 Makefile 文件:
mkdir 01_helloworld三、编写驱动源码
1、源码
我们在 01_helloworld/ 文件夹下创建一个 hello_world.c 文件,并在其中编写以下代码:
#include <linux/module.h> /* 模块相关宏和函数 */
#include <linux/kernel.h> /* printk日志函数 */
/* 加载函数(驱动入口),当驱动被 insmod 加载时自动执行 */
static int __init helloworld_init(void)
{
printk("helloworld_init\r\n"); // 内核日志打印
return 0; // 返回0代表加载成功
}
/* 卸载函数(驱动出口),当驱动被 rmmod 卸载时自动执行 */
static void __exit helloworld_exit(void)
{
printk("helloworld_exit\r\n");
}
/* 下面这两行,告诉内核入口和出口分别是哪两个函数 */
module_init(helloworld_init);
module_exit(helloworld_exit);
/* 这3个是模块信息声明 */
MODULE_LICENSE("GPL v2"); /* 模块许可证 */
MODULE_VERSION("1.0"); /* 模块版本,可选 */
MODULE_DESCRIPTION("helloworld Driver");/* 模块描述,可选,一般用于 lsmod 时显示 */2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
2、函数讲解
- 头文件:
#include <linux/module.h> /* 模块相关宏和函数 */
#include <linux/kernel.h> /* printk日志函数 */2
linux/module.h:提供模块的初始化、退出和信息声明相关的宏和 API,编写内核模块必须包含。linux/kernel.h:包含内核常用功能,比如printk日志输出函数。
- 加载函数(驱动入口):
static int __init helloworld_init(void)
{
printk("helloworld_init\r\n"); // 内核日志打印
return 0; // 返回0代表加载成功
}2
3
4
5
- static int:声明为静态、只在本文件可见,返回整型。
- __init:GCC 标识,告诉内核这是初始化代码。模块加载后其内存可被回收,优化内存占用。
- helloworld_init:模块入口函数。
- printk:内核中用来打印信息到系统日志(比用户空间的 printf 适用)。
- return 0:成功返回 0,否则内核拒绝加载该模块。
- 卸载函数(驱动出口):
static void __exit helloworld_exit(void)
{
printk("helloworld_exit\r\n");
}2
3
4
- static void:静态、无返回值。
- __exit:告知内核这是卸载相关代码。卸载模块时自动调用。
- helloworld_exit:模块出口(释放资源、清理善后)。
- printk:打印卸载信息到内核日志。
- 告诉内核这两个关键函数的位置:
module_init(helloworld_init);
module_exit(helloworld_exit);2
- module_init:注册入口函数。加载模块时自动执行
helloworld_init。 - module_exit:注册出口函数。卸载模块时自动执行
helloworld_exit。
- 模块信息声明:
MODULE_LICENSE("GPL v2"); /* 模块许可证 */
MODULE_VERSION("1.0"); /* 模块版本,可选 */
MODULE_DESCRIPTION("helloworld Driver");/* 模块描述,可选,一般用于 lsmod 时显示 */2
3
- MODULE_LICENSE:告知内核模块遵循的协议(必须填写,否则内核会报警告“tainted”)。
- MODULE_VERSION:为你的驱动指定版本号,方便维护和升级(可选)。
- MODULE_DESCRIPTION:描述模块用途,便于识别(可选,
lsmod显示时体现)。
3、printk详解
printk 是 Linux 内核中的“打印日志”函数,相当于我们在普通 C 程序里的 printf,但它专门用在内核空间,用于给开发者输出调试信息、状态信息或错误信息。
在 Linux 驱动开发、内核模块开发中,printk 是主要的调试工具。
为什么要用 printk 而不是 printf?
printf只能在用户空间(比如你写的应用程序)用,不能直接在内核里用,因为内核和用户空间的输入输出机制不一样。- 内核中没有标准输出窗口,也没有终端,所以只能通过日志系统(
dmesg)来查看信息。 printk会把你的信息记录到内核日志缓冲区,然后可以用dmesg命令或查看/var/log/messages(不同系统不同路径)来读取。
printk 还支持日志等级,可以指定消息的重要程度:
| 等级 | 表示意义 |
|---|---|
KERN_EMERG | 严重到系统可能瘫痪 |
KERN_ALERT | 需要立即处理的警报 |
KERN_CRIT | 危急错误 |
KERN_ERR | 普通错误 |
KERN_WARNING | 警告(不是致命错误) |
KERN_NOTICE | 需要注意的信息 |
KERN_INFO | 一般信息(成功/流程) |
KERN_DEBUG | 调试信息(代码跟踪) |
如果不指定日志等级,那么就是默认的
KERN_WARNING级别。
用法示例:
printk(KERN_INFO "Driver loaded successfully.\n"); // 流程性通知
printk(KERN_ERR "Can not open device!\n"); // 错误通知
printk(KERN_DEBUG "x value is %d\n", x); // 普通调试时用2
3
内核会把日志分级保存,你可以通过设置过滤只看高“优先级”的日志(比如只看错误/警告,不看调试信息),常见
dmesg或日志管理工具支持等级过滤,让你专注于当前关心的问题。
四、编写Makefile
我们在 01_helloworld/ 文件夹下创建一个 Makefile 文件,并在其中编写以下代码:
export ARCH=arm64
# 交叉编译器绝对路径前缀
export CROSS_COMPILE=/home/lckfb/TaishanPi-3-Linux/prebuilts/gcc/linux-x86/aarch64/gcc-arm-10.3-2021.07-x86_64-aarch64-none-linux-gnu/bin/aarch64-none-linux-gnu-
# 和源文件名一致
obj-m += hello_world.o
# 内核源码目录
KDIR := /home/lckfb/TaishanPi-3-Linux/kernel-6.1
PWD ?= $(shell pwd)
all:
make -C $(KDIR) M=$(PWD) modules
clean:
make -C $(KDIR) M=$(PWD) clean2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
1、CROSS_COMPILE 讲解
这个参数指定的是我们SDK中自带的交叉编译器,编译 hello_world.c 源码的时候调用这个编译器进行编译,这个 CROSS_COMPILE 的值填的是编译器前缀,结尾带 短横线(-)。
一般情况下SDK版本一致编译器路径都会在:
TaishanPi-3-Linux/prebuilts/gcc/linux-x86/aarch64/gcc-arm-10.3-2021.07-x86_64-aarch64-none-linux-gnu/bin/
2、.o 说明
obj-m += hello_world.o作用:指定需要编译的对象文件名
obj-m表示当前模块需要编译的对象文件(object:.o),后面跟的是你的源代码文件名(去掉扩展名加.o)。- 例如有
hello_world.c,此处就写hello_world.o。
INFO
编译过程实际是先把 hello_world.c 变为 hello_world.o,再根据内核框架生成最终的模块文件 hello_world.ko(Kernel Object)。
3、KDIR 说明
KDIR := /home/lckfb/TaishanPi-3-Linux/kernel-6.1作用:告诉 Makefile 用哪个内核版本的源码和头文件进行编译链接。
- 很多设备驱动模块需要与目标内核环境配套,否则无法正常加载和运行。
KDIR := /home/.../kernel-6.1指明当前使用的 Linux 内核源码主目录。
五、编译
我们进入 01_helloworld/ 目录,使用下面的命令进行编译:
make最终使用的就是生成的 .ko 文件,我们只需要将此文件复制到开发板中,进行模块加载即可:
六、驱动测试
我们将 hello_world.ko 复制到开发板中:
运行下面的命令加载驱动模块:
sudo insmod hello_world.ko有些用户可能就发现了,加载之后没有任何的反应,这是因为日志自动被系统归类隐藏了,运行下面的命令即可查看 hello_world 相关的日志:
dmesg | grep -E 'hello'到此我们第一个 hello world 驱动就完成了。