epoll使用詳解

前提綱要

Linux中的文件描述符與打開(kāi)文件之間的關(guān)系

  1. 概述
    在Linux系統(tǒng)中一切皆可以看成是文件,文件又可分為:普通文件、目錄文件、鏈接文件和設(shè)備文件。文件描述符(file descriptor)是內(nèi)核為了高效管理已被打開(kāi)的文件所創(chuàng)建的索引,其是一個(gè)非負(fù)整數(shù)(通常是小整數(shù)),用于指代被打開(kāi)的文件,所有執(zhí)行I/O操作的系統(tǒng)調(diào)用都通過(guò)文件描述符。程序剛剛啟動(dòng)的時(shí)候,0是標(biāo)準(zhǔn)輸入,1是標(biāo)準(zhǔn)輸出,2是標(biāo)準(zhǔn)錯(cuò)誤。如果此時(shí)去打開(kāi)一個(gè)新的文件,它的文件描述符會(huì)是3。POSIX標(biāo)準(zhǔn)要求每次打開(kāi)文件時(shí)(含socket)必須使用當(dāng)前進(jìn)程中最小可用的文件描述符號(hào)碼,因此,在網(wǎng)絡(luò)通信過(guò)程中稍不注意就有可能造成串話。標(biāo)準(zhǔn)文件描述符圖如下:

    image.png

  2. 文件描述限制
    在編寫(xiě)文件操作的或者網(wǎng)絡(luò)通信的軟件時(shí),初學(xué)者一般可能會(huì)遇到“Too many open files”的問(wèn)題。這主要是因?yàn)槲募枋龇窍到y(tǒng)的一個(gè)重要資源,雖然說(shuō)系統(tǒng)內(nèi)存有多少就可以打開(kāi)多少的文件描述符,但是在實(shí)際實(shí)現(xiàn)過(guò)程中內(nèi)核是會(huì)做相應(yīng)的處理的,一般最大打開(kāi)文件數(shù)會(huì)是系統(tǒng)內(nèi)存的10%(以KB來(lái)計(jì)算)(稱之為系統(tǒng)級(jí)限制),查看系統(tǒng)級(jí)別的最大打開(kāi)文件數(shù)可以使用sysctl -a | grep fs.file-max命令查看。與此同時(shí),內(nèi)核為了不讓某一個(gè)進(jìn)程消耗掉所有的文件資源,其也會(huì)對(duì)單個(gè)進(jìn)程最大打開(kāi)文件數(shù)做默認(rèn)值處理(稱之為用戶級(jí)限制),默認(rèn)值一般是1024,使用ulimit -n命令可以查看。

  3. 文件描述符合打開(kāi)文件之間的關(guān)系
    每一個(gè)文件描述符會(huì)與一個(gè)打開(kāi)文件相對(duì)應(yīng),同時(shí),不同的文件描述符也會(huì)指向同一個(gè)文件。相同的文件可以被不同的進(jìn)程打開(kāi)也可以在同一個(gè)進(jìn)程中被多次打開(kāi)。系統(tǒng)為每一個(gè)進(jìn)程維護(hù)了一個(gè)文件描述符表,該表的值都是從0開(kāi)始的,所以在不同的進(jìn)程中你會(huì)看到相同的文件描述符,這種情況下相同文件描述符有可能指向同一個(gè)文件,也有可能指向不同的文件。具體情況要具體分析,要理解具體其概況如何,需要查看由內(nèi)核維護(hù)的3個(gè)數(shù)據(jù)結(jié)構(gòu)。

    1. 進(jìn)程級(jí)的文件描述符表
    2. 系統(tǒng)級(jí)的打開(kāi)文件描述符表
    3. 文件系統(tǒng)的i-node表

進(jìn)程級(jí)的描述符表的每一條目記錄了單個(gè)文件描述符的相關(guān)信息。
1. 控制文件描述符操作的一組標(biāo)志。(目前,此類標(biāo)志僅定義了一個(gè),即close-on-exec標(biāo)志)
2. 對(duì)打開(kāi)文件句柄的引用

