溫馨提示×

溫馨提示×

您好,登錄后才能下訂單哦!

密碼登錄×
登錄注冊×
其他方式登錄
點(diǎn)擊 登錄注冊 即表示同意《億速云用戶服務(wù)條款》

IO復(fù)用之——epoll

發(fā)布時間:2020-06-22 16:12:45 來源:網(wǎng)絡(luò) 閱讀:5345 作者:給我個bit位 欄目:網(wǎng)絡(luò)安全

一. 關(guān)于epoll

    對于IO復(fù)用模型,前面談?wù)撨^了關(guān)于select和poll函數(shù)的使用,select提供給用戶一個關(guān)于存儲事件的數(shù)據(jù)結(jié)構(gòu)fd_set來統(tǒng)一監(jiān)測等待事件的就緒,分為讀、寫和異常事件集;而poll則是用一個個的pollfd類型的結(jié)構(gòu)體管理事件的文件描述符和事件所關(guān)心的events,并通過結(jié)構(gòu)體里面的輸出型參數(shù)revents來通知用戶事件的就緒狀態(tài);

    但是對于上述兩種函數(shù),都是需要用戶遍歷所有的事件集合來確定到底是哪一個或者是哪些事件已經(jīng)就緒可以進(jìn)行數(shù)據(jù)的處理了,因此當(dāng)要處理等待的事件比較多時,就會有數(shù)據(jù)復(fù)制和系統(tǒng)遍歷的開銷導(dǎo)致效率并不高效;針對select和poll的缺點(diǎn),另外一種相對高效的處理IO復(fù)用的函數(shù)就出現(xiàn)了,那就是epoll;



二. epoll相關(guān)函數(shù)的使用

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


  1. epoll_create

IO復(fù)用之——epoll

epoll_create函數(shù)創(chuàng)建一個epoll的“實(shí)例”,請求內(nèi)核分配一個指定大小的空間用于事件的后臺存儲,函數(shù)參數(shù)size只是一個關(guān)于內(nèi)核如何維護(hù)內(nèi)部結(jié)構(gòu)的提示,不過現(xiàn)在這個size已經(jīng)被忽略并不需要在意了;

函數(shù)成功會返回一個引用新創(chuàng)建的epoll實(shí)例的一個文件描述符,用于隨后調(diào)用其他的epoll函數(shù)的結(jié)構(gòu),如果不再需要的話,應(yīng)當(dāng)使用close函數(shù)關(guān)閉,這時內(nèi)核會銷毀該epoll實(shí)例并釋放相關(guān)資源;如果函數(shù)失敗會返回-1并置相應(yīng)的錯誤碼;


2. epoll_ctl

IO復(fù)用之——epoll

函數(shù)參數(shù)中,

epfd是用epoll_create創(chuàng)建出來的epoll文件描述符,用來操縱epoll實(shí)例;

op是要對創(chuàng)建出的epoll實(shí)例進(jìn)行操作,而op的操作選項(xiàng)有如下三種宏:

IO復(fù)用之——epoll

EPOLL_CTL_ADD用于在epfd標(biāo)識的epoll實(shí)例中添加登記要處理的事件;

EPOLL_CTL_MOD用于更改特定的文件描述符所關(guān)心的事件;

EPOLL_CTL_DEL用于刪除在epoll實(shí)例中登記的事件,標(biāo)識并不需要再關(guān)心了;


fd是指要進(jìn)行數(shù)據(jù)IO的事件的文件描述符,也就是用戶需要進(jìn)行操作的事件的文件描述符;

event是一個epoll_event的結(jié)構(gòu)體,用于存放需要對fd進(jìn)行操作的相關(guān)信息:

IO復(fù)用之——epoll

結(jié)構(gòu)體中,

events表示文件描述符fd所對應(yīng)的事件所關(guān)心的操作,是相應(yīng)的比特位的設(shè)置,有如下幾種宏:

IO復(fù)用之——epoll

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

EPOLLIN表示fd可以進(jìn)行數(shù)據(jù)的讀取;

EPOLLOUT表示fd可以進(jìn)行數(shù)據(jù)的寫入;

