Linux字符设备驱动模型
一、Linux字符设备简介
学习 Linux 驱动开发时,经常会接触到各种各样的设备,比如 LED 灯、按键、EEPROM、LCD 屏等等。
那这些设备和我们的上层应用程序是怎么打交道的呢?答案就是——通过驱动程序中的字符设备接口。
💡 为什么叫“字符设备”?
“字符设备(Character Device)”其实指的是一种以字节流(character stream)方式进行数据传输的设备。
比如我们平时操作的串口(UART)、EEPROM、键盘、鼠标等,数据是一字节一字节传输的。
和它相对的是“块设备”(如硬盘),那种是以“块”为单位传输的。
所以呢,像 AT24C02 这种 EEPROM,它存储和读写都是按字节来的,因此驱动设计时会采用字符设备驱动模型。
在 Linux 系统中,发展到现在为止,大体有三种不同的字符设备驱动框架或模型:
🧩 Linux中字符设备的三种驱动模型
在 Linux 系统中,字符设备驱动的实现方式主要有三种:
① 杂项设备驱动模型(Misc Device)——重点、最常用
这个模型使用起来最简单,注册也最方便,常用于一些简单的外设,比如:LED、蜂鸣器、按键等等。
很多开发板上最基础的驱动,都是通过杂项设备模型实现的。
② 早期经典字符设备驱动模型
这是比较老的写法,比如 Linux 2.4 时代经常用。
它的接口是 register_chrdev() 这种函数,现在已经比较少用了,因为功能比较有限,不能灵活分配设备号,也不太方便管理多个设备。
③ Linux 2.6 标准字符设备驱动模型
这是我们现在主流使用的模型,功能最完善,也最灵活。
它是通过 cdev_init()、cdev_add() 等函数完成注册和管理的,支持动态分配设备号,还能配合 udev 自动创建设备节点。
所以,如果我们后面要写比较完整的驱动,比如 I2C、SPI、LCD 等设备驱动,基本都会基于这个模型来实现。
🔗 三种模型的共同点
不管是哪一种模型,它们的核心结构其实都一样:
① 都需要定义一个 struct file_operations 类型的结构体。
里面定义了设备的各种操作接口,比如:
struct file_operations {.open = xxx_open, // 打开设备.read = xxx_read, // 读设备.write = xxx_write, // 写设备.release = xxx_release, // 关闭设备
};
这个结构体的作用就像“设备说明书”,告诉内核,当应用程序对设备执行 open()、read()、write() 这些系统调用时,应该调用驱动中的哪个函数。
② 另外,所有的字符设备驱动都必须在使用前注册(register),在不使用时注销(unregister)。
也就是说,我们的驱动程序要先告诉内核:
“我是谁(主设备号、次设备号)”,“我能做什么(操作函数)”。
只有注册成功,用户空间才能通过设备文件去访问我们这个设备。
⚙️ 三种模型的不同点
不同点主要有两方面:
① 使用的主设备号和次设备号不同
杂项设备通常共用主设备号 10,次设备号由系统自动分配;
标准字符设备和早期模型则需要自己申请主次设备号。
② 注册与注销的方法不同
杂项设备用
misc_register()和misc_deregister();早期模型用
register_chrdev()和unregister_chrdev();标准模型则用一系列 cdev 相关函数:
cdev_init()、cdev_add()、cdev_del()等。
📚 补充说明
注意一点,注册字符设备驱动的本质工作,其实就是:
👉 向内核注册一个主设备号 + 次设备号 + 文件操作方法。
至于其他的一些信息,比如模块描述、作者、版本号之类的,更多是附属属性,属于辅助说明。
二、Linux文件操作函数
① open —— 打开设备或创建文件
📘 头文件:
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
📗 函数原型:
int open(const char *pathname, int flags, mode_t mode);
📙 功能:
以指定的方式打开或创建一个文件(包括设备文件)。
📒 参数说明:
pathname:文件路径,比如/dev/ledflags:打开方式,比如O_RDWR、O_RDONLY、O_WRONLY、O_CREAT等mode:权限位(当创建文件时才有用,比如0666)
📘 返回值:
成功返回一个文件描述符(fd),失败返回 -1。
📖 说明:
打开 /dev/xxx 时,内核其实会调用驱动中的 open() 函数。
比如:
int fd = open("/dev/led", O_RDWR);
就会进入我们驱动里定义的 .open = led_open 函数。
② pread —— 从指定位置读取数据
📘 头文件:
#include <unistd.h>
📗 函数原型:
ssize_t pread(int fd, void *buf, size_t count, off_t offset);
📙 功能:
从文件中指定偏移量位置读取 count 个字节到缓冲区 buf。
📒 参数说明:
fd:文件描述符buf:用户缓存区地址count:期望读取的字节数offset:相对文件头的偏移位置
📘 返回值:
成功返回实际读取到的字节数,失败返回 -1。
📖 讲解示例:
比如我们要从 EEPROM 某个地址读 16 字节:
pread(fd, buffer, 16, 0x20); // 从偏移0x20开始读16字节
③ pwrite —— 向指定位置写数据
📘 头文件:
#include <unistd.h>
📗 函数原型:
ssize_t pwrite(int fd, const void *buf, size_t count, off_t offset);
📙 功能:
从 buf 中取出 count 个字节,写入文件中指定的偏移位置。
📒 参数说明:
fd:文件描述符buf:用户空间要写入的数据缓冲区count:要写入的字节数offset:写入位置偏移量
📘 返回值:
成功返回写入的字节数,失败返回 -1。
📖 讲解示例:
例如向 EEPROM 地址 0x40 写 8 字节数据:
pwrite(fd, buffer, 8, 0x40);
④ ioctl —— 控制设备行为的万能接口
📘 头文件:
#include <sys/ioctl.h>
📗 函数原型:
int ioctl(int fd, unsigned long request, ...);
📙 功能:
通过命令码控制设备执行特定的操作。
可以理解为 Linux 提供的一个“万能控制口”,让我们可以自定义命令让驱动执行各种任务。
📒 参数说明:
fd:设备对应的文件描述符request:命令码(系统预定义或用户自定义)...:可选参数,根据命令不同传不同内容
📘 返回值:
成功返回 0 或正数,失败返回 -1。
📖 讲解示例:
#define LED_ON 0x01
#define LED_OFF 0x02
ioctl(fd, LED_ON); // 打开LED
在驱动中会进入 .unlocked_ioctl = led_ioctl 函数,根据命令执行不同的操作。
⑤ poll —— 监控设备状态是否就绪
📘 头文件:
#include <poll.h>
📗 函数原型:
int poll(struct pollfd *fds, nfds_t nfds, int timeout);
📙 功能:
用来监控一个或多个文件描述符是否可读、可写或有异常。
在驱动开发中,常用于中断事件检测或按键输入等待。
📒 参数说明:
struct pollfd {int fd; // 要监测的文件描述符short events; // 期望的事件,例如 POLLIN(可读)、POLLOUT(可写)short revents; // 实际返回的事件
};
📘 返回值:
0:有就绪的文件描述符数量
0:超时
-1:出错
📖 讲解示例:
struct pollfd fds;
fds.fd = fd;
fds.events = POLLIN;
int ret = poll(&fds, 1, 3000); // 等待3秒
如果设备可读,则返回1;否则超时。
三、杂项字符设备驱动模型
特征:
杂项字符设备(misc device)主要用于一些简单、轻量级的字符设备,比如传感器(DHT11)、LED 灯、蜂鸣器等。
特点如下:
1️⃣ 服务单一设备
杂项设备一般只对应一个具体的硬件设备(例如一个传感器或控制模块),结构简单,适合小型设备驱动。
2️⃣ 主设备号固定为 10
在 Linux 中,字符设备由“主设备号 + 次设备号”唯一标识。
主设备号用于标识驱动程序类型,而次设备号标识该驱动下的具体设备。
杂项设备驱动的主设备号是固定的 10。
3️⃣ 次设备号范围:0~255
每注册一个杂项设备,会自动分配一个唯一的次设备号,用于区分不同的杂项设备实例。
4️⃣ 内核自动创建设备节点文件
驱动加载成功后,内核会自动在 /dev/ 目录下创建一个与 .name 同名的设备节点文件,例如 /dev/misc。
这意味着开发者不再需要手动执行 mknod 命令,非常方便。
缺点:
虽然简单易用,但杂项设备模型也有一定局限性:
① 主设备号固定为 10,不可更改,灵活性较差。
② 可用的次设备号仅 256 个(0~255),资源有限。
③ 驱动加载时只能创建一个设备节点文件,无法同时服务多个设备实例。
④ 节点由内核自动创建,应用层不便于统一管理 /dev 目录下的设备文件。
因此,杂项设备模型更适合“单一功能的小模块驱动”,不适合复杂设备系统。
核心数据结构:
在内核源码中,杂项设备的核心结构体为 struct miscdevice:
#include <linux/miscdevice.h>struct miscdevice {int minor; // 次设备号(0~255),也可以设置为 MISC_DYNAMIC_MINOR 由系统自动分配const char *name; // 设备名,内核会自动在 /dev/ 下创建同名设备节点文件const struct file_operations *fops; // 指向文件操作方法结构体的指针struct list_head list;struct device *parent;struct device *this_device;const struct attribute_group **groups;const char *nodename;umode_t mode;
};
其中最关键的三个字段是:
minor:次设备号
name:设备节点文件名
fops:文件操作方法结构体(
struct file_operations)
其余的字段要么是:
由内核自动维护的(如
list、this_device)或是高级功能扩展(如
groups、parent)
所以,普通驱动开发者 几乎不用管其他字段。
关键 API函数:
misc_register()
用于注册杂项设备(misc device)
① 头文件
#include <linux/miscdevice.h>② 函数原型
int misc_register(struct miscdevice *misc);③ 参数说明
参数 | 类型 | 说明 |
|---|---|---|
|
| 指向杂项设备结构体的指针,必须事先填好 |
struct miscdevice —— 杂项字符设备核心结构体
Ⅰ头文件
#include <linux/miscdevice.h>Ⅱ结构体原型
struct miscdevice {int minor;const char *name;const struct file_operations *fops;struct list_head list;struct device *parent;struct device *this_device;const char *nodename;umode_t mode; };Ⅲ成员字段详细说明
1)minor —— 次设备号
int minor;含义:
指定杂项设备使用的 次设备号。
主设备号固定为 10(
MISC_MAJOR = 10)次设备号可以为:
MISC_DYNAMIC_MINOR(=255):表示动态自动分配
固定的次设备号,例如:1、2、5 等
推荐使用:
.minor = MISC_DYNAMIC_MINOR
2)name —— 设备名
const char *name;功能:
决定设备节点名称
/dev/<name>必须唯一,否则注册失败
例如:
.name = "my_misc",对应
/dev/my_misc。
3)fops —— 文件操作结构体
const struct file_operations *fops;说明:
指向驱动的
file_operations操作集合决定驱动的功能:
系统调用
对应驱动回调
open()
fops->open
read()
fops->read
write()
fops->write
ioctl()
fops->unlocked_ioctl
release()
fops->release
示例:
.fops = &misc_fops,
4)list —— 内核用于管理 misc 的链表节点
struct list_head list;说明:
驱动无需管理
内核用于维护
/dev下所有 misc 设备的链表
5)parent —— 父设备
struct device *parent;说明:
一般填
NULL如果挂接在一个更具体的设备下,可设置此字段
6)this_device —— 注册后生成的 device 对象指针
struct device *this_device;说明:
注册后由内核填写
表示
/sys/class/misc/<name>对应的设备对象驱动不需要手动设置。
7)nodename —— 节点名称(可选)
const char *nodename;功能:
强制指定
/dev下的节点名不建议使用,一般用
.name
8)mode —— 设备权限(可选)
umode_t mode;例如:
.mode = 0666,表示
/dev/<name>可读可写。
struct file_operations —— 字符设备/杂项设备的操作方法集合
Ⅰ头文件
#include <linux/fs.h>Ⅱ结构体原型
struct file_operations {struct module *owner;int (*open)(struct inode *, struct file *);ssize_t (*read)(struct file *, char __user *, size_t, loff_t *);ssize_t (*write)(struct file *, const char __user *, size_t, loff_t *);loff_t (*llseek)(struct file *, loff_t, int);long (*unlocked_ioctl)(struct file *, unsigned int, unsigned long);int (*release)(struct inode *, struct file *); };Ⅲ结构体成员
字段 类型 功能说明 owner struct module * 通常填 THIS_MODULE,用于模块引用计数,防止模块被卸载。 open int (*)(struct inode *, struct file *) open("/dev/xxx")时调用,设备打开。read ssize_t (*)(struct file *, char __user *, size_t, loff_t *) read()调用,驱动向用户空间提供数据。write ssize_t (*)(struct file *, const char __user *, size_t, loff_t *) write()调用,用户写数据到驱动。llseek loff_t (*)(struct file *, loff_t, int) lseek()调用,移动文件指针。unlocked_ioctl long (*)(struct file *, unsigned int, unsigned long) ioctl()调用,执行用户命令。release int (*)(struct inode *, struct file *) close()调用时执行,释放资源。
④ 返回值
返回值 | 含义 |
|---|---|
| 注册成功 |
| 注册失败,通常为标准 Linux 错误码,如 |
⑤ 函数功能总结
misc_register() 是杂项设备的核心注册函数,其主要功能包括:
为设备分配次设备号(根据
misc->minor,可以动态分配)将设备加入内核的 misc 设备链表
自动创建设备节点
/dev/<name>为设备创建并初始化
struct device(即misc->this_device)若设置了
.mode,设置设备节点权限若设置了
.groups,创建 sysfs 属性完成字符设备的注册工作,并使其可被用户空间访问
简单总结:自动完成字符设备驱动大部分繁琐的注册流程。
misc_deregister()
用于注销杂项设备
① 头文件
#include <linux/miscdevice.h>② 函数原型
void misc_deregister(struct miscdevice *misc);③ 参数说明
参数 | 类型 | 说明 |
|---|---|---|
|
| 要注销的杂项设备结构体指针(必须是之前注册成功的设备) |
④ 返回值
返回值 | 含义 |
|---|---|
无返回值(void) | 始终成功执行,不需要检查返回值 |
⑤ 函数功能总结
misc_deregister() 是注销杂项设备的函数,其主要功能包括:
从 misc 设备链表中移除设备
删除
/dev/<name>设备节点注销字符设备,回收系统资源
销毁与本设备关联的
this_device设备模型对象若有 sysfs 属性(由
.groups定义),一并移除
简单总结:反向操作,撤销 misc_register() 做的所有工作。
杂项字符设备驱动模型示例代码
驱动代码misc_drv.c实现:
📝 杂项字符设备驱动代码完整流程总览
| 步骤 | 功能 | 代码位置/示例 | 作用 | 注意点 |
|---|---|---|---|---|
| 第一步 | 引入头文件 | #include <linux/module.h> 等 | 提供模块初始化/退出、文件操作结构体、杂项设备结构体、内核与用户空间安全拷贝接口 | 忘记 <linux/uaccess.h> 会导致 copy_to_user / copy_from_user 报错;缺 <linux/miscdevice.h> 会找不到 miscdevice |
| 第二步 | 构建驱动模块框架 | misc_init() / misc_exit() + module_init/module_exit | 提供模块加载和卸载的入口与出口函数 | 忘记 module_init / module_exit 会导致模块无法自动执行;MODULE_LICENSE 防止内核 taint |
| 第三步 | 构建杂项设备核心结构体并注册设备 | 1. 定义操作函数 misc_open/read/write/release2. 定义 file_operations misc_fops3. 定义 miscdevice misc_dev4. 在入口函数 misc_init() 调用 misc_register(&misc_dev) | 注册设备到内核,使内核创建 /dev/misc_demo 并绑定文件操作接口 | 操作函数必须在 file_operations 之前定义;file_operations 必须在 miscdevice 之前定义;copy_to_user / copy_from_user 防止内核访问用户空间出错 |
| 第四步 | 注销杂项设备 | misc_exit() 调用 misc_deregister(&misc_dev) | 卸载模块时移除设备节点 /dev/misc_demo 并释放内核资源 | 必须与 misc_register() 配对使用;确保模块卸载时资源释放干净 |
①引入头文件
#include <linux/module.h> // 模块初始化/退出、模块信息
#include <linux/init.h> // __init / __exit 修饰符
#include <linux/fs.h> // 文件操作结构体 file_operations
#include <linux/miscdevice.h> // 杂项设备核心结构体与注册接口
#include <linux/uaccess.h> // 提供内核与用户空间安全数据拷贝相关的函数和宏
每个头文件的作用详解
1. <linux/module.h>
提供模块加载与卸载接口(module_init、module_exit)
提供模块信息声明宏(MODULE_LICENSE 等)
👉 没有它,驱动模块无法正常加载到内核中。
2. <linux/init.h>
定义
__init和__exit修饰符用于明确入口函数和退出函数在内核内存中的位置与释放机制
👉 驱动的入口函数和出口函数必须用它的修饰符。
3. <linux/fs.h>
定义
struct file_operations包含 open/read/write/ioctl 等文件操作接口原型
👉 字符设备一定要用 file_operations,所以这个必须包含。
4. <linux/miscdevice.h>
杂项设备的核心头文件
提供
struct miscdevice定义提供
misc_register()和misc_deregister()接口
👉 不包含这个头文件就无法使用杂项设备模型。
5. <linux/uaccess.h>
提供用户空间与内核空间之间安全拷贝的函数:
copy_to_user()copy_from_user()
防止越界访问导致内核崩溃
👉 所有 read/write 中的数据传输都需要它。
②构建杂项设备驱动框架
#include <linux/module.h> // 模块初始化/退出、模块信息
#include <linux/init.h> // __init / __exit 修饰符
#include <linux/fs.h> // 文件操作结构体 file_operations
#include <linux/miscdevice.h> // 杂项设备核心结构体与注册接口
#include <linux/uaccess.h> // 提供内核与用户空间安全数据拷贝相关的函数和宏//==================== 模块入口 ====================//
static int __init misc_init(void) {return 0;
}//==================== 模块出口 ====================//
static void __exit misc_exit(void) {}module_init(misc_init);
module_exit(misc_exit);MODULE_LICENSE("GPL");
MODULE_AUTHOR("Liao");
MODULE_DESCRIPTION("Misc device demo");1.实现入口函数(module init)
static int __init misc_init(void) {return 0;
}
作用:
模块加载时执行,类似“驱动的构造函数”
后面会在这里加入:
misc_register()注册杂项设备也可以创建资源、初始化变量
目前只是一个空框架,表示正常加载。
2.实现出口函数(module exit)
static void __exit misc_exit(void) {}
作用:
模块卸载时执行,类似“驱动的析构函数”
后面会在这里加入:
misc_deregister()注销杂项设备释放资源、删除节点
当前为空框架,表示正常卸载。
3.注册入口和出口函数
module_init(misc_init);
module_exit(misc_exit);
将这两个函数注册给内核,让 insmod/rmmod 可以自动调用。
内核模块加载时自动执行
misc_init()模块卸载时自动执行
misc_exit()
4.模块元信息声明
MODULE_LICENSE("GPL");
MODULE_AUTHOR("Liao");
MODULE_DESCRIPTION("Misc device demo");作用:
声明模块许可(GPL)
作者信息
模块描述信息
保证模块加载时不被内核 taint,同时便于维护和调试
③构建杂项设备核心结构体并注册设备
#include <linux/module.h> // 模块初始化/退出、模块信息
#include <linux/init.h> // __init / __exit 修饰符
#include <linux/fs.h> // 文件操作结构体 file_operations
#include <linux/miscdevice.h> // 杂项设备核心结构体与注册接口
#include <linux/uaccess.h> // 提供内核与用户空间安全数据拷贝相关的函数和宏static char kernel_buf[64] = "Hello from kernel!";//==================== 文件操作函数 ====================//
static int misc_open(struct inode *inode, struct file *file) {printk("misc: device opened\n");return 0;
}static ssize_t misc_read(struct file *file, char __user *buf,size_t size, loff_t *offset) {printk("misc: read invoked\n");return copy_to_user(buf, kernel_buf, strlen(kernel_buf));
}static ssize_t misc_write(struct file *file, const char __user *buf,size_t size, loff_t *offset) {printk("misc: write invoked\n");memset(kernel_buf, 0, sizeof(kernel_buf));copy_from_user(kernel_buf, buf, size);printk("misc: kernel_buf = %s\n", kernel_buf);return size;
}static int misc_release(struct inode *inode, struct file *file) {printk("misc: device closed\n");return 0;
}//==================== 文件操作结构体 ====================//
static struct file_operations misc_fops = {.owner = THIS_MODULE,.open = misc_open,.read = misc_read,.write = misc_write,.release = misc_release,
};//==================== 杂项设备结构体 ====================//
static struct miscdevice misc_dev = {.minor = 255, // 固定次设备号.name = "misc_demo", // 自动生成 /dev/misc_demo.fops = &misc_fops,
};//==================== 模块入口 ====================//
static int __init misc_init(void) {int ret = misc_register(&misc_dev);if (ret)printk("misc device register failed!\n");elseprintk("misc device register success!\n");return ret;
}//==================== 模块出口 ====================//
static void __exit misc_exit(void) {}module_init(misc_init);
module_exit(misc_exit);MODULE_LICENSE("GPL");
MODULE_AUTHOR("Liao");
MODULE_DESCRIPTION("Misc device demo");1️⃣ 模块入口注册杂项字符设备
static int __init misc_init(void) {int ret = misc_register(&misc_dev);if (ret)printk("misc device register failed!\n");elseprintk("misc device register success!\n");return ret;
}
✔ 作用
将杂项设备注册到内核
内核自动创建设备节点
/dev/misc_demo后续用户空间可通过 open/read/write 访问设备
✔ 注意点
misc_register()必须在 miscdevice 结构体定义之后调用返回值非 0 表示注册失败,需要打印日志或处理
2️⃣ 定义杂项设备核心结构体 miscdevice
static struct miscdevice misc_dev = {.minor = 255, // 固定次设备号.name = "misc_demo", // 设备节点名称.fops = &misc_fops, // 文件操作集合
};
✔ 作用
minor:指定次设备号,可固定或用MISC_DYNAMIC_MINOR自动分配name:决定生成的/dev/节点名称fops:指向文件操作集合,实现设备操作接口
✔ 注意点
miscdevice 必须在入口函数调用 misc_register 之前定义
fops 指针必须指向已定义的 file_operations 结构体
3️⃣ 定义文件操作集合 file_operations
static struct file_operations misc_fops = {.owner = THIS_MODULE,.open = misc_open,.read = misc_read,.write = misc_write,.release = misc_release,
};
✔ 作用
描述驱动对用户空间操作的接口映射
内核在用户调用 open/read/write/close 时会查找此结构体执行相应函数
✔ 注意点
owner = THIS_MODULE用于防止模块被卸载成员函数必须在此之前定义或至少声明
4️⃣ 文件操作函数
static int misc_open(...) { ... }
static ssize_t misc_read(...) { ... }
static ssize_t misc_write(...) { ... }
static int misc_release(...) { ... }
//具体代码看完整版misc_drv.c
✔ 作用
misc_open:设备打开时执行misc_read:用户 read 时,将内核缓冲区拷贝到用户空间misc_write:用户 write 时,将数据拷贝到内核缓冲区misc_release:设备关闭时执行
✔ 注意点
使用
copy_to_user/copy_from_user进行内核/用户空间数据安全传输不要直接访问用户空间指针,否则可能导致内核崩溃
④注销杂项字符设备
调用 misc_deregister(),确保驱动卸载时正确删除杂项设备节点并释放系统资源。
static void __exit misc_exit(void) {misc_deregister(&misc_dev); // 注销杂项设备printk("misc device deregister success!\n");
}
✔ 作用
misc_deregister()将设备从内核的杂项设备链表中移除自动删除对应的
/dev/misc_demo设备节点释放内核资源,防止模块卸载后留下“死设备”
✔ 运行时效果
当使用
rmmod卸载模块时,misc_exit()被自动执行设备节点
/dev/misc_demo被删除内核日志中输出:
misc device deregister success!用户空间无法再访问该设备
✔ 注意点
必须在模块卸载时调用,否则内核中仍保留设备信息
注销顺序必须在模块资源释放前完成
与
misc_register()配对使用,确保驱动加载与卸载一致
⑤完整驱动代码misc_drv.c
#include <linux/module.h> // 模块初始化/退出、模块信息
#include <linux/init.h> // __init / __exit 修饰符
#include <linux/fs.h> // 文件操作结构体 file_operations
#include <linux/miscdevice.h> // 杂项设备核心结构体与注册接口
#include <linux/uaccess.h> // 提供内核与用户空间安全数据拷贝相关的函数和宏static char kernel_buf[64] = "Hello from kernel!";//==================== 文件操作函数 ====================//
static int misc_open(struct inode *inode, struct file *file) {printk("misc: device opened\n");return 0;
}static ssize_t misc_read(struct file *file, char __user *buf,size_t size, loff_t *offset) {printk("misc: read invoked\n");return copy_to_user(buf, kernel_buf, strlen(kernel_buf));
}static ssize_t misc_write(struct file *file, const char __user *buf,size_t size, loff_t *offset) {printk("misc: write invoked\n");memset(kernel_buf, 0, sizeof(kernel_buf));copy_from_user(kernel_buf, buf, size);printk("misc: kernel_buf = %s\n", kernel_buf);return size;
}static int misc_release(struct inode *inode, struct file *file) {printk("misc: device closed\n");return 0;
}//==================== 文件操作结构体 ====================//
static struct file_operations misc_fops = {.owner = THIS_MODULE,.open = misc_open,.read = misc_read,.write = misc_write,.release = misc_release,
};//==================== 杂项设备结构体 ====================//
static struct miscdevice misc_dev = {.minor = 255, // 固定次设备号.name = "misc_demo", // 自动生成 /dev/misc_demo.fops = &misc_fops,
};//==================== 模块入口 ====================//
static int __init misc_init(void) {int ret = misc_register(&misc_dev);if (ret)printk("misc device register failed!\n");elseprintk("misc device register success!\n");return ret;return 0;
}//==================== 模块出口 ====================//
static void __exit misc_exit(void) {misc_deregister(&misc_dev);printk("misc device deregister success!\n");
}module_init(misc_init);
module_exit(misc_exit);MODULE_LICENSE("GPL");
MODULE_AUTHOR("Liao");
MODULE_DESCRIPTION("Misc device demo");应用代码app.c实现:
#include <stdio.h>
#include <fcntl.h>
#include <unistd.h>int main(void) {int fd;char buf[64] = {0};fd = open("/dev/misc_demo", O_RDWR);if (fd < 0) {perror("open failed");return -1;}printf("open success, fd=%d\n", fd);// 读取内核消息read(fd, buf, sizeof(buf));printf("read from driver: %s\n", buf);// 写入数据write(fd, "Hello kernel!", 13);close(fd);return 0;
}
open()
打开设备节点
/dev/misc_demo成功返回文件描述符
fd触发驱动的
misc_open()
read()
从内核缓冲区读取数据到用户空间
触发驱动的
misc_read()内核日志打印
misc: read invoked数据被拷贝到
buf
write()
将用户空间数据写入内核缓冲区
触发驱动的
misc_write()内核缓冲区
kernel_buf更新为"Hello kernel!"内核打印更新后的数据
close()
关闭设备
触发驱动的
misc_release()
程序执行流程:
用户空间程序│ open("/dev/misc_demo")▼
驱动 misc_open()│
read(fd) ------------------> misc_read() -> copy_to_user() -> buf
write(fd, "Hello kernel!") --> misc_write() -> copy_from_user() -> kernel_buf│
close(fd) -----------------> misc_release()
Makefile编译杂项设备驱动和应用程序:
obj-m += misc_drv.o# 内核源码目录(根据你的环境修改)
KDIR ?= /home/liao/rk3399/nanopc-t4/kernel-rockchip
PWD := $(shell pwd)all:make -C $(KDIR) M=$(PWD) modulesaarch64-linux-gnu-gcc app.c -o appclean:make -C $(KDIR) M=$(PWD) cleanrm -f app
1️⃣ obj-m += misc_drv.o
作用:告诉内核构建系统,这里有一个模块
misc_drv.o需要编译为内核模块(.ko)注意点:
misc_drv.c必须在当前目录存在
2️⃣ KDIR 和 PWD
KDIR ?= /home/liao/rk3399/nanopc-t4/kernel-rockchip
PWD := $(shell pwd)KDIR:指向你的内核源码路径
PWD:当前目录路径
用途:内核模块编译时使用
make -C $(KDIR) M=$(PWD)将模块源码交给内核构建系统
3️⃣ all 目标
all:make -C $(KDIR) M=$(PWD) modulesaarch64-linux-gnu-gcc app.c -o app
make -C $(KDIR) M=$(PWD) modules
调用内核的 Make 系统编译模块
生成
misc_drv.ko
aarch64-linux-gnu-gcc app.c -o app
编译用户空间应用程序
app.c生成可执行文件
app
4️⃣ clean 目标
clean:make -C $(KDIR) M=$(PWD) cleanrm -f app
清理模块生成的中间文件和
.ko删除应用程序可执行文件
保持源码目录干净
操作流程:
① 编译驱动和应用程序
make成功后会生成:
misc_drv.ko
app
② 加载驱动模块
查看内核日志:
③ 查看自动创建设备节点
④ 运行应用程序
若出现报错没有权限
-bash: ./app: Permission denied
执行:
chmod +x app
添加可执行权限。
查看内核打印:
⑤ 卸载模块
四、早期经典字符设备驱动模型
特征:
①一个驱动可管理多个同类字符设备(群驱动)
注册一次驱动(一个主设备号)即可管理最多 256 个次设备节点,适合同类设备的统一管理。
例如:
多个鼠标
多个 LED 灯
多个数模转换器等
应用层可创建多个设备节点,例如:
/dev/mouse0
/dev/mouse1
...
② 没有独立的核心数据结构(原始模型)
不同于:
miscdevice(杂项设备)
cdev(现代模型)
早期模型没有统一结构体,只依赖 file_operations。
③ 主设备号范围有限(0~255,不含 10)
主设备号用于标识驱动本身
内核将主设备号 10 保留用于杂项设备
④次设备号范围:0~255(一次性占满)
注册成功后,驱动占用 256 个次设备号全部区间,
因此同一个主设备号下可以管理最多 256 个设备节点。
⑤内核不会自动在 /dev 下创建设备节点
必须由用户手动创建:
mknod /dev/xxx c 主设备号 次设备号
⭐ 补充:驱动代码可分层
典型的 Linux 驱动体系:
主控(controller)驱动:管理总线、设备发现
子设备(slave)驱动:控制具体硬件设备
早期字符设备驱动同样可这样拆分。
缺点:
❌(1)没有核心数据结构
不如 cdev 或 misc 规范化 → 可维护性差
❌(2)主设备号、次设备号数量有限
随着设备数增多,容易冲突,无法满足现代系统需求。
手动创建字符设备节点:
命令格式:
mknod /dev/设备名 c 主设备号 次设备号
参数解释:
| 参数 | 说明 |
|---|---|
| 设备名 | 任意命名,与驱动名无关 |
| c | 字符设备 |
| 主设备号 | 必须与 register_chrdev 返回值一致 |
| 次设备号 | 0~255 任意可用一次设备号 |
示例:
mknod /dev/early0 c 240 0
驱动本身不会创建节点,必须依靠用户或自动创建机制。
关键 API函数:
register_chrdev() —— 注册字符设备驱动
① 头文件
#include <linux/fs.h>
② 函数原型
int register_chrdev(unsigned int major, const char *name, const struct file_operations *fops);
③ 参数说明
| 参数名 | 类型 | 说明 |
|---|---|---|
major | unsigned int | 要申请的主设备号。若为 0:让内核自动分配一个可用主设备号。 |
name | const char* | 字符设备名称,显示在 /proc/devices 中,与 /dev/xxx 节点无直接关系。 |
fops | struct file_operations * | 指向已经定义好的驱动操作方法集合,如 open/read/write/release 等。 |
④ 返回值
| 返回值 | 含义 |
|---|---|
| > 0 | 成功,返回主设备号 |
| < 0 | 失败,返回负错误码 |
⑤ 功能总结
向内核注册一个字符设备,使其获得主设备号,并由 file_operations 与系统层建立绑定关系,成为正式的字符设备驱动。注册后可在 /proc/devices 看到设备名称。
unregister_chrdev() —— 注销字符设备
① 头文件
#include <linux/fs.h>
② 函数原型
void unregister_chrdev(unsigned int major, const char *name);
③ 参数说明
| 参数名 | 类型 | 说明 |
|---|---|---|
major | unsigned int | 之前 register_chrdev() 获得的主设备号 |
name | const char* | 设备名称,必须与注册时的 name 一致 |
④ 返回值
无返回值
⑤ 功能总结
注销由 register_chrdev() 注册的字符设备,释放主设备号,使其从 /proc/devices 中消失。
class_create() —— 创建一个类(class)
① 头文件
#include <linux/device.h>② 函数原型
struct class *class_create(struct module *owner, const char *name);③ 函数参数
参数名 | 类型 | 说明 |
|---|---|---|
| struct module * | 一般写 |
| const char * | 类名称,将出现在 |
④ 返回值
返回值 | 含义 |
|---|---|
struct class* | 指向新创建的 class |
ERR_PTR(err) | 失败,需要使用 |
⑤ 功能总结
创建一个系统类,并在 /sys/class/<name>/ 生成对应目录。
这是 Linux 字符设备自动创建设备节点的第一步,必须与 device_create() 配合使用。
device_create() —— 自动创建设备节点 /dev/xxx
① 头文件
#include <linux/device.h>② 函数原型
struct device *device_create(struct class *class,struct device *parent,dev_t devt,void *drvdata,const char *fmt, ...);
③ 函数参数
参数名 | 类型 | 说明 |
|---|---|---|
| struct class * | class_create() 的返回值,用于指定设备属于哪个类 |
| struct device * | 父设备,通常填 NULL |
| dev_t | 设备号,由 |
| void * | 驱动私有数据,用不到一般填 NULL |
| const char * | 设备节点的名称格式(例如 |
| 可变参数 | 配合 fmt 用,可忽略 |
④ 返回值
返回值 | 含义 |
|---|---|
struct device* | 成功 返回设备结构体指针 |
ERR_PTR(err) | 失败 |
⑤ 功能总结
根据设备号 devt 自动创建 /dev/xxx 设备节点,
无需 mknod,是现代 Linux 驱动创建设备节点的主要方式。
class_destroy() —— 删除类
① 头文件
#include <linux/device.h>② 函数原型
void class_destroy(struct class *cls);③ 参数
参数名 | 类型 | 说明 |
|---|---|---|
| struct class * | class_create() 的返回值 |
④ 返回值
无返回值
⑤ 功能总结
删除 class 对象,从 /sys/class/xxx/ 中移除对应目录。
device_destroy() —— 删除 /dev/xxx 节点
① 头文件
#include <linux/device.h>② 函数原型
void device_destroy(struct class *class, dev_t devt);③ 参数(详细)
参数名 | 类型 | 说明 |
|---|---|---|
| struct class * | class_create() 的返回值 |
| dev_t | 设备号,一般 |
④ 返回值
无
⑤ 功能总结
删除通过 device_create() 创建的设备节点 /dev/xxx,同时移除 sysfs 下对应的设备信息。
早期经典字符设备驱动模型示例代码:
手动添加设备节点版本(mknod 手动创建 /dev 节点)
驱动代码(early_chr.c)— 手动 mknod 版本
// early_chr.c —— 早期经典字符设备驱动(手动 mknod 版)#include <linux/module.h>
#include <linux/init.h>
#include <linux/fs.h>//===================== 文件操作方法 =====================//static int early_open(struct inode *inode, struct file *file)
{printk("early_open() 被调用\n");return 0;
}static ssize_t early_read(struct file *file, char __user *buf,size_t size, loff_t *offset)
{printk("early_read() 被调用\n");return 0;
}static ssize_t early_write(struct file *file, const char __user *buf,size_t size, loff_t *offset)
{printk("early_write() 被调用\n");return size; // 假装写成功
}static int early_release(struct inode *inode, struct file *file)
{printk("early_release() 被调用\n");return 0;
}//===================== 文件操作结构体 =====================//static struct file_operations early_fops = {.owner = THIS_MODULE,.open = early_open,.read = early_read,.write = early_write,.release = early_release,
};//===================== 全局变量 =====================//
static int major = 0; // 主设备号(0 表示自动分配)//===================== 入口函数 =====================//static int __init early_init(void)
{// 注册字符设备驱动major = register_chrdev(0, "early_chr", &early_fops);if (major < 0) {printk("register_chrdev 失败!\n");return major;}printk("早期字符设备驱动注册成功!主设备号 = %d\n", major);printk("请手动创建设备节点: mknod /dev/early_chr c %d 0\n", major);return 0;
}//===================== 出口函数 =====================//static void __exit early_exit(void)
{unregister_chrdev(major, "early_chr");printk("early_chr 驱动已卸载\n");
}module_init(early_init);
module_exit(early_exit);
MODULE_LICENSE("GPL");
应用层测试程序(app.c)
// app.c — 用户层测试程序#include <stdio.h>
#include <fcntl.h>
#include <unistd.h>int main()
{int fd;char buf[10] = {0};// 打开设备文件fd = open("/dev/early_chr", O_RDWR);if (fd < 0) {printf("open /dev/early_chr 失败!\n");return -1;}printf("打开成功,fd = %d\n", fd);read(fd, buf, 1);write(fd, "A", 1);close(fd);return 0;
}
Makefile(驱动 + 应用一键编译)
obj-m += early_chr.o# 内核源码路径
KDIR ?= /home/liao/rk3399/nanopc-t4/kernel-rockchip # ←根据你的系统修改 !!!PWD := $(shell pwd)all:make -C $(KDIR) M=$(PWD) modulesaarch64-linux-gnu-gcc app.c -o appclean:make -C $(KDIR) M=$(PWD) cleanrm -f app
操作流程:
① 编译驱动和应用程序
在 Ubuntu 下执行:
make
成功后会生成:
early_chr.ko(驱动)app(应用程序)
②安装驱动模块
查看主设备号:
③手动创建设备节点
查看自动创建设备节点:
④运行测试程序
查看 dmesg:
⑤卸载驱动
自动创建设备节点版本
驱动代码(early_chr_auto.c)— 自动创建设备节点
// early_chr_auto.c — 自动创建设备节点版本(class_create + device_create)#include <linux/module.h>
#include <linux/fs.h>
#include <linux/device.h>static int major = 0;
static struct class *early_class;
static struct device *early_device;/************************ 文件操作方法 ************************/static int early_open(struct inode *inode, struct file *file)
{printk("early_open() 调用\n");return 0;
}static ssize_t early_read(struct file *file, char __user *buf,size_t size, loff_t *offset)
{printk("early_read() 调用\n");return 0;
}static ssize_t early_write(struct file *file, const char __user *buf,size_t size, loff_t *offset)
{printk("early_write() 调用\n");return size;
}static int early_release(struct inode *inode, struct file *file)
{printk("early_release() 调用\n");return 0;
}/************************ 文件操作结构体 ************************/static struct file_operations early_fops = {.owner = THIS_MODULE,.open = early_open,.read = early_read,.write = early_write,.release = early_release,
};/************************ 模块入口函数 ************************/static int __init early_init(void)
{// 注册字符设备,自动分配主设备号major = register_chrdev(0, "early_chr_auto", &early_fops);if (major < 0) {printk("register_chrdev 失败\n");return major;}printk("早期字符设备驱动(自动节点)注册成功,主设备号 = %d\n", major);// 创建设备类 /sys/class/early_chr_class/early_class = class_create(THIS_MODULE, "early_chr_class");if (early_class != NULL) {printk("成功创建设备类 /sys/class/early_chr_class/\n");}// 创建设备节点 /dev/early_chr_autoearly_device = device_create(early_class, NULL,MKDEV(major, 0),NULL,"early_chr_auto");if (early_device != NULL) {class_destroy(early_class);printk("成功创建设备节点 /dev/early_chr_auto\n");}return 0;
}/************************ 模块出口函数 ************************/static void __exit early_exit(void)
{device_destroy(early_class, MKDEV(major, 0));class_destroy(early_class);unregister_chrdev(major, "early_chr_auto");printk("early_chr_auto 驱动已卸载\n");
}module_init(early_init);
module_exit(early_exit);
MODULE_LICENSE("GPL");
应用程序(app.c)
与手动节点版相同,只是设备名字不同:
#include <stdio.h>
#include <fcntl.h>
#include <unistd.h>int main()
{int fd;char c;fd = open("/dev/early_chr_auto", O_RDWR);if (fd < 0) {printf("open /dev/early_chr_auto 失败!\n");return -1;}printf("打开成功 fd=%d\n", fd);read(fd, &c, 1);write(fd, "A", 1);close(fd);return 0;
}
Makefile
obj-m += early_chr_auto.oKDIR ?= /home/liao/rk3399/nanopc-t4/kernel-rockchip # ←根据你的系统修改 !!!
PWD := $(shell pwd)all:make -C $(KDIR) M=$(PWD) modulesaarch64-linux-gnu-gcc app.c -o appclean:make -C $(KDIR) M=$(PWD) cleanrm -f app
操作流程参考上述例程。
五、Linux2.6标准字符设备模型
Linux 2.6 开始,字符设备驱动的注册方式全面升级,引入了 cdev 核心结构体 + 动态设备号机制。这是目前 Linux 内核最标准、最规范、最推荐的字符设备驱动模型。
特征:
标准字符设备模型与之前两个模型(杂项模型、早期经典模型)相比,具有以下重要特征:
① 一个驱动可以服务多个设备(批量设备)
一个驱动可一次性注册多个连续的设备号,例如:
驱动有能力管理 10000 个鼠标
主设备号固定为一个
次设备号连续分配 0~9999
这使得驱动非常适合管理 大量相同类型的设备。
② 使用 dev_t(32 位)描述设备号
dev_t 本质为 u32,被拆分为:
位宽 | 含义 |
|---|---|
高 12 位 | 主设备号(0~4095,不包含 10——杂项设备保留) |
低 20 位 | 次设备号(0~1,048,575≈1M) |
因此:
主设备号范围更大
次设备号从原来的 0~255 扩展到 0~1M,更能容纳大量设备
③ 次设备号必须连续分配(不可分散)
标准字符设备注册(无论动态/静态)都要求:
仅能注册一段连续区间的次设备号
不能分散注册多个离散的次设备号段
如果要注册多个离散段,需要多次 register/alloc,但每段必须连续。
④ 驱动加载后不会自动创建 /dev 节点
与杂项设备不同:
内核不会自动创建 /dev 设备文件
由用户层通过 mknod 手动创建:
例如:
sudo mknod /dev/demo c 240 0当然,在较新内核中,可使用 class_create + device_create 自动创建节点,不过本模型强调的是“标准 cdev 驱动模型”,它本质上不负责自动创建设备文件。
核心数据结构:
内核头文件:
#include <linux/cdev.h>
struct cdev — 标准字符设备的核心结构体
struct cdev {struct kobject kobj; // 内核对象,用于 sysfs 管理,内核自动维护struct module *owner; // 指向本驱动模块const struct file_operations *ops; // 文件操作方法struct list_head list; // 内核链表,自动维护dev_t dev; // 起始设备号(包含主次设备号)unsigned int count; // 该驱动占用的连续设备号个数
};
这里必须由驱动程序填充的成员:
.ops.dev.count
结构体本身由开发者维护,其他复杂结构由内核自动处理。
关键API函数:
cdev_alloc()------在堆空间动态申请一个struct cdev对象空间
①头文件
#include <linux/cdev.h>
②函数原型
struct cdev *cdev_alloc(void);
③函数参数
无
④返回值
| 返回值 | 含义 |
|---|---|
struct cdev * | 返回动态申请的 cdev 对象指针 |
NULL | 申请失败(可能内存不足) |
struct cdev 结构体 ------标准字符设备的核心结构体
struct cdev {struct kobject kobj; // 表示内核设备对象,提供 sysfs 支持struct module *owner; // 拥有此 cdev 的模块(一般是 THIS_MODULE)const struct file_operations *ops; // 文件操作方法struct list_head list; // cdev 链表dev_t dev; // 设备号(包含主+次设备号)unsigned int count; // 次设备号数量 };1. struct kobject kobj
作用:
把 cdev 纳入 Linux 核心对象系统(kobject、kset 等)
让字符设备可以在 sysfs 中出现,如:
/sys/dev/char/<major>:<minor>/提供引用计数(防止设备被错误释放)
📌 简单理解:
kobject 是内核万物之父,所有设备对象都需要它。
2. struct module *owner
常见使用方式:
cdev->owner = THIS_MODULE;
用途:
避免在设备还在使用时卸载模块
若用户打开
/dev/xxx,内核会增加此模块引用计数文件关闭后才允许卸载
📌 没有 owner,模块可能在使用途中被卸载,导致内核崩溃(Oops)。
3. const struct file_operations *ops
这是字符设备的灵魂,用户对设备的所有操作都要从这里进去。
典型结构:
struct file_operations {int (*open)(...);ssize_t (*read)(...);ssize_t (*write)(...);int (*release)(...);long (*unlocked_ioctl)(...);... };
;📌 ops 把 cdev 连接到驱动操作函数。
没有 ops,字符设备就是一个“空壳”。
4. struct list_head list
在系统中可能存在多个 cdev,内核使用链表管理它们
这个字段使 cdev 支持多个次设备号(count > 1)
5. dev_t dev
保存字符设备的设备号:
MKDEV(major, minor)包含:
主设备号(major)
次设备号(minor)
例如:
dev_t dev = MKDEV(240, 0);这是驱动在
/dev中的唯一标识。
6. unsigned int count
表示 cdev 所涵盖的连续次设备号数量
例如:
register_chrdev_region(MKDEV(240, 0), 3, "mydev");则你可以创建:
240:0
240:1
240:2
count = 3。📌 若 count 是 1,则是单一设备号。
⑤函数功能
cdev_alloc() 的作用是:在堆空间动态申请并初始化一个 struct cdev 对象。
//内核源码实现(简化理解):struct cdev *cdev_alloc(void)
{struct cdev *p = kzalloc(sizeof(struct cdev), GFP_KERNEL);if (p) {INIT_LIST_HEAD(&p->list);kobject_init(&p->kobj, &ktype_cdev_dynamic);}return p;
}具体做了哪些事:
使用 kzalloc 申请 cdev 结构体内存并清零
初始化内部链表(INIT_LIST_HEAD)
初始化 kobject(用于 sysfs 管理)
返回可直接使用的 cdev 对象
该 cdev 对象接下来通常需要:
设置 ops(文件操作方法)
调用
cdev_add()加入内核卸载时调用
cdev_del()并手动kfree()
register_chrdev_region()------静态注册一个指定范围的设备号
①头文件
#include <linux/fs.h>
②函数原型
int register_chrdev_region(dev_t from, unsigned count, const char *name);
③函数参数
| 参数 | 类型 | 说明 |
|---|---|---|
from | dev_t | 起始设备号(包含主设备号 + 次设备号)。由 MKDEV(major, minor) 构造 |
count | unsigned | 请求的连续设备号数量 |
name | const char* | 设备或驱动名称,会显示在 /proc/devices 中 |
④返回值
| 返回值 | 含义 |
|---|---|
0 | 注册成功 |
<0 | 注册失败(常见:-EBUSY,表示设备号被占用) |
⑤函数功能
register_chrdev_region() 用于静态注册一个指定范围的设备号。
即:
你自己指定主设备号与起始次设备号
内核验证该设备号区间是否空闲
空闲则注册,否则失败
适用于 你明确知道要使用的主设备号 的情况:
厂家要求固定主设备号
驱动文档规定主设备号
某些商业驱动规定固定 major
需要和老版本驱动兼容
⚠️ 如果不知道该用哪个主设备号,不要用这个 API,应使用
alloc_chrdev_region()。
⑥示例:
dev_t devno;
int ret;/* 指定主设备号 200,次设备号从 0 开始,连续申请 2 个设备号 */
devno = MKDEV(200, 0);ret = register_chrdev_region(devno, 2, "mydriver");
if (ret < 0) {printk("register_chrdev_region failed!\n");return ret;
}printk("register success: major=%d minor=%d\n",MAJOR(devno), MINOR(devno));
⑦常见错误与注意事项
Ⅰ 主设备号不能为 10
主设备号 10 由 杂项设备模型 exclusively 使用。
Ⅱ设备号冲突(-EBUSY)
若主设备号已被其他驱动占用,会注册失败。
Ⅲ count 必须为连续数量
不能申请离散设备号,只能申请一个区间。
alloc_chrdev_region()------自动分配主设备号,并注册一段连续的字符设备号。
①头文件
#include <linux/fs.h>
②函数原型
int alloc_chrdev_region(dev_t *dev, unsigned baseminor,unsigned count, const char *name);
③函数参数
| 参数 | 类型 | 说明 |
|---|---|---|
dev | dev_t* | 输出参数,用于存放分配得到的起始设备号(主设备号 & 次设备号) |
baseminor | unsigned | 起始次设备号,一般设为 0 |
count | unsigned | 申请的连续设备号数量 |
name | const char* | 驱动名称,用于 /proc/devices 中显示 |
④返回值
| 返回值 | 含义 |
|---|---|
0 | 注册成功 |
<0 | 注册失败,通常为 -EBUSY 或 -EINVAL |
⑤函数功能
alloc_chrdev_region() 的作用:自动分配主设备号,并注册一段连续的字符设备号。
它完成两件重要的事:
内核自动分配主设备号
无需指定主设备号,也不用担心与系统已有驱动冲突。
例如:
dev_t dev;
alloc_chrdev_region(&dev, 0, 2, "mydev");
假设系统分配的主设备号为 245,则:
dev = MKDEV(245, 0)
为驱动预留 count 个连续的次设备号
如果 count=2,则系统同时分配:
主设备号 | 次设备号 |
|---|---|
245 | 0 |
245 | 1 |
将结果保存到 dev 中
可以通过宏解析主次设备号:
int major = MAJOR(dev);
int minor = MINOR(dev);
适用于:
标准字符设备驱动模型(Linux 2.6+)强烈推荐使用
当你不确定一个主设备号是否被占用时使用
当驱动中需要多个设备节点时(如 led0、led1)
⑥示例:
dev_t devno;
int ret;/* 申请 1 个设备号,起始次设备号 0 */
ret = alloc_chrdev_region(&devno, 0, 1, "led");
if (ret < 0) {printk("alloc_chrdev_region failed\n");return ret;
}printk("major = %d, minor = %d\n", MAJOR(devno), MINOR(devno));
cdev_init()------初始化一个 struct cdev 结构体,并将其与 file_operations 绑定
①头文件
#include <linux/cdev.h>
②函数原型
void cdev_init(struct cdev *cdev, const struct file_operations *fops);
③函数参数
| 参数 | 类型 | 说明 |
|---|---|---|
cdev | struct cdev * | 指向要初始化的 cdev 结构体对象 |
fops | const struct file_operations * | 指向文件操作集(open/read/write/ioctl 等) |
④返回值
| 返回值 | 含义 |
|---|---|
| 无返回值 | 该函数永远不会失败,但必须保证 cdev 指针有效 |
⑤函数功能
cdev_init() 用于初始化一个 struct cdev 结构体,并将其与 file_operations 绑定。
主要功能:
将
cdev->ops = fops设置 owner 字段为
THIS_MODULE(在 cdev_add 时完成)初始化 cdev 的内部链表
为后续的
cdev_add()做准备
❗注意:cdev_init 不会分配内存,你必须确保 cdev 已经存在(静态或动态分配)。
使用场景:
当你已经分配好了一个 struct cdev(通常是静态或使用 kmalloc 申请)
在调用
cdev_add()注册前,必须先进行初始化旧式字符设备模型的核心步骤之一
⑥示例
① 静态分配方式:
static struct cdev my_cdev;cdev_init(&my_cdev, &my_fops);
my_cdev.owner = THIS_MODULE;cdev_add(&my_cdev, devno, 1);
② 动态分配方式:
struct cdev *cdev;cdev = cdev_alloc(); // 申请结构体空间
cdev_init(cdev, &my_fops);
cdev->owner = THIS_MODULE;cdev_add(cdev, devno, 1);
cdev_add()------将初始化后的 cdev 正式注册到内核
①头文件
#include <linux/cdev.h>
②函数原型
int cdev_add(struct cdev *cdev, dev_t dev, unsigned int count);
③函数参数
| 参数 | 类型 | 说明 |
|---|---|---|
cdev | struct cdev * | 已初始化的 cdev 对象(必须先 cdev_init) |
dev | dev_t | 起始设备号(包含主+次设备号) |
count | unsigned int | 需要注册的连续设备号数量 |
④返回值
| 返回值 | 含义 |
|---|---|
0 | 注册成功 |
< 0 | 注册失败(例如:设备号冲突、cdev 指针无效等) |
⑤函数功能
⭐ cdev_add() 用于将初始化后的 cdev 正式注册到内核,使其成为可用的字符设备。
其主要功能包括:
注册 cdev 对象到内核
绑定设备号 dev(主次设备号)
启用该字符设备,让用户空间程序可以 open/read/write
内核会在
/proc/devices中显示该设备
这是字符设备驱动真正生效的关键步骤。
没有 cdev_add(),驱动等于没注册。
⑥示例:
1)静态 cdev 使用示例
static struct cdev my_cdev;dev_t dev_no; // 已经通过 register/alloc_chrdev_region 获得的设备号cdev_init(&my_cdev, &my_fops);
my_cdev.owner = THIS_MODULE;if (cdev_add(&my_cdev, dev_no, 1) < 0) {printk("cdev_add error\n");
}
2)动态 cdev 使用示例
struct cdev *cdev;cdev = cdev_alloc();
cdev_init(cdev, &my_fops);
cdev->owner = THIS_MODULE;if (cdev_add(cdev, dev_no, 2) < 0) {printk("cdev_add failed\n");
}
⑦注意事项
⚠ 必须先 cdev_init() 再 cdev_add(),顺序不能反
否则内核会崩溃或 oops。
正确顺序必须是:
① 分配设备号 register_chrdev_region / alloc_chrdev_region ② 分配 cdev (静态 or cdev_alloc) ③ 初始化 cdev cdev_init() ④ 添加 cdev cdev_add()卸载驱动时:
① cdev_del() ② unregister_chrdev_region()
⚠ 设备号 dev 必须是有效且已申请的
否则会返回 -EINVAL / -EBUSY。
⚠ count 表示连续设备号数量
例如 count=2 表示注册 dev、dev+1 两个设备号。
⚠ 设置 owner 非常重要:
cdev->owner = THIS_MODULE;否则模块可能被正在使用时卸载,导致 crash。
cdev_del()------将已经注册到内核的 cdev 从内核中注销
①头文件
#include <linux/cdev.h>
②函数原型
void cdev_del(struct cdev *cdev);
③函数参数
| 参数 | 类型 | 说明 |
|---|---|---|
cdev | struct cdev * | 需要从内核注销的 cdev 对象指针(之前已通过 cdev_add 注册) |
④返回值
| 返回值 | 含义 |
|---|---|
| 无(void) | 此函数没有返回值 |
虽然没有返回值,但如果传入无效指针,内核可能产生 OOPS,因此需要确保 cdev 有效。
⑤函数功能
⭐ cdev_del() 用于将已经注册到内核的 cdev 从内核中注销。
具体功能:
将 cdev 从内核字符设备表中删除
释放 cdev 对应的内核资源
使设备不可再被用户空间访问
和 cdev_add() 相反,是卸载驱动时必须执行的清理动作
必须在驱动退出函数中调用,否则会出现设备号资源泄漏。
⑥示例:
● 驱动入口(注册字符设备)
static struct cdev my_cdev;
dev_t dev_no;alloc_chrdev_region(&dev_no, 0, 1, "mydev");cdev_init(&my_cdev, &my_fops);
my_cdev.owner = THIS_MODULE;cdev_add(&my_cdev, dev_no, 1);
● 驱动退出(注销字符设备)
cdev_del(&my_cdev);
unregister_chrdev_region(dev_no, 1);
⑦注意事项
✔ 必须确保 cdev_add() 已经成功执行过,才能调用 cdev_del()
✔ 通常只删除一次,否则可能造成内核崩溃
✔ 如果 cdev 是通过 cdev_alloc() 动态申请的,需要在 cdev_del() 后手动 kfree(cdev)
unregister_chrdev_region()------注销之前注册的一段设备号区域
①头文件
#include <linux/fs.h>
②函数原型
void unregister_chrdev_region(dev_t from, unsigned int count);
③函数参数
| 参数 | 类型 | 说明 |
|---|---|---|
from | dev_t | 起始设备号(主设备号 + 次设备号) |
count | unsigned int | 需要注销的连续设备号数量 |
④返回值
无
⑤函数功能
⭐ unregister_chrdev_region() 用于注销之前注册的一段设备号区域。
具体功能:
释放之前申请的设备号(无论是 alloc_chrdev_region 还是 register_chrdev_region 获得的)
将设备号从内核设备表中移除
必须在驱动退出函数中调用,否则设备号会泄漏
⑥示例:
● 驱动入口(申请设备号)
dev_t dev_no;
alloc_chrdev_region(&dev_no, 0, 2, "mydev");
● 驱动退出(释放设备号)
unregister_chrdev_region(dev_no, 2);
⑦常见错误及注意事项
| 错误 | 原因 |
|---|---|
| 不调用 unregister_chrdev_region | 设备号泄漏,导致下次加载驱动失败 |
| count 填写错误 | 部分设备号未释放,导致冲突 |
| from 不是申请得到的设备号 | 会释放错误区域,引发不可预测后果 |
