【IO】Linux下的五種IO模型

A minor發表於2020-12-27

Linux 提供了五種 IO 模型,包括:阻塞IO、非阻塞IO、IO多路複用、訊號驅動IO、非同步IO。

當使用者程式請求I/O操作,發起對socket套接字的讀操作時,該使用者程式會執行一個系統呼叫,將本程式的控制權移交給核心。在unix作業系統中,一個IO操作主要經過兩個階段:

  1. 等待資料準備;等資料流到來後,會將它從網路卡複製到核心空間的緩衝區(階段一)
  2. 將資料從核心空間拷貝到使用者程式中;將資料從核心緩衝區向使用者程式緩衝區複製(階段二)

在這裡插入圖片描述

PS:當程式執行在核心態時,會產生上下文切換,並且在應用程式看來,其自身使用者程式此時是阻塞等待的

1.阻塞 IO 模型

Linux作業系統中,這就是一種最簡單的IO模型,即阻塞IO。一般表現為程式或執行緒等待某個條件,如果條件不滿足,則一直等下去。條件滿足,則進行下一步操作。

當使用者執行緒發出 IO 請求之後,核心會去檢視資料是否就緒,如果沒有就緒就會等待資料就緒,而使用者執行緒就會處於阻塞狀態,使用者執行緒交出 CPU。當資料就緒之後,核心會將資料拷貝到使用者執行緒,並返回結果給使用者執行緒,使用者執行緒才解除 block 狀態。

典型的阻塞 IO 模型的例子為:

data = socket.read();

如果資料沒有就緒,就會一直阻塞在 read 方法。

在這裡插入圖片描述

recvfrom 是 Linux 系統提供給使用者用於接收網路IO的系統介面,作用是從套接字上接收一個訊息。

ssize_t recvfrom(int sockfd,void *buf,size_t len,unsigned int flags, struct sockaddr *from,socket_t *fromlen);
  • 成功執行時,返回接收到的位元組數
  • 另一端已關閉時,返回0
  • 失敗返回-1,errno被設為以下的某個值
    • EAGAIN:套接字已標記為非阻塞,而接收操作被阻塞或者接收超時
    • EBADF:sock不是有效的描述詞
    • ECONNREFUSE:遠端主機阻絕網路連線
    • EFAULT:記憶體空間訪問出錯
    • EINTR:操作被訊號中斷
    • EINVAL:引數無效
    • ENOMEM:記憶體不足
    • ENOTCONN:與面向連線關聯的套接字尚未被連線上
    • ENOTSOCK:sock索引的不是套接字


recvfrom 很關鍵,因為前4種IO模型都設計此係統呼叫。

2.非阻塞 IO 模型

當使用者執行緒發起一個 read 操作後,並不需要等待,而是馬上就得到了一個結果。如果結果是一個 error 時,它就知道資料還沒有準備好,於是它可以再次傳送 read 操作。一旦核心中的資料準備好了,並且又再次收到了使用者執行緒的請求,那麼它馬上就將資料拷貝到了使用者執行緒,然後返回。

所以事實上,在非阻塞 IO 模型中,使用者執行緒需要不斷地詢問核心資料是否就緒,也就說非阻塞 IO 不會交出 CPU,而會一直佔用 CPU。

典型的非阻塞 IO 模型一般如下:

while(true){
	data = socket.read();   
	if(data!= error){
		process(data);
         break;
    }   
}	

但是對於非阻塞 IO 就有一個非常嚴重的問題,在 while 迴圈中需要不斷地去詢問核心資料是否就緒,這樣會導致 CPU 佔用率非常高,因此一般情況下很少使用 while 迴圈這種方式來讀取資料。

在這裡插入圖片描述

3.多路複用 IO 模型

多路複用 IO 模型是目前使用得比較多的模型。Java NIO 實際上就是多路複用 IO。

在多路複用 IO 模型中,會有一個執行緒不斷去輪詢多個 socket 的狀態,只有當 socket 真正有讀寫事件時,才真正呼叫實際的 IO 讀寫操作。因為在多路複用 IO 模型中,只需要使用一個執行緒就可以管理多個 socket,系統不需要建立新的程式或者執行緒,也不必維護這些執行緒和程式,並且只有在真正有 socket 讀寫事件進行時,才會使用 IO 資源,所以它大大減少了資源佔用。

