字符设备驱动

一、设备驱动概述

linux内核的驱动模型为编写驱动提供了抽象,将驱动公共的部分提取了出去简化了驱动的编写工作,但是那并不是实际的驱动,如果要实现真正的驱动,还要给予驱动模型做一些其它的工作。根据外设和内核交互数据的方式,内核将驱动分成了几种类别。基本上设备可以分为两类,一类适合于面向字符的交换,一类适合于处理包含固定数目字节的数据块,这两类分别称为字符设备和块设备(网络设备是一种特殊的设备,它以一种完全不同的方式来实现)。
设备文件都位于/dev下,通过ls命令可以查看系统中设备的信息,设备文件没有长度,而多了主次设备号。在ls中可以看到访问权限之前的那个位如果是c表示是字符设备,如果是b则表示是块设备。比如
hwang7@hwang7$ ls /dev/sda -l
brw-rw---- 1 root disk 8, 0  1月 26 09:41 /dev/sda
hwang7@hwang7$ ls /dev/tty -l
crw-rw-rw- 1 root tty 5, 0  1月 26 09:42 /dev/tty

1.1 主次设备号

linux内核使用主次设备号来识别设备驱动程序以及所驱动的设备,具体的来说主设备号用来查找驱动程序,而次设备号则可以提供使用了那个设备的信息。因为一个驱动可以管理同种类型的多个设备,比如一个uart的驱动可以管理芯片上的多个uart硬件。
主设备号的分配不是随意的,其分配来自于一个组织,设备号的当前列表可以从http://lanana.org中获取。内核源码中的文档Documents/device.txt中也给出了该版本内核发布时的最新主设备号的分配结果。另外major.h头文件定义了主次设备号的相关宏,但是不是所有的主设备号都有自己的宏。从该文件也可以看出有的类型的设备可能会分配多个主设备,比如SCSI设备。

1.2 动态创建设备文件

设备是给使用的,如果无法使用,那么假如这个设备就没什么实际的意义,linux通过/dev中的设备文件向用户空间提供对设备的访问功能。通常/dev中的文件应该在系统启动时就被创建出来,但是由于各个系统支持硬件变化较大,所支持的硬件越来越多,而又不是所有的硬件都是用户所要的,因而linux提供了uevent机制来动态的创建/dev下的设备文件,根据驱动模型,uevent事件在硬件加载的时候才会被创建,因而这就可以使得只加载需要的硬件。而当今的计算机系统中,外设的种类又千变万化,种类极其繁多,不可能在内核中将所有的硬件驱动都加载起来并提供接口给用户空间。
uevent的基本工作过程是:
当设备的硬件状态发生变化时,就会发送uevent消息(即热插拔消息)到用户空间的udevd守护进程,该进程会根据消息内容创建、删除dev设备文件或者进行其它处理。所以在热插拔消息中,非常重要的一点就是要提供设备的主次设备号信息,这是通过devices_kset的device_uevent_ops中的uevent函数完成的,该函数会往热插拔消息中添加设备主次设备号的环境变量。由于devices_kset是设备模型的kset,这就保证了所有设备在添加和删除的uevent处理中都会包括设备主次设备号的环境变量。
另外在引入uevent机制后,/dev不再是一个持久文件系统,其内容是动态生成的,一旦关机其信息就消失,系统启动时,再由udevd守护进程生成。
另外需要注意的是字符设备和块设备的主次设备号可以相同,因为它们对应不同类型的设备,因而内核是可以区分它们的。

1.3 主次设备号的表示

在内核中, dev_t 类型(在 <linux/types.h>中定义)用来表示设备编号--即包括主设备号也包括次设备号。在使用设备号时,不要对其组成方式做任何假设(即使你知道旧的系统使用16位表示设备号,新的系统使用32位表示设备号),这会使得你写的代码具有最好的可移植性,内核提供了如下宏来帮助使用设备号:
  • MAJOR(dev_t dev)用于从设备号中得到其主设备号
  • MINOR(dev_t dev)用于从设备号中得到其次设备号
  • MKDEV(int major, int minor)用于从主次设备号得到一个设备号。
