優於select的epoll

之前談到了select,我們認爲它的性能容易受影響的原因在於調用select後常見的針對所有文件描述符的循環語句和每次都需要向select函數傳遞需要監視的對象信息。那麼有沒有更好的IO複用函數能解決這個問題提高效率呢?

答案就是epoll

epoll不需要針對所有的fd做循環,它可以直接返回有事件發生的fd。epoll自己維持着一個fd,我們僅僅需要用它來監視所有的文件描述符,不需要每次都給內核傳遞需要監視的fd集合。對於epoll這個技術,linux給出了3個函數來處理。

#include <sys/epoll.h>
int epoll_create(int size);//失敗返回-1,成功返回一個文件描述符

這個函數返回一個文件描述符標識着epoll句柄。調用這個函數,內核內部產生了很多必須的數據結構和資源,然後返回一個文件描述符來標識。參數size在linux2.6.8以上版本就ignored了,但是必須傳遞一個大於0的值,這個參數是對操作系統用這個epoll_fd監視多少個socket的建議。

既然創建了epoll_fd,那麼就需要往這個epoll_fd中添加需要監視的socket了

#include <sys/epoll.h>
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
成功返回0,失敗返回-1
epfd是epoll_create返回的epoll_fd
op表示在epoll_fd這個句柄中做添加,刪除,修改socket的操作
fd表示需要監視的socket
epoll_event是一個結構體
struct epoll_event
{
    __uint32_t events;//需要監視的socket上發生的事件
    epoll_data_t data;//用戶自定義數據,可以傳遞fd進去,也可以綁定一些數據和fd一起傳遞進去,這就給了很多擴展的空間。如果這個結構體不掛着socket,那麼你從epoll_wait得到了結果也無法使用
};

typedef union epoll_data
{
	void *ptr;
	int fd;
	__uint32_t u32;
	__uint64_t u64;
}epoll_data_t;

//舉一個代碼的實例
int ret = epoll_ctl(A, EPOLL_CTL_ADD, B, C);
這表示在epoll實例A中註冊文件描述符B,主要是監視B上是否發生C事件
int ret = epoll_ctl(A, EPOLL_CTL_DEL, B, NULL);
在epoll實例A中刪除文件描述符B,此時最後一個參數沒必要了
int ret = epoll_ctl(A, EPOLL_CTL_MOD, B, D);
這表示在epoll實例A中修改文件描述符B,監視B上是否發生D事件(原來是C事件)
    
//疑問:如果想A中連續註冊兩次B的C事件,會有什麼效果?先註冊B的C事件,在註冊B的D事件?

下面是一個事件的集合

在這裏插入圖片描述

上面兩個函數主要是爲了做準備,那麼最後一個函數就與select有相同的功能了

#include <sys/epoll.h>
int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout);
失敗返回-1, 成功返回有io事件產生的socket的個數。0表示超時,大於0表示有多少個socket上的事件發生了
很好理解它的各個參數的意義
epfd和epoll_ctl中的epfd一樣
events是一個有maxevents個epoll_event大小的內存塊的指針
timeout表示超時事件,-1表示一直等

這個函數如果返回一個大於0的值,那麼意味着有多少個socket上有io事件發生,它把之前epoll_ctl你傳遞socket上掛着的epoll_event返回給你(如果這個socket上有事件發生),並保存在events上

疑問:我們傳events,是傳遞一個儘量大的,還是傳遞一個小的?

通過這三個函數做一個對比,我們可以看到,在epoll_fd中,內核自動維持着一個socket監視隊列,所以無需每次像select一樣需要我們傳遞sockets進去。在epoll_wait返回後,我們得到的是一個已經發生了io事件的socket上掛着的epoll_event的集合。我們無需遍歷所有的socket來找到發生io事件socket。這裏在應用層面上就體現了epoll的高效的祕密。在內核實現上,epoll_fd裏面,內核維持着一個紅黑樹,socket是key,socket上的epoll_event就是value,大致是這個意思。具體的後面討論。

介紹了這三個函數,關於這三個函數還有一些疑問

疑問:如果想A中連續註冊兩次B的C事件,會有什麼效果?先註冊B的C事件,在註冊B的D事件?寫代碼看看有啥效果