EPOLLPRI表示當(dāng)前有緊急數(shù)據(jù)可供讀??;

EPOLLERR表示當(dāng)前事件發(fā)生錯誤;

EPOLLHUP表示當(dāng)前事件被掛斷;

EPOLLET將相關(guān)的文件描述符設(shè)置為邊緣觸發(fā),因?yàn)槟J(rèn)是水平觸發(fā)的;對于LT和ET模式下面會討論;


對于結(jié)構(gòu)體中的data則是一個聯(lián)合,用于表示有關(guān)文件描述符操作的數(shù)據(jù)信息:

IO復(fù)用之——epoll

ptr是指向數(shù)據(jù)緩沖區(qū)的一個指針;

fd是相應(yīng)操作的文件描述符;


epoll_ctl函數(shù)成功返回0,失敗返回-1并置相應(yīng)的錯誤碼;


3. epoll_wait

如果說上面的epoll_create和epoll_ctl是為了進(jìn)行相關(guān)事件的操作而進(jìn)行的準(zhǔn)備工作,那么真正和select及poll函數(shù)一樣用來進(jìn)行多個事件的等待就緒則就是epoll_wait函數(shù)了:

IO復(fù)用之——epoll

函數(shù)參數(shù)中,

epfd是用epoll_create創(chuàng)建出的epoll實(shí)例的文件描述符;

events是上述的一個結(jié)構(gòu)體的指針,這里一般是一個數(shù)組的首地址,是一個輸入輸出型參數(shù),當(dāng)作為輸入時,是用戶提供給系統(tǒng)一個用來存放就緒事件的地址空間,而作為輸出型參數(shù)時,系統(tǒng)會將就緒的事件放入其中供用戶提取,因此不可以為NULL

maxevents是events的大?。?/p>

timeout則是設(shè)置等待的超時時間,單位為毫秒;


這里值得一提的是,既然epoll是select和poll的改進(jìn),那么其最主要的高效就是體現(xiàn)在epoll_wait的返回值:

  • 函數(shù)失敗返回-1并置相應(yīng)的錯誤碼;

  • 函數(shù)返回0表示超時,預(yù)定時間內(nèi)并沒有事件就緒;

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



三. 栗子時間

    同樣的,使用epoll相關(guān)的接口函數(shù),可以自主來編寫一個基于TCP協(xié)議的服務(wù)端,其基本步驟如下:

  1. 首先,先要創(chuàng)建出一個監(jiān)聽socket,綁定好本地網(wǎng)絡(luò)地址信息并將其處于監(jiān)聽狀態(tài),但是這里,為了使其更為高效,還需要調(diào)用setsockopt函數(shù)來將其屬性設(shè)定為SO_REUSEADDR,使其地址信息可被重用;

  2. 調(diào)用epoll_create創(chuàng)建出一個關(guān)于epoll實(shí)例的文件描述符,用于以后操作epoll相關(guān)函數(shù);

  3. 調(diào)用epoll_ctl函數(shù),將監(jiān)聽socket登記添加到epoll實(shí)例中;

  4. 定義一個epoll_event結(jié)構(gòu)體數(shù)組,用戶指定大小,供系統(tǒng)存放就緒的IO事件;

  5. 調(diào)用epoll_wait進(jìn)行事件的就緒等待,并接收其返回值;

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

  7. 當(dāng)連接的一端關(guān)閉或者epoll實(shí)例使用完畢的時候,需要調(diào)用close函數(shù)關(guān)閉相應(yīng)的文件描述符回收資源;


server客戶端程序設(shè)計(jì)如下:

#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  //網(wǎng)絡(luò)中連接請求等待隊(duì)列最大值
#define _MAX_NUM_ 20 //事件就緒隊(duì)列存儲空間
#define _DATA_SIZE_ 1024 //數(shù)據(jù)緩沖區(qū)大小

