linux網路編程之socket(十三):epoll 系列函數簡介、與select、poll 的區別

來源:互聯網
上載者:User

一、epoll 系列函數簡介

#include <sys/epoll.h>
int epoll_create(int size);
int epoll_create1(int flags);
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout);

       *  epoll_create(2)  creates  an epoll instance and returns a file descriptor referring to that instance.  (The more recent
          epoll_create1(2) extends the functionality of epoll_create(2).)

       *  Interest in particular file descriptors is then registered via epoll_ctl(2).  The set  of  file  descriptors  currently
          registered on an epoll instance is sometimes called an epoll set.

       *  epoll_wait(2) waits for I/O events, blocking the calling thread if no events are currently available.


1、epoll_create1 產生一個epoll 執行個體,返回的是執行個體的控制代碼。flag 可以設定為0 或者EPOLL_CLOEXEC,為0時函數表現與epoll_create一致,EPOLL_CLOEXEC標誌與open 時的O_CLOEXEC 標誌類似,即進程被替換時會關閉檔案描述符。

2、epoll_ctl :

(1)epfd:epoll 執行個體控制代碼;

(2)op:對檔案描述符fd 的操作,主要有EPOLL_CTL_ADD、 EPOLL_CTL_DEL等;

(3)fd:需要操作的目標檔案描述符;

(4)event:結構體指標

  typedef union epoll_data {
               void        *ptr;
               int          fd;
               uint32_t     u32;
               uint64_t     u64;
           } epoll_data_t;

 struct epoll_event {
          uint32_t     events;      /* Epoll events */
                epoll_data_t  data;        /* User data variable */
           };

events 參數主要有EPOLLIN、EPOLLOUT、EPOLLET、EPOLLLT等;一般data 共同體我們設定其成員fd即可,也就是epoll_ctl 函數的第三個參數。

3、epoll_wait:

(1)epfd:epoll 執行個體控制代碼;

(2)events:結構體指標

(3)maxevents:事件的最大個數

(4)timeout:逾時時間,設為-1表示永不逾時

下面我們使用c++ 來實現一個伺服器端程式:

 C++ Code 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <signal.h>
#include <fcntl.h>
#include <sys/wait.h>
#include <sys/epoll.h>

#include <stdlib.h>
#include <stdio.h>
#include <errno.h>
#include <string.h>

#include <vector>
#include <algorithm>

#include "read_write.h"
#include "sysutil.h"

typedef std::vector<struct epoll_event> EventList;

