epoll/poll/epoll & 高級IO詳解

五種IO模型

阻塞IO

  • 阻塞IO:在內核將數據準備好之前,系統調用會一直等待,所有的套接字,默認都是阻塞方式

非阻塞IO

  • 非阻塞IO:如果內核的還未將數據準備好,系統調用仍然會直接返回,並且返回EWOULDBLOCK錯誤碼
  • 非阻塞IO往往需要程序員循環的方式反覆嘗試讀寫文件描述符,這個過程稱爲輪詢,這對CPU來說是較大的浪費,只有特定的場景下才使用。

信號驅動

  • 信號驅動IO:內核將數據準備好的時候,使用SIGIO信號通知應用程序進行IO操作

IO多路轉接

  • IO多路轉接:雖然從流程圖上看起來和阻塞IO類似,實際上最核心在於IO多路轉接能夠同時等待多個文件描述符的就緒狀態

異步IO

  • 異步IO:由內核在數據拷貝完成時,通知應用程序(而信號驅動是告訴應用程序何時可以開始拷貝數據),不需要自己主動獲取
  • 任何IO過程,都包含兩個步驟,第一是等待,第二是拷貝,而且在實際的應用場景中,等待消耗的時間玩往往高於拷貝的時間,讓IO更高效,最核心的辦法就是讓等待的時間儘量少

釣魚

  • 阻塞
  • 非阻塞:釣魚的時候玩玩手機,再看看魚
  • 信號:魚缸上綁鈴鐺,鈴鐺響了了
  • IO多路轉接:阻塞的,紮了一排魚杆,一眼不眨的等魚
  • 異步IO:魚竿是自動的,可以自己釣魚,魚釣上來就通知

高級IO的重要概念

同步通信和異步通信

同步和異步關注的是消息通信機制

  • 所謂同步,就是在發出一個調用時,在沒有得到結果之前,該調用就不反悔,但是一旦返回,就得到了返回值了,換句話說,就是由調用者主動等待這個調用過程
  • 異步則相反,調用在出發之後,這個調用就直接返回了,所以沒有返回結果,換句話說,當一個異步過程調用出發後,調用者不會立刻得到結果,而是在調用出發後,被調用者通過狀態,通知調用者,或通過回調函數處理這個調用
  • 這裏的同步和多線程的同步和互斥中的同步不是一個東西,不要混淆
  • 例如epoll的回調函數機制,通過回調函數來直接修改數組中的標記,調用epoll不需要返回結果,這個場景就是異步。

例如

假設你去吃飯,點了一個炒菜
(1)同步就是你點飯後,一直在那裏等着廚師給你做,直到廚師做完,你自己把飯端走,即就是說結果由你自己主動獲取
(2)異步,就是點飯之後,去找一個位置作者,當服務員告訴你的飯做好了,你去把飯端過來,即就是說,結果由服務員通知你,你自己使被動的得知調用結果,如果服務員不通知,你是不知道結果的
  • 在於這個結果你是怎麼知道的
  • 是主動的還是被動的知道的

阻塞和非阻塞

阻塞和非阻塞關注的是程序在等待調用結果(消息,返回值)時的狀態

  • 阻塞調用是指調用結果返回之前,當前線程會被掛起,調用線程只有在得到結果之後纔會返回
  • 非阻塞調用指在不能立刻得到結果之前,該調用不會阻塞當前線程
  • 關注調用函數的時候是否阻塞

同步阻塞

  • 調用結果由調用者主動獲取,且是阻塞式的等待,即調用者會主動的關注調用結果,一直在那看着

同步非阻塞

  • 調用結果是由調用者主動獲取,但是是非阻塞的等着,輪詢的方式,時不時回來看一下執行的結果。

異步阻塞

  • 調用結果是被調用者通知給調用者的,而且是阻塞式的在哪裏等着,並且自己不光等着,也不會看執行得怎麼樣了,而是等待被調用者的通知

異步非阻塞I

  • 調用結果是由被調用者通知給調用者就行了,調用者自己可以去幹其他事,不需要主動的獲取結果,同時自己也不會被阻塞。

等女朋友的例子:

