🌟?各位看官好,我是egoist2023!
🌍?Linux == Linux is not Unix !
🚀?今天來學習C語言緩沖區和內核緩存區的區別以及緩存類型。
👍?如果覺得這篇文章有幫助,歡迎您一鍵三連,分享給更多人哦!
目錄
書接上文
引入
什么是緩沖區
為什么要引入緩沖區
緩沖類型
FILE結構體
擴展
cout和cerr分開
將文件內核緩沖區刷新到磁盤
語言級緩沖區的意義
總結
書接上文
上文我們深入拆解了 Linux “一切皆文件” 設計哲學的核心支撐:從進程task_struct
中的files_struct
結構出發,明確了文件描述符(FD)作為 “數組下標” 的本質與分配規則;更通過struct file
與file_operations
結構體的分析,揭示了 Linux 如何以 “上層統一接口(如read
/write
)、下層設備差異化實現” 的 “類 C++ 多態” 思路,讓進程能以一致視角訪問異構資源,極大簡化了開發復雜度。
然而,當我們聚焦于這些統一接口的實際 I/O 執行細節時會發現:用戶空間與內核空間的頻繁切換、高速 CPU 與低速外設(磁盤、網絡卡等)的速度鴻溝、以及 “單次小數據 I/O” 帶來的硬件調度低效,都會成為制約資源訪問性能的關鍵瓶頸。為了解決這些矛盾、在 “統一接口” 的基礎上進一步優化 I/O 效率,Linux 引入了緩沖區(Buffer/Cache)?機制 —— 它既是銜接 “用戶進程” 與 “底層文件 / 設備” 的核心中間層,也是 “一切皆文件” 哲學在性能層面的重要延伸。接下來,我們將深入解析緩沖區的設計邏輯、實現機制及其在 I/O 流程中的關鍵作用。
引入
根據前面所學,一個進程要打開文件必須要通過OS進行打開,操作系統為了管理所打開的?件,都會為這個?件創建?個file結構體,內部就要加載一個文件的屬性和內容,屬性加載到文件屬性?,內容加載到文件緩沖區中。這里的緩沖區指的是文件內核緩沖區。
在進程·柒章節,我們講了三種退出場景,對exit和_exit做了對比,exit屬于庫函數,終止進程時會主動刷新緩沖區。這里的緩沖區指的是C語言庫提供的緩沖區。
什么是緩沖區
緩沖區是內存空間的?部分。也就是說,在內存空間中預留了?定的存儲空間,這些存儲空間?來緩沖輸?或輸出的數據,這部分預留的空間就叫做緩沖區。緩沖區根據其對應的是輸?設備還是輸出設備,分為輸?緩沖區和輸出緩沖區。
簡單來說,緩沖區的本質就是一段內存空間
為什么要引入緩沖區
這里提供一段小故事供大家進行理解:
事件一:假期之余,張三買了一臺大疆action5pro的運動相機記錄生活。李四恰巧這幾天要出外旅游,打算租臺相機拍拍vlog,聽聞朋友張三買了臺action5pro。李四給張三呼電話:老朋友啊!聽聞你這幾天買了臺相機,這幾天打算出外玩玩,可否借你相機一用。熱情的張三對朋友也是極為真誠,直接答應了,說道:李四啊!在家里等我一下,我現在開車就把相機送你那。
張三到李四的家費了半天的時間。
事件二:待張三回來后,發現街拍套裝忘給李四送過去了,但此時他自己也有事情要做。張三想了一想,要不把自己的東西放菜鳥驛站上寄送,由快遞員代替他講街拍套裝送到李四那的菜鳥驛站。
快遞員從張三的菜鳥驛站到李四的菜鳥驛站也是需要花費半天的時間。只不過這個動作主體由張三替換成了快遞員。這意味著什么呢?張三可以在這段本是要他送貨的時間轉移到了快遞員,允許自己做更多的工作了。
事件三:我們清楚菜鳥驛站的貨并不是一收到貨就馬上派送的,可以等貨多了再進行派送。
相機和街拍套裝就是數據,是緩存的角色;
菜鳥驛站就是所謂的緩沖區;
將數據給菜鳥本質就是拷貝;
菜鳥驛站允許等貨多了再進行派送就是允許數據在緩沖區中積壓。這樣做的目的一次就可以刷新多次數據,變相減少IO次數。
讀寫?件時,如果不會開辟對?件操作的緩沖區,直接通過系統調?對磁盤進?操作(讀、寫等),那么每次對?件進??次讀寫操作時,都需要使?讀寫系統調?來處理此操作,即需要執??次系統調?,執??次系統調?將涉及到CPU狀態的切換,即從??空間切換到內核空間,實現進程上下?的切換,這將損耗?定的CPU時間,頻繁的磁盤訪問對程序的執?效率造成很?的影響。
為了減少使?系統調?的次數,提?效率,我們就可以采?緩沖機制。?如我們從磁盤?取信息,可以在磁盤?件進?操作時,可以?次從?件中讀出?量的數據到緩沖區中,以后對這部分的訪問就不需要再使?系統調?了,等緩沖區的數據取完后再去磁盤中讀取,這樣就可以減少磁盤的讀寫次數,再加上計算機對緩沖區的操作? 快于對磁盤的操作,故應?緩沖區可? 提?計算機的運?速度。??如,我們使?打印機打印?檔,由于打印機的打印速度相對較慢,我們先把?檔輸出到打印機相應的緩沖區,打印機再??逐步打印,這時我們的CPU可以處理別的事情。可以看出,緩沖區就是?塊內存區,它?在輸?輸出設備和CPU之間,?來緩存數據。它使得低速的輸?輸出設備和?速的CPU能夠協調?作,避免低速的輸?輸出設備占?CPU,解放出CPU,使其能夠?效率?作。
緩沖類型
由于我們的緩沖區是允許數據進行積壓的,那么肯定有自身的一套積壓規則存在:
標準I/O提供了3種類型的緩沖區。
- 全緩沖區:這種緩沖?式要求填滿整個緩沖區后才進?I/O系統調?操作。對于磁盤?件的操作通常使?全緩沖的?式訪問。
- ?緩沖區:在?緩沖情況下,當在輸?和輸出中遇到換?符時,標準I/O庫函數將會執?系統調?操作。當所操作的流涉及?個終端時(例如標準輸?和標準輸出),使??緩沖?式。因為標準I/O庫每?的緩沖區?度是固定的,所以只要填滿了緩沖區,即使還沒有遇到換?符,也會執?I/O系統調?操作,默認?緩沖區的??為1024。
- ?緩沖區:?緩沖區是指標準I/O庫不對字符進?緩存,直接調?系統調?。標準出錯流stderr通常是不帶緩沖區的,這使得出錯信息能夠盡快地顯示出來。
除了上述列舉的默認刷新?式,下列特殊情況也會引發緩沖區的刷新:
緩沖區滿時;
執行fflush語句;
驗證stderr是否帶緩沖區:
int main()
{close(2);int fd = open("log.txt", O_WRONLY | O_CREAT | O_TRUNC, 0666);if (fd < 0) {perror("open");return 0;}perror("hello world");close(fd);return 0;
}
將2號?件描述符重定向??件,由于stderr沒有緩沖區,“hello world”不?fflush就可以寫??件。
cat log.txt
hello world : Success
FILE結構體
printf("hello linux,hello everyone!");
sleep(3);
return 0;
在上面這段代碼中,printf執行完畢后,并未打印到顯示器上,而是3s后顯示。那么在sleep期間,我們的數據在哪呢?毫無疑問,在緩存區中。
是什么緩沖區呢?C語言庫提供的緩沖區。那這個緩沖區又在哪呢?FILE結構體中。
- struct FILE 本質是一個結構體
- C語言上,輸入輸出格式化
- C訪問文件,都是通過FILE訪問的,包括stdin,stdout, stderr!
FILE結構體內部為我們維護:語言級別的緩沖區空間!
struct FILE
{int fd;char *inbuffer;char *outbuffer;
}
如何理解printf,scanf 的格式化過程?
int a = 123456;
printf("%d", a);
- 格式化
- 格式化結果刷新到FILE緩沖區中
- 檢測是否需要刷新
- 滿足條件時調用write系統調用刷新到文件內核緩沖區中
文件內核緩沖區的刷新方式比較特殊,是單獨的執行流,這里不做考慮。
由此我們也可以回答上一章節留下的疑惑,為什么close該文件后,并沒有顯示打印的內容。
int main()
{close(1);int fd = open("log.txt",O_CREAT | O_WRONLY | O_TRUNC,0666);if(fd<0){perror("fd");return 1;}printf("hello file,fd:%d\n",fd); // stdout -> 1close(fd);return 0;
}
一般向普通文件寫入是全緩沖。此時程序還沒結束,文件被關閉了,而我們的內容還在FILE內緩沖區中,沒有刷新到文件內核緩沖區中,所以沒有顯示打印內容。
那為什么fflush刷新呢?想必底層是調用write系統調用,實際上就是如此。
fflush底層調用write系統調用,將FILE內緩沖區強制刷新到文件內核緩沖區中,文件內核緩沖區在刷新出來給我們看到。(由語言到內核的過程)
擴展
int main()
{//C提供const char *s1 = "hello printf\n";printf(s1);const char *s2 = "hello fprintf\n";fprintf(stdout,s2);const char *s3 = "hello fwrite\n";fwrite(s3,strlen(s3),1,stdout);//系統const char *s4 = "hello write[syscall]\n";write(1,s4,strlen(s4));//創建子進程fork();return 0;
}
在上面這段程序當中, 執行過程是:調用printf、fprintf、fwrite三個C庫函數和系統調用write,再創建一個子進程,我們來觀察兩種情況:
情況1:向顯示器進行寫入 --> 刷新策略:行刷新
由于是行刷新策略,當遇到換?符時語言級緩沖區會自動刷新到文件內核緩沖區中,因此是有規律地刷新到文件內核緩沖區中,此時創建子進程,之后程序結束需要情況語言級緩沖區,但緩沖區上并沒有數據。最終文件內核緩沖區再刷新出去,向顯示器寫入數據,給我們看到符合我們需求的結果。
情況2:向文件進行寫入 --> 刷新策略:全緩沖
但當向文件進行寫入時,打印結果并不是符合我們所想的,為什么同樣的代碼會有兩種不同的結果呢?
首先,向文件進行寫入一般的刷新策略是全緩沖,前三個都是C語言提供的庫函數,因此都是向FILE內緩沖區進行寫入,而write是系統調用,寫到文件內核緩沖區中,因此先看到write[syscall]這個結果并不例外。
但是為什么有兩段同樣的打印結果呢?
write結束后,此時會調用fork創建子進程,之后程序就結束了。由于程序結束了,就需要清空緩沖區,而此時我們是父子進程啊,而子進程清空緩沖區實際上就是修改數據啊,修改就要發生寫時拷貝,并不會影響父進程的緩沖區。父進程再進行清空緩沖區,呈現出刷新兩次的結果。(即父子進程各自執行退出邏輯,刷新自己的緩沖區)
cout和cerr分開
將文件內核緩沖區刷新到磁盤
int fsync(int fd);
強制將指定文件描述符(
fd
)對應的文件的所有已修改數據和元數據從操作系統緩存(如頁緩存)刷新到磁盤。
語言級緩沖區的意義
- IO相關函數與系統調?接?對應,并且庫函數封裝系統調?,所以本質上,訪問?件都是通過fd訪問的,而調用系統調用是有成本的(浪費時間)。
- 就拿C語言的malloc來說,我們在造vector輪子時,說過擴容盡量往1.5倍和2倍去靠,是為了減少擴容次數,頻繁擴容只是原因之一,并不是重點。malloc底層調用了系統調用,這意味著會頻繁調用系統調用,要花費操作系統的時間。
- C語言為什么要提供緩沖區呢?FILE結構體內的緩沖區允許積壓,加速IO函數的調用頻率。而使用C語言IO接口,提高了效率,進而使單位時間內,執行C代碼行數就變多了,從而也反向提高了IO接口的效率。
總結
本文深入探討了Linux系統中緩沖區的設計原理與實現機制。文章從Linux"一切皆文件"的哲學出發,分析了緩沖區作為銜接用戶進程與底層設備的關鍵中間層,如何通過減少系統調用次數、平衡CPU與低速I/O設備的速度差異來提升系統性能。內容涵蓋緩沖區的三種類型(全緩沖、行緩沖、無緩沖)、FILE結構體實現原理,并通過具體代碼示例演示了不同緩沖策略下的I/O行為差異。文章特別解析了C語言緩沖區與內核緩沖區的交互機制,包括緩沖區刷新時機、父子進程間的緩沖區復制問題等,最終闡明了緩沖區設計在平衡統一接口與高效I/O訪問之間的重要意義。
?