1. 程式人生 > >【Linux】I/O多路複用

【Linux】I/O多路複用

五種IO模型     阻塞IO(等待魚上鉤)         在核心將資料準備好之前,系統呼叫會一直等待,所有的套接字,預設是阻塞模式。         等待,拷貝資料到buf中,(等待的時間長)     非阻塞IO(定期檢視是否有魚上鉤)         如果核心還未將資料準備好,系統呼叫仍然會直接返回, 並且返回EWOULDBLOCK錯誤碼。         設定迴圈,定期詢問     訊號驅動IO(加個鈴鐺提醒)         核心將資料準備好的時候,使用SIGIO訊號通知應用程式進行IO操作。         一旦收到訊號,那麼程序中的執行緒都會被掛起,去處理相應的動作。     多路轉接IO(放一排魚竿,看哪個魚竿動了就表示有魚)   (醫院看病例子,做檢查)         與阻塞IO類似,其核心在於IO多路轉接能夠同時等待多個檔案描述符的就緒狀態。         某檔案可讀,會通知。    序列等待     非同步IO(魚竿自動釣魚)  aio  沒有阻塞  (讓別人去釣魚,然後打電話通知自己)         由核心在資料拷貝完成時, 通知應用程式(而訊號驅動是告訴應用程式什麼時候可以開始拷貝資料)。         自己控制處理IO的時間。         主動通知              前四種都是同步IO(都有自己親力親為的釣魚動作),最後一種是非同步IO。                  任何IO過程中, 都包含兩個步驟。第一是等待, 第二是拷貝。而且在實際的應用場景中, 等待消耗的時     間往往都遠遠高於拷貝的時間。讓IO更高效, 最核心的辦法就是讓等待的時間儘量少。(等待與資料搬遷)              效能優化:先找到效能瓶頸。      同步VS非同步(同步:打電話   非同步:發簡訊)     在計算機領域,同步就是指一個程序在執行某個請求的時候,若該請求需要一段時間才能返回資訊,那麼這個程序將會一直等待下去, 直到收到返回資訊才繼續執行下去;非同步是指程序不需要一直等下去,而是繼續執行下面的操作,不管其他程序的狀態。 當有訊息返回時系統會通知程序進行處理,這樣可以提高執行的效率。舉個例子,打電話時就是同步通訊,發短息時就是非同步通訊。

        所謂同步,就是在發出一個呼叫時,在沒有得到結果之前,該呼叫就不返回。但是一旦呼叫返回,     就得到返回值了。換句話說,就是由呼叫者主動等待這個呼叫的結果。    (沒得到結果不返回)   (自己等待,自己返回)         eg:發起一個請求獲取許可權,在沒有獲得資料之前不返回。         非同步則是相反,呼叫在發出之後,這個呼叫就直接返回了,所以沒有返回結果。換句話說,當一個非同步過程呼叫發出後,     呼叫者不會立刻得到結果。而是在呼叫發出後,被呼叫者通過狀態、通知來通知呼叫者,或通過回撥函式處理這個呼叫。         主動通知         eg:發起一個請求獲取許可權,立即返回,如果資料獲取完畢,通過訊號通知發起者。  (自己發起申請就可以了)              同步與非同步的關注點是發起一個請求,能不能立即得到結果,是否阻塞。          阻塞VS非阻塞     阻塞和非阻塞關注的是程式在等待呼叫結果(訊息、返回值)時的狀態。         阻塞呼叫是指在呼叫結果返回之前,當前執行緒會被掛起,呼叫執行緒只有在得到結果之後才會返回。             eg:發起一個請求獲取資料,如果沒有資料就掛起等待。     (不滿足條件時就掛起等待)         非阻塞呼叫指在不能立刻得到結果之前,該呼叫不會阻塞當前執行緒。(eg:吃麵時看到人多扭頭就走)             eg:發起一個請求獲取資料,如果沒有資料就立即返回。      (不滿足條件時立即返回)              select     阻塞型函式     eg:醫生看病例子(同時看病)         醫生診斷完病情後,讓一個病人去做相關項檢查,然後在等待的過程中繼續診斷第二個病人病情。          建立一個執行緒,去處理多個檔案描述符。     為什麼不建立多個執行緒來處理?         建立多個執行緒的話開銷比較大。     fd_set是描述符的一個集合,相當於點陣圖,通過標誌位的變化來表示當前的檔案狀態。(點陣圖比較省空間)     處於就緒狀態就會把該位 置1,其他的剔除掉。

