1. 程式人生 > >Linux系統I/O模型詳解

Linux系統I/O模型詳解

使用 通過 數據加載 導致 字節 gin 某個文件 lec locked

前言

本文從基本的原理上了解用戶空間、內核空間、進程上下文、及系統的五種常用I/O模型,加深對Linux系統的理解。

1. 概念說明

1.1 用戶空間與內核空間

現在操作系統都是采用虛擬存儲器,那麽對32位操作系統而言,它的尋址空間(虛擬存儲空間)為4G(2的32次方)。操心系統的核心是內核,獨立於普通的應用程序,可以訪問受保護的內存空間,也有訪問底層硬件設備的所有權限。為了保證用戶進程不能直接操作內核,保證內核的安全,操心系統將虛擬空間劃分為兩部分,一部分為內核空間,一部分為用戶空間。針對linux操作系統而言,將最高的1G字節(從虛擬地址0xC0000000到0xFFFFFFFF),供內核使用,稱為內核空間,而將較低的3G字節(從虛擬地址0x00000000到0xBFFFFFFF),供各個進程使用,稱為用戶空間。每個進程可以通過系統調用進入內核,因此,Linux內核由系統內的所有進程共享。於是,從具體進程的角度來看,每個進程可以擁有4G字節的虛擬空間。空間分配如下圖所示:

技術分享圖片

有了用戶空間和內核空間,整個linux內部結構可以分為三部分,從最底層到最上層依次是:硬件-->內核空間-->用戶空間。如下圖所示:

技術分享圖片

需要註意的細節問題:

(1) 內核空間中存放的是內核代碼和數據,而進程的用戶空間中存放的是用戶程序的代碼和數據。不管是內核空間還是用戶空間,它們都處於虛擬空間中。

(2) Linux使用兩級保護機制:0級供內核使用,3級供用戶程序使用。

內核態與用戶態:

(1)當一個任務(進程)執行系統調用而陷入內核代碼中執行時,稱進程處於內核運行態(內核態)。此時處理器處於特權級最高的(0級)內核代碼中執行。當進程處於內核態時,執行的內核代碼會使用當前進程的內核棧。每個進程都有自己的內核棧。

(2)當進程在執行用戶自己的代碼時,則稱其處於用戶運行態(用戶態)。此時處理器在特權級最低的(3級)用戶代碼中運行。當正在執行用戶程序而突然被中斷程序中斷時,此時用戶程序也可以象征性地稱為處於進程的內核態。因為中斷處理程序將使用當前進程的內核棧。

參考資料:http://www.cnblogs.com/Anker/p/3269106.html

1.2 進程切換

為了控制進程的執行,內核必須有能力掛起正在CPU上運行的進程,並恢復以前掛起的某個進程的執行。這種行為被稱為進程切換。因此可以說,任何進程都是在操作系統內核的支持下運行的,是與內核緊密相關的。

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

  • 保存處理器上下文,包括程序計數器和其他寄存器。
  • 更新PCB信息。
  • 把進程的PCB移入相應的隊列,如就緒、在某事件阻塞等隊列。
  • 選擇另一個進程執行,並更新其PCB。
  • 更新內存管理的數據結構。
  • 恢復處理器上下文。

1.3 阻塞與非阻塞

關註的是調用者等待被調用者返回調用結果時的狀態。

  • 阻塞:調用結果返回之前,調用者會被掛起(不可中斷睡眠態),調用者只有在得到返回結果之後才能繼續;
  • 非阻塞:調用者在結果返回之前,不會被掛起;即調用不會阻塞調用者,調用者可以繼續處理其他的工作;

1.4 同步與異步

關註的是消息通知機制、狀態;

  • 同步:調用發出之後不會立即返回,但一旦返回則是最終結果;
  • 異步:調用發出之後,被調用方立即返回消息,但返回的並非最終結果;被調用者通過狀態、通知機制等來通知調用者,會通過回調函數處理;

1.5 文件描述符

文件描述符(File descriptor)是計算機科學中的一個術語,是一個用於表述指向文件的引用的抽象化概念。

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

1.6 緩存I/O

緩存 I/O 又被稱作標準 I/O,大多數文件系統的默認 I/O 操作都是緩存 I/O。在 Linux 的緩存 I/O 機制中,操作系統會將 I/O 的數據緩存在文件系統的頁緩存( page cache )中,也就是說,數據會先被拷貝到操作系統內核的緩沖區中,然後才會從操作系統內核的緩沖區拷貝到應用程序的地址空間。