同步阻塞

  • 等女朋友,單純的等,啥都不幹,就一直看着她有沒有做完事
    同步非阻塞
  • 等女朋友的時候還一般等一邊打遊戲,打會遊戲然後看看她做完事沒
    異步阻塞
  • 等女朋友,然後什麼也不做,就一直等,但是也不會關注她什麼時候收拾好,等她收拾好了會主動通知你,然後你就知道她收拾好了
    異步非阻塞
  • 等女朋友,然後你還邊敲代碼,你也不用去關注她收拾好沒,她會主動通知你,然後你就知道她完事了

fcntl

  • 一個文件描述符,默認都是阻塞IO,fcntl有能力讓阻塞變成阻塞
函數原型
#include <unistd>
#include <fcntl.h>
int fcntl(int fd,int cmd,.../*arg*/);

根據傳入的cmd值不同,後面追加的參數也不相同
,fcntl函數有五種功能

  • 複製一個現有的文件描述符 (cmd = F_DUPFD)
  • 獲得/設置文件描述符標記 (cmd = F_GETFD 或 F_SETFD)
  • 獲得/設置文件狀態標記 (cmd = F_GETFL或 F_SETEL)
  • 獲得/設置異步I/O所有權 (cmd = F_GETOWN或F_SETOWN)
  • 獲得/設置記錄鎖 (cmd = F_GETLK,F_SETLK 或 F_SETLKW)

我們此處只是用三種功能,獲取/設置文件狀態標記,就可以將一個文件描述符設置爲非阻塞

基於fcntl實現一個SetNoBlock函數
//傳入文件描述符
void SetNoBlock(int fd)
{ 
  //保存修改前的狀態信息(一個位圖)
  int f1 = fcntl(fd,F_GETFL);//get file
  if(f1<0)
  {
    cerr<<"fcntl"<<endl;
    return;
  }
  //將文件描述符設置爲非阻塞
  fcntl(fd,F_SETFL,f1|O_NONBLOCK);//set file
}

select

  • select系統調用是讓我們的程序監控多個文件描述符的狀態變化的
  • 程序會停在select這裏等待,直到被監視的文件描述符有一個或多個發生了狀態改變

select函數原型

#include <sys/select.h>
int select(int nfds,fd_Set *readfds,fd_set * writefds,fd_set* exceptfds,struct timeval * timeout);

參數解釋

  • 參數nfds是需要監視的最大的文件描述符值+1
  • rdset,wrset,exset分別對於於需要檢測的可讀文件描述符的集合,可寫文件描述符的集合以及異常文件描述符集合
  • 參數timeout爲解僱timeval,用來設置select的等待時間

參數timeout取值

  • NULL表示select沒有timeout,一直將阻塞,直到有某個文件描述符上發生了事件
  • 0 僅檢測文件描述符的集合狀態,然後立即返回,並不等待外部時間的發生
  • 特定的是時間值,如果在指定時間沒有事件發生,select將超時返回

fd_set的結構

  • 其實fd_set的結構就是一個整數數組,更嚴格的說,就是一個"位圖",使用位圖中對應的位來表示要監視的文件描述符
  • 提供一組操作fd_set的接口,來比較方便的操作位圖
void FD_CLR(int fd ,set* set);          //用來清除描述詞組set中相關fd的位       
int FD_ISSET(int fd,fd_set* set);       //用來測試描述詞組set中相關的fd的位是否爲真
void FD_SET(int fd,fd_set* set);        //用來設置描述詞組set中相關fd的位
void FD_ZERO(fd_set* set)               //用來清除描述詞組set的全部位

關於timeval結構

timeval結構用於描述一段時間長度,如果在這個時間內,需要監視的描述符沒有事件發生則函數返回,返回值爲0

struct timeval
{
    __time_t tv_set;//這裏設置秒
    __suseconds_t tv_usec;//這裏設置微妙
};

函數返回值

  • 執行成功則返回文件描述詞狀態已改變的個數
  • 如果返回0代表文件描述符狀態改變已經超過timeout時間,沒有返回
  • 當有錯誤發生時則返回-1,錯誤原因存在errno,此時參數readfds,writefds,exceptfds和timeout的值變成不可預測

錯誤值可能

  • EBADF 文件描述符爲無效的或該文件已經寡女i
  • EINTR 此調用被信號所中斷
  • EINVAL 參數n爲負值
  • ENOMEN 核心內存不足