select系統呼叫是用來讓我們的程式監視多個檔案描述符的狀態變化的。 程式會停在select這裡等待,直到被監視的檔案描述符有一個或者多個發生了狀態改變。     1.監控描述符是否可讀;(重點)     2.監控描述符是否可寫;     3.監控描述符的異常處理; #include <sys/select.h> int select(int nfds, fd_set *readfds, fd_set *writefds,            fd_set *exceptfds, struct timeval *timeout);                     如果沒有這個nfds引數,那麼需要做很多無謂的遍歷。(考慮到檔案描述符是比較小的)     引數nfds是需要監視的最大的檔案描述符值+1;     nfds表示的含義是後面三個點陣圖結構中儲存的最大的檔案描述符數值加1。         比如設定的檔案描述符為5,10,15,那麼這裡nfds設定為16。              rdset,wrset,exset分別對應於需要檢測的可讀檔案描述符的集合,可寫檔案描述符的集合及異常檔案描述符的集合;     引數timeout為結構timeval,用來設定select()的等待時間          struct timeval {        time_t      tv_sec;         /* seconds */        suseconds_t tv_usec;        /* microseconds */     };

        readfds:監控可讀的描述符集合         writefds:監控可寫的描述符集合         exceptfds:監控異常的描述符集合         tv:select是一個阻塞呼叫,但是可以設定阻塞的時間             NULL:一直阻塞             0:非阻塞             >0:在指定時間內如果沒有描述符就緒,則返回0,表示超時

        NULL:則表示select()沒有timeout,select將一直被阻塞,直到某個檔案描述符上發生了事件就緒。         0:僅檢測描述符集合的狀態,然後立即返回,並不等待外部事件的發生。         特定的時間值:如果在指定的時間段裡沒有事件發生,select將超時返回。                      std::bitset<100>   點陣圖