由于dev_t是一个只由内核使用的数据结构,外部是看不到的(即用户空间程序),因此内核也为设备号提供了外部可见的数据类型的表示,在新的系统中是u32,在旧的系统中是u16,对应的内核提供了api用于在dev_t和外部可见格式之间的转换,相关API定义如下:
u32 new_encode_dev(dev_t dev)
dev_t new_decode_dev(u32 dev)
u16 old_encode_dev(dev_t dev)
dev_t old_decode_dev(u16 val)
前两个用于在dev_t和新的用于外部表示的u32之间进行转换,后两个用于dev_t和旧的用于外部表示的u16之间进行转换。

1.4 和文件系统的关联

内核使用struct file来表示一个打开的文件,而struct inode用于在内核内部表示一个文件。对于一个文件来说,可能有多个file结构,但是只会有一个inode结构。
struct file,这是内核中常见的一个数据结构,对于每一个打开的文件就有一个它的实例,我们关心的是其中的f_op成员,它是一个类型为file_operations的结构,该结构包括了操作文件的函数集合,在打开文件时会给它赋值,可以在任何需要的时候修改它,修改完后新的函数就立即生效。由于每个文件都对应于一个file结构,因而我们甚至可以给同一个主设备下的各个次设备分配不同的操作函数集,从而实现对设备的多种操作。
struct inode也是一个内核常见的数据结构,我们关心其中的如下几个域:
umode_t i_mode; 文件模式
dev_t i_rdev; 设备号
union {
struct pipe_inode_info *i_pipe;
struct block_device *i_bdev;
struct cdev *i_cdev;
}; 设备文件是字符设备、块设备还是管道设备
const struct file_operations *i_fop; 文件的操作函数集
这里最关键的一步就是打开文件的操作,在打开文件时,各种文件系统的实现者会调用init_special_inode(各种文件系统在这点上是统一的,如果不是常规文件,不是目录文件也不是链接文件就会调用该函数)。该函数的实现很简单:
void init_special_inode(struct inode *inode, umode_t mode, dev_t rdev)
{
	inode->i_mode = mode;
	if (S_ISCHR(mode)) {
		inode->i_fop = &def_chr_fops;
		inode->i_rdev = rdev;
	} else if (S_ISBLK(mode)) {
		inode->i_fop = &def_blk_fops;
		inode->i_rdev = rdev;
	} else if (S_ISFIFO(mode))
		inode->i_fop = &def_fifo_fops;
	else if (S_ISSOCK(mode))
		inode->i_fop = &bad_sock_fops;
	else
		printk(KERN_DEBUG "init_special_inode: bogus i_mode (%o) for"
				  " inode %s:%lu\n", mode, inode->i_sb->s_id,
				  inode->i_ino);
}
这就给特殊文件的inode的文件操作赋值了,而对应的文件操作中的open又会进一步对打开文件进行初始化,比如字符设备中就会给file的f_op赋值。
从总体上来说,文件系统的操作大致流程都是首先根据名字获取inode,如果还没有相应的inode就创建相应的inode,然后打开文件,在这个过程中会初始化打开文件的的各个域,包括文件操作指针。

1.5 IOCTL

在文件操作函数集中包括了一个ioctl函数,它用于不能用常规方式处理的文件操作,一般是控制文件的操作。/proc,/sys、以及文件的IOCTL都能帮助我们实现一些特殊的功能,可以根据自己的需要选择。

1.5.1 IOCTL概述

