Linux 字符设备驱动基础开发详解(1)
“书接上回 Linux 应用程序如何操作底层硬件。 在上一小节中我们分析了 Linux 操作底层硬件的原理,以及什么是字符设备。本节将具体讲解字符设备驱动的开发流程与关键步骤之-如何在内核空间中加载/卸载设备驱动?。
01 学习重点
在明确字符设备概念及其运行空间(内核空间)的基础上,接下来我们需要焦两个核心问题:

- 如何初始化外设? —— 与裸机开发类似,Linux 驱动同样需要初始化相应外设寄存器(这个问题将在下一节中详细学习)。
02 驱动的加载与卸载
两种加载方式
| | |
|---|
| 编译进内核 | | |
| 编译成模块 | | 内核启动后用 insmod / rmmod 动态加载/卸载,修改驱动只需重新编译模块,无需重编整个内核 |
⚠️ 模块依赖与正确用法
内核中可能存在多个驱动,且模块之间会有依赖(互相调用,类似 include、import)。
- 仅用 insmod:无法自动处理依赖,依赖未加载时可能失败。
- � 推荐*:使用
modprobe 加载模块。它会解析依赖,按顺序加载当前模块及其依赖。 modprobe 默认在 /lib/modules/<kernel-version> 下查找模块(如内核 4.1.15 则查找 /lib/modules/4.1.15)。- 卸载建议:用
rmmod 卸载当前模块;慎用 modprobe -r,它会连同依赖一起卸载,可能影响其他模块。
入口与出口:init 与 release
驱动需显式指定加载时入口和卸载时出口,对应「初始化」和「释放」逻辑:
module_init(xxx_init):指定模块加载函数。执行 insmod 时,内核会调用 xxx_init()。module_exit(xxx_exit):指定模块卸载函数。执行 rmmod 时,内核会调用 xxx_exit()。
// 驱动入口函数:加载时执行
staticint __init xxx_init(void)
{
// 入口函数具体内容(如注册设备、申请资源)
return0;
}
// 驱动出口函数:卸载时执行
staticvoid __exit xxx_exit(void)
{
// 出口函数具体内容(如注销设备、释放资源)
}
// 将上面两个函数指定为驱动的入口和出口
module_init(xxx_init);
module_exit(xxx_exit);
03 设备与设备号
一切皆文件
Linux 遵循 「一切皆文件」:硬件设备在系统中以设备节点的形式呈现(设备树等机制在后续说明)。
应用层通过操作设备文件来访问设备,背后由 file_operations 与驱动对接。
设备号:主设备号 + 次设备号
为便于管理,每个设备都有设备号,由两部分组成:
- 内核用
dev_t(32 位)表示设备号:高 12 位为主设备号,低 20 位为次设备号。 - 因此主设备号范围为 0~4095,且应不与其他驱动冲突。
- 查看已占用设备号:**
cat /proc/devices**(静态分配时选号前建议先查)。
静态分配 vs 动态分配
- 静态分配:自己指定主设备号(如 200),需确保未被占用。
- 动态分配:由内核分配主设备号,避免冲突,推荐在可插拔或通用驱动中使用。
/*
* 动态申请设备号
* @param dev : 输出参数,保存申请到的设备号
* @param baseminor: 次设备号起始值(常为 0)
* @param count : 申请的设备数量(一段连续号,主设备号相同,次设备号从 baseminor 递增)
* @param name : 设备名(出现在 /proc/devices)
*/
intalloc_chrdev_region(dev_t *dev, unsigned baseminor, unsigned count, constchar *name);
/*
* 注销字符设备后释放设备号
* @param from : 要释放的起始设备号
* @param count: 从 from 起要释放的设备号数量
*/
voidunregister_chrdev_region(dev_t from, unsigned count);
为了让快速上手,我们本篇先使用设备号静态分配, 以及 最简单的旧版静态注册接口 register_chrdev。
04 file_operations:应用与驱动的桥梁
file_operations 是连接用户空间系统调用与内核驱动实现的桥梁。
用户态调用 open、read、write、close 等,最终会落到该结构体中对应的函数指针。
因此在模块加载函数中需要注册字符设备(绑定设备号与 file_operations),在模块卸载函数中需要注销设备。
注册 / 注销接口(旧接口示例)
/**
* 注册字符设备(指定主设备号)
* @param major : 主设备号
* @param name : 设备名
* @param fops : 指向 file_operations 的指针
*/
staticinlineintregister_chrdev(unsignedint major, constchar *name,
const struct file_operations *fops);
/**
* 注销字符设备
* @param major : 要注销的设备的主设备号
* @param name : 要注销的设备名
*/
staticinlinevoidunregister_chrdev(unsignedint major, constchar *name);
05 实现操作函数与完整流程示例
驱动需要实现 open、read、write、release 等,并填入 **file_operations**,再在 init 中注册、在 exit 中注销。
操作函数与 file_operations 定义
// 打开设备
staticintchrtest_open(struct inode *inode, struct file *filp){ return0; }
// 从设备读取
staticssize_tchrtest_read(struct file *filp, char __user *buf, size_t cnt, loff_t *offt)
{
return0;
}
// 向设备写入
staticssize_tchrtest_write(struct file *filp, constchar __user *buf, size_t cnt, loff_t *offt)
{
return0;
}
// 关闭/释放设备
staticintchrtest_release(struct inode *inode, struct file *filp){ return0; }
// 连接用户空间与内核驱动的核心结构
staticstructfile_operationstest_fops = {
.owner = THIS_MODULE,
.open = chrtest_open,
.read = chrtest_read,
.write = chrtest_write,
.release = chrtest_release,
};
在 init/exit 中注册与注销
#define CHRDEVBASE_MAJOR 200 // 主设备号(需保证未占用)
#define CHRDEVBASE_NAME "chrdevbase"// 设备名
staticint __init xxx_init(void)
{
int retvalue = 0;
retvalue = register_chrdev(CHRDEVBASE_MAJOR, CHRDEVBASE_NAME, &test_fops);
if (retvalue < 0) {
printk("driver register failed\n");
return retvalue;
}
return0;
}
staticvoid __exit xxx_exit(void)
{
unregister_chrdev(CHRDEVBASE_MAJOR, CHRDEVBASE_NAME);
printk("chrdevbase_exit()\n");
}
module_init(xxx_init);
module_exit(xxx_exit);
模块信息(必选与可选)
// LICENSE 必须声明,否则编译会报错
MODULE_LICENSE("GPL");
// 作者信息可选
MODULE_AUTHOR("xxxxx");
06 小结
| |
|---|
| 实现 入口xxx_init、出口xxx_exit,并用 module_init / module_exit 注册 |
| 实现 open / read / write / release 等,并填入 file_operations |
| 在 init 中 注册字符设备(register_chrdev 或新接口 + 设备号),在 exit 中 注销(unregister_chrdev 等) |
| 合理选择设备号(静态时用 cat /proc/devices 避免冲突,或使用 动态分配) |
| 添加 **MODULE_LICENSE("GPL")**,必要时加 MODULE_AUTHOR |
按上述步骤即可完成一个最简字符设备驱动的加载、卸载与读写框架,为后续具体硬件操作打下基础。