《UNIX网络编程 卷1》 笔记: 信号驱动式I/O

来源:互联网 发布:java中final和static 编辑:程序博客网 时间:2024/06/05 01:58

信号驱动式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 4096typedef 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;elseerr_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信号。

阅读全文
0 0
原创粉丝点击