//因?yàn)閑poll_event結(jié)構(gòu)體中的data成員是一個聯(lián)合體,因此當(dāng)需要同時使用聯(lián)合中的fd和ptr的時候就會有問題
//因此可以將其各自單獨(dú)拿出存儲
typedef struct data_buf
{
    int _fd;
    char _buf[_DATA_SIZE_];
}data_buf_t, *data_buf_p;

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

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

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

        //設(shè)置本地網(wǎng)絡(luò)地址信息
    struct sockaddr_in server;
    server.sin_family = AF_INET;
    server.sin_port = htons(port);
    server.sin_addr.s_addr = ip;

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

    //設(shè)定套接字為監(jiān)聽狀態(tài)
    if(listen(sock, _BACKLOG_) < 0)
    {
        perror("listen");
        exit(4);
    }

    return sock;
}

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

    //定義一個epoll_event結(jié)構(gòu)體用于向epoll實(shí)例中注冊需要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);
    }

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

    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)
                    {
                            //判斷是否為監(jiān)聽套接字,如果是,獲取連接請求
                        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));

                            //將新的事件添加進(jìn)epoll實(shí)例中
                            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//除了監(jiān)聽套接字之外的IO套接字
                        {
                                //如果為讀事件就緒
                            if(evs[i].events & EPOLLIN)
                            {
                                    //申請空間用于同時存儲文件描述符和緩沖區(qū)地址
                                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);
                                //從緩沖區(qū)中讀取數(shù)據(jù)
                                ssize_t size = read(_data->_fd, _data->_buf, sizeof(_data->_buf)-1);
                                if(size < 0)//讀取出錯
                                    printf("read error...\n");
                                else if(size == 0)//遠(yuǎn)端關(guān)閉連接
                                {
                                    printf("client closed...\n");
                                    //收尾工作,將事件從epoll實(shí)例中移除,關(guān)閉文件描述符和防止內(nèi)存泄露
                                    epoll_ctl(epoll_fd, EPOLL_CTL_DEL, _data->_fd, NULL);
                                    close(_data->_fd);
                                    free(_data);
                                }
                                else
                                {
                                        //讀取成功,輸出數(shù)據(jù)
                                    (_data->_buf)[size] = '\0';
                                    printf("client# %s", _data->_buf);
                                    fflush(stdout);
                                    //將事件改為關(guān)心寫事件,進(jìn)行回寫
                                    ep_ev.data.ptr = _data;
                                    ep_ev.events = EPOLLOUT;
                                    //在epoll實(shí)例中更改同一個事件
                                    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;

                                //向緩沖區(qū)中回寫數(shù)據(jù)
                                write(_data->_fd, _data->_buf, strlen(_data->_buf));
                                //寫完之后就進(jìn)行完畢一次通信,進(jìn)行收尾
                                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)//判斷命令行參數(shù)的正確性
        Usage(argv[0]);

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

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

    //進(jìn)行epoll操作
    epoll_server(listen_sock);
    close(listen_sock);//關(guān)閉文件描述符

    return 0;
}

這里要說明一下,系統(tǒng)內(nèi)部其實(shí)是為epoll相關(guān)的操作維護(hù)了一棵平衡搜索二叉樹和一張鏈表,如果用戶一次性提供出來的空間不夠存放所有就緒的事件,那么下一次系統(tǒng)會將剩下的再提供出來,因此不必要擔(dān)心提供給epoll_wait的結(jié)構(gòu)體數(shù)組空間的問題;


運(yùn)行程序:

IO復(fù)用之——epoll左邊為server端,右邊為使用telnet請求連接端

因?yàn)樵O(shè)計(jì)的是一問一答的模式,因此在server端收到連接請求和數(shù)據(jù)之后,將數(shù)據(jù)讀取出再回寫回連接請求端,就認(rèn)為完成了一次通信;


如上的模式,還可以用瀏覽器來進(jìn)行測試,只是當(dāng)瀏覽器進(jìn)行連接請求之后,server端就認(rèn)為收到了數(shù)據(jù),轉(zhuǎn)而需要進(jìn)行回寫,而回寫的內(nèi)容則有所要求,因?yàn)榇蟛糠譃g覽器所使用的是HTTP協(xié)議,因此在瀏覽器接收的時候,應(yīng)該收到的是server端寫回的作為響應(yīng)的消息,而這里,HTTP的響應(yīng)由三部分組成,狀態(tài)行、消息報頭和響應(yīng)正文,而作為狀態(tài)行的格式為“協(xié)議版本+響應(yīng)狀態(tài)碼+表示狀態(tài)碼的文本”,過多的內(nèi)容并不屬于本篇文章的討論范圍,因此不贅述,總之,作為響應(yīng)消息,server端寫回的內(nèi)容應(yīng)該是如下格式:

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


