真实的国产乱ⅩXXX66竹夫人,五月香六月婷婷激情综合,亚洲日本VA一区二区三区,亚洲精品一区二区三区麻豆

成都創(chuàng)新互聯(lián)網(wǎng)站制作重慶分公司

IO復(fù)用之——epoll-創(chuàng)新互聯(lián)

一. 關(guān)于epoll

創(chuàng)新互聯(lián)建站成都網(wǎng)站建設(shè)按需策劃設(shè)計(jì),是成都網(wǎng)站開發(fā)公司,為成都汽車玻璃修復(fù)提供網(wǎng)站建設(shè)服務(wù),有成熟的網(wǎng)站定制合作流程,提供網(wǎng)站定制設(shè)計(jì)服務(wù):原型圖制作、網(wǎng)站創(chuàng)意設(shè)計(jì)、前端HTML5制作、后臺(tái)程序開發(fā)等。成都網(wǎng)站推廣熱線:13518219792

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

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


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

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

  1. epoll_create

IO復(fù)用之——epoll

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

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

2. epoll_ctl

IO復(fù)用之——epoll

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

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

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

IO復(fù)用之——epoll

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

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

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

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

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

IO復(fù)用之——epoll

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

events表示文件描述符fd所對(duì)應(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ā)生錯(cuò)誤;

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

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

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

IO復(fù)用之——epoll

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

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

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

3. epoll_wait

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

IO復(fù)用之——epoll

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

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

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

maxevents是events的大小;

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

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

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

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

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


三. 栗子時(shí)間

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

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

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

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

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

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

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

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

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

#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 

#define _BACKLOG_ 5  //網(wǎng)絡(luò)中連接請(qǐng)求等待隊(duì)列大值
#define _MAX_NUM_ 20 //事件就緒隊(duì)列存儲(chǔ)空間
#define _DATA_SIZE_ 1024 //數(shù)據(jù)緩沖區(qū)大小

//因?yàn)閑poll_event結(jié)構(gòu)體中的data成員是一個(gè)聯(lián)合體,因此當(dāng)需要同時(shí)使用聯(lián)合中的fd和ptr的時(shí)候就會(huì)有問題
//因此可以將其各自單獨(dú)拿出存儲(chǔ)
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首先斷開連接的時(shí)候避免進(jìn)入一個(gè)TIME_WAIT的等待時(shí)間
    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)建出一個(gè)epoll實(shí)例,獲取其文件描述符,大小隨意指定
    int epoll_fd = epoll_create(256);
    if(epoll_fd < 0)
    {
        perror("epoll_create");
        exit(5);
    }

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

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

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

                            //處理連接請(qǐng)求,獲取新的通信套接字
                            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ǐng)空間用于同時(shí)存儲(chǔ)文件描述符和緩沖區(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)//讀取出錯(cuò)
                                    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í)例中更改同一個(gè)事件
                                    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]);

    //獲取端口號(hào)和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)會(huì)將剩下的再提供出來,因此不必要擔(dān)心提供給epoll_wait的結(jié)構(gòu)體數(shù)組空間的問題;

運(yùn)行程序:

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

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

如上的模式,還可以用瀏覽器來進(jìn)行測(cè)試,只是當(dāng)瀏覽器進(jìn)行連接請(qǐng)求之后,server端就認(rèn)為收到了數(shù)據(jù),轉(zhuǎn)而需要進(jìn)行回寫,而回寫的內(nèi)容則有所要求,因?yàn)榇蟛糠譃g覽器所使用的是HTTP協(xié)議,因此在瀏覽器接收的時(shí)候,應(yīng)該收到的是server端寫回的作為響應(yīng)的消息,而這里,HTTP的響應(yīng)由三部分組成,狀態(tài)行、消息報(bào)頭和響應(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和端口號(hào):

IO復(fù)用之——epoll

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


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

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

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

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

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

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

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

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

IO復(fù)用之——epoll

若輸出其對(duì)應(yīng)錯(cuò)誤描述,為:Resource temporarily unavailable,意思是資源暫時(shí)不可用,可以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è)定時(shí)需要用到;之后需要再次調(diào)用fcntl函數(shù),將cmd設(shè)定為F_SETFL,要重新設(shè)置文件描述符的標(biāo)志,其中有一個(gè)選項(xiàng)就是O_NONBLOCK

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

IO復(fù)用之——epoll

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

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

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