返回值:等於0表示等待超時,大於0表示有幾個描述符等待就緒,小於0表示出錯。     typedef long int _fd_mask;   (long int 在32位機是4個位元組,64位是8個位元組)         _NFDBITS          grep -ER '_NFDBITS' /usr/include/        注意:這裡使用單引號的目的是為了防止字元的轉義("會被轉義)          正則表示式         例如email地址的正則表示式可以寫成[a-zA-Z0-9_.-][email protected][a-zA-Z0-9_.-]+\.[a-zA-Z0-9_.-]+,         IP地址的正則表示式可以寫成[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}             查詢檔案中的ip地址                 egrep '[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}' ip_test     注意:最多包含1024個描述符。      fd_set介面     void FD_CLR(int fd, 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的全部位          需要把這些介面放在迴圈內部,否則就會導致每次設定標誌位只構造一次。          ctrl+d  結束輸入 最後一個字元就沒了。         如果設定時間放在迴圈外面,就會破壞掉設定的定時時間。         需要注意的是,timeout每次都需要重新設定。              IO多路複用的本質         本來read既能完成等待,又能完成拷貝。         但是read有個重要缺陷,那就是一次read只能等待一個檔案描述符,如果要想同時等待多個,就得配合多個執行緒。         如果執行緒太多了,開銷太大了,於是就想辦法讓一個執行緒等待多個檔案描述符。              陣列專門用於儲存所有的描述符,開始的時候只有lst_fd,當要開始select監控時,需要將所有的描述符新增到集合中。 select開始等待,一旦返回,就代表有描述符就緒(超時,出錯暫不考慮),select將所有沒有就緒的描述符,都從集合中移除了。 迴圈判斷陣列中的描述符是否在集合中,來確定這個描述符是否就緒,因為select中只儲存了就緒的,如果就緒的描述符是監聽描述符, 這就代表有新的連線要去接收,當接收後,將新的連線的描述符新增到陣列中。如果不是監聽描述符,代表有資料可讀。

    訪問IO的開銷遠遠高於訪問記憶體的開銷。

    socket就緒條件         讀就緒             1.socket核心中, 接收緩衝區中的位元組數, 大於等於低水位標記SO_RCVLOWAT. 此時可以無阻塞的讀該檔案描述符, 並且返回值大於0;             2.socket TCP通訊中, 對端關閉連線, 此時讀該socket,可以讀到EOF,然後返回0;             3.監聽的socket上有新的連線請求;             4.socket上有未處理的錯誤。         寫就緒             1.socket核心中, 傳送緩衝區中的可用位元組數(傳送緩衝區的空閒位置大小), 大於等於低水位標記SO_SNDLOWAT.         此時可以無阻塞的讀該檔案描述符, 並且返回值大於0;             2.socket的寫操作被關閉(close或者shutdown). 對一個寫操作被關閉的socket進行寫操作, 會觸發SIGPIPE訊號;             3.socket使用非阻塞connect連線成功或失敗之後;             4.socket上有未讀取的錯誤。         異常就緒             socket上收到帶外資料. 關於帶外資料, 和TCP緊急模式相關(回憶TCP協議頭中, 有一個緊急指標的欄位)。                      對於普通的TCP伺服器來說,使用accept完成等待的過程,此處使用select,把所有的檔案描述符的等待,都交給select來處理,     另外,accept返回的new_sock,也加入到select中進行等待,一旦select返回,根據返回的檔案描述符的種類,分別處理。         a.listen_sock就緒,呼叫accept;         b.new_sock就緒,就呼叫read/write。         檔案描述符如果出現一些變化(新增或者關閉),也需要更新select關注的點陣圖的對應狀態。              select缺點         1.檔案描述符有上限;         2.每次呼叫select,都需要重新新增fd集合從使用者態拷貝到核心態,這個開銷在fd很大時會很大;   (因為要改變集合中元素的狀態)         3.每次呼叫select都需要在核心遍歷來判斷是否就緒,fd很大時效能就會下降;         4.需要手動設定fd集合,從介面使用角度來看比較麻煩。(每次都需要自己設定集合)                  1.每次呼叫select, 都需要手動設定fd集合, 從介面使用角度來說非常不便   (輸入輸出使用了同一個陣列)         2.每次呼叫select,都需要把fd集合從使用者態拷貝到核心態,這個開銷在fd很多時會很大         3.同時每次呼叫select都需要在核心遍歷傳遞進來的所有fd,這個開銷在fd很多時也很大         4.select支援的檔案描述符數量太小   (作為入口伺服器時,一般不夠用)              優點         windows下也有select,可以實現跨平臺。      poll

用陣列來監控狀態集合。   相當於標誌起來,pollin。     輸入輸出引數分離。

#include <poll.h> int poll(struct pollfd *fds, nfds_t nfds, int timeout); // pollfd結構     struct pollfd {         int fd; /* file descriptor */         short events; /* requested events */   //輸入         short revents; /* returned events */   //輸出     };          fds是一個poll函式監聽的結構列表. 每一個元素中, 包含了三部分內容: 檔案描述符, 監聽的事件集合(讀寫事件), 返回的事件集合. nfds表示fds陣列的長度。timeout表示poll函式的超時時間, 單位是毫秒          介面發生了變化,將輸入輸出隔離。     select拷貝的是點陣圖,而poll拷貝了一個結構體,顯然效率變低了。          優點:         1.描述符無上限;         2.編寫程式碼簡單。     缺點         不跨平臺          epoll

為了處理大批量控制代碼而做了改進的poll。 多路轉接技術多用於有大量併發連線,但是同一時間只有少量活躍的情況。

    eg: 相當於將就緒的元素存放在另一個數組中。     epoll可以理解為一個容器,裡面存放了鍵值對,鍵是檔案描述符fd,值是自定義結構體epoll_event。不能讀寫          // 建立一個epoll的控制代碼     #include <sys/epoll.h>     int epoll_create(int size);          size大於0即可。     返回值:成功返回描述符的控制代碼,失敗返回-1。     自從linux2.6.8之後,size引數是被忽略的。用完之後,必須呼叫close()關閉。          // epoll的事件註冊     int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);              typedef union epoll_data {            void        *ptr;  // 如果想要使用更加複雜的結構,就得自己封裝一個結構體,存在堆上,然後把地址賦給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 */         };          第一個引數是epoll_create()的返回值(epoll的控制代碼).     第二個引數表示動作,用三個巨集來表示.     第三個引數是需要監聽的fd.         struct epoll_event ev, evs[1024];         ev.data.fd = lst_fd;        // key value相同,都是lst_fd  這裡設定的目的是在epoll_wait的返回值,返回了這個value     第四個引數是告訴核心需要監聽什麼事.         ev.events = EPOLLIN;  // 讀就緒  無阻塞         ev.events = EPOLLOUT; // 寫就緒  無阻塞

        epoll_ctl(epfd, EPOLL_CTL_ADD, lst_fd, &ev);             最後兩個引數相當於是鍵值對一樣的存在,ev中還得再儲存下value   也就是ev.data.fd              第二個引數的取值:         EPOLL_CTL_ADD :註冊新的fd到epfd中;         EPOLL_CTL_MOD :修改已經註冊的fd的監聽事件;         EPOLL_CTL_DEL :從epfd中刪除一個fd;  // 只需要知道鍵即可,對於值是什麼不關心                  這些選項是互斥的。              int epoll_wait(int epfd, struct epoll_event * events, int maxevents, int timeout);         第二三個引數相當於是確定了緩衝區         返回值表示當前有幾個檔案描述符就緒。  返回的是之前epoll_ctl中新增的鍵值對中的值listen_sock         核心中的檔案描述符不需要遍歷,但是返回的events陣列中的檔案描述符需要遍歷。(適用於客戶端連線的多,但是活躍的比較少)          返回值:         大於0表示幾個描述符就緒         等於0表示當前沒有就緒的         小於0表示出錯              測試中發現連線上的檔案描述符是5,這是因為3是listen_sock,而4被epoll佔用了。         出錯日誌沒列印,可能是沒有加換行,那就沒有重新整理緩衝區。          引數events是分配好的epoll_event結構體陣列.epoll將會把發生的事件賦值到events陣列中 (events不可以是空指標, 核心只負責把資料複製到這個events陣列中,不會去幫助我們在使用者態中分配記憶體).maxevents告之核心這個events有多大, 這個 maxevents的值不能大於建立epoll_create()時的size.引數timeout是超時時間 (毫秒,0會立即返回,-1是永久阻塞). 如果函式呼叫成功,返回對應I/O上已準備好的檔案描述符數目,如返回0表示已超時, 返回小於0表示函式失敗。

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

    struct epitem{         struct rb_node rbn;//紅黑樹節點         struct list_head rdllink;//雙向連結串列節點         struct epoll_filefd ffd; //事件控制代碼資訊         struct eventpoll *ep; //指向其所屬的eventpoll物件         struct epoll_event event; //期待發生的事件型別     }          epoll工作原理(紅黑樹+佇列)         紅黑樹:             儲存了鍵值對資訊(鍵是檔案描述符,值監控哪種方式就緒以及使用者自定製的資訊)             std::set   std::map   epoll         就緒佇列:             某個檔案描述符就緒後,就會觸發對應的回撥函式,由核心把檔案描述符資訊放到就緒佇列中,epoll_wait返回的時候         就把就緒佇列中的內容拷貝到使用者提供的緩衝區裡面。                  epoll底層是紅黑樹,相對平衡的二叉搜尋樹(每次查詢時相當於減少一半區間,二分查詢),成就了查詢效率,但是喪失了插入效率,     每次都需要保證平衡。紅黑樹在平衡和旋轉之間找到了一個橋樑。從而達到查詢和插入的效率都還可以。         epoll摒棄了輪詢遍歷就緒描述符的缺點,而是採用回撥機制來處理。網絡卡觸發,告訴驅動,然後驅動告訴核心,核心來呼叫回撥函式。         通過回撥函式把就緒的檔案描述符放到就緒佇列中。                  每一個epoll物件都有一個獨立的eventpoll結構體,用於存放通過epoll_ctl方法向epoll物件中新增進來的事件。     這些事件都會掛載在紅黑樹中,如此,重複新增的事件就可以通過紅黑樹而高效的識別出來(紅黑樹的插入時間效率是lgn,其中n為樹的高度)。     而所有新增到epoll中的事件都會與裝置(網絡卡)驅動程式建立回撥關係,也就是說,當響應的事件發生時會呼叫這個回撥方法。     這個回撥方法在核心中叫eppollcallback,它會將發生的事件新增到rdlist雙鏈表中。在epoll中,對於每一個事件,都會建立一個epitem結構體。              當呼叫epoll_wait檢查是否有事件發生時,只需要檢查eventpoll物件中的rdlist雙鏈表中是否有epitem元素即可。     如果rdlist不為空,則把發生的事件複製到使用者態,同時將事件數量返回給使用者,這個操作的時間複雜度是O(1)。(基於回撥機制)          epoll使用三部曲         呼叫epoll_create建立一個epoll控制代碼;         呼叫epoll_ctl, 將要監控的檔案描述符進行註冊;         呼叫epoll_wait, 等待檔案描述符就緒。          IO多路複用的核心思想     一個執行緒來監控多個檔案描述符的就緒狀態。

    select         1.每次呼叫select都需要重新設定要監控的檔案描述符;  介面使用不便         2.輸入的檔案描述符集和輸出的檔案描述符集沒有分離;  介面使用不便         3.需要頻繁地將檔案描述符集拷貝到核心中,也需要從核心中拷貝回用戶程式碼中,影響效能;         4.檔案描述符集是一個位圖結構,需要頻繁地進行遍歷,才能知道要監控哪些檔案描述符,以及當前哪些檔案描述符就緒了,     頻繁遍歷也會影響效能,尤其是當前要監控的檔案描述符數量比較多的時候;         5.要監控的檔案描述符有上限(fd_set點陣圖結構能表示的最大位是有限制的)。     poll         1.檔案描述符無上限;         2.輸入和輸出分離;         3.需要頻繁拷貝(拷貝的資料量比select還要多,poll拷貝的是結構體陣列中的內容,而select拷貝的是點陣圖中檔案描述符的就緒狀態)。     epoll         1.從介面上看,epoll拆分成三個介面,不需要每次都重新設定要監控的檔案描述符;         2.輸入和輸出分離,設定監控檔案描述符使用epoll_ctl來完成,獲取到當前哪些檔案描述符就緒通過epoll_wait來完成;         3.不需要頻繁地把要監控的檔案描述符拷貝到核心中;         4.使用回撥機制避免了對很大的檔案描述符集進行遍歷;         5.檔案描述符沒有上限。      epoll poll select區別     1.select         描述符有上限             因為輪詢遍歷判斷是否有描述符就緒,因此隨著描述符增加,效能降低         每次都需要修改集合內容,因此需要每次重新新增描述符,並且拷貝到核心態,效率降低         並不會告訴我們到底哪一個描述符就緒,因此需要我們手動遍歷判斷,效率較低             select是通過遍歷描述符來監控的。         可以跨平臺     2.poll         描述符無上限         因為輪詢遍歷判斷是否有描述符就緒,因此隨著描述符增加,效能降低         每次都需要修改集合內容,因此需要每次重新新增描述符,並且拷貝到核心態,效率降低         並不會告訴我們到底哪一個描述符就緒,因此需要我們手動遍歷判斷,效率較低         無法跨平臺     3.epoll         描述符無上限         基於事件回撥,因此不會隨著描述符增加而效能下降。         每個描述符的事件僅向核心新增一次,不需要重複新增,因此效率較高,編碼較為簡單。         直接交付給我們的都是就緒的事件,因此不需要無謂的遍歷,也就沒有空遍歷,效率較高。  時間複雜度始終是O(1)         無法跨平臺                  設定監控的描述符是通過EPOLL_CTL_ADD新增的。         epoll也要通過核心將資料拷貝到使用者態。(並不是mmap這種記憶體對映的方式)  struct epoll_event是我們在使用者空間中分配好的記憶體。      epoll工作方式         EPOLLET 邊緣觸發(ET)   高速         再次呼叫epoll_wait,epoll_wait不會返回,此時緩衝區中還剩下的1K資料就不能立刻被讀取到,就需要等到對應的socket再次     收到資料觸發讀就緒,才有機會把之前殘留的1k資料讀出來。         只有每次新事件就緒的時候才會返回通知我們              LT和ET方式是互斥的。              EPOLLLT 水平觸發(LT)            select 和 poll都只有水平觸發            再次呼叫epoll_wait,epoll_wait就會立刻返回,提示還是同一個沒讀完的檔案描述符就緒。         預設的工作模式是水平觸發         只要事件滿足就緒條件就會一直返回通知我們         一次只讀取給定位元組的資料              當epoll事件結點觸發方式設定為邊緣觸發的時候,需要我們一次將所有資料都讀取出來(效率高),否則不會再次提醒,直到下次新資料到來, 又因為一次讀取所有(比如說一次讀取1024,當讀取完後,不知道還有沒有資料,而這時沒了資料就會阻塞),有可能造成recv阻塞, 因此需要將socket描述符設定為非阻塞,讓socket的所有操作都是非阻塞的。                EINTR 阻塞操作,有可能被訊號打斷,只需要重新讀取資料,所以用continue     EAGAIN 本次讀取時緩衝區沒有資料  資料讀完了因此用break          讀取的時候如果資料的長度小於想要的資料長度(len - total_len),那麼認為沒資料了,應該break          阻塞的read,每次讀取1k,一共10k資料,這時候讀取完1k後,剩餘的9k存在緩衝區裡不能被讀取。         ET非阻塞輪詢方式讀取         LT會一直讀取          設定非阻塞IO         #include <fcntl.h>         int fcntl(int fd, int cmd, ... /* arg */ );                           設定屬性                     cmd=F_SETFL或者F_GETFL    (設定或者獲得flag)         #define O_NONBLOCK 04000 非阻塞屬性                  eg;             int flag = fcntl(fd, F_GETFL);                          int ret = fcntl(fd, F_SETFL, flag | O_NONBLOCK);  //點陣圖

    QPS/TPS:每秒鐘處理的請求量(Query)     線上         log 會把日誌字串放到記憶體(緩衝區)中。         有一個專門的執行緒,每隔一段時間定期重新整理。(非同步非阻塞IO)             優先保障效率,對於可靠性就沒有仔細考慮。     線下         log 直接寫到磁碟上,並且一旦失敗就會通知呼叫者執行失敗  (同步阻塞)             優先保障可靠性,對效能要求不高。              epoll中的驚群問題         如今網路程式設計中經常用到多程序或多執行緒模型,大概的思路是父程序建立socket,bind、listen後,通過fork建立多個子程序,     每個子程序繼承了父程序的socket,呼叫accpet開始監聽等待網路連線。這個時候有多個程序同時等待網路的連線事件,當這個事件發生時,     這些程序被同時喚醒,就是“驚群”。這樣會導致什麼問題呢?我們知道程序被喚醒,需要進行核心重新排程,這樣每個程序同時去響應這一個事件,     而最終只有一個程序能處理事件成功,其他的程序在處理該事件失敗後重新休眠或其他。         簡而言之,驚群現象(thundering herd)就是當多個程序和執行緒在同時阻塞等待同一個事件時,如果這個事件發生,     會喚醒所有的程序,但最終只可能有一個程序/執行緒對該事件進行處理,其他程序/執行緒會在失敗後重新休眠,這種效能浪費就是驚群。         其實在Linux2.6版本以後,核心核心已經解決了accept()函式的“驚群”問題,大概的處理方式就是,當核心接收到一個客戶連線後,     只會喚醒等待佇列上的第一個程序或執行緒。所以,如果伺服器採用accept阻塞呼叫方式,在最新的Linux系統上,已經沒有“驚群”的問題了。         但是,對於實際工程中常見的伺服器程式,大都使用select、poll或epoll機制,此時,伺服器不是阻塞在accept,     而是阻塞在select、poll或epoll_wait,這種情況下的“驚群”仍然需要考慮。         Nginx中使用mutex互斥鎖解決這個問題,具體措施有使用全域性互斥鎖,每個子程序在epoll_wait()之前先去申請鎖,申請到則繼續處理,     獲取不到則等待,並設定了一個負載均衡的演算法(當某一個子程序的任務量達到總設定量的7/8時,則不會再嘗試去申請鎖)來均衡各個程序的任務量。