UNP-UNIX网络编程 第六章:I/O复用

一.I/O模型

我们看到上面的TCP客户同时处理两个输入:标准输入和TCP套接字。我们遇到的问题就是在客户阻塞于(标准输入上的)fgets调用期间,服务器进程会被杀死。服务器TCP虽然正确地给客户TCP发送一个FIN,但是既然客户进程阻塞于从标准输入读入的过程,它将看不到这个ROF,知道从套接字读时为止(可能已经过了很长时间)。这样的进程需要一种预先告知内核的能力,使得内核一旦发现进程指定的一个或多个I/O条件就绪(也就是说输入已准备好被读取,或者描述符已能承接更多的输出),它就通知进程。这个能力成为I/O复用,是由select和poll这两个函数支持的。
I/O复用典型使用在下列网络应用场合:
1)当客户处理多个描述符(通常是交互式输入和网络套接字)时,必须使用I/O复用(就像上述那样)
2)一个客户同时处理多个套接字是可能的,不过比较少见。在16.5节结合一个web客户的上下文给出这种场合使用select的例子
3)如果一个TCP服务器既要处理监听套接字,又要处理已连接套接字,一般就要使用I/O复用(服务器一般都是这个样子的)
4)如果一个服务器既要处理TCP,又要处理UDP,一般就要使用I/O复用。8.15节有这么一个例子(这个厉害了)
5)如果一个服务器要处理多个服务或者多个协议(在13.5节讲述的inetd守护进程),就要用I/O复用

I/O复用并非只限于网络编程,许多重要的应用程序也需要使用这项技术
在UNIX下可用的5种I/O模型:(1)阻塞式I/O;(2) 非阻塞式I/O;(3)I/O复用(select和poll);(4)信号驱动式I/O(SIGIO);(5)异步I/O(POSIX的aio_系列函数)
在上述所说的那样,一个输入操作通常包括两个不同的阶段:
1)等待数据准备好;
2)从内核向进程复制数据

对于一个套接字上的输入操作,第一步通常涉及等待数据从网络中到达。当所等待分组到达时,它被复制到内核总的某个缓冲区。
第二步就是把数据从内核缓冲区复制到应用进程缓冲区。

(1) 阻塞式I/O模型

进程调用recvfrom()系统函数,其系统调用直到数据报到达且从内核被复制到应用进程的缓冲区中或者发送错误才返回。
最常见的错误是系统调用被信号中断,比如第五章的accept()的EINTR错误(被中断的系统调用)
我们说进程在从调用recvfrom开始到它返回的整段时间内是被阻塞的。recvfrom成功返回后,应用进程开始处理数据报。

(2) 非阻塞式I/O模型:

当所有请求的I/O操作非得把本进程投入睡眠才能完成时,不要把本进程投入睡眠,而是返回一个错误。
前三次调用recvfrom时没有数据可返回,因此内核转而立即返回一个EWOULDBLOCK错误。第四次调用recvfrom时
已有一个数据报准备好,它被复制到应用进程缓冲区,于是recvfrom成功返回。接着处理数据。
当一个应用进程像这样对一个非阻塞描述符循环调用recvfrom时,我们成为轮询,应用进程持续轮询内核,
以查看某个操作是否就绪。这么做往往耗费大量CPU时间,不过这种模型偶尔也会遇到。

(3)I/O复用模型

有了I/O复用,我们就可以调用select或者poll,阻塞在这两个系统调用中的某一个,
而不是阻塞在真正的I/O系统调用上。使用select需要两个而不是单个系统调用,其优势在于可以等待多个描述符就绪。
如图

(4)信号驱动式I/O模型

前4种模型主要区别在第一阶段,第二阶段都是一样的(在数据从内核复制到调用者的缓冲区期间,进程阻塞于recvfrom调用)

(5)异步I/O模型

我们调用aio_read函数,给内核传递描述符、缓冲区指针。缓冲区大小和文件偏移,并告诉内核当整个操作完成时如何通知我们。
该系统调用立即返回,而且在等到I/O完成期间,我们的进程不被阻塞。
5中类型的比较p127!

二.select函数

select函数允许进程指示内核等待多个事件中的任一个发生,并仅在一个或者多个事件发生或经过某个指定的时间后才唤醒进程。
我们调用select告知内核对哪些描述符(就读、写或异常条件)感兴趣以及等待多长时间。
我们所关心的描述字不受限于套接口,任何描述字都可用select来监听。

#include <sys/select.h>  
#include <sys/time.h>  
int select(int maxfdp1, fd_set *readset, fd_set *writeset, fd_set *exceptset,const struct timeval * timeout);  
/****** 返回,准备好描述字的正数目,0----超时,-1------出错*******/ 

(1)最后一个参数timeout,它告诉内核等待一组指定的描述字中的任一个准备好可花多长时间,结构timeval指定了秒数和微妙数

struct timeval
{  
    long tv_sec; //秒秒   long tv_usec; //好眠
};

