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

信号驱动式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信号。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值