Linux下主要的IO主要分為:阻塞IO(Blocking IO),非阻塞IO(Non-blocking IO),同步IO(Sync IO)和異步IO(Async IO)。 同步:調用端會一直等待服務端響應,直到返回結果。 異步:調用端發起調用之后不會立刻返回,不會等待服務端響應。服務端通過通知機制或者回調函數來通知客戶端。 阻塞:服務端返回結果之前,客戶端線程會被掛起,此時線程不可被CPU調度,線程暫停運行。 非阻塞:在服務端返回前,函數不會阻塞調用端線程,而會立刻返回。
同步異步的區別在於:服務端在拷貝數據時是否阻塞調用端線程;阻塞和非阻塞的區別在於:調用端線程在調用function后是否立刻返回。要理解這些I/O,需要先理解一些基本的概念。
用戶態和核心態
Linux系統中分為核心態(Kernel model)和用戶態(User model),CPU會在兩個model之間切換。
-
-
核心態代碼擁有完全的底層資源控制權限,可以執行任何CPU指令,訪問任何內存地址,其占有的處理機是不允許被搶占的。內核態的指令包括:啟動I/O,內存清零,修改程序狀態字,設置時鍾,允許/終止中斷和停機。內核態的程序崩潰會導致PC停機。
-
用戶態是用戶程序能夠使用的指令,不能直接訪問底層硬件和內存地址。用戶態運行的程序必須委托系統調用來訪問硬件和內存。用戶態的指令包括:控制轉移,算數運算,取數指令,訪管指令(使用戶程序從用戶態陷入內核態)。
-
用戶態和核心態的切換
用戶態切換到核心態有三種方式:
a.系統調用 這是用戶態進程主動要求切換到內核態的一種方式,用戶態進程通過系統調用申請使用操作系統提供的服務程序完成工作,比如前例中fork()實際上就是執行了一個創建新進程的系統調用。而系統調用的機制其核心還是使用了操作系統為用戶特別開放的一個中斷來實現,例如Linux的int 80h中斷。
b.異常 當CPU在執行運行在用戶態下的程序時,發生了某些事先不可知的異常,這時會觸發由當前運行進程切換到處理此異常的內核相關程序中,也就轉到了內核態,比如缺頁異常。
c.外圍設備的中斷 當外圍設備完成用戶請求的操作后,會向CPU發出相應的中斷信號,這時CPU會暫停執行下一條即將要執行的指令轉而去執行與中斷信號對應的處理程序,如果先前執行的指令是用戶態下的程序,那么這個轉換的過程自然也就發生了由用戶態到內核態的切換。比如硬盤讀寫操作完成,系統會切換到硬盤讀寫的中斷處理程序中執行后續操作等。
進程切換
為了控制進程的執行,內核必須有能力掛起正在CPU上運行的進程,並恢復以前掛起的某個進程的執行。這種行為被稱為進程切換。因此可以說,任何進程都是在操作系統內核的支持下運行的,是與內核緊密相關的。從一個進程的運行轉到另一個進程上運行,這個過程中經過下面這些變化:
-
-
保存處理機上下文,包括程序計數器和其他寄存器。
-
更新PCB信息。
-
把進程的PCB移入相應的隊列,如就緒、在某事件阻塞等隊列。
-
選擇另一個進程執行,並更新其PCB。
-
更新內存管理的數據結構。
-
恢復處理機上下文。
-
進程阻塞
正在執行的進程由於一些事情發生,如請求資源失敗、等待某種操作完成、新數據尚未達到或者沒有新工作做等,由系統自動執行阻塞原語,使進程狀態變為阻塞狀態。因此,進程阻塞是進程自身的一種主動行為,只有處於運行中的進程才可以將自身轉化為阻塞狀態。當進程被阻塞,它是不占用CPU資源的。
文件描述符(fd, File Descriptor)
FD用於描述指向文件的引用的抽象化概念。文件描述符在形式上是一個非負整數。實際上,它是一個索引值,指向內核為每一個進程所維護的該進程打開文件的記錄表。當程序打開一個現有文件或者創建一個新文件時,內核向進程返回一個文件描述符。在程序設計中,一些涉及底層的程序編寫往往會圍繞着文件描述符展開。但是文件描述符這一概念往往只適用於UNIX、Linux這樣的操作系統。
緩存I/O
緩存IO又被稱作標准IO,大多數文件系統的默認IO 操作都是緩存IO。在Linux的緩存IO 機制中,操作系統會將 IO 的數據緩存在文件系統的頁緩存( page cache )中,也就是說,數據會先被拷貝到操作系統內核的緩沖區中,然后才會從操作系統內核的緩沖區拷貝到應用程序的地址空間。
緩存I/O的缺點
數據在傳輸過程中需要在應用程序地址空間和內核進行多次數據拷貝操作,這些數據拷貝操作所帶來的 CPU 以及內存開銷是非常大的。
事件驅動模型
事件驅動模型是一種編程范式,也就是編程思想。這種思想會在我們以后經常性的用到,它與傳統編程思想最大不同的地方在於他是一種非線性的模式。
這個有點不好解釋,我們來看一個例子。
我們用最常見的網頁瀏覽做一個引子,任何的UI編程都是基於事件驅動模型來完成的,當我們的鼠標放在任何一段文字之上,它會根據文字不同而做出對應的不同反應。
並且,我們進入一個網頁不僅僅可以用鼠標與網頁產生交互,也可以使用鍵盤與網頁產生交互,那么這里就會有很多很多種不同的選擇,如果想嘗試用傳統的編程思想來解決識別用戶的操作無疑效率是非常低下的。
傳統編程思想解決方案:
1.死循環來不斷的檢測是否有鼠標點擊,鍵盤按下,鼠標懸浮等等操作。
2.通過阻塞的方式來等待用戶的一次點擊或者鍵盤按下或者鼠標懸浮的等等操作。
這種解決方案看似十分完美,但實際上是非常不明智的,它的缺點如下:
1.死循環占用大量CPU資源,並且如果需要檢測的事件太多勢必會引發延遲問題。
2.通過阻塞方式只能檢測一種操作,並不能同時檢測多種操作。
那么到底有什么方案能夠完美的解決這些問題呢?我們看看UI編程的事件驅動模型的是怎么解決這些問題的:
1. 有一個事件(消息)隊列,包括但不僅是鼠標事件,鍵盤事件,懸浮事件等等。
2. 假設當鼠標按下,便往這個隊列中增加一個點擊事件(消息)。
3. 有一個循環,不斷的從隊列中取出事件,根據不同的事件調用不同的函數。
4. 事件(消息)一般都各自保存各自的處理函數指針,這樣每個消息都有獨立的處理函數。
事件驅動模型圖解:
所以說事件驅動的一大特點就是:
包含一個事件循環並且只有當外部事件發生時才使用回調機制來觸發相應的處理。也就是說程序運行的整個流程都是取決於用戶觸發的各種事件來決定的,開發者並不用關心大體流程,而只是需要做好每一個事件對應的處理方式即可。
Linux下的五種I/O模型
Linux下主要有以下五種I/O模型:
-
阻塞I/O(blocking IO)
-
非阻塞I/O (nonblocking I/O)
-
I/O 復用 (I/O multiplexing)
-
信號驅動I/O (signal driven I/O (SIGIO))
-
異步I/O (asynchronous I/O)
阻塞IO模型
進程會一直阻塞,直到數據拷貝完成 應用程序調用一個IO函數,導致應用程序阻塞,等待數據准備好。數據准備好后,從內核拷貝到用戶空間,IO函數返回成功指示。阻塞IO模型圖
非阻塞IO模型
通過進程反復調用IO函數,在數據拷貝過程中,進程是阻塞的。模型圖如下所示:
IO復用模型
主要是select和epoll。一個線程可以對多個IO端口進行監聽,當socket有讀寫事件時分發到具體的線程進行處理。模型如下所示:
信號驅動IO模型
信號驅動式I/O:首先我們允許Socket進行信號驅動IO,並安裝一個信號處理函數,進程繼續運行並不阻塞。當數據准備好時,進程會收到一個SIGIO信號,可以在信號處理函數中調用I/O操作函數處理數據。過程如下圖所示:
異步IO模型
相對於同步IO,異步IO不是順序執行。用戶進程進行aio_read系統調用之后,無論內核數據是否准備好,都會直接返回給用戶進程,然后用戶態進程可以去做別的事情。等到socket數據准備好了,內核直接復制數據給進程,然后從內核向進程發送通知。IO兩個階段,進程都是非阻塞的。異步過程如下圖所示:
五種IO模型比較
阻塞IO和非阻塞IO的區別 調用阻塞IO后進程會一直等待對應的進程完成,而非阻塞IO不會等待對應的進程完成,在kernel還在准備數據的情況下直接返回。 同步IO和異步IO的區別 首先看一下POSIX中對這兩個IO的定義:
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會一直輪詢(polling),這個過程是沒有阻塞的,但是recvfrom階段blocking IO,non-blocking IO和IO multiplexing都是阻塞的。 而asynchronous IO則不一樣,當進程發起IO 操作之后,就直接返回再也不理睬了,直到kernel發送一個信號,告訴進程說IO完成。在這整個過程中,進程完全沒有被block。
IO復用之select、poll、epoll簡介
epoll是linux所特有,而select是POSIX所規定,一般操作系統均有實現。
select
select本質是通過設置或檢查存放fd標志位的數據結構來進行下一步處理。缺點是:
-
-
單個進程可監視的fd數量被限制,即能監聽端口的大小有限。一般來說和系統內存有關,具體數目可以cat /proc/sys/fs/file-max察看。32位默認是1024個,64位默認為2048個
-
對socket進行掃描時是線性掃描,即采用輪詢方法,效率低。當套接字比較多的時候,每次select()都要遍歷FD_SETSIZE個socket來完成調度,不管socket是否活躍都遍歷一遍。會浪費很多CPU時間。如果能給套接字注冊某個回調函數,當他們活躍時,自動完成相關操作,就避免了輪詢,這正是epoll與kqueue做的
-
需要維護一個用來存放大量fd的數據結構,會使得用戶空間和內核空間在傳遞該結構時復制開銷大
-
poll
poll本質和select相同,將用戶傳入的數據拷貝到內核空間,然后查詢每個fd對應的設備狀態,如果設備就緒則在設備等待隊列中加入一項並繼續遍歷,如果遍歷所有fd后沒有發現就緒設備,則掛起當前進程,直到設備就緒或主動超時,被喚醒后又要再次遍歷fd。它沒有最大連接數的限制,原因是它是基於鏈表來存儲的,但缺點是:
-
-
大量的fd的數組被整體復制到用戶態和內核空間之間,不管有無意義。
-
poll還有一個特點“水平觸發”,如果報告了fd后,沒有被處理,那么下次poll時再次報告該ffd。
-
epoll
epoll支持水平觸發和邊緣觸發,最大特點在於邊緣觸發,只告訴哪些fd剛剛變為就緒態,並且只通知一次。還有一特點是,epoll使用“事件”的就緒通知方式,通過epoll_ctl注冊fd,一量該fd就緒,內核就會采用類似callback的回調機制來激活該fd,epoll_wait便可以收到通知。epoll的優點:
-
-
沒有最大並發連接的限制。
-
效率提升,只有活躍可用的FD才會調用callback函數。
-
內存拷貝,利用mmap()文件映射內存加速與內核空間的消息傳遞。
-
select、poll、epoll區別總結
支持一個進程打開連接數 | IO效率 | 消息傳遞方式 | |
---|---|---|---|
select | 32位機器1024個,64位2048個 | IO效率低 | 內核需要將消息傳遞到用戶空間,都需要內核拷貝動作 |
poll | 無限制,原因基於鏈表存儲 | IO效率低 | 內核需要將消息傳遞到用戶空間,都需要內核拷貝動作 |
epoll | 有上限,但很大,2G內存20W左右 | 只有活躍的socket才調用callback,IO效率高 | 通過內核與用戶空間共享一塊內存來實現 |