信号驱动式I/O

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

  异步I/O是进程执行I/O系统调用(读或写)告知内核启动某个I/O操作,内核启动I/O操作后立刻返回到进程,进程在I/O操作发生期间继续执行,当操作完成或遭遇错误时,内核以进程在I/O系统调用中指定的某种方式通知进程,

对一个套接字使用信号驱动式I/O

  1. 建立SIGIO信号的信号处理函数。
  2. 设置该套接字的属主,通常使用fcntl的F_SETOWN命令设置。(因该在设置套接字属主之前建立信号处理函数,因为在调用fcntl后调用signal之前有较小的机会产生SIGIO信号,此时信号被丢弃)
  3. 开启该套接字的信号驱动式I/O,通常通过使用fcntl的F_SETFL命令打开O_ASYNC标志完成。

UDP套接字的SIGIO信号

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

  1. 数据报到达套接字
  2. 套接字上发生异步错误

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

TCP套接字的SIGIO信号

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

  1. 监听套接字上某个连接请求已经完成
  2. 某个断连请求已经发起
  3. 某个断连请求已经完成
  4. 某个连接之半已经关闭
  5. 数据到达套接字
  6. 数据已经从套接字发送走
  7. 发生某个异步错误

  如果一个进程既读又写一个tcp套接字(此时应该设置成非阻塞套接字,防止read或write阻塞),那么当有新数据到达时或数据写出前SIGIO信号均会产生,而且信号处理函数无法区分这两种情况。

  应该只对监听TCP套接字使用SIGIO,因为对于监听套接字产生SIGIO的唯一条件是某个新的连接已完成。

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

  当一个新数据报到达时,SIGIO处理函数读入该数据报,同时记录它到达的时刻,然后将它置于进程内核的另一个队列中,以便服务器循环移走并处理(下图)

client:

#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);
}

server:

#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]);
}

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

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

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

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

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

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

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

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

  我们也可以去掉for循环内的两个sigprocmask调用,整个循环期间SIGIO一直阻塞,从而降低了信号处理函数的及时性,数据报不应该因此变动而丢失(假设套接字缓冲区足够大),但是SIGIO信号向进程的递交将在整个阻塞期间一直被拖延。

  然而当一个数据报到达导致SIGIO被递交,它的信号处理函数读入该数据报并把它放到供主循环读取的队列中,然而在信号处理函数执行期间,另有两个数据包到达,这一点意味着如果我们在信号处理函数执行(期间确保该信号被阻塞),期间该信号又发生了2次,那么它实际只被递交1次。让我们考虑下述情形。一个数据报到达导致SIGIO被递交。它的信号处理函数读入该数据报并把它放到供主循环读取的队列中。然而在信号处理函数执行期间,另有两个数据报到达,导致SIGIO再产生两次。由于SIGIO被阻塞,当他的信号处理函数返回时,该处理函数仅仅再被调用一次。该信号处理函数的第二次执行读入第二个数据报,第三个数据报则仍然留在套接字接收队列中。第三个数据报被读入的前提条件时由第四个数据报到达。当第四个数据报到达时,被读入并放到供主循环读取的队列中的是第三个而不是第四个数据报。

  既然信号时不排队的,开启信号驱动式I/O的描述符通常也被设置为非阻塞式。这个前提下,我们把SIGIO信号处理函数编写成在一个循环中执行读入操作,知道该操作返回EWOULDBLOCK时菜结束循环。

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

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

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

猜你喜欢

转载自www.cnblogs.com/tianzeng/p/11968151.html
I/O