为什么要使用 cdev
在 Linux 中,驱动的核心思想是:一切设备皆文件,用户程序访问设备时,必须运行
open("/dev/xxx")
read()
write()
而 Linux 内核通过 VFS(虚拟文件系统)把这些操作转发给驱动,真正完成这个连接的,就是 file_operations,而 cdev 的作用是将 file_operations 挂到一个设备号上,可以理解为
用户程序
│
│ open/read/write
▼
VFS
│
▼
file_operations
│
▼
cdev
│
▼
驱动代码
综上所述,cdev 本质上是字符设备在内核中的一个对象。
主流字符设备驱动框架
一个完整驱动一般包含以下步骤:
1 分配设备号
2 初始化 cdev
3 注册 cdev
4 创建设备节点
5 实现 file_operations
6 卸载驱动释放资源
整体流程如下
module_init
│
▼
alloc_chrdev_region
│
▼
cdev_init
│
▼
cdev_add
│
▼
class_create
│
▼
device_create
卸载流程如下:
device_destroy
class_destroy
cdev_del
unregister_chrdev_region
新增核心数据结构
device numberr 设备号
Linux 使用设备号区分设备,结构:
dev_t = 主设备号+次设备号
例如
major = 200
minor = 0
获取方式
1 2
| dev_t devid; alloc_chrdev_region(&devid, 0, 1, "mychrdev");
|
cdev
字符设备对象 struct cdev 保存了 file_operations、设备号、内核设备管理信息。定义好 cdev 变量以后就要使用 cdev_init 函数对其进行初始化,此函数原型如下
void cdev_init(struct cdev *cdev, const struct file_operations *fops)
参数 cdev 就是要初始化的 cdev 结构体变量,参数 fops 就是字符设备文件操作函数合集。cdev_add 函数用于向 Linux 系统添加字符设备(cdev 结构体变量),首先使用 cdev_init 函数完成对 cdev 结构体变量的初始化,然后使用 cdev_add 函数向 Linux 系统添加这个字符设备。cdev_add 函数原型如下:
int cdev_add(struct cdev *p, dev_t dev, unsigned count)
参数 p 指向要添加的字符设备(cdev 结构体变量),参数 dev 就是设备所用的设备号,参数 count 是要添加的设备数量。
卸载驱动的时候一定要使用 cdev_del 函数从 Linux 内核中删除相应的字符设备,cdev_del 的函数原型如下,参数 p 就是要删除的字符设备。
void cdev_del(struct cdev *p)
示例代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89
| #include <linux/module.h> #include <linux/fs.h> #include <linux/cdev.h> #include <linux/device.h>
#define DEV_NAME "mychrdev"
static dev_t devid; static struct cdev mycdev; static struct class *myclass; static struct device *mydevice;
static int drv_open(struct inode *inode, struct file *file) { printk("device open\n"); return 0; }
static ssize_t drv_read(struct file *file, char __user *buf, size_t size, loff_t *off) { printk("device read\n"); return 0; }
static ssize_t drv_write(struct file *file, const char __user *buf, size_t size, loff_t *off) { printk("device write\n"); return size; }
static int drv_release(struct inode *inode, struct file *file) { printk("device release\n"); return 0; }
static struct file_operations fops = { .owner = THIS_MODULE, .open = drv_open, .read = drv_read, .write = drv_write, .release = drv_release, };
static int __init drv_init(void) { alloc_chrdev_region(&devid, 0, 1, DEV_NAME);
cdev_init(&mycdev, &fops);
cdev_add(&mycdev, devid, 1);
myclass = class_create(THIS_MODULE, DEV_NAME);
mydevice = device_create(myclass, NULL, devid, NULL, DEV_NAME);
printk("driver init\n"); return 0; }
static void __exit drv_exit(void) { device_destroy(myclass, devid); class_destroy(myclass);
cdev_del(&mycdev); unregister_chrdev_region(devid, 1);
printk("driver exit\n"); }
module_init(drv_init); module_exit(drv_exit);
MODULE_LICENSE("GPL");
|
总结
主流字符驱动的核心逻辑可以总结为
用户程序
│
▼
系统调用 open/read/write
│
▼
VFS
│
▼
file_operations
│
▼
cdev
│
▼
驱动代码
│
▼
硬件
所以 Linux 驱动本质上是一个函数回调系统。