緩存 I/O 的缺點:
數據在傳輸過程中需要在應用程序地址空間和內核進行多次數據拷貝操作,這些數據拷貝操作所帶來的 CPU 以及內存開銷是非常大的。

2. 常用模型介紹

  • 阻塞式I/O:blocking I/O
  • 非阻塞式I/O:non-blocking I/O
  • I/O復用: I/O multiplexing
  • 信號驅動式I/O:signal driven I/O
  • 異步I/O:asynchronous I/O

從磁盤上的一次read操作;用戶發起IO調用

用戶空間進程無法直接訪問硬件;

進程無法直接訪問內核緩沖區;
技術分享圖片

1)用戶空間的進程向linux內核發起IO調用(讀取文件)請求;

2)內核從磁盤中讀取數據;(等待數據完成階段。)

3)內核將數據加載至內核緩沖區;(等待數據完成階段。)

4)內核在將內核緩沖區中的數據復制到用戶空間中的進程內存中(真正執行IO過程的階段)

2.1 阻塞式I/O:blocking I/O

阻塞式I/O模型是最常用的一個模型,也是最簡單的模型。在linux中,所有的套接字默認情況下都是阻塞的。

進程在向內核調用執行recvfrom操作時阻塞,只有當內核將磁盤中的數據復制到內核緩沖區(內核內存空間),並實時復制到進程的緩存區完畢後返回;或者發生錯誤時(系統調用信號被中斷)返回。在加載數據到數據復制完成,整個進程都是被阻塞的,不能處理的別的I/O,此時的進程不再消費CPU時間,而是等待響應的狀態,從處理的角度來看,這是非常有效的。

阻塞式I/O整個過程圖如下:
技術分享圖片

第一階段(阻塞):

  • ①:進程向內核發起系統調用(recvfrom);當進程發起調用後,進程開始掛起(進程進入不可中斷睡眠狀態),進程一直處於等待內核處理結果的狀態,此時的進程不能處理其他I/O,亦被阻塞。
  • ②:內核收到進程的系統調用請求後,此時的數據包並未準備好,此時內核亦不會給進程發送任何消息,直到磁盤中的數據加載至內核緩沖區;

第二階段(阻塞):

  • ③:內核再將內核緩沖區中的數據復制到用戶空間中的進程緩沖區中(真正執行IO過程的階段),直到數據復制完成。
  • ④:內核返回成功數據處理完成的指令給進程;進程在收到指令後再對數據包進程處理;處理完成後,此時的進程解除不可中斷睡眠態,執行下一個I/O操作。

特點:I/O執行的兩個階段進程都是阻塞的。

優點:

? 1)能夠及時的返回數據,無延遲;

? 2)程序簡單,進程掛起基本不會消耗CPU時間;

缺點:

? 1)I/O等待對性能影響較大;

? 2)每個連接需要獨立的一個進程/線程處理,當並發請求量較大時為了維護程序,內存、線程和CPU上下文切換開銷較大,因此較少在開發環境中使用。

2.2 非阻塞式I/O:non-blocking I/O

進程在向內核調用函數recvfrom執行I/O操作時,socket是以非阻塞的形式打開的。也就是說,進程進行系統調用後,內核沒有準備好數據的情況下,會立即返回一個錯誤碼,說明進程的系統調用請求不會立即滿足(WAGAIN或EWOULDBLOCK)。

該模型在進程發起recvfrom系統調用時,進程並沒有被阻塞,內核馬上返回了一個error。進程在收到error,可以處理其他的事物,過一段時間在次發起recvfrom系統調用;其不斷的重復發起recvfrom系統調用,這個過程即為進程輪詢(polling)。輪詢的方式向內核請求數據,直到數據準備好,再復制到用戶空間緩沖區,進行數據處理。需要註意的是,復制過程中進程還是阻塞的。

一般情況下,進程采用輪詢(polling)的機制檢測I/O調用的操作結果是否已完成,會消耗大量的CPU時鐘周期,性能上並不一定比阻塞式I/O高;

非阻塞式I/O模型的整個過程圖如下:
技術分享圖片

第二階段(非阻塞):

  • ①:進程向內核發起IO調用請求,內核接收到進程的I/O調用後準備處理並返回“error”的信息給進程;此後每隔一段時間進程都會想內核發起詢問是否已處理完,即輪詢,此過程稱為為忙等待;
  • ②:內核收到進程的系統調用請求後,此時的數據包並未準備好,此時內核會給進程發送error信息,直到磁盤中的數據加載至內核緩沖區;

