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 //重定向

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