select用法

  • 自己建立一個集合
  • 可以監控三種集合,read ,write except(異常事件)
  • 一般是使用read集合
  • 監視多個文件描述符的變化
  • select自己是阻塞的,直到被監視的文件描述符有一個或多個發生了狀態改變

select程序流程

實現多個客戶端的連接和數據處理(聊天)
1.新連接的描述符會被覆蓋,因此需要一個數組保存
2.將監聽socket描述符添加到數組中
3.定義一個select可讀事件描述符集合
4.將數組中可用的描述符全都添加到集合中,並選擇出最大的描述符
5.定義一個select等待的超時事件
6.select開始監控描述符的狀態改變
    1)select出錯返回
    2)select超時返回
    3)代表有描述符可讀,但是我們不知道哪一個描述符可讀,但是select返回之前幹了一件事,將沒有就緒的描述符從集合中移除,意味着現在集合中存在的描述符都是就緒描述符
    4)現在判斷數組中的哪一個描述符還繼續在集合中,如果在,就代表這個描述符是就緒狀態的
        1))如果這個就緒的描述符是監聽描述符,代表有新連接,接受鏈接
        2))如果不是,代表連接有數據到來,接受數據

關於socket的就緒條件

###讀就緒

  • socket內核中,接收緩衝區中的字節數,大於等於低水位標記SO_RCVLOWAT,此時可以無阻塞的讀寫該文件描述符,並且返回值大於0
  • socket TCP通信中,對端關閉連接,此時對該socket讀,則返回0
  • 監聽的socket上有新的連接請求
  • socket上有未處理的錯誤

寫就緒

  • socket內核中,發送緩衝區中的可用字節大於等於低水位標記,SO_SNDLOWAT,此時可以無阻塞的寫,並且返回值大於0
  • socket的寫操作被關閉,對於一個寫操作被關閉的socket進行寫操作,會觸發SIGPIPE信號
  • socket使用非阻塞connect連接成功或失敗之後
  • socket上有未讀取的錯誤

異常就緒

  • socket上收到帶外數據,關於帶外數據,和TCP緊急模式相關,回憶TCP協議頭中,有一個緊急指針的字段

select缺點

  • 0.select能夠監控的最大描述符個數是有限的
  • 1.每次都需要將監控的描述符從用戶態中拷貝到內核態
    ,效率下降
  • 2.select並不會直接告訴我們哪一個描述符就緒了,而是將未就緒的描述符從集合中移除,因此需要遍歷所有的描述符就是否在集合中,才知道到底哪一個就緒,需要遍歷,描述符太多就會效率下降
  • 3.因爲select每次都會修改監控集合的內容,因此每次調用select之前需要重新添加描述符到集合中
  • 4.因爲以上的原因,因此代碼編寫稍顯複雜

select優點

  • 跨平臺,windows下也有select
  • 多路複用和多線程/多進程對比
  • 在資源足夠的欠款下,多線程多進程可以並行,但是多路複用同一時間僅能接受一個請求,只能併發
  • 但是多路複用省資源
  • 多路複用技術用於有大量連接,但是同一時間只有少量活躍連接的情況

I/O多路轉接之poll

  • 相比select就是沒有最大描述符上限
  • 備註: fd_set的大小可以調整,可能涉及到重新編譯內核. 感興趣的同學可以自己去收集相關資料

poll函數接口

#include <poll.h>
int poll(struct pollfd* fds,nfds_t nfds,int timeout);

struct pollfd
{
    int fd;
    short events;
    short revents;
}

示例:
int ret = poll(events,10,1000);
//events是一個pollfd數組,10是代表數組的長度,1000表示超時時間是1000毫秒
  • fds是一個poll函數監聽的結構列表,每個元素中,包含了三部分內容:文件描述符,監聽的事件集合,返回的事件集合
  • nfds表示fds數組的長度
  • timeout表示poll函數的超時時間,單位是毫秒

events和revents的取值

