1. 程式人生 > 實用技巧 >I/O模式與I/O多路複用

I/O模式與I/O多路複用

一、使用者空間與核心空間

現在作業系統都採用虛擬定址,處理器先產生一個虛擬地址,通過地址翻譯成實體地址(記憶體的地址),再通過匯流排的傳遞,最後處理器拿到某個實體地址返回的位元組。

對32位作業系統而言,它的定址空間(虛擬儲存空間)為4G(2的32次方)。作業系統的核心是核心,獨立於普通的應用程式,可以訪問受保護的記憶體空間,也有訪問底層硬體裝置的所有許可權。為了保證使用者程序不能直接操作核心(kernel),保證核心的安全,操心繫統將虛擬空間劃分為兩部分,一部分為核心空間,一部分為使用者空間

什麼是核心空間,什麼是使用者空間? 

核心空間,使用者空間這些術語讓人有點發蒙。空間的本質就是記憶體。一堆符號叫程式;跑起來之後叫程序;記憶體條插入主機板,跑上程式,就變成了空間,

被作業系統佔用的叫核心空間,被使用者程序佔用的叫使用者空間。

  針對linux作業系統而言,將最高的1G位元組(從虛擬地址0xC0000000到0xFFFFFFFF),供核心使用,稱為核心空間,而將較低的3G位元組(從虛擬地址0x00000000到0xBFFFFFFF),供各個程序使用,稱為使用者空間

補充:地址空間就是一個非負整數地址的有序集合。如{0,1,2...}。

1.1 程序的上下文切換

為了控制程序的執行,核心必須有能力掛起正在CPU上執行的程序,並恢復以前掛起的某個程序的執行。這種行為被稱為程序切換(也叫排程)。因此可以說,任何程序都是在作業系統核心的支援下執行的,是與核心緊密相關的。

從一個程序的執行轉到另一個程序上執行,這個過程中經過下面這些變化


1.儲存當前程序A的上下文

  上下文就是核心再次喚醒當前程序時所需要的狀態,由一些物件(程式計數器、狀態暫存器、使用者棧等各種核心資料結構)的值組成。

  這些值包括描繪地址空間的頁表、包含程序相關資訊的程序表、檔案表等。
2.切換頁全域性目錄以安裝一個新的地址空間

    ...
3.恢復程序B的上下文

  可以理解成一個比較耗資源的過程。

1.2 程序的阻塞

正在執行的程序,由於期待的某些事件未發生,如請求系統資源失敗、等待某種操作的完成、新資料尚未到達或無新工作做等,則由系統自動執行阻塞原語(Block),使自己由執行狀態變為阻塞狀態。可見,程序的阻塞是程序自身的一種主動行為,也因此只有處於執行態的程序(獲得CPU),才可能將其轉為阻塞狀態

當程序進入阻塞狀態,是不佔用CPU資源的

1.3 檔案描述符

  檔案描述符(File descriptor)是電腦科學中的一個術語,是一個用於表述指向檔案的引用的抽象化概念。

  檔案描述符在形式上是一個非負整數。實際上,它是一個索引值,指向核心為每一個程序所維護的該程序開啟檔案的記錄表。當程式開啟一個現有檔案或者建立一個新檔案時,核心向程序返回一個檔案描述符。在程式設計中,一些涉及底層的程式編寫往往會圍繞著檔案描述符展開。但是檔案描述符這一概念往往只適用於UNIX、Linux這樣的作業系統。

1.4 直接I/O和快取I/O

  快取 I/O 又被稱作標準 I/O,大多數檔案系統的預設 I/O 操作都是快取 I/O。在 Linux 的快取 I/O 機制中,以write為例,資料會先被拷貝程序緩衝區,在拷貝到作業系統核心的緩衝區中,然後才會寫到儲存裝置中。

二、I/O模式

對於一次IO訪問(這回以read舉例),資料會先被拷貝到作業系統核心的緩衝區中,然後才會從作業系統核心的緩衝區拷貝到應用程式的緩衝區,最後交給程序。所以說,當一個read操作發生時,它會經歷兩個階段:
  1. 等待資料準備 (Waiting for the data to be ready)
  2. 將資料從核心拷貝到程序中 (Copying the data from the kernel to the process)

正式因為這兩個階段,linux系統產生了下面五種網路模式的方案:
  -- 阻塞 I/O(blocking IO)
  -- 非阻塞 I/O(nonblocking IO)
  -- I/O 多路複用( IO multiplexing)
  -- 訊號驅動 I/O( signal driven IO)
  -- 非同步 I/O(asynchronous IO)

  注:由於signal driven IO在實際中並不常用,所以我這隻提及剩下的四種IO 模型。

1.1 阻塞I/O

阻塞I/O模型示意圖:

read為例:

(1)程序發起read,進行recvfrom系統呼叫;

(2)核心開始第一階段,準備資料(從磁碟拷貝到緩衝區),程序請求的資料並不是一下就能準備好;準備資料是要消耗時間的;

(3)與此同時,程序阻塞(程序是自己選擇阻塞與否),等待資料ing;

(4)直到資料從核心拷貝到了使用者空間,核心返回結果,程序解除阻塞。

也就是說,核心準備資料資料從核心拷貝到程序記憶體地址這兩個過程都是阻塞的。

2.2 non-block(非阻塞I/O模型)

