1. linux 高並發之IO多路復用select、poll和epoll的區別
文件描述符(fd)代表對文件操作的句柄,例如socket套接字。通常,對fd進行讀寫操作需要操作fd,如read(),但read()本身是BIO,即阻塞IO。當對fd調用read()時,如果沒有數據輸入到fd,read()會處於阻塞狀態,直到有數據輸入,read()才會返回。
如果有客戶端連接到伺服器並想要與伺服器通信,那麼伺服器將對表示伺服器的sd(socket也可作為fd)調用read()。此時,如果客戶端有信息進入,read()將返回;否則,read()會一直阻塞。
如果有兩個客戶端連接到伺服器並想要與伺服器通信,怎麼辦?答案是開多線程,讓另一個線程對第二個客戶端調用read(),並阻塞到客戶端有信息進入伺服器為止。但這樣會出現問題,實際應用中,客戶端數量可能不止幾個,可能有成千上萬個客戶端想要與伺服器通信,那麼也要開成千上萬個線程?顯然是不實際的。
因此,解決方案就是IO多路復用。IO多路復用一般有select()、poll()、epoll()方式,它們都是對連接到伺服器的客戶端socket進行監控。例如,現在有100個客戶端socket,那麼就監控這100個,如果這100個socket中有信息進入,則IO多路復用會返回;否則,就阻塞。即IO多路復用可以同時阻塞多個I/O操作,並且可以同時對多個讀操作、多個寫操作的I/O函數進行檢測,直到有數據可讀或可寫(就是監聽多個socket)。
由於阻塞I/O只能阻塞一個I/O操作,而I/O復用模型能夠阻塞多個I/O操作,所以才叫做多路復用。
IO多路復用一般分為:select、poll、epoll三種方式。三個都屬於系統調用。
2.1 select的大致過程如下:
用戶進程調用select()監控用戶指定的多個文件描述符,若沒有一個文件描述符有數據返回,則阻塞;若有文件描述符有數據返回,則會對這個文件描述符調用read()進行讀取數據。
2.2 poll的優缺點如下:
1. 相對於select,poll沒有監聽文件描述符的數目上限。
2. 由於poll監聽文件描述符的方式都是輪詢,與select一樣,所以poll在高並發下的表現也不是特別好。
2.3 epoll的大致工作流程如下:
我們在調用epoll_create時,在內核cache里建了個紅黑樹用於存儲以後epoll_ctl傳來的socket外,還會再建立一個rdllist雙向鏈表,用於存儲准備就緒的文件描述符fd,當epoll_wait調用時,僅僅觀察這個rdllist雙向就緒鏈表裡有沒有數據即可。有數據就返回,沒有數據就sleep,等到timeout時間到後即使鏈表沒數據也返回。所以,epoll_wait非常高效。
所有添加到epoll中的fd都會與設備(如網卡)驅動程序建立回調關系,也就是說相應fd的監聽事件發生時會調用這里的回調方法。這個回調方法在內核中叫做ep_poll_callback,它會把這樣的fd放到上面的rdllist雙向就緒鏈表中。
epoll特點:
2.3.1 操作epoll的介面
epoll_create (int size):創建(即返回)一個epfd句柄,參數size表明內核要監聽的描述符數量。調用成功時返回一個epoll句柄描述符,失敗時返回-1。
epoll_ctl (int epfd, int op, int fd, struct epoll_event *event):注冊要監聽的時間類型。
epoll_wait (int epfd, struct epoll_event *events, int maxevents, int timeout):等待事件的就緒。
2.3.2 epoll的兩種觸發模式(LT和ET)
epoll有LT和ET兩種觸發模式,LT是默認的模式,ET是高速的模式。
ET模式在很大程度上降低了同一個epoll事件被重復觸發的次數,因此ET模式效率比LT模式高。
2.3.3 對比select和poll的遺留缺點,epoll的解決方法
第一個:select和poll每次調用時都會反復在用戶態和內核態中來回復制fd集合。
epoll的解決方案是:調用epoll_wait,相當於調用select、poll。而在epoll_ctl期間,把需要監聽的fd一次性拷貝到內核,這樣就避免了調用epoll_wait時把fd集合重復拷貝。
第二個:select和poll存在大量無效輪詢。
epoll並不是像select一樣去逐個輪詢的監控fd的事件狀態,而是事先就建立了fd與之對應的回調函數,當事件激活後主動回調callback函數,這也就避免了遍歷事件列表的這個操作,所以epoll並不會像select和poll一樣隨著監控的fd變多而效率降低。
第三個:文件描述符上限的限制。
epoll沒有最大監聽文件描述符數目的限制,它所支持的FD上限是最大可以打開文件的數目,這個數字一般遠大於2048,舉個例子,在1GB內存的機器上大約是10萬左右,具體數目可以cat /proc/sys/fs/file-max察看,一般來說這個數目和系統內存關系很大。
2. linux select函數解析以及事例
Linux系統中的I/O操作模式解析,主要涉及阻塞、非阻塞和I/O多路復用三種,其中前兩者屬於同步I/O,而多路復用是非同步操作。同步I/O如阻塞I/O,如recv()函數,當數據未准備好時,進程會阻塞等待,直到數據接收完成。非阻塞模式雖然避免了等待,但可能導致CPU資源浪費。而I/O多路復用,如select函數,允許一個線程同時監測多個套接字,當其中任一有數據時,函數會立即返回,節省了線程和資源的開銷。
select函數的用法包括清零fd_set(FD_ZERO)、添加待監測的fd(FD_SET)和檢查fd狀態(FD_ISSET)。其工作流程是:首先清零fd_set,然後將需要監測的文件描述符(fd)加入,調用select函數進行監測,最後根據FD_ISSET檢查結果處理數據。在實際應用中,例如伺服器端,可以利用select監控客戶端連接,一旦有新連接,即可進行處理。
在使用示例中,伺服器端通過select持續監聽客戶端連接,一旦接收到數據,即進行相應的邏輯處理。這簡化了資源管理和提高程序效率。
3. 同步與非同步,阻塞與非阻塞的區別,以及select,poll和epoll
非同步的概念和同步相對。
(1)當一個同步調用發出後,調用者要一直等待返回消息(結果)通知後,才能進行後續的執行;
(2)當一個非同步過程調用發出後,調用者不能立刻得到返回消息(結果)。實際處理這個調用的部件在完成後,通過 狀態、通知和回調 來通知調用者。
這里提到執行部件和調用者通過三種途徑返回結果:狀態、通知和回調。使用哪一種通知機制,依賴於執行部件的實現,除非執行部件提供多種選擇,否則不受調用者控制。
(A)阻塞調用是指調用結果返回之前,當前線程會被掛起,一直處於等待消息通知,不能夠執行其他業務
(B)非阻塞調用是指在不能立刻得到結果之前,該函數不會阻塞當前線程,而會立刻返回
場景比喻:
舉個例子,比如我去銀行辦理業務,可能會有兩種方式:
在上面的場景中,如果:
a)如果選擇排隊(同步),且排隊的時候什麼都不幹(線程被掛起,什麼都幹不了),是同步阻塞模型;
b)如果選擇排隊(同步),但是排隊的同時做與辦銀行業務無關的事情,比如抽煙,(線程沒有被掛起,還可以干一些其他的事),是同步非阻塞模型;
c)如果選擇拿個小票,做在位置上等著叫號(通知),但是坐在位置上什麼都不幹(線程被掛起,什麼都幹不了),這是非同步阻塞模型;
d)如果選擇那個小票,坐在位置上等著叫號(通知),但是坐著的同時還打電話談生意(線程沒有被掛起,還可以干其他事情),這是非同步非阻塞模型。
對這四種模型做一個總結:
1:同步阻塞模型,效率最低,即你專心排隊,什麼都不幹。
2:非同步阻塞,效率也非常低,即你拿著號等著被叫(通知),但是坐那什麼都不幹
3:同步非阻塞,效率其實也不高,因為涉及到線程的來回切換。即你在排隊的同時打電話或者抽煙,但是你必須時不時得在隊伍中挪動。程序需要在排隊和打電話這兩種動作之間來回切換,系統開銷可想而知。
4:非同步非阻塞,效率很高,你拿著小票在那坐著等叫號(通知)的同時,打電話談你的生意。
linux下幾個基本概念
1:用戶控制項和內核空間。 現代操作系統都是採用虛擬存儲器,在32位操作系統下,它的定址空間(虛擬存儲空間)為4G(2的32次方)。為了保證用戶進程補鞥呢直接操作內核,保證內核的安全,操作系統將虛擬空間劃分為兩部分,一部分為內核空間,一部分為用戶空間。對linux操作系統而言,將最高的1G位元組空間分給了內核使用,稱為內核空間,將較低的3G位元組的空間劃分為用戶空間。
2:進程切換很耗資源 ,為了控制進程的執行,內核必須有能力掛起正在cpu上運行的進程,並恢復以前掛起的某個進程的執行,這種行為叫進程的切換。每次切換,要保存上一個的上下文環境等等,總之記住進程切換很耗資源。
3:文件描述符 :文件描述符在形式上是一個非負整數。實際上,他是一個索引,指向內核為每個進程所維護的該進程打開文件的記錄表。當程序打開一個文件時,內核就會向進程返回一個非負整數的文件描述符。但是文件描述符一般在unix,linux系統中才講。
緩存IO ,大多數系統的默認IO操作都是緩存IO,在linux的緩存IO機制中,操作系統會將IO的數據緩存在系統的頁緩存(page cache)中,也就是說,數據會先被拷貝到操作系統內核的緩沖區,然後才會從操作系統內核的緩沖區拷貝到應用程序的地址空間。 緩存IO的缺點: 數據在傳輸過程中需要在應用程序和地址空間和內核進行多次數據拷貝操作,這種數據拷貝操作鎖帶來的cpu以及內存消耗是很大的。
LINUX的IO模型
網路IO的本質是socket的讀取。socket在linux系統被抽象為流,故對網路IO的操作可以理解為對流的操作。
對於一次IO訪問,比如以read操作為例, 數據會先被拷貝到操作系統內核的緩沖區,然後才會從內核緩沖區拷貝到進程的用戶層,即應用程序的地址空間 。故當一個read操作發生時,其實是經歷了兩個階段:
1:內核緩沖區的數據就位
2:數據從內核緩沖區拷貝到用戶程序地址空間
那麼具體到socket io的一次read操來說,這兩步分別是:
1:等待網路上的數據分組到達,然後復制到內核緩沖區中
2:數據從內核緩沖區拷貝到用戶程序的地址空間(緩沖區)
所以說 網路應用要處理的無非就兩個問題:網路IO和數據計算 ,一般來說網路io帶來的延遲影響比較大。
網路IO的模型大致有如下幾種:
熟悉不? 我們常說的select,poll和epoll就是屬於同步模型中多路復用IO的不同實現方法罷了。 下面分別對同步阻塞,同步不阻塞,同步io復用進行說明。
一:同步阻塞
它是最簡單也最常用的網路IO模型。linux下默認的socket都是blocking的。
從圖中可以看到,用戶進程調用recvfrom這個系統調用後,就處於阻塞狀態。然後kernel就開始了IO的第一個階段:數據准備。等第一個階段准備完成之後,kernel開始第二階段,將數據從內核緩沖區拷貝到用戶程序緩沖區(需要花費一定時間)。然後kernel返回結果(確切的說是recvfrom這個系統調用函數返回結果),用戶進程才結束blocking,重新運行起來。
總結 : 同步阻塞模型下,用戶程序在kernel執行io的兩個階段都被blocking住了 。但是優點也是因為這個,無延遲能及時返回數據,且程序模型簡單。
二:同步非阻塞
同步非阻塞就是隔一會瞄一下的輪詢方式。同步非阻塞模式其實是可以看做一小段一小段的同步阻塞模式。
三:IO多路復用
由於同步非阻塞方式需要不斷的輪詢,光輪詢就占據了很大一部分過程,且消耗cpu資源。而這個用戶進程可能不止對這個socket的read,可能還有對其他socket的read或者write操作,那人們就想到了一次輪詢的時候,不光只查詢詢一個socket fd,而是在一次輪詢下,查詢多個任務的socket fd的完成狀態,只要有任何一個任務完成,就去處理它。而且,輪詢人不是進程的用戶態,而是有人幫忙就好了。那麼這就是所謂的 IO多路復用 。總所周知的linux下的select,poll和epoll就是這么乾的。。。
selelct調用是內核級別的,selelct輪詢相比較同步非阻塞模式下的輪詢的區別為: 前者可以等待多個socket,能實現同時對多個IO埠的監聽 ,當其中任何一個socket數據准備好了,就返回可讀。 select或poll調用之後,會阻塞進程 ,與blocking IO 阻塞不用在於,此時的select不是等到所有socket數據達到再處理,而是某個socket數據就會返回給用戶進程來處理。
其實select這種相比較同步non-blocking的效果在單個任務的情況下可能還更差一些 ,因為這里調用了select和recvfrom兩個system call,而non-blocking只調用了一個recvfrom,但是 用select的優勢在於它可以同時處理多個socket fd 。
在io復用模型下,對於每一個socket,一般都設置成non-blocking,但是其實 整個用戶進程是一直被block的 ,只不過用戶process不是被socket IO給block住,而是被select這個函數block住的。
與多進程多線程技術相比,IO多路復用的最大優勢是系統開銷小。
一:select
select函數監視多個socket fs,直到有描述符就緒或者超時,函數返回。當select函數返回後,可以通過遍歷fdset,來找到就緒的描述符。select的基本流程為:
二:poll
poll本質上跟select沒有區別,它將用戶傳入的數組拷貝到內核空間,然後查詢每個fd的狀態,如果某個fd的狀態為就緒,則將此fd加入到等待隊列中並繼續遍歷。如果遍歷完所有的fd後發現沒有就緒的,則掛起當前進程,直到設備就緒或者主動超時。被喚醒後它又要再次遍歷fd。
特點:
1:poll沒有最大連接數限制,因為它是用基於鏈表來存儲的,跟selelct直接監聽fd不一樣。
2:同樣的大量的fd的數組被整體復制與用戶態和內核地址空間之間。
3:poll還有一個特點是水平觸發:如果報告了fd後沒有被處理,則下次poll時還會再次報告該fd。
4:跟select一樣,在poll返回後,還是需要通過遍歷fdset來獲取已經就緒的socket。當fd很多時,效率會線性下降。
三:epoll
epoll支持水平觸發和邊緣觸發,最大的特點在於邊緣觸發,它只告訴進程哪些fd剛剛變為就緒態,並且只會通知一次。還有一個特點是,epoll使用「事件」的就緒通知方式,通過epoll_ctl注冊fd,一旦該fd就緒,內核就會採用類似callback的回調機制來激活該fd,epoll_wait便可以收到通知。
沒有最大並發連接的限制,能打開的FD的上限遠大於1024(1G的內存上能監聽約10萬個埠)。
效率提升,不是輪詢的方式,不會隨著FD數目的增加效率下降。只有活躍可用的FD才會調用callback函數;即Epoll最大的優點就在於它只管你「活躍」的連接,而跟連接總數無關,因此在實際的網路環境中,Epoll的效率就會遠遠高於select和poll。
內存拷貝,利用mmap()文件映射內存加速與內核空間的消息傳遞;即epoll使用mmap減少復制開銷。
聊聊同步、非同步、阻塞與非阻塞
聊聊Linux 五種IO模型
聊聊IO多路復用之select、poll、epoll詳解