ioctl被用来实现一些比较难用读写文件的方式来完成的功能,在设备驱动中,通常这涉及到对硬件的控制能力。ioctl文件操作被包括在文件操作函数集中,并且通过同名系统调用提供给了用户空间程序。用户使用ioctl时只需要打开文件,然后向文件发出ioctl调用即可,用户空间的ioctl函数原型如下:
int ioctl(int fd, unsigned long cmd, ...);
  • fd:文件描述符
  • cmd:命令常数
  • ...:表示一个变参列表,但是实际上只使用了一个参数,之所以用该形式,是因为该参数的类型和意义取决于第二个参数,它可能是一个常数值,也可能是一个指针。ioctl的这种形式使得无法用统一的方式对它进行处理,由于无法确定参数的确切类型、含义,因而每个ioctl都相当于一个私有的系统调用,如果没有相关的文档,你无法知道一个文件支持哪些ioctl,它支持什么功能,因而在内核开发中不推荐使用该功能。
在内核中ioctl的原型如下:
long (*compat_ioctl) (struct file *, unsigned int, unsigned long);
long (*unlocked_ioctl) (struct file *, unsigned int, unsigned long);
它的三个参数分别对应到用户发出的系统调用的三个参数,其中发出的调用的第三个参数被以unsigned long的形式传递。

1.5.2 选择ioctl命令

如果要实现ioctl,首先第一步要为自己的ioctl定义全局唯一的命令编码,否则可能得不到期望的结果。比如假如用户错误的将一个IOCTL命令发给了一个错误的设备,如果每个IOCTL在全局都是唯一的,则这就会导致一个错误被返回,但是如果命令不是唯一的,则可能该错误的设备也实现了该IOCTL命令,那么用户就会得到自己不期望的结果。
为了方便定义IOCTL命令,内核将命令号分成了几个字段:类型、序数、传送方向(即是读还是写)和参数大小等等,可以参考文件include/asm/ioctl.h和相关内核文档ioctl-number.txt文件。同时内核定义了一些列宏来方便定义一个全局唯一的I0CTL命令:
#define _IO(type,nr)		_IOC(_IOC_NONE,(type),(nr),0)                        定义无参数的命令
#define _IOR(type,nr,size)	_IOC(_IOC_READ,(type),(nr),(_IOC_TYPECHECK(size)))   定义用于读的命令
#define _IOW(type,nr,size)	_IOC(_IOC_WRITE,(type),(nr),(_IOC_TYPECHECK(size)))  定义用于写的命令
#define _IOWR(type,nr,size)	_IOC(_IOC_READ|_IOC_WRITE,(type),(nr),(_IOC_TYPECHECK(size))) 定义用于读写的命令
其中的type为类型字段参数,nr为序号字段参数,size为参数大小
另外内核还定义了从cmd中获取序号,类型,是读还是写以及大小的宏
#define _IOC_DIR(nr)		(((nr) >> _IOC_DIRSHIFT) & _IOC_DIRMASK)
#define _IOC_TYPE(nr)		(((nr) >> _IOC_TYPESHIFT) & _IOC_TYPEMASK)
#define _IOC_NR(nr)		(((nr) >> _IOC_NRSHIFT) & _IOC_NRMASK)
#define _IOC_SIZE(nr)		(((nr) >> _IOC_SIZESHIFT) & _IOC_SIZEMASK)

1.5.3 预定义IOCTL命令

系统预定义了一些IOCTL命令,这些命令对于任何文件都是成立的,即任何文件都支持这些命令(可以参考函数compat_sys_ioctl,这就是ioctl系统调用的内核入口):
  • FIOCLEX:设置 close-on-exec 标志(File IOctl Close on EXec)。
  • FIONCLEX:清除 close-no-exec 标志
  • FIOASYNC:设置或者清除文件的异步通知标记。
  • FIOQSIZE:返回一个文件或者目录的大小,对于设备文件,它返回一个ENOTTY错误。
  • FIONBIO:"File IOctl Non-Blocking I/O"(在"阻塞和非阻塞操作"一节中描述)。用于修改文件的标记中的O_NONBLOCK标志。

1.5.4 ioctl参数