運(yùn)行server端程序,打開瀏覽器輸入IP和端口號:

IO復(fù)用之——epoll

當(dāng)瀏覽器連接上server時,server端會接收到關(guān)于瀏覽器方面的信息,也就是獲取了瀏覽器的請求信息,而之后會將響應(yīng)消息返回給瀏覽器,而瀏覽器會根據(jù)接收到的響應(yīng)消息得到正文內(nèi)容并顯示出來,如右邊的顯示(使用本地環(huán)回IP進(jìn)行的測試即127.0.0.1);



四. 水平觸發(fā)和邊緣觸發(fā)

    當(dāng)epoll_wait在進(jìn)行多個事件的等待時,如果有數(shù)據(jù)發(fā)送到緩沖區(qū)中時,則表示當(dāng)前事件處于就緒狀態(tài),則需要返回來通知用戶“有數(shù)據(jù)來了,可以進(jìn)行處理了”,那么對于系統(tǒng)通知用戶的方式,就分為水平式觸發(fā)和邊緣式觸發(fā):

    水平觸發(fā)(Level Trigger)簡稱LT,其特點(diǎn)是當(dāng)數(shù)據(jù)到來的時候會通知用戶,如果用戶一次數(shù)據(jù)處理并沒有將緩沖區(qū)中的數(shù)據(jù)全部取走還留有一部分,那么下一次再進(jìn)行相同事件的epoll_wait的時候系統(tǒng)會認(rèn)為事件仍然是就緒的,還會繼續(xù)通知用戶來取走剩下的數(shù)據(jù),因此,水平觸發(fā)的特點(diǎn)是:只要數(shù)據(jù)緩沖區(qū)中有數(shù)據(jù),當(dāng)前的IO事件始終都是就緒的,epoll_wait始終會返回有效值通知用戶程序

    邊緣觸發(fā)(Edge Triggered)簡稱ET,當(dāng)有數(shù)據(jù)到來的時候仍然會返回通知用戶程序,但是和水平觸發(fā)不同的是,如果用戶在通知一次后對數(shù)據(jù)的IO處理并不完全,也就是一次處理之后緩沖區(qū)中還留有數(shù)據(jù),那么再次返回進(jìn)行epoll_wait的時候就不會再表明當(dāng)前事件是就緒的了,只有當(dāng)這個事件再次有數(shù)據(jù)到達(dá)時才會再一次通知用戶程序來處理數(shù)據(jù),因此,邊緣觸發(fā)的特點(diǎn)是:只有當(dāng)數(shù)據(jù)到來的時候系統(tǒng)才會通知用戶程序且只會通知一次,如果還有數(shù)據(jù)沒有處理完,只有等到再次有數(shù)據(jù)到來的時候才會再次滿足事件就緒,epoll_wait返回通知用戶程序處理數(shù)據(jù);


    這里需要注意的是:對于邊緣式觸發(fā),因?yàn)橹挥挟?dāng)數(shù)據(jù)到來時系統(tǒng)才會通知用戶程序一次,如果當(dāng)前的IO接口工作于阻塞模式,那么當(dāng)一個事件被阻塞的時候,其他事件的就緒也就只會被通知一次但并得不到處理,因此會導(dǎo)致多數(shù)據(jù)的堆積,所以,當(dāng)使用邊緣式觸發(fā)的時候:

  • 最好將當(dāng)前的IO接口設(shè)定為非阻塞的;

  • 當(dāng)一個IO事件進(jìn)行數(shù)據(jù)的讀取和寫入的時候,最好一次性就將緩沖區(qū)中的數(shù)據(jù)全部都處理完;因此,對于數(shù)據(jù)的讀取,可以用一個循環(huán)來每次讀取特定的長度,當(dāng)最后一次讀取的長度小于特定的長度時,就可以認(rèn)為當(dāng)前緩沖區(qū)的數(shù)據(jù)已經(jīng)全部讀取完畢終止循環(huán);但是,不可避免的是,如果最后一次的讀取恰好也就是特定的長度,那么在此進(jìn)行讀取緩沖區(qū)中數(shù)據(jù)為0,就會返回一個EAGAIN的錯誤碼,這個就可以作為循環(huán)的終止條件;


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