在 Java NIO 中,是通過 selector.select () 去查詢每個通道是否有到達事件,如果沒有事件,則一直阻塞在那裡,因此這種方式會導致使用者執行緒的阻塞。

也許有朋友會說,我可以採用多執行緒 + 阻塞 IO 達到類似的效果,但是由於在多執行緒 + 阻塞 IO 中,每個 socket 對應一個執行緒,這樣會造成很大的資源佔用,並且尤其是對於長連線來說,執行緒的資源一直不會釋放,如果後面陸續有很多連線的話,就會造成效能上的瓶頸。

而多路複用 IO 模式,通過一個執行緒就可以管理多個 socket,只有當 socket 真正有讀寫事件發生才會佔用資源來進行實際的讀寫操作。因此,多路複用 IO 比較適合連線數比較多的情況。

另外多路複用 IO 為何比非阻塞 IO 模型的效率高是因為在非阻塞 IO 中,不斷地詢問 socket 狀態時通過使用者執行緒去進行的,而在多路複用 IO 中,輪詢每個 socket 狀態是核心在進行的,這個效率要比使用者執行緒要高的多。

不過要注意的是,多路複用 IO 模型是通過輪詢的方式來檢測是否有事件到達,並且對到達的事件逐一進行響應。因此對於多路複用 IO 模型來說,一旦事件響應體很大,那麼就會導致後續的事件遲遲得不到處理,並且會影響新的事件輪詢。

在這裡插入圖片描述

4.訊號驅動 IO 模型

在訊號驅動 IO 模型中,當使用者執行緒發起一個 IO 請求操作,會給對應的 socket 註冊一個訊號函式,然後使用者執行緒會繼續執行,當核心資料就緒時會傳送一個訊號給使用者執行緒,使用者執行緒接收到訊號之後,便在訊號函式中呼叫 IO 讀寫操作來進行實際的 IO 請求操作。這個一般用於 UDP 中,對 TCP 套介面幾乎是沒用的,原因是該訊號產生得過於頻繁,並且該訊號的出現並沒有告訴我們發生了什麼事情

在這裡插入圖片描述

5.非同步 IO 模型

非同步 IO 模型才是最理想的 IO 模型,在非同步 IO 模型中,當使用者執行緒發起 read 操作之後,立刻就可以開始去做其它的事。而另一方面,從核心的角度,當它受到一個 asynchronous read 之後,它會立刻返回,說明 read 請求已經成功發起了,因此不會對使用者執行緒產生任何 block。然後,核心會等待資料準備完成,然後將資料拷貝到使用者執行緒,當這一切都完成之後,核心會給使用者執行緒傳送一個訊號,告訴它 read 操作完成了。也就說使用者執行緒完全不需要關心實際的整個 IO 操作是如何進行的,只需要先發起一個請求,當接收核心返回的成功訊號時表示 IO 操作已經完成,可以直接去使用資料了。

也就說在非同步 IO 模型中,IO 操作的兩個階段都不會阻塞使用者執行緒,這兩個階段都是由核心自動完成,然後傳送一個訊號告知使用者執行緒操作已完成。使用者執行緒中不需要再次呼叫 IO 函式進行具體的讀寫。這點是和訊號驅動模型有所不同的,在訊號驅動模型中,當使用者執行緒接收到訊號表示資料已經就緒,然後需要使用者執行緒呼叫 IO 函式進行實際的讀寫操作;而在非同步 IO 模型中,收到訊號表示 IO 操作已經完成,不需要再在使用者執行緒中呼叫 iO 函式進行實際的讀寫操作。

注意,非同步 IO 是需要作業系統的底層支援,在 Java 7 中,提供了 Asynchronous IO。簡稱 AIO

在這裡插入圖片描述

小結

將這五種 IO 模型放到一起對比一下:

【IO】Linux下的五種IO模型

前面四種 IO 模型實際上都屬於同步 IO,只有最後一種是真正的非同步 IO,因為無論是多路複用 IO 還是訊號驅動模型,IO 操作的第 2 個階段都會引起使用者執行緒阻塞,也就是核心進行資料拷貝的過程都會讓使用者執行緒阻塞。

相關文章