IO複用之——epoll

一. 關於epoll

    對於IO複用模型,前面談論過了關於select和poll函數的使用,select提供給用戶一個關於存儲事件的數據結構fd_set來統一監測等待事件的就緒,分爲讀、寫和異常事件集;而poll則是用一個個的pollfd類型的結構體管理事件的文件描述符和事件所關心的events,並通過結構體裏面的輸出型參數revents來通知用戶事件的就緒狀態;

    但是對於上述兩種函數,都是需要用戶遍歷所有的事件集合來確定到底是哪一個或者是哪些事件已經就緒可以進行數據的處理了,因此當要處理等待的事件比較多時,就會有數據複製和系統遍歷的開銷導致效率並不高效;針對select和poll的缺點,另外一種相對高效的處理IO複用的函數就出現了,那就是epoll;



二. epoll相關函數的使用

    首先,和select及poll函數不同的是,epoll並沒有直接的一個用epoll來命名的函數使用,而是分別提供出來三個函數:epoll_createepoll_ctlepoll_wait


  1. epoll_create

wKioL1dKc0OjGQjvAAAH_Pjisxk527.png

epoll_create函數創建一個epoll的“實例”,請求內核分配一個指定大小的空間用於事件的後臺存儲,函數參數size只是一個關於內核如何維護內部結構的提示,不過現在這個size已經被忽略並不需要在意了;

函數成功會返回一個引用新創建的epoll實例的一個文件描述符,用於隨後調用其他的epoll函數的結構,如果不再需要的話,應當使用close函數關閉,這時內核會銷燬該epoll實例並釋放相關資源;如果函數失敗會返回-1並置相應的錯誤碼


2. epoll_ctl

wKioL1dKdxDxZc4NAAAIeYVCTEw897.png

函數參數中,

epfd是用epoll_create創建出來的epoll文件描述符,用來操縱epoll實例;

op是要對創建出的epoll實例進行操作,而op的操作選項有如下三種宏:

wKioL1dKdxDg7taeAAAcw24qBpg026.png

EPOLL_CTL_ADD用於在epfd標識的epoll實例中添加登記要處理的事件;

EPOLL_CTL_MOD用於更改特定的文件描述符所關心的事件;

EPOLL_CTL_DEL用於刪除在epoll實例中登記的事件,標識並不需要再關心了;


fd是指要進行數據IO的事件的文件描述符,也就是用戶需要進行操作的事件的文件描述符;

event是一個epoll_event的結構體,用於存放需要對fd進行操作的相關信息:

wKiom1dKdheg3AfeAAAG1FYwPjc281.png

結構體中,

events表示文件描述符fd所對應的事件所關心的操作,是相應的比特位的設置,有如下幾種宏:

wKiom1dKdhjRqFC_AABc0X0ReFY488.png

如上的宏中,最主要使用的有如下幾種:

EPOLLIN表示fd可以進行數據的讀取;

EPOLLOUT表示fd可以進行數據的寫入;

EPOLLPRI表示當前有緊急數據可供讀取;

EPOLLERR表示當前事件發生錯誤;

EPOLLHUP表示當前事件被掛斷;

EPOLLET將相關的文件描述符設置爲邊緣觸發,因爲默認是水平觸發的;對於LT和ET模式下面會討論;


對於結構體中的data則是一個聯合,用於表示有關文件描述符操作的數據信息:

wKioL1dKdxKTCVYrAAAHRLOQKFY569.png

ptr是指向數據緩衝區的一個指針;

fd是相應操作的文件描述符;


epoll_ctl函數成功返回0,失敗返回-1並置相應的錯誤碼;


3. epoll_wait

如果說上面的epoll_create和epoll_ctl是爲了進行相關事件的操作而進行的準備工作,那麼真正和select及poll函數一樣用來進行多個事件的等待就緒則就是epoll_wait函數了:

wKioL1dKjOqSfqHJAAAM_vX-ny4647.png

函數參數中,

epfd是用epoll_create創建出的epoll實例的文件描述符;

events是上述的一個結構體的指針,這裏一般是一個數組的首地址,是一個輸入輸出型參數,當作爲輸入時,是用戶提供給系統一個用來存放就緒事件的地址空間,而作爲輸出型參數時,系統會將就緒的事件放入其中供用戶提取,因此不可以爲NULL

maxevents是events的大小;

timeout則是設置等待的超時時間,單位爲毫秒


這裏值得一提的是,既然epoll是select和poll的改進,那麼其最主要的高效就是體現在epoll_wait的返回值:

  • 函數失敗返回-1並置相應的錯誤碼;

  • 函數返回0表示超時,預定時間內並沒有事件就緒;

  • 當函數返回值大於0時,是告訴用戶當前事件集中已經就緒的IO事件的個數,並且將其按序從頭開始排列在了用戶提供的空間events內,因此,不需要像select和poll那樣遍歷整個事件集找出就緒的事件,只需要在相應的數組中從頭訪問固定的返回值的個數就拿到了所有就緒的事件了;



三. 栗子時間

    同樣的,使用epoll相關的接口函數,可以自主來編寫一個基於TCP協議的服務端,其基本步驟如下:

  1. 首先,先要創建出一個監聽socket,綁定好本地網絡地址信息並將其處於監聽狀態,但是這裏,爲了使其更爲高效,還需要調用setsockopt函數來將其屬性設定爲SO_REUSEADDR,使其地址信息可被重用;

  2. 調用epoll_create創建出一個關於epoll實例的文件描述符,用於以後操作epoll相關函數;

  3. 調用epoll_ctl函數,將監聽socket登記添加到epoll實例中;

  4. 定義一個epoll_event結構體數組,用戶指定大小,供系統存放就緒的IO事件;

  5. 調用epoll_wait進行事件的就緒等待,並接收其返回值;

  6. 當epoll_wait返回時,對返回的事件一一進行判斷處理,如果是監聽事件就緒,表明有連接請求需要處理,並將新的套接字添加進epoll實例中;如果是其他socket就緒,表明數據就緒可以進行讀取和寫入了;

  7. 當連接的一端關閉或者epoll實例使用完畢的時候,需要調用close函數關閉相應的文件描述符回收資源;


server客戶端程序設計如下:

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <sys/epoll.h>
#include <fcntl.h>
#include <assert.h>
#include <errno.h>

#define _BACKLOG_ 5  //網絡中連接請求等待隊列最大值
#define _MAX_NUM_ 20 //事件就緒隊列存儲空間
#define _DATA_SIZE_ 1024 //數據緩衝區大小

//因爲epoll_event結構體中的data成員是一個聯合體,因此當需要同時使用聯合中的fd和ptr的時候就會有問題
//因此可以將其各自單獨拿出存儲
typedef struct data_buf
{
    int _fd;
    char _buf[_DATA_SIZE_];
}data_buf_t, *data_buf_p;

//命令行參數的格式判斷
void Usage(const char *argv)
{
    assert(argv);
    printf("Usage: %s  [ip]  [port]\n", argv);
    exit(0);
}

//創建監聽套接字
static int CreateListenSock(int ip, int port)
{
    int sock = socket(AF_INET, SOCK_STREAM, 0);//創建新socket
    if(sock < 0)
    {
        perror("socket");
        exit(1);
    }

    int opt = 1;//調用setsockopt函數使當server首先斷開連接的時候避免進入一個TIME_WAIT的等待時間
    if(setsockopt(sock, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt)) < 0)
    {
        perror("setsockopt");
        exit(2);
    }

        //設置本地網絡地址信息
    struct sockaddr_in server;
    server.sin_family = AF_INET;
    server.sin_port = htons(port);
    server.sin_addr.s_addr = ip;

    //綁定套接字和本地網絡信息
    if(bind(sock, (struct sockaddr*)&server, sizeof(server)) < 0)
    {
        perror("bind");
        exit(3);
    }

    //設定套接字爲監聽狀態
    if(listen(sock, _BACKLOG_) < 0)
    {
        perror("listen");
        exit(4);
    }

    return sock;
}