ioctl的第三个参数可能是整数,这个时候可以直接使用,但是它也可能是指针。这时候就要要小心了, 因为必须保证它指向的内存地址是有效的。
如果第三个参数是指针,则可以使用copy_from_user和copy_to_user函数来完成内核和用户空间的数据交互。但是由于ioctl的第三个参数包含的数据往往较小,因而还有其它选择,可以调用access_ok函数来进行检查,其原型如下:
#define access_ok(type, addr, size)	(__chk_user_ptr(addr),	__access_ok((__force unsigned long)(addr), (size), get_fs()))
  • type:应该是VERIFY_READ或VERIFY_WRITE,表示要进行的是读还是写
  • addr:用户空间地址
  • size:字节大小
如果access_ok检查通过,则就可安全地进行真正的数据读写了。此时可以使用内核提供的另外一组API:
put_user(datum, ptr)
__put_user(datum, ptr)
get_user(local, ptr)
__get_user(local, ptr)
来完成内核和用户空间的数据交互。它们会根据ptr参数的大小来完成传输。

1.6 定位设备

默认情况下,系统认为文件都是支持定位的及lseek文件操作,如果一个设备不支持该操作,则应该在open时调用nonseekable_open来通知文件系统该设备不支持定位操作。

二、字符设备

2.1 数据结构

内核使用struct cdev来表示一个字符设备,其结构定义如下:
struct cdev {
	struct kobject kobj;
	struct module *owner;
	const struct file_operations *ops;
	struct list_head list;
	dev_t dev;
	unsigned int count;
};
  • kobj:用于将设备链接到kobject层次结构中,进而出现在sysfs中
  • owner:拥有者
  • ops:字符设备的操作函数集
  • list:用于将该设备上打开的文件的inode链接起来。一个文件只会有一个inode节点,但是我们需要注意的是一个字符设备可能包括多个连续的次设备号,只要次设备号不同,在设备文件系统中即/dev下就会表示为一个单独的文件,但是在内核中具有相同主设备号的多个连续的设备可能以同一个cdev结构表示。具体的说就是调用cdev_add一次只会创建一个cdev结构用于表示字符设备,这个cdev结构会保存这些连续的字符设备的起始设备号和连续设备的数目。因而内核中的一个cdev可能对应多个设备文件,进而就对应了多个inode。
  • dev:设备号
  • count:该设备所拥有的次设备号数目
字符设备相关数据结构之间的关系如图:


2.2 分配和释放设备编号

在编写驱字符驱动时,第一件事就是要获取一个设备号来使用。内核提供了api来获取字符设备号:
int register_chrdev_region(dev_t from, unsigned count, const char *name);
  • from:要分配的起始设备编号。必须包含主设备号
  • count:要为其分配设备号的连续的设备数目
  • name:驱动的名字
如果分配失败会返回负的出错码。该函数适用于使用者确切的直到应该使用哪个主设备号的场合。如果使用者不知道应该使用哪个主设备号,可以使用如下API,由内核来分配主次设备号:
int alloc_chrdev_region(dev_t *dev, unsigned baseminor, unsigned count,	const char *name);
  • dev:分配的第一个设备号
  • baseminor:期望分配的第一个次设备号,通常为0
  • count:期望为多少个连续的设备分配设备号
  • name:驱动的名字
不管是如何得到设备号的,在不适用的时候都应该将它释放掉。相关的api如下:
void unregister_chrdev_region(dev_t from, unsigned count);
  • from:要释放的起始设备号
  • count:释放多少个连续设备的设备号
内核使用了类型为char_device_struct的一个数据库chrdevs来跟踪管理字符设备号。设备的设备号可以从/proc/devices中查看,也可以通过/sys文件系统查看。/proc/devices在proc_devices_init中被创建,其实现很简单就是遍历相关的设备号数据库然后输出。

2.3 添加字符设备到系统中、从系统中删除字符设备

在获取设备号后需要将设备添加到系统中,这需要两步:
  1. 调用cdev_init初始化cdev数据结构,这一步会将字符设备的kobject的kobj_type设置为ktype_cdev_default,它指定了该类型的字符设备的释放函数
  2. 调用cdev_add将设备添加到系统中。
