UNIX网络编程——信号驱动式I/O

时间:2023-11-25 14:34:56

信号驱动式I/O是指进程预先告知内核,使得当某个描述符上发生某事时,内核使用信号通知相关进程。

针对一个套接字使用信号驱动式I/O,要求进程执行以下3个步骤:

  • 建立SIGIO信号的信号处理函数。
  • 设置该套接字的属主,通常使用fcntl的F_SETOWN命令设置。
  • 开启该套接字的信号驱动式I/O,通常通过使用fcntl的F_SETFL命令打开O_ASYNC标志完成。

1.对于UDP套接字的SIGIO信号

在UDP上使用信号驱动式I/O是简单的。SIGIO信号在发生以下事件时产生:

(1)数据报到达套接字;

(2)套接字上发生异步错误。

因此当捕获对于某个UDP套接字的SIGIO信号时,我们调用recvfrom或者读入到达的数据报,或者获取发生的异步错误。

2.对于TCP套接字的SIGIO信号

不幸的是,信号驱动式I/O对于TCP套接字近乎无用。问题在于该信号产生的过于频繁,并且它的出现并没有告诉我们发生了什么事情。下列条件均导致对于一个TCP套接字产生SIGIO信号(假设该套接字的信号驱动式I/O已经开启):

(1)监听套接字上某个连接请求已经完成;

(2)某个断连请求已经发起;

(3)某个断连请求已经完成;

(4)某个连接之半已经关闭;

(5)数据到达套接字;

(6)数据已经从套接字发送走

(7)发生某个异步错误。

3.使用SIGIO的UDP回射服务器程序

UNIX网络编程——信号驱动式I/O

客户端程序:

#include<stdio.h>
#include<sys/types.h>
#include<sys/socket.h>
#include<unistd.h>
#include<stdlib.h>
#include<errno.h>
#include<arpa/inet.h>
#include<netinet/in.h>
#include<string.h>
#include<signal.h>
#include <fcntl.h>
#define MAXLINE 1024
#define SERV_PORT 3333 #define ERR_EXIT(m) \
do { \
perror(m); \
exit(EXIT_FAILURE); \
} while (0)
typedef struct sockaddr SA; void dg_cli(FILE *fp, int sockfd, const SA *pservaddr, socklen_t servlen)
{
int n;
char sendline[MAXLINE], recvline[MAXLINE + 1]; while (fgets(sendline, MAXLINE, fp) != NULL) { sendto(sockfd, sendline, strlen(sendline), 0, pservaddr, servlen); n = recvfrom(sockfd, recvline, MAXLINE, 0, NULL, NULL); recvline[n] = 0; /* null terminate */
fputs(recvline, stdout);
}
}
int main(int argc, char **argv)
{
int sockfd;
struct sockaddr_in servaddr; if (argc != 2)
ERR_EXIT("usage: udpcli <IPaddress>"); bzero(&servaddr, sizeof(servaddr));
servaddr.sin_family = AF_INET;
servaddr.sin_port = htons(SERV_PORT);
inet_pton(AF_INET, argv[1], &servaddr.sin_addr); sockfd = socket(AF_INET, SOCK_DGRAM, 0); dg_cli(stdin, sockfd, (SA *) &servaddr, sizeof(servaddr)); exit(0);
}

服务器程序:

#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{} w/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++) { /* init queue of buffers */
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);
fcntl(sockfd, F_SETOWN, getpid());
ioctl(sockfd, FIOASYNC, &on);
ioctl(sockfd, FIONBIO, &on); sigemptyset(&zeromask); /* init three signal sets */
sigemptyset(&oldmask);
sigemptyset(&newmask);
sigaddset(&newmask, SIGIO); /* signal we want to block */ sigprocmask(SIG_BLOCK, &newmask, &oldmask);
for ( ; ; ) {
while (nqueue == 0)
sigsuspend(&zeromask); /* wait for datagram to process */ /* 4unblock 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; /* 4block SIGIO */
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_EXIT("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; /* all done; no more queued to read */
else
ERR_EXIT("recvfrom error");
}
ptr->dg_len = len; nread++;
nqueue++;
if (++iput >= QSIZE)
iput = 0; }
cntread[nread]++; /* histogram of # datagrams read per signal */
}
static void
sig_hup(int signo)
{
int i; for (i = 0; i <= QSIZE; i++)
printf("cntread[%d] = %ld\n", i, cntread[i]);
}