//執行epoll
void epoll_server(int listen_sock)
{
        //創建出一個epoll實例,獲取其文件描述符,大小隨意指定
    int epoll_fd = epoll_create(256);
    if(epoll_fd < 0)
    {
        perror("epoll_create");
        exit(5);
    }

    //定義一個epoll_event結構體用於向epoll實例中註冊需要IO的事件信息
    struct epoll_event ep_ev;
    ep_ev.events = EPOLLIN;
    ep_ev.data.fd = listen_sock;
    if(epoll_ctl(epoll_fd, EPOLL_CTL_ADD, listen_sock, &ep_ev) < 0)
    {
        perror("epoll_ctl");
        exit(6);
    }

    //申請一個確定的空間提供給系統,用於存放就緒事件隊列
    struct epoll_event evs[_MAX_NUM_];
    int maxnum = _MAX_NUM_;//提供的空間大小
    int timeout = 10000;//設定超時時間,如果爲-1,則以阻塞方式一直等待
    int ret = 0;//epoll_wait的返回值,獲取就緒事件的個數

    while(1)
    {
        switch((ret = epoll_wait(epoll_fd, evs, maxnum, timeout)))
        {
            case -1://出錯
                perror("epoll_wait");
                break;
            case 0://超時
                printf("timeout...\n");
                break;
            default://至少有一個事件就緒
                {
                    int i = 0;
                    for(; i < ret; ++i)
                    {
                            //判斷是否爲監聽套接字,如果是,獲取連接請求
                        if((evs[i].data.fd == listen_sock) && (evs[i].events & EPOLLIN))
                        {
                            struct sockaddr_in client;
                            socklen_t client_len = sizeof(client);

                            //處理連接請求,獲取新的通信套接字
                            int accept_sock = accept(listen_sock, (struct sockaddr*)&client, &client_len);
                            if(accept_sock < 0)
                            {
                                perror("accept");
                                continue;
                            }
                            printf("connect with a client...[fd]:%d   [ip]:%s  [port]:%d\n", accept_sock, inet_ntoa(client.sin_addr), ntohs(client.sin_port));

                            //將新的事件添加進epoll實例中
                            ep_ev.events = EPOLLIN;
                            ep_ev.data.fd = accept_sock;
                            if(epoll_ctl(epoll_fd, EPOLL_CTL_ADD, accept_sock, &ep_ev) < 0)
                            {
                                perror("epoll_ctl");
                                close(accept_sock);
                            }
                        }
                        else//除了監聽套接字之外的IO套接字
                        {
                                //如果爲讀事件就緒
                            if(evs[i].events & EPOLLIN)
                            {
                                    //申請空間用於同時存儲文件描述符和緩衝區地址
                                data_buf_p _data = (data_buf_p)malloc(sizeof(data_buf_t));
                                if(!_data)
                                {
                                    perror("malloc");
                                    continue;
                                }
                                _data->_fd = evs[i].data.fd;
                                printf("read from fd: %d\n", _data->_fd);
                                //從緩衝區中讀取數據
                                ssize_t size = read(_data->_fd, _data->_buf, sizeof(_data->_buf)-1);
                                if(size < 0)//讀取出錯
                                    printf("read error...\n");
                                else if(size == 0)//遠端關閉連接
                                {
                                    printf("client closed...\n");
                                    //收尾工作,將事件從epoll實例中移除,關閉文件描述符和防止內存泄露
                                    epoll_ctl(epoll_fd, EPOLL_CTL_DEL, _data->_fd, NULL);
                                    close(_data->_fd);
                                    free(_data);
                                }
                                else
                                {
                                        //讀取成功,輸出數據
                                    (_data->_buf)[size] = '\0';
                                    printf("client# %s", _data->_buf);
                                    fflush(stdout);
                                    //將事件改爲關心寫事件,進行回寫
                                    ep_ev.data.ptr = _data;
                                    ep_ev.events = EPOLLOUT;
                                    //在epoll實例中更改同一個事件
                                    epoll_ctl(epoll_fd, EPOLL_CTL_MOD, _data->_fd, &ep_ev);
                                }
                            }
                            else if(evs[i].events & EPOLLOUT)//判斷爲寫事件就緒
                            {
                                data_buf_p _data = (data_buf_p)evs[i].data.ptr;

                                //向緩衝區中回寫數據
                                write(_data->_fd, _data->_buf, strlen(_data->_buf));
                                //寫完之後就進行完畢一次通信,進行收尾
                                epoll_ctl(epoll_fd, EPOLL_CTL_DEL, _data->_fd, NULL);
                                close(_data->_fd);
                                free(_data);
                            }
                            else
                            {}
                        }
                    }
                }
                break;
        }
    }
}