要从系统中删除一个字符设备,需要首先调用cdev_del,然后再释放其设备号。
系统还提供了一个api cdev_alloc可以用于创建并初始化字符设备数据结构struct cdev,但是如果使用该api,需要自己设置字符设备的操作函数集。cdev_alloc会把字符设备的kobject的kobj_type设置为ktype_cdev_dynamic。
除了以上两个API之外,内核还提供有一对API用于向系统添加或者删除字符设备:它们分别为:
int register_chrdev(unsigned int major, const char *name, const struct file_operations *fops);
void unregister_chrdev(unsigned int major, const char *name);
这是旧式的API。
register_chrdev最终会调用cdev_alloc和cdev_add将设备添加到系统中。所谓的添加到系统中并没有添加到sysfs中,也没有创建/dev下的设备文件,cdev_add完成的工作是将字符设备添加到一个全局数组cdev_map中,它是一个kobj_map类型的数组,该结构定义如下:
struct kobj_map {
	struct probe {
		struct probe *next; 
		dev_t dev;
		unsigned long range;
		struct module *owner;
		kobj_probe_t *get;
		int (*lock)(dev_t, void *);
		void *data;
	} *probes[255];
	struct mutex *lock;
};
  • next:散列到同一个哈希槽位的下一个设备probe结构
  • dev:设备号
  • range:该设备的主设备号下的连续的次设备数目
  • owner:所有者
  • get:用于由设备号获取设备的kobject
  • lock:用于对获取的设备上锁
  • data:get的参数。
该结构用于维护一个对象数据库,该数据库采用哈希表来实现,并用链式法来解决冲突。系统中当前定义了两个该类型的数据库,一个为cdev_map,一个bdev_map,分别对应字符设备和块设备。在调用cdev_add时,字符设备被添加到cdev_map数据库中,同时添加到数据库中的还有一个用于上锁的函数exact_lock和一个用于获取设备的函数exact_match,在打开字符设备时(chrdev_open函数中)会首先查找该数据库,在获得设备后才会继续进行操作。
但是完成cdev_add并不会创建sysfs下的文件,也不会为设备创建/dev下的设备文件,这要通过调用device_create或者其它设备子系统提供的类似API来实现。在/dev下创建文件以及在/sys下创建文件最终都是由device_add完成的。
/dev文件系统的内容由内核线程devtmpfsd维护,相关代码路径在drivers/base/devtmpfs.c中

2.4 打开设备

正如前边所说,所有文件系统的实现都会对不是常规文件、不是目录文件也不是连接文件的特殊文件调用init_special_inode,对于设备文件,这会将文件inode的文件操作函数集设置为def_chr_fops,该操作集主要的是提供了打开文件的操作chrdev_open,chrdev_open主要完成:
  • 判断inode的i_cdev
    • 如果是第一次打开,即如果inode的i_cdev为空,则调用kobj_lookup从字符设备数据库中查找字符设备,并在成功时获取它,同时还会将inode添加到字符设备的链表中(由字符设备的list元所指向的链表)
    • 如果已经打开过了,则只是增加设备的引用计数
  • 为该设备的本次打开准备操作函数集,即给file结构的f_op赋值
  • 如果该字符设备提供了打开的操作函数,就调用它。
由此也可以看出所有对字符设备的操作,必须经过的一步就是打开,因此如果要对设备文件的使用添加限制,open函数里是一个绝佳的时机,驱动的实现者可以在这里添加所想要的限制。

2.5 字符文件的引用计数

字符文件使用了kobject,因而其引用计数也是使用了kobject提供的接口,这也看到了kobject的威力了,它的一套机制贯穿了设备驱动模型的所有部分。相关的API为:
struct kobject *cdev_get(struct cdev *p)
void cdev_put(struct cdev *p)
字符设备的put也没什么特别之处,它遵循kobject提供的机制,在引用计数为0时会进行释放,在释放时会调用在初始化字符设备时提供的释放函数,这里唯一需要注意的是通过cdev_init初始化的字符设备的kobj_type为ktype_cdev_default而通过cdev_alloc初始化的字符设备的kobj_type为ktype_cdev_dynamic,二者的释放函数不同。

