linux io指的是一種文件操作;在Linux中,文件就是一串二進(jìn)制流,那么在信息的交換過程中,我們都是對這些流進(jìn)行數(shù)據(jù)收發(fā)操作,這些操作簡稱為I/O操作;由于Linux使用的是虛擬內(nèi)存機(jī)制,所以必須通過系統(tǒng)調(diào)用請求內(nèi)核來完成IO動作。
本教程操作環(huán)境:linux5.9.8系統(tǒng)、Dell G3電腦。
linux io指的是什么?
我們都知道在Linux的世界,一切皆文件。
而文件就是一串二進(jìn)制流,不管Socket、FIFO、管道還是終端,對我們來說,一切都是流。
-
在信息的交換過程中,我們都是對這些流進(jìn)行數(shù)據(jù)收發(fā)操作,簡稱為I/O操作。
-
往流中讀取數(shù)據(jù),系統(tǒng)調(diào)用Read,寫入數(shù)據(jù),系統(tǒng)調(diào)用Write。
通常用戶進(jìn)程的一個完整的IO分為兩個階段:
磁盤IO:
網(wǎng)絡(luò)IO:
操作系統(tǒng)和驅(qū)動程序運(yùn)行在內(nèi)核空間,應(yīng)用程序運(yùn)行在用戶空間,兩者不能使用指針傳遞數(shù)據(jù),因?yàn)長inux使用的虛擬內(nèi)存機(jī)制,必須通過系統(tǒng)調(diào)用請求內(nèi)核來完成IO動作。
IO有內(nèi)存IO、網(wǎng)絡(luò)IO和磁盤IO三種,通常我們說的IO指的是后兩者!
為什么需要IO模型
如果使用同步的方式來通信的話,所有的操作都在一個線程內(nèi)順序執(zhí)行完成,這么做缺點(diǎn)是很明顯的:
- 因?yàn)橥降耐ㄐ挪僮鲿枞粋€線程的其他任何操作,只有這個操作完成了之后,后續(xù)的操作才可以完成,所以出現(xiàn)了同步阻塞+多線程(每個Socket都創(chuàng)建一個線程對應(yīng)),但是系統(tǒng)內(nèi)線程數(shù)量是有限制的,同時線程切換很浪費(fèi)時間,適合Socket少的情況。
因該需要出現(xiàn)IO模型。
Linux的IO模型
在描述Linux IO模型之前,我們先來了解一下Linux系統(tǒng)數(shù)據(jù)讀取的過程:
以用戶請求index.html文件為例子說明
基本概念
用戶空間和內(nèi)核空間
操作系統(tǒng)的核心是內(nèi)核,獨(dú)立于普通的應(yīng)用程序,可以訪問受保護(hù)的內(nèi)存空間,也有訪問底層硬件設(shè)備的所有權(quán)限。
- 為了保證內(nèi)核的安全,用戶進(jìn)程不能直接操作內(nèi)核,操作系統(tǒng)將虛擬空間劃分為兩部分,一部分為內(nèi)核空間,一部分為用戶空間。
進(jìn)程切換
為了控制進(jìn)程的執(zhí)行,內(nèi)核必須有能力掛起正在CPU上運(yùn)行的進(jìn)程,并恢復(fù)以前掛起的某個進(jìn)程的執(zhí)行。
這種行為被稱為進(jìn)程切換。
因此可以說,任何進(jìn)程都是在操作系統(tǒng)內(nèi)核的支持下運(yùn)行的,是與內(nèi)核緊密相關(guān)的。
進(jìn)程的阻塞
正在執(zhí)行的進(jìn)程,由于期待的某些事件未發(fā)生,如請求系統(tǒng)資源失敗、等待某種操作的完成、新數(shù)據(jù)尚未到達(dá)或無新工作做等,則由系統(tǒng)自動執(zhí)行阻塞原語(Block),使自己由運(yùn)行狀態(tài)變?yōu)樽枞麪顟B(tài)。
可見,進(jìn)程的阻塞是進(jìn)程自身的一種主動行為,也因此只有處于運(yùn)行態(tài)的進(jìn)程(獲得CPU),才可能將其轉(zhuǎn)為阻塞狀態(tài)。
當(dāng)進(jìn)程進(jìn)入阻塞狀態(tài),是不占用CPU資源的。
文件描述符
文件描述符(File Descriptor)是計(jì)算機(jī)科學(xué)中的一個術(shù)語,是一個用于表述指向文件的引用的抽象化概念。
文件描述符在形式上是一個非負(fù)整數(shù),實(shí)際上,它是一個索引值,指向內(nèi)核為每一個進(jìn)程所維護(hù)的該進(jìn)程打開文件的記錄表。
- 當(dāng)程序打開一個現(xiàn)有文件或者創(chuàng)建一個新文件時,內(nèi)核向進(jìn)程返回一個文件描述符。
緩存IO
大多數(shù)文件系統(tǒng)的默認(rèn) IO 操作都是緩存 IO。
其讀寫過程如下:
-
讀操作:操作系統(tǒng)檢查內(nèi)核的緩沖區(qū)有沒有需要的數(shù)據(jù),如果已經(jīng)緩存了,那么就直接從緩存中返回;否則從磁盤、網(wǎng)卡等中讀取,然后緩存在操作系統(tǒng)的緩存中;
-
寫操作:將數(shù)據(jù)從用戶空間復(fù)制到內(nèi)核空間的緩存中。這時對用戶程序來說寫操作就已經(jīng)完成,至于什么時候再寫到磁盤、網(wǎng)卡等中由操作系統(tǒng)決定,除非顯示地調(diào)用了 sync 同步命令。
假設(shè)內(nèi)核空間緩存無需要的數(shù)據(jù),用戶進(jìn)程從磁盤或網(wǎng)絡(luò)讀數(shù)據(jù)分兩個階段:
-
階段一: 內(nèi)核程序從磁盤、網(wǎng)卡等讀取數(shù)據(jù)到內(nèi)核空間緩存區(qū);
-
階段二: 用戶程序從內(nèi)核空間緩存拷貝數(shù)據(jù)到用戶空間。
緩存 IO 的缺點(diǎn):
數(shù)據(jù)在傳輸過程中需要在應(yīng)用程序地址空間和內(nèi)核空間進(jìn)行多次數(shù)據(jù)拷貝操作,這些數(shù)據(jù)拷貝操作所帶來的CPU以及內(nèi)存開銷非常大。
同步阻塞
用戶空間的應(yīng)用程序執(zhí)行一個系統(tǒng)調(diào)用,這會導(dǎo)致應(yīng)用程序阻塞,什么也不干,直到數(shù)據(jù)準(zhǔn)備好,并且將數(shù)據(jù)從內(nèi)核復(fù)制到用戶進(jìn)程,最后進(jìn)程再處理數(shù)據(jù),在等待數(shù)據(jù)到處理數(shù)據(jù)的兩個階段,整個進(jìn)程都被阻塞,不能處理別的網(wǎng)絡(luò)IO。
- 調(diào)用應(yīng)用程序處于一種不再消費(fèi) CPU 而只是簡單等待響應(yīng)的狀態(tài),因此從處理的角度來看,這是非常有效的。
這也是最簡單的IO模型,在通常FD較少、就緒很快的情況下使用是沒有問題的。
同步非阻塞
非阻塞的系統(tǒng)調(diào)用調(diào)用之后,進(jìn)程并沒有被阻塞,內(nèi)核馬上返回給進(jìn)程,如果數(shù)據(jù)還沒準(zhǔn)備好,此時會返回一個Error。
-
進(jìn)程在返回之后,可以干點(diǎn)別的事情,然后再發(fā)起系統(tǒng)調(diào)用。
-
重復(fù)上面的過程,循環(huán)往復(fù)的進(jìn)行系統(tǒng)調(diào)用。這個過程通常被稱之為輪詢。
-
輪詢檢查內(nèi)核數(shù)據(jù),直到數(shù)據(jù)準(zhǔn)備好,再拷貝數(shù)據(jù)到進(jìn)程,進(jìn)行數(shù)據(jù)處理。
-
需要注意,拷貝數(shù)據(jù)整個過程,進(jìn)程仍然是屬于阻塞的狀態(tài)。
-
這種方式在編程中對Socket設(shè)置O_NONBLOCK即可。
IO多路復(fù)用
IO多路復(fù)用,這是一種進(jìn)程預(yù)先告知內(nèi)核的能力,讓內(nèi)核發(fā)現(xiàn)進(jìn)程指定的一個或多個IO條件就緒了,就通知進(jìn)程。
使得一個進(jìn)程能在一連串的事件上等待。
IO復(fù)用的實(shí)現(xiàn)方式目前主要有select、Poll和Epoll。
偽代碼描述IO多路復(fù)用:
while(status?==?OK)?{?//?不斷輪詢?ready_fd_list?=?io_wait(fd_list);?//內(nèi)核緩沖區(qū)是否有準(zhǔn)備好的數(shù)據(jù)?for(fd?in?ready_fd_list)?{??data?=?read(fd)?//?有準(zhǔn)備好的數(shù)據(jù)讀取到用戶緩沖區(qū)??process(data)?}}
信號驅(qū)動
首先我們允許Socket進(jìn)行信號驅(qū)動IO,并安裝一個信號處理函數(shù),進(jìn)程繼續(xù)運(yùn)行并不阻塞。
當(dāng)數(shù)據(jù)準(zhǔn)備好時,進(jìn)程會收到一個SIGIO信號,可以在信號處理函數(shù)中調(diào)用I/O操作函數(shù)處理數(shù)據(jù)。
流程如下:
-
開啟套接字信號驅(qū)動IO功能
-
系統(tǒng)調(diào)用Sigaction執(zhí)行信號處理函數(shù)(非阻塞,立刻返回)
-
數(shù)據(jù)就緒,生成Sigio信號,通過信號回調(diào)通知應(yīng)用來讀取數(shù)據(jù)
此種IO方式存在的一個很大的問題:Linux中信號隊(duì)列是有限制的,如果超過這個數(shù)字問題就無法讀取數(shù)據(jù)
異步非阻塞
異步IO流程如下所示:
-
當(dāng)用戶線程調(diào)用了aio_read系統(tǒng)調(diào)用,立刻就可以開始去做其它的事,用戶線程不阻塞
-
內(nèi)核就開始了IO的第一個階段:準(zhǔn)備數(shù)據(jù)。當(dāng)內(nèi)核一直等到數(shù)據(jù)準(zhǔn)備好了,它就會將數(shù)據(jù)從內(nèi)核內(nèi)核緩沖區(qū),拷貝到用戶緩沖區(qū)
-
內(nèi)核會給用戶線程發(fā)送一個信號,或者回調(diào)用戶線程注冊的回調(diào)接口,告訴用戶線程Read操作完成了
-
用戶線程讀取用戶緩沖區(qū)的數(shù)據(jù),完成后續(xù)的業(yè)務(wù)操作
相對于同步IO,異步IO不是順序執(zhí)行。
用戶進(jìn)程進(jìn)行aio_read系統(tǒng)調(diào)用之后,無論內(nèi)核數(shù)據(jù)是否準(zhǔn)備好,都會直接返回給用戶進(jìn)程,然后用戶態(tài)進(jìn)程可以去做別的事情。
等到數(shù)據(jù)準(zhǔn)備好了,內(nèi)核直接復(fù)制數(shù)據(jù)給進(jìn)程,然后從內(nèi)核向進(jìn)程發(fā)送通知。
對比信號驅(qū)動IO,異步IO的主要區(qū)別在于:
- 信號驅(qū)動由內(nèi)核告訴我們何時可以開始一個IO操作(數(shù)據(jù)在內(nèi)核緩沖區(qū)中),而異步IO則由內(nèi)核通知IO操作何時已經(jīng)完成(數(shù)據(jù)已經(jīng)在用戶空間中)。
異步IO又叫做事件驅(qū)動IO,在unix中,為異步方式訪問文件定義了一套庫函數(shù),定義了AIO的一系列接口。
- 使用aio_read或者aio_write發(fā)起異步IO操作,使用aio_error檢查正在運(yùn)行的IO操作的狀態(tài)。
目前Linux中AIO的內(nèi)核實(shí)現(xiàn)只對文件IO有效,如果要實(shí)現(xiàn)真正的AIO,需要用戶自己來實(shí)現(xiàn)。
目前有很多開源的異步IO庫,例如libevent、libev、libuv。
相關(guān)推薦:《Linux視頻教程》