第二階段(阻塞):

  • ③:內核再將內核緩沖區中的數據復制到用戶空間中的進程緩沖區中(真正執行IO過程的階段,進程阻塞),直到數據復制完成。
  • ④:內核返回成功數據處理完成的指令給進程;進程在收到指令後再對數據包進程處理;

特點:non-blocking I/O模式需要不斷的主動詢問kernel數據是否已準備好。

優點:進程在等待當前任務完成時,可以同時執行其他任務;進程不會被阻塞在內核等待數據過程,每次發起的I/O請求會立即返回,具有較好的實時性;

缺點:不斷的輪詢將占用大量的CPU時間,系統資源利用率大打折扣,影響性能,整體數據的吞吐量下降;該模型不適用web服務器;

非阻塞式IO相對於阻塞式IO,性能並沒有提升;因為輪詢是很耗資源的。不斷的輪詢意味著內核需要騰出更多的時間來反饋當前的處理情況,很可能沒辦法處理第②步了。

2.3 I/O 復用:I/O multiplexing

I/O multiplexing 模型也成為事件驅動式I/O模型(event driven I/O)。

在該模型中,每一個socket,一般都會設置成non-blocking;進程通過調用內核中的select()、poll()、epoll()函數發起系統調用請求。selec/poll/epoll相當於內核中的代理,進程所有的請求都會先請求這幾個函數中的某一個;此時,一個進程可以同時處理多個網絡連接的I/O;select/poll/epoll這個函數會不斷的輪詢(polling)所負責的socket,當某個socket有數據報準備好了(意味著socket可讀),就會返回可讀的通知信號給進程。

用戶進程調用select/poll/epoll後,進程實際上是被阻塞的,同時,內核會監視所有select/poll/epoll所負責的socket,當其中任意一個數據準備好了,就會通知進程。只不過進程是阻塞在select/poll/epoll之上,而不是被內核準備數據過程中阻塞。此時,進程再發起recvfrom系統調用,將數據中內核緩沖區拷貝到內核進程,這個過程是阻塞的。

雖然select/poll/epoll可以使得進程看起來是非阻塞的,因為進程可以處理多個連接,但是最多只有1024個網絡連接的I/O;本質上進程還是阻塞的,只不過它可以處理更多的網絡連接的I/O而已。

I/O復用模型的整個過程圖如下:
技術分享圖片

第一階段(阻塞在select/poll之上):

  • ①:進程向內核發起select/poll的系統調用,select將該調用通知內核開始準備數據,而內核不會返回任何通知消息給進程,但進程可以繼續處理更多的網絡連接I/O;
  • ②:內核收到進程的系統調用請求後,此時的數據包並未準備好,此時內核亦不會給進程發送任何消息,直到磁盤中的數據加載至內核緩沖區;而後通過select()/poll()函數將socket的可讀條件返回給進程

第二階段(阻塞):

  • ③:進程在收到SIGIO信號程序之後,進程向內核發起系統調用(recvfrom);
  • ④:內核再將內核緩沖區中的數據復制到用戶空間中的進程緩沖區中(真正執行IO過程的階段),直到數據復制完成。
  • ⑤:內核返回成功數據處理完成的指令給進程;進程在收到指令後再對數據包進程處理;處理完成後,此時的進程解除不可中斷睡眠態,執行下一個I/O操作。

特點:通過一種機制能同時等待多個文件描述符,而這些文件描述符(套接字描述符)其中的任意一個變為可讀就緒狀態,select()/poll()函數就會返回。

優點:可以基於一個阻塞對象,同時在多個描述符上可讀就緒,而不是使用多個線程(每個描述符一個線程),即能處理更多的連接;這樣可以節省更多的系統資源。

缺點:如果處理的連接數不是很多的話,使用select/poll的web server不一定比使用multi-threading + blocking I/O的web server性能更好;反而可能延遲還更大;原因在於,處理一個連接數需要發起兩次system call;

2.4 信號驅動式I/O:signal driven I/O

信號驅動式I/O是指進程預先告知內核,使得某個文件描述符上發生了變化時,內核使用信號通知該進程。

在信號驅動式I/O模型,進程使用socket進行信號驅動I/O,並建立一個SIGIO信號處理函數,當進程通過該信號處理函數向內核發起I/O調用時,內核並沒有準備好數據報,而是返回一個信號給進程,此時進程可以繼續發起其他I/O調用。也就是說,在第一階段內核準備數據的過程中,進程並不會被阻塞,會繼續執行。當數據報準備好之後,內核會遞交SIGIO信號,通知用戶空間的信號處理程序,數據已準備好;此時進程會發起recvfrom的系統調用,這一個階段與阻塞式I/O無異。也就是說,在第二階段內核復制數據到用戶空間的過程中,進程同樣是被阻塞的。