int listen_fd = ::socket(AF_INET, SOCK_STREAM, 0);
    if (listen_fd == -1)
        exit(0);
    struct sockaddr_in addr;
    bzero(&addr, sizeof(addr));
    addr.sin_family = AF_INET;
    addr.sin_port = htons(5050);
    if (1 != inet_pton(AF_INET, "192.168.196.130", &addr.sin_addr))
        exit(0);
    if (0 != ::bind(listen_fd, (const sockaddr*)&addr, sizeof(addr)))
        exit(0);
    if (0 != listen(listen_fd, 5))
        exit(0);

    int epoll_fd = epoll_create(1024);
    struct epoll_event listen_event;
    listen_event.data.fd = listen_fd;
    listen_event.events |= EPOLLIN;
    int ret = epoll_ctl(epoll_fd, EPOLL_CTL_ADD, listen_fd, &listen_event);
    ret = epoll_ctl(epoll_fd, EPOLL_CTL_ADD, listen_fd, &listen_event);
    if (ret != 0)
    {
        printf("error : %s\n", strerror(errno));
    }

得到的結果是:error : File exists。這個socket已經存在了。那麼我們在看一下它的事件是否做了修改

代碼改爲:

	int listen_fd = ::socket(AF_INET, SOCK_STREAM, 0);
    if (listen_fd == -1)
        exit(0);
    struct sockaddr_in addr;
    bzero(&addr, sizeof(addr));
    addr.sin_family = AF_INET;
    addr.sin_port = htons(5050);
    if (1 != inet_pton(AF_INET, "192.168.196.130", &addr.sin_addr))
        exit(0);
    if (0 != ::bind(listen_fd, (const sockaddr*)&addr, sizeof(addr)))
        exit(0);
    if (0 != listen(listen_fd, 5))
        exit(0);

    int epoll_fd = epoll_create(1024);
    struct epoll_event listen_event;
    listen_event.data.fd = listen_fd;
    listen_event.events |= EPOLLERR;
    int ret = epoll_ctl(epoll_fd, EPOLL_CTL_ADD, listen_fd, &listen_event);
    //listen_event.events = 0;
    //listen_event.events |= EPOLLIN;
    //ret = epoll_ctl(epoll_fd, EPOLL_CTL_ADD, listen_fd, &listen_event);
    if (ret != 0)
    {
        printf("error : %s\n", strerror(errno));
    }
    struct epoll_event events[1024];
    while (1)
    {
        memset(&events, 0, 1024 * sizeof(epoll_event));
        ret = epoll_wait(epoll_fd, events, 1024, -1);
        if (ret <= 0)
            continue;
        for (int  i = 0; i < ret; i++)
        {
            int fd = events[i].data.fd;//斷點
            if (fd == listen_fd)
            {
                struct sockaddr_in client_addr;
                socklen_t addr_len = sizeof(client_addr);
                int client_fd = ::accept(fd, (struct sockaddr*)&client_addr, &addr_len);
                if (-1 == client_fd)
                {
                    exit(0);
                }
                char sz[] = "asfddsf";
                send(client_fd, sz, strlen(sz), 0);
                close(fd);
            }
        }
    }
    close(epoll_fd);

我們監視的是EPOLLERR消息,這個消息是收到OOB數據的情況,一般如果有人連接上的話,這個epoll_wait是不會管的。當把註釋掉的三行代碼解封,得到的結果還是當有連接來臨,斷點無法命中。說明我們第一次往A中添加B,並對C事件進行監視,後面再調用一次,無論繼續監視什麼事件,它始終監視C事件。如果需要修改,必須使用epoll_mod來修改。

第二個疑問

我們在epoll_wait中傳events,是傳遞一個儘量大的結構體數組,還是傳遞一個小的?

操作系統會把發生io事件的socket上掛的epoll_event返回來,我們用一個內存塊保存它。那麼按照常理來講,我們必須儘快獲取所有的event來處理io事件。對吧!按照這個道理來說,我們做的是對的。那麼我們需要傳遞多大的內存塊,也就是說這個maxevents給多少合適?1024或者更多?

陳碩的muduo裏面的EPollPoller內部維持着一個kInitEventListSize=16的初始值。

也有人建議這個 maxevents的值不能大於創建epoll_create()時給定的的size。

綜上意見來說,用一個vector的events來表示,可以給一個初始值,當調用epoll_wait後,返回的socket_count的值如果等於events.size()。那麼可以使用events.resize(socket_count * 2);動態分配雙倍內存。這樣子比較適合。

