0%

Linux驱动学习日记(16) 主流字符设备驱动如何编写(cdev)

为什么要使用 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;

/* open */
static int drv_open(struct inode *inode, struct file *file)
{
printk("device open\n");
return 0;
}

/* read */
static ssize_t drv_read(struct file *file, char __user *buf,
size_t size, loff_t *off)
{
printk("device read\n");
return 0;
}

/* write */
static ssize_t drv_write(struct file *file, const char __user *buf,
size_t size, loff_t *off)
{
printk("device write\n");
return size;
}

/* release */
static int drv_release(struct inode *inode, struct file *file)
{
printk("device release\n");
return 0;
}

/* file_operations */
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)
{
/* 1 分配设备号 */
alloc_chrdev_region(&devid, 0, 1, DEV_NAME);

/* 2 初始化cdev */
cdev_init(&mycdev, &fops);

/* 3 添加cdev */
cdev_add(&mycdev, devid, 1);

/* 4 创建设备类 */
myclass = class_create(THIS_MODULE, DEV_NAME);

/* 5 创建设备节点 */
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 驱动本质上是一个函数回调系统。