int main(int argc, char *argv[])
{
    if(argc != 3)//判斷命令行參數的正確性
        Usage(argv[0]);

    //獲取端口號和IP地址
    int port = atoi(argv[2]);
    int ip = inet_addr(argv[1]);

    //獲取監聽套接字
    int listen_sock = CreateListenSock(ip, port);

    //進行epoll操作
    epoll_server(listen_sock);
    close(listen_sock);//關閉文件描述符

    return 0;
}

這裏要說明一下,系統內部其實是爲epoll相關的操作維護了一棵平衡搜索二叉樹和一張鏈表,如果用戶一次性提供出來的空間不夠存放所有就緒的事件,那麼下一次系統會將剩下的再提供出來,因此不必要擔心提供給epoll_wait的結構體數組空間的問題;


運行程序:

wKioL1dNH_2QycQ2AACMyqOhU90188.png左邊爲server端,右邊爲使用telnet請求連接端

因爲設計的是一問一答的模式,因此在server端收到連接請求和數據之後,將數據讀取出再回寫回連接請求端,就認爲完成了一次通信;


如上的模式,還可以用瀏覽器來進行測試,只是當瀏覽器進行連接請求之後,server端就認爲收到了數據,轉而需要進行回寫,而回寫的內容則有所要求,因爲大部分瀏覽器所使用的是HTTP協議,因此在瀏覽器接收的時候,應該收到的是server端寫回的作爲響應的消息,而這裏,HTTP的響應由三部分組成,狀態行、消息報頭和響應正文,而作爲狀態行的格式爲“協議版本+響應狀態碼+表示狀態碼的文本”,過多的內容並不屬於本篇文章的討論範圍,因此不贅述,總之,作爲響應消息,server端寫回的內容應該是如下格式:

char *msg = "HTTP/1.1 200 OK\r\n\r\nHello, what can i do for you ? :)\r\n";
write(_data->_fd, msg, strlen(msg));


運行server端程序,打開瀏覽器輸入IP和端口號:

wKioL1dNRbTiMyr7AAAuAc8MzOE684.png

當瀏覽器連接上server時,server端會接收到關於瀏覽器方面的信息,也就是獲取了瀏覽器的請求信息,而之後會將響應消息返回給瀏覽器,而瀏覽器會根據接收到的響應消息得到正文內容並顯示出來,如右邊的顯示(使用本地環回IP進行的測試即127.0.0.1);