內(nèi)核對(duì)所有打開(kāi)的文件的文件維護(hù)有一個(gè)系統(tǒng)級(jí)的描述符表格(open file description table)。有時(shí),也稱之為打開(kāi)文件表(open file table),并將表格中各條目稱為打開(kāi)文件句柄(open file handle)。一個(gè)打開(kāi)文件句柄存儲(chǔ)了與一個(gè)打開(kāi)文件相關(guān)的全部信息,如下所示:
1. 當(dāng)前文件偏移量(調(diào)用read()和write()時(shí)更新,或使用lseek()直接修改)
2. 打開(kāi)文件時(shí)所使用的狀態(tài)標(biāo)識(shí)(即,open()的flags參數(shù))
3. 文件訪問(wèn)模式(如調(diào)用open()時(shí)所設(shè)置的只讀模式、只寫(xiě)模式或讀寫(xiě)模式)
4. 與信號(hào)驅(qū)動(dòng)相關(guān)的設(shè)置
5. 對(duì)該文件i-node對(duì)象的引用
6. 文件類型(例如:常規(guī)文件、套接字或FIFO)和訪問(wèn)權(quán)限
7. 一個(gè)指針,指向該文件所持有的鎖列表
8. 文件的各種屬性,包括文件大小以及與不同類型操作相關(guān)的時(shí)間戳

下圖展示了文件描述符、打開(kāi)的文件句柄以及i-node之間的關(guān)系,圖中,兩個(gè)進(jìn)程擁有諸多打開(kāi)的文件描述符。


Epoll

Epoll - I/O event notification facility
翻譯一下,epoll是一種I/O事件通知機(jī)制,這句話基本上包含了所有需要理解的要點(diǎn):

  • I/O事件
    基于file descriptor,支持file, socket, pipe等各種I/O方式
    當(dāng)文件描述符關(guān)聯(lián)的內(nèi)核讀緩沖區(qū)可讀,則觸發(fā)可讀事件,什么是可讀呢?就是內(nèi)核緩沖區(qū)非空,有數(shù)據(jù)可以讀取
    當(dāng)文件描述符關(guān)聯(lián)的內(nèi)核寫(xiě)緩沖區(qū)可寫(xiě),則觸發(fā)可寫(xiě)事件,什么是可寫(xiě)呢?就是內(nèi)核緩沖區(qū)不滿,有空閑空間可以寫(xiě)入
  • 通知機(jī)制
    通知機(jī)制,就是當(dāng)事件發(fā)生的時(shí)候,去通知他
    通知機(jī)制的反面,就是輪詢機(jī)制

epoll是一種當(dāng)文件描述符的內(nèi)核緩沖區(qū)非空的時(shí)候,發(fā)出可讀信號(hào)進(jìn)行通知,當(dāng)寫(xiě)緩沖區(qū)不滿的時(shí)候,發(fā)出可寫(xiě)信號(hào)通知的機(jī)制

Epoll函數(shù)

epoll的接口非常簡(jiǎn)單,一共就三個(gè)函數(shù):

  1. int epoll_create(int size);
    創(chuàng)建一個(gè)epoll的句柄,size用來(lái)告訴內(nèi)核這個(gè)監(jiān)聽(tīng)的數(shù)目一共有多大。這個(gè)參數(shù)不同于select()中的第一個(gè)參數(shù),給出最大監(jiān)聽(tīng)的fd+1的值。需要注意的是,當(dāng)創(chuàng)建好epoll句柄后,它就是會(huì)占用一個(gè)fd值,在linux下如果查看/proc/進(jìn)程id/fd/,是能夠看到這個(gè)fd的,所以在使用完epoll后,必須調(diào)用close()關(guān)閉,否則可能導(dǎo)致fd被耗盡。

  2. int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
    epoll的事件注冊(cè)函數(shù),它不同與select()是在監(jiān)聽(tīng)事件時(shí)告訴內(nèi)核要監(jiān)聽(tīng)什么類型的事件,而是在這里先注冊(cè)要監(jiān)聽(tīng)的事件類型。第一個(gè)參數(shù)是epoll_create()的返回值,第二個(gè)參數(shù)表示動(dòng)作,用三個(gè)宏來(lái)表示:
    EPOLL_CTL_ADD:注冊(cè)新的fd到epfd中;
    EPOLL_CTL_MOD:修改已經(jīng)注冊(cè)的fd的監(jiān)聽(tīng)事件;
    EPOLL_CTL_DEL:從epfd中刪除一個(gè)fd;
    第三個(gè)參數(shù)是需要監(jiān)聽(tīng)的fd,第四個(gè)參數(shù)是告訴內(nèi)核需要監(jiān)聽(tīng)什么事,struct epoll_event結(jié)構(gòu)如下:

 typedef union epoll_data {
 2     void *ptr;
 3     int fd;
 4     __uint32_t u32;
 5     __uint64_t u64;
 6 } epoll_data_t;
 7 
 8 struct epoll_event {
 9     __uint32_t events; /* Epoll events */
10     epoll_data_t data; /* User data variable */
11 };

