前言
从本章开始,后续的数章都将基于虚拟的globalmem设备进行字符设备驱动的讲解。globalmem意味着“全局内存”,在globalmem字符设备驱动中会分配一片大小为GLOBALMEM_SIZE(4KB)的内存空间,并在驱动中提供针对该片内存的读写、控制和定位函数,以供用户空间的进程能通过Linux系统调用获取或设置这片内存的内容。
实际上,这个虚拟的globalmem设备几乎没有任何实用价值,仅仅是一种为了讲解问题的方便而凭空制造的设备。
本章将给出globalmem设备驱动的雏形,而后续章节会在这个雏形的基础上添加并发与同步控制等复杂功能。
1-头文件、宏及设备结构体
在globalmem字符设备驱动中,应包含它要使用的头文件,并定义globalmem设备结构体及相关宏。
1#include <linux/module.h>
2#include <linux/fs.h>
3#include <linux/init.h>
4#include <linux/cdev.h>
5#include <linux/slab.h>
6#include <linux/uaccess.h>
7
8#define GLOBALMEM_SIZE 0x1000
9#define MEM_CLEAR 0x1
10#define GLOBALMEM_MAJOR 230
11
12static int globalmem_major = GLOBALMEM_MAJOR;
13module_param(globalmem_major, int, S_IRUGO);
14
15struct globalmem_dev {
16 struct cdev cdev;
17 unsigned char mem[GLOBALMEM_SIZE];
18};
19
20struct globalmem_dev *globalmem_devp;
从第15~18行代码可以看出,定义的globalmem_dev设备结构体包含了对应于globalmem字符设备的cdev、使用的内存mem[GLOBALMEM_SIZE]。
当然,程序中并不一定要把mem[GLOBALMEM_SIZE]和cdev包含在一个设备结构体中,但这样定义的好处在于,它借用了面向对象程序设计中“封装”的思想,体现了一种良好的编程习惯。
2-加载与卸载设备驱动
globalmem设备驱动的模块加载和卸载函数遵循代码清单6.5的类似模板,其实现的工作与代码清单6.5完全一致,如代码清单6.9所示。
1static void globalmem_setup_cdev(struct globalmem_dev *dev, int index)
2 {
3 int err, devno = MKDEV(globalmem_major, index);
4
5 cdev_init(&dev->cdev, &globalmem_fops);
6 dev->cdev.owner = THIS_MODULE;
7 err = cdev_add(&dev->cdev, devno, 1);
8 if (err)
9 printk(KERN_NOTICE "Error %d adding globalmem%d", err, index);
10 }
11
12static int __init globalmem_init(void)
13{
14 int ret;
15 dev_t devno = MKDEV(globalmem_major, 0);
16
17 if (globalmem_major)
18 ret = register_chrdev_region(devno, 1, "globalmem");
19 else {
20 ret = alloc_chrdev_region(&devno, 0, 1, "globalmem");
21 globalmem_major = MAJOR(devno);
22 }
23 if (ret < 0)
24 return ret;
25
26 globalmem_devp = kzalloc(sizeof(struct globalmem_dev), GFP_KERNEL);
27 if (!globalmem_devp) {
28 ret = -ENOMEM;
29 goto fail_malloc;
30 }
31
32 globalmem_setup_cdev(globalmem_devp, 0);
33 return 0;
34
35 fail_malloc:
36 unregister_chrdev_region(devno, 1);
37 return ret;
38 }
39module_init(globalmem_init);
- 第1~10行的globalmem_setup_cdev()函数完成cdev的初始化和添加,
- 17~22行完成了设备号的申请,
- 第26行调用kzalloc()申请了一份globalmem_dev结构体的内存并清0。
在cdev_init()函数中,与globalmem的cdev关联的file_operations结构体如代码清单6.10所示。
1static const struct file_operations globalmem_fops = {
2 .owner = THIS_MODULE,
3 .llseek = globalmem_llseek,
4 .read = globalmem_read,
5 .write = globalmem_write,
6 .unlocked_ioctl = globalmem_ioctl,
7 .open = globalmem_open,
8 .release = globalmem_release,
9};
3-读写函数
globalmem设备驱动的读写函数主要是让设备结构体的mem[]数组与用户空间交互数据,并随着访问的字节数变更更新文件读写偏移位置。读和写函数的实现分别如代码清单6.11和6.12所示。
1static ssize_t globalmem_read(struct file *filp, char __user * buf, size_t size,
2 loff_t * ppos)
3{
4 unsigned long p = *ppos;
5 unsigned int count = size;
6 int ret = 0;
7 struct globalmem_dev *dev = filp->private_data;
8
9 if (p >= GLOBALMEM_SIZE)
10 return 0;
11 if (count > GLOBALMEM_SIZE - p)
12 count = GLOBALMEM_SIZE - p;
13
14 if (copy_to_user(buf, dev->mem + p, count)) {
15 ret = -EFAULT;
16 } else {
17 *ppos += count;
18 ret = count;
19
20 printk(KERN_INFO "read %u bytes(s) from %lu\n", count, p);
21 }
22
23 return ret;
24}
*ppos是要读的位置相对于文件开头的偏移,如果该偏移大于或等于GLOBALMEM_SIZE,意味着已经到达文件末尾,所以返回0(EOF)。
1static ssize_t globalmem_write(struct file *filp, const char __user * buf,
2 size_t size, loff_t * ppos)
3{
4 unsigned long p = *ppos;
5 unsigned int count = size;
6 int ret = 0;
7 struct globalmem_dev *dev = filp->private_data;
8
9 if (p >= GLOBALMEM_SIZE)
10 return 0;
11 if (count > GLOBALMEM_SIZE - p)
12 count = GLOBALMEM_SIZE - p;
13
14 if (copy_from_user(dev->mem + p, buf, count))
15 ret = -EFAULT;
16 else {
17 *ppos += count;
18 ret = count;
19
20 printk(KERN_INFO "written %u bytes(s) from %lu\n", count, p);
21 }
22
23 return ret;
24}
4-seek函数
seek()函数对文件定位的起始地址可以是文件开头(SEEK_SET,0)、当前位置(SEEK_CUR,1)和文件尾(SEEK_END,2),假设globalmem支持从文件开头和当前位置的相对偏移。
在定位的时候,应该检查用户请求的合法性,若不合法,函数返回-EINVAL,合法时更新文件的当前位置并返回该位置,如代码清单6.13所示。
1static loff_t globalmem_llseek(struct file *filp, loff_t offset, int orig)
2 {
3 loff_t ret = 0;
4 switch (orig) {
5 case 0: /* 从文件开头位置seek */
6 if (offset< 0) {
7 ret = -EINVAL;
8 break;
9 }
10 if ((unsigned int)offset > GLOBALMEM_SIZE) {
11 ret = -EINVAL;
12 break;
13 }
14 filp->f_pos = (unsigned int)offset;
15 ret = filp->f_pos;
16 break;
17 case 1: /* 从文件当前位置开始seek */
18 if ((filp->f_pos + offset) > GLOBALMEM_SIZE) {
19 ret = -EINVAL;
20 break;
21 }
22 if ((filp->f_pos + offset) < 0) {
23 ret = -EINVAL;
24 break;
25 }
26 filp->f_pos += offset;
27 ret = filp->f_pos;
28 break;
29 default:
30 ret = -EINVAL;
31 break;
32 }
33 return ret;
34}
5-ioctl函数
1.globalmem设备驱动的ioctl()函数
globalmem设备驱动的ioctl()函数接受MEM_CLEAR命令,这个命令会将全局内存的有效数据长度清0,对于设备不支持的命令,ioctl()函数应该返回-EINVAL,如代码清单6.14所示。
1static long globalmem_ioctl(struct file *filp, unsigned int cmd,
2 unsigned long arg)
3{
4 struct globalmem_dev *dev = filp->private_data;
5
6 switch (cmd) {
7 case MEM_CLEAR:
8 memset(dev->mem, 0, GLOBALMEM_SIZE);
9 printk(KERN_INFO "globalmem is set to zero\n");
10 break;
11
12 default:
13 return -EINVAL;
14 }
15
16 return 0;
17 }
在上述程序中,MEM_CLEAR被宏定义为0x01,实际上这并不是一种值得推荐的方法,简单地对命令定义为0x0、0x1、0x2等类似值会导致不同的设备驱动拥有相同的命令号。
如果设备A、B都支持0x0、0x1、0x2这样的命令,就会造成命令码的污染。因此,Linux内核推荐采用一套统一的ioctl()命令生成方式。
2.ioctl()命令
Linux建议以如图6.2所示的方式定义ioctl()的命令。
设备类型(8)+序列号(8)+方向(2)+数据尺寸(13/14)
命令码的设备类型字段为一个“幻数”,可以是0~0xff的值,内核中的ioctl-number.txt给出了一些推荐的和已经被使用的“幻数”,新设备驱动定义“幻数”的时候要避免与其冲突。
命令码的序列号也是8位宽。
命令码的方向字段为2位,该字段表示数据传送的方向,可能的值是_IOC_NONE(无数据传输)、_IOC_READ(读)、_IOC_WRITE(写)和_IOC_READ|_IOC_WRITE(双向)。数据传送的方向是从应用程序的角度来看的。
命令码的数据长度字段表示涉及的用户数据的大小,这个成员的宽度依赖于体系结构,通常是13或者14位。
内核还定义了_IO()、_IOR()、_IOW()和_IOWR()这4个宏来辅助生成命令,这4个宏的通用定义如代码清单6.15所示。
1#define _IO(type,nr) _IOC(_IOC_NONE,(type),(nr),0)
2#define _IOR(type,nr,size) _IOC(_IOC_READ,(type),(nr),\
3 (_IOC_TYPECHECK(size)))
4#define _IOW(type,nr,size) _IOC(_IOC_WRITE,(type),(nr),\
5 (_IOC_TYPECHECK(size)))
6#define _IOWR(type,nr,size) _IOC(_IOC_READ|_IOC_WRITE,(type),(nr), \
7 (_IOC_TYPECHECK(size)))
8/* _IO、_IOR等使用的_IOC宏*/
9#define _IOC(dir,type,nr,size) \
10 (((dir) << _IOC_DIRSHIFT) | \
11 ((type) << _IOC_TYPESHIFT) | \
12 ((nr) << _IOC_NRSHIFT) | \
13 ((size) << _IOC_SIZESHIFT))
由此可见,这几个宏的作用是根据传入的type(设备类型字段)、nr(序列号字段)、size(数据长度字段)和宏名隐含的方向字段移位组合生成命令码。由于globalmem的MEM_CLEAR命令不涉及数据传输,所以它可以定义为:
#define GLOBALMEM_MAGIC 'g'
#define MEM_CLEAR _IO(GLOBALMEM_MAGIC,0)
3.预定义命令
内核中预定义了一些I/O控制命令,如果某设备驱动中包含了与预定义命令一样的命令码,这些命令会作为预定义命令被内核处理而不是被设备驱动处理,下面列举一些常用的预定义命令。
- FIOCLEX:即File IOctl Close on Exec,对文件设置专用标志,通知内核当exec()系统调用发生时自动关闭打开的文件。
- FIONCLEX:即File IOctl Not Close on Exec,与FIOCLEX标志相反,清除由FIOCLEX命令设置的标志。
- FIOQSIZE:获得一个文件或者目录的大小,当用于设备文件时,返回一个ENOTTY错误。
- FIONBIO:即File IOctl Non-Blocking I/O,这个调用修改在filp->f_flags中的O_NONBLOCK标志。
FIOCLEX、FIONCLEX、FIOQSIZE和FIONBIO这些宏定义在内核的include/uapi/asm-generic/ioctls.h文件中。
6-使用文件私有数据
6.3.1~6.3.5 节给出的代码完整地实现了预期的globalmem雏形,代码清单6.11的第7行,代码清单6.12的第7行,代码清单6.14的第4行,都使用了struct globalmem_dev*dev=filp->private_data获取globalmem_dev的实例指针。
实际上,大多数Linux驱动遵循一个“潜规则”,那就是将文件的私有数据private_data指向设备结构体,再用read()、write()、ioctl()、llseek()等函数通过private_data访问设备结构体。私有数据的概念在Linux驱动的各个子系统中广泛存在,实际上体现了Linux的面向对象的设计思想。对于globalmem驱动而言,私有数据的设置是在globalmem_open()中完成的,如代码清单6.16所示。
1static int globalmem_open(struct inode *inode, struct file *filp)
2 {
3 filp->private_data = globalmem_devp;
4 return 0;
5}
为了让读者建立字符设备驱动的全貌视图,代码清单6.17列出了完整的使用文件私有数据的globalmem的设备驱动,本程序位于本书配套虚拟机代码的/kernel/drivers/globalmem/ch6目录下。
1/*
2 * a simple char device driver: globalmem without mutex
3 *
4 * Copyright (C) 2014 Barry Song (baohua@kernel.org)
5 *
6 * Licensed under GPLv2 or later.
7 */
8
9#include <linux/module.h>
10#include <linux/fs.h>
11#include <linux/init.h>
12#include <linux/cdev.h>
13#include <linux/slab.h>
14#include <linux/uaccess.h>
15
16#define GLOBALMEM_SIZE 0x1000
17#define MEM_CLEAR 0x1
18#define GLOBALMEM_MAJOR 230
19
20static int globalmem_major = GLOBALMEM_MAJOR;
21module_param(globalmem_major, int, S_IRUGO);
22
23struct globalmem_dev {
24 struct cdev cdev;
25 unsigned char mem[GLOBALMEM_SIZE];
26};
27
28struct globalmem_dev *globalmem_devp;
29
30static int globalmem_open(struct inode *inode, struct file *filp)
31{
32 filp->private_data = globalmem_devp;
33 return 0;
34}
35
36static int globalmem_release(struct inode *inode, struct file *filp)
37{
38 return 0;
39}
40
41static long globalmem_ioctl(struct file *filp, unsigned int cmd,
42 unsigned long arg)
43{
44 struct globalmem_dev *dev = filp->private_data;
45
46 switch (cmd) {
47 case MEM_CLEAR:
48 memset(dev->mem, 0, GLOBALMEM_SIZE);
49 printk(KERN_INFO "globalmem is set to zero\n");
50 break;
51
52 default:
53 return -EINVAL;
54 }
55
56 return 0;
57 }
58
59static ssize_t globalmem_read(struct file *filp, char __user * buf, size_t size,
60 loff_t * ppos)
61{
62 unsigned long p = *ppos;
63 unsigned int count = size;
64 int ret = 0;
65 struct globalmem_dev *dev = filp->private_data;
66
67 if (p >= GLOBALMEM_SIZE)
68 return 0;
69 if (count > GLOBALMEM_SIZE - p)
70 count = GLOBALMEM_SIZE - p;
71
72 if (copy_to_user(buf, dev->mem + p, count)) {
73 ret = -EFAULT;
74 } else {
75 *ppos += count;
76 ret = count;
77
78 printk(KERN_INFO "read %u bytes(s) from %lu\n", count, p);
79 }
80
81 return ret;
82}
83
84static ssize_t globalmem_write(struct file *filp, const char __user * buf,
85 size_t size, loff_t * ppos)
86{
87 unsigned long p = *ppos;
88 unsigned int count = size;
89 int ret = 0;
90 struct globalmem_dev *dev = filp->private_data;
91
92 if (p >= GLOBALMEM_SIZE)
93 return 0;
94 if (count > GLOBALMEM_SIZE - p)
95 count = GLOBALMEM_SIZE - p;
96
97 if (copy_from_user(dev->mem + p, buf, count))
98 ret = -EFAULT;
99 else {
100 *ppos += count;
101 ret = count;
102
103 printk(KERN_INFO "written %u bytes(s) from %lu\n", count, p);
104 }
105
106 return ret;
107 }
108
109static loff_t globalmem_llseek(struct file *filp, loff_t offset, int orig)
110 {
111 loff_t ret = 0;
112 switch (orig) {
113 case 0:
114 if (offset < 0) {
115 ret = -EINVAL;
116 break;
117 }
118 if ((unsigned int)offset > GLOBALMEM_SIZE) {
119 ret = -EINVAL;
120 break;
121 }
122 filp->f_pos = (unsigned int)offset;
123 ret = filp->f_pos;
124 break;
125 case 1:
126 if ((filp->f_pos + offset) > GLOBALMEM_SIZE) {
127 ret = -EINVAL;
128 break;
129 }
130 if ((filp->f_pos + offset) < 0) {
131 ret = -EINVAL;
132 break;
133 }
134 filp->f_pos += offset;
135 ret = filp->f_pos;
136 break;
137 default:
138 ret = -EINVAL;
139 break;
140 }
141 return ret;
142}
143
144static const struct file_operations globalmem_fops = {
145 .owner = THIS_MODULE,
146 .llseek = globalmem_llseek,
147 .read = globalmem_read,
148 .write = globalmem_write,
149 .unlocked_ioctl = globalmem_ioctl,
150 .open = globalmem_open,
151 .release = globalmem_release,
152};
153
154static void globalmem_setup_cdev(struct globalmem_dev *dev, int index)
155{
156 int err, devno = MKDEV(globalmem_major, index);
157
158 cdev_init(&dev->cdev, &globalmem_fops);
159 dev->cdev.owner = THIS_MODULE;
160 err = cdev_add(&dev->cdev, devno, 1);
161 if (err)
162 printk(KERN_NOTICE "Error %d adding globalmem%d", err, index);
163}
164
165static int __init globalmem_init(void)
166{
167 int ret;
168 dev_t devno = MKDEV(globalmem_major, 0);
169
170 if (globalmem_major)
171 ret = register_chrdev_region(devno, 1, "globalmem");
172 else {
173 ret = alloc_chrdev_region(&devno, 0, 1, "globalmem");
174 globalmem_major = MAJOR(devno);
175 }
176 if (ret < 0)
177 return ret;
178
179 globalmem_devp = kzalloc(sizeof(struct globalmem_dev), GFP_KERNEL);
180 if (!globalmem_devp) {
181 ret = -ENOMEM;
182 goto fail_malloc;
183 }
184
185 globalmem_setup_cdev(globalmem_devp, 0);
186 return 0;
187
188 fail_malloc:
189 unregister_chrdev_region(devno, 1);
190 return ret;
191}
192module_init(globalmem_init);
193
194static void __exit globalmem_exit(void)
195{
196 cdev_del(&globalmem_devp->cdev);
197 kfree(globalmem_devp);
198 unregister_chrdev_region(MKDEV(globalmem_major, 0), 1);
199}
200module_exit(globalmem_exit);
201
202MODULE_AUTHOR("Barry Song <baohua@kernel.org>");
203MODULE_LICENSE("GPL v2");
上述代码的行数与代码清单1.3已经不能相比了,除了代码清单1.3中的硬件操作函数仍然需要外,代码清单1.4中还包含了大量暂时陌生的元素,如结构体file_operations、cdev,Linux内核模块声明用的MODULE_AUTHOR、MODULE_LICENSE、module_init、module_exit,以及用于字符设备注册、分配和注销的函数register_chrdev_region()、alloc_chrdev_region()、unregister_chrdev_region()等。我们也不能理解为什么驱动中要包含light_init()、light_cleanup()、light_read()、light_write()等函数。
此时,我们只需要有一个感性认识,那就是,上述暂时陌生的元素都是Linux内核为字符设备定义的,以实现驱动与内核接口而定义的。 Linux对各类设备的驱动都定义了类似的数据结构和函数。
globalmem驱动在用户空间中的验证
在globalmem的源代码目录通过“make”命令编译globalmem的驱动,得到globalmem.ko文件。运行
baohua@baohua-VirtualBox:~/develop/training/kernel/drivers/globalmem/ch6$ sudo
insmod globalmem.ko
命令加载模块,通过“lnsmod”命令,发现globalmem模块已被加载。再通过“cat/proc/devices”命令查看,发现多出了主设备号为230的“globalmem”字符设备驱动:
$ cat /proc/devices
Character devices:
1mem
4/dev/vc/0
4tty
4ttyS
5/dev/tty
5/dev/console
5/dev/ptmx
7vcs
10misc
13input
14sound
21sg
29fb
116alsa
128ptm
136pts
180usb
189usb_device
202cpu/msr
203cpu/cpuid
226drm
230globalmem
249hidraw
250usbmon
251bsg
252ptp
253pps
254rtc
接下来,通过命令
#mknod /dev/globalmem c 230 0
创建“/dev/globalmem”设备节点,并通过“echo’hello world’>/dev/globalmem”命令和“cat/dev/globalmem”命令分别验证设备的写和读,结果证明“hello world”字符串被正确地写入了globalmem字符设备:
# echo "hello world" > /dev/globalmem
# cat /dev/globalmem
hello world
如果启用了sysfs文件系统,将发现多出了/sys/module/globalmem目录,该目录下的树形结构为:
.
├── coresize
├── holders
├── initsize
├── initstate
├── notes
├── parameters
│ └── globalmem_major
├── refcnt
├── sections
│ └── __param
├── taint└── uevent
refcnt记录了globalmem模块的引用计数,sections下包含的几个文件则给出了globalmem所包含的BSS、数据段和代码段等的地址及其他信息。
对于代码清单6.18给出的支持N个globalmem设备的驱动,在加载模块后需创建多个设备节点,
如运行mknod/dev/globalmem0c 2300使得/dev/globalmem0对应主设备号为globalmem_major、次设备号为0的设备,
运行mknod/dev/globalmem1c 2301使得/dev/globalmem1对应主设备号为globalmem_major、次设备号为1的设备。
分别读写/dev/globalmem0和/dev/globalmem1,发现都读写到了正确的对应的设备。
小结
字符设备是3大类设备(字符设备、块设备和网络设备)中的一类,其驱动程序完成的主要工作是
- 初始化、添加和删除cdev结构体,
- 申请和释放设备号,
- 以及填充file_operations结构体中的操作函数,实现file_operations结构体中的read()、write()和ioctl()等函数是驱动设计的主体工作。
内容来自:Linux设备驱动开发详解