2.6 读写字符文件

在打开文件之后,读写文件就没有什么特别之处了,打开文件时已经设置好了文件的操作函数集,因而直接调用即可。当然字符驱动的实现必须遵循读写文件应该遵循的规则,比如怎么和用户空间交互数据,如何处理出错、返回何值。

在文件读写IO中,由于各种原因,读写请求可能会被阻塞,这时候读写操作就应该休眠以等待操作可进行执行的时刻,则会时候就要用到内核提供的一些同步互斥机制。通常的做法是声明一个等待队列,然后等待被唤醒。

2.7 poll和select支持

有时候应用程序会想使用poll或者select以完成一些高级的读写功能,比如确定接下来的I/O操作是否会阻塞,一次等待多个数据流等。无论是poll还是select还是epoll在最底层都对应到文件操作函数中的poll。其原型为:
unsigned int (*poll) (struct file *, struct poll_table_struct *);
  • file:打开的文件
  • poll_table:该结构对驱动是透明的,驱动可以不关心它的内容。它用于在内核中实现poll,select和epoll系统调用。驱动只需要知道它可以通过poll_wait向poll_table中添加一个等待队列即可(当你调用wakeup家族来唤醒等待队列上的等待任务时添加到poll_table中的等待队列就会被唤醒)。
poll_wait的原型如下:
void poll_wait(struct file * filp, wait_queue_head_t * wait_address, poll_table *p)
参数分别为:
  • filp:打开文件指针
  • wait_address:等待队列头
  • p:poll_table
poll需要的返回值中包含了哪些操作可以进行的信息,或者是出错信息。常见的一些返回值及其意义如下:
  1. POLLIN:设备可以无阻塞读
  2. POLLRDNORM:设备可以被用来读取"正常"的数据。因此一个可读的设备返回( POLLIN|POLLRDNORM ).
  3. POLLRDBAND:设备可以被用来读取带外数据。常用于socket。
  4. POLLPRI:高优先级数据(带外)可不阻塞地被读取。
  5. POLLHUP:读取设备的进程读到了文件尾。
  6. POLLERR:发生了错误
  7. POLLOUT:设备可以无阻塞的写入
  8. POLLWRNORM:它和POLLOUT的关系类似于POLLRDNORM和POLLIN的关系,一个可写的设备返回( POLLOUT|POLLWRNORM).
  9. POLLWRBAND:类似于POLLRDBAND,带外数据可写。常用于socket。
在驱动程序的poll被调用时,poll_table参数有时候被设置为NULL,这可能有两个原因:
  1. 应用程序调用poll时超时值被设置为0,此时内核知道没有任何必要等待,因而就不要创建等待队列。
  2. 当被指定的多个驱动中有一个表明可以进行IO操作时,poll_table也会被设置为NULL,因为根据poll的语义,只要有任何一个指定的文件描述符可以IO就可以了,因而当一个设备表明可以进行IO后,就没必要再创建等待队列进行等待了。
poll的数据结构关系图:

2.8 poll、read、write的一些原则

这是最基本的三个API,为了保证应用程序正确工作,这三个API必须正确的被实现,正确实现这三个API是保证所有文件的读写语义一致的基础。正确实现这三者需要保证以下几点:

2.8.1 从设备读数据

  • 如果在输入缓冲中有数据, 则即使缓冲区额数据少于请求的数目,并且驱动可以保证后续数据可以很快到达,read也应该以尽可能小的延时立即返回。read甚至可以一直返回少于请求数目的字节数,当然至少要返回一个字节。在缓冲区有数据的情况下,poll 应当返回 POLLIN|POLLRDNORM。
  • 如果输入缓冲中没有数据,默认情况下read必须阻塞等待,直到输入缓冲区中至少有一个字节。但是如果设置了O_NONBLOCK标记,则read应该立即返回-EAGIN。在缓冲区中没有数据时,poll应该报告设备是不可读的。
  • 如果到达了文件尾,  不管是否阻塞read都应立即返回一个0。这种情况下poll应该报告POLLHUP。