events可以是以下幾個(gè)宏的集合:
EPOLLIN :表示對(duì)應(yīng)的文件描述符可以讀(包括對(duì)端SOCKET正常關(guān)閉);
EPOLLOUT:表示對(duì)應(yīng)的文件描述符可以寫(xiě);
EPOLLPRI:表示對(duì)應(yīng)的文件描述符有緊急的數(shù)據(jù)可讀(這里應(yīng)該表示有帶外數(shù)據(jù)到來(lái));
EPOLLERR:表示對(duì)應(yīng)的文件描述符發(fā)生錯(cuò)誤;
EPOLLHUP:表示對(duì)應(yīng)的文件描述符被掛斷;
EPOLLET: 將EPOLL設(shè)為邊緣觸發(fā)(Edge Triggered)模式,這是相對(duì)于水平觸發(fā)(Level Triggered)來(lái)說(shuō)的。
EPOLLONESHOT:只監(jiān)聽(tīng)一次事件,當(dāng)監(jiān)聽(tīng)完這次事件之后,如果還需要繼續(xù)監(jiān)聽(tīng)這個(gè)socket的話,需要再次把這個(gè)socket加入到EPOLL隊(duì)列里

  1. int epoll_wait(int epfd, struct epoll_event * events, int maxevents, int timeout);
    等待事件的產(chǎn)生,類似于select()調(diào)用。參數(shù)events用來(lái)從內(nèi)核得到事件的集合,maxevents告之內(nèi)核這個(gè)events有多大,這個(gè) maxevents的值不能大于創(chuàng)建epoll_create()時(shí)的size,參數(shù)timeout是超時(shí)時(shí)間(毫秒,0會(huì)立即返回,-1將不確定,也有說(shuō)法說(shuō)是永久阻塞)。該函數(shù)返回需要處理的事件數(shù)目,如返回0表示已超時(shí)。
  1. 關(guān)于ET、LT兩種工作模式:
    可以得出這樣的結(jié)論:
    ET模式僅當(dāng)狀態(tài)發(fā)生變化的時(shí)候才獲得通知,這里所謂的狀態(tài)的變化并不包括緩沖區(qū)中還有未處理的數(shù)據(jù),也就是說(shuō),如果要采用ET模式,需要一直read/write直到出錯(cuò)為止,很多人反映為什么采用ET模式只接收了一部分?jǐn)?shù)據(jù)就再也得不到通知了,大多因?yàn)檫@樣;而LT模式是只要有數(shù)據(jù)沒(méi)有處理就會(huì)一直通知下去的.

首先通過(guò)create_epoll(int maxfds)來(lái)創(chuàng)建一個(gè)epoll的句柄,其中maxfds為你epoll所支持的最大句柄數(shù)。這個(gè)函數(shù)會(huì)返回一個(gè)新的epoll句柄,之后的所有操作將通過(guò)這個(gè)句柄來(lái)進(jìn)行操作。在用完之后,記得用close()來(lái)關(guān)閉這個(gè)創(chuàng)建出來(lái)的epoll句柄。

之后在你的網(wǎng)絡(luò)主循環(huán)里面,每一幀的調(diào)用epoll_wait(int epfd, epoll_event events, int max events, int timeout)來(lái)查詢所有的網(wǎng)絡(luò)接口,看哪一個(gè)可以讀,哪一個(gè)可以寫(xiě)了?;镜恼Z(yǔ)法為:
nfds = epoll_wait(kdpfd, events, maxevents, -1);
其中kdpfd為用epoll_create創(chuàng)建之后的句柄,events是一個(gè)epoll_event*的指針,當(dāng)epoll_wait這個(gè)函數(shù)操作成功之后,epoll_events里面將儲(chǔ)存所有的讀寫(xiě)事件。max_events是當(dāng)前需要監(jiān)聽(tīng)的所有socket句柄數(shù)。最后一個(gè)timeoutepoll_wait的超時(shí),為0的時(shí)候表示馬上返回,為-1的時(shí)候表示一直等下去,直到有事件范圍,為任意正整數(shù)的時(shí)候表示等這么長(zhǎng)的時(shí)間,如果一直沒(méi)有事件,則范圍。一般如果網(wǎng)絡(luò)主循環(huán)是單獨(dú)的線程的話,可以用-1來(lái)等,這樣可以保證一些效率,如果是和主邏輯在同一個(gè)線程的話,則可以用0來(lái)保證主循環(huán)的效率。