IO復(fù)用之——epoll

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


將IO接口設(shè)置為非阻塞的,可以調(diào)用fcntl函數(shù):

IO復(fù)用之——epoll

函數(shù)參數(shù)中,

fd表示要進(jìn)行操作的文件描述符;

cmd表示要進(jìn)行的操作;

至于后面的參數(shù),則有cmd來決定;

IO復(fù)用之——epoll


在這里要設(shè)置文件接口為非阻塞的,首先要將cmd設(shè)置為F_GETFL,表示獲取當(dāng)前文件描述符的標(biāo)志,因?yàn)橹匦略O(shè)定時需要用到;之后需要再次調(diào)用fcntl函數(shù),將cmd設(shè)定為F_SETFL,要重新設(shè)置文件描述符的標(biāo)志,其中有一個選項(xiàng)就是O_NONBLOCK

對于fcntl函數(shù)的返回值,根據(jù)操作的不同而不同:

IO復(fù)用之——epoll

    對比水平觸發(fā)和邊緣觸發(fā),可以發(fā)現(xiàn)水平觸發(fā)對于數(shù)據(jù)的處理來說是更安全更可靠的,而邊緣觸發(fā)是要更為高效的,因此,選擇哪種通知方式,可以依情況而定;


因?yàn)樯厦娴某绦蛑校J(rèn)epoll_wait的通知方式是LT也就是水平觸發(fā)的,要將其改為高效一些的ET邊緣觸發(fā)模式,則需要滿足如上所述的非阻塞條件和數(shù)據(jù)一次性讀取完畢條件:

  • 首先將事件的IO接口設(shè)置為非阻塞模式,則在listen socket創(chuàng)建中以及每一次有新的連接請求獲得新的IO文件描述符之后,都需要調(diào)用如下的函數(shù):

int set_non_block(int fd) 
{
    //獲取當(dāng)前文件描述符的文件標(biāo)識
    int old_fl = fcntl(fd, F_GETFL);
    if(old_fl < 0)
    {   
        perror("fcntl");
        return -1; 
    }
    //將文件描述符所對應(yīng)的事件設(shè)置為非阻塞模式
    if(fcntl(fd, F_SETFL, old_fl|O_NONBLOCK))
    {   
        perror("fcntl");
        return -1; 
    }   
    return 0;
}
  • 其次,就需要自行封裝出一個函數(shù)來進(jìn)行循環(huán)地獲取或者寫入緩沖區(qū)中數(shù)據(jù),直到?jīng)]有數(shù)據(jù)可讀為止,這是為了避免邊緣觸發(fā)的特點(diǎn)帶來的數(shù)據(jù)擁堵不能夠被處理的現(xiàn)象:

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

    int index = 0;
    ssize_t ret = 0;
    //如果讀取到的數(shù)據(jù)等于0,則說明遠(yuǎn)端關(guān)閉連接,直接返回0
    //而如果為非0,不管是大于零還是出錯小于零都需要進(jìn)入循環(huán)
    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;//返回獲得的總數(shù)據(jù)量
}

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

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


將上面修改的代碼添加到上述例子中之后,運(yùn)行程序:

IO復(fù)用之——epoll

分析一下程序結(jié)果,會發(fā)現(xiàn)第一次連接并沒有什么問題,得到了一問一答的結(jié)果,但是如果第二次連接包括以后的多次連接,所發(fā)送的數(shù)據(jù)就無法被server端接收到,反而被認(rèn)為連接端已經(jīng)關(guān)閉了,因此server端就主動關(guān)閉了連接和相關(guān)事件的清除;這是怎么一回事呢?


