目录
[添加 LICENSE 和作者信息](#添加 LICENSE 和作者信息)
[Linux 设备号](#Linux 设备号)
前言
在上一讲内容里,字符设备驱动简介,我们介绍了linux驱动开发的3种类型、Linux 应用程序对驱动程序的调用流程、用户空间和内核空间、file_operations 的结构体(Linux 内核驱动操作函数集合)。
本讲实验,就是学习字符设备驱动的开发步骤,包括模块加载/卸载机制、设备号管理、操作函数实现等。
开发步骤
驱动模块的加载/卸载
Linux 驱动有两种运行方式:
- 将驱动编译进 Linux 内核中,这样当 Linux 内核启动的时候就会自动运行驱动程序。
- 将驱动编译成模块(Linux 下模块扩展名为.ko),在Linux 内核启动以后使用"insmod"命令加载驱动模块。
在调试驱动的时候一般都选择将其编译为模块,好处有两点:
- 编译:只编译修改好的驱动代码,不需要编译整个 Linux 代码。
- 调试:只需要加载或者卸载驱动模块,不需要重启整个系统。
模块有加载和卸载两种操作,我们在编写驱动的时候需要注册这两种操作函数。
模块的加/卸载
模块的加载和卸载注册函数如下:
cpp
module_init(xxx_init); //注册模块加载函数
module_exit(xxx_exit); //注册模块卸载函数
**module_init 函数:**用来向 Linux 内核注册一个模块加载函数,参数 xxx_init 就是需要注册的具体函数,当使用"insmod"命令加载驱动的时候, xxx_init 这个函数就会被调用。
**module_exit()函数:**用来向 Linux 内核注册一个模块卸载函数,参数 xxx_exit 就是需要注册的具体函数,当使用"rmmod"命令卸载具体驱动的时候 xxx_exit 函数就会被调用。
字符设备驱动模块加载和卸载模板如下所示:
cpp
/**
* 驱动初始化函数 - 内核模块加载时自动执行
* 返回值:0 表示成功,负数表示失败
*/
static int __init xxx_init(void)
{
/* 初始化操作通常包括:
* 1. 注册字符设备/块设备/平台设备
* 2. 申请硬件资源(IO端口、内存、中断等)
* 3. 初始化设备默认状态
* 4. 创建sysfs/debugfs调试接口
*/
printk(KERN_INFO "Driver initialized\n");
return 0; // 返回0表示初始化成功
}
/**
* 驱动退出函数 - 内核模块卸载时自动执行
*/
static void __exit xxx_exit(void)
{
/* 清理操作通常包括:
* 1. 释放设备号与注销设备
* 2. 释放硬件资源
* 3. 删除调试接口
* 4. 重置硬件状态
*/
printk(KERN_INFO "Driver removed\n");
}
/* 宏声明指定入口/出口函数 */
module_init(xxx_init); // 将xxx_init注册为模块加载函数
module_exit(xxx_exit); // 将xxx_exit注册为模块卸载函数
模块加载命令
命令 | 功能 | 使用示例 | 特点 |
---|---|---|---|
insmod |
直接加载指定 .ko 文件 |
insmod drv.ko |
- 需手动处理依赖 - 必须指定完整路径 |
modprobe |
自动加载模块及其依赖项 | modprobe drv |
- 自动解析依赖关系 - 默认搜索 /lib/modules/<kernel-version> 目录 |
关键区别:
- insmod是基础命令,不处理依赖;modprobe是智能工具,需依赖 depmod生成的模块依赖列表。
- 使用 modprobe前需确保模块已复制到标准路径(如 /lib/modules/4.1.15/)并运行 depmod -a生成依赖关系。
模块卸载命令
命令 | 功能 | 使用示例 | 特点 |
---|---|---|---|
rmmod |
卸载指定模块 | rmmod drv |
- 直接卸载,不检查依赖 |
modprobe -r |
卸载模块及其未使用的依赖 | modprobe -r drv |
- 自动卸载孤立依赖项 - 若依赖被其他模块占用则失败 |
推荐场景:
- 卸载单个模块:优先用 rmmod(简单直接)
- 彻底清理依赖:用 modprobe -r(需确保无冲突)
操作示例
安装模块到标准路径
cpp
sudo cp drv.ko /lib/modules/$(uname -r)/kernel/drivers/
sudo depmod -a # 生成模块依赖关系
加载模块(推荐modprobe)
cpp
sudo modprobe drv # 自动加载依赖
卸载模块
cpp
sudo rmmod drv # 快速卸载
# 或
sudo modprobe -r drv # 尝试卸载依赖(谨慎使用)
modprobe 命令相比 insmod 要智能一些,提供了模块的依赖性分析、错误检查、错误报告等功能,推荐使用 modprobe 命令来加载驱动。
使用 modprobe 命令可以卸载掉驱动模块所依赖的其他模块,前提是这些依赖模块已经没有被其他模块所使用,否则就不能使用 modprobe 来卸载驱动模块。
所以对于模块的卸载,还是推荐使用 rmmod 命令。
字符设备注册与注销
对于字符设备驱动而言,当驱动模块加载成功以后需要注册字符设备,同样,卸载驱动模块的时候也需要注销掉字符设备。
注册函数
字符设备的注册函数原型如下所示:
cpp
/**
* register_chrdev - 注册字符设备驱动(传统方式)
* @major: 主设备号(传入0表示自动分配)
* @name: 设备名称(显示在/proc/devices)
* @fops: 文件操作结构体指针
*
* 此函数会注册一个主设备号及对应的256个次设备号范围。
* 新驱动建议使用cdev接口代替。
*
* 返回值:
* 成功 - 返回分配的主设备号(≥0)
* 失败 - 返回负的错误码(如-ENOMEM)
*/
static inline int register_chrdev(unsigned int major, const char *name,
const struct file_operations *fops);
- major: 主设备号, Linux 下每个设备都有一个设备号,设备号分为主设备号和次设备号两部分。
- name:设备名字,指向一串字符串。
- fops: 结构体 file_operations 类型指针,指向设备的操作函数集合变量。
举例说明:
cpp
#include <linux/fs.h>
static int major; // 保存分配的主设备号
static struct file_operations fops = {
.owner = THIS_MODULE,
.open = my_open,
.read = my_read,
.write = my_write,
};
static int __init my_init(void)
{
major = register_chrdev(0, "mydev", &fops); // 自动分配主设备号
if (major < 0) {
printk(KERN_ERR "Failed to register: %d\n", major);
return major;
}
printk(KERN_INFO "Registered with major=%d\n", major);
return 0;
}
注销函数
字符设备的注销函数原型如下所示:
cpp
/**
* unregister_chrdev - 注销字符设备驱动
* @major: 要释放的主设备号(必须与注册时一致)
* @name: 设备名称(需与注册时完全匹配)
*
* 注意事项:
* 1. 必须在模块退出函数中调用
* 2. 调用前需确保所有设备节点已关闭
* 3. 不会自动释放次设备号资源
*/
static inline void unregister_chrdev(unsigned int major, const char *name);
- major: 要注销的设备对应的主设备号。
- name: 要注销的设备对应的设备名。
举例说明:
cpp
static void __exit my_exit(void)
{
unregister_chrdev(major, "mydev");
printk(KERN_INFO "Unregistered\n");
}
module_init(my_init);
module_exit(my_exit);
操作示例
一般字符设备的注册在驱动模块的入口函数 xxx_init 中进行,
字符设备的注销在驱动模块的出口函数 xxx_exit 中进行。
cpp
#include <linux/fs.h>
/* 文件操作结构体实现 */
static struct file_operations my_fops = {
.owner = THIS_MODULE,
.open = mydev_open,
.release = mydev_release,
.read = mydev_read,
.write = mydev_write,
};
static int __init my_init(void)
{
int ret;
/* 注册设备(自动分配主设备号) */
ret = register_chrdev(0, "mydev", &my_fops);
if (ret < 0) {
pr_err("Registration failed: %d\n", ret);
return ret;
}
pr_info("Registered with major=%d\n", ret);
return 0;
}
static void __exit my_exit(void)
{
/* 注销设备 */
unregister_chrdev(major_num, "mydev");
pr_info("Driver unregistered\n");
}
module_init(my_init);
module_exit(my_exit);
其中需要注意的一点就是,注册设备时要选择没有被使用的主设备号。
输入命令"cat /proc/devices"可以查看当前已经被使用掉的设备号,如图

可以列出当前系统中所有的字符设备和块设备,其中第 1 列就是设备对应的主设备号。
实现设备的具体操作函数
file_operations 结构体是设备的具体操作函数。
现在想注册一个字符设备,主设备号为 200,设备名字为"chrtest",那么我们需要初始化哪些函数?
1、能够对 chrtest 进行打开和关闭操作
- 设备打开和关闭是最基本的要求,几乎所有的设备都得提供打开和关闭的功能。因此我们需要实现 file_operations 中的 open 和 release 这两个函数。
2、对 chrtest 进行读写操作
- 假设 chrtest 这个设备控制着一段缓冲区(内存),应用程序需要通过 read 和 write 这两个函数对 chrtest 的缓冲区进行读写操作。所以需要实现 file_operations 中的 read 和 write 这两个函数。
示例代码如下:
cpp
#include <linux/module.h>
#include <linux/fs.h>
#include <linux/uaccess.h>
#define CHRDEV_NAME "chrtest" // 设备名称
#define CHRDEV_MAJOR 200 // 主设备号(需确保未被占用)
/* 设备打开函数 */
static int chrtest_open(struct inode *inode, struct file *filp)
{
/* 用户实现具体功能 */
return 0;
}
/* 设备读取函数 */
static ssize_t chrtest_read(struct file *filp, char __user *buf,
size_t count, loff_t *f_pos)
{
/* 用户实现具体功能 */
return 0;
}
/* 设备写入函数 */
static ssize_t chrtest_write(struct file *filp,
const char __user *buf,
size_t count, loff_t *f_pos)
{
/* 用户实现具体功能 */
return 0; 0
}
/* 设备关闭/释放函数 */
static int chrtest_release(struct inode *inode, struct file *filp)
{
/* 用户实现具体功能 */
return 0;
}
/* 文件操作结构体 */
static struct file_operations test_fops = {
.owner = THIS_MODULE,
.open = chrtest_open,
.read = chrtest_read,
.write = chrtest_write,
.release = chrtest_release,
};
/* 驱动入口函数 */
static int __init chrdev_init(void)
{
int ret;
// 注册字符设备(固定主设备号200)
ret = register_chrdev(CHRDEV_MAJOR, CHRDEV_NAME, &test_fops);
if (ret < 0) {
printk(KERN_ERR "Failed to register chrdev: %d\n", ret);
return ret;
}
printk(KERN_INFO "Registered chrdev: major=%d, name=%s\n",
CHRDEV_MAJOR, CHRDEV_NAME);
return 0;
}
/* 驱动出口函数 */
static void __exit chrdev_exit(void)
{
/* 注销字符设备驱动 */
unregister_chrdev(CHRDEV_MAJOR, CHRDEV_NAME);
printk(KERN_INFO "Unregistered chrdev\n");
}
/* 模块声明 */
module_init(chrdev_init);
module_exit(chrdev_exit);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("HUAX");
我们编写了四个函数:
- chrtest_open
- chrtest_read
- chrtest_write
- chrtest_release
这四个函数就是 chrtest 设备的 open、 read、 write 和 release 操作函数。
然后初始化 test_fops 的 open、 read、 write 和 release 这四个成员变量。
添加 LICENSE 和作者信息
我们需要在驱动中加入 LICENSE 信息和作者信息,其中 LICENSE 是必须添加的,否则的话编译的时候会报错,作者信息可以添加也可以不添加。
LICENSE 和作者信息的添加使用如下两个函数:
cpp
MODULE_LICENSE() //添加模块 LICENSE 信息
MODULE_AUTHOR() //添加模块作者信息
所以上面的示例代码最后有这一段:
cpp
/* 模块声明 */
module_init(chrdev_init);
module_exit(chrdev_exit);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("HUAX");
LICENSE :采用 GPL 协议。
字符设备驱动开发的完整步骤就是上面所说的4步,我们也编写好了一个完整的字符设备驱动模板,以后字符设备驱动开发都可以在此模板上进行。
Linux 设备号
设备号的组成
Linux 中每个设备都有一个设备号,设备号由主设备号和次设备号两部分组成:
- 主设备号表示某一个具体的驱动,
- 次设备号表示使用这个驱动的各个设备。
Linux 提供了一个名为 dev_t 的数据类型表示设备号, dev_t 定义在文件 include/linux/types.h 里面,定义如下:
cpp
typedef __u32 __kernel_dev_t;
typedef __kernel_dev_t dev_t;
typedef unsigned int __u32;
可以看出,dev_t 其实就是 unsigned int 类型,是一个 32 位的数据类型。其中高 12 位为主设备号, 低 20 位为次设备号。

因此 Linux系统中主设备号范围为 0~4095。
在文件 include/linux/kdev_t.h 中有关于设备号的宏,如下所示:
cpp
#define MINORBITS 20 // 次设备号占用的位数(20位)
#define MINORMASK ((1U << MINORBITS) - 1) // 次设备号掩码(低20位为1)
#define MAJOR(dev) ((unsigned int) ((dev) >> MINORBITS)) // 从dev_t提取主设备号
#define MINOR(dev) ((unsigned int) ((dev) & MINORMASK)) // 从dev_t提取次设备号
#define MKDEV(ma,mi) (((ma) << MINORBITS) | (mi)) // 组合主次设备号为dev_t
宏 | 输入 | 输出 | 典型用途 |
---|---|---|---|
MAJOR() |
dev_t 设备号 |
主设备号(12位) | 识别设备类型 |
MINOR() |
dev_t 设备号 |
次设备号(20位) | 识别设备实例 |
MKDEV() |
主设备号+次设备号 | 完整 dev_t |
设备号注册前的组合 |
设备号的分配
设备号分配主要是主设备号的分配。
静态分配设备号
使用"cat /proc/devices"命令即可查看当前系统中所有已经使用了的设备号。
驱动开发者可以静态地指定一个设备号,比如选择 200 这个主设备号。前提是这个设备号,硬件平台运行过程中没有使用。
有一些常用的设备号已经被 Linux 内核开发者给分配掉了,具体分配的内容可以查看文档 Documentation/devices.txt。
动态分配设备号
Linux 社区推荐使用动态分配设备号:在注册字符设备之前先申请一个设备号,系统会自动给你一个没有被使用的设备号,这样就避免了冲突。卸载驱动的时候释放掉这个设备号即可。
设备号的申请函数如下:
cpp
int alloc_chrdev_region(dev_t *dev, unsigned baseminor, unsigned count, const char *name);
参数 | 类型 | 作用 |
---|---|---|
dev |
dev_t * |
输出参数,保存分配到的起始设备号(主设备号 + 起始次设备号) |
baseminor |
unsigned |
起始次设备号(通常设为0) |
count |
unsigned |
需要分配的连续设备号数量 (主设备号相同,次设备号从baseminor 递增) |
name |
const char * |
设备名称(出现在/proc/devices 和内核日志中) |
返回值
-
成功 :返回
0
,并通过dev
参数返回分配的设备号 -
失败 :返回负的错误码(如
-EBUSY
、-ENOMEM
)
举例:单个设备注册
cpp
dev_t dev;
int ret;
ret = alloc_chrdev_region(&dev, 0, 1, "mydev");
if (ret < 0) {
printk(KERN_ERR "Failed to allocate device number\n");
return ret;
}
printk(KERN_INFO "Allocated major=%u, minor=%u\n", MAJOR(dev), MINOR(dev));
注销字符设备之后要释放掉设备号,设备号释放函数如下:
cpp
void unregister_chrdev_region(dev_t from, unsigned count);
参数 | 类型 | 作用 |
---|---|---|
from |
dev_t |
要释放的起始设备号(包含主设备号和次设备号) |
count |
unsigned |
连续释放的设备号数量 (从 from 开始递增次设备号) |
举例:释放单个设备号
cpp
dev_t dev;
// 分配设备号(动态主设备号,次设备号0)
alloc_chrdev_region(&dev, 0, 1, "mydev");
// 使用设备号...
// 释放设备号(需与分配时的参数对应)
unregister_chrdev_region(dev, 1);