四. 水平觸發和邊緣觸發

    當epoll_wait在進行多個事件的等待時,如果有數據發送到緩衝區中時,則表示當前事件處於就緒狀態,則需要返回來通知用戶“有數據來了,可以進行處理了”,那麼對於系統通知用戶的方式,就分爲水平式觸發和邊緣式觸發:

    水平觸發(Level Trigger)簡稱LT,其特點是當數據到來的時候會通知用戶,如果用戶一次數據處理並沒有將緩衝區中的數據全部取走還留有一部分,那麼下一次再進行相同事件的epoll_wait的時候系統會認爲事件仍然是就緒的,還會繼續通知用戶來取走剩下的數據,因此,水平觸發的特點是:只要數據緩衝區中有數據,當前的IO事件始終都是就緒的,epoll_wait始終會返回有效值通知用戶程序

    邊緣觸發(Edge Triggered)簡稱ET,當有數據到來的時候仍然會返回通知用戶程序,但是和水平觸發不同的是,如果用戶在通知一次後對數據的IO處理並不完全,也就是一次處理之後緩衝區中還留有數據,那麼再次返回進行epoll_wait的時候就不會再表明當前事件是就緒的了,只有當這個事件再次有數據到達時纔會再一次通知用戶程序來處理數據,因此,邊緣觸發的特點是:只有當數據到來的時候系統纔會通知用戶程序且只會通知一次,如果還有數據沒有處理完,只有等到再次有數據到來的時候纔會再次滿足事件就緒,epoll_wait返回通知用戶程序處理數據


    這裏需要注意的是:對於邊緣式觸發,因爲只有當數據到來時系統纔會通知用戶程序一次,如果當前的IO接口工作於阻塞模式,那麼當一個事件被阻塞的時候,其他事件的就緒也就只會被通知一次但並得不到處理,因此會導致多數據的堆積,所以,當使用邊緣式觸發的時候:

  • 最好將當前的IO接口設定爲非阻塞的;

  • 當一個IO事件進行數據的讀取和寫入的時候,最好一次性就將緩衝區中的數據全部都處理完;因此,對於數據的讀取,可以用一個循環來每次讀取特定的長度,當最後一次讀取的長度小於特定的長度時,就可以認爲當前緩衝區的數據已經全部讀取完畢終止循環;但是,不可避免的是,如果最後一次的讀取恰好也就是特定的長度,那麼在此進行讀取緩衝區中數據爲0,就會返回一個EAGAIN的錯誤碼,這個就可以作爲循環的終止條件;


EAGAIN的錯誤碼爲11,可在/usr/include/asm-generic/errno.h及errno-base.h中查到:

wKioL1dNazvAtma4AADt1MStCXA722.png

若輸出其對應錯誤描述,爲:Resource temporarily unavailable,意思是資源暫時不可用,可以try again;


將IO接口設置爲非阻塞的,可以調用fcntl函數:

wKiom1dKpX3SVFxMAAAc6v6J9Rk864.jpg

函數參數中,

fd表示要進行操作的文件描述符;

cmd表示要進行的操作;

至於後面的參數,則有cmd來決定;

wKiom1dKptPCbyb3AAAZGe0xUjI512.png


在這裏要設置文件接口爲非阻塞的,首先要將cmd設置爲F_GETFL,表示獲取當前文件描述符的標誌,因爲重新設定時需要用到;之後需要再次調用fcntl函數,將cmd設定爲F_SETFL,要重新設置文件描述符的標誌,其中有一個選項就是O_NONBLOCK

對於fcntl函數的返回值,根據操作的不同而不同:

wKioL1dKvoSj8qy0AAAfExlymeI066.png

    對比水平觸發和邊緣觸發,可以發現水平觸發對於數據的處理來說是更安全更可靠的,而邊緣觸發是要更爲高效的,因此,選擇哪種通知方式,可以依情況而定;


因爲上面的程序中,默認epoll_wait的通知方式是LT也就是水平觸發的,要將其改爲高效一些的ET邊緣觸發模式,則需要滿足如上所述的非阻塞條件和數據一次性讀取完畢條件:

  • 首先將事件的IO接口設置爲非阻塞模式,則在listen socket創建中以及每一次有新的連接請求獲得新的IO文件描述符之後,都需要調用如下的函數:

int set_non_block(int fd) 
{
    //獲取當前文件描述符的文件標識
    int old_fl = fcntl(fd, F_GETFL);
    if(old_fl < 0)
    {   
        perror("fcntl");
        return -1; 
    }
    //將文件描述符所對應的事件設置爲非阻塞模式
    if(fcntl(fd, F_SETFL, old_fl|O_NONBLOCK))
    {   
        perror("fcntl");
        return -1; 
    }   
    return 0;
}
  • 其次,就需要自行封裝出一個函數來進行循環地獲取或者寫入緩衝區中數據,直到沒有數據可讀爲止,這是爲了避免邊緣觸發的特點帶來的數據擁堵不能夠被處理的現象:

//讀取數據
ssize_t MyRead(int fd, char *buf, size_t size)
{
    assert(buf);

    int index = 0;
    ssize_t ret = 0;
    //如果讀取到的數據等於0,則說明遠端關閉連接,直接返回0
    //而如果爲非0,不管是大於零還是出錯小於零都需要進入循環
    while((ret = read(fd, buf+index, size-index)))
    {
        if(errno == EAGAIN)//如果錯誤碼爲EAGAIN,則說明讀取完畢,打印出錯誤碼和錯誤消息並退出
        {
            printf("read errno: %d\n", errno);
            perror("read");
            break;
        }
        index += ret;
    }
    return (ssize_t)index;//返回獲得的總數據量
}

//寫入數據
ssize_t MyWrite(int fd, char* buf, size_t size)
{
    assert(buf);

    int index = 0;
    ssize_t ret = -1;
    //和讀取數據一樣,當寫入數據量爲0的時候直接返回0
    //否則,返回值爲非零進入循環
    while((ret = write(fd, buf+index, size-index)))
    {
        if(errno == EAGAIN)//當數據全部寫完的時候返回錯誤碼爲EAGAIN
        {
            printf("write errno: %d\n", errno);
            perror("write");
            break;
        }
        index += ret;
    }
    return (ssize_t)index;//和讀取數據相同,返回寫入的總數據量
}


將上面修改的代碼添加到上述例子中之後,運行程序:

wKioL1dNZhrx7B3tAADCYpc4tE0860.png

分析一下程序結果,會發現第一次連接並沒有什麼問題,得到了一問一答的結果,但是如果第二次連接包括以後的多次連接,所發送的數據就無法被server端接收到,反而被認爲連接端已經關閉了,因此server端就主動關閉了連接和相關事件的清除;這是怎麼一回事呢?


這是因爲,在上面所封裝的數據的讀寫函數中,當第一次連接進行數據的讀取,讀取完畢緩衝區中所有的數據之後,再次進行read就會出錯,因而錯誤碼被置爲了EAGAIN,而錯誤碼errno是個全局變量,所以當再次或者多次連接進行數據的讀取的時候,即使讀到了數據read的返回值大於零,但進入循環進行

if(errno == EAGAIN)

判斷的時候,errno已經被第一次連接置爲了EAGAIN,而運行是在同一個進程當中的,所以始終滿足上述條件跳出循環,返回值爲0,之後再進行判斷,就會認爲並沒有讀到數據,轉而關閉相應的文件描述符;

這就是在一個函數中使用了全局變量造成了函數的不可重入性;


要解決上述問題,

  1. 可以在上述的判斷條件增加一個條件,即:

if((ret < 0) && (errno == EAGAIN))
{   
     printf("read errno: %d\n", errno);
     perror("read");
     break;
}

當read出錯進入循環的時候,要和read成功分開進行操作,這樣就不會有誤了,雖然無法避免使用全局變量errno,但是可以通過read的返回值來進一步加強判斷;