epoll_wait范圍之后應(yīng)該是一個(gè)循環(huán),遍利所有的事件。

幾乎所有的epoll程序都使用下面的框架:

for( ; ; )
    {
        nfds = epoll_wait(epfd,events,20,500);
        for(i=0;i<nfds;++i)
        {
            if(events[i].data.fd==listenfd) //有新的連接
            {
                connfd = accept(listenfd,(sockaddr *)&clientaddr, &clilen); //accept這個(gè)連接
                ev.data.fd=connfd;
                ev.events=EPOLLIN|EPOLLET;
                epoll_ctl(epfd,EPOLL_CTL_ADD,connfd,&ev); //將新的fd添加到epoll的監(jiān)聽(tīng)隊(duì)列中
            }
            else if( events[i].events&EPOLLIN ) //接收到數(shù)據(jù),讀socket
            {
                n = read(sockfd, line, MAXLINE)) < 0    //讀
                ev.data.ptr = md;     //md為自定義類型,添加數(shù)據(jù)
                ev.events=EPOLLOUT|EPOLLET;
                epoll_ctl(epfd,EPOLL_CTL_MOD,sockfd,&ev);//修改標(biāo)識(shí)符,等待下一個(gè)循環(huán)時(shí)發(fā)送數(shù)據(jù),異步處理的精髓
            }
            else if(events[i].events&EPOLLOUT) //有數(shù)據(jù)待發(fā)送,寫(xiě)socket
            {
                struct myepoll_data* md = (myepoll_data*)events[i].data.ptr;    //取數(shù)據(jù)
                sockfd = md->fd;
                send( sockfd, md->ptr, strlen((char*)md->ptr), 0 );        //發(fā)送數(shù)據(jù)
                ev.data.fd=sockfd;
                ev.events=EPOLLIN|EPOLLET;
                epoll_ctl(epfd,EPOLL_CTL_MOD,sockfd,&ev); //修改標(biāo)識(shí)符,等待下一個(gè)循環(huán)時(shí)接收數(shù)據(jù)
            }
            else
            {
                //其他的處理
            }
        }
    }

下面給出一個(gè)完整的服務(wù)器端例子:

#include <iostream>
#include <sys/socket.h>
#include <sys/epoll.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <fcntl.h>
#include <unistd.h>
#include <stdio.h>
#include <errno.h>

using namespace std;

#define MAXLINE 5
#define OPEN_MAX 100
#define LISTENQ 20
#define SERV_PORT 5000
#define INFTIM 1000

void setnonblocking(int sock)
{
    int opts;
    opts=fcntl(sock,F_GETFL);
    if(opts<0)
    {
        perror("fcntl(sock,GETFL)");
        exit(1);
    }
    opts = opts|O_NONBLOCK;
    if(fcntl(sock,F_SETFL,opts)<0)
    {
        perror("fcntl(sock,SETFL,opts)");
        exit(1);
    }
}