已收取数据报队列
5-15  SIGIO信号处理函数把到达的数据报放入一个队列。该队列是一个DG结构数组,我们把它作为一个环形缓冲区处理。每个DG结构包括指向所收取数据报的一个指针,该数据报的长度,指向含有客户协议地址的某个套接字地址结构的一个指针,该协议地址的大小。静态分配QSIZE个DG结构,dg_echo函数调用malloc动态分配所有数据报和套接字地址结构的内存空间。我们还分配一个诊断用计数器cntread。下图展示了这个DG结构数组,其中假设第一个元素指向一个150字节的数据报,与它关联的套接字地址结构长度为16.

UNIX网络编程——信号驱动式I/O

数组下标
17-19   iget是主循环将处理的下一个数组元素的下标,iput是信号处理函数将存放到的下一个数组元素的下标,nqueue是队列*主循环处理的数据报的总数。

初始化已接收数据报队列
34-39   把套接字描述符保存在一个全局变量中,因为信号处理函数需要它。初始化已接收数据报队列。

建立信号处理函数并设置套接字标志
41-45   为SIGHUP(用于诊断目的)和SIGIO建立信号处理函数。使用fcntl设置套接字的属主,使用ioctl设置信号驱动和非阻塞式I/O标志。

初始化信号集
47-50   初始化三个信号集:zeromask(从不改变),oldmask(记录我们阻塞SIGIO时原来的信号掩码)和newmask。使用sigaddset打开newmask中与SIGIO对应的位。

阻塞SIGIO并等待有事可做
52-55   调用sigprocmask把进程的当前信号掩码保存到oldmask中,然后把newmask逻辑或到当前信号掩码。这将阻塞SIGIO并返回当前信号掩码。接着进入for循环,并测试nqueue计数器。只要该计数器为0,进程就无事可做,这时我们可以调用sigsuspend。该POSIX函数先内部保存当前信号掩码,再把当前信号掩码设置为它的参数(zeromask)。既然zeromask是一个空信号集,因而所有信号都被开通。sigsuspend在进程捕获一个信号并且该信号的处理函数返回之后才返回。(它是一个不寻常的函数,因为它总是返回EINTR错误)。在返回之前sigsuspend总是把当前信号掩码恢复为调用时刻的值,在本例中就是newmask的值,从而确保sigsuspend返回之后SIGIO继续被阻塞。这时我们可以测试计数器nqueue的理由,因为我们知道测试它时SIGIO信号不可能被递交。

解阻塞SIGIO并发送应答
57-66   调用sigprocmask把进程的信号掩码设置为先前保存的值(oldmask),从而解除SIGIO的阻塞。然后调用sendto发送应答。递增iget下标,若其值等于DG结构数组元素数目则将其值置回0。因为我们把该数组作为环形缓冲区对待。注意:修改iget时我们不必阻塞SIGIO,因为只有主循环使用这个下标,信号处理函数从不改动它。

阻塞SIGIO
67-68   阻塞SIGIO,递减nqueue。修改nqueue时我们必须阻塞SIGIO,因为它是主循环和信号处理函数共同使用的变量。我们在循环顶部测试nqueue时也需要SIGIO阻塞着。

编写本信号处理函数时我们遇到的问题是POSIX信号通常不排队。这一点意味着如果我们在信号处理函数执行(期间确保该信号被阻塞),期间该信号又发生了2次,那么它实际只被递交1次。让我们考虑下述情形。一个数据报到达导致SIGIO被递交。它的信号处理函数读入该数据报并把它放到供主循环读取的队列中。然而在信号处理函数执行期间,另有两个数据报到达,导致SIGIO再产生两次。由于SIGIO被阻塞,当他的信号处理函数返回时,该处理函数仅仅再被调用一次。该信号处理函数的第二次执行读入第二个数据报,第三个数据报则仍然留在套接字接收队列中。第三个数据报被读入的前提条件时由第四个数据报到达。当第四个数据报到达时,被读入并放到供主循环读取的队列中的是第三个而不是第四个数据报。
    既然信号时不排队的,开启信号驱动式I/O的描述符通常也被设置为非阻塞式。这个前提下,我们把SIGIO信号处理函数编写成在一个循环中执行读入操作,知道该操作返回EWOULDBLOCK时菜结束循环。

检查队列溢出
79-80   如果DG结构数组队列已满,进程就终止。

读入数据报
82-92   在非阻塞套接字上调用recvfrom。下标为iput的数组元素用于存放读入的数据报。如果没有可读的数据报,那就break出for循环。

递增计数器和下标
92-97   nread是一个计量每次信号递交读入数据报数目的诊断计数器。nqueue是有待主循环处理的数据报数目。
100     在信号处理函数返回之前,递增与每次信号递交读入数据报数目对应的计数器。当SIGHUP信号被递交时。