事件          描述                            是否可作爲輸入  是否可作爲輸出
POLLIN        數據                               是               是
POLLRDNORM    普通數據可讀                       是               是
POLLRDBAND    優先級帶數據可讀                   是               是
POLLPRI       高優先級數據可讀,比如TCP外帶數據  是               是
POLLOUT       數據,可寫                         是               是
POLLWANORM    普通數據可寫                       是               是
POLLRDHUP     TCP連接被對方關閉,或者對方關閉    是               是
POLLERR       錯誤                               否               是
POLLHUP       掛起                               否               是
POLLNVAL      文件描述符沒有打開                 否               是

返回結果

  • 返回值小於0,表示出錯
  • 返回值等於0,表示poll函數等待超時
  • 返回值大於0,表示poll由於監聽的文件描述符就緒而返回

poll優點

  • pollfd結構包含了要監視的event和發生的event,不再使用select"參數-值"傳遞的方式,接口使用比select方便
  • poll並沒有最大數量限制 (但是=數量過大後性能也是會下降)

poll缺點

  • poll中監聽的文件描述符數目增多時
  • 和select函數一樣,poll返回後,需要輪詢pollfd來獲取就緒的描述符
  • 每次調用poll都需要把大量的pollfd結構從用戶態拷貝到內核中
  • 同時連接的大量客戶端在一時刻可能只有很少的處於就緒狀態,因此隨着監視的描述符數量增長,其效率也會線性下降

使用poll監控標準輸入

#include <poll.h>
#include <unistd.h>
#include <stdio.h>


int main()
{
  struct pollfd poll_fd;
  poll_fd.fd =0;
  poll_fd.events=POLLIN;//表示監聽的是輸入時間

  for(;;)
  {
    int ret = poll (&poll_fd,1,1000);//超時時間是1000毫秒
    if(ret< 0)
    {
      perror("poll");
      continue;
    }
    if(ret == 0)
    {
      printf("poll timeout\n");
    }
    if(poll_fd.revents==POLLIN)
    {
      char buf[1024] = {0};
        read(0,buf,sizeof(buf)-1);
        printf("stdin:%s",buf);
    }
  }
  return 0;
}

epoll

  • epoll是poll的改進版本
  • 它幾乎解決了select和poll的所有缺點,是Linux2.6下性能最好的多路I/O就緒通知方法

epoll系統調用

epoll_create & epoll_ctl

int epoll_create(int size)
//創建一個epoll的句柄
//自從Linux2.6.8之後,size參數是被忽略的也就是說,這個size在這個版本後是什麼值不重要了,內部不會因爲這個值限制監聽描述符的數量
// 用完之後,必須調用close關閉,避免資源的浪費

int epoll_ctl(int epfd,int op,int fd, struct epoll_event* event);
// epoll的事件註冊函數
// 它不同於select是在監聽事件告訴內核要監聽什麼類型的時間,而是在這裏先註冊要監聽的事件的類型
// epfd 是epoll_create的返回值,epoll的句柄
// op 表示動作,用三個宏表示   
// 三個宏(EPOLL_CTL_ADD 註冊新的fd)  (EPOLL_CTL_MOD 修改已經註冊的fd的監聽事件) (EPOLL_CTL_DEL 從epfd中刪除一個fd)
//  fd,是需要監聽的fd 
// event 是告訴內核需要監聽什麼事

epoll_event結構

image
image

centos7中
image

event的集合

  • EPOLLIN 表示對應的文件描述符可以讀
  • EPOLLIN 表示對應的文件描述符可以寫
  • EPOLLPRI 表示對應的文件描述符有緊急的數據可讀
  • EPOLLERR 表示對應的文件描述符發生錯誤
  • EPOLLHUP 表示對應的文件描述符被掛斷
  • EPOLLET 將EPOLL設爲邊緣觸發模式,這是相對於水平觸發來說的
  • EPOLLONESHOT 只監聽一次時間,當監聽完事這次事件後,如果還需要繼續監聽的話,需要再次把這個socket假如到EPOLL隊列裏

epoll_wait

int epoll_wait(int epfd,struct epoll_event* events,int maxevents ,int timeout);

監控到有就緒文件後

  • 參數events是分配好的epoll_event結構體數組
  • epoll將會把發生的事件複製到events數組中 (evens不可以是空指正,內核只負責把數據複製到這個events數組中,不會去幫助我們在用戶態中分配內存)
  • maxevents告知內核我們的events有多大,這個maxevnets的值不能大於創建epoll_create時的size
  • 參數timeout是超時時間 (毫秒,0會立即返回,-1是永久阻塞)
  • 如果調用函數成功,返回對應I/O上已準備好的文件描述符數目,如返回0表示已超時,返回小於0表示函數失敗