/* 相比於select與poll,epoll最大的好處是不會隨著關心的fd數目的增多而降低效率 */
int main(void)
{
    int count = 0;
    int listenfd;
    if ((listenfd = socket(PF_INET, SOCK_STREAM, IPPROTO_TCP)) < 0)
        ERR_EXIT("socket");

    struct sockaddr_in servaddr;
    memset(&servaddr, 0, sizeof(servaddr));
    servaddr.sin_family = AF_INET;
    servaddr.sin_port = htons(5188);
    servaddr.sin_addr.s_addr = htonl(INADDR_ANY);

    int on = 1;
    if (setsockopt(listenfd, SOL_SOCKET, SO_REUSEADDR, &on, sizeof(on)) < 0)
        ERR_EXIT("setsockopt");

    if (bind(listenfd, (struct sockaddr *)&servaddr, sizeof(servaddr)) < 0)
        ERR_EXIT("bind");
    if (listen(listenfd, SOMAXCONN) < 0)
        ERR_EXIT("listen");

    std::vector<int> clients;
    int epollfd;
    epollfd = epoll_create1(EPOLL_CLOEXEC); //epoll執行個體控制代碼

    struct epoll_event event;
    event.data.fd = listenfd;
    event.events = EPOLLIN | EPOLLET; //邊沿觸發
    epoll_ctl(epollfd, EPOLL_CTL_ADD, listenfd, &event);

    EventList events(16);
    struct sockaddr_in peeraddr;
    socklen_t peerlen;
    int conn;
    int i;

    int nready;
    while (1)
    {
        nready = epoll_wait(epollfd, &*events.begin(), static_cast<int>(events.size()), -1);
        if (nready == -1)
        {
            if (errno == EINTR)
                continue;

            ERR_EXIT("epoll_wait");
        }
        if (nready == 0)
            continue;

        if ((size_t)nready == events.size())
            events.resize(events.size() * 2);

        for (i = 0; i < nready; i++)
        {
            if (events[i].data.fd == listenfd)
            {
                peerlen = sizeof(peeraddr);
                conn = accept(listenfd, (struct sockaddr *)&peeraddr, &peerlen);
                if (conn == -1)
                    ERR_EXIT("accept");

                printf("ip=%s port=%d\n", inet_ntoa(peeraddr.sin_addr), ntohs(peeraddr.sin_port));
                printf("count = %d\n", ++count);
                clients.push_back(conn);

                activate_nonblock(conn);

                event.data.fd = conn;
                event.events = EPOLLIN | EPOLLET;
                epoll_ctl(epollfd, EPOLL_CTL_ADD, conn, &event);
            }
            else if (events[i].events & EPOLLIN)
            {
                conn = events[i].data.fd;
                if (conn < 0)
                    continue;

                char recvbuf[1024] = {0};
                int ret = readline(conn, recvbuf, 1024);
                if (ret == -1)
                    ERR_EXIT("readline");
                if (ret == 0)
                {
                    printf("client close\n");
                    close(conn);

                    event = events[i];
                    epoll_ctl(epollfd, EPOLL_CTL_DEL, conn, &event);
                    clients.erase(std::remove(clients.begin(), clients.end(), conn), clients.end());
                }

                fputs(recvbuf, stdout);
                writen(conn, recvbuf, strlen(recvbuf));
            }

        }
    }

    return 0;
}

在程式的最開始定義一個新類型EventList,內部裝著struct epoll_event 結構體的容器。

接下面的socket,bind,listen 都跟以前說的一樣,不述。接著使用epoll_create1 建立一個epoll 執行個體,再來看下面四行代碼:

struct epoll_event event;
 event.data.fd = listenfd;
 event.events = EPOLLIN | EPOLLET; //邊沿觸發
 epoll_ctl(epollfd, EPOLL_CTL_ADD, listenfd, &event);

根據前面的函數分析,這四句意思就是將監聽通訊端listenfd 加入關心的通訊端序列。

在epoll_wait 函數中的第二個參數,其實events.begin() 是個迭代器,但其具體實現也是struct epoll_event* 類型,雖然 &*events.begin() 得到的也是struct epoll_event* ,但不能直接使用events.begin() 做參數,因為類型不符,編譯會出錯。

EventList events(16); 即初始化容器的大小為16,當返回的事件個數nready 已經等於16時,需要增大容器的大小,使用events.resize 函數即可,容器可以動態增大,這也是我們使用c++實現的其中一個原因。

當監聽通訊端有可讀事件,accept 返回的conn也需要使用epoll_ctl 函數將其加入關心的通訊端隊列。

還需要調用 activate_nonblock(conn); 將conn 設定為非阻塞,man 7 epoll 裡有這樣一句話:

An application that employs the EPOLLET flag should use nonblocking file descriptors to avoid having a  blocking  read  or
 write  starve  a  task  that  is  handling multiple file descriptors.

當下次迴圈回來某個已串連通訊端有可讀事件,則讀取資料,若read 返回0表示對方關閉,需要使用epoll_ctl 函數將conn 從隊列中清除,我們使用 std::vector<int> clients; 來儲存每次accept 返回的conn,所以現在也需要將其擦除掉,調用clients.erase() 函數。


我們可以使用前面寫的conntest 用戶端程式測試一下,先運行伺服器程式,再運行用戶端,輸出如下:

simba@ubuntu:~/Documents/code/linux_programming/UNP/socket$ ./echoser_epoll

................................

