在 Linux 系統中,傳統的訪問方式是通過 write() 和 read() 兩個系統調用實現的,通過 read() 函數讀取文件到到緩存區中,然後通過 write() 方法把緩存中的數據輸出到網絡端口。
read(file_fd, tmp_buf, len);
write(socket_fd, tmp_buf, len);
下圖分别對應傳統 I/O 操作的數據讀寫流程,整個過程涉及 2 次 CPU 拷貝、2 次 DMA 拷貝,總共 4 次拷貝,以及 4 次上下文切換。
當應用程序執行 read 系統調用讀取一塊數據的時候,如果這塊數據已經存在于用戶進程的頁内存中,就直接從内存中讀取數據。
如果數據不存在,則先将數據從磁盤加載數據到内核空間的讀緩存(Read Buffer)中,再從讀緩存拷貝到用戶進程的頁内存中。
read(file_fd, tmp_buf, len);
基于傳統的 I/O 讀取方式,read 系統調用會觸發 2 次上下文切換,1 次 DMA 拷貝和 1 次 CPU 拷貝。
發起數據讀取的流程如下:
相關視頻推薦
面試中正經“八股文”網絡原理tcp/udp,網絡編程epoll/reactor
6種epoll的設計,讓你吊打面試官,而且他不能還嘴
LinuxC 後台服務器開發架構師免費學習地址:C/C Linux服務器開發/後台架構師【零聲教育】-學習視頻教程-騰訊課堂
【文章福利】:小編整理了一些個人覺得比較好的學習書籍、視頻資料共享在群文件裡面,有需要的可以自行添加哦!~點擊加入(832218493需要自取)
寫操作
當應用程序準備好數據,執行 write 系統調用發送網絡數據時,先将數據從用戶空間的頁緩存拷貝到内核空間的網絡緩沖區(Socket Buffer)中,然後再将寫緩存中的數據拷貝到網卡設備完成數據發送。
write(socket_fd, tmp_buf, len);
基于傳統的 I/O 寫入方式,write() 系統調用會觸發 2 次上下文切換,1 次 CPU 拷貝和 1 次 DMA 拷貝。
用戶程序發送網絡數據的流程如下:
磁盤 I/O
高性能優化的 I/O
其中,頁緩存(PageCache)是操作系統對文件的緩存,用來減少對磁盤的 I/O 操作,以頁為單位的,内容就是磁盤上的物理塊,頁緩存能幫助程序對文件進行順序讀寫的速度幾乎接近于内存的讀寫速度,主要原因就是由于 OS 使用 PageCache 機制對讀寫訪問操作進行了性能優化。
頁緩存讀取策略:當進程發起一個讀操作 (比如,進程發起一個 read() 系統調用),它首先會檢查需要的數據是否在頁緩存中:
頁緩存寫策略:當進程發起 write 系統調用寫數據到文件中,先寫到頁緩存,然後方法返回。此時數據還沒有真正的保存到文件中去,Linux 僅僅将頁緩存中的這一頁數據标記為 “髒”,并且被加入到髒頁鍊表中。
然後,由 flusher 回寫線程周期性将髒頁鍊表中的頁寫到磁盤,讓磁盤中的數據和内存中保持一緻,最後清理“髒”标識。在以下三種情況下,髒頁會被寫回磁盤:
由圖可見,從系統調用的接口再往下,Linux 下的 IO 棧緻大緻有三個層次:
結合這個圖,想想 Linux 系統編程裡用到的 Buffered IO、mmap、Direct IO,這些機制怎麼和 Linux I/O 棧聯系起來呢?上面的圖有點複雜,我畫一幅簡圖,把這些機制所在的位置添加進去:
Linux IO系統
這下一目了然了吧?傳統的 Buffered IO 使用 read 讀取文件的過程什麼樣的?假設要去讀一個冷文件(Cache 中不存在),open 打開文件内核後建立了一系列的數據結構,接下來調用 read,到達文件系統這一層,發現 Page Cache 中不存在該位置的磁盤映射,然後創建相應的 Page Cache 并和相關的扇區關聯。然後請求繼續到達塊設備層,在 IO 隊列裡排隊,接受一系列的調度後到達設備驅動層,此時一般使用 DMA 方式讀取相應的磁盤扇區到 Cache 中,然後 read 拷貝數據到用戶提供的用戶态 buffer 中去(read 的參數指出的)。
整個過程有幾次拷貝?從磁盤到 Page Cache 算第一次的話,從 Page Cache 到用戶态 buffer 就是第二次了。而 mmap 做了什麼?mmap 直接把 Page Cache 映射到了用戶态的地址空間裡了,所以 mmap 的方式讀文件是沒有第二次拷貝過程的。
那 Direct IO 做了什麼?這個機制更狠,直接讓用戶态和塊 IO 層對接,直接放棄 Page Cache,從磁盤直接和用戶态拷貝數據。好處是什麼?寫操作直接映射進程的buffer到磁盤扇區,以 DMA 的方式傳輸數據,減少了原本需要到 Page Cache 層的一次拷貝,提升了寫的效率。對于讀而言,第一次肯定也是快于傳統的方式的,但是之後的讀就不如傳統方式了(當然也可以在用戶态自己做 Cache,有些商用數據庫就是這麼做的)。
除了傳統的 Buffered IO 可以比較自由的用偏移 長度的方式讀寫文件之外,mmap 和 Direct IO 均有數據按頁對齊的要求,Direct IO 還限制讀寫必須是底層存儲設備塊大小的整數倍(甚至 Linux 2.4 還要求是文件系統邏輯塊的整數倍)。所以接口越來越底層,換來表面上的效率提升的背後,需要在應用程序這一層做更多的事情。所以想用好這些高級特性,除了深刻理解其背後的機制之外,也要在系統設計上下一番功夫。
I/O Buffering
如圖,當程序調用各類文件操作函數後,用戶數據(User Data)到達磁盤(Disk)的流程如圖所示。
圖中描述了 Linux 下文件操作函數的層級關系和内存緩存層的存在位置。中間的黑色實線是用戶态和内核态的分界線。
從上往下分析這張圖:
1. 首先是 C 語言 stdio 庫定義的相關文件操作函數,這些都是用戶态實現的跨平台封裝函數。stdio 中實現的文件操作函數有自己的 stdio buffer,這是在用戶态實現的緩存。此處使用緩存的原因很簡單 — 系統調用總是昂貴的。如果用戶代碼以較小的 size 不斷的讀或寫文件的話,stdio 庫将多次的讀或者寫操作通過 buffer 進行聚合是可以提高程序運行效率的。stdio 庫同時也支持 fflush 函數來主動的刷新 buffer,主動的調用底層的系統調用立即更新 buffer 裡的數據。特别地,setbuf 函數可以對 stdio 庫的用戶态 buffer 進行設置,甚至取消 buffer 的使用。
2. 系統調用的 read/write 和真實的磁盤讀寫之間也存在一層 buffer,這裡用術語 Kernel Buffer Cache 來指代這一層緩存。在 Linux 下,文件的緩存習慣性的稱之為 Page Cache,而更低一級的設備的緩存稱之為 Buffer Cache。這兩個概念很容易混淆,這裡簡單的介紹下概念上的區别:Page Cache 用于緩存文件的内容,和文件系統比較相關。文件的内容需要映射到實際的物理磁盤,這種映射關系由文件系統來完成;Buffer Cache 用于緩存存儲設備塊(比如磁盤扇區)的數據,而不關心是否有文件系統的存在(文件系統的元數據緩存在 Buffer Cache 中)。
,更多精彩资讯请关注tft每日頭條,我们将持续为您更新最新资讯!