【转】poll()函数 select()函数以及FD_ZERO、FD_SET、FD_CLR、FD_ISSET
select()函数以及FD_ZERO、FD_SET、FD_CLR、FD_ISSET
select函数用于在非阻塞中,当一个套接字或一组套接字有信号时通知你,系统提供select函数来实现多路复用输入/输出模型,原型:
#include <sys/time.h>
#include <unistd.h>
int select(int maxfd,fd_set *rdset,fd_set *wrset,fd_set *exset,struct timeval *timeout);
参数maxfd是需要监视的最大的文件描述符值+1;rdset,wrset,exset分别对应于需要检测的可读文件描述符的集合,可写文件描述符的集 合及异常文件描述符的集合。struct timeval结构用于描述一段时间长度,如果在这个时间内,需要监视的描述符没有事件发生则函数返回,返回值为0。
fd_set(它比较重要所以先介绍一下)是一组文件描述字(fd)的集合,它用一位来表示一个fd(下面会仔细介绍),对于fd_set类型通过下面四个宏来操作:
FD_ZERO(fd_set *fdset);将指定的文件描述符集清空,在对文件描述符集合进行设置前,必须对其进行初始化,如果不清空,由于在系统分配内存空间后,通常并不作清空处理,所以结果是不可知的。
FD_SET(int fd, fd_set *fdset);用于在文件描述符集合中增加一个新的文件描述符。
FD_CLR(int fd, fd_set *fdset);用于在文件描述符集合中删除一个文件描述符。
FD_ISSET(int fd, fd_set *fdset);用于测试指定的文件描述符是否在该集合中。
过去,一个fd_set通常只能包含<32的fd(文件描述字),因为fd_set其实只用了一个32位矢量来表示fd;现在,UNIX系统通常会在头文件<sys/select.h>中定义常量FD_SETSIZE,它是数据类型fd_set的描述字数量,其值通常是1024,这样就能表示<1024的fd。根据fd_set的位矢量实现,我们可以重新理解操作fd_set的四个宏:
fd_set set;
FD_ZERO(&set);
FD_SET(0, &set);
FD_CLR(4, &set);
FD_ISSET(5, &set);
―――――――――――――――――――――――――――――――――――――――
注意fd的最大值必须<FD_SETSIZE。
―――――――――――――――――――――――――――――――――――――――
select函数的接口比较简单:
int select(int nfds, fd_set *readset, fd_set *writeset,fd_set* exceptset, struct tim *timeout);
功能:
测试指定的fd可读?可写?有异常条件待处理?
参数:
—— nfds
需要检查的文件描述字个数(即检查到fd_set的第几位),数值应该比三组fd_set中所含的最大fd值更大,一般设为三组fd_set中所含的最大fd值加1(如在readset,writeset,exceptset中所含最大的fd为5,则nfds=6,因为fd是从0开始的)。设这个值是为提高效率,使函数不必检查fd_set的所有1024位。
—— readset
用来检查可读性的一组文件描述字。
—— writeset
用来检查可写性的一组文件描述字。
—— exceptset
用来检查是否有异常条件出现的文件描述字。(注:错误不包括在异常条件之内)
—— timeout
用于描述一段时间长度,如果在这个时间内,需要监视的描述符没有事件发生则函数返回,返回值为0。
有三种可能:
1.timeout=NULL(阻塞:select将一直被阻塞,直到某个文件描述符上发生了事件)
2.timeout所指向的结构设为非零时间(等待固定时间:如果在指定的时间段里有事件发生或者时间耗尽,函数均返回)
3.timeout所指向的结构,时间设为0(非阻塞:仅检测描述符集合的状态,然后立即返回,并不等待外部事件的发生)
返回值:
返回对应位仍然为1的fd的总数。
Remarks:
三组fd_set均将某些fd位置0,只有那些可读,可写以及有异常条件待处理的fd位仍然为1。
举个例子,比如recv(), 在没有数据到来调用它的时候,你的线程将被阻塞,如果数据一直不来,你的线程就要阻塞很久.这样显然不好.
所以采用select来查看套节字是否可读(也就是是否有数据读了)
步骤如下——
socket s;
.....
fd_set set;
while(1)
{
FD_ZERO(&set);//将你的套节字集合清空
FD_SET(s, &set);//加入你感兴趣的套节字到集合,这里是一个读数据的套节字s
select(0,&set,NULL,NULL,NULL);//检查套节字是否可读,
//很多情况下就是是否有数据(注意,只是说很多情况)
//这里select是否出错没有写
if(FD_ISSET(s, &set) //检查s是否在这个集合里面,
{ //select将更新这个集合,把其中不可读的套节字去掉
//只保留符合条件的套节字在这个集合里面
recv(s,...);
}
//do something here
}
理解select模型的关键在于理解fd_set,为说明方便,取fd_set长度为1字节,fd_set中的每一bit可以对应一个文件描述符fd。则1字节长的fd_set最大可以对应8个fd。
(1)执行fd_set set; FD_ZERO(&set);则set用位表示是0000,0000。
(2)若fd=5,执行FD_SET(fd,&set);后set变为0001,0000(第5位置为1)
(3)若再加入fd=2,fd=1,则set变为0001,0011
(4)执行select(6,&set,0,0,0)阻塞等待
(5)若fd=1,fd=2上都发生可读事件,则select返回,此时set变为0000,0011。注意:没有事件发生的fd=5被清空。
基于上面的讨论,可以轻松得出select模型的特点:
(1)可监控的文件描述符个数取决与sizeof(fd_set)的值。我这边服务 器上sizeof(fd_set)=512,每bit表示一个文件描述符,则我服务器上支持的最大文件描述符是512*8=4096。据说可调,另有说虽 然可调,但调整上限受于编译内核时的变量值。本人对调整fd_set的大小不太感兴趣,参考http://www.cppblog.com/CppExplore/archive/2008/03/21/45061.html中的模型2(1)可以有效突破select可监控的文件描述符上限。
(2)将fd加入select监控集的同时,还要再使用一个数据结构array保存放到select监控集中的fd,一是用于再select 返回后,array作为源数据和fd_set进行FD_ISSET判断。二是select返回后会把以前加入的但并无事件发生的fd清空,则每次开始 select前都要重新从array取得fd逐一加入(FD_ZERO最先),扫描array的同时取得fd最大值maxfd,用于select的第一个 参数。
(3)可见select模型必须在select前循环array(加fd,取maxfd),select返回后循环array(FD_ISSET判断是否有时间发生)。
下面给一个伪码说明基本select模型的服务器模型:
array[slect_len];
nSock=0;
array[nSock++]=listen_fd;(之前listen port已绑定并listen)
maxfd=listen_fd;
while{
FD_ZERO(&set);
foreach (fd in array)
{
fd大于maxfd,则maxfd=fd
FD_SET(fd,&set)
}
res=select(maxfd+1,&set,0,0,0);
if(FD_ISSET(listen_fd,&set))
{
newfd=accept(listen_fd);
array[nsock++]=newfd;
if(--res=0) continue
}
foreach 下标1开始 (fd in array)
{
if(FD_ISSET(fd,&set))
执行读等相关操作
如果错误或者关闭,则要删除该fd,将array中相应位置和最后一个元素互换就好,nsock减一
if(--res=0) continue
}
}
使用select函数的过程一般是:
先调用宏FD_ZERO将指定的fd_set清零,然后调用宏FD_SET将需要测试的fd加入fd_set,接着调用函数select测试fd_set中的所有fd,最后用宏FD_ISSET检查某个fd在函数select调用后,相应位是否仍然为1。
以下是一个测试单个文件描述字可读性的例子:
int isready(int fd)
{
int rc;
fd_set fds;
struct tim tv;
FD_ZERO(&fds);
FD_SET(fd,&fds);
tv.tv_sec = tv.tv_usec = 0;
rc = select(fd+1, &fds, NULL, NULL, &tv);
if (rc < 0) //error
return -1;
return FD_ISSET(fd,&fds) ? 1 : 0;
}
下面还有一个复杂一些的应用:
//这段代码将指定测试Socket的描述字的可读可写性,因为Socket使用的也是fd
uint32 SocketWait(TSocket *s,bool rd,bool wr,uint32 timems)
{
fd_set rfds,wfds;
#ifdef _WIN32
TIM tv;
#else
struct tim tv;
#endif
FD_ZERO(&rfds);
FD_ZERO(&wfds);
if (rd) //TRUE
FD_SET(*s,&rfds); //添加要测试的描述字
if (wr) //FALSE
FD_SET(*s,&wfds);
tv.tv_sec=timems/1000; //second
tv.tv_usec=timems%1000; //ms
for (;;) //如果errno==EINTR,反复测试缓冲区的可读性
switch(select((*s)+1,&rfds,&wfds,NULL,
(timems==TIME_INFINITE?NULL:&tv))) //测试在规定的时间内套接口接收缓冲区中是否有数据可读
{ //0--超时,-1--出错
case 0:
return 0;
case (-1):
if (SocketError()==EINTR)
break;
return 0; //有错但不是EINTR
default:
if (FD_ISSET(*s,&rfds)) //如果s是fds中的一员返回非0,否则返回0
return 1;
if (FD_ISSET(*s,&wfds))
return 2;
return 0;
};
}
poll函数和select函数非常相似,但是函数接口不一样。
int poll(struct pollfd fdarray[], nfds_t nfds, int timeout);
int select(int maxfdp1, fd_set *restrict readfds, fd_set *restrict expectfds, struct timeval * restrict tvptr);
其中poll函数中,结构pollfd如下:
struct pollfd{
int fd; //file descriptor
short event;//event of interest on fd
short revent;//event that occurred on fd
}
每一个pollfd结构体指定了一个被监视的文件描述符,可以传递多个结构体,指示poll()监视多个文件描述符。每个结构体的events域是监视该文件描述符的事件掩码,由用户来设置这个域。revents域是文件描述符的操作结果事件掩码。内核在调用返回时设置这个域。events域中请求的任何事件都可能在revents域中返回。合法的事件如下:
POLLIN
有数据可读。
POLLRDNORM
有普通数据可读。
POLLRDBAND
有优先数据可读。
POLLPRI
有紧迫数据可读。
POLLOUT
写数据不会导致阻塞。
POLLWRNORM
写普通数据不会导致阻塞。
POLLWRBAND
写优先数据不会导致阻塞。
POLLMSG
SIGPOLL 消息可用。
此外,revents域中还可能返回下列事件:
POLLER
指定的文件描述符发生错误。
POLLHUP
指定的文件描述符挂起事件。
POLLNVAL
指定的文件描述符非法。
这些事件在events域中无意义,因为它们在合适的时候总是会从revents中返回。使用poll()和select()不一样,你不需要显式地请求异常情况报告。
POLLIN | POLLPRI等价于select()的读事件,POLLOUT |POLLWRBAND等价于select()的写事件。POLLIN等价于POLLRDNORM |POLLRDBAND,而POLLOUT则等价于POLLWRNORM。
例如,要同时监视一个文件描述符是否可读和可写,我们可以设置 events为POLLIN |POLLOUT。在poll返回时,我们可以检查revents中的标志,对应于文件描述符请求的events结构体。如果POLLIN事件被设置,则文件描述符可以被读取而不阻塞。如果POLLOUT被设置,则文件描述符可以写入而不导致阻塞。这些标志并不是互斥的:它们可能被同时设置,表示这个文件描述符的读取和写入操作都会正常返回而不阻塞。
timeout参数指定等待的毫秒数,无论I/O是否准备好,poll都会返回。timeout指定为负数值表示无限超时;timeout为0指示poll调用立即返回并列出准备好I/O的文件描述符,但并不等待其它的事件。这种情况下,poll()就像它的名字那样,一旦选举出来,立即返回。
返回值和错误代码
成功时,poll()返回结构体中revents域不为0的文件描述符个数;如果在超时前没有任何事件发生,poll()返回0;失败时,poll()返回-1,并设置errno为下列值之一:
EBADF
一个或多个结构体中指定的文件描述符无效。
EFAULT
fds指针指向的地址超出进程的地址空间。
EINTR
请求的事件之前产生一个信号,调用可以重新发起。
EINVAL
nfds参数超出PLIMIT_NOFILE值。
ENOMEM
可用内存不足,无法完成请求。
三、poll 和 select
当应用程序需要进行对多文件读写时,若某个文件没有准备好,则系统会处于读 写阻塞的状态,并影响了其他文件的读写。为了避免这种情况,在必须使用多 输入输出流又不想阻塞在它们任何一个上的应用程序常将非阻塞 I/O 和poll(System V)、select(BSD Unix)、 epoll(linux2.5.45开始)系统调 用配合使用。当poll函数返回时,会给出一个文件是否可读写的标志,应用程序根据不同的标志读写相应的文件,实现非阻塞 的读写。这些系统调用功能相同: 允许进程来决定它是否可读或写一个或多 个文件而不阻塞。这些调用也可阻塞进程直到任何一个给定集合的文件描述符可用来读或写。这些调用都需要来自设备驱动中poll 方法的支持,poll返 回不同的标志,告诉主进程文件是否可以读写,其原型(定义在<linux\poll.h> ):
unsigned int (*poll) (struct file *filp, poll_table *wait);
实现这个设备方法分两步:
1. 在 一个或多个可指示查询状态变化的等待队列上调用 poll_wait. 如果没有文件描述符可用来执行 I/O, 内核使这个进程在等待队列上等待所有的传递给系统调用的文件描述符. 驱动通过调用函数 poll_wait增加一个等待队列到 poll_table结构,原型:
void poll_wait (struct file *, wait_queue_head_t *, poll_table *);
2. 返 回一个位掩码:描述可能不必阻塞就立刻进行的操作,几个标志(通过<linux/poll.h> 定义)用来指示可能的操作:
| 标志 | 含义 |
| POLLIN | 如果设备无阻塞的读,就返回该值 |
| POLLRDNORM | 通常的数据已经准备好,可以读了,就返回该值。通常的做法是会返回(POLLLIN|POLLRDNORA) |
| POLLRDBAND | 如果可 以从设备读出带外数据,就返回该值,它只可在linux内 核的某些网络代码中使用,通常不用在设备驱动程序中 |
| POLLPRI | 如果可 以无阻塞的读取高优先级(带外)数据,就返回该值,返回该值会导致select报告文件发生异常,以为select八带外数据当作异常处理 |
| POLLHUP | 当读设 备的进程到达文件尾时,驱动程序必须返回该值,依照select的功能描述,调用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_set set;
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的基本接口十分简单:
int select(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平台移植上的时间不等问 题。移植问题还包括在System V风格中select()在函数退出前会 把timeout设为未定义的NULL状 态,而在BSD中则不是这样, Linux在这点上遵从System V,因此在重复利用timeout指针问题上也应该注意。]
Linux下select调用的过程:
1.用户层应用程序调用select(),底层调用poll())
2.核心层调 用sys_select() ------> do_select()
最终调用文件描述符fd对应的struct file类型变量的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_t handler);
但这种格式在不同的系统中有 不同的类型定义,所以要使用这种格式,最好还是参考一下联机手册。
在调用中,参数signum指出要设置处理方法的信号。第二个参数handler是一个处理函数,或者是
SIG_IGN:忽略参数signum所指的信号。
SIG_DFL:恢复参数signum所指信号的处理方法为默认值。
传递给信号处理例程的整数参数是信号值,这样可以使得一个信号处理例程处理多个信号。系统调用signal返回值是指定信号signum前一次的处理例程或者错误时返回错误代码SIG_ERR
信号的释放
设备驱动中异步通知编程比较简单,主要用到一项数据结果和两个函数,数据结构是fasync_struct结构体,两个函数分别如下:
处理FASYNC标志变更的函数
int fasync_helper(int fd, struct file *filp, int mode, struct fasync_struct **fa);
释放信号用的函数
void kill_fasync(struct fasync_struct *fa, int sig,int band);
和其它的设备驱动一样,将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 Control Block)结构体进行区分,这个结构体包含了有关传输的所有信息,包括为数据准 备的用户缓冲区,在产生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 );
这个函数可以返回以下内容:
- EINPROGRESS,说明请求尚未完成
- ECANCELLED,说明请求被应用程序取消了
- -1,说明发生了错误,具体错误原因可以查阅 errno
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 struct aiocb *const cblist[],
int n, const struct timespec *timeout );
aio_suspend 的使用非常简单。我们要提供一个 aiocb 引用列表。如果任何一个完成了,这个调用就会返回 0。否则就会返回 -1, 说明发生了错误。
aio_cancel
aio_cancel 函数允许我们取消对某个文件描述符执行的一个或所有 I/O 请求。其原型如下:
int aio_cancel( int fd, struct aiocb *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 :
int nonseekable_open(struct inode *inode; struct file *filp);
完整起见, 你也应该在你的 file_operations 结构中设置 llseek 方法到一个特殊的帮助函数 no_llseek(定义在 <linux/fs.h> )。
浙公网安备 33010602011771号