int main(int argc, char* argv[])
{
    int i, maxi, listenfd, connfd, sockfd,epfd,nfds, portnumber;
    ssize_t n;
    char line[MAXLINE];
    socklen_t clilen;


    if ( 2 == argc )
    {
        if( (portnumber = atoi(argv[1])) < 0 )
        {
            fprintf(stderr,"Usage:%s portnumber/a/n",argv[0]);
            return 1;
        }
    }
    else
    {
        fprintf(stderr,"Usage:%s portnumber/a/n",argv[0]);
        return 1;
    }



    //聲明epoll_event結(jié)構(gòu)體的變量,ev用于注冊(cè)事件,數(shù)組用于回傳要處理的事件

    struct epoll_event ev,events[20];
    //生成用于處理accept的epoll專用的文件描述符

    epfd=epoll_create(256);
    struct sockaddr_in clientaddr;
    struct sockaddr_in serveraddr;
    listenfd = socket(AF_INET, SOCK_STREAM, 0);
    //把socket設(shè)置為非阻塞方式

    //setnonblocking(listenfd);

    //設(shè)置與要處理的事件相關(guān)的文件描述符

    ev.data.fd=listenfd;
    //設(shè)置要處理的事件類型

    ev.events=EPOLLIN|EPOLLET;
    //ev.events=EPOLLIN;

    //注冊(cè)epoll事件

    epoll_ctl(epfd,EPOLL_CTL_ADD,listenfd,&ev);
    bzero(&serveraddr, sizeof(serveraddr));
    serveraddr.sin_family = AF_INET;
    char *local_addr="127.0.0.1";
    inet_aton(local_addr,&(serveraddr.sin_addr));//htons(portnumber);

    serveraddr.sin_port=htons(portnumber);
    bind(listenfd,(sockaddr *)&serveraddr, sizeof(serveraddr));
    listen(listenfd, LISTENQ);
    maxi = 0;
    for ( ; ; ) {
        //等待epoll事件的發(fā)生

        nfds=epoll_wait(epfd,events,20,500);
        //處理所發(fā)生的所有事件

        for(i=0;i<nfds;++i)
        {
            if(events[i].data.fd==listenfd)//如果新監(jiān)測(cè)到一個(gè)SOCKET用戶連接到了綁定的SOCKET端口,建立新的連接。

            {
                connfd = accept(listenfd,(sockaddr *)&clientaddr, &clilen);
                if(connfd<0){
                    perror("connfd<0");
                    exit(1);
                }
                //setnonblocking(connfd);

                char *str = inet_ntoa(clientaddr.sin_addr);
                cout << "accapt a connection from " << str << endl;
                //設(shè)置用于讀操作的文件描述符

                ev.data.fd=connfd;
                //設(shè)置用于注測(cè)的讀操作事件

                ev.events=EPOLLIN|EPOLLET;
                //ev.events=EPOLLIN;

                //注冊(cè)ev

                epoll_ctl(epfd,EPOLL_CTL_ADD,connfd,&ev);
            }
            else if(events[i].events&EPOLLIN)//如果是已經(jīng)連接的用戶,并且收到數(shù)據(jù),那么進(jìn)行讀入。

            {
                cout << "EPOLLIN" << endl;
                if ( (sockfd = events[i].data.fd) < 0)
                    continue;
                if ( (n = read(sockfd, line, MAXLINE)) < 0) {
                    if (errno == ECONNRESET) {
                        close(sockfd);
                        events[i].data.fd = -1;
                    } else
                        std::cout<<"readline error"<<std::endl;
                } else if (n == 0) {
                    close(sockfd);
                    events[i].data.fd = -1;
                }
                line[n] = '/0';
                cout << "read " << line << endl;
                //設(shè)置用于寫(xiě)操作的文件描述符

                ev.data.fd=sockfd;
                //設(shè)置用于注測(cè)的寫(xiě)操作事件

                ev.events=EPOLLOUT|EPOLLET;
                //修改sockfd上要處理的事件為EPOLLOUT

                //epoll_ctl(epfd,EPOLL_CTL_MOD,sockfd,&ev);

            }
            else if(events[i].events&EPOLLOUT) // 如果有數(shù)據(jù)發(fā)送

            {
                sockfd = events[i].data.fd;
                write(sockfd, line, n);
                //設(shè)置用于讀操作的文件描述符

                ev.data.fd=sockfd;
                //設(shè)置用于注測(cè)的讀操作事件

                ev.events=EPOLLIN|EPOLLET;
                //修改sockfd上要處理的事件為EPOLIN

                epoll_ctl(epfd,EPOLL_CTL_MOD,sockfd,&ev);
            }
        }
    }
    return 0;
}
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

  • 在Linux網(wǎng)絡(luò)編程當(dāng)中,很長(zhǎng)時(shí)間都是使用select來(lái)做事件的觸發(fā),而在新的linux內(nèi)核當(dāng)中,有一種替換他的機(jī)...
    Joe_HUST閱讀 2,760評(píng)論 0 2
  • 本文摘抄自linux基礎(chǔ)編程 IO概念 Linux的內(nèi)核將所有外部設(shè)備都可以看做一個(gè)文件來(lái)操作。那么我們對(duì)與外部設(shè)...
    VD2012閱讀 1,064評(píng)論 0 2
  • 本文討論的背景是Linux環(huán)境下的network IO。 一、 概念說(shuō)明 在進(jìn)行解釋之前,首先要說(shuō)明幾個(gè)概念: 用...
    faunjoe閱讀 4,539評(píng)論 1 15
  • epoll概述 epoll是linux中IO多路復(fù)用的一種機(jī)制,I/O多路復(fù)用就是通過(guò)一種機(jī)制,一個(gè)進(jìn)程可以監(jiān)視多...
    發(fā)仔很忙閱讀 11,071評(píng)論 4 35
  • 同步IO和異步IO,阻塞IO和非阻塞IO分別是什么,到底有什么區(qū)別?不同的人在不同的上下文下給出的答案是不同的。所...
    lxqfirst閱讀 2,217評(píng)論 0 47

友情鏈接更多精彩內(nèi)容