信号驱动式I/O
1、概述
信号驱动式I/O是指进程预先告知内核,使得当某个描述符上发生某事时,内核使用信号通知相关进程。
信号驱动式I/O与异步式I/O是有区别的:后者通常定义为进程执行I/O系统调用告知内核启动某个I/O操作,内核启动I/O操作后立即返回到进程中。进程在I/O操作发生期间继续执行。当操作完成或遇到错误时,内核以进程在I/O系统调用中指定的某种方式通知进程。
POSIX提供了真正的异步IO操作,函数aio_XXX允许进程指定I/O操作完成时是否由内核产生信号以及产生什么信号。
2、套接字的信号驱动式I/O
一个套接字使用信号驱动式I/O(SIGIO)要求进程执行以下3个步骤:
- 建立SIGIO信号的信号处理函数(可以使用sigaction重新的signal函数进行绑定)。
- 设置该套接字的属主,通常使用fcntl的F_SETOWN命令设置。
- 开启该套接字的信号驱动式I/O,通常通过fcntl的F_SETFL命令打开O_ASYNC标志完成。
2.1对于UDP套接字的SIGIO信号
在UDP上使用信号驱动式I/O是简单的,SIGIO信号在发生以下两种事件是产生:
-
数据报到达套接字;
-
套接字上发生异步错误。
其中UDP套接字上发生异步错误的前提是UDP套接字已连接(使用connect函数连接)。
2.2对于TCP套接字的SIGIO信号
信号驱动式I/O对于TCP套接字近乎无用,因为该信号产生的太过频繁了,以下条件都会使TCP套接字产生SIGIO信号:
-
监听套接字上某个连接请求已经完成;
-
某个断连请求已经完成;
-
某个断连请求已经发起;
-
某个连接之半已经关闭;(这里我的理解应该是半关闭状态吧,主动关闭的一方接受到了被动关闭一方的ACK响应吧)
-
数据到达套接字;
-
数据已经从套接字发送走(即输出缓冲区有空闲空间);
-
发生某个异步的错误。
3、使用SIGIO实现一个UDP回射服务器
下面是使用信号驱动式的代码片段。
#include "unp.h"
static int sockfd;
#define QSIZE 8 /* size of input queue */
#define MAXDG 4096 /* max datagram size */
typedef struct {
void *dg_data; /* ptr to actual datagram */
size_t dg_len; /* length of datagram */
struct sockaddr *dg_sa; /* ptr to sockaddr{} client's address */
socklen_t dg_salen; /* length of sockaddr{} */
}DG;
static DG dg[QSIZE]; /* queue of datagrams to process */
static long cntread[QSIZE+1];/* diagnostic counter*/
static int iget; /* next one for main loop to process */
static int iput; /* next one for signal handler to read into*/
static int nqueue; /* on queue for main loop to process */
static socklen_t clilen; /* max length of sockaddr{} */
static void sig_io(int);
static void sig_hup(int);
void
dg_echo(int sockfd_arg,SA *pcliaddr,socklen_t clilen_arg)
{
int i;
const int on = 1;
sigset_t zeromask,newmask,oldmask;
sockfd = sockfd_arg;
clilen = clilen_arg;
for(i=0;i<QSIZE;++i){
dg[i].dg_data = Malloc(MAXDG);
dg[i].dg_sa = Malloc(clilen);
dg[i].dg_salen = clilen;
}
iget = iput = nqueue = 0;
Signal(SIGHUP,sig_hup);
Signal(SIGIO,sig_io);//为SIGIO信号添加信号处理函数
Fcntl(sockfd,F_SETOWN,getpid());//为sockfd绑定属主进程
Ioctl(sockfd,FIOASYNC,&on);//开启信号驱动式
Ioctl(sockfd,FIONBIO,&on);//开启该套接字为非阻塞的套接字
Sigemptyset(&zeromask);
Sigemptyset(&oldmask);
Sigemptyset(&newmask);
Sigaddset(&newmask,SIGIO);//signal we want to block
Sigprocmask(SIG_BLOCK,&newmask,&oldmask);//先blockSIGIO信号
for(;;){
while(nqueue == 0){
sigsuspend(&zeromask);//wait for datagram to process
}
Sigprocmask(SIG_SETMASK,&oldmask,NULL);
Sendto(sockfd,dg[iget].dg_data,dg[iget].dg_len,0,dg[iget].dg_sa,dg[iget].dg_salen);
if(++iget >= QSIZE)
iget = 0;
Sigprocmask(SIG_BLOCK,&newmask,&oldmask);
nqueue--;
}
}
static void
sig_io(int signo)
{
ssize_t len;
int nread;
DG *ptr;
for(nread = 0;;){
if(nqueue >= QSIZE)
err_quit("receive overflow");//接收队列已满
ptr = &dg[iput];
ptr->dg_salen = clilen;
len = recvfrom(sockfd,ptr->dg_data,MAXDG,0,ptr->dg_sa,&ptr->dg_salen);
if(len < 0){
/* 由于sockfd设置为非阻塞的,故而在没有数据可读的时候,会直接返回,errno将被设置为EWOULDBLOCK */
if(errno == EWOULDBLOCK)//没有数据到达
break;
else
err_sys("recvfrom error");//recvfrom函数出现错误
}
ptr->dg_len = len;
nread++;
nqueue++;
if(++iput >=QSIZE)
iput = 0;
}
cntread[nread]++;
}
static void
sig_hup(int signo)
{
int i;
for(i=0;i<=QSIZE;++i){
printf("cntread[%d] = %ld\n",i,cntread[i]);
}
}
4、小结
信号驱动式I/O就是让内核在套接字上发生“某事”时使用SIGIO信号通知进程。
- 对于已连接TCP套接字,因为通知的条件太多,导致这个特性近乎无用。
- 对于TCP监听套接字,当通知发生时表示有一个新连接已准备好接受了。
- 对于UDP套接字,这个通知意味着或者到达一个数据报,或者到达一个异步错误。