int set_non_block(int fd) 
{
    //獲取當(dāng)前文件描述符的文件標(biāo)識(shí)
    int old_fl = fcntl(fd, F_GETFL);
    if(old_fl < 0)
    {   
        perror("fcntl");
        return -1; 
    }
    //將文件描述符所對(duì)應(yīng)的事件設(shè)置為非阻塞模式
    if(fcntl(fd, F_SETFL, old_fl|O_NONBLOCK))
    {   
        perror("fcntl");
        return -1; 
    }   
    return 0;
}
  • 其次,就需要自行封裝出一個(gè)函數(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,不管是大于零還是出錯(cuò)小于零都需要進(jìn)入循環(huán)
    while((ret = read(fd, buf+index, size-index)))
    {
        if(errno == EAGAIN)//如果錯(cuò)誤碼為EAGAIN,則說明讀取完畢,打印出錯(cuò)誤碼和錯(cuò)誤消息并退出
        {
            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的時(shí)候直接返回0
    //否則,返回值為非零進(jìn)入循環(huán)
    while((ret = write(fd, buf+index, size-index)))
    {
        if(errno == EAGAIN)//當(dāng)數(shù)據(jù)全部寫完的時(shí)候返回錯(cuò)誤碼為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é)果,會(huì)發(fā)現(xiàn)第一次連接并沒有什么問題,得到了一問一答的結(jié)果,但是如果第二次連接包括以后的多次連接,所發(fā)送的數(shù)據(jù)就無法被server端接收到,反而被認(rèn)為連接端已經(jīng)關(guān)閉了,因此server端就主動(dòng)關(guān)閉了連接和相關(guān)事件的清除;這是怎么一回事呢?

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

if(errno == EAGAIN)

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

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

要解決上述問題,

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

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

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

2. 另外有一種方法,就是可以用多進(jìn)程來操作,即將errno變成某一個(gè)進(jìn)程專屬的全局變量,也就是當(dāng)一個(gè)IO的讀事件就緒的時(shí)候,就創(chuàng)建出一個(gè)子進(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)建一個(gè)子進(jìn)程的時(shí)候,子進(jìn)程復(fù)制父進(jìn)程的PCB,自然也就會(huì)獲取其相應(yīng)的文件描述符進(jìn)行操作,但是當(dāng)需要改變其內(nèi)容的時(shí)候,比如文件描述符和epoll實(shí)例,子進(jìn)程就會(huì)進(jìn)行寫時(shí)拷貝,這個(gè)時(shí)候已經(jīng)不能單單進(jìn)行子進(jìn)程中關(guān)閉文件描述符和釋放空間的操作了,因?yàn)檫@并沒有起到實(shí)際效果,只不過是清除了拷貝出來的內(nèi)容而已,這就是為什么上面的程序中注釋掉了子進(jìn)程中的收尾工作,轉(zhuǎn)而在父進(jìn)程中進(jìn)行;而與此同時(shí),父進(jìn)程是需要進(jìn)行等待的,如果不進(jìn)行等待就會(huì)導(dǎo)致同一個(gè)IO事件的亂序而無法達(dá)到預(yù)期的效果

運(yùn)行程序:

IO復(fù)用之——epoll

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

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

《完》

另外有需要云服務(wù)器可以了解下創(chuàng)新互聯(lián)scvps.cn,海內(nèi)外云服務(wù)器15元起步,三天無理由+7*72小時(shí)售后在線,公司持有idc許可證,提供“云服務(wù)器、裸金屬服務(wù)器、高防服務(wù)器、香港服務(wù)器、美國服務(wù)器、虛擬主機(jī)、免備案服務(wù)器”等云主機(jī)租用服務(wù)以及企業(yè)上云的綜合解決方案,具有“安全穩(wěn)定、簡單易用、服務(wù)可用性高、性價(jià)比高”等特點(diǎn)與優(yōu)勢(shì),專為企業(yè)上云打造定制,能夠滿足用戶豐富、多元化的應(yīng)用場(chǎng)景需求。


網(wǎng)站標(biāo)題:IO復(fù)用之——epoll-創(chuàng)新互聯(lián)
本文地址:http://weahome.cn/article/dgihpp.html

其他資訊

在線咨詢

微信咨詢

電話咨詢

028-86922220(工作日)

18980820575(7×24)

提交需求

返回頂部