1.永远等待下去,仅在有一个描述字准备好I/O时才返回,需要将此指针设置为空指针
2.等到固定时间,在有一个秒数字准备好I/O时才返回,需要将此指针指向的结构timeval中指定的秒数和微秒数
3.根本不等待,同上,只不过秒数和微秒数都为0

(2)中间的三个参数readset、writeset和exceptset指定我们要让内核测试读写和异常条件所需的描述字。这三个参数均为值-结果参数。

    void FD_ZERO(fd_set * fdset);   // 将集合清空  
    void FD_SET(int fd,fd_set * fdset); // 添加描述字fd到集合中  
    void FD_CLR(int fd,fd_set * fdset);//在集合fdset清除描述字fd  
    int FD_ISSET(int fd,fd_set * fdset); //判断描述字fd 是否在集合fdset中  

描述符集的初始化非常重要,因为作为自动变量分片的一个描述符集如果没有初始化,那么可能发生不可预期的后果。
如果我们对某个条件不感兴趣,可以让其指针为空。假设三个函数都为空,我们就有了一个比sleep()函数更为精确的睡眠函数。

(3)参数maxfdp1指定被测试的描述字个数,它的值为待测试的最大描述字加1。述字集中任何与没有准备好的描述字相对应的位返回时清0

(4)下列四个条件中的任何一个满足时,套接字口准备好读:

1、套接字接收缓冲区中的数据字节数大于等于套接字结束缓冲区低潮限度的当前值。
我们可以用套接字选项SO_RCVLOWAT来设置此低潮限度,对于TCP和UDP套接字,其值缺省为1.
2、连接的读这一半关闭(也就是说接收了FIN的TCP连接)。对这样的套接口的读操作将不阻塞且返回0(即文件结束符)。
3、监听套接字且已完成的连接数为非0.
4、有一个套接字错误待处理。

(5)下面的三种情况满足其中一种即可认为可以写:

1、套接口发送缓冲区的可用空间字节数大于等于套接字发送缓冲区低潮限度的当前值,且或者套接口已连接,或者套接口不要求连接(例如UDP套接字)。这意味着,如果我们将这样的套接字设置为非阻塞,写操作将不阻塞且返回一个正值。对于TCP和UDP套接字,其缺省值一般为2048.
2、连接的写这一半关闭。
3、有一个套接字错误待处理
4、非阻塞的connect已连接或者connect连接失败。

三.客户端程序见第五章

有三个条件通过套接口处理:
1、如果对方TCP发送数据,套接口就变为可读且read返回大于0的值(即数据的字节数)
2、如果对方TCP发送一个FIN(对方进程终止),套接口就变成为刻度切read返回0(文件结束)
3、如果对方TCP发送一个RST(对方主机崩溃并重新启动),套接口就变为了可读且read返回-1

四.批量输入

废弃使用以文本行为中心的代码,改为针对缓冲区操作,消除复杂性问题。

五.TCP回射服务程序

select单进程tcpcliserv/tcpservselect01.c

六.poll函数

#include <poll.h>
int poll (struct pollfd *fdarray,unsigned long nfds,int timeout);
fdarray:一看就是指向某个结构的指针,这个结构如下:
struct pollfd
{
    int fd; //descriptor to check
    short events;//events of interest on fd
    short revents;//events that occurred on fd
};

对于events和revents而言,每个描述符都有两个变量,一个是调用值,一个是返回结果。从而避免使用一个值-结果变量参数。
所以events对应调用值,revents对应返回结果。因此系统也给定了一些常值处理输入、输出和异常。

int main(int argc, char ** argv) 
{
    //描述符0表示标准输入
    //描述符1表示标准输出
    int fd;
    char buf[1024];
    int i;
    struct pollfd pfds[2];
    fd = open(argv[1], O_RDONLY|O_NONBLOCK);
    if(fd < 0)
    {
        printf("open file error");
    }

    while (1) 
    {
        pfds[0].fd = 0;//stdin
        pfds[0].events = POLLIN;
        pfds[1].fd = fd;//FIFO fd
        pfds[1].events = POLLIN;
        poll(pfds, 2, 0);

        if (pfds[0].revents&POLLIN)
        {
            printf("stdin\r\n");
            i = read(0, buf, 1024);
            if (!i) 
            {
                printf("stdin closed\r\n");
                return 0;
            }

            write(1, buf, i);//output
        }


        if (pfds[1].revents&POLLIN ) 
        {
            printf("FIFO in\r\n");
            i = read(fd, buf, 1024);
            if (!i)
            {
                printf("file closed\r\n");
                return 0;
            }
            write(1, buf, i);//output
        }
    }
}

在终端输入:
mknod mypipe p //创建一个FIFO
make polltest //编译
./polltest mypipe //运行后进程阻塞在poll调用并监听两个描述符
//直接输入文本
//也可以在新的终端同一文件目录下输入
echo test >> mypipe //重定向

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章