日韩av黄I国产麻豆传媒I国产91av视频在线观看I日韩一区二区三区在线看I美女国产在线I麻豆视频国产在线观看I成人黄色短片

歡迎訪問 生活随笔!

生活随笔

當前位置: 首頁 >

linux网络编程--select/poll/epoll 详解

發布時間:2024/2/28 39 豆豆
生活随笔 收集整理的這篇文章主要介紹了 linux网络编程--select/poll/epoll 详解 小編覺得挺不錯的,現在分享給大家,幫大家做個參考.

目錄

參考鏈接

epoll函數

close

epoll event

EL/LT

ET Edge Trigger 邊沿觸發工作模式

LT Level Trigger 水平觸發工作模式

epoll 源碼解析

一道騰訊后臺開發面試題

ET/LT 比較

epoll 優點

epoll 源碼解讀

select()

select()簡介

為什么需要select()?

select()函數

select 缺點

select() 文件描述符上限

poll()

fdarray

nfds

timeout

poll() 文件描述符上限

poll()/select()的區別


參考鏈接

  • epoll簡介及觸發模式(accept、read、send)
  • epoll內核源碼詳解+自己總結的流程
  • linux man page
  • epoll函數

    注意: epoll不屬于任何namespace。

    #include <sys/epoll.h>int epoll_create(int size); // return epollfd, 失敗return -1/* op: EPOLL_CTL_ADD EPOLL_CTL_MOD EPOLL_CTL_DEL 如果是delete的話, epoll_ctl的最后一個參數event可以是NULL */ int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event); // 成功return0, 失敗return -1 int epoll_wait(int epfd, struct epoll_event *events,int maxevents, int timeout); // 成功return nready. 失敗return -1//epoll_event /* 其實這個epoll_data只是給用戶自行使用的,epoll不關心里面的內容。 這個dta回隨著epoll_data 返回的epoll_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 */ };

    close

    其實在外面關閉一個fd之后,就可以不用再在epoll list里面刪除了,但是為了安全起見,還是用EPOLL_CTL_DEL刪掉吧。詳情可以看 epoll(7) man page FAQ。

    epoll event

  • EPOLLIN :表示對應的文件描述符可以讀(包括對端SOCKET正常關閉);
  • EPOLLOUT:表示對應的文件描述符可以寫;
  • EPOLLPRI:表示對應的文件描述符有緊急的數據可讀(這里應該表示有帶外數據到來);
  • EPOLLERR:表示對應的文件描述符發生錯誤;
  • EPOLLHUP:表示對應的文件描述符被掛斷;
  • EPOLLET: 將EPOLL設為邊緣觸發(Edge Triggered)模式,這是相對于水平觸發(Level Triggered)來說的。
  • EPOLLONESHOT:只監聽一次事件,當監聽完這次事件之后,如果還需要繼續監聽這個socket的話,需要再次把這個socket加入到EPOLL隊列里
  • EL/LT

    有關ET/LT 阻塞/非阻塞的操作,網絡上基本都是錯的,只要你安排的好,既可以用阻塞,也可以用非阻塞。(linux man page上也讓你用阻塞)

    ET Edge Trigger 邊沿觸發工作模式

  • 必須使用非阻塞 工作模式,因為在循環調用epoll_wait的時候,有可能某個句柄已知會ready, 如果用阻塞操作,會導致一個文件句柄的阻塞操作把多個文件描述符餓死。
  • 基于非阻塞文件句柄
  • 只有當read(2)或者write(2)返回EAGAIN時才需要掛起,等待(退出read/write返回epoll_wait)。但這并不是說每次read()時都需要循環讀,直到讀到產生一個EAGAIN才認為此次事件處理完成,當read()返回的讀到的數據長度小于請求的數據長度時(即小于sizeof(buf)),就可以確定此時緩沖中已沒有數據了,也就可以認為此事讀事件已處理完成。
  • 阻塞IO的事件處理原則:
  • recv() > 0:(并且小于請求的數據長度sizeof(buf)), 表示接收數據完畢,返回值即是接收到的字節數。
  • recv() == 0: 表示鏈接已經正常斷開,這個時候就可以把fd關掉,從epoll里面移除了
  • recv() < 0 && errno == EAGAIN: 表示recv操作還未完成
  • recv() < 0 && errno != EAGAIN: 表示操作遇到系統errno
  • 邊緣觸發但是這種模式下在讀數據的時候一定要注意,因為如果一次可寫事件我們沒有把數據讀完,如果沒有讀完,在socket沒有新的數據可讀時epoll就不回返回了,只有在新的數據到來時,我們才能讀取到上次沒有讀完的數據。最差的情況是client在發送的n個byte之后已經關閉了,但是epoll由于接收緩沖區沒有清空,這個fd在服務端并不會關掉。
  • 使用ET模式,就算接收緩沖區里的數據沒有讀完,如果再接收到新的數據, epoll_wait 還是會觸發可讀事件的。
  • 設置為EPOLLET之后仍然會對同一事件多次觸發的原因:
  • 接收緩沖區過小,無法容納所有發送過來的數據(這個我自己沒有復現出來)
  • 用EPOLL_CTL_MOD更改了epollevent,會重置之前的觸發
  • LT Level Trigger 水平觸發工作模式

  • poll(), select() 都是水平觸發
  • 如果我們用水平觸發不用擔心數據有沒有讀完因為下次epoll返回時,沒有讀完的socket依然會被返回
  • 但是要注意這種模式下的寫事件,因為是水平觸發,每次socket可寫時epoll都會返回,當我們寫的數據包過大時,一次寫不完,要多次才能寫完或者每次socket寫都寫一個很小的數據包時,每次寫都會被epoll檢測到,因此長期關注socket寫事件會無故cpu消耗過大甚至導致cpu跑滿,所以在水平觸發模式下我們一般不關注socket可寫事件而是通過調用socket write或者send api函數來寫socket
  • 但是如果使用LT模式,每次讀事件只要調用一次recv()就可以了。不用像ET一樣反復調用recv()直到返回EAGAIN,對于追求低延遲的系統調用來說,這么做是搞笑的,并且也不用擔心因為某個連接上數據量過大導致影響其他連接處理消息。
  • 我們可以看到這種模式在效率上是沒有邊緣觸發高的,因為每個socket讀或者寫可能被返回兩次甚至多次
  • epoll 源碼解析

    https://blog.csdn.net/wangyin159/article/details/48895287

    epoll_wait

  • 檢查MAXEXENT參數
  • 用access_ok() 檢查event指針是否可寫,如果這個指針是空指針或者指向內核態的指針,那么會設置errno EFAULT。
  • Just because a pointer was supplied by userspace doesn't mean that it's definitely a userspace pointer - in many cases "kernel pointer" simply means that it's pointing within a particular region of the virtual address space.https://stackoverflow.com/questions/12357752/what-is-the-point-of-using-the-linux-macro-access-ok
  • 獲取epfd對應的eventpoll文件實例,如果取不到,errno:EBADF
  • 檢查eventpoll文件是不是真的是一個epoll文件, 如果不是說值errno EINVAL
  • 其實epoll_wait 中如果出錯了,那么基本上應該是程序本身的問題,比如陷入死循環之類
  • 調用ep_epoll函數,這個函數在做一些配置之后就會主動讓出處理器,進入睡眠狀態,等待文件就緒(回調函數喚醒本進程)或者超時或者信號中斷
    • 缺省的工作模式

    一道騰訊后臺開發面試題

    Q:使用Linux?epoll模型,水平(LT)觸發模式,當socket可寫時,會不停的觸發socket可寫的事件,如何處理?

  • 第一種最普遍的方式:
    • 需要向socket寫數據的時候才把socket加入epoll,等待可寫事件。接受到可寫事件后,調用write或者send發送數據。當所有數據都寫完后,把socket移出epoll(用EPOLLONESHOT也行)。
    • 這種方式的缺點是,即使發送很少的數據,也要把socket加入epoll,寫完后在移出epoll,有一定操作代價。
  • 一種改進的方式:
    • 開始不把socket加入epoll,需要向socket寫數據的時候,直接調用write或者send發送數據。如果返回EAGAIN(緩沖區滿了,后面還需要繼續發),把socket加入epoll,在epoll的驅動下寫數據,全部數據發送完畢后,再移出epoll。
    • 這種方式的優點是:數據不多的時候可以避免epoll的事件處理,提高效率。
  • ET/LT 比較

  • 因為ET要基于非阻塞IO, LT在讀寫的時候不必等待EAGAIN的出現,可以節省系統調用次數,降低延遲
  • ET可以保證每次只觸發一個
  • epoll 優點

  • 對應select()的缺點, epoll都有解決的方法

  • 每次調用select,都需要把fd集合從用戶態拷貝到內核態,這個開銷在fd很多時會很大
    • 使用epoll_ctl()函數,只有在注冊、修改、刪除的時候才會對內核進行操作。
  • 同時每次調用select都需要在內核遍歷傳遞進來的所有fd,這個開銷在fd很多時也很大
    • epoll的解決方案不像select或poll一樣每次都把current輪流加入fd對應的設備等待隊列中,而只在epoll_ctl時把current掛一遍(這一遍必不可少)并為每個fd指定一個回調函數,當設備就緒,喚醒等待隊列上的等待者時,就會調用這個回調函數,而這個回調函數會把就緒的fd加入一個就緒鏈表)。epoll_wait的工作實際上就是在這個就緒鏈表中查看有沒有就緒的fd(利用schedule_timeout()實現睡一會,判斷一會的效果,和select實現中的第7步是類似的)
  • select支持的文件描述符數量太小了,默認是1024
    • epoll沒有這個限制,它所支持的FD上限是最大可以打開文件的數目,這個數字一般遠大于2048,舉個例子,我的1GB內存阿里云ECS是999999,具體數目可以cat /proc/sys/fs/file-max察看,一般來說這個數目和系統內存關系很大。
  • poll每次返回整個文件描述符數組, 用戶需要遍歷數組已找到哪些文件描述符上有IO事件。 而epoll_wait(2)返回的是活動fd的列表,需要遍歷的數組通常會小很多,在并發連接數較大而活動連接比例不高時,epoll(4)比epoll(2)更高效。

  • epoll 源碼解讀

    當某一進程調用epoll_create方法時,Linux內核會創建一個eventpoll結構體,這個結構體中有兩個成員與epoll的使用方式密切相關。eventpoll結構體如下所示:

    struct eventpoll{..../*紅黑樹的根節點,這顆樹中存儲著所有添加到epoll中的需要監控的事件*/struct rb_root rbr;/*雙鏈表中則存放著將要通過epoll_wait返回給用戶的滿足條件的事件*/struct list_head rdlist;....};

    每一個epoll對象都有一個獨立的eventpoll結構體,用于存放通過epoll_ctl方法向epoll對象中添加進來的事件。這些事件都會掛載在紅黑樹中,如此,重復添加的事件就可以通過紅黑樹而高效的識別出來(紅黑樹的插入時間效率是lgn,其中n為樹的高度)。

    而所有添加到epoll中的事件都會與設備(網卡)驅動程序建立回調關系,也就是說,當相應的事件發生時會調用這個回調方法。這個回調方法在內核中叫ep_poll_callback,它會將發生的事件添加到rdlist雙鏈表中。

    struct epitem{struct rb_node rbn;//紅黑樹節點struct list_head rdllink;//雙向鏈表節點struct epoll_filefd ffd; //事件句柄信息struct eventpoll *ep; //指向其所屬的eventpoll對象struct epoll_event event; //期待發生的事件類型}

    當調用epoll_wait檢查是否有事件發生時,只需要檢查eventpoll對象中的rdlist雙鏈表中是否有epitem元素即可。如果rdlist不為空,則把發生的事件復制到用戶態,同時將事件數量返回給用戶。

    select()

    select()簡介

  • select()函數是阻塞的, 只有某些端口狀態轉換了或者達到timeout才會返回
  • 該函數可以允許進程指示等待多個事件中任何一個的發生
  • select(), poll() 都是水平觸發
  • 為什么需要select()?

  • 多路復用io mutiplexing
  • 如果不采用多路復用,要么使用阻塞IO(會使線程長時間處于阻塞狀態,無法執行任何計算或者響應任何網絡請求),要么使用非阻塞IO:(要用while循環調用recv函數,大幅占用CPU資源), 復用的優勢在于可以同時處理多個連接
  • select()函數

    #include <sys/select.h> #include <sys/time.h>int select(int maxfdp1,fd_set *readset, fd_set *writeset, fd_set *exceptset, const struct timeval *timeout);

    返回: 若有描述符就緒,則返回就緒描述符的數量,若超時則為0, 若出錯則為1

    1. timeout-->timeval

    struct timeval {long tv_sec; // secondslog tv_usec; // microseconds }
    • 用于指定timeout的秒數和微秒數
    • 如果輸入為0,那么select函數會一直等下去一直到某個描述符準備好
    • 如果輸入這個參數,那么最長等待時間就確定了
    • 如果輸入這個結構,但是其中的兩個值為0,那么就不等待-->輪詢機制

    延伸 gettimeofday()

    • 用gettimeofday() 可以獲得微秒(us)級別的時間。
    • 會把目前的時間tv所指的結構返回,當地時區的信息則放到tz所指的結構中。
    • 1970年1月1日到現在的時間
    • 調用兩次gettimeofday(), 前后做減法,從而達到計算時間的目的。
    #include <sys/time.h> int gettimeofday(struct timeval *tv,struct timezone *tz);

    2. readset, writeset, exceptset

    #include <sys/select.h>struct fd_set myset; //四個相關的宏函數void FD_ZERO(fd_set *fdset); // clean all bits at fdset void FD_SET(int fd, fd_set *fdset); // turn on the bit for fd in fdset void FD_CLR(int fd, fd_set *fdset); // turn on the bit for fd in fdset void FD_ISSET(int fd, fd_set *fdset); //is the bit for fd on in fdset? 如果set了,返回1
  • fd_set 每一位表示一個fd, set其中的某一位就表示要監視某個fd.
  • 指針輸入, 輸入的時候把我們所關心的fd置為1. 返回時,他將指示哪些描述符已經就緒了。因此,每次重新調用select時,我們都需要再次把所有我們關心的描述符置為1。
  • 3. maxfdp1

  • maxfdp1 = 最大描述符+1
  • 最大描述符系統內是有定義的 FD_SETSIZE
  • 例子

    select\strcliselect01.c

    void str_cli(FILE *fp, int sockfd) {int maxfdp1;fd_set rset;char sendline[MAXLINE], recvline[MAXLINE];FD_ZERO(&rset);for ( ; ; ) {FD_SET(fileno(fp), &rset);FD_SET(sockfd, &rset);maxfdp1 = max(fileno(fp), sockfd) + 1;Select(maxfdp1, &rset, NULL, NULL, NULL);if (FD_ISSET(sockfd, &rset)) { /* socket is readable */if (Readline(sockfd, recvline, MAXLINE) == 0)err_quit("str_cli: server terminated prematurely");Fputs(recvline, stdout);}if (FD_ISSET(fileno(fp), &rset)) { /* input is readable */if (Fgets(sendline, MAXLINE, fp) == NULL)return; /* all done */Writen(sockfd, sendline, strlen(sendline));}} }

    select 缺點

  • 每次調用select,都需要把fd集合從用戶態拷貝到內核態,這個開銷在fd很多時會很大
  • 同時每次調用select都需要在內核遍歷傳遞進來的所有fd,這個開銷在fd很多時也很大
  • select支持的文件描述符數量太小了,默認是1024
  • select() 文件描述符上限

    這個問題的關鍵其實要先理解select關于文件描述符上限的原因

  • linux系統本身就有文件描述符上限,文件描述符的建立會連帶建立很多其它表項,具體可以搜索文件描述符的詳解,也就是說文件描述符一定會占用資源,那在有限的硬件條件下,文件描述符必定會有上限,我在ubuntu14.04的ECS里通過
  • cat /proc/sys/fs/file-max //結果99999
  • 進程文件描述符上限user limit中nofile的soft limit,實際上這是單個用戶的文件描述符上限,通過
  • ulimit -n //結果65535

    soft limit可以修改,但是不能超過hard limit

    ulimit -Hn //結果65535
  • select函數本身限制,主要是頭文件中FD_SETSIZE的大小,一般來說是1024,這就限定了select函數中的文件描述符上限,當然可以做修改,但是需要重新編譯內核,而且效果由于select的實現機制,會比較差
  • poll()

    #include <poll.h> #include <limits.h> /* for OPEN_MAX */ // 描述了poll的最大數量int poll(struct pollfd *fdarray, unsigned long nfds, int timeout);

    返回: 若有描述符就緒,則返回就緒描述符的數量,若超時則為0, 若出錯則為1

    fdarray

    指向一個數組結構第一個元素的指針,沒一個元素都是一個pollfd結構,使用這個結構,避免了select中使用一個參數既表示我們關心的值,又表示結果。

    struct pollfd {int fd; // 描述符short events; // 我們關心的狀態short revents; // 返回的結果 }

    nfds

    第一個參數中的數組元素的個數

    timeout

    timeout說明
    INFTIM永遠等待
    0立即返回,不阻塞進程
    >?0等待指定的毫秒

    poll() 文件描述符上限

    poll雖然不像select一樣受到select() 中FD_SETSIZE 的限制,但是仍然受到ulimit中設定的一個進程所能打開的最大文件描述符的限制

    ulimit -n //結果65535

    poll()/select()的區別

  • poll() 解決了select文件描述符最大只有1024的限制
  • select和poll都需要自己不斷輪詢所有fd集合,直到設備就緒,(首先把所有的fd掛到對應的等待隊列上,然后睡眠,在設備收到一條消息或者填寫完文件數據之后,會喚醒設備等待隊列上的進程,進程會再次掃描整個注冊文件描述符的集合,并返回就緒文件描述符的數目給用戶)期間可能要睡眠和喚醒多次交替(存疑),雖然epoll也需要喚醒,但是喚醒之后只需要檢測就緒鏈表是否為空就行了。

  • 本篇文章由一文多發平臺ArtiPub自動發布

    總結

    以上是生活随笔為你收集整理的linux网络编程--select/poll/epoll 详解的全部內容,希望文章能夠幫你解決所遇到的問題。

    如果覺得生活随笔網站內容還不錯,歡迎將生活随笔推薦給好友。