可以通過設定socket使其變為non-blocking。當對一個non-blocking socket執行讀操作時,流程是這個樣子:

(1)當用戶程序發出read操作時,如果kernel中的資料還沒有準備好;

(2)那麼它並不會block使用者程序,而是立刻返回一個error,從使用者程序角度講 ,它發起一個read操作後,並不需要等待,而是馬上就得到了一個結果;

(3)使用者程序判斷結果是一個error時,它就知道資料還沒有準備好,於是它可以再次傳送read操作。一旦kernel中的資料準備好了,並且又再次收到了使用者程序的system call;

(4)那麼它馬上就將資料拷貝到了使用者記憶體,然後返回。

所以,nonblocking IO的特點是使用者程序核心準備資料的階段需要不斷的主動詢問資料好了沒有

2.3 I/O多路複用

I/O多路複用實際上就是用select, poll, epoll監聽多個io物件,當io物件有變化(有資料)的時候就通知使用者程序。好處就是單個程序可以處理多個socket。當然具體區別我們後面再討論,現在先來看下I/O多路複用的流程:

(1)當用戶程序呼叫了select,那麼整個程序會被block;

(2)而同時,kernel會“監視”所有select負責的socket;

(3)當任何一個socket中的資料準備好了,select就會返回;

(4)這個時候使用者程序再呼叫read操作,將資料從kernel拷貝到使用者程序。

所以,I/O 多路複用的特點是通過一種機制一個程序能同時等待多個檔案描述符,而這些檔案描述符(套接字描述符)其中的任意一個進入讀就緒狀態,select()函式就可以返回

這個圖和blocking IO的圖其實並沒有太大的不同,事實上,還更差一些。因為這裡需要使用兩個system call (select 和 recvfrom),而blocking IO只調用了一個system call (recvfrom)。但是,用select的優勢在於它可以同時處理多個connection

所以,如果處理的連線數不是很高的話,使用select/epoll的web server不一定比使用多執行緒+ 阻塞 IO的web server效能更好,可能延遲還更大。

select/epoll的優勢並不是對於單個連線能處理得更快,而是在於能處理更多的連線。

在IO multiplexing Model中,實際中,對於每一個socket,一般都設定成為non-blocking,但是,如上圖所示,整個使用者的process其實是一直被block的。只不過process是被select這個函式block,而不是被socket IO給block。

2.4asynchronous I/O(非同步 I/O)

真正的非同步I/O很牛逼,流程大概如下:

(1)使用者程序發起read操作之後,立刻就可以開始去做其它的事。

(2)而另一方面,從kernel的角度,當它受到一個asynchronous read之後,首先它會立刻返回,所以不會對使用者程序產生任何block。

(3)然後,kernel會等待資料準備完成,然後將資料拷貝到使用者記憶體,當這一切都完成之後,kernel會給使用者程序傳送一個signal,告訴它read操作完成了。

2.5 小結

(1)blocking和non-blocking的區別

呼叫blocking IO會一直block住對應的程序直到操作完成,而non-blocking IO在kernel還準備資料的情況下會立刻返回。

(2)synchronous同步 IO和asynchronous非同步 IO的區別

在說明synchronous IO和asynchronous IO的區別之前,需要先給出兩者的定義。POSIX的定義是這樣子的:
    - A synchronous I/O operation causes the requesting process to be blocked until that I/O operation completes;
    - An asynchronous I/O operation does not cause the requesting process to be blocked;

兩者的區別就在於synchronous IO做”IO operation”的時候會將process阻塞。按照這個定義,之前所述的blocking IO,non-blocking IO,IO multiplexing都屬於synchronous IO。

有人會說,non-blocking IO並沒有被block啊。這裡有個非常“狡猾”的地方,定義中所指的”IO operation”是指真實的IO操作,就是例子中的recvfrom這個system call。non-blocking IO在執行recvfrom這個system call的時候,如果kernel的資料沒有準備好,這時候不會block程序。但是,當kernel中資料準備好的時候,recvfrom會將資料從kernel拷貝到使用者記憶體中,這個時候程序是被block了,在這段時間內,程序是被block的。

而asynchronous IO則不一樣,當程序發起IO 操作之後,就直接返回再也不理睬了,直到kernel傳送一個訊號,告訴程序說IO完成。在這整個過程中,程序完全沒有被block。

(3)non-blocking IO和asynchronous IO的區別

可以發現non-blocking IO和asynchronous IO的區別還是很明顯的。

  --在non-blocking IO中,雖然程序大部分時間都不會被block,但是它仍然要求程序去主動的check,並且當資料準備完成以後,也需要程序主動的再次呼叫recvfrom來將資料拷貝到使用者記憶體

  --而asynchronous IO則完全不同。它就像是使用者程序將整個IO操作交給了他人(kernel)完成,然後他人做完後發訊號通知。在此期間,使用者程序不需要去檢查IO操作的狀態,也不需要主動的去拷貝資料。

三、 select/poll/epoll的區別

  首先前文已述I/O多路複用的本質就是用select/poll/epoll,去監聽多個socket物件,如果其中的socket物件有變化,只要有變化,使用者程序就知道了。

  select是不斷輪詢去監聽的socket,socket個數有限制,一般為1024個;

  poll還是採用輪詢方式監聽,只不過沒有個數限制;

  epoll並不是採用輪詢方式去監聽了,而是當socket有變化時通過回撥的方式主動告知使用者程序。