在Linux内核中,使用cdev结构体描述一个字符设备,cdev结构体的定义如下所示(在vscode按ctrl+T,进行符号搜索,Linux内核不提供clangd的搜索)
struct cdev {
struct kobject kobj; /* 内嵌的kobject对象 */
struct module *owner; /* 所属的操作模块 */
const struct file_operations *ops; /* 文件操作结构体 */
struct list_head list;
dev_t dev; /* 设备号*/
unsigned int count;
};
cdev结构体的dev_t成员定义了设备号,为32位,其中12位为主设备号,20位为次设备号。使用下列宏可以从dev_t获得主设备号和从设备号
MAJOR(dec_t dev)
MINOR(dev_t dev)
而使用下列宏则可以通过主设备号和次设备号生成 dev_t:
MKDEV(int major, int minor)
#define MKDEV(ma,mi) (((ma) << MINORBITS) | (mi))
Linux 2.6 内核提供了一组函数用于操作 cdev 结构体:
void cdev_init(struct cdev *, struct file_operations *);
struct cdev *cdev_alloc(void);
void cdev_put(struct cdev *p);
int cdev_add(struct cdev *, dev_t, unsigned);
void cdev_del(struct cdev *);
cdev_init()函数用于初始化 cdev 的成员,并建立 cdev 和 file_operations 之间的连接,其源代码如代码清单 6.2 所示
1 void cdev_init(struct cdev *cdev, struct file_operations *fops)
2 {
3 memset(cdev, 0, sizeof *cdev);
4 INIT_LIST_HEAD(&cdev->list);
5 kobject_init(&cdev->kobj, &ktype_cdev_default);
6 cdev->ops = fops; /*将传入的文件操作结构体指针赋值给 cdev 的 ops*/
7 }
cdev_alloc()函数用于动态申请一个 cdev 内存,其源代码如代码清单 6.3 所示。
1 struct cdev *cdev_alloc(void)
2 {
3 struct cdev *p = kzalloc(sizeof(struct cdev), GFP_KERNEL);
4 if (p) {
5 INIT_LIST_HEAD(&p->list);
6 kobject_init(&p->kobj, &ktype_cdev_dynamic);
7 }
8 return p;
9 }
cdev_add()函数和 cdev_del()函数分别向系统添加和删除一个 cdev,完成字符设备的注册和注销。对 cdev_add()的调用通常发生在字符设备驱动模块加载函数中,而对 cdev_del()函数的调用则通常发生在字符设备驱动模块卸载函数中。
在调用 cdev_add()函数向系统注册字符设备之前,应首先调用 register_chrdev_region()或alloc_chrdev_region()函数向系统申请设备号,这两个函数的原型为:
int register_chrdev_region(dev_t from, unsigned count, const char *name);
int alloc_chrdev_region(dev_t *dev, unsigned baseminor, unsigned count,const char *name);
register_chrdev_region()函数用于已知起始设备的设备号的情况,而 alloc_chrdev_region()用于设备号未知,向系统动态申请未被占用的设备号的情况,函数调用成功之后,会把得到的设备号放入第一个参数 dev 中。 alloc_chrdev_region()与 register_chrdev_region()对比的优点在于它会自动避开设备号重复的冲突。
相反地,在调用 cdev_del()函数从系统注销字符设备之后, unregister_chrdev_region()应该被调用以释放原先申请的设备号,这个函数的原型为:
void unregister_chrdev_region(dev_t from, unsigned count);
除了open、read、write,介绍了很多类似的操作
在 Linux 中,字符设备驱动由如下几个部分组成。
1.字符设备驱动模块加载与卸载函数
在字符设备驱动模块加载函数中应该实现设备号的申请和 cdev 的注册,而在卸载函数中应实现设备号的释放和 cdev 的注销。
工程师通常习惯为设备定义一个设备相关的结构体,其包含该设备所涉及的 cdev、私有数据及信号量等信息。常见的设备结构体、模块加载和卸载函数形式如代码清单 6.5 所示。
1 /* 设备结构体 */
2 struct xxx_dev_t {
3 struct cdev cdev;
4 ...
5 } xxx_dev;
6 /* 设备驱动模块加载函数*/
7 static int _ _init xxx_init(void)
8 {
9 ...
10 cdev_init(&xxx_dev.cdev, &xxx_fops); /* 初始化 cdev */
11 xxx_dev.cdev.owner = THIS_MODULE;
12 /* 获取字符设备号*/
13 if (xxx_major) {
14 register_chrdev_region(xxx_dev_no, 1, DEV_NAME);
15 } else {
16 alloc_chrdev_region(&xxx_dev_no, 0, 1, DEV_NAME);
17 }
18
19 ret = cdev_add(&xxx_dev.cdev, xxx_dev_no, 1); /* 注册设备*/
20 ...
21 }
22 /*设备驱动模块卸载函数*/
23 static void _ _exit xxx_exit(void)
24 {
25 unregister_chrdev_region(xxx_dev_no, 1); /* 释放占用的设备号*/
26 cdev_del(&xxx_dev.cdev); /* 注销设备*/
27 ...
28 }
2.字符设备驱动的 file_operations 结构体中成员函数
file_operations 结构体中成员函数是字符设备驱动与内核的接口,是用户空间对 Linux 进行系统调用最终的落实者。大多数字符设备驱动会实现 read()、 write()和 ioctl()函数。
1 /* 读设备*/
2 ssize_t xxx_read(struct file *filp, char __user *buf, size_t count,
3 loff_t*f_pos)
4 {
5 ...
6 copy_to_user(buf, ..., ...);
7 ...
8 }
9 /* 写设备*/
10 ssize_t xxx_write(struct file *filp, const char __user *buf, size_t count,
11 loff_t *f_pos)
12 {
13 ...
14 copy_from_user(..., buf, ...);
15 ...
16 }
17 /* ioctl 函数 */
18 int xxx_ioctl(struct inode *inode, struct file *filp, unsigned int cmd,
19 unsigned long arg)
20 {
21 ...
22 switch (cmd) {
23 case XXX_CMD1:
24 ...
25 break;
26 case XXX_CMD2:
27 ...
28 break;
29 default:
30 /* 不能支持的命令 */
31 return - ENOTTY;
32 }
33 return 0;
34 }
设备驱动的读函数中, filp 是文件结构体指针, buf 是用户空间内存的地址,该地址在内核空间不能直接读写, count 是要读的字节数, f_pos 是读的位置相对于文件开头的偏移。
设备驱动的写函数中, filp 是文件结构体指针, buf 是用户空间内存的地址,该地址在内核空间不能直接读写, count 是要写的字节数, f_pos 是写的位置相对于文件开头的偏移。
由于内核空间与用户空间的内存不能直接互访,因此借助了函数 copy_from_user()完成用户空间到内核空间的拷贝,以及 copy_to_user()完成内核空间到用户空间的拷贝,见代码第 6 行和第 14 行.
完成内核空间和用户空间内存拷贝的 copy_from_user()和 copy_to_user()的原型分别为:
unsigned long copy_from_user(void *to, const void _ _user *from, unsigned long count);
unsigned long copy_to_user(void _ _user *to, const void *from, unsigned long count);
上述函数均返回不能被复制的字节数,因此,如果完全复制成功,返回值为 0。
如果要复制的内存是简单类型,如 char、 int、 long 等,则可以使用简单的 put_user()和 get_ user(),如:
int val; /* 内核空间整型变量
...
get_user(val, (int *) arg); /* 用户→内核, arg 是用户空间的地址
...
put_user(val, (int *) arg); /* 内核→用户, arg 是用户空间的地址
读和写函数中的_ _user 是一个宏,表明其后的指针指向用户空间,实际上更多地充当了代码自注释的功能。这个宏定义为:
#ifdef _ _CHECKER_ _
# define _ _user _ _attribute_ _((noderef, address_space(1)))
#else
# define _ _user
#endif
内核空间虽然可以访问用户空间的缓冲区,但是访问前,一般需要先检查其合法性,通过access_ok(type,addr,size)进行判断
I/O 控制函数的 cmd 参数为事先定义的 I/O 控制命令,而 arg 为对应于该命令的参数。例如对于串行设备,如果 SET_BAUDRATE 是一道设置波特率的命令,那后面的 arg 就应该是波特率值。
在字符设备驱动中,需要定义一个 file_operations 的实例,并将具体设备驱动的函数赋值给file_operations 的成员,如代码清单 6.7 所示。
1 struct file_operations xxx_fops = {
2 .owner = THIS_MODULE,
3 .read = xxx_read,
4 .write = xxx_write,
5 .ioctl = xxx_ioctl,
6 ...
7 };
上述 xxx_fops 在代码清单 6.5 第 10 行的 cdev_init(&xxx_dev.cdev, &xxx_fops)的语句中被建立与 cdev 的连接
从本章开始,后续的数章都将基于虚拟的 globalmem 设备进行字符设备驱动的讲解。 globalmem意味着“全局内存”,在 globalmem 字符设备驱动中会分配一片大小为 GLOBALMEM_SIZE( 4KB)的内存空间,并在驱动中提供针对该片内存的读写、控制和定位函数,以供用户空间的进程能通过 Linux 系统调用访问这片内存。
实际上,这个虚拟的 globalmem 设备几乎没有任何实用价值,仅仅是一种为了讲解问题的方便而凭空制造的设备。当然,它也并非百无一用,由于 globalmem 可被两个或两个以上的进程同时访问,其中的全局内存可作为用户空间进程进行通信的一种蹩脚的手段。
本章将给出 globalmem 设备驱动的雏形,而后续章节会在这个雏形的基础上添加并发与同步控制等复杂功能。
1 #include <linux/module.h>
2 #include <linux/types.h>
3 #include <linux/fs.h>
4 #include <linux/errno.h>
5 #include <linux/mm.h>
6 #include <linux/sched.h>
7 #include <linux/init.h>
8 #include <linux/cdev.h>
9 #include <asm/io.h>
10 #include <asm/system.h>
11 #include <asm/uaccess.h>
12
13 #define GLOBALMEM_SIZE 0x1000 /*全局内存大小: 4KB*/
14 #define MEM_CLEAR 0x1 /*清零全局内存*/
15 #define GLOBALMEM_MAJOR 250 /*预设的 globalmem 的主设备号*/
16
17 static int globalmem_major = GLOBALMEM_MAJOR;
18 /*globalmem 设备结构体*/
19 struct globalmem_dev {
20 struct cdev cdev; /*cdev 结构体*/
21 unsigned char mem[GLOBALMEM_SIZE]; /*全局内存*/
22 };
23
24 struct globalmem_dev dev; /*设备结构体实例*/
从第 19~22 行代码可以看出,定义的 globalmem_dev 设备结构体包含了对应于 globalmem 字符 设 备 的 cdev 、 使 用 的 内 存 mem[GLOBALMEM_SIZE] 。 当 然 , 程 序 中 并 不 一 定 要 把mem[GLOBALMEM_SIZE]和 cdev 包含在一个设备结构体中,但这样定义的好处在于,它借用了面向对象程序设计中“封装”的思想,体现了一种良好的编程习惯。
总结一下大概步骤,先是创建设备号(MKDEV)、再是申请设备号(register_chrdev_region)、申请globalmem_dev结构体内存(书上有,但下面的代码是我在书本pdf复制的,所以没有),cdev的初始化和添加。
1 /*globalmem 设备驱动模块加载函数*/
2 int globalmem_init(void)
3 {
4 int result;
5 dev_t devno = MKDEV(globalmem_major, 0);
6
7 /* 申请字符设备驱动区域*/
8 if (globalmem_major)
9 result = register_chrdev_region(devno, 1, "globalmem");
10 else {
11 /* 动态获得主设备号 */
12 result = alloc_chrdev_region(&devno, 0, 1, "globalmem");
13 globalmem_major = MAJOR(devno);
14 }
15 if (result < 0)
16 return result;
17
18 globalmem_setup_cdev();
19 return 0;
20 }
21
22 /*globalmem 设备驱动模块卸载函数*/
23 void globalmem_exit(void)
24 {
25 cdev_del(&dev.cdev); /*删除 cdev 结构*/
26 unregister_chrdev_region(MKDEV(globalmem_major, 0), 1);/*注销设备区域*/
27 }
第 18 行调用的 globalmem_setup_cdev()函数完成 cdev 的初始化和添加,如代码清单 6.10 所示。
1 /*初始化并添加 cdev 结构体*/
2 static void globalmem_setup_cdev()
3 {
4 int err, devno = MKDEV(globalmem_major, 0);
5
6 cdev_init(&dev.cdev, &globalmem_fops);
7 dev.cdev.owner = THIS_MODULE;
8 err = cdev_add(&dev.cdev, devno, 1);
9 if (err)
10 printk(KERN_NOTICE "Error %d adding globalmem", err);
11 }
后面就是实现了这个驱动的read、write、llseek各种操作,就不记录了
在上述的几个函数的实现中,都使用到了struct globalmem_dev *dev = flip->private_data
获取globalmem_dev 的实例指针。实际上,大多数Linux驱动遵循一个潜规则,那就是将文件的私有数据private_data指向设备结构体,再用read()、write()等函数通过private_data访问设备结构体。私有数据的概念在Linux驱动的各个子系统中广泛存在,实际上体现了Linux的面向对象的设计思想。对于globel驱动而言,私有数据的设置在globalmem驱动而言,私有数据的设置在globalmem_open()中完成,如下所示。
25 /*文件打开函数*/
26 int globalmem_open(struct inode *inode, struct file *filp)
27 {
28 /*将设备结构体指针赋值给文件私有数据指针*/
29 filp->private_data = globalmem_devp;
30 return 0;
31 }
这边提到了一个container_of(),作用是就可以轻松地让 globalmem 驱动中包含两个同样的设备。第 7 行调用的 container_of()的作用是通过结构体成员的指针找到对应结构体的指针,这个技巧在 Linux 内核编程中十分常用。在 container_of(inode->i_cdev,struct globalmem_dev, cdev)语句中,传给 container_of()的第 1 个参数是结构体成员的指针,第 2 个参数为整个结构体的类型,第 3 个参数为传入的第 1 个参数即结构体成员的类型, container_of()返回值为整个结构体的指针。
1 /*文件打开函数*/
2 int globalmem_open(struct inode *inode, struct file *filp)
3 {
4 /*将设备结构体指针赋值给文件私有数据指针*/
5 struct globalmem_dev *dev;
6
7 dev = container_of(inode->i_cdev,struct globalmem_dev,cdev);
8 filp->private_data = dev;
9 return 0;
10 }
字符设备是 3 大类设备(字符设备、块设备和网络设备)中较简单的一类设备,其驱动程序中完成的主要工作是初始化、添加和删除 cdev 结构体,申请和释放设备号,以及填充 file_operations结构体中的操作函数,实现 file_operations 结构体中的 read()、 write()和 ioctl()等函数是驱动设计的主体工作。