epoll工作原理

-== 當某一進程調用epoll_create方法時,Linux內核會創建一個eventpoll結構體(管理兩個結構)==,這個結構體中有兩個成員與epoll的使用方式密切相關

struct eventpoll
{
    /* 紅黑樹的根節點,這棵樹中儲存着所有添加到epoll中的需要監聽的事件
    插入的時候也不用擔心有重複的問題了,因爲紅黑樹本身就有查重的功能*/
    struct rb_root rbr;
    /*
    /*雙鏈表中則存放着將要通過epoll_wait返回給用戶的滿足條件的事件
    epoll_wait調用成功內核就將雙鏈表中的數據拷貝到用戶給的緩衝區中(events數組)*/
    struct list_head rdlist;
}
  • 每個epoll對象都有一個獨立的eventpoll結構體,用於存放通過epoll_ctl方法向epoll對象添加進來的時間
  • 這些事件都會掛載在紅黑樹中,如此,重複添加的時間就可以通過紅黑樹而高效的識別出來(紅黑樹的插入時間效率是lgn,其中n爲樹的高度)
  • 所有添加到epoll中的時間都會與設備驅動程序建立回調關係,也就是薯片,當響應的時間發生時會調用或者回調方法
  • 這個回調方法在內核中叫做ep_poll_callback,它會將發生的事件添加到rdlist雙鏈表中
  • 在epoll中,對於每一個事件,都會建立一個eptiem結構體(有兩個結構的兩個節點),紅黑樹的每個節點都是基於epitem結構體中的rbn成員(紅黑樹的節點)
  • image

epitem

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雙鏈表中是否有eptiem的元素即可
  • 如果rdlist不爲空,則把發生的時間複製到用戶態,同時將事件數量返回給用戶,這個操作的時間複雜度是O(1)

epoll的使用三步

  • 調用epoll_create創建一個epoll句柄
  • 調用epoll_ctl,將要監控的文件描述符進行註冊
  • 調用epoll_wait,等待文件描述符就緒

epoll的優點 (和select的缺點對應)

  • 接口方便使用,雖然拆分成了三個函數,但是反而使用起來更加的方便,不需要每次循環都設置關注的文件描述符,也做到了輸入輸出參數分離開
  • 數據拷貝少:只需要在合適的時候調用 EPOLL_CTL_ADD 將文件描述符拷貝到內核中,這個操作並不頻繁 (而select/poll每次都需要循環的拷貝)
  • 事件回調機制,避免使用遍歷主動去知道,而是使用回調函數的方法,將就緒的文件描述符結構加入到就緒隊列,epoll_wait返回直接訪問就緒隊列就知道哪些文件描述符就緒,這個操作時間複雜度O(1),即使文件描述符數目很多,效率也不會收到影響

容易認識的誤區

有人說,epoll使用的是內存映射機制

  • 內存映射機制是內核直接將就緒隊列通過mmap的方式映射到用戶態,避免了拷貝內存這樣額外性能開銷
    這種說法不準確,我們定義的是 struct epoll_event是我們在用戶空間中分配好的內存,勢必還是需要將內核的數據拷貝到這個用戶空間的內存中的,這個拷貝過程還是存在的

epoll的工作方式

假設爲1代表有數據

水平觸發:

  • 只要滿足觸發條件,就會提醒 爲1狀態就觸發

邊沿觸發

  • 每次新數據到來,僅提醒一次 因此需要每次將緩衝數據全部讀取,因此需要每次將緩衝區的數據全部讀取,因爲全部讀取的話,一不小心就會導致recv阻塞,所以需要將socket描述符屬性設置爲非阻塞 從0跳到1才觸發,並且觸發處理後不管有沒有數據又回到0狀態

水平觸發Level Triggered工作模式

