一、常見的IO模型
參考文章:https://www.cnblogs.com/yanguhung/p/10145755.html
服務器端編程經常需要構造高性能的IO模型,常見的5種IO處理模型
- 同步阻塞IO
- 同步非阻塞IO
- IO多路復用(又被稱為“事件驅動”)
- 異步IO
- 信號驅動IO(本文不講,我不會。。。)
同步和異步的概念描述的是用戶線程與內核的交互方式:同步是指用戶線程發起IO請求后需要等待或者輪詢內核IO操作完成后才能繼續執行;
而異步是指用戶線程發起IO請求后仍繼續執行,當內核IO操作完成后會通知用戶線程,或者調用用戶線程注冊的回調函數。
阻塞和非阻塞的概念描述的是用戶線程調用內核IO操作的方式:阻塞是指IO操作需要徹底完成后才返回到用戶空間;而非阻塞是指IO操作被調用后立即返回給用戶一個狀態值,無需等到IO操作徹底完成。
1、同步阻塞IO
同步阻塞IO模型是最簡單的IO模型,用戶線程在內核進行IO操作時被阻塞。
阻塞IO意味着當我們發起一次IO操作后一直等待成功或失敗之后才返回,在這期間程序不能做其它的事情。
阻塞IO操作只能對單個文件描述符進行操作。
當用戶進程調用了recv/recvfrom這些系統調用時,操作系統就開始了IO的第一個階段:准備數據。因為很多時候數據一開始還沒有到達(比如,還沒有收到一個完整的UDP包),這個時候操作系統就要等待足夠的數據到來。
而在用戶進程這邊,整個進程會被阻塞。操作系統一直等到數據准備好了,它就會將數據拷貝到用戶內存,然后返回結果,用戶進程才解除block的狀態,重新運行起來。
所以,blocking IO的特點就是在IO執行的兩個階段(等待數據和拷貝數據兩個階段)都被block了。
幾乎所有的程序員第一次接觸到的網絡編程都是從listen()、send()、recv() 等接口開始的,使用這些接口可以很方便的構建服務器/客戶機的模型。然而大部分的socket接口都是阻塞型的。
阻塞型接口是指系統調用(一般是IO接口)不返回調用結果並讓當前線程一直阻塞,只有當該系統調用獲得結果或者超時出錯時才返回。
實際上,除非特別指定,幾乎所有的IO接口 ( 包括socket接口 ) 都是阻塞型的。這給網絡編程帶來了一個很大的問題,如在調用recv(1024)的同時,線程將被阻塞,在此期間,線程將無法執行任何運算或響應任何的網絡請求。
解決方案:
在服務器端使用多線程(或多進程)。多線程(或多進程)的目的是讓每個連接都擁有獨立的線程(或進程),這樣任何一個連接的阻塞都不會影響其他的連接。
該方案的問題是:
開啟多進程或都線程的方式,在遇到要同時響應成百上千路的連接請求,則無論多線程還是多進程都會嚴重占據系統資源,降低系統對外界響應效率,而且線程與進程本身也更容易進入假死狀態。
改進方案:
很多程序員可能會考慮使用“線程池”或“連接池”。“線程池”旨在減少創建和銷毀線程的頻率,其維持一定合理數量的線程,並讓空閑的線程重新承擔新的執行任務。“連接池”維持連接的緩存池,盡量重用已有的連接、減少創建和關閉連接的頻率。這兩種技術都可以很好的降低系統開銷,都被廣泛應用很多大型系統,如websphere、tomcat和各種數據庫等。
改進后方案其實也存在着問題:
“線程池”和“連接池”技術也只是在一定程度上緩解了頻繁調用IO接口帶來的資源占用。而且,所謂“池”始終有其上限,當請求大大超過上限時,“池”構成的系統對外界的響應並不比沒有池的時候效果好多少。所以使用“池”必須考慮其面臨的響應規模,並根據響應規模調整“池”的大小。
對應上例中的所面臨的可能同時出現的上千甚至上萬次的客戶端請求,“線程池”或“連接池”或許可以緩解部分壓力,但是不能解決所有問題。總之,多線程模型可以方便高效的解決小規模的服務請求,但面對大規模的服務請求,多線程模型也會遇到瓶頸,可以用非阻塞接口來嘗試解決這個問題。
2、同步非阻塞IO
同步非阻塞IO是在同步阻塞IO的基礎上,將socket設置為NONBLOCK。這樣做用戶線程可以在發起IO請求后可以立即返回。
在非阻塞式IO中,用戶進程其實是需要不斷的主動詢問操作系統數據是否准備好了,這種類似輪詢的方式會浪費很多不必要的CPU資源。
和阻塞IO一樣,非阻塞IO也是通過調用read或write來進行操作的,也只能對單個描述符進行操作。
用戶需要不斷地調用read,嘗試讀取socket中的數據,直到讀取成功后,才繼續處理接收的數據。整個IO請求的過程中,雖然用戶線程每次發起IO請求后可以立即返回(當數據未准備好時,會返回error),但是為了等到數據,仍需要不斷地輪詢、重復請求,消耗了大量的CPU的資源。一般很少直接使用這種模型,而是在其他IO模型中使用非阻塞IO這一特性。
例如:
# Server端 import socket sk = socket.socket() sk.bind(('127.0.0.1',8080)) sk.setblocking(False) # 設置當前的server為一個非阻塞IO模型 sk.listen() conn_lst = [] del_lst = [] while True: try: conn,addr = sk.accept() conn_lst.append(conn) except BlockingIOError: for conn in conn_lst: try: conn.send(b'hello') print(conn.recv(1024)) except (NameError,BlockingIOError):pass except ConnectionResetError: conn.close() del_lst.append(conn) for del_conn in del_lst: conn_lst.remove(del_conn) del_lst.clear() # Client端 import socket sk = socket.socket() sk.connect(('127.0.0.1',8080)) while 1: print(sk.recv(1024)) sk.send(b'heiheihei') 這樣就可以實現非阻塞IO下基於TCP的並發socket, 但是這樣的非阻塞IO大量的占用了CPU導致了資源的浪費並且給CPU造成了很大的負擔
二、IO多路復用
我們都知道unix(like)世界里,一切皆文件,而文件就是一串二進制流。
在信息交換的過程中,其實就是對這些流進行數據的收發(讀寫)操作,簡稱為I/O操作(input and output)。
從流中讀出數據,系統調用read,寫入數據,系統調用write。
文件描述符就是讓我們具體操作去哪個流的一個操作符。
當我們調用socket進行信息交換時,實際上會通過系統調用返回一個文件描述符,我們就是對這個文件描述符進行操作。
實際中一個應用程序通常需要處理多條數據流中的事件,
比如:nginx、redis等數據庫,需要同時處理來自N個客戶端的事件。
那么IO多路復用是什么?
IO多路復用是一種同步IO模型,單線程或單進程同時監測若干個文件描述符(文件句柄)是否可以執行IO操作的能力。
一旦某個文件句柄就緒,就能夠通知應用程序進行相應的讀寫操作;沒有文件句柄就緒時會阻塞應用程序,交出cpu。
多路是指網絡連接,復用指的是同一個線程。
1、普通IO多路復用
IO多路復用,經典的Reactor設計模式。
IO多路復用模型是建立在內核提供的多路分離函數select基礎之上的,使用select函數可以避免同步非阻塞IO模型中輪詢等待的問題。
多路復用IO需要使用兩次系統調用,而阻塞IO只調用了一個系統調用。
select的優勢在於它可以同時處理多個連接,首先都會對一組文件描述符進行相關事件的注冊,然后阻塞等待某些事件的發生或等待超時。而阻塞IO只能處理一個。
用戶首先將需要進行IO操作的socket添加到select中,然后阻塞等待select系統調用返回。當數據到達時,socket被激活,select函數返回。用戶線程正式發起read請求,讀取數據並繼續執行。
從流程上來看,使用select函數進行IO請求和同步阻塞模型沒有太大的區別,甚至還多了添加監視socket,以及調用select函數的額外操作,效率更差。但是,使用select以后最大的優勢是用戶可以在一個線程內同時處理多個socket的IO請求。用戶可以注冊多個socket,然后不斷地調用select讀取被激活的socket,即可達到在同一個線程內同時處理多個IO請求的目的。而在同步阻塞模型中,必須通過多線程的方式才能達到這個目的。
可以看出,select()每次選出一個sock,然后對這個sock上的事件進行處理。所以io多路復用,在沒有用特殊異步api的情況下,還是同步的操作。
可實現IO多路復用(asycio tornado twisted)的方法(機制)
select 在windows\mac\linux可用
底層是操作系統的輪詢
有監聽對象個數的限制
隨着監聽對象的個數增加,效率降低
poll 只在mac\linux可用
底層是操作系統的輪詢
沒有監聽對象個數的限制
隨着監聽對象的個數增加,效率降低
epoll 只在mac\linux可用
給每一個要監聽的對象都綁定了一個回調函數
不再受到個數增加 效率降低的影響
socketserver機制
IO多路復用 + threading線程
selectors模塊
幫助你在不同的操作系統上進行IO多路復用機制的自動篩選
2、Reactor模式IO多路復用
為什么IO多路復用,有時也稱為異步阻塞IO(實際是同步的,別混淆)?
因為使用select函數的優點並不僅限於此。雖然上述方式允許單線程內處理多個IO請求,但是每個IO請求的過程還是阻塞的(在select函數上阻塞),平均時間甚至比同步阻塞IO模型還要長。如果用戶線程只注冊自己感興趣的socket或者IO請求,然后去做自己的事情,等到數據到來時再進行處理,則可以提高CPU的利用率。
IO多路復用模型使用了Reactor設計模式實現了這一機制。
Reactor的類圖
如圖所示,Event_handler抽象類表示IO事件處理器,它擁有IO文件句柄Handle(通過get_handle獲取),以及對Handle的操作handle_event(讀/寫等)。繼承於EventHandler的子類可以對事件處理器的行為進行定制。Reactor類用於管理Event_handler(注冊、刪除等),並使用handle_events實現事件循環,不斷調用同步事件多路分離器(一般是內核)的多路分離函數select,只要某個文件句柄被激活(可讀/寫等),select就返回(阻塞),handle_events就會調用與文件句柄關聯的事件處理器的handle_event進行相關操作。
上面的那段解釋,用我們的理解來說,如下圖
通過Reactor的方式,可以將用戶線程輪詢IO操作狀態的工作統一交給handle_events事件循環進行處理。用戶線程注冊事件處理器之后可以繼續執行做其他的工作(異步),而Reactor線程負責調用內核的select函數檢查socket狀態。當有socket被激活時,則通知相應的用戶線程(或執行用戶線程的回調函數),執行handle_event進行數據讀取、處理的工作。由於select函數是阻塞的,因此多路IO復用模型也被稱為異步阻塞IO模型。注意,這里的所說的阻塞是指select函數執行時線程被阻塞,而不是指socket。一般在使用IO多路復用模型時,socket都是設置為NONBLOCK的。
3、redis中的IO多路復用
順便說一下redis是如何處理大量的請求的?
redis是一個單線程程序,也就是說一個時刻它只能處理一個客戶端請求;
redis是通過IO多路復用(select,epoll,kqueue,依據不同平台,采取不同的實現)來處理多個客戶端請求的。
IO即為網絡I/O,多路即為多個TCP連接,復用即為共用一個線程或者進程,模型最大的優勢是系統開銷小,不必創建也不必維護過多的線程或進程。
IO多路復用是經典的Reactor設計模式,有時也稱為異步阻塞IO(異步指socket為non-blocking,堵塞指select堵塞)
IO多路復用的核心是可以同時處理多個連接請求,為此使用了兩個系統調用。
select/poll/epoll--模型機制:可以監視多個描述符(fd),一旦某個描述符就緒(讀/寫/異常)就能通知程序進行相應的讀寫操作。
讀寫操作都是自己負責的,也即是阻塞的,所以本質上都是同步(阻塞)IO。
4、異步IO(asynchronous I/O)
- 用戶進程發起read操作之后,操作系統會立刻返回,所有不會形成阻塞,用戶進程立刻就可以開始去做其它的事。
- 操作系統接受到異步請求后,首先會立刻返回,不對用戶進程進行阻塞,
- 同時操作系統會等待數據准備完成,然后將數據拷貝到用戶內存(也就是說,異步I/O用戶進程無需自己負責進行讀寫),當這一切都完成之后,會給用戶進程發送一個signal,告訴它read操作完成了。
三、select、poll、epoll之間的區別
原文:https://www.cnblogs.com/aspirant/p/9166944.html
(1)select -- 時間復雜度O(n)
它僅僅知道了,有I/O事件發生了,卻並不知道是哪那幾個流(可能有一個,多個,甚至全部),我們只能無差別輪詢所有流,找出能讀出數據,或者寫入數據的流,對他們進行操作。所以select具有O(n)的無差別輪詢復雜度,同時處理的流越多,無差別輪詢時間就越長。
(2)poll -- 時間復雜度O(n)
poll本質上和select沒有區別,它將用戶傳入的數組拷貝到內核空間,然后查詢每個fd對應的設備狀態, 但是它沒有最大連接數的限制,原因是它是基於鏈表來存儲的.
(3)epoll -- 時間復雜度O(1)
epoll可以理解為event poll,不同於忙輪詢和無差別輪詢,epoll會把哪個流發生了怎樣的I/O事件通知我們。所以我們說epoll實際上是事件驅動(每個事件關聯上fd)的,此時我們對這些流的操作都是有意義的。(復雜度降低到了O(1))
select,poll,epoll都是IO多路復用的機制。I/O多路復用就通過一種機制,可以監視多個描述符,一旦某個描述符就緒(一般是讀就緒或者寫就緒),能夠通知程序進行相應的讀寫操作。但select,poll,epoll本質上都是同步I/O,因為他們都需要在讀寫事件就緒后自己負責進行讀寫,也就是說這個讀寫過程是阻塞的,而異步I/O則無需自己負責進行讀寫,異步I/O的實現會負責把數據從內核拷貝到用戶空間。
epoll跟select都能提供多路I/O復用的解決方案。在現在的Linux內核里有都能夠支持,其中epoll是Linux所特有,而select則應該是POSIX所規定,一般操作系統均有實現
select:
select本質上是通過設置或者檢查存放fd標志位的數據結構來進行下一步處理。這樣所帶來的缺點是:
1、 單個進程可監視的fd數量被限制,即能監聽端口的大小有限。
一般來說這個數目和系統內存關系很大,具體數目可以cat /proc/sys/fs/file-max察看。32位機默認是1024個。64位機默認是2048.
2、 對socket進行掃描時是線性掃描,即采用輪詢的方法,效率較低:
當套接字比較多的時候,每次select()都要通過遍歷FD_SETSIZE個Socket來完成調度,不管哪個Socket是活躍的,都遍歷一遍。這會浪費很多CPU時間。如果能給套接字注冊某個回調函數,當他們活躍時,自動完成相關操作,那就避免了輪詢,這正是epoll與kqueue做的。
3、需要維護一個用來存放大量fd的數據結構,這樣會使得用戶空間和內核空間在傳遞該結構時復制開銷大
poll:
poll本質上和select沒有區別,它將用戶傳入的數組拷貝到內核空間,然后查詢每個fd對應的設備狀態,如果設備就緒則在設備等待隊列中加入一項並繼續遍歷,如果遍歷完所有fd后沒有發現就緒設備,則掛起當前進程,直到設備就緒或者主動超時,被喚醒后它又要再次遍歷fd。這個過程經歷了多次無謂的遍歷。
它沒有最大連接數的限制,原因是它是基於鏈表來存儲的,但是同樣有一個缺點:
1、大量的fd的數組被整體復制於用戶態和內核地址空間之間,而不管這樣的復制是不是有意義。
2、poll還有一個特點是“水平觸發”,如果報告了fd后,沒有被處理,那么下次poll時會再次報告該fd。
epoll:
epoll有EPOLLLT和EPOLLET兩種觸發模式,LT是默認的模式,ET是“高速”模式。LT模式下,只要這個fd還有數據可讀,每次 epoll_wait都會返回它的事件,提醒用戶程序去操作,而在ET(邊緣觸發)模式中,它只會提示一次,直到下次再有數據流入之前都不會再提示了,無 論fd中是否還有數據可讀。所以在ET模式下,read一個fd的時候一定要把它的buffer讀光,也就是說一直讀到read的返回值小於請求值,或者 遇到EAGAIN錯誤。還有一個特點是,epoll使用“事件”的就緒通知方式,通過epoll_ctl注冊fd,一旦該fd就緒,內核就會采用類似callback的回調機制來激活該fd,epoll_wait便可以收到通知。
epoll為什么要有EPOLLET觸發模式?
如果采用EPOLLLT模式的話,系統中一旦有大量你不需要讀寫的就緒文件描述符,它們每次調用epoll_wait都會返回,這樣會大大降低處理程序檢索自己關心的就緒文件描述符的效率.。而采用EPOLLET這種邊沿觸發模式的話,當被監控的文件描述符上有可讀寫事件發生時,epoll_wait()會通知處理程序去讀寫。如果這次沒有把數據全部讀寫完(如讀寫緩沖區太小),那么下次調用epoll_wait()時,它不會通知你,也就是它只會通知你一次,直到該文件描述符上出現第二次可讀寫事件才會通知你!!!這種模式比水平觸發效率高,系統不會充斥大量你不關心的就緒文件描述符
epoll的優點:
1、沒有最大並發連接的限制,能打開的FD的上限遠大於1024(1G的內存上能監聽約10萬個端口);
2、效率提升,不是輪詢的方式,不會隨着FD數目的增加效率下降。只有活躍可用的FD才會調用callback函數;
即Epoll最大的優點就在於它只管你“活躍”的連接,而跟連接總數無關,因此在實際的網絡環境中,Epoll的效率就會遠遠高於select和poll。
3、 內存拷貝,利用mmap()文件映射內存加速與內核空間的消息傳遞;即epoll使用mmap減少復制開銷。
select、poll、epoll 區別總結:
1、支持一個進程所能打開的最大連接數
select
單個進程所能打開的最大連接數有FD_SETSIZE宏定義,其大小是32個整數的大小(在32位的機器上,大小就是3232,同理64位機器上FD_SETSIZE為3264),當然我們可以對進行修改,然后重新編譯內核,但是性能可能會受到影響,這需要進一步的測試。
poll
poll本質上和select沒有區別,但是它沒有最大連接數的限制,原因是它是基於鏈表來存儲的
epoll
雖然連接數有上限,但是很大,1G內存的機器上可以打開10萬左右的連接,2G內存的機器可以打開20萬左右的連接
2、FD劇增后帶來的IO效率問題
select
因為每次調用時都會對連接進行線性遍歷,所以隨着FD的增加會造成遍歷速度慢的“線性下降性能問題”。
poll
同上
epoll
因為epoll內核中實現是根據每個fd上的callback函數來實現的,只有活躍的socket才會主動調用callback,所以在活躍socket較少的情況下,使用epoll沒有前面兩者的線性下降的性能問題,但是所有socket都很活躍的情況下,可能會有性能問題。
3、 消息傳遞方式
select
內核需要將消息傳遞到用戶空間,都需要內核拷貝動作
poll
同上
epoll
epoll通過內核和用戶空間共享一塊內存來實現的。
總結:
綜上,在選擇select,poll,epoll時要根據具體的使用場合以及這三種方式的自身特點。
1、表面上看epoll的性能最好,但是在連接數少並且連接都十分活躍的情況下,select和poll的性能可能比epoll好,畢竟epoll的通知機制需要很多函數回調。
2、select低效是因為每次它都需要輪詢。但低效也是相對的,視情況而定,也可通過良好的設計改善
關於這三種IO多路復用的用法,前面三篇總結寫的很清楚,並用服務器回射echo程序進行了測試。連接如下所示:
select:IO多路復用之select總結
poll:O多路復用之poll總結
epoll:IO多路復用之epoll總結
今天對這三種IO多路復用進行對比,參考網上和書上面的資料,整理如下:
1、select實現
select的調用過程如下所示:
(1)使用copy_from_user從用戶空間拷貝fd_set到內核空間
(2)注冊回調函數__pollwait
(3)遍歷所有fd,調用其對應的poll方法(對於socket,這個poll方法是sock_poll,sock_poll根據情況會調用到tcp_poll,udp_poll或者datagram_poll)
(4)以tcp_poll為例,其核心實現就是__pollwait,也就是上面注冊的回調函數。
(5)__pollwait的主要工作就是把current(當前進程)掛到設備的等待隊列中,不同的設備有不同的等待隊列,對於tcp_poll來說,其等待隊列是sk->sk_sleep(注意把進程掛到等待隊列中並不代表進程已經睡眠了)。在設備收到一條消息(網絡設備)或填寫完文件數據(磁盤設備)后,會喚醒設備等待隊列上睡眠的進程,這時current便被喚醒了。
(6)poll方法返回時會返回一個描述讀寫操作是否就緒的mask掩碼,根據這個mask掩碼給fd_set賦值。
(7)如果遍歷完所有的fd,還沒有返回一個可讀寫的mask掩碼,則會調用schedule_timeout是調用select的進程(也就是current)進入睡眠。當設備驅動發生自身資源可讀寫后,會喚醒其等待隊列上睡眠的進程。如果超過一定的超時時間(schedule_timeout指定),還是沒人喚醒,則調用select的進程會重新被喚醒獲得CPU,進而重新遍歷fd,判斷有沒有就緒的fd。
(8)把fd_set從內核空間拷貝到用戶空間。
總結:
select的幾大缺點:
(1)每次調用select,都需要把fd集合從用戶態拷貝到內核態,這個開銷在fd很多時會很大
(2)同時每次調用select都需要在內核遍歷傳遞進來的所有fd,這個開銷在fd很多時也很大
(3)select支持的文件描述符數量太小了,默認是1024
2、poll實現
poll的實現和select非常相似,只是描述fd集合的方式不同,poll使用pollfd結構而不是select的fd_set結構,其他的都差不多,管理多個描述符也是進行輪詢,根據描述符的狀態進行處理,但是poll沒有最大文件描述符數量的限制。poll和select同樣存在一個缺點就是,包含大量文件描述符的數組被整體復制於用戶態和內核的地址空間之間,而不論這些文件描述符是否就緒,它的開銷隨着文件描述符數量的增加而線性增大。
3、epoll
epoll既然是對select和poll的改進,就應該能避免上述的三個缺點。那epoll都是怎么解決的呢?在此之前,我們先看一下epoll和select和poll的調用接口上的不同,select和poll都只提供了一個函數——select或者poll函數。而epoll提供了三個函數,epoll_create,epoll_ctl和epoll_wait,epoll_create是創建一個epoll句柄;epoll_ctl是注冊要監聽的事件類型;epoll_wait則是等待事件的產生。
對於第一個缺點,epoll的解決方案在epoll_ctl函數中。每次注冊新的事件到epoll句柄中時(在epoll_ctl中指定EPOLL_CTL_ADD),會把所有的fd拷貝進內核,而不是在epoll_wait的時候重復拷貝。epoll保證了每個fd在整個過程中只會拷貝一次。
對於第二個缺點,epoll的解決方案不像select或poll一樣每次都把current輪流加入fd對應的設備等待隊列中,而只在epoll_ctl時把current掛一遍(這一遍必不可少)並為每個fd指定一個回調函數,當設備就緒,喚醒等待隊列上的等待者時,就會調用這個回調函數,而這個回調函數會把就緒的fd加入一個就緒鏈表)。epoll_wait的工作實際上就是在這個就緒鏈表中查看有沒有就緒的fd(利用schedule_timeout()實現睡一會,判斷一會的效果,和select實現中的第7步是類似的)。
對於第三個缺點,epoll沒有這個限制,它所支持的FD上限是最大可以打開文件的數目,這個數字一般遠大於2048,舉個例子,在1GB內存的機器上大約是10萬左右,具體數目可以cat /proc/sys/fs/file-max察看,一般來說這個數目和系統內存關系很大。
總結:
(1)select,poll實現需要自己不斷輪詢所有fd集合,直到設備就緒,期間可能要睡眠和喚醒多次交替。而epoll其實也需要調用epoll_wait不斷輪詢就緒鏈表,期間也可能多次睡眠和喚醒交替,但是它是設備就緒時,調用回調函數,把就緒fd放入就緒鏈表中,並喚醒在epoll_wait中進入睡眠的進程。雖然都要睡眠和交替,但是select和poll在“醒着”的時候要遍歷整個fd集合,而epoll在“醒着”的時候只要判斷一下就緒鏈表是否為空就行了,這節省了大量的CPU時間。這就是回調機制帶來的性能提升。
(2)select,poll每次調用都要把fd集合從用戶態往內核態拷貝一次,並且要把current往設備等待隊列中掛一次,而epoll只要一次拷貝,而且把current往等待隊列上掛也只掛一次(在epoll_wait的開始,注意這里的等待隊列並不是設備等待隊列,只是一個epoll內部定義的等待隊列)。這也能節省不少的開銷。
參考:linux下select/poll/epoll機制的比較
參考:select、poll、epoll之間的區別總結[整理]【轉】