下面一段代碼介紹了epoll_wait的常用做法

for( ; ; )
    {
        nfds = epoll_wait(epfd,events,20,500);
        for(i=0;i<nfds;++i)
        {
            if(events[i].data.fd==listenfd) //如果是主socket的事件,則表示有新的連接
            {
                connfd = accept(listenfd,(sockaddr *)&clientaddr, &clilen); //accept這個連接
                ev.data.fd=connfd;
                ev.events=EPOLLIN|EPOLLET;
                epoll_ctl(epfd,EPOLL_CTL_ADD,connfd,&ev); //將新的fd添加到epoll的監聽隊列中
            }
            else if( events[i].events&EPOLLIN ) //接收到數據,讀socket
            {

            if ( (sockfd = events[i].data.fd) < 0) continue;
                n = read(sockfd, line, MAXLINE)) < 0    //讀
                ev.data.ptr = md;     //md爲自定義類型,添加數據
                ev.events=EPOLLOUT|EPOLLET;
                epoll_ctl(epfd,EPOLL_CTL_MOD,sockfd,&ev);//修改標識符,等待下一個循環時發送數據,異步處理的精髓
            }
            else if(events[i].events&EPOLLOUT) //有數據待發送,寫socket
            {
                struct myepoll_data* md = (myepoll_data*)events[i].data.ptr;    //取數據
                sockfd = md->fd;
                send( sockfd, md->ptr, strlen((char*)md->ptr), 0 );        //發送數據
                ev.data.fd=sockfd;
                ev.events=EPOLLIN|EPOLLET;
                epoll_ctl(epfd,EPOLL_CTL_MOD,sockfd,&ev); //修改標識符,等待下一個循環時接收數據
            }
            else
            {
                //其他情況的處理
            }
        }
    }

下面說一下epoll中關於LT和ET的區別:

LT是水平觸發,只要socket處於readable/writeable狀態下,無論什麼時候epoll_wait都會返回該socket。ET是邊緣觸發,只有socket從unreadable/unwriteable變爲readable/writeable時,epoll_wait纔會返回該socket,ET模式注重的是狀態改變時纔會觸發。

在這裏插入圖片描述

ET模式下,在使用epoll_ctl註冊socket時,必須將socket設置爲非阻塞。原因:一般來說,我們設置ET模式,做數據讀取寫入的話,一般來說要將數據一次性的的讀完或者寫完,比如這樣子

while (true)
{
	epoll_wait(epfd, events, max_events, timeout);
	//可讀
	while (true)
	{
		int ret = recv(fd, ...);
		//直到返回0或者返回-1,,errno = EAGAIN/EWOULDBLOCK
	}
	//可寫
	while (true)
	{
		int ret = send(fd, ...);
		//直到返回0或者返回-1,,errno = EAGAIN/EWOULDBLOCK
	}
    這樣子下次調用時,纔會繼續返回該fd上信息
}

這樣子的目的是爲了讓下一次調用epoll_wait時,還會返回有事件發生的socket。當然如果是LT就不需如此,但是et的性質是狀態改變時纔會觸發,所以只能這樣。如果,socket設置阻塞的話,得到的問題就會很大,比如recv倒數第二次的時候,socket可讀緩衝區已經讀完了,但是由於返回值是一個大於0的值,那麼我們繼續讀取,但此時緩衝區是沒有數據可以讀的,那麼就意味着recv會阻塞了。這樣子效率太低了。所以一般來說ET模式配合非阻塞socket,效率很高。

關於epoll系列還有很多內部細節,比如內核是怎麼實現epoll的?有什麼高效率的辦法?等等!這些問題留到後面繼續研究

就不需如此,但是et的性質是狀態改變時纔會觸發,所以只能這樣。如果,socket設置阻塞的話,得到的問題就會很大,比如recv倒數第二次的時候,socket可讀緩衝區已經讀完了,但是由於返回值是一個大於0的值,那麼我們繼續讀取,但此時緩衝區是沒有數據可以讀的,那麼就意味着recv會阻塞了。這樣子效率太低了。所以一般來說ET模式配合非阻塞socket,效率很高。

關於epoll系列還有很多內部細節,比如內核是怎麼實現epoll的?有什麼高效率的辦法?等等!這些問題留到後面繼續研究

在這裏插入圖片描述

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