三、poll和select
当应用程序需要进行对多文件读写时,若某个文件没有准备好,则系统会处于读写阻塞的状态,并影响了其他文件的读写。为了避免这种情况,在必须使用多输入输出流又不想阻塞在它们任何一个上的应用程序常将非阻塞I/O和poll(SystemV)、select(BSD Unix)、epoll(linux2.5.45开始)系统调用配合使用。当poll函数返回时,会给出一个文件是否可读写的标志,应用程序根据不同的标志读写相应的文件,实现非阻塞的读写。这些系统调用功能相同:允许进程来决定它是否可读或写一个或多个文件而不阻塞。这些调用也可阻塞进程直到任何一个给定集合的文件描述符可用来读或写。这些调用都需要来自设备驱动中poll方法的支持,poll返回不同的标志,告诉主进程文件是否可以读写,其原型(定义在<linuxpoll.h>):
unsignedint(*poll)(structfile*filp,poll_table*wait);
实现这个设备方法分两步:
1.在一个或多个可指示查询状态变化的等待队列上调用poll_wait.如果没有文件描述符可用来执行I/O,内核使这个进程在等待队列上等待所有的传递给系统调用的文件描述符.驱动通过调用函数poll_wait增加一个等待队列到poll_table结构,原型:
voidpoll_wait(structfile*,wait_queue_head_t*,poll_table*);
2.返回一个位掩码:描述可能不必阻塞就立刻进行的操作,几个标志(通过<linux/poll.h>定义)用来指示可能的操作:
标志 | 含义 |
POLLIN | 如果设备无阻塞的读,就返回该值 |
POLLRDNORM | 通常的数据已经准备好,可以读了,就返回该值。通常的做法是会返回(POLLLIN|POLLRDNORA) |
POLLRDBAND | 如果可以从设备读出带外数据,就返回该值,它只可在linux内核的某些网络代码中使用,通常不用在设备驱动程序中 |
POLLPRI | 如果可以无阻塞的读取高优先级(带外)数据,就返回该值,返回该值会导致select报告文件发生异常,以为select八带外数据当作异常处理 |
POLLHUP | 当读设备的进程到达文件尾时,驱动程序必须返回该值,依照se lect的功能描述,调用select的进程被告知进程时可读的。 |
POLLERR | 如果设备发生错误,就返回该值。 |
POLLOUT | 如果设备可以无阻塞地些,就返回该值 |
POLLWRNORM | 设备已经准备好,可以写了,就返回该值。通常地做法是(POLLOUT|POLLNORM) |
POLLWRBAND | 于POLLRDBAND类似 |
应当重复一下POLLRDBAND和POLLWRBAND仅仅对关联到socket的文件描述符有意义:通常设备驱动不使用这些标志.
poll的描述使用了大量在实际使用中相对简单的东西.考虑poll方法的scullpipe实现:
static unsigned int scull_p_poll(struct file *filp, poll_table *wait)
{
struct scull_pipe *dev = filp->private_data;
unsigned int mask = 0;
down(&dev->sem);
poll_wait(filp, &dev->inq, wait);
poll_wait(filp, &dev->outq, wait);
if (dev->rp != dev->wp)
mask |= POLLIN | POLLRDNORM;
if (spacefree(dev))
mask |= POLLOUT | POLLWRNORM;
up(&dev->sem);
return mask;
}
这个代码简单地增加了2个scullpipe等待队列到poll_table,接着设置正确的掩码位,根据数据是否可以读或写.
所示的poll代码缺乏文件尾支持,因为scullpipe不支持文件尾情况.对大部分真实的设备,poll方法应当返回POLLHUP如果没有更多数据(或者将)可用.如果调用者使用select系统调用,文件被报告为可读.不管是使用poll还是select,应用程序知道它能够调用read而不必永远等待,并且read方法返回0来指示文件尾.
select()函数(可以参考linux内核中Select函数实现原理分析):
系统调用select和poll的后端实现,用这两个系统调用来查询设备是否可读写,或是否处于某种状态。如果poll为空,则驱动设备会被认为即可读又可写,返回值是一个状态掩码
如何使用select()函数?
select()函 数的接口主要是建立在一种叫'fd_set'类型的基础上。它('fd_set')是一组文件描述符(fd)的集合。由于fd_set类型的长度在不同平台上不同,因此应该用一组标准的宏定义来处理此类变量:
fd_setset;
FD_ZERO(&set);
FD_SET(fd,&set);
FD_CLR(fd,&set);
FD_ISSET(fd,&set);
在过去,一个fd_set通常只能包含少于等于32个文件描述符,因为fd_set其实只用了一个int的比特矢量来实现,在大多数情况下,检查fd_set能包括任意值的文件描述符是系统的责任,但确定你的fd_set到底能放多少有时你应该检查/修改宏FD_SETSIZE的值。*这个值是系统相关的*,同时 检查你的系统中的select()的man手册。有一些系统对多于1024个文件描述符的支持有问题。[译者注:Linux就是这样的系统!你会发现sizeof(fd_set)的结果是128(*8=FD_SETSIZE=1024)尽管很少你会遇到这种情况。]
select的基本接口十分简单:
intselect(int nfds, fd_set *readset, fd_set *writeset,
fd_set *exceptset, struct timeval *timeout);
其中:
nfds
需要检查的文件描述符个数,数值应该比是三组fd_set中最大数
更大,而不是实际文件描述符的总数。
readset
用来检查可读性的一组文件描述符。
writeset
用来检查可写性的一组文件描述符。
exceptset
用来检查意外状态的文件描述符。(注:错误并不是意外状态)
timeout
NULL指针代表无限等待,否则是指向timeval结构的指针,代表最
长等待时间。(如果其中tv_sec和tv_usec都等于0,则文件描述符
的状态不被影响,但函数并不挂起)
函数将返回响应操作的对应操作文件描述符的总数,且三组数据均在恰当位置被修改,只有响应操作的那一些没有修改。接着应该用FD_ISSET宏来查找返回的文件描述符组。
这里是一个简单的测试单个文件描述符可读性的例子:
int isready(int fd)
{
int rc;
fd_set fds;
struct timeval tv;
FD_ZERO(&fds);
FD_SET(fd,&fds);
// tv.tv_sec = tv.tv_usec = 0;
//rc = select(fd+1, &fds, NULL, NULL,&tv);
rc = select(fd+1,&fds, NULL, NULL, NULL);
if (rc < 0)
return -1;
return FD_ISSET(fd,&fds) ? 1 : 0;
}
当然如果我们把NULL指 针作为fd_set传入的话,这就表示我们对这种操作的发生不感兴趣,但select()还是会等待直到其发生或者超过等待时间。
[译者注:在Linux中,timeout指的是程序在非sleep状态中度过的时间,而不是实际上过去的时间,这就会引起和非Linux平台移植上的时间不等问题。移植问题还包括在SystemV风格中select()在函数退出前会把timeout设为未定义的NULL状 态,而在BSD中则不是这样,Linux在这点上遵从SystemV,因此在重复利用timeout指针问题上也应该注意。]
Linux下select调用的过程:
1.用户层应用程序调用select(),底层调用poll())
2.核心层调 用sys_select()------> do_select()
最终调用文件描述符fd对应的structfile类型变量的struct file_operations*f_op的poll函数。
poll指向的函数返回当前可否读写的信息。
1)如果当前可读写,返回读写信息。
2)如果当前不可读写,则阻塞进程,并等待驱动程序唤醒,重新调用poll函数,或超时返回。
3.驱动需要实现poll函数。
当驱动发现有数据可以读写时,通知核心层,核心层重新调用poll指向的函数查询信息。
poll_wait(filp,&wait_q,wait)//此处将当前进程加入到等待队列中,但并不阻塞
在 中断中使用wake_up_interruptible(&wait_q)唤醒等待队列。
四、异步通知与异步IO
在设备驱动中使用异步通知可以使得对设备的访问可进行时,由驱动主动通知应用程序进行访问,这样,使用无阻塞IO的应用程序无须轮询设备是否可访问,而阻塞访问也可被类似“中断”的异步通知所取代。
异步通知
概念:一旦设备就绪,则主动通知应用程序,这样应用程序根本就不需要查询设备状态,这一点非常类似硬件上“中断”的概念,可称之为“信号驱使的异步IO”。
阻塞IO意味着一直等待设备可访问后再访问。
非阻塞IO中使用poll意味着查询设备是否可访问。
异步通知则意味着设备通知自身可访问,实现了异步IO。
Linux异步通知编程
讲linux异步通知编程就要说到Linux信号(可参考linux其他中的linux信号)。
使用信号进行进程间通信(IPC)是UNIX系统中的一种传统机制,当然,Linux也支持这种机制,并且在Linux系统中,异步通知使用信号来实现。
在Linux信 号中,除了SIGSTOP(停止执行)和SIGKILL(强行终止)两个信号外,进程能够忽略或捕获其它的全部信号,一个信号被捕获意味着当一个信号到达时有相应的代码去处理它,如果一个信号没有被这个进程捕获,内核将采用默认的处理。
信号的接收
系统调用signal用来设定某个信号的处理方法。该调用声明的格式如下:
void (*signal(int signum, void(*handler)(int)))(int);
在使用该调用的进程中加入以下头文件:
#include<signal.h>
上述 声明格式比较复杂,如果不清楚如何使用,也可以通过下面这种类型定义的格式来使用(POSIX的定义):
typedef void (*sighandler_t)(int);
sighandler_t signal(int signum, sighandler_thandler);
但这种格式在不同的系统中有不同的类型定义,所以要使用这种格式,最好还是参考一下联机手册。
在调用中,参数signum指出要设置处理方法的信号。第二个参数handler是一个处理函数,或者是
SIG_IGN:忽略参数signum所指的信号。
SIG_DFL:恢复参数signum所指信号的处理方法为默认值。
传递给信号处理例程的整数参数是信号值,这样可以使得一个信号处理例程处理多个信号。系统调用signal返回值是指定信号signum前一次的处理例程或者错误时返回错误代码SIG_ERR
信号的释放
设备驱动中异步通知编程比较简单,主要用到一项数据结果和两个函数,数据结构是fasync_struct结构体,两个函数分别如下:
处理FASYNC标志变更的函数
int fasync_helper(int fd, structfile *filp, int mode, struct fasync_struct **fa);
释放信号用的函数
void kill_fasync(structfasync_struct *fa, int sig,intband);
和其它的设备驱动一样,将fasync_struct结构体指针放在设备结构体中仍然是最佳选择。
Linux2.6异步IO
AIO的引入
输入输出模型是Linux系统中最常见的同步IO,在这个模型中,当请求发出之后,应用程序就会阻塞,直到请求满足为止。这是一种很好的解决方案,因为调用应用程序在等待IO请求完成时不需要使用任何CPU, 但是在某些情况下,IO请求可能需要与其他进程产生交叠,可移植操作系统接口(POSIX)异步IO(AIO)应用程序接口就提供了这种功能。
AIO就基本思想是允许进程发起很多IO操作,而不用阻塞或等待任何操作完成,稍后或在接收到IO操作完成的通知时,进程就可以检索IO操作的结果。
Select()函数所提供的功能(异步阻塞IO)与AIO类似,它对通知事件进行阻塞,而不是对IO调用进行阻塞。
在异步非阻塞IO中,我们可以同时发起多个传输操作,这需要每个传输操作都有唯一的上下文,这样才能在它们完成时区分到底是哪个传输操作完成了,在AIO中,通过aiocb(AIO IO ControlBlock)结构体进行区分,这个结构体包含了有关传输的所有信息,包括为数据准备的用户缓冲区,在产生IO(称为完成)通知时,aiocb结构就被用来唯一标识所完成的IO操作。
AIO系列API被GUN库函数所包含,它被POSIX.1b所要求。主要包括如下函数:
aio_read
aio_read函数请求对一个有效的文件描述符进行异步读操作。这个文件描述符可以表示一个文件、套接字甚至管道。aio_read函数的原型如下:
int aio_read( struct aiocb*aiocbp );
aio_read函数在请求进行排队之后会立即返回。如果执行成功,返回值就为0;如果出现错误,返回值就为-1,并设置errno的值。
要执行读操作,应用程序必须 对aiocb结构进行初始化。下面这个简短的例子就展示了如何填充aiocb请求结构,并使用aio_read来执行异步读请求(现在暂时忽略通知)操作。它还展示了aio_error的用法,不过我们将稍后再作解释。
aio_write
aio_write函数用来请求一个异步写操作。其函数原型如下:
int aio_write( struct aiocb*aiocbp );
aio_write函数会立即返回,说明请求已经进行排队(成功时返回值为0,失败时返回值为-1,并相应地设置errno)。
这与read系统调用类似,但是有一点不一样的行为需要注意。回想一下对于read调用来说,要使用的偏移量是非常重要的。然而,对于write来说,这个偏移量只有在没有设置O_APPEND选项的文件上下文中才会非常重要。如果设置了O_APPEND,那么这个偏移量就会被忽略,数据都会被附加到文件的末尾。否则,aio_offset域就确定了数据在要写入的文件中的偏移量。
aio_error
aio_error函数被用来确定请求的状态。其原型如下:
int aio_error( struct aiocb*aiocbp );
这个函数可以返回以下内容:
aio_return
异步I/O和标准块I/O之间的另外一个区别是我们不能立即访问这个函数的返回状态,因为我们并没有阻塞在read调用上。在标准的read调用中,返回状态是在该函数返回时提供的。但是在异步I/O中,我们要使用aio_return函数。这个函数的原型如下:
ssize_t aio_return( struct aiocb*aiocbp )
只有在aio_error调用确定请求已经完成(可能成功,也可能发生了错误)之后,才会调用这个函数。aio_return的返回值就等价于同步情况中read或write系统调用的返回值(所传输的字节数,如果发生错误,返回值就为-1)。
aio_suspend
我们可以使用aio_suspend函数来挂起(或阻塞)调用进程,直到异步请求完成为止,此时会产生一个信号,或者发生其他超时操作。调用者提供了一个aiocb引用列表,其中任何一个完成都会导致aio_suspend返回。aio_suspend的函数原型如下:
int aio_suspend( const structaiocb *const cblist[],
intn, const struct timespec *timeout );
aio_suspend的使用非常简单。我们要提供一个aiocb引用列表。如果任何一个完成了,这个调用就会返回0。否则就会返回-1,说明发生了错误。
aio_cancel
aio_cancel函数允许我们取消对某个文件描述符执行的一个或所有I/O请求。其原型如下:
int aio_cancel( int fd, structaiocb *aiocbp );
要取消一个请求,我们需要提 供文件描述符和aiocb引用。如果这个请求被成功取消了,那么这个函数就会返回AIO_CANCELED。如果请求完成了,这个函数就会返回AIO_NOTCANCELED。
要取消对某个给定文件描述符的所有请求,我们需要提供这个文件的描述符,以及一个对aiocbp的NULL引用。如果所有的请求都取消了,这个函数就会返回AIO_CANCELED;如果至少有一个请求没有被取消,那么这个函数就会返回AIO_NOT_CANCELED;如果没有一个请求可以被取消,那么这个函数就会返回AIO_ALLDONE。我们然后可以使用aio_error来验证每个AIO请求。如果这个请求已经被取消了,那么aio_error就会返回-1,并且errno会被设置为ECANCELED。
使用信号作为IO的通知
Linux信号作为异步通知的机制在AIO中仍然是适用的,为使用信号,使用AIO的应用程序同样需要定义信号处理程序,在指定的信号被产生时会触发调用这个处理程序,作为信号上下文的一部分,特定的aiocb请求被提供给信号处理函数用来区分AIO请求。
使用回调函数作为AIO的通知
除了信号以外,应用程序还可提供一个回调(callback)函数给内核,以便AIO的请求完成后内核调用这个函数。
总结
本节所将的异步IO可以使得应用程序在等待IO操作的同时进行其他操作。
使用信号可以实现驱动程序与用户程序之间的异步通知,总体而言,设备驱动和用户空间要完成以下工作:用户空间设置文件的拥有者、FASYNC标志及捕获信号,内核空间响应对文件的拥有者,FASYNC标志的设置,并在资源可获得时释放信号。
Linux2.6内核包含对AIO的支持为用户空间提供统一的异步IO接口,在AIO中,信号和回调函数是实现内核空间对用户空间应用程序通知的两种机制。
五、移位一个设备(llseek)
llseek是修改文件中的当前读写位置的系统调用。内核中的缺省的实现进行移位通过修改filp->f_pos,这是文件中的当前读写位置。对于lseek系统调用要正确工作,读和写方法必须通过更新它们收到的偏移量来配合。
如果设备是不允许移位的,你 不能只制止声明llseek操作,因为缺省的方法允许移位。应当在你的open方法中,通过调用nonseekable_open通知内核你的设备不支持llseek:
intnonseekable_open(structinode*inode;structfile*filp);
完整起见,你也应该在你的file_operations结构中设置llseek方法到一个特殊的帮助函数no_llseek(定义在<linux/fs.h>)。