『壹』 linux系統I/O模型及select、poll、epoll原理和應用
理解Linux的IO模型之前,首先要了解一些基本概念,才能理解這些IO模型設計的依據
操作系統使用虛擬內存來映射物理內存,對於32位的操作系統來說,虛擬地址空間為4G(2^32)。操作系統的核心是內核,為了保護用戶進程不能直接操作內核,保證內核安全,操作系統將虛擬地址空間劃分為內核空間和用戶空間。內核可以訪問全部的地址空間,擁有訪問底層硬體設備的許可權,普通的應用程序需要訪問硬體設備必須通過 系統調用 來實現。
對於Linux系統來說,將虛擬內存的最高1G位元組的空間作為內核空間僅供內核使用,低3G位元組的空間供用戶進程使用,稱為用戶空間。
又被稱為標准I/O,大多數文件系統的默認I/O都是緩存I/O。在Linux系統的緩存I/O機制中,操作系統會將I/O的數據緩存在頁緩存(內存)中,也就是數據先被拷貝到內核的緩沖區(內核地址空間),然後才會從內核緩沖區拷貝到應用程序的緩沖區(用戶地址空間)。
這種方式很明顯的缺點就是數據傳輸過程中需要再應用程序地址空間和內核空間進行多次數據拷貝操作,這些操作帶來的CPU以及內存的開銷是非常大的。
由於Linux系統採用的緩存I/O模式,對於一次I/O訪問,以讀操作舉例,數據先會被拷貝到內核緩沖區,然後才會從內核緩沖區拷貝到應用程序的緩存區,當一個read系統調用發生的時候,會經歷兩個階段:
正是因為這兩個狀態,Linux系統才產生了多種不同的網路I/O模式的方案
Linux系統默認情況下所有socke都是blocking的,一個讀操作流程如下:
以UDP socket為例,當用戶進程調用了recvfrom系統調用,如果數據還沒准備好,應用進程被阻塞,內核直到數據到來且將數據從內核緩沖區拷貝到了應用進程緩沖區,然後向用戶進程返回結果,用戶進程才解除block狀態,重新運行起來。
阻塞模行下只是阻塞了當前的應用進程,其他進程還可以執行,不消耗CPU時間,CPU的利用率較高。
Linux可以設置socket為非阻塞的,非阻塞模式下執行一個讀操作流程如下:
當用戶進程發出recvfrom系統調用時,如果kernel中的數據還沒准備好,recvfrom會立即返回一個error結果,不會阻塞用戶進程,用戶進程收到error時知道數據還沒准備好,過一會再調用recvfrom,直到kernel中的數據准備好了,內核就立即將數據拷貝到用戶內存然後返回ok,這個過程需要用戶進程去輪詢內核數據是否准備好。
非阻塞模型下由於要處理更多的系統調用,因此CPU利用率比較低。
應用進程使用sigaction系統調用,內核立即返回,等到kernel數據准備好時會給用戶進程發送一個信號,告訴用戶進程可以進行IO操作了,然後用戶進程再調用IO系統調用如recvfrom,將數據從內核緩沖區拷貝到應用進程。流程如下:
相比於輪詢的方式,不需要多次系統調用輪詢,信號驅動IO的CPU利用率更高。
非同步IO模型與其他模型最大的區別是,非同步IO在系統調用返回的時候所有操作都已經完成,應用進程既不需要等待數據准備,也不需要在數據到來後等待數據從內核緩沖區拷貝到用戶緩沖區,流程如下:
在數據拷貝完成後,kernel會給用戶進程發送一個信號告訴其read操作完成了。
是用select、poll等待數據,可以等待多個socket中的任一個變為可讀,這一過程會被阻塞,當某個套接字數據到來時返回,之後再用recvfrom系統調用把數據從內核緩存區復制到用戶進程,流程如下:
流程類似阻塞IO,甚至比阻塞IO更差,多使用了一個系統調用,但是IO多路復用最大的特點是讓單個進程能同時處理多個IO事件的能力,又被稱為事件驅動IO,相比於多線程模型,IO復用模型不需要線程的創建、切換、銷毀,系統開銷更小,適合高並發的場景。
select是IO多路復用模型的一種實現,當select函數返回後可以通過輪詢fdset來找到就緒的socket。
優點是幾乎所有平台都支持,缺點在於能夠監聽的fd數量有限,Linux系統上一般為1024,是寫死在宏定義中的,要修改需要重新編譯內核。而且每次都要把所有的fd在用戶空間和內核空間拷貝,這個操作是比較耗時的。
poll和select基本相同,不同的是poll沒有最大fd數量限制(實際也會受到物理資源的限制,因為系統的fd數量是有限的),而且提供了更多的時間類型。
總結:select和poll都需要在返回後通過輪詢的方式檢查就緒的socket,事實上同時連的大量socket在一個時刻只有很少的處於就緒狀態,因此隨著監視的描述符數量的變多,其性能也會逐漸下降。
epoll是select和poll的改進版本,更加靈活,沒有描述符限制。epoll使用一個文件描述符管理多個描述符,將用戶關系的文件描述符的事件存放到內核的一個事件表中,這樣在用戶空間和內核空間的只需一次。
epoll_create()用來創建一個epoll句柄。
epoll_ctl() 用於向內核注冊新的描述符或者是改變某個文件描述符的狀態。已注冊的描述符在內核中會被維護在一棵紅黑樹上,通過回調函數內核會將 I/O 准備好的描述符加入到一個就緒鏈表中管理。
epoll_wait() 可以從就緒鏈表中得到事件完成的描述符,因此進程不需要通過輪詢來獲得事件完成的描述符。
當epoll_wait檢測到描述符IO事件發生並且通知給應用程序時,應用程序可以不立即處理該事件,下次調用epoll_wait還會再次通知該事件,支持block和nonblocking socket。
當epoll_wait檢測到描述符IO事件發生並且通知給應用程序時,應用程序需要立即處理該事件,如果不立即處理,下次調用epoll_wait不會再次通知該事件。
ET模式在很大程度上減少了epoll事件被重復觸發的次數,因此效率要比LT模式高。epoll工作在ET模式的時候,必須使用nonblocking socket,以避免由於一個文件句柄的阻塞讀/阻塞寫操作把處理多個文件描述符的任務餓死。
【segmentfault】 Linux IO模式及 select、poll、epoll詳解
【GitHub】 CyC2018/CS-Notes
『貳』 什麼是IO埠
就是指介面電路中能夠被 cpu 訪問的寄存器,若干個埠加上邏輯電路組成 io 介面。一般有 數據埠,狀態埠和命令埠。
『叄』 什麼是IO介面
IO介面,又稱為輸入輸出介面,是信息處理系統(例如計算機)與外部世界(可能是人類或另一個信息處理系統)之間的通信。輸入是系統接收的信號或數據,輸出是從其發送的信號或數據。該術語也可以用作行動的一部分;到「執行I / O」是執行輸入或輸出操作。
I / O設備是件硬體中使用由人(或其他系統)與計算機進行通信。例如,鍵盤或計算機滑鼠是計算機的輸入設備,而監視器和列印機是輸出設備。計算機之間的通信設備(如數據機和網卡)通常執行輸入和輸出操作。
將設備指定為輸入或輸出取決於視角。滑鼠和鍵盤採用人體用戶輸出的物理動作,並將其轉換為計算機可以理解的輸入信號;這些設備的輸出是計算機的輸入。同樣,列印機和監視器將計算機輸出的信號作為輸入,並將這些信號轉換為人類用戶可以理解的表示。從用戶的角度來看,閱讀或查看這些表示的過程正在接受輸入;人機交互領域研究了計算機與人類之間的這種類型的交互。
(3)linuxio埠意義擴展閱讀:
在計算機體系結構中,CPU和主存儲器的組合(CPU可以使用單獨的指令直接讀取或寫入)被認為是計算機的大腦。任何信息傳入或傳出CPU /內存組合,例如通過從磁碟驅動器讀取數據,被認為是I / O。[1]CPU及其支持電路可以提供用於低級計算機編程的存儲器映射I / O,例如用於實現設備驅動程序,或者可以提供對I / O通道的訪問。一種I / O演算法是設計用於在與輔助存儲設備(如磁碟驅動器)交換數據時充分利用本地性和高效執行的設計。
『肆』 Linux 磁碟IO
磁碟結構與數據存儲方式, 數據是如何存儲的,又通過怎樣的方式被訪問?
機械硬碟主要由磁碟碟片、磁頭、主軸與傳動軸等組成;數據就存放在磁碟碟片中
現代硬碟尋道都是採用CHS( Cylinder Head Sector )的方式,硬碟讀取數據時,讀寫磁頭沿徑向移動,移到要讀取的扇區所在磁軌的上方,這段時間稱為 尋道時間(seek time) 。 因讀寫磁頭的起始位置與目標位置之間的距離不同,尋道時間也不同 。磁頭到達指定磁軌後,然後通過碟片的旋轉,使得要讀取的扇區轉到讀寫磁頭的下方,這段時間稱為 旋轉延遲時間(rotational latencytime) 。然後再讀寫數據,讀寫數據也需要時間,這段時間稱為 傳輸時間(transfer time) 。
固態硬碟主要由主控晶元、快閃記憶體顆粒與緩存組成;數據就存放在快閃記憶體晶元中
通過主控晶元進行定址, 因為是電信號方式, 沒有任何物理結構, 所以定址速度非常快且與數據存儲位置無關
如何查看系統IO狀態
查看磁碟空間
調用 open , fwrite 時到底發生了什麼?
在一個IO過程中,以下5個API/系統調用是必不可少的
Create 函數用來打開一個文件,如果該文件不存在,那麼需要在磁碟上創建該文件
Open 函數用於打開一個指定的文件。如果在 Open 函數中指定 O_CREATE 標記,那麼 Open 函數同樣可以實現 Create 函數的功能
Clos e函數用於釋放文件句柄
Write 和 Read 函數用於實現文件的讀寫過程
O_SYNC (先寫緩存, 但是需要實際落盤之後才返回, 如果接下來有讀請求, 可以從內存讀 ), write-through
O_DSYNC (D=data, 類似O_SYNC, 但是只同步數據, 不同步元數據)
O_DIRECT (直接寫盤, 不經過緩存)
O_ASYNC (非同步IO, 使用信號機制實現, 不推薦, 直接用aio_xxx)
O_NOATIME (讀取的時候不更新文件 atime(access time))
sync() 全局緩存寫回磁碟
fsync() 特定fd的sync()
fdatasync() 只刷數據, 不同步元數據
mount noatime(全局不記錄atime), re方式(只讀), sync(同步方式)
一個IO的傳奇一生 這里有一篇非常好的資料,講述了整個IO過程;
下面簡單記錄下自己的理解的一次常見的Linux IO過程, 想了解更詳細及相關源碼,非常推薦閱讀上面的原文
Linux IO體系結構
[站外圖片上傳中...(image-38a7b-1644137945193)]
Superblock 超級描述了整個文件系統的信息。為了保證可靠性,可以在每個塊組中對superblock進行備份。為了避免superblock冗餘過多,可以採用稀疏存儲的方式,即在若干個塊組中對superblock進行保存,而不需要在所有的塊組中都進行備份
GDT 組描述符表 組描述符表對整個組內的數據布局進行了描述。例如,數據塊點陣圖的起始地址是多少?inode點陣圖的起始地址是多少?inode表的起始地址是多少?塊組中還有多少空閑塊資源等。組描述符表在superblock的後面
數據塊點陣圖 數據塊點陣圖描述了塊組內數據塊的使用情況。如果該數據塊已經被某個文件使用,那麼點陣圖中的對應位會被置1,否則該位為0
Inode點陣圖 Inode點陣圖描述了塊組內inode資源使用情況。如果一個inode資源已經使用,那麼對應位會被置1
Inode表 (即inode資源)和數據塊。這兩塊占據了塊組內的絕大部分空間,特別是數據塊資源
一個文件是由inode進行描述的。一個文件佔用的數據塊block是通過inode管理起來的 。在inode結構中保存了直接塊指針、一級間接塊指針、二級間接塊指針和三級間接塊指針。對於一個小文件,直接可以採用直接塊指針實現對文件塊的訪問;對於一個大文件,需要採用間接塊指針實現對文件塊的訪問
最簡單的調度器。它本質上就是一個鏈表實現的 fifo 隊列,並對請求進行簡單的 合並 處理。
調度器本身並沒有提供任何可以配置的參數
讀寫請求被分成了兩個隊列, 一個用訪問地址作為索引,一個用進入時間作為索引,並且採用兩種方式將這些request管理起來;
在請求處理的過程中,deadline演算法會優先處理那些訪問地址臨近的請求,這樣可以最大程度的減少磁碟抖動的可能性。
只有在有些request即將被餓死的時候,或者沒有辦法進行磁碟順序化操作的時候,deadline才會放棄地址優先策略,轉而處理那些即將被餓死的request
deadline演算法可調整參數
read_expire : 讀請求的超時時間設置(ms)。當一個讀請求入隊deadline的時候,其過期時間將被設置為當前時間+read_expire,並放倒fifo_list中進行排序
write_expire :寫請求的超時時間設置(ms)
fifo_batch :在順序(sort_list)請求進行處理的時候,deadline將以batch為單位進行處理。每一個batch處理的請求個數為這個參數所限制的個數。在一個batch處理的過程中,不會產生是否超時的檢查,也就不會產生額外的磁碟尋道時間。這個參數可以用來平衡順序處理和飢餓時間的矛盾,當飢餓時間需要盡可能的符合預期的時候,我們可以調小這個值,以便盡可能多的檢查是否有飢餓產生並及時處理。增大這個值當然也會增大吞吐量,但是會導致處理飢餓請求的延時變長
writes_starved :這個值是在上述deadline出隊處理第一步時做檢查用的。用來判斷當讀隊列不為空時,寫隊列的飢餓程度是否足夠高,以時deadline放棄讀請求的處理而處理寫請求。當檢查存在有寫請求的時候,deadline並不會立即對寫請求進行處理,而是給相關數據結構中的starved進行累計,如果這是第一次檢查到有寫請求進行處理,那麼這個計數就為1。如果此時writes_starved值為2,則我們認為此時飢餓程度還不足夠高,所以繼續處理讀請求。只有當starved >= writes_starved的時候,deadline才回去處理寫請求。可以認為這個值是用來平衡deadline對讀寫請求處理優先順序狀態的,這個值越大,則寫請求越被滯後處理,越小,寫請求就越可以獲得趨近於讀請求的優先順序
front_merges :當一個新請求進入隊列的時候,如果其請求的扇區距離當前扇區很近,那麼它就是可以被合並處理的。而這個合並可能有兩種情況,一個是向當前位置後合並,另一種是向前合並。在某些場景下,向前合並是不必要的,那麼我們就可以通過這個參數關閉向前合並。默認deadline支持向前合並,設置為0關閉
在調度一個request時,首先需要選擇一個一個合適的cfq_group。Cfq調度器會為每個cfq_group分配一個時間片,當這個時間片耗盡之後,會選擇下一個cfq_group。每個cfq_group都會分配一個vdisktime,並且通過該值採用紅黑樹對cfq_group進行排序。在調度的過程中,每次都會選擇一個vdisktime最小的cfq_group進行處理。
一個cfq_group管理了7棵service tree,每棵service tree管理了需要調度處理的對象cfq_queue。因此,一旦cfq_group被選定之後,需要選擇一棵service tree進行處理。這7棵service tree被分成了三大類,分別為RT、BE和IDLE。這三大類service tree的調度是按照優先順序展開的
通過優先順序可以很容易的選定一類Service tree。當一類service tree被選定之後,採用service time的方式選定一個合適的cfq_queue。每個Service tree是一棵紅黑樹,這些紅黑樹是按照service time進行檢索的,每個cfq_queue都會維護自己的service time。分析到這里,我們知道,cfq演算法通過每個cfq_group的vdisktime值來選定一個cfq_group進行服務,在處理cfq_group的過程通過優先順序選擇一個最需要服務的service tree。通過該Service tree得到最需要服務的cfq_queue。該過程在 cfq_select_queue 函數中實現
一個cfq_queue被選定之後,後面的過程和deadline演算法有點類似。在選擇request的時候需要考慮每個request的延遲等待時間,選擇那種等待時間最長的request進行處理。但是,考慮到磁碟抖動的問題,cfq在處理的時候也會進行順序批量處理,即將那些在磁碟上連續的request批量處理掉
cfq調度演算法的參數
back_seek_max :磁頭可以向後定址的最大范圍,默認值為16M
back_seek_penalty :向後定址的懲罰系數。這個值是跟向前定址進行比較的
fifo_expire_async :設置非同步請求的超時時間。同步請求和非同步請求是區分不同隊列處理的,cfq在調度的時候一般情況都會優先處理同步請求,之後再處理非同步請求,除非非同步請求符合上述合並處理的條件限制范圍內。當本進程的隊列被調度時,cfq會優先檢查是否有非同步請求超時,就是超過fifo_expire_async參數的限制。如果有,則優先發送一個超時的請求,其餘請求仍然按照優先順序以及扇區編號大小來處理
fifo_expire_sync :這個參數跟上面的類似,區別是用來設置同步請求的超時時間
slice_idle :參數設置了一個等待時間。這讓cfq在切換cfq_queue或service tree的時候等待一段時間,目的是提高機械硬碟的吞吐量。一般情況下,來自同一個cfq_queue或者service tree的IO請求的定址局部性更好,所以這樣可以減少磁碟的定址次數。這個值在機械硬碟上默認為非零。當然在固態硬碟或者硬RAID設備上設置這個值為非零會降低存儲的效率,因為固態硬碟沒有磁頭定址這個概念,所以在這樣的設備上應該設置為0,關閉此功能
group_idle :這個參數也跟上一個參數類似,區別是當cfq要切換cfq_group的時候會等待一段時間。在cgroup的場景下,如果我們沿用slice_idle的方式,那麼空轉等待可能會在cgroup組內每個進程的cfq_queue切換時發生。這樣會如果這個進程一直有請求要處理的話,那麼直到這個cgroup的配額被耗盡,同組中的其它進程也可能無法被調度到。這樣會導致同組中的其它進程餓死而產生IO性能瓶頸。在這種情況下,我們可以將slice_idle = 0而group_idle = 8。這樣空轉等待就是以cgroup為單位進行的,而不是以cfq_queue的進程為單位進行,以防止上述問題產生
low_latency :這個是用來開啟或關閉cfq的低延時(low latency)模式的開關。當這個開關打開時,cfq將會根據target_latency的參數設置來對每一個進程的分片時間(slice time)進行重新計算。這將有利於對吞吐量的公平(默認是對時間片分配的公平)。關閉這個參數(設置為0)將忽略target_latency的值。這將使系統中的進程完全按照時間片方式進行IO資源分配。這個開關默認是打開的
target_latency :當low_latency的值為開啟狀態時,cfq將根據這個值重新計算每個進程分配的IO時間片長度
quantum :這個參數用來設置每次從cfq_queue中處理多少個IO請求。在一個隊列處理事件周期中,超過這個數字的IO請求將不會被處理。這個參數只對同步的請求有效
slice_sync :當一個cfq_queue隊列被調度處理時,它可以被分配的處理總時間是通過這個值來作為一個計算參數指定的。公式為: time_slice = slice_sync + (slice_sync/5 * (4 - prio)) 這個參數對同步請求有效
slice_async :這個值跟上一個類似,區別是對非同步請求有效
slice_async_rq :這個參數用來限制在一個slice的時間范圍內,一個隊列最多可以處理的非同步請求個數。請求被處理的最大個數還跟相關進程被設置的io優先順序有關
通常在Linux上使用的IO介面是同步方式的,進程調用 write / read 之後會阻塞陷入到內核態,直到本次IO過程完成之後,才能繼續執行,下面介紹的非同步IO則沒有這種限制,但是當前Linux非同步IO尚未成熟
目前Linux aio還處於較不成熟的階段,只能在 O_DIRECT 方式下才能使用(glibc_aio),也就是無法使用默認的Page Cache機制
正常情況下,使用aio族介面的簡要方式如下:
io_uring 是 2019 年 5 月發布的 Linux 5.1 加入的一個重大特性 —— Linux 下的全新的非同步 I/O 支持,希望能徹底解決長期以來 Linux AIO 的各種不足
io_uring 實現非同步 I/O 的方式其實是一個生產者-消費者模型:
邏輯卷管理
RAID0
RAID1
RAID5(糾錯)
條帶化
Linux系統性能調整:IO過程
Linux的IO調度
一個IO的傳奇一生
理解inode
Linux 文件系統是怎麼工作的?
Linux中Buffer cache性能問題一探究竟
Asynchronous I/O and event notification on linux
AIO 的新歸宿:io_uring
Linux 文件 I/O 進化史(四):io_uring —— 全新的非同步 I/O