2. 另外有一種方法,就是可以用多進程來操作,即將errno變成某一個進程專屬的全局變量,也就是當一個IO的讀事件就緒的時候,就創建出一個子進程來進行緩衝區中數據的讀寫,將進行epoll_wait之後的讀事件就緒以後的代碼改爲如下:

else
{
    if(evs[i].events & EPOLLIN)//讀事件就緒
    {
        data_buf_p _data = (data_buf_p)malloc(sizeof(data_buf_t));
        if(!_data)
        {
            perror("malloc");
            continue;
        }
        _data->_fd = evs[i].data.fd;
        printf("read from fd: %d\n", _data->_fd);

        //創建進程
        pid_t id = fork();
        if(id < 0)//創建失敗
            perror("fork");
        else if(id == 0)//子進程
        {
            printf("child proc: %d\n", getpid());
            ssize_t size = MyRead(_data->_fd, _data->_buf, sizeof(_data->_buf)-1);
            //ssize_t size = read(_data->_fd, _data->_buf, sizeof(_data->_buf)-1);
            if(size < 0)
                printf("read error...\n");
            else if(size == 0)
            {
                printf("client closed...\n");
                exit(12);
                //epoll_ctl(epoll_fd, EPOLL_CTL_DEL, _data->_fd, NULL);
                //close(_data->_fd);
                //free(_data);
            }
            else
            {
                (_data->_buf)[size] = '\0';
                printf("client# %s", _data->_buf);
                fflush(stdout);
                ep_ev.data.ptr = _data;
                ep_ev.events = EPOLLOUT | EPOLLET;
                epoll_ctl(epoll_fd, EPOLL_CTL_MOD, _data->_fd, &ep_ev);
            }
        }
        else
        {
            pid_t ret = wait(NULL);
            if(ret < 0)
                perror("waitpid");
            else
                printf("wait success : %d\n", ret);
            epoll_ctl(epoll_fd, EPOLL_CTL_DEL, _data->_fd, NULL);
            close(_data->_fd);
            free(_data);
        }
    }
    else if(evs[i].events & EPOLLOUT)
    {
        data_buf_p _data = (data_buf_p)evs[i].data.ptr;
        MyWrite(_data->_fd, _data->_buf, strlen(_data->_buf));
        //epoll_ctl(epoll_fd, EPOLL_CTL_DEL, _data->_fd, NULL);
        //close(_data->_fd);
        //free(_data);
        exit(11);
    }

這裏要解釋:當創建一個子進程的時候,子進程複製父進程的PCB,自然也就會獲取其相應的文件描述符進行操作,但是當需要改變其內容的時候,比如文件描述符和epoll實例,子進程就會進行寫時拷貝,這個時候已經不能單單進行子進程中關閉文件描述符和釋放空間的操作了,因爲這並沒有起到實際效果,只不過是清除了拷貝出來的內容而已,這就是爲什麼上面的程序中註釋掉了子進程中的收尾工作,轉而在父進程中進行;而與此同時,父進程是需要進行等待的,如果不進行等待就會導致同一個IO事件的亂序而無法達到預期的效果


運行程序:

wKiom1dNeAOxq4yKAAAponXUbyo285.png


其實,對於函數的可重入性,不免就會想到線程的安全問題,那麼上面的程序如果給改成多線程的話是能不能行呢?

對於線程而言,是共享進程的資源的,而errno是一個全局變量,在整個進程空間內都有效,因此,對於多線程也是同樣共享這一個全局變量的,雖然全局變量是臨界資源,但上述的問題並不是因爲爭奪臨界資源而造成的,因爲使用了for循環來一個一個地處理IO事件,而是前一個操作對全局變量的改變影響了後來的操作,這是典型的函數的可重入性,函數的可重入性並不等同於線程安全,它需要函數內部使用的變量全部來自於自身的棧空間,因此,如果用多線程或者線程互斥來進行操作是沒有什麼變化的。



《完》

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