2.8.2 向设备写入数据

  • 如果输出缓冲区有空闲区域,则即使空闲区域小于请求的大小,write也应该立即返回,唯一的要求时空闲区域至少要能够容纳一个字节。这种情况下,poll应该报告该设备是可写的POLLOUT|POLLWRNORM。
  • 如果输出缓冲已满,则默认情况下write要阻塞直到有一些空间被释放了。但是如果设置了O_NOBLOCK,则write应该立即返回一个-EAGAIN的错误。这种情况下,poll应当报告文件是不可写的。另外,如果设备不能接受任何多余数据,则无论是否设置了 O_NONBLOCK,write都应该返回-ENOSPC。
  • 不要让write在返回之前等待数据传输结束,即便没有设置O_NONBLOCK标记。因为许多应用程序使用select来检查write是否会阻塞。如果返回的结果是设备可写,那么调用就不能阻塞(否则就是说话不算话了:))。如果应用程序想要确保数据报真正写到设备上,则应该调用fsync,相应的驱动必须支持该接口。

2.8.3 刷新待处理输出

fsync函数,用于保证缓冲区中的数据被发送到了设备上。不过大多数情况下只有块设备实现了该函数,字符设备是不支持该调用的。

2.9 异步通知

在有的场景下,设备可能在将来不确定的时间点才能准备好用于读取数据,为了应对这种场景,用户可以使用poll或者select,但是这显然不是最优的,很显然这里如果有异步通知机制就好了,事实上也确实有异步通知机制。对于用户程序来说,如果要接收异步通知,它需要完成:
  • 使用fcntl系统调用发出F_SETOWN 命令,这样该进行的ID就被保存到了filp->f_owner中。
  • 使用fcntl系统调用发出F_SETFL命令来设置设备的FASYNC标记
完成这两步之后,当有新数据可读时,相应的进程就会收到SIGIO信号。不过信号并不能表明是哪个文件有数据可读,如果进程订阅了多个文件的异步通知事件,则它应该在收到信号时再调用以下poll或者select等函数以确定发生了什么。
对于驱动来说,要支持异步通知需要做:
  • 当用户进程发出F_SETOWN 调用时,做的时间很简单,就是保存用户进程PID即可。
  • 当用户进程发出设置FASYNC的调用时,驱动程序的fasync函数会被调用,事实上只要文件标记中的FASYNC标记发生了变化,相应驱动的fasync函数就会被调用。
  • 当有数据到达时,给所有注册了异步通知的进程发送SIGIO。
由于可能有多个进程向设备注册异步通知事件,因而设备需要维护注册异步消息的进程的信息,对于这一点内核已经提供了支持,内核提供了如下数据结构用于保存异步通知的注册者的信息:
struct fasync_struct {
spinlock_t	 fa_lock;
int	 magic;
int	 fa_fd;
struct fasync_struct	*fa_next; /* singly linked list */
struct file	 *fa_file;
struct rcu_head	 fa_rcu;
};
支持异步通知的设备需要包含该结构,每次应用注册异步通知事件时内核都会创建一个新的该类型的结构,然后添加到设备中,这些结构被以链表的形式管理。不过驱动不必关心,对于驱动来说很简单,只需要在自己的fasync实现中调用fasync_helper即可,fasync_helper会完成创建该结构并添加到设备的异步注册者链表中的工作,其原型如下:
int fasync_add_entry(int fd, struct file *filp, struct fasync_struct **fapp);
在数据到达的时候,驱动需要发送异步通知,在这一点上内核也提供了支持,驱动只需要调用kill_async即可,其原型如下:
void kill_fasync(struct fasync_struct **fp, int sig, int band);
最后需要注意的是在关闭文件时,一定要注意调用文件的fasync方法将异步通知的注册者删除。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值