第6章字符设备驱动
本章导读
在整个Linux设备驱动的学习中,字符设备驱动较为基础。本章将讲解Linux字符设备驱动程序的结构,并解释其主要组成部分的编程方法。
6.1节讲解了 Linux 字符设备驱动的关键数据结构 cdev 及 file_operations 结构体的操作方法,并分析了 Linux 字符设备的整体结构,给出了简单的设计模板。
6.2节描述了本章及后续各章节所基于的globalmem虚拟字符设备,第6~9章都将基于该虚拟设备实例进行字符设备驱动及并发控制等知识的讲解。
6.3节依据6.1节的知识讲解globalmem设备的驱动编写方法,对读写函数、seek()函数和 I/O 控制函数等进行了重点分析。该节的最后改造globalmem的驱动程序以利用文件私有数据。
6.4节给出了6.3节的globalmem设备驱动在用户空间的验证。
6.1 Linux字符设备驱动结构
6.1.1 cdev结构体
在Linux2.6内核中,使用cdev结构体描述一个字符设备,cdev结构体的定义如代码清单6.1。
代码清单6.1 cdev结构体
1 struct cdev { 2 struct kobject kobj; /* 内嵌的kobject对象 */ 3 struct module *owner; /*所属模块*/ 4 struct file_operations *ops; /*文件操作结构体*/ 5 struct list_head list; 6 dev_t dev; /*设备号*/ 7 unsigned int count; 8 };
cdev结构体的dev_t成员定义了设备号,为32位,其中12位主设备号,20位次设备号。使用下列宏可以从dev_t获得主设备号和次设备号:
MAJOR(dev_t dev) MINOR(dev_t dev)
而使用下列宏则可以通过主设备号和次设备号生成dev_t:
MKDEV(int major, int minor)
cdev 结构体的另一个重要成员 file_operations 定义了字符设备驱动提供给虚拟文件系统的接口函数。
Linux2.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所示。
代码清单6.2 cdev_init()函数
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所示。
代码清单6.3 cdev_alloc()函数
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结构体
file_operations结构体中的成员函数是字符设备驱动程序设计的主体内容,这些函数实际会在应用程序进行 Linux 的open()、write()、read()、close()等系统调用时最终被调用。file_operations结构体目前已经比较庞大,它的定义如代码清单6.4所示。
代码清单6.4 file_operations结构体
1 struct file_operations { 2 struct module *owner; 3 /* 拥有该结构的模块的指针,一般为THIS_MODULES */ 4 loff_t(*llseek)(struct file *, loff_t, int); 5 /* 用来修改文件当前的读写位置*/ 6 ssize_t(*read)(struct file *, char __user *, size_t, loff_t*); 7 /* 从设备中同步读取数据 */ 8 ssize_t(*write)(struct file *, const char __user *, size_t, loff_t*); 9 /* 向设备发送数据*/ 10 ssize_t(*aio_read)(struct kiocb *, char __user *, size_t, loff_t); 11 /* 初始化一个异步的读取操作*/ 12 ssize_t(*aio_write)(struct kiocb *, const char __user *, size_t, loff_t); 13 /* 初始化一个异步的写入操作*/ 14 int(*readdir)(struct file *, void *, filldir_t); 15 /* 仅用于读取目录,对于设备文件,该字段为 NULL */ 16 unsigned int(*poll)(struct file *, struct poll_table_struct*); 17 /* 轮询函数,判断目前是否可以进行非阻塞的读取或写入*/ 18 int(*ioctl)(struct inode *, struct file *, unsigned int, unsigned long); 19 /* 执行设备I/O控制命令*/ 20 long(*unlocked_ioctl)(struct file *, unsigned int, unsigned long); 21 /* 不使用BLK的文件系统,将使用此种函数指针代替ioctl */ 22 long(*compat_ioctl)(struct file *, unsigned int, unsigned long); 23 /* 在64位系统上,32位的ioctl调用,将使用此函数指针代替*/ 24 int(*mmap)(struct file *, struct vm_area_struct*); 25 /* 用于请求将设备内存映射到进程地址空间*/ 26 int(*open)(struct inode *, struct file*); 27 /* 打开 */ 28 int(*flush)(struct file*); 29 int(*release)(struct inode *, struct file*); 30 /* 关闭*/ 31 int (*fsync) (struct file *, struct dentry *, int datasync); 32 /* 刷新待处理的数据*/ 33 int(*aio_fsync)(struct kiocb *, int datasync); 34 /* 异步fsync */ 35 int(*fasync)(int, struct file *, int); 36 /* 通知设备FASYNC标志发生变化*/ 37 int(*lock)(struct file *, int, struct file_lock*); 38 ssize_t(*sendpage)(struct file *, struct page *, int, size_t, loff_t *, int); 39 /* 通常为NULL */ 40 unsigned long(*get_unmapped_area)(struct file *,unsigned long, unsigned long, 41 unsigned long, unsigned long); 42 /* 在当前进程地址空间找到一个未映射的内存段 */ 43 int(*check_flags)(int); 44 /* 允许模块检查传递给fcntl(F_SETEL...)调用的标志 */ 45 int(*dir_notify)(struct file *filp, unsigned long arg); 46 /* 对文件系统有效,驱动程序不必实现*/ 47 int(*flock)(struct file *, int, struct file_lock*); 48 ssize_t (*splice_write)(struct pipe_inode_info *, struct file *, loff_t *, size_t, 49 unsigned int); /* 由VFS调用,将管道数据粘接到文件 */ 50 ssize_t (*splice_read)(struct file *, loff_t *, struct pipe_inode_info *, size_t, 51 unsigned int); /* 由VFS调用,将文件数据粘接到管道 */ 52 int (*setlease)(struct file *, long, struct file_lock **); 53 };
下面我们对file_operations结构体中的主要成员进行分析。
llseek()函数用来修改一个文件的当前读写位置,并将新位置返回,在出错时,这个函数返回一个负值。
read()函数用来从设备中读取数据,成功时函数返回读取的字节数,出错时返回一个负值。
write()函数向设备发送数据,成功时该函数返回写入的字节数。如果此函数未被实现,当用户进行write()系统调用时,将得到-EINVAL返回值。
readdir()函数仅用于目录,设备节点不需要实现它。
ioctl()提供设备相关控制命令的实现(既不是读操作也不是写操作),当调用成功时,返回给调用程序一个非负值。
mmap()函数将设备内存映射到进程内存中,如果设备驱动未实现此函数,用户进行 mmap()系统调用时将获得-ENODEV返回值。这个函数对于帧缓冲等设备特别有意义。
当用户空间调用Linux API函数open()打开设备文件时,设备驱动的open()函数最终被调用。驱动程序可以不实现这个函数,在这种情况下,设备的打开操作永远成功。与 open()函数对应的是release()函数。
poll()函数一般用于询问设备是否可被非阻塞地立即读写。当询问的条件未触发时,用户空间进行select()和poll()系统调用将引起进程的阻塞。
aio_read()和 aio_write()函数分别对与文件描述符对应的设备进行异步读、写操作。设备实现这两个函数后,用户空间可以对该设备文件描述符调用aio_read()、aio_write()等系统调用进行读写。
6.1.4 Linux字符设备驱动的组成
在Linux中,字符设备驱动由如下几个部分组成。
1.字符设备驱动模块加载与卸载函数
在字符设备驱动模块加载函数中应该实现设备号的申请和 cdev 的注册,而在卸载函数中应实现设备号的释放和cdev的注销。
工程师通常习惯为设备定义一个设备相关的结构体,其包含该设备所涉及的cdev、私有数据及信号量等信息。常见的设备结构体、模块加载和卸载函数形式如代码清单6.5所示。
代码清单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()函数,常见的字符设备驱动的这3个函数的形式如代码清单6.6所示。
代码清单6.6 字符设备驱动读、写、I/O控制函数模板
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
I/O控制函数的cmd参数为事先定义的I/O控制命令,而arg为对应于该命令的参数。例如对于串行设备,如果SET_BAUDRATE是一道设置波特率的命令,那后面的arg就应该是波特率值。
在字符设备驱动中,需要定义一个file_operations 的实例,并将具体设备驱动的函数赋值给file_operations的成员,如代码清单6.7所示。
代码清单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.1所示为字符设备驱动的结构、字符设备驱动与字符设备以及字符设备驱动与用户空间访问该设备的程序之间的关系。
图6.1 字符设备驱动的结构
6.2 globalmem虚拟设备实例描述
从本章开始,后续的数章都将基于虚拟的globalmem设备进行字符设备驱动的讲解。globalmem意味着“全局内存”,在globalmem字符设备驱动中会分配一片大小为GLOBALMEM_SIZE(4KB)的内存空间,并在驱动中提供针对该片内存的读写、控制和定位函数,以供用户空间的进程能通过Linux系统调用访问这片内存。
实际上,这个虚拟的globalmem设备几乎没有任何实用价值,仅仅是一种为了讲解问题的方便而凭空制造的设备。当然,它也并非百无一用,由于globalmem可被两个或两个以上的进程同时访问,其中的全局内存可作为用户空间进程进行通信的一种蹩脚的手段。
本章将给出globalmem设备驱动的雏形,而后续章节会在这个雏形的基础上添加并发与同步控制等复杂功能。
6.3 globalmem设备驱动
6.3.1 头文件、宏及设备结构体
在globalmem字符设备驱动中,应包含它要使用的头文件,并定义globalmem设备结构体及相关宏。
代码清单6.8 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包含在一个设备结构体中,但这样定义的好处在于,它借用了面向对象程序设计中“封装”的思想,体现了一种良好的编程习惯。
6.3.2 加载与卸载设备驱动
globalmem设备驱动的模块加载和卸载函数遵循代码清单6.5的类似模板,其实现的工作与代码清单6.5完全一致,如代码清单6.9所示。
代码清单6.9 globalmem设备驱动模块加载与卸载函数
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所示。
代码清单6.10 初始化并添加cdev结构体
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 }
在cdev_init()函数中,与globalmem的cdev关联的file_operations结构体如代码清单6.11所示。
代码清单6.11 globalmem设备驱动文件操作结构体
1 static const struct file_operations globalmem_fops = { 2 .owner = THIS_MODULE, 3 .llseek = globalmem_llseek, 4 .read = globalmem_read, 5 .write = globalmem_write, 6 .ioctl = globalmem_ioctl, 7 };
6.3.3 读写函数
globalmem 设备驱动的读写函数主要是让设备结构体的mem[]数组与用户空间交互数据,并随着访问的字节数变更返回给用户的文件读写偏移位置。读和写函数的实现分别如代码清单6.12和6.13所示。
代码清单6.12 globalmem设备驱动读函数
1 static ssize_t globalmem_read(struct file *filp, char __user *buf, size_t count, 2 loff_t *ppos) 3 { 4 unsigned long p = *ppos; 5 int ret = 0; 6 7 /*分析和获取有效的读长度*/ 8 if (p >= GLOBALMEM_SIZE) /* 要读的偏移位置越界 9 return 0; 10 if (count > GLOBALMEM_SIZE - p)/* 要读的字节数太大 11 count = GLOBALMEM_SIZE - p; 12 13 /*内核空间→用户空间*/ 14 if (copy_to_user(buf, (void*)(dev.mem + p), count)) 15 ret = - EFAULT; 16 else { 17 *ppos += count; 18 ret = count; 19 20 printk(KERN_INFO "read %d bytes(s) from %d\n", count, p); 21 } 22 23 return ret; 24 }
代码清单6.13 globalmem设备驱动写函数
1 static ssize_t globalmem_write(struct file *filp, const char __user *buf, 2 size_t count, loff_t *ppos) 3 { 4 unsigned long p = *ppos; 5 int ret = 0; 6 7 /*分析和获取有效的写长度*/ 8 if (p >= GLOBALMEM_SIZE) /* 要写的偏移位置越界 9 return 0; 10 if (count > GLOBALMEM_SIZE - p) /* 要写的字节数太多 11 count = GLOBALMEM_SIZE - p; 12 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 %d bytes(s) from %d\n", count, p); 21 } 22 23 return ret; 24 }
6.3.4 seek函数
seek()函数对文件定位的起始地址可以是文件开头(SEEK_SET,0)、当前位置(SEEK_CUR,1)和文件尾(SEEK_END,2),globalmem支持从文件开头和当前位置相对偏移。
在定位的时候,应该检查用户请求的合法性,若不合法,函数返回- EINVAL,合法时返回文件的当前位置,如代码清单6.14。
代码清单6.14 globalmem设备驱动seek()函数
1 static loff_t globalmem_llseek(struct file *filp, loff_t offset, int orig) 2 { 3 loff_t ret; 4 switch (orig) { 6 case 0: /*从文件开头开始偏移*/ 7 if (offset < 0) { 8 ret = - EINVAL; 9 break; 10 } 11 if ((unsigned int)offset > GLOBALMEM_SIZE) { 12 ret = - EINVAL; 13 break; 14 } 15 filp->f_pos = (unsigned int)offset; 16 ret = filp->f_pos; 17 break; 18 case 1: /*从当前位置开始偏移*/ 19 if ((filp->f_pos + offset) > GLOBALMEM_SIZE) { 20 ret = - EINVAL; 21 break; 22 } 23 if ((filp->f_pos + offset) < 0) { 24 ret = - EINVAL; 25 break; 26 } 27 filp->f_pos += offset; 28 ret = filp->f_pos; 29 break; 30 default: 31 ret = - EINVAL; 32 } 33 return ret; 34 }
6.3.5 ioctl函数
1.globalmem设备驱动的ioctl()函数
globalmem设备驱动的ioctl()函数接受MEM_CLEAR命令,这个命令会将全局内存的有效数据长度清0,对于设备不支持的命令,ioctl()函数应该返回- EINVAL,如代码清单6.15所示。
代码清单6.15 globalmem设备驱动的I/O控制函数
1 static int globalmem_ioctl(struct inode *inodep, struct file *filp, unsigned 2 int cmd, unsigned long arg) 3 { 4 switch (cmd) { 5 case MEM_CLEAR: 6 /* 清除全局内存 7 memset(dev->mem, 0, GLOBALMEM_SIZE); 8 printk(KERN_INFO "globalmem is set to zero\n"); 9 break; 10 11 default: 12 return - EINVAL; /* 其他不支持的命令 13 } 14 return 0; 15 }
在上述程序中,MEM_CLEAR 被宏定义为 0x01,实际上并不是一种值得推荐的方法,简单地对命令定义为0x0、0x1、0x2等类似值会导致不同的设备驱动拥有相同的命令号。如果设备A、B都支持0x0、0x1、0x2这样的命令,假设用户本身希望给A发0x1命令,可是不经意间发给了B,这个时候B 因为支持该命令,它就会执行该命令。因此,Linux 内核推荐采用一套统一的ioctl()命令生成方式。
2.ioctl()命令
Linux建议以如图6.2所示的方式定义ioctl()的命令。
图6.2 I/O控制命令的组成
命令码的设备类型字段为一个“幻数”,可以是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.16所示。
代码清单6.16 _IO()、_IOR()、_IOW()和_IOWR()宏定义
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 … #define MEM_CLEAR _IO(GLOBALMEM_MAGIC,0)
3.预定义命令
内核中预定义了一些I/O控制命令,如果某设备驱动中包含了与预定义命令一样的命令码,这些命令会被当作预定义命令被内核处理而不是被设备驱动处理,预定义命令有如下4种。
(1)FIOCLEX:即File IOctl Close on Exec,对文件设置专用标志,通知内核当exec()系统调用发生时自动关闭打开的文件。
(2)FIONCLEX:即File IOctl Not CLose on Exec,与FIOCLEX标志相反,清除由FIOCLEX命令设置的标志。
(3)FIOQSIZE:获得一个文件或者目录的大小,当用于设备文件时,返回一个ENOTTY错误。(4)FIONBIO:即File IOctl Non-Blocking I/O,这个调用修改在 filp->f_flags中的O_NONBLOCK标志。
FIOCLEX、FIONCLEX、FIOQSIZE和FIONBIO这些宏的定义为:
#define FIONCLEX 0x5450 #define FIOCLEX 0x5451 #define FIOQSIZE 0x5460 #define FIONBIO 0x5421
6.3.6 使用文件私有数据
6.3.1~6.3.5节给出的代码完整地实现了预期的globalmem雏形,在其代码中,为globalmem设备结构体globalmem_dev定义了全局实例dev(见代码清单6.7第25行),而globalmem的驱动中read()、write()、ioctl()、llseek()函数都针对dev进行操作。
实际上,大多数Linux驱动工程师遵循一个“潜规则”,那就是将文件的私有数据private_data指向设备结构体,在read()、write()、ioctl()、llseek()等函数通过private_data访问设备结构体。
这个时候,我们要将各函数进行少量的修改,为了让读者朋友建立字符设备驱动的全貌视图,代码清单6.17列出了完整的使用文件私有数据的globalmem的设备驱动,本程序位于虚拟机/home/lihacker/develop/svn/ldd6410-read-only/training/kernel/drivers/globalmem/ch6目录。
代码清单6.17使用文件私有数据的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 *globalmem_devp; /*设备结构体指针*/ 25 /*文件打开函数*/ 26 int globalmem_open(struct inode *inode, struct file *filp) 27 { 28 /*将设备结构体指针赋值给文件私有数据指针*/ 29 filp->private_data = globalmem_devp; 30 return 0; 31 } 32 /*文件释放函数*/ 33 int globalmem_release(struct inode *inode, struct file *filp) 34 { 35 return 0; 36 } 37 38 /* ioctl设备控制函数 */ 39 static int globalmem_ioctl(struct inode *inodep, struct file *filp, unsigned 40 int cmd, unsigned long arg) 41 { 42 struct globalmem_dev *dev = filp->private_data;/*获得设备结构体指针*/ 43 44 switch (cmd) { 45 case MEM_CLEAR: 46 memset(dev->mem, 0, GLOBALMEM_SIZE); 47 printk(KERN_INFO "globalmem is set to zero\n"); 48 break; 49 50 default: 51 return - EINVAL; 52 } 53 54 return 0; 55 } 56 57 /*读函数*/ 58 static ssize_t globalmem_read(struct file *filp, char __user *buf, size_t size, 59 loff_t *ppos) 60 { 61 unsigned long p = *ppos; 62 unsigned int count = size; 63 int ret = 0; 64 struct globalmem_dev *dev = filp->private_data;/*获得设备结构体指针*/ 65 66 /*分析和获取有效的写长度*/ 67 if (p >= GLOBALMEM_SIZE) 68 return 0; 69 if (count > GLOBALMEM_SIZE - p) 70 count = GLOBALMEM_SIZE - p; 71 72 /*内核空间→用户空间*/ 73 if (copy_to_user(buf, (void *)(dev->mem + p), count)) { 74 ret = - EFAULT; 75 } else { 76 *ppos += count; 77 ret = count; 78 79 printk(KERN_INFO "read %u bytes(s) from %lu\n", count, p); 80 } 81 82 return ret; 83 } 84 85 /*写函数*/ 86 static ssize_t globalmem_write(struct file *filp, const char __user *buf, 87 size_t size, loff_t *ppos) 88 { 89 unsigned long p = *ppos; 90 unsigned int count = size; 91 int ret = 0; 92 struct globalmem_dev *dev = filp->private_data;/*获得设备结构体指针*/ 93 94 /*分析和获取有效的写长度*/ 95 if (p >= GLOBALMEM_SIZE) 96 return 0; 97 if (count > GLOBALMEM_SIZE - p) 98 count = GLOBALMEM_SIZE - p; 99 100 /*用户空间→内核空间*/ 101 if (copy_from_user(dev->mem + p, buf, count)) 102 ret = - EFAULT; 103 else { 104 *ppos += count; 105 ret = count; 106 107 printk(KERN_INFO "written %u bytes(s) from %lu\n", count, p); 108 } 109 110 return ret; 111 } 112 113 /* seek文件定位函数 */ 114 static loff_t globalmem_llseek(struct file *filp, loff_t offset, int orig) 115 { 116 loff_t ret = 0; 117 switch (orig) { 118 case 0: /*相对文件开始位置偏移*/ 119 if (offset < 0) { 120 ret = - EINVAL; 121 break; 122 } 123 if ((unsigned int)offset > GLOBALMEM_SIZE) { 124 ret = - EINVAL; 125 break; 126 } 127 filp->f_pos = (unsigned int)offset; 128 ret = filp->f_pos; 129 break; 130 case 1: /*相对文件当前位置偏移*/ 131 if ((filp->f_pos + offset) > GLOBALMEM_SIZE) { 132 ret = - EINVAL; 133 break; 134 } 135 if ((filp->f_pos + offset) < 0) { 136 ret = - EINVAL; 137 break; 138 } 139 filp->f_pos += offset; 140 ret = filp->f_pos; 141 break; 142 default: 143 ret = - EINVAL; 144 break; 145 } 146 return ret; 147 } 148 149 /*文件操作结构体*/ 150 static const struct file_operations globalmem_fops = { 151 .owner = THIS_MODULE, 152 .llseek = globalmem_llseek, 153 .read = globalmem_read, 154 .write = globalmem_write, 155 .ioctl = globalmem_ioctl, 156 .open = globalmem_open, 157 .release = globalmem_release, 158 }; 159 160 /*初始化并注册cdev*/ 161 static void globalmem_setup_cdev(struct globalmem_dev *dev, int index) 162 { 163 int err, devno = MKDEV(globalmem_major, index); 164 165 cdev_init(&dev->cdev, &globalmem_fops); 166 dev->cdev.owner = THIS_MODULE; 167 err = cdev_add(&dev->cdev, devno, 1); 168 if (err) 169 printk(KERN_NOTICE "Error %d adding globalmem %d", err, index); 170 } 171 172 /*设备驱动模块加载函数*/ 173 int globalmem_init(void) 174 { 175 int result; 176 dev_t devno = MKDEV(globalmem_major, 0); 177 178 /* 申请设备号*/ 179 if (globalmem_major) 180 result = register_chrdev_region(devno, 1, "globalmem"); 181 else { /* 动态申请设备号 */ 182 result = alloc_chrdev_region(&devno, 0, 1, "globalmem"); 183 globalmem_major = MAJOR(devno); 184 } 185 if (result < 0) 186 return result; 187 188 /* 动态申请设备结构体的内存*/ 189 globalmem_devp = kmalloc(sizeof(struct globalmem_dev), GFP_KERNEL); 190 if (!globalmem_devp) { /*申请失败*/ 191 result = - ENOMEM; 192 goto fail_malloc; 193 } 194 195 memset(globalmem_devp, 0, sizeof(struct globalmem_dev)); 196 197 globalmem_setup_cdev(globalmem_devp, 0); 198 return 0; 199 200 fail_malloc: 201 unregister_chrdev_region(devno, 1); 202 return result; 203 } 204 205 /*模块卸载函数*/ 206 void globalmem_exit(void) 207 { 208 cdev_del(&globalmem_devp->cdev); /*注销cdev*/ 209 kfree(globalmem_devp); /*释放设备结构体内存*/ 210 unregister_chrdev_region(MKDEV(globalmem_major, 0), 1);/*释放设备号*/ 211 } 212 213 MODULE_AUTHOR("Barry Song <21cnbao@gmail.com>"); 214 MODULE_LICENSE("Dual BSD/GPL"); 215 216 module_param(globalmem_major, int, S_IRUGO); 217 218 module_init(globalmem_init); 219 module_exit(globalmem_exit);
除了在globalmem_open()函数中通过filp->private_data = globalmem_devp语句(见第29行)将设备结构体指针赋值给文件私有数据指针并在 globalmem_read()、globalmem_write()、globalmem_llseek()和globalmem_ioctl()函数中通过struct globalmem_dev *dev = filp->private_data语句获得设备结构体指针并使用该指针操作设备结构体外,代码清单6.17与代码清单6.7~6.15的程序基本相同。
读者朋友们,这个时候,请您翻回到本书的第1章,再次阅读代码清单1.4,即Linux下LED的设备驱动,是否豁然开朗?
代码清单6.17仅仅作为使用private_data的范例,实际上,在这个程序中使用private_data没有任何意义,直接访问全局变量globalmem_devp会更加结构清晰。如果globalmem不只包括一个设备,而是同时包括两个或两个以上的设备,采用private_data的优势就会集中显现出来。
在不对代码清单6.17中的globalmem_read()、globalmem_write()、globalmem_ioctl()等重要函数及 globalmem_fops 结构体等数据结构进行任何修改的前提下,只是简单地修改 globalmem_init()、globalmem_exit()和globalmem_open(),就可以轻松地让globalmem驱动中包含两个同样的设备(次设备号分别为0和1),如代码清单6.18所示。
代码清单6.18支持2个globalmem设备的globalmem驱动
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 } 11 12 /*设备驱动模块加载函数*/ 13 int globalmem_init(void) 14 { 15 int result; 16 dev_t devno = MKDEV(globalmem_major, 0); 17 18 /* 申请设备号*/ 19 if (globalmem_major) 20 result = register_chrdev_region(devno, 2, "globalmem"); 21 else { /* 动态申请设备号 */ 23 result = alloc_chrdev_region(&devno, 0, 2, "globalmem"); 24 globalmem_major = MAJOR(devno); 25 } 26 if (result < 0) 27 return result; 28 29 /* 动态申请两个设备结构体的内存*/ 30 globalmem_devp = kmalloc(2*sizeof(struct globalmem_dev), GFP_KERNEL); 31 if (!globalmem_devp) { /*申请失败*/ 33 result = - ENOMEM; 34 goto fail_malloc; 35 } 36 memset(globalmem_devp, 0, 2*sizeof(struct globalmem_dev)); 37 38 globalmem_setup_cdev(&globalmem_devp[0], 0); 39 globalmem_setup_cdev(&globalmem_devp[1], 1); 40 return 0; 41 42 fail_malloc: unregister_chrdev_region(devno, 1); 43 return result; 44 } 45 46 /*模块卸载函数*/ 47 void globalmem_exit(void) 48 { 49 cdev_del(&(globalmem_devp[0].cdev)); 50 cdev_del(&(globalmem_devp[1].cdev)); /* 注销cdev */ 51 kfree(globalmem_devp); /*释放设备结构体内存*/ 52 unregister_chrdev_region(MKDEV(globalmem_major, 0), 2); /*释放设备号*/ 53 } /* 其他代码同清单6.16 */
代码清单6.18第7行调用的container_of()的作用是通过结构体成员的指针找到对应结构体的指针,这个技巧在Linux内核编程中十分常用。在container_of(inode->i_cdev,struct globalmem_dev, cdev)语句中,传给container_of()的第1个参数是结构体成员的指针,第2个参数为整个结构体的类型,第3个参数为传入的第1个参数即结构体成员的类型,container_of()返回值为整个结构体的指针。
6.4 globalmem驱动在用户空间的验证
在对应目录通过“make”命令编译globalmem的驱动,得到globalmem.ko文件。运行:
lihacker@lihacker-laptop: ~ /develop/svn/ldd6410-read-only/training/kernel/drivers/ globalmem/ch6$ sudo su root@lihacker-laptop:/home/lihacker/develop/svn/ldd6410-read-only/training/kernel/d rivers/globalmem/ch6# insmod globalmem.ko
命令加载模块,通过“lsmod”命令,发现globalmem模块已被加载。再通过“cat /proc/devices”命令查看,发现多出了主设备号为250的“globalmem”字符设备驱动:
root@lihacker-laptop:/home/lihacker/develop/svn/ldd6410-read-only/training/kernel/d
rivers/globalmem/ch6# cat /proc/devices
Character devices:
1 mem
4 /dev/vc/0
4 tty
4 ttyS
5 /dev/tty
5 /dev/console
5 /dev/ptmx
6 lp
7 vcs
10 misc
13 input
14 sound
21 sg
29 fb
99 ppdev
108 ppp
116 alsa
128 ptm
136 pts
180 usb
188 ttyUSB
189 usb_device
216 rfcomm
226 drm
250 globalmem
接下来,通过命令:
root@lihacker-laptop:/home/lihacker/develop/svn/ldd6410-read-only/training/kernel/d
rivers/globalmem/ch6# mknod /dev/globalmem c 250 0
创建“/dev/globalmem”设备节点,并通过“echo 'hello world' > /dev/globalmem”命令和“cat/dev/globalmem”命令分别验证设备的写和读,结果证明“hello world”字符串被正确地写入globalmem字符设备:
root@lihacker-laptop:/home/lihacker/develop/svn/ldd6410-read-only/training/kernel/d rivers/globalmem/ch6# echo "hello world" > /dev/globalmem root@lihacker-laptop:/home/lihacker/develop/svn/ldd6410-read-only/training/kernel/d rivers/globalmem/ch6# cat /dev/globalmem hello world
如果启用了sysfs文件系统,将发现多出了/sys/module/globalmem目录,该目录下的树型结构为:
|-- refcnt '-- sections |-- .bss |-- .data |-- .gnu.linkonce.this_module |-- .rodata |-- .rodata.str1.1 |-- .strtab |-- .symtab |-- .text '-- __versions
refcnt记录了globalmem模块的引用计数,sections下包含的数个文件则给出了globalmem所包含的BSS、数据段和代码段等的地址及其他信息。
对于代码清单6.18给出的支持两个globalmem设备的驱动,在加载模块后需创建两个设备节点,/dev/globalmem0对应主设备号globalmem_major,次设备号0,/dev/globalmem1对应主设备号 globalmem_major,次设备号1。分别读写/dev/globalmem0 和/dev/globalmem1,发现都读写到了正确的对应的设备。
6.5 总结
字符设备是3大类设备(字符设备、块设备和网络设备)中较简单的一类设备,其驱动程序中完成的主要工作是初始化、添加和删除cdev结构体,申请和释放设备号,以及填充file_operations结构体中的操作函数,实现 file_operations 结构体中的read()、write()和 ioctl()等函数是驱动设计的主体工作。