epoll 默認狀態下就是LT工作模式

  • 當epoll檢測到socket上事件就緒的時候,可以不立刻進行處理,或者只處理一部分
  • 如上面的例子,由於只讀了1K數據,緩衝區中還剩下1K數據,在第二次調用epoll_wait時,epoll_wait依然會立刻返回並通知socket讀時間就行
  • 知道緩衝區上所有的數據都被處理完,epoll_wait纔不會立刻返回
  • 支持阻塞讀寫和非阻塞讀寫

邊緣觸發 Edge Triggered工作模式

如果我們在第1步講socket添加到epoll描述符的時候使用EPOLLET標誌,epoll進入ET工作模式

  • 檢測到就緒事件,立刻處理
  • 如果沒讀完,下一次不會再通知,必須等新的就緒通知
  • ET模式下,文件描述符上事件就緒後,只有一次處理機會
  • ET性能比LT性能更搞 (epoll_wait返回次數國家少),Nginx默認採用ET模式使用epoll
  • 只支持非阻塞讀寫 (如果是阻塞可能讀不完,就算讀完了也可能導致阻塞情況(剛好讀完))

select和poll其實也是工作在LT模式下,epoll既可以支持LT,也可以支持ET

對比LT和ET

  • LT是epoll的默認行爲,使用ET能夠減少epoll觸發的次數,但是代價就是強逼着程序員一次響應就緒過程中就把所有數據都處理完
  • 相當於一個文件描述符就緒後,不會反覆被提示就緒,看起來就比LT更高效一些,但是在LT情況下如果也能做到每次就緒的文件描述符都立刻處理,不讓這個就緒被重複提示的話,其實性能也是一樣的。
  • ET的代碼也更加的複雜

理解ET模式和非阻塞文件描述符

  • 使用 ET 模式的 epoll, 需要將文件描述設置爲非阻塞. 這個不是接口上的要求, 而是 “工程實踐” 上的要求.
    假設這樣的場景: 服務器接受到一個10k的請求, 會向客戶端返回一個應答數據. 如果客戶端收不到應答, 不會發送第
    二個10k請求
  • 如果服務端寫的代碼是阻塞式的read, 並且一次只 read 1k 數據的話(read不能保證一次就把所有的數據都讀出來,
    參考 man 手冊的說明, 可能被信號打斷), 剩下的9k數據就會待在緩衝區中
  • 此時由於 epoll 是ET模式, 並不會認爲文件描述符讀就緒. epoll_wait 就不會再次返回. 剩下的 9k 數據會一直在緩
    衝區中. 直到下一次客戶端再給服務器寫數據. epoll_wait 才能返回

阻塞導致的問題

  • 服務器只讀到1k個數據, 要10k讀完纔會給客戶端返回響應數據.
  • 客戶端要讀到服務器的響應, 纔會發送下一個請求
  • 客戶端發送了下一個請求, epoll_wait 纔會返回, 才能去讀緩衝區中剩餘的數據.

解決

  • 所以, 爲了解決上述問題(阻塞read不一定能一下把完整的請求讀完), 於是就可以使用非阻塞輪訓的方式來讀緩衝區,
    保證一定能把完整的請求都讀出來.

epoll的使用場景 (高併發)

epoll得到高性能,是有一定的特定場景的,如果場景選擇不適宜,epoll的性能可能適得其反

  • 對於多連接,且多連接中只有一部分連接比較活躍時,比較適合使用epoll
    典型的需要除了處理上萬個客戶端的服務器,例如各種互聯網APP的入口服務器,這樣的服務器就很適合epoll,如果只是系統內部,服務器和服務器之間通信,只有少數的幾個連接,這種情況下epoll就不適合

epoll驚羣問題

產生原因

在多線程或者多進程環境下,有些人爲了提高程序的穩定性,往往會讓多個線程或者多個進程同時在epoll_wait監聽的socket描述符。當一個新的鏈接請求進來時,操作系統不知道選派那個線程或者進程處理此事件,則乾脆將其中幾個線程或者進程給喚醒,而實際上只有其中一個進程或者線程能夠成功處理accept事件,其他線程都將失敗,且errno錯誤碼爲EAGAIN。這種現象稱爲驚羣效應,結果是肯定的,驚羣效應肯定會帶來資源的消耗和性能的影響。
那麼如何解決這個問題

多線程下的解決方法 (不然多個線程epoll_wait)

