一般聊到所線程或者并發網絡上面的知識面試官都會延伸到此知識點哦!!!
一、相關概念講解
1、同步與異步
同步就是一個任務的完成需要依賴另外一個任務時,只有等待被依賴的任務完成后,依賴的任務才能算完成,這是一種可靠的任務序列。要么成功都成功,失敗都失敗,兩個任務的狀態可以保持一致。
異步是不需要等待被依賴的任務完成,只是通知被依賴的任務要完成什么工作,依賴的任務也立即執行,只要自己完成了整個任務就算完成了。至于被依賴的任務最終是否真正完成,依賴它的任務無法確定,所以它是不可靠的任務序列。
2、堵塞與非堵塞
阻塞和非阻塞這兩個概念與程序(線程)等待消息通知(無所謂同步或者異步)時的狀態有關。也就是說阻塞與非阻塞主要是程序(線程)等待消息通知時的狀態角度來說的。
阻塞調用是指調用結果返回之前,當前線程會被掛起,一直處于等待消息通知,不能夠執行其他業務。函數只有在得到結果之后才會返回。
非阻塞和阻塞的概念相對應,指在不能立刻得到結果之前,該函數不會阻塞當前線程,而會立刻返回。雖然表面上看非阻塞的方式可以明顯的提高CPU的利用率,但是也帶了另外一種后果就是系統的線程切換增加。增加的CPU執行時間能不能補償系統的切換成本需要好好評估。
(a) 如果這個線程在等待當前函數返回時,仍在執行其他消息處理,那這種情況就叫做同步非阻塞;
(b) 如果這個線程在等待當前函數返回時,沒有執行其他消息處理,而是處于掛起等待狀態,那這種情況就叫做同步阻塞;
同步/異步關注的是消息通知的機制,而阻塞/非阻塞關注的是程序(線程)等待消息通知時的狀態。
3、用戶空間與內核空間
現在操作系統都是采用虛擬存儲器,那么對32位操作系統而言,它的尋址空間(虛擬存儲空間)為4G(2的32次方)。操作系統的核心是內核,獨立于普通的應用程序,可以訪問受保護的內存空間,也有訪問底層硬件設備的所有權限。為了保證用戶進程不能直接操作內核(kernel),保證內核的安全,操作系統將虛擬空間劃分為兩部分,一部分為內核空間,一部分為用戶空間。針對linux操作系統而言,將最高的1G字節(從虛擬地址0xC0000000到0xFFFFFFFF),供內核使用,稱為內核空間,而將較低的3G字節(從虛擬地址0x00000000到0xBFFFFFFF),供各個進程使用,稱為用戶空間。
4、進程切換
為了控制進程的執行,內核必須有能力掛起正在CPU上運行的進程,并恢復以前掛起的某個進程的執行。這種行為被稱為進程切換。因此可以說,任何進程都是在操作系統內核的支持下運行的,是與內核緊密相關的。
從一個進程的運行轉到另一個進程上運行,這個過程中經過下面這些變化:
1、保存處理機上下文,包括程序計數器和其他寄存器。
2、更新PCB信息。
3、把進程的PCB移入相應的隊列,如就緒、在某事件阻塞等隊列。
4、選擇另一個進程執行,并更新其PCB。
5、更新內存管理的數據結構。
6、恢復處理機上下文。
注:總而言之就是很耗資源
5、進程的堵塞
正在執行的進程,由于期待的某些事件未發生,如請求系統資源失敗、等待某種操作的完成、新數據尚未到達或無新工作做等,則由系統自動執行阻塞原語(Block),使自己由運行狀態變為阻塞狀態。可見,進程的阻塞是進程自身的一種主動行為,也因此只有處于運行態的進程(獲得CPU),才可能將其轉為阻塞狀態。當進程進入阻塞狀態,是不占用CPU資源的。
6、文件描述符
文件描述符(File descriptor)是計算機科學中的一個術語,是一個用于表述指向文件的引用的抽象化概念。
文件描述符在形式上是一個非負整數。實際上,它是一個索引值,指向內核為每一個進程所維護的該進程打開文件的記錄表。當程序打開一個現有文件或者創建一個新文件時,內核向進程返回一個文件描述符。在程序設計中,一些涉及底層的程序編寫往往會圍繞著文件描述符展開。但是文件描述符這一概念往往只適用于UNIX、Linux這樣的操作系統。
7、緩存
緩存 IO 又被稱作標準 IO,大多數文件系統的默認 IO 操作都是緩存 IO。在 Linux 的緩存 IO 機制中,操作系統會將 IO 的數據緩存在文件系統的頁緩存( page cache )中,也就是說,數據會先被拷貝到操作系統內核的緩沖區中,然后才會從操作系統內核的緩沖區拷貝到應用程序的地址空間。
緩存 IO 的缺點:
數據在傳輸過程中需要在應用程序地址空間和內核進行多次數據拷貝操作,這些數據拷貝操作所帶來的 CPU 以及內存開銷是非常大的。
二、IO模型
網絡IO的本質是socket的讀取,socket在linux系統被抽象為流,IO可以理解為對流的操作。剛才說了,對于一次IO訪問(以read舉例),數據會先被拷貝到操作系統內核的緩沖區中,然后才會從操作系統內核的緩沖區拷貝到應用程序的地址空間。
所以說,當一個read操作發生時,它會經歷兩個階段:
第一階段:等待數據準備 (Waiting for the data to be ready)。
第二階段:將數據從內核拷貝到進程中 (Copying the data from the kernel to the process)。
對于socket流而言,
第一步:通常涉及等待網絡上的數據分組到達,然后被復制到內核的某個緩沖區。
第二步:把數據從內核緩沖區復制到應用進程緩沖區。
網絡應用需要處理的無非就是兩大類問題,網絡IO,數據計算。相對于后者,網絡IO的延遲,給應用帶來的性能瓶頸大于后者。
網絡IO的模型大致有如下幾種:
· 同步模型(synchronous IO)
· 阻塞IO(bloking IO)
· 非阻塞IO(non-blocking IO)
· 多路復用IO(multiplexing IO)
· 信號驅動式IO(signal-driven IO)
· 異步IO(asynchronous IO)注:由于signal driven IO在實際中并不常用,所以我這只提及剩下的四種IO Model。
1、堵塞IO模型
應用程序調用一個 IO 函數,導致應用程序阻塞,等待數據準備好。 如果數據沒有準備好,一直等待…數據準備好了,從內核拷貝到用戶空間,IO 函數返回成功指示。
當調用 recv()函數時,系統首先查是否有準備好的數據。如果數據沒有準備好,那么系統就處于等待狀態。當數據準備好后,將數據從系統緩沖區復制到用戶空間,然后該函數返回。在套接應用程序中,當調用 recv()函數時,未必用戶空間就已經存在數據,那么此時 recv()函數就會處于等待狀態。
2、非堵塞IO模型
我們把一個 SOCKET 接口設置為非阻塞就是告訴內核,當所請求的 I/O 操作無法完成時,不要將進程睡眠,而是返回一個錯誤。這樣我們的 I/O 操作函數將不斷的測試數據是否已經準備好,如果沒有準備好,繼續測試,直到數據準備好為止。在這個不斷測試的過程中,會大量的占用 CPU 的時間。上述模型絕不被推薦。
3、IO 復用模型
由于同步非阻塞方式需要不斷主動輪詢,輪詢占據了很大一部分過程,輪詢會消耗大量的CPU時間,而 “后臺” 可能有多個任務在同時進行,人們就想到了循環查詢多個任務的完成狀態,只要有任何一個任務完成,就去處理它。如果輪詢不是進程的用戶態,而是有人幫忙就好了。那么這就是所謂的 “IO 多路復用”
IO多路復用有兩個特別的系統調用select、poll、epoll函數。select調用是內核級別的,select輪詢相對非阻塞的輪詢的區別在于---前者可以等待多個socket,能實現同時對多個IO端口進行監聽,當其中任何一個socket的數據準好了,就能返回進行可讀,然后進程再進行recvform系統調用,將數據由內核拷貝到用戶進程,當然這個過程是阻塞的。select或poll調用之后,會阻塞進程,與blocking IO阻塞不同在于,此時的select不是等到socket數據全部到達再處理, 而是有了一部分數據就會調用用戶進程來處理。如何知道有一部分數據到達了呢?監視的事情交給了內核,內核負責數據到達的處理。也可以理解為"非阻塞"吧。
I/O復用模型會用到select、poll、epoll函數,這幾個函數也會使進程阻塞,但是和阻塞I/O所不同的的,這兩個函數可以同時阻塞多個I/O操作。而且可以同時對多個讀操作,多個寫操作的I/O函數進行檢測,直到有數據可讀或可寫時(注意不是全部數據可讀或可寫),才真正調用I/O操作函數。
對于多路復用,也就是輪詢多個socket。多路復用既然可以處理多個IO,也就帶來了新的問題,多個IO之間的順序變得不確定了,當然也可以針對不同的編號。
在I/O編程過程中,當需要同時處理多個客戶端接入請求時,可以利用多線程或者I/O多路復用技術進行處理。I/O多路復用技術通過把多個I/O的阻塞復用到同一個select的阻塞上,從而使得系統在單線程的情況下可以同時處理多個客戶端請求。與傳統的多線程/多進程模型比,I/O多路復用的最大優勢是系統開銷小,系統不需要創建新的額外進程或者線程,也不需要維護這些進程和線程的運行,降底了系統的維護工作量,節省了系統資源,I/O多路復用的主要應用場景如下:
1、服務器需要同時處理多個處于監聽狀態或者多個連接狀態的套接字。
2、服務器需要同時處理多種網絡協議的套接字。
此時你是不是想到的了redis如何做的啊,redis用的就是多路復用。
3、信號驅動IO
簡介:兩次調用,兩次返回;
首先我們允許套接口進行信號驅動 I/O,并安裝一個信號處理函數,進程繼續運行并不阻塞。當數據準備好時,進程會收到一個 SIGIO 信號,可以在信號處理函數中調用 I/O 操作函數處理數據。
4、異步IO模型
相對于同步IO,異步IO不是順序執行。用戶進程進行aio_read系統調用之后,無論內核數據是否準備好,都會直接返回給用戶進程,然后用戶態進程可以去做別的事情。等到socket數據準備好了,內核直接復制數據給進程,然后從內核向進程發送通知。IO兩個階段,進程都是非阻塞的。
Linux提供了AIO庫函數實現異步,但是用的很少。目前有很多開源的異步IO庫,例如libevent、libev、libuv。
5、5種I/O模型的比較
不同 I/O 模型的區別,其實主要在等待數據和數據復制這兩個時間段不同,圖形中已經表示得很清楚了。
通過上面的圖片,可以發現non-blocking IO和asynchronous IO的區別還是很明顯的。在non-blocking IO中,雖然進程大部分時間都不會被block,但是它仍然要求進程去主動的check,并且當數據準備完成以后,也需要進程主動的再次調用recvfrom來將數據拷貝到用戶內存。而asynchronous IO則完全不同。它就像是用戶進程將整個IO操作交給了他人(kernel)完成,然后他人做完后發信號通知。在此期間,用戶進程不需要去檢查IO操作的狀態,也不需要主動的去拷貝數據。
同步非阻塞方式相比同步阻塞方式:
優點:能夠在等待任務完成的時間里干其他活了(包括提交其他任務,也就是 “后臺” 可以有多個任務在同時執行)。
缺點:任務完成的響應延遲增大了,因為每過一段時間才去輪詢一次read操作,而任務可能在兩次輪詢之間的任意時間完成。這會導致整體數據吞吐量的降低。
三、select 、poll 、epoll的區別?
1、支持一個進程所能打開的最大連接數
2、FD (文件描述符)劇增后帶來的 IO 效率問題
3、消息傳遞方式
綜上,在選擇 select,poll,epoll 時要根據具體的使用場合以及這三種方式的自身特點。
1、表面上看 epoll 的性能最好,但是在連接數少并且連接都十分活躍的情況下,select 和 poll 的性能可能比 epoll 好,畢竟 epoll 的通知機制需要很多函數回調。
2、select 低效是因為每次它都需要輪詢。但低效也是相對的,視情況而定,也可通過良好的設計改善
補充知識點:
Level_triggered(水平觸發):當被監控的文件描述符上有可讀寫事件發生時,epoll_wait()會通知處理程序去讀寫。如果這次沒有把數據一次性全部讀寫完(如讀寫緩沖區太小),那么下次調用 epoll_wait()時,它還會通知你在上沒讀寫完的文件描述符上繼續讀寫,當然如果你一直不去讀寫,它會一直通知你!!!如果系統中有大量你不需要讀寫的就緒文件描述符,而它們每次都會返回,這樣會大大降低處理程序檢索自己關心的就緒文件描述符的效率!!!
Edge_triggered(邊緣觸發):當被監控的文件描述符上有可讀寫事件發生時,epoll_wait()會通知處理程序去讀寫。如果這次沒有把數據全部讀寫完(如讀寫緩沖區太小),那么下次調用 epoll_wait()時,它不會通知你,也就是它只會通知你一次,直到該文件描述符上出現第二次可讀寫事件才會通知你!!!這種模式比水平觸發效率高,系統不會充斥大量你不關心的就緒文件描述符!!
select(),poll()模型都是水平觸發模式,信號驅動 IO 是邊緣觸發模式,epoll()模型即支持水平觸發,也支持邊緣觸發,默認是水平觸發。
【謝謝你的關注】