信号驱动式I/O是指进程预先告知内核,使得当某个描述符上发生事件时,内核使用信号通知该进程。
针对一个套接字使用信号驱动式I/O(SIGIO)的步骤如下:
1. 建立SIGIO信号的处理函数
2. 设置该套接字的属主,通常使用fcntl的F_SETFL命令设置。
3. 开启套接字的信号驱动式I/O,通常使用fcntl的F_SETFL命令打开O_ASYNC标志。
对于TCP套接字,信号式驱动几乎无用,因为很多事件发生都会产生SIGIO信号,而我们无法区分。
对于UDP套接字,SIGIO信号在发生以下事件时产生:
1. 数据报到达套接字
2. 套接字上发生异步错误(前提是套接字已连接)
因此我们在SIGIO信号处理函数中调用recvfrom就能读取到达的数据报或者获取异步错误。
在基本UDP套接字编程一节中,我们实现了一个UDP回显服务器,它是以等-停方式工作的,即读取一个数据报,将它发送回去,再等待读取下一个数据报。
本节我们使用信号驱动式I/O实现一个UDP回显服务器程序,它完全由信号SIGIO驱动。一个SIGIO信号产生,说明套接字收到了一个或多个数据报(Linux信号不排队),为了读取这些数据报,我们把套接字设置成非阻塞,调用recvfrom循环读取数据报放到一个队列中,直到返回EWOULDBLOCK。主函数(dg_echo函数)调用sigsuspend等待SIGIO信号发生,然后从队列中取出所有的数据报回射出去。代码如下:
#include "unp.h"
static int sockfd;
#define QSIZE 8
#define MAXDG 4096
typedef struct {
void *dg_data; /*存放数据报的缓冲区*/
size_t dg_len; /*数据长度*/
struct sockaddr *dg_sa; /*指向客户套接字地址结构*/
socklen_t dg_salen; /*套接字地址结构大小*/
} DG;
static DG dg[QSIZE]; /*处理数据报队列的大小*/
static int iget;
static int iput;
static int nqueue; /*队列大小*/
static socklen_t clilen;
static void sig_io(int);
static void sig_hup(int);
void dg_echo(int sockfd_arg, SA *pcliaddr, socklen_t clilen_arg)
{
int i;
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(SIGIO, sig_io);
Fcntl(sockfd, F_SETOWN, getpid()); /*设置套接字属主*/
Ioctl(sockfd, FIOASYNC, &on); /*设置套接字异步访问标志*/
Ioctl(sockfd, FIONBIO, &on); /*设置套接字非阻塞标志*/
Sigemptyset(&zeromask);
Sigemptyset(&oldmask);
Sigemptyset(&newmask);
Sigaddset(&newmask, SIGIO);
/*阻塞SIGIO信号*/
Sigprocmask(SIG_BLOCK, &newmask, &oldmask);
for ( ; ; ) {
while (nqueue == 0) /*当队列为空,等待信号发生*/
sigsuspend(&zeromask);
/*解阻塞SIGIO信号*/
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;
/*阻塞SIGIO信号*/
Sigprocmask(SIG_BLOCK, &newmask, &oldmask);
nqueue--;
}
}
static void sig_io(int sigio)
{
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) {
if (errno == EWOULDBLOCK)
break;
else
err_sys("recvfrom error");
}
ptr->dg_len = len;
nread++;
nqueue++;
if (++iput >= QSIZE)
iput = 0;
}
}
这段代码有几个很关键的地方:
1. 主程序for循环前要先阻塞SIGIO信号。如果没有阻塞SIGIO信号,在执行for循环的前两行语句,我们可能测试nqueue时发现它为0,但是刚测试完毕SIGIO信号就递交了,导致nqueue被设置为1.我们接着调用sigsuspend进入睡眠,这样实际上我们就错过了这个信号,除非还有信号发生,否则我们将永远不能从sigsuspend调用中被唤醒。也就是代码包含有竞争条件。
2. sigsuspend函数的功能是原子性地将进程投入睡眠,并把它的信号掩码设置成zeromask,直到某个信号发生并且该信号的信号处理函数返回之后才返回,并将信号掩码恢复成调用该函数之前的信号掩码。由于在信号处理函数中已经读取了数据报,nqueue不为零,所以while循环必然不成立。之后就从队列中取出数据报发送出去。
3. nqueue变量由主函数和信号处理函数共享,所以在修改时,一定要先阻塞SIGIO信号。而iget变量由主函数独有,所以在修改之前不需要阻塞SIGIO信号。