這是因?yàn)?,在上面所封裝的數(shù)據(jù)的讀寫函數(shù)中,當(dāng)?shù)谝淮芜B接進(jìn)行數(shù)據(jù)的讀取,讀取完畢緩沖區(qū)中所有的數(shù)據(jù)之后,再次進(jìn)行read就會出錯,因而錯誤碼被置為了EAGAIN,而錯誤碼errno是個全局變量,所以當(dāng)再次或者多次連接進(jìn)行數(shù)據(jù)的讀取的時候,即使讀到了數(shù)據(jù)read的返回值大于零,但進(jìn)入循環(huán)進(jìn)行

if(errno == EAGAIN)

判斷的時候,errno已經(jīng)被第一次連接置為了EAGAIN,而運(yùn)行是在同一個進(jìn)程當(dāng)中的,所以始終滿足上述條件跳出循環(huán),返回值為0,之后再進(jìn)行判斷,就會認(rèn)為并沒有讀到數(shù)據(jù),轉(zhuǎn)而關(guān)閉相應(yīng)的文件描述符;

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


要解決上述問題,

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

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

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


2. 另外有一種方法,就是可以用多進(jìn)程來操作,即將errno變成某一個進(jìn)程專屬的全局變量,也就是當(dāng)一個IO的讀事件就緒的時候,就創(chuàng)建出一個子進(jìn)程來進(jìn)行緩沖區(qū)中數(shù)據(jù)的讀寫,將進(jìn)行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);

        //創(chuàng)建進(jìn)程
        pid_t id = fork();
        if(id < 0)//創(chuàng)建失敗
            perror("fork");
        else if(id == 0)//子進(jìn)程
        {
            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);
    }

這里要解釋:當(dāng)創(chuàng)建一個子進(jìn)程的時候,子進(jìn)程復(fù)制父進(jìn)程的PCB,自然也就會獲取其相應(yīng)的文件描述符進(jìn)行操作,但是當(dāng)需要改變其內(nèi)容的時候,比如文件描述符和epoll實(shí)例,子進(jìn)程就會進(jìn)行寫時拷貝,這個時候已經(jīng)不能單單進(jìn)行子進(jìn)程中關(guān)閉文件描述符和釋放空間的操作了,因?yàn)檫@并沒有起到實(shí)際效果,只不過是清除了拷貝出來的內(nèi)容而已,這就是為什么上面的程序中注釋掉了子進(jìn)程中的收尾工作,轉(zhuǎn)而在父進(jìn)程中進(jìn)行;而與此同時,父進(jìn)程是需要進(jìn)行等待的,如果不進(jìn)行等待就會導(dǎo)致同一個IO事件的亂序而無法達(dá)到預(yù)期的效果;


運(yùn)行程序:

IO復(fù)用之——epoll


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

對于線程而言,是共享進(jìn)程的資源的,而errno是一個全局變量,在整個進(jìn)程空間內(nèi)都有效,因此,對于多線程也是同樣共享這一個全局變量的,雖然全局變量是臨界資源,但上述的問題并不是因?yàn)闋帄Z臨界資源而造成的,因?yàn)槭褂昧薴or循環(huán)來一個一個地處理IO事件,而是前一個操作對全局變量的改變影響了后來的操作,這是典型的函數(shù)的可重入性,函數(shù)的可重入性并不等同于線程安全,它需要函數(shù)內(nèi)部使用的變量全部來自于自身的??臻g,因此,如果用多線程或者線程互斥來進(jìn)行操作是沒有什么變化的。



《完》

向AI問一下細(xì)節(jié)

免責(zé)聲明:本站發(fā)布的內(nèi)容(圖片、視頻和文字)以原創(chuàng)、轉(zhuǎn)載和分享為主,文章觀點(diǎn)不代表本網(wǎng)站立場,如果涉及侵權(quán)請聯(lián)系站長郵箱:is@yisu.com進(jìn)行舉報,并提供相關(guān)證據(jù),一經(jīng)查實(shí),將立刻刪除涉嫌侵權(quán)內(nèi)容。

AI