這種情況,不建議讓多個線程同時在epoll_wait監聽的socket,而是讓其中一個線程epoll_wait監聽的socket,當有新的鏈接請求進來之後,由epoll_wait的線程調用accept,建立新的連接,然後交給其他工作線程處理後續的數據讀寫請求,這樣就可以避免了由於多線程環境下的epoll_wait驚羣效應問題。

多進程 (捕獲EAGAIN無視驚羣 | 互斥鎖+負載均衡技術)

  • lighttpd的解決思路是無視驚羣效應,仍然採用master/workers模式,每個子進程仍然管自己在監聽的socket上調用epoll_wait,當有新的鏈接請求發生時,操作系統仍然只是喚醒其中部分的子進程來處理該事件,仍然只有一個子進程能夠成功處理此事件==,那麼其他被驚醒的子進程捕獲EAGAIN錯誤==,並無視。
  • nginx的解決思路:在同一時刻,永遠都只有一個子進程在監聽的socket上epoll_wait,其做法是,創建一個全局的pthread_mutex_t,在子進程進行epoll_wait前,則先獲取鎖。代碼如下:
//負載能力+鎖
ngx_int_t  ngx_trylock_accept_mutex(ngx_cycle_t *cycle)  
{  
    //先嚐試獲取鎖
    if (ngx_shmtx_trylock(&ngx_accept_mutex))
    {
    //是否有能力獲取監聽時間
    if (ngx_enable_accept_events(cycle) == NGX_ERROR) 
        {  
            ngx_shmtx_unlock(&ngx_accept_mutex);  
            return NGX_ERROR;  
        }  
  
        ngx_accept_mutex_held = 1;  
        return NGX_OK;  
    }  
  
    if (ngx_accept_mutex_held)
    {  
        if (ngx_disable_accept_events(cycle) == NGX_ERROR)
        {  
            return NGX_ERROR;  
        }  
  
        ngx_accept_mutex_held = 0;  
    }  
    return NGX_OK;  
}  

且只有在ngx_accept_disabled < 0 時,纔會去獲取全局鎖,即只有在子進程的負載能力在一定的範圍下才會嘗試去獲取鎖,並進入epoll_wait監聽的socket。

//處理負載能力 無負載能力就不能獲取鎖了,因爲服務器不行了,承受不起了
void  ngx_process_events_and_timers(ngx_cycle_t *cycle)  
{   
    if (ngx_accept_disabled > 0)
    {  
        ngx_accept_disabled--;    
    }

else

{  
        if (ngx_trylock_accept_mutex(cycle) == NGX_ERROR) {  
            return;  
        }     
    }  
}



ngx_accept_disabled = ngx_cycle->connection_n / 8  - ngx_cycle->free_connection_n;

表示當子進程的連接數達到連接總數的7/8時,是不會嘗試去獲取全局鎖,只會專注於自己的連接事件請求。

select poll epoll對比

描述符

  • slect 描述符有限 描述符多了性能降低 需要輪詢找出就緒描述符
  • poll 描述符無上限 描述符多了性能降低 需要輪詢找出就緒的描述符,效率降低
  • epoll 描述符無上限 描述符多了性能不會降低 拿到了就是就需要的,不需要無畏的輪詢
  • 事件觸發回調就是直接在數組中添加了就緒的文件描述符

編碼

  • select 需要每次重新添加描述符到集合,並且需要從用戶態拷貝到內核態
  • poll 編碼相對簡單 並且每次需要從用戶狀拷貝到內核態
  • 編碼相對簡單,但是事件僅需定義一次,並且向內核拷貝一次即可

平臺

  • 跨平臺
  • linux
  • linux

邊沿出發和水平出發

  • 邊沿出發只在有新數據的時候提醒,要是這次不讀完,剩下的數據下次不會提醒了,出發又有新的數據來了
  • 水平觸發只要有數據就提醒

優點

  • 用的是紅黑樹,不會有重複,也不限制大小
  • wait的時候返回了大小,就不用遍歷是否就緒
  • 不會修改最初的集合,不用每次都重新設置集合
    對於多路轉接技術來說:都是僅適用於大量連接,但是同一時間僅有少量活躍的情況

小知識點

gcc 加上 -Wall 選項 顯示警告

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