第6章 简单的字符设备驱动程序   在Linux设备驱动程序的家族中,字符设备驱动程序是较为简单的驱动程序,同时也是应用非常广泛的驱动程序。所以学习字符设备驱动程序,对构建Linux设备驱动程序的知识结构非常重要。本章将带领读者编写一个完整的字符设备驱动程序。 6.1 字符设备驱动程序框架   本节对字符设备驱动程序框架进行了简要的分析。字符设备驱动程序中有许多非常重要的概念,下面将从最简单的概念讲起:字符设备和块设备。 6.1.1 字符设备和块设备   Linux系统将设备分为3种类型:字符设备、块设备和网络接口设备。其中字符设备和块设备难以区分,下面将对其进行重要讲解。   1.字符设备   字符设备是指那些只能一个字节一个字节读写数据的设备,不能随机读取设备内存中的某一数据。其读取数据需要按照先后顺序,从这点来看,字符设备是面向数据流的设备。常见的字符有鼠标、键盘、串口、控制台和LED等设备。   2.块设备   块设备是指那些可以从设备的任意位置读取一定长度数据的设备。其读取数据不必按照先后顺序,可以定位到设备的某一具体位置,读取数据。常见的块设备有硬盘、磁盘、U盘、SD卡等。   3.字符设备和块设备的区分   每一个字符设备或者块设备都在/dev目录下对应一个设备文件。读者可以通过查看/dev目录下的文件的属性,来区分设备是字符设备还是块设备。使用cd命令进入/dev目录,并执行ls –l命令就可以看到设备的属性。 [root@tom /]# cd /dev /*进入/dev目录*/ [root@tom dev]# ls -l /*列出/dev中文件的信息*/、 /*第1字段 2 3 4 5 6 7 8 */ crw-rw----+ 1 root root 14, 12 12-21 22:56 adsp crw------- 1 root root 10, 175 12-21 22:56 agpgart crw-rw----+ 1 root root 14, 4 12-21 22:56 audio brw-r----- 1 root disk 253, 0 12-21 22:56 dm-0 brw-r----- 1 root disk 253, 1 12-21 22:56 dm-1 crw-rw---- 1 root root 14, 9 12-21 22:56 dmmidi   ls –l命令的第一字段中的第一个字符c表示设备是字符设备,b表示设备是块设备。第234字段对驱动程序开发来说没有关系。第5,6字段分别表示设备的主设备号和次设备号,将在6.1.2节讲解。第7字段表示文件的最后修改时间。第8字段表示设备的名字。   由第1和8字段可知,adsp是字符设备,dm-0是块设备。其中adsp设备的主设备号是14,次设备号是12。 6.1.2 主设备号和次设备号   一个字符设备或者块设备都有一个主设备号和次设备号。主设备号和次设备号统称为设备号。主设备号用来表示一个特定的驱动程序。次设备号用来表示使用该驱动程序的各设备。例如一个嵌入式系统,有两个LED指示灯,LED灯需要独立的打开或者关闭。那么,可以写一个LED灯的字符设备驱动程序,可以将其主设备号注册成5号设备,次设备号分别为1和2。这里,次设备号就分别表示两个LED灯。   1.主设备号和次设备号的表示   在Linux内核中,dev_t类型用来表示设备号。在Linux 2.6.29.4中,dev_t定义为一个无符号长整型变量,如下: typedef u_long dev_t;   u_long在32位机中是4个字节,在64位机中是8字节。以32位机为例,其中高12表示主设备号,低20为表示次设备号,如图6.1所示。 图6.1 dev_t结构   2.主设备号和次设备号的获取   为了写出可移植的驱动程序,不能假定主设备号和次设备号的位数。不同的机型中,主设备号和次设备号的位数可能是不同的。应该使用MAJOR宏得到主设备号,使用MINOR宏来得到次设备号。下面是两个宏的定义: #define MINORBITS 20 /*次设备号位数*/ #define MINORMASK ((1U << MINORBITS) - 1) /*次设备号掩码*/ #define MAJOR(dev) ((unsigned int) ((dev) >> MINORBITS)) /*dev右移20位得到主设备号*/ #define MINOR(dev) ((unsigned int) ((dev) & MINORMASK)) /*与次设备掩码与,得到次设备号*/   MAJOR宏将dev_t向右移动20位,得到主设备号;MINOR宏将dev_t的高12位清零,得到次设备号。相反,可以将主设备号和次设备号转换为设备号类型(dev_t),使用宏MKDEV可以完成这个功能。 #define MKDEV(ma,mi) (((ma) << MINORBITS) | (mi))   MKDEV宏将主设备号(ma)左移20位,然后与次设备号(mi)相与,得到设备号。   3.静态分配设备号   静态分配设备号,就是驱动程序开发者,静态地指定一个设备号。对于一部分常用的设备,内核开发者已经为其分配了设备号。这些设备号可以在内核源码documentation/ devices.txt文件中找到。如果只有开发者自己使用这些设备驱动程序,那么其可以选择一个尚未使用的设备号。在不添加新硬件的时候,这种方式不会产生设备号冲突。但是当添加新硬件时,则很可能造成设备号冲突,影响设备的使用。   4.动态分配设备号   由于静态分配设备号存在冲突的问题,所以内核社区建议开发者使用动态分配设备号的方法。动态分配设备号的函数是alloc_chrdev_region(),该函数将在“申请和释放设备号”一节讲述。   5.查看设备号   当静态分配设备号时,需要查看系统中已经存在的设备号,从而决定使用哪个新设备号。可以读取/proc/devices文件获得设备的设备号。/proc/devices文件包含字符设备和块设备的设备号,如下所示。 [root@tom /]# cat /proc/devices /*cat命令查看/proc/devices文件的内容*/ Character devices: /*字符设备*/ 1 mem 4 /dev/vc/0 7 vcs 13 input 14 sound 21 sg Block devices: /*块设备*/ 1 ramdisk 2 fd 8 sd 253 device-mapper 254 mdp 6.1.3 申请和释放设备号   内核维护着一个特殊的数据结构,用来存放设备号与设备的关系。在安装设备时,应该给设备申请一个设备号,使系统可以明确设备对应的设备号。设备驱动程序中的很多功能,是通过设备号来操作设备的。下面,首先对申请设备号进行简述。   1.申请设备号   在构建字符设备之前,首先要向系统申请一个或者多个设备号。完成该工作的函数是register_chrdev_region(),该函数在中定义: int register_chrdev_region(dev_t from, unsigned count, const char *name);   其中,from是要分配的设备号范围的起始值。一般只提供from的主设备号,from的次设备号通常被设置成0。count是需要申请的连续设备号的个数。最后name是和该范围编号关联的设备名称,该名称不能超过64字节。   和大多数内核函数一样,register_chrdev_region()函数成功时返回0。错误时,返回一个负的错误码,并且不能为字符设备分配设备号。下面是一个例子代码,其申请了CS5535_GPIO_COUNT个设备号。 retval = register_chrdev_region(dev_id, CS5535_GPIO_COUNT,NAME);   在Linux中有非常多的字符设备,在人为的为字符设备分配设备号时,很可能发生冲突。Linux内核开发者一直在努力将设备号变为动态的。可以使用alloc_chrdev_region()函数达到这个目的。 int alloc_chrdev_region(dev_t *dev, unsigned baseminor, unsigned count, const char *name)   在上面的函数中,dev作为输出参数,在函数成功返回后将保存已经分配的设备号。函数有可能申请一段连续的设备号,这是dev返回第一个设备号。baseminor表示要申请的第一个次设备号,其通常设为0。count和name与register_chrdev_region()函数的对应参数一样。count表示要申请的连续设备号个数,name表示设备的名字。下面是一个例子代码,其申请了CS5535_GPIO_COUNT个设备号。 retval = alloc_chrdev_region(&dev_id, 0, CS5535_GPIO_COUNT, NAME);   2.释放设备号   使用上面两种方式申请的设备号,都应该在不使用设备时,释放设备号。设备号的释放统一使用下面的函数: void unregister_chrdev_region(dev_t from, unsigned count);   在上面这个函数中,from表示要释放的设备号,count表示从from开始要释放的设备号个数。通常,在模块的卸载函数中调用unregister_chrdev_region()函数。 6.2 初识cdev结构   当申请字符设备的设备号后,这时,需要将字符设备注册到系统中,才能使用字符设备。为了理解这个实现过程,首先解释一下cdev结构体。 6.2.1 cdev结构体   在Linux内核中使用cdev结构体描述字符设备。该结构体是所有字符设备的抽象,其包含了大量字符设备所共有的特性。cdev结构体定义如下: 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结构中的kobj结构用于内核管理字符设备,驱动开发人员一般不使用该成员。ops是指向file_operations结构的指针,该结构定义了操作字符设备的函数。由于此结构体较为复杂,所以将在6.2.2 file_operations结构体一节讲解。   dev就是用来存储字符设备所申请的设备号。count表示目前有多少个字符设备在使用该驱动程序。当使用rmmod卸载模块时,如果count成员不为0,那么系统不允许卸载 模块。   list结构是一个双向链表,用于将其他结构体连接成一个双向链表。该结构在Linux内核中广泛使用,需要读者掌握。 struct list_head { struct list_head *next, *prev; }; 图6.2 cdev与inode的关系   如图6.2所示,cdev结构体的list成员连接到了inode结构体i_devices成员。其中i_devices也是一个list_head结构。这样,使cdev结构与inode结点组成了一个双向链表。inode结构体表示/dev目录下的设备文件,该结构体较为复杂,所以将在下面讲述。   每一个字符设备在/dev目录下都有一个设备文件,打开设备文件就相当于打开相应的字符设备。例如应用程序打开设备文件A,那么系统会产生一个inode结点。这样可以通过inode结点的i_cdev字段找到cdev字符结构体。通过cdev的ops指针,就能找到设备A的操作函数。对操作函数的讲解,将放在后面的内容中。 6.2.2 file_operations结构体   file_operations是一个对设备进行操作的抽象结构体。Linux内核的设计非常巧妙。内核允许为设备建立一个设备文件,对设备文件的所有操作,就相当于对设备的操作。这样的好处是,用户程序可以使用访问普通文件的方法访问设备文件,进而访问设备。这样的方法,极大地减轻了程序员的编程负担,程序员不必去熟悉新的驱动接口,就能够访问 设备。   对普通文件的访问常常使用open()、read()、write()、close()、ioctl()等方法。同样对设备文件的访问,也可以使用这些方法。这些调用最终会引起对file_operations结构体中对应函数的调用。对于程序员来说,只要为不同的设备编写不同的操作函数就可以了。   为了增加file_operations的功能,所以将很多函数集中在了该结构中。该结构的定义目前已经比较庞大了,其定义如下: struct file_operations { struct module *owner; loff_t (*llseek) (struct file *, loff_t, int); ssize_t (*read) (struct file *, char __user *, size_t, loff_t *); ssize_t (*write) (struct file *, const char __user *, size_t, loff_t *); ssize_t (*aio_read) (struct kiocb *, const struct iovec *, unsigned long, loff_t); ssize_t (*aio_write) (struct kiocb *, const struct iovec *, unsigned long, loff_t); int (*readdir) (struct file *, void *, filldir_t); unsigned int (*poll) (struct file *, struct poll_table_struct *); int (*ioctl) (struct inode *, struct file *, unsigned int, unsigned long); long (*unlocked_ioctl) (struct file *, unsigned int, unsigned long); long (*compat_ioctl) (struct file *, unsigned int, unsigned long); int (*mmap) (struct file *, struct vm_area_struct *); int (*open) (struct inode *, struct file *); int (*flush) (struct file *, fl_owner_t id); int (*release) (struct inode *, struct file *); int (*fsync) (struct file *, struct dentry *, int datasync); int (*aio_fsync) (struct kiocb *, int datasync); int (*fasync) (int, struct file *, int); int (*lock) (struct file *, int, struct file_lock *); ssize_t (*sendpage) (struct file *, struct page *, int, size_t, loff_t *, int); unsigned long (*get_unmapped_area)(struct file *, unsigned long, unsigned long, unsigned long, unsigned long); int (*check_flags)(int); int (*flock) (struct file *, int, struct file_lock *); ssize_t (*splice_write)(struct pipe_inode_info *, struct file *, loff_t *, size_t, unsigned int); ssize_t (*splice_read)(struct file *, loff_t *, struct pipe_inode_info *, size_t, unsigned int); int (*setlease)(struct file *, long, struct file_lock **); };   下面对file_operations结构体的重要成员进行讲解。 * owner成员根本不是一个函数;它是一个指向拥有这个结构模块的指针。这个成员用来维持模块的引用计数,当模块还在使用时,不能用rmmod卸载模块。几乎所有时刻,它被简单初始化为 THIS_MODULE,一个在中定义的宏。 * llseek()函数用来改变文件中的当前读/写位置,并将新位置返回。loff_t参数是一个“long long”类型,“long long”类型即使在32位机上也是64位宽。这是为了与64位机兼容而定的,因为64位机的文件大小完全可以突破4G。 * read()函数用来从设备中获取数据,成功时函数返回读取的字节数,失败时返回一个负的错误码。 * write()函数用来写数据到设备中。成功时该函数返回写入的字节数,失败时返回一个负的错误码。 * ioctl()函数提供了一种执行设备特定命令的方法。例如使设备复位,这既不是读操作也不是写操作,不适合用read()和write()方法来实现。如果在应用程序中给ioctl传入没有定义的命令,那么将返回-ENOTTY的错误,表示该设备不支持这个命令。 * open()函数用来打开一个设备,在该函数中可以对设备进行初始化。如果这个函数被复制NULL,那么设备打开永远成功,并不会对设备产生影响。 * release()函数用来释放open()函数中申请的资源,将在文件引用计数为0时,被系统调用。其对应应用程序的close()方法,但并不是每一次调用close()方法,都会触发release()函数,在对设备文件的所有打开都释放后,才会被调用。 6.2.3 cdev和file_operations结构体的关系   一般来说,驱动开发人员会将特定设备的特定数据放到cdev结构体后,组成一个新的结构体。如图6.3所示,“自定义字符设备”中就包含特定设备的数据。该“自定义设备”中有一个cdev结构体。cdev结构体中有一个指向file_operations的指针。这里,file_operations中的函数就可以用来操作硬件,或者“自定义字符设备”中的其他数据,从而起到控制设备的作用。 图6.3 cdev与file_operations结构体的关系 6.2.4 inode结构体   内核使用inode结构在内部表示文件。inode一般作为file_operations结构中函数的参数传递过来。例如,open()函数将传递一个inode指针进来,表示目前打开的文件结点。需要注意的是,inode的成员已经被系统赋予了合适的值,驱动程序只需要使用该结点中的信息,而不用更改。Oepn()函数为: int (*open) (struct inode *, struct file *);   inode结构中包含大量的有关文件的信息。这里,只对编写驱动程序有用的字段进行介绍,对于该结构更多的信息,可以参看内核源码。 * dev_t i_rdev,表示设备文件对应的设备号。 * struct list_head i_devices,如图6.2所示,该成员使设备文件连接到对应的cdev结构,从而对应到自己的驱动程序。 * struct cdev *i_cdev,如图6.2所示,该成员也指向cdev设备。   除了从dev_t得到主设备号和次设备号外,这里还可以使用imajor()和iminor()函数从i_rdev中得到主设备号和次设备号。   imajor()函数在内部调用MAJOR宏,如下代码所示。 static inline unsigned imajor(const struct inode *inode) { return MAJOR(inode->i_rdev); /*从inode->i_rdev中提取主设备号*/ }   同样,iminor()函数在内部调用MINOR宏,如下代码所示。 static inline unsigned iminor(const struct inode *inode) { return MINOR(inode->i_rdev); ; /*从inode->i_rdev中提取次设备号*/ } 6.3 字符设备驱动的组成   了解字符设备驱动程序的组成,对编写驱动程序非常有用。因为字符设备在结构上都有很多相似的地方,所以只要会编写一个字符设备驱动程序,那么相似的字符设备驱动程序的编写,就不难了。在Linux系统中,字符设备驱动程序由以下几个部分组成。 6.3.1 字符设备加载和卸载函数   在字符设备的加载函数中,应该实现字符设备号的申请和cdev的注册。相反,在字符设备的卸载函数中应该实现字符设备号的释放和cdev的注销。   cdev是内核开发者对字符设备的一个抽象。除了cdev中的信息外,特定的字符设备还需要特定的信息,常常将特定的信息放在cdev之后,形成一个设备结构体,如代码中的xxx_dev。   常见的设备结构体、加载函数和卸载函数如下面的代码: struct xxx_dev /*自定义设备结构体*/ { struct cdev cdev; /*cdev结构体*/ ... /*特定设备的特定数据*/ }; static int __init xxx_init(void) /*设备驱动模块加载函数*/ { ... /* 申请设备号,当xxx_major不为0时,表示静态指定;当为0时,表示动态申请*/ if (xxx_major) result = register_chrdev_region(xxx_devno, 1, "DEV_NAME"); /*静态申请设备号*/ else /*动态申请设备号*/ { result = alloc_chrdev_region(&xxx_devno, 0, 1, " DEV_NAME "); xxx_major = MAJOR(xxx_devno); /*获得申请的主设备号*/ } /*初始化cdev结构,并传递file_operations结构指针*/ cdev_init(&xxx_dev.cdev, &xxx_fops); dev->cdev.owner = THIS_MODULE; /*指定所属模块*/ err = cdev_add(&xxx_dev .cdev, xxx_devno, 1); /*注册设备*/ } static void __exit xxx_exit(void) /*模块卸载函数*/ { cdev_del(&xxx_dev.cdev); /*注销cdev*/ unregister_chrdev_region(xxx_devno, 1); /*释放设备号*/ } 6.3.2 file_operations结构体和其成员函数   file_operations结构体中的成员函数都对应着驱动程序的接口,用户程序可以通过内核来调用这些接口,从而控制设备。大多数字符设备驱动都会实现read()、write()和ioctl()函数,这3个函数的常见写法如下面的代码所示。 /*文件操作结构体*/ static const struct file_operations xxx_fops = { .owner = THIS_MODULE, /*模块引用,任何时候都赋值THIS_MODULE */ .read = xxx_read, /*指定设备的读函数 */ .write = xxx_write, /*指定设备的写函数 */ .ioctl = xxx_ioctl, /*指定设备的控制函数 */ }; /*读函数*/ static ssize_t xxx_read(struct file *filp, char __user *buf, size_t size,loff_t *ppos) { ... if(size>8) copy_to_user(buf,...,...); /*当数据较大时,使用copy_to_user(),效率较高*/ esle put_user(...,buf); /*当数据较小时,使用put_user(),效率较高*/ ... } /*写函数*/ static ssize_t xxx_write(struct file *filp, const char __user *buf,size_t size, loff_t *ppos) { ... if(size>8) copy_from_user(..., buf,...); /*当数据较大时,使用copy_to_user(),效率较高*/ else get_user(..., buf); /*当数据较小时,使用put_user(),效率较高*/ ... } /* ioctl设备控制函数 */ static long xxx_ioctl(struct file *file, unsigned int cmd, unsigned long arg) { ... switch (cmd) { case xxx_cmd1: ... /*命令1执行的操作*/ break; case xxx_cmd1: ... /*命令2执行的操作*/ break; default: return - EINVAL; /*内核和驱动程序都不支持该命令时,返回无效的命令*/ } return 0; }   文件操作结构体xxx_fops中保存了操作函数的指针。对于没有实现的函数,被赋值为NULL。xxx_fops结构体在字符设备加载函数中,作为cdev_init()的参数,与cdev建立了关联。   设备驱动的read()和write()函数有同样的参数。filp是文件结构体的指针,指向打开的文件。buf是来自用户空间的数据地址,该地址不能在驱动程序中直接读取。size是要读的字节。ppos是读写的位置,其相对于文件的开头。   xxx_ioctl控制函数的cmd参数是事先定义的I/O控制命令,arg对应该命令的参数。 6.3.3 驱动程序与应用程序的数据交换   驱动程序和应用程序的数据交换是非常重要的。file_operations中的read()和write()函数,就是用来在驱动程序和应用程序间交换数据的。通过数据交换,驱动程序和应用程序可以彼此了解对方的情况。但是驱动程序和应用程序属于不同的地址空间。驱动程序不能直接访问应用程序的地址空间;同样应用程序也不能直接访问驱动程序的地址空间,否则会破坏彼此空间中的数据,从而造成系统崩溃,或者数据损坏。   安全的方法是使用内核提供的专用函数,完成数据在应用程序空间和驱动程序空间的交换。这些函数对用户程序传过来的指针进行了严格的检查和必要的转换,从而保证用户程序与驱动程序交换数据的安全性。这些函数有: unsigned long copy_to_user(void __user *to, const void *from, unsigned long n); unsigned long copy_from_user(void *to, const void __user *from, unsigned long n); put_user(local,user); get_user(local,user); 6.3.4 字符设备驱动程序组成小结   字符设备是3大类设备(字符设备、块设备、网络设备)中较简单的一类设备,其驱动程序中完成的主要工作是初始化、添加和删除cdev结构体,申请和释放设备号,以及填充file_operation结构体中操作函数,并实现file_operations结构体中的read()、write()、ioctl()等重要函数。如图6.4所示为cdev结构体、file_operations和用户空间调用驱动的关系。 图6.4 字符设备与用户空间关系 6.4 VirtualDisk字符设备驱动   从本节开始,后续的几节都将以一个VirtualDisk设备为蓝本进行讲解。VirtualDisk是一个虚拟磁盘设备,在这个虚拟磁盘设备中分配了8K的连续内存空间,并定义了两个端口数据(port1和port2)。驱动程序可以对设备进行读写、控制和定位操作,用户空间的程序可以通过Linux系统调用访问VirtualDisk设备中的数据。 6.4.1 VirtualDisk的头文件、宏和设备结构体   VirtualDisk驱动程序应该包含必要的头文件和宏信息,并定义一个与实际设备相对应的设备结构体,相关的定义如下面的代码所示。 01 #include 02 #include 03 #include 04 #include 05 #include 06 #include 07 #include 08 #include 09 #include 10 #include 11 #include 12 #define VIRTUALDISK_SIZE 0x2000 /*全局内存最大8K字节*/ 13 #define MEM_CLEAR 0x1 /*全局内存清零*/ 14 #define PORT1_SET 0x2 /*将port1端口清零*/ 15 #define PORT2_SET 0x3 /*将port2端口清零*/ 16 #define VIRTUALDISK_MAJOR 200 /*预设的VirtualDisk的主设备号为200*/ 17 static int VirtualDisk_major = VIRTUALDISK_MAJOR; 18 /*VirtualDisk设备结构体*/ 19 struct VirtualDisk 20 { 21 struct cdev cdev; /*cdev结构体*/ 22 unsigned char mem[VIRTUALDISK_SIZE]; /*全局内存8K*/ 23 int port1; /*两个不同类型的端口*/ 24 long port2; 25 long count; /*记录设备目前被多少设备打开*/ 26 }; * 从第01~11行列出了必要的头文件,这些头文件中包含驱动程序可能使用的函数。 * 从第19~26行代码,定义了VirtualDisk设备结构体。其中包含了cdev字符设备结构体,和一块连续的8K的设备内存。另外定义了两个端口port1和port2,用来模拟实际设备的端口。count表示设备被打开的次数。在驱动程序中,可以不将这些成员放在一个结构中,但放在一起的好处是借助了面向对象的封装思想,将设备相关的成员封装成了一个整体。 * 第22行定义了一个8K的内存块,驱动程序中一般不静态的分配内存,因为静态分配的内存的生命周期非常长,随着驱动程序生和死。而驱动程序一般运行在系统的整个开机状态中,所以驱动程序分配的内存,一直不会得到释放。所以,编写驱动程序,应避免申请大块内存和静态分配内存。这里,只是为了演示方便,所以分配了静态内存。 6.4.2 加载和卸载驱动程序   第6.3节已经对字符设备驱动程序的加载和卸载模板进行了介绍。VirtualDisk的加载和卸载函数也和6.3节介绍的类似,其实现代码如下: 01 /*设备驱动模块加载函数*/ 02 int VirtualDisk_init(void) 03 { 04 int result; 05 dev_t devno = MKDEV(VirtualDisk_major, 0); /*构建设备号*/ 06 /* 申请设备号*/ 07 if (VirtualDisk_major) /* 如果不为0,则静态申请*/ 08 result = register_chrdev_region(devno, 1, "VirtualDisk"); 09 else /* 动态申请设备号 */ 10 { 11 result = alloc_chrdev_region(&devno, 0, 1, "VirtualDisk"); 12 VirtualDisk_major = MAJOR(devno); /*从申请设备号中得到主设备号 */ 13 } 14 if (result < 0) 15 return result; 16 /* 动态申请设备结构体的内存*/ 17 Virtualdisk_devp = kmalloc(sizeof(struct VirtualDisk), GFP_KERNEL); 18 if (!Virtualdisk_devp) /*申请失败*/ 19 { 20 result = - ENOMEM; 21 goto fail_kmalloc; 22 } 23 memset(Virtualdisk_devp, 0, sizeof(struct VirtualDisk));/*将内存清零*/ 24 /*初始化并且添加cdev结构体*/ 25 VirtualDisk_setup_cdev(Virtualdisk_devp, 0); 26 return 0; 27 fail_kmalloc: 28 unregister_chrdev_region(devno, 1); 29 return result; 30 } 31 /*模块卸载函数*/ 32 void VirtualDisk_exit(void) 33 { 34 cdev_del(&Virtualdisk_devp->cdev); /*注销cdev*/ 35 kfree(Virtualdisk_devp); /*释放设备结构体内存*/ 36 unregister_chrdev_region(MKDEV(VirtualDisk_major, 0), 1); /*释放设备号*/ 37 } * 第07~13行,使用两种方式申请设备号。VirtualDisk_major变量被静态定义为200。当加载模块时不使VirtualDisk_major等于0,那么就执行register_chrdev_ region()函数静态分配一个设备号;如果VirtualDisk_major等于0,那么就使用alloc_chrdev_ region()函数动态分配一个设备号,并由参数devno返回。第12行,使用MAJOR宏返回得到的主设备号。 * 第17~22行,分配一个VirtualDisk设备结构体。 * 第23行,将分配的VirtualDisk设备结构体清零。 * 第25行,调用自定义的VirtualDisk_setup_cdev()函数初始化cdev结构体,并加入内核中。该函数将在下面讲到。 * 第32~37行是卸载函数,该函数中注销了cdev结构体,释放了VirtualDisk设备所占的内存,并且释放了设备占用的设备号。 6.4.3 cdev的初始化和注册   6.4.2节代码中第25行调用的VirtualDisk_setup_cdev()函数完成了cdev的初始化和注册,其代码如下: 01 /*初始化并注册cdev*/ 02 static void VirtualDisk_setup_cdev(struct VirtualDisk *dev, int minor) 03 { 04 int err; 05 devno = MKDEV(VirtualDisk_major, minor); /*构造设备号*/ 06 cdev_init(&dev->cdev, &VirtualDisk_fops); /*初始化cdev设备*/ 07 dev->cdev.owner = THIS_MODULE; /*使驱动程序属于该模块*/ 08 dev->cdev.ops = &VirtualDisk_fops; /*cdev连接file_operations指针*/ 09 err = cdev_add(&dev->cdev, devno, 1); /*将cdev注册到系统中*/ 10 if (err) 11 printk(KERN_NOTICE "Error in cdev_add()\n"); 12 }   下面对该函数进行简要的解释: * 第05行,使用MKDEV宏构造一个主设备号为VirtualDisk_major,次设备号为minor的设备号。 * 第06行,调用cdev_init()函数,将设备结构体cdev与file_operators指针相关联。这个文件操作指针定义如下代码所示。 /*文件操作结构体*/ static const struct file_operations VirtualDisk_fops = { .owner = THIS_MODULE, .llseek = VirtualDisk_llseek, /*定位偏移量函数*/ .read = VirtualDisk_read, /*读设备函数*/ .write = VirtualDisk_write, /*写设备函数*/ .ioctl = VirtualDisk_ioctl, /*控制函数*/ .open = VirtualDisk_open, /*打开设备函数*/ .release = VirtualDisk_release, /*释放设备函数*/ }; * 第08行,指定VirtualDisk_fops为字符设备的文件操作函数指针。 * 第09行,调用cdev_add()函数将字符设备加入到内核中。 * 第10、11行,如果注册字符设备失败,则返回。 6.4.4 打开和释放函数   当用户程序调用open()函数打开设备文件时,内核会最终调用VirtualDisk_open()函数。该函数的代码如下: 01 /*文件打开函数*/ 02 int VirtualDisk_open(struct inode *inode, struct file *filp) 03 { 04 /*将设备结构体指针赋值给文件私有数据指针*/ 05 filp->private_data = Virtualdisk_devp; 06 struct VirtualDisk *devp = filp->private_data; /*获得设备结构体指针*/ 07 devp->count++; /*增加设备打开次数*/ 08 return 0; 09 }   下面对该函数进行简要的解释: * 第05、06行,将Virtualdisk_devp赋给私有数据指针,在后面将用到这个指针。 * 第07行,将设备打开计数增加1。   当用户程序调用close()函数关闭设备文件时,内核会最终调用VirtualDisk_release()函数。这个函数主要是将计数器减1。该函数的代码如下: 01 /*文件释放函数*/ 02 int VirtualDisk_release(struct inode *inode, struct file *filp) 03 { 04 struct VirtualDisk *devp = filp->private_data; /*获得设备结构体指针*/ 05 devp->count--; /*减少设备打开次数*/ 06 return 0; 07 } 6.4.5 读写函数   当用户程序调用read()函数读设备文件中的数据时,内核会最终调用VirtualDisk_read()函数。该函数的代码如下: 01 /*读函数*/ 02 static ssize_t VirtualDisk_read(struct file *filp, char __user *buf, size_t size, 03 loff_t *ppos) 04 { 05 unsigned long p = *ppos; /*记录文件指针偏移位置*/ 06 unsigned int count = size; /*记录需要读取的字节数*/ 07 int ret = 0;/*返回值*/ 08 struct VirtualDisk *devp = filp->private_data; /*获得设备结构体指针*/ 09 /*分析和获取有效的读长度*/ 10 if (p >= VIRTUALDISK_SIZE) /*要读取的偏移大于设备的内存空间*/ 11 return count ? - ENXIO: 0; /*读取地址错误*/ 12 if (count > VIRTUALDISK_SIZE - p) /*要读取的字节大于设备的内存空间*/ 13 count = VIRTUALDISK_SIZE - p; /*将要读取的字节数设为剩余的字节数*/ 14 /*内核空间->用户空间交换数据*/ 15 if (copy_to_user(buf, (void*)(devp->mem + p), count)) 16 { 17 ret = - EFAULT; 18 } 19 else 20 { 21 *ppos += count; 22 ret = count; 23 printk(KERN_INFO "read %d bytes(s) from %d\n", count, p); 24 } 25 return ret; 26 }   下面对该函数进行简要的分析: * 第05~07行,定义了一些局部变量。 * 第08行,从文件指针中获得设备结构体指针。 * 第10行,如果要读取的位置大于设备的大小,则出错。 * 第12行,如果要读的数据的位置大于设备的大小,则只读到设备的末尾。 * 第15~24行,从用户空间复制数据到设备中。如果复制数据成功,就将文件的偏移位置加上读出的数据个数。   当用户程序调用write()函数向设备文件写入数据时,内核会最终调用VirtualDisk_ write()函数。该函数的代码如下: 01 /*写函数*/ 02 static ssize_t VirtualDisk_write(struct file *filp, const char __user *buf, 03 size_t size, loff_t *ppos) 04 { 05 unsigned long p = *ppos; /*记录文件指针偏移位置*/ 06 int ret = 0; /*返回值*/ 07 unsigned int count = size; /*记录需要写入的字节数*/ 08 struct VirtualDisk *devp = filp->private_data; /*获得设备结构体指针*/ 09 /*分析和获取有效的写长度*/ 10 if (p >= VIRTUALDISK_SIZE) /*要写入的偏移大于设备的内存空间*/ 11 return count ? - ENXIO: 0; /*写入地址错误*/ 12 if (count > VIRTUALDISK_SIZE - p) /*要写入的字节大于设备的内存空间*/ 13 count = VIRTUALDISK_SIZE - p; /*将要写入的字节数设为剩余的字节数*/ 14 /*用户空间->内核空间*/ 15 if (copy_from_user(devp->mem + p, buf, count)) 16 ret = - EFAULT; 17 else 18 { 19 *ppos += count; /*增加偏移位置*/ 20 ret = count; /*返回实际的写入字节数*/ 21 printk(KERN_INFO "written %d bytes(s) from %d\n", count, p); 22 } 23 return ret; 24 }   下面对该函数进行简要的介绍: * 第05~07行,定义了一些局部变量。 * 第08行,从文件指针中获得设备结构体指针。 * 第10行,如果要读取的位置大于设备的大小,则出错。 * 第12行,如果要读的数据的位置大于设备的大小,则只读到设备的末尾。 * 第15~24行,从设备中复制数据到用户空间中。如果复制数据成功,就将文件的偏移位置加上写入的数据个数。 6.4.6 seek()函数   当用户程序调用fseek()函数在设备文件中移动文件指针时,内核会最终调用VirtualDisk_llseek()函数。该函数的代码如下: 01 /* seek文件定位函数 */ 02 static loff_t VirtualDisk_llseek(struct file *filp, loff_t offset, int orig) 03 { 04 loff_t ret = 0; /*返回的位置偏移*/ 05 switch (orig) 06 { 07 case SEEK_SET: /*相对文件开始位置偏移*/ 08 if (offset < 0) /*offset不合法*/ 09 { 10 ret = - EINVAL; /*无效的指针*/ 11 break; 12 } 13 if ((unsigned int)offset > VIRTUALDISK_SIZE) /*偏移大于设备内存*/ 14 { 15 ret = - EINVAL; /*无效的指针*/ 16 break; 17 } 18 filp->f_pos = (unsigned int)offset; /*更新文件指针位置*/ 19 ret = filp->f_pos; /*返回的位置偏移*/ 20 break; 21 case SEEK_CUR: /*相对文件当前位置偏移*/ 22 if ((filp->f_pos + offset) > VIRTUALDISK_SIZE) /*偏移大于设备内存*/ 23 { 24 ret = - EINVAL; /*无效的指针*/ 25 break; 26 } 27 if ((filp->f_pos + offset) < 0) /*指针不合法*/ 28 { 29 ret = - EINVAL; /*无效的指针*/ 30 break; 31 } 32 filp->f_pos += offset; /*更新文件指针位置*/ 33 ret = filp->f_pos; /*返回的位置偏移*/ 34 break; 35 default: 36 ret = - EINVAL; /*无效的指针*/ 37 break; 38 } 39 return ret; 40 }   下面对该函数进行简要的介绍: * 第04行,定义了一个返回值,用来表示文件指针现在的偏移量。 * 第05行,用来选择文件指针移动的方向。 * 第07~20行,表示文件指针移动的类型是SEEK_SET,表示相对于文件的开始移动指针offset个位置。 * 第08~12行,如果偏移小于0,则返回错误。 * 第13~17行,如果偏移值大于文件的长度,则返回错误。 * 第18行,设置文件的偏移值到 filp->f_pos,这个指针表示文件的当前位置。 * 第21~34行,表示文件指针移动的类型是SEEK_CUR,表示相对于文件的当前位置移动指针offset个位置。 * 第22~26行,如果偏移值大于文件的长度,则返回错误。 * 第27~31行,表示指针小于0的情况,这种情况指针是不合法的。 * 第32行,将文件的偏移值filp->f_pos加上offset个偏移。 * 第35、36行,表示命令不是SEEK_SET或者SEEK_CUR,这种情况下表示传入了非法的命令,直接返回。 6.4.7 ioctl()函数   当用户程序调用ioctl()函数改变设备的功能时,内核会最终调用VirtualDisk_ioctl()函数。该函数的代码如下: 01 /* ioctl设备控制函数 */ 02 static int VirtualDisk_ioctl(struct inode *inodep, struct file *filp, unsigned 03 int cmd, unsigned long arg) 04 { 05 struct VirtualDisk *devp = filp->private_data; /*获得设备结构体指针*/ 06 switch (cmd) 07 { 08 case MEM_CLEAR: /*设备内存清零*/ 09 memset(devp->mem, 0, VIRTUALDISK_SIZE); 10 printk(KERN_INFO "VirtualDisk is set to zero\n"); 11 break; 12 case PORT1_SET: /*将端口1置0*/ 13 devp->port1=0; 14 break; 15 case PORT2_SET: /*将端口2置0*/ 16 devp->port2=0; 17 break; 18 default: 19 return - EINVAL; 20 } 21 return 0; 22 }   下面对该函数进行简要的介绍: * 第05行,得到文件的私有数据,私有数据中存放的是VirtualDisk设备的指针。 * 第06~20行,根据ioctl()函数传进来的参数判断将要执行的操作。这里的字符设备支持3个操作,第1个操作是将字符设备的内存全部清0,第2个操作是将端口1设置为0,第3个操作是将端口2设置成0。 6.5 小 结   本章主要讲解了字符设备驱动程序。字符设备是Linux中的三大设备之一,很多设备都可以看成是字符设备,所以学习字符设备驱动程序的编程是很有用的。本章首先从整体上介绍了字符设备的框架结构,然后介绍了字符设备结构体struct cdev,接着介绍了字符设备的组成,最后详细讲解了一个VirtualDisk字符设备驱动程序。 第1篇 Linux驱动开发基础    第6章 简单的字符设备驱动程序    ·106·       ·105·