count = 1015
ip=127.0.0.1 port=60492
count = 1016
ip=127.0.0.1 port=60493
count = 1017
ip=127.0.0.1 port=60494
count = 1018
ip=127.0.0.1 port=60495
count = 1019
accept: Too many open files


simba@ubuntu:~/Documents/code/linux_programming/UNP/socket$ ./conntest

.........................................................

count = 1015
ip=127.0.0.1 port=60492
count = 1016
ip=127.0.0.1 port=60493
count = 1017
ip=127.0.0.1 port=60494
count = 1018
ip=127.0.0.1 port=60495
count = 1019
connect: Connection reset by peer


為什麼伺服器端的count 只有1019呢,因為除去012,一個監聽通訊端還有一個epoll 執行個體控制代碼,所以1024 - 5 = 1019。

為什麼用戶端的錯誤提示跟這裡的不一樣呢?這正說明epoll 處理效率比poll和select 都高,因為處理得快,來一個串連就accept一個,當伺服器端accept 完第1019個串連,再次accept 時會因為檔案描述符總數超出限制,列印錯誤提示,而此時用戶端雖然已經建立了第1020個sock,但在connect
過程中發現對等方已經退出了,故列印錯誤提示,串連被對等方重設。如果伺服器端處理得慢的話,那麼用戶端會connect 成功1021個串連,然後在建立第1022個sock 的時候出錯,列印錯誤提示:socket: Too many open files,當然因為檔案描述符的限制,伺服器端也只能從已完成串連隊列中accept 成功1019個串連。


二、epoll與select、poll區別

1、相比於select與poll,epoll最大的好處在於它不會隨著監聽fd數目的增長而降低效率。核心中的select與poll的實現是採用輪詢來處理的,輪詢的fd數目越多,自然耗時越多。
2、epoll的實現是基於回調的,如果fd有期望的事件發生就通過回呼函數將其加入epoll就緒隊列中,也就是說它只關心“活躍”的fd,與fd數目無關。
3、核心 / 使用者空間 記憶體拷貝問題,如何讓核心把 fd訊息通知給使用者空間呢?在這個問題上select/poll採取了記憶體拷貝方法。而epoll採用了共用記憶體的方式。
4、epoll不僅會告訴應用程式有I/0 事件到來,還會告訴應用程式相關的資訊,這些資訊是應用程式填充的,因此根據這些資訊應用程式就能直接定位到事件,而不必遍曆整個fd集合。


三、epoll 的EPOLLLT (電平觸發,預設)和 EPOLLET(邊沿觸發)模式的區別

1、EPOLLLT:完全靠kernel epoll驅動,應用程式只需要處理從epoll_wait返回的fds,這些fds我們認為它們處於就緒狀態。此時epoll可以認為是更快速的poll。

2、EPOLLET:此模式下,系統僅僅通知應用程式哪些fds變成了就緒狀態,一旦fd變成就緒狀態,epoll將不再關注這個fd的任何狀態資訊,(從epoll隊列移除)直到應用程式通過讀寫操作(非阻塞)觸發EAGAIN狀態,epoll認為這個fd又變為空白閑狀態,那麼epoll又重新關注這個fd的狀態變化(重新加入epoll隊列)。隨著epoll_wait的返回,隊列中的fds是在減少的,所以在大並發的系統中,EPOLLET更有優勢,但是對程式員的要求也更高,因為有可能會出現資料讀取不完整的問題,舉例如下:

假設現在對方發送了2k的資料,而我們先讀取了1k,然後這時調用了epoll_wait,如果是邊沿觸發,那麼這個fd變成就緒狀態就會從epoll 隊列移除,很可能epoll_wait 會一直阻塞,忽略尚未讀取的1k資料,與此同時對方還在等待著我們發送一個回複ack,表示已經接收到資料;如果是電平觸發,那麼epoll_wait 還會檢測到可讀事件而返回,我們可以繼續讀取剩下的1k 資料。


參考:

《Linux C 編程一站式學習》

《TCP/IP詳解 卷一》

《UNP》

相關文章

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.