信號驅動式I/O的整個過程圖如下:
技術分享圖片

第一階段(非阻塞):

  • ①:進程使用socket進行信號驅動I/O,建立SIGIO信號處理函數,向內核發起系統調用,內核在未準備好數據報的情況下返回一個信號給進程,此時進程可以繼續做其他事情;
  • ②:內核將磁盤中的數據加載至內核緩沖區完成後,會遞交SIGIO信號給用戶空間的信號處理程序;

第二階段(阻塞):

  • ③:進程在收到SIGIO信號程序之後,進程向內核發起系統調用(recvfrom);
  • ④:內核再將內核緩沖區中的數據復制到用戶空間中的進程緩沖區中(真正執行IO過程的階段),直到數據復制完成。
  • ⑤:內核返回成功數據處理完成的指令給進程;進程在收到指令後再對數據包進程處理;處理完成後,此時的進程解除不可中斷睡眠態,執行下一個I/O操作。

特點:借助socket進行信號驅動I/O並建立SIGIO信號處理函數

優點:線程並沒有在第一階段(數據等待)時被阻塞,提高了資源利用率;

缺點:

? 1)在程序的實現上比較困難;

? 2)信號 I/O 在大量 IO 操作時可能會因為信號隊列溢出導致沒法通知。信號驅動 I/O 盡管對於處理 UDP 套接字來說有用,即這種信號通知意味著到達一個數據報,或者返回一個異步錯誤。但是,對於 TCP 而言,信號驅動的 I/O 方式近乎無用,因為導致這種通知的條件為數眾多,每一個來進行判別會消耗很大資源,與前幾種方式相比優勢盡失。

信號通知機制:

水平觸發:指數據報到內核緩沖區準備好之後,內核通知進程後,進程因繁忙未發起recvfrom系統調用;內核會再次發送通知信號,循環往復,直到進程來請求recvfrom系統調用。很明顯,這種方式會頻繁消耗過多的系統資源。

邊緣觸發:內核只會發送一次通知信號。

2.5 異步I/O:asynchronous I/O

異步I/O可以說是在信號驅動式I/O模型上改進而來。

在異步I/O模型中,進程會向內核請求air_read(異步讀)的系統調用操作,會把套接字描述符、緩沖區指針、緩沖區大小和文件偏移一起發給內核,當內核收到後會返回“已收到”的消息給進程,此時進程可以繼續處理其他I/O任務。也就是說,在第一階段內核準備數據的過程中,進程並不會被阻塞,會繼續執行。第二階段,當數據報準備好之後,內核會負責將數據報復制到用戶進程緩沖區,這個過程也是由內核完成,進程不會被阻塞。復制完成後,內核向進程遞交aio_read的指定信號,進程在收到信號後進行處理並處理數據報向外發送。

在進程發起I/O調用到收到結果的過程,進程都是非阻塞的。

異步I/O模型的整個過程圖如下:
技術分享圖片

第一階段(非阻塞):

  • ①:進程向內核請求air_read(異步讀)的系統調用操作,會把套接字描述符、緩沖區指針、緩沖區大小和文件偏移一起發給內核,當內核收到後會返回“已收到”的消息給進程
  • ②:內核將磁盤中的數據加載至內核緩沖區,直到數據報準備好;

第二階段(非阻塞):

  • ③:內核開始復制數據,將準備好的數據報復制到進程內存空間,知道數據報復制完成
  • ④:內核向進程遞交aio_read的返回指令信號,通知進程數據已復制到進程內存中;

特點:第一階段和第二階段都是有內核完成

優點:能充分利用DMA的特性,將I/O操作與計算重疊,提高性能、資源利用率與並發能力

缺點:

? 1)在程序的實現上比較困難;

? 2)要實現真正的異步 I/O,操作系統需要做大量的工作。目前 Windows 下通過 IOCP 實現了真正的異步 I/O。而在 Linux 系統下,Linux 2.6才引入,目前 AIO 並不完善,因此在 Linux 下實現高並發網絡編程時都是以 復用式I/O模型為主。

3. 五種I/O模型總結

阻塞式I/O與非阻塞式I/O的區別:

阻塞式I/O會一直阻塞對應的進程,直至操作完成;

非阻塞式I/O在內核還在準備數據的情況下,會立即返回“error”給對應的進程;

同步I/O和異步I/O的區別:

根據POSIX中的定義:

A synchronous I/O operation causes the requesting process to be blocked until that I/O operation completes;

  • 同步I / O操作會導致請求進程被阻塞,直到I / O操作完成為止;

An asynchronous I/O operation does not cause the requesting process to be blocked;

  • 異步I / O操作不會導致請求進程被阻止;

兩者的區別就在於,同步I/O在“I/O operation” 會將進程阻塞。按照這個定義,並根據圖-7和圖-8所示,阻塞式I/O、非阻塞式 I/O、I/O 復用、信號驅動式I/O都屬於同步I/O;

這裏需要註意的是,非阻塞式I/O並沒有在第一階段並沒有阻塞,但是POSIX中定義的“I/O operation” 只指的整個I/O操作,即recvfrom這個system call的時候。如果數據報沒準備好,不會阻塞進程,一旦數據準備好,recvfrom將內核緩沖區中將數據報拷貝到用戶進程時,這個時候進程是被阻塞的;因此,阻塞式I/O屬於同步I/O

異步I/O則不一樣,當進程發起I/O調用請求後,內核立即返回,進程也不會管這個I/O操作了。直到內核發送通知信號給進程,通知進程數據報已復制到用戶進程,進程直接進進程緩沖區處理數據;因此,進程整個過程一直沒有被阻塞。

如圖-7、圖-8所示
技術分享圖片

<center>圖-7</center>

技術分享圖片

圖-8

這五種 I/O 模型中,前四種屬於同步 I/O,因為其中真正的 I/O 操作(recvfrom)將阻塞進程/線程,只有異步 I/O 模型才與 POSIX 定義的異步 I/O 相匹配。

4. select/poll/epoll對比

select/poll/epoll對比,如下圖所示
技術分享圖片

其中,遍歷相當於查看所有的位置,回調相當於查看對應的位置

Select

POSIX所規定,目前幾乎在所有的平臺上支持,其良好跨平臺支持也是它的一個優點,本質上是通過設置或者檢查存放fd標誌位的數據結構來進行下一步處理

缺點

  • 單個進程可監視的fd數量被限制,即能監聽端口的數量有限,數值存在如下文件裏
cat /proc/sys/fs/file-max
  • 對socket是線性掃描,即采用輪詢的方法,效率較低
  • select采取了內存拷貝方法來實現內核將FD消息通知給用戶空間,這樣一個用來存放大量fd的數據結構,這樣會使得用戶空間和內核空間在傳遞該結構時復制開銷大

poll

本質上和select沒有區別,它將用戶傳入的數組拷貝到內核空間,然後查詢每個fd對應的設備狀態

  • 其沒有最大連接數的限制,原因是它是基於鏈表來存儲的
  • 大量的fd的數組被整體復制於用戶態和內核地址空間之間,而不管這樣的復制是不是有意義
  • poll特點是“水平觸發”,如果報告了fd後,沒有被處理,那麽下次poll時會再次報告該fd
  • 邊緣觸發:只通知一次,epoll用的就是邊緣觸發

epoll

在Linux2.6內核中提出的select和poll的增強版本

  • 支持水平觸發和邊緣觸發,最大的特點在於邊緣觸發,它只告訴進程哪些fd剛剛變為就緒態,並且只會通知一次
  • 使用“事件”的就緒通知方式,通過epoll_ctl註冊fd,一旦該fd就緒,內核就會采用類似callback的回調機制來激活該fd,epoll_wait便可以收到通知

優點:

  • 沒有最大並發連接的限制:能打開的FD的上限遠大於1024(1G的內存能監聽約10萬個端口)
  • 效率提升:非輪詢的方式,不會隨著FD數目的增加而效率下降;只有活躍可用的FD才會調用callback函數,即epoll最大的優點就在於它只管理“活躍”的連接,而跟連接總數無關
  • 內存拷貝,利用mmap()文件映射內存加速與內核空間的消息傳遞;即epoll使用mmap減少復制開銷
  • 文件映射內存直接通過地址空間訪問,效率更高,把文件映射到內存中。

5. httpd和nginx使用的模型?

5.1 httpd使用的模型:

prefork模型是復用性I/O模型,以多進程的方式處理請求;

worker模型是復用性I/O模型,只能不過是以多進程多線程的方式處理請求。

event模型是使用的信號驅動式I/O模型,httpd2.4也實現異步I/O模型。

5.2 nginx使用的模型:

nginx一開始設計就是基於信號驅動式I/O模型,使用邊緣觸發來實現,所以並發能力強、性能好,並且支持異步I/O

能完成基於mmap內存映射機制完成數據的發放。

完!

Linux系統I/O模型詳解