实现设备的具体操作函数
file_operations结构体就是设备的具体操作函数,在示例代码40.2.2.1中我们定义了file_operations结构体类型的变量test_fops,但是还没对其进行初始化,也就是初始化其中的open、release、read和write等具体的设备操作函数。本节小节我们就完成变量test_fops的初始化,设置好针对chrtest设备的操作函数。在初始化test_fops之前我们要分析一下需求,也就是要对chrtest这个设备进行哪些操作,只有确定了需求以后才知道我们应该实现哪些操作函数。假设对chrtest这个设备有如下两个要求:
1、能够对chrtest进行打开和关闭操作
设备打开和关闭是最基本的要求,几乎所有的设备都得提供打开和关闭的功能。因此我们需要实现file_operations中的open和release这两个函数。
2、对chrtest进行读写操作
假设chrtest这个设备控制着一段缓冲区(内存),应用程序需要通过read和write这两个函数对chrtest的缓冲区进行读写操作。所以需要实现file_operations中的read和write这两个函数。
需求很清晰了,在其中加入test_fops这个结构体变量的初始化操作,完成以后的内容如下所示:
1 /* 打开设备 */
2 static int chrtest_open(struct inode *inode, struct file *filp)
3 {
4 /* 用户实现具体功能 */
5 return 0;
6 }
7
8 /* 从设备读取 */
9 static ssize_t chrtest_read(struct file *filp, char __user *buf, size_t cnt, loff_t *offt)
10 {
11 /* 用户实现具体功能 */
12 return 0;
13 }
14
15 /* 向设备写数据 */
16 static ssize_t chrtest_write(struct file *filp,
const char __user *buf,
size_t cnt, loff_t *offt)
17 {
18 /* 用户实现具体功能 */
19 return 0;
20 }
21
22 /* 关闭/释放设备 */
23 static int chrtest_release(struct inode *inode, struct file *filp)
24 {
25 /* 用户实现具体功能 */
26 return 0;
27 }
28
29 static struct file_operations test_fops = {
30 .owner = THIS_MODULE,
31 .open = chrtest_open,
32 .read = chrtest_read,
33 .write = chrtest_write,
34 .release = chrtest_release,
35 };
36
37 /* 驱动入口函数 */
38 static int __init xxx_init(void)
39 {
40 /* 入口函数具体内容 */
41 int retvalue = 0;
42
43 /* 注册字符设备驱动 */
44 retvalue = register_chrdev(200, "chrtest", &test_fops);
45 if(retvalue < 0){
46 /* 字符设备注册失败,自行处理 */
47 }
48 return 0;
49 }
50
51 /* 驱动出口函数 */
52 static void __exit xxx_exit(void)
53 {
54 /* 注销字符设备驱动 */
55 unregister_chrdev(200, "chrtest");
56 }
57
58 /* 将上面两个函数指定为驱动的入口和出口函数 */
59 module_init(xxx_init);
60 module_exit(xxx_exit);
最后我们需要在驱动中加入LICENSE信息和作者信息,其中LICENSE是必须添加的,否则的话编译的时候会报错,作者信息可以添加也可以不添加。LICENSE和作者信息的添加使用如下两个函数:
MODULE_LICENSE() //添加模块LICENSE信息
MODULE_AUTHOR() //添加模块作者信息
最后给示例代码40.2.3.1加入LICENSE和作者信息,完成以后的内容如下:
1 /* 打开设备 */
2 static int chrtest_open(struct inode *inode, struct file *filp)
3 {
4 /* 用户实现具体功能 */
5 return 0;
6 }
......
57
58 /* 将上面两个函数指定为驱动的入口和出口函数 */
59 module_init(xxx_init);
60 module_exit(xxx_exit);
61
62 MODULE_LICENSE("GPL");
63 MODULE_AUTHOR("honr");
Linux设备号
设备号的组成
为了方便管理,Linux中每个设备都有一个设备号,设备号由主设备号和次设备号两部分组成,主设备号表示某一个具体的驱动,次设备号表示使用这个驱动的各个设备。Linux提供了一个名为dev_t的数据类型表示设备号,dev_t定义在文件include/linux/types.h里面,定义如下:
设备号dev_t
12 typedef __u32 __kernel_dev_t;
…
15 typedef __kernel_dev_t dev_t;
可以看出dev_t是__u32类型的,而__u32定义在文件include/uapi/asm-generic/int-ll64.h里面,定义如下:
示例代码40.3.2 __u32类型
26 typedef unsigned int __u32;
综上所述,dev_t其实就是unsigned int类型,是一个32位的数据类型。这32位的数据构成了主设备号和次设备号两部分,其中高12位为主设备号,低20位为次设备号。因此Linux系统中主设备号范围为0~4095,所以大家在选择主设备号的时候一定不要超过这个范围。在文件include/linux/kdev_t.h中提供了几个关于设备号的操作函数(本质是宏),如下所示:
6 #define MINORBITS 20
7 #define MINORMASK ((1U << MINORBITS) - 1)
8
9 #define MAJOR(dev) ((unsigned int) ((dev) >> MINORBITS))
10 #define MINOR(dev) ((unsigned int) ((dev) & MINORMASK))
11 #define MKDEV(ma,mi) (((ma) << MINORBITS) | (mi))
设备号的分配
1、静态分配设备号
本小节讲的设备号分配主要是主设备号的分配。前面讲解字符设备驱动的时候说过了,注册字符设备的时候需要给设备指定一个设备号,这个设备号可以是驱动开发者静态的指定一个设备号,比如选择200这个主设备号。有一些常用的设备号已经被Linux内核开发者给分配掉了,具体分配的内容可以查看文档Documentation/devices.txt。并不是说内核开发者已经分配掉的主设备号我们就不能用了,具体能不能用还得看我们的硬件平台运行过程中有没有使用这个主设备号,使用“cat /proc/devices”命令即可查看当前系统中所有已经使用了的设备号。
2、动态分配设备号
静态分配设备号需要我们检查当前系统中所有被使用了的设备号,然后挑选一个没有使用的。而且静态分配设备号很容易带来冲突问题,Linux社区推荐使用动态分配设备号,在注册字符设备之前先申请一个设备号,系统会自动给你一个没有被使用的设备号,这样就避免了冲突。卸载驱动的时候释放掉这个设备号即可,设备号的申请函数如下:
int alloc_chrdev_region(dev_t *dev, unsigned baseminor, unsigned count, const char *name)
函数alloc_chrdev_region用于申请设备号,此函数有4个参数:
dev:保存申请到的设备号。
baseminor:次设备号起始地址,alloc_chrdev_region可以申请一段连续的多个设备号,这些设备号的主设备号一样,但是次设备号不同,次设备号以baseminor为起始地址地址开始递增。一般baseminor为0,也就是说次设备号从0开始。
count:要申请的设备号数量。
name:设备名字。
注销字符设备之后要释放掉设备号,设备号释放函数如下:
void unregister_chrdev_region(dev_t from, unsigned count)
此函数有两个参数:
from:要释放的设备号。
count:表示从from开始,要释放的设备号数量。
chrdevbase字符设备驱动开发实验
字符设备驱动开发的基本步骤我们已经了解了,本节我们就以chrdevbase这个虚拟设备为例,完整的编写一个字符设备驱动模块。chrdevbase不是实际存在的一个设备,是笔者为了方便讲解字符设备的开发而引入的一个虚拟设备。chrdevbase设备有两个缓冲区,一个为读缓冲区,一个为写缓冲区,这两个缓冲区的大小都为100字节。在应用程序中可以向chrdevbase设备的写缓冲区中写入数据,从读缓冲区中读取数据。chrdevbase这个虚拟设备的功能很简单,但是它包含了字符设备的最基本功能。