第6章 字符设备驱动(宋宝华Linux设备驱动开发详解)

第6章 字符设备驱动

6.1 Linux字符设备驱动结构

6.1.1 cdev结构体

​ 在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()函数的调用则通常发生在字符设备驱动模块卸载函数中。

6.1.2 分配和释放设备号

​ 在调用 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);

6.1.3 file_operations结构体

​ 除了open、read、write,介绍了很多类似的操作

6.1.4 Linux字符设备驱动的组成

​ 在 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 的连接

6.2 globalmem虚拟设备实例描述(用来学习的)

​ 从本章开始,后续的数章都将基于虚拟的 globalmem 设备进行字符设备驱动的讲解。 globalmem意味着“全局内存”,在 globalmem 字符设备驱动中会分配一片大小为 GLOBALMEM_SIZE( 4KB)的内存空间,并在驱动中提供针对该片内存的读写、控制和定位函数,以供用户空间的进程能通过 Linux 系统调用访问这片内存。

​ 实际上,这个虚拟的 globalmem 设备几乎没有任何实用价值,仅仅是一种为了讲解问题的方便而凭空制造的设备。当然,它也并非百无一用,由于 globalmem 可被两个或两个以上的进程同时访问,其中的全局内存可作为用户空间进程进行通信的一种蹩脚的手段。

​ 本章将给出 globalmem 设备驱动的雏形,而后续章节会在这个雏形的基础上添加并发与同步控制等复杂功能。

6.3 globalmem设备驱动

6.3.1 头文件、宏及设备结构体

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 包含在一个设备结构体中,但这样定义的好处在于,它借用了面向对象程序设计中“封装”的思想,体现了一种良好的编程习惯。

6.3.2 加载与卸载设备驱动

​ 总结一下大概步骤,先是创建设备号(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各种操作,就不记录了

6.3.3 读写函数

6.3.4 seek函数

6.3.5 ioctl函数

6.3.6 使用文件私有数据

​ 在上述的几个函数的实现中,都使用到了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 }

6.4 globalmem驱动在用户空间中的验证

6.5 总结

​ 字符设备是 3 大类设备(字符设备、块设备和网络设备)中较简单的一类设备,其驱动程序中完成的主要工作是初始化、添加和删除 cdev 结构体,申请和释放设备号,以及填充 file_operations结构体中的操作函数,实现 file_operations 结构体中的 read()、 write()和 ioctl()等函数是驱动设计的主体工作。

你可能感兴趣的:(linux,驱动开发,运维)