Linux文件系統(tǒng)之緩沖區(qū)詳解
一、先看現(xiàn)象
#include <stdio.h>
#include <string.h>
#include <unistd.h>
int main()
{
const char* fstr = "Hello fwrite\n";
const char* str = "Hello write\n";
printf("Hello printf\n");
fprintf(stdout, "Hello fprintf\n");
fwrite(fstr, strlen(fstr), 1, stdout); // 返回值是寫入成功的快數(shù)
write(1, str, strlen(str)); // 返回值是寫入成功的字節(jié)數(shù)
// fork();
return 0;
}

結(jié)構(gòu)分析:帶 fork 的輸出重定向最終把有一些內(nèi)容向 log.txt 文件中寫入了多次,并且打印順序也有所不同。
int main()
{
const char* fstr = "Hello fwrite";
const char* str = "Hello write";
printf("Hello printf");
fprintf(stdout, "Hello fprintf");
fwrite(fstr, strlen(fstr), 1, stdout); // 返回值是寫入成功的快數(shù)
close(1);
// write(1, str, strlen(str)); // 返回值是寫入成功的字節(jié)數(shù)
// fork();
return 0;
}

結(jié)果分析:代碼中只使用了庫函數(shù)向顯示器中進行寫入,并且在字符串的結(jié)尾沒有加 \n,在最后面將標(biāo)準(zhǔn)輸出對應(yīng)的文件描述符進行了關(guān)閉,最終顯示器上什么也沒有。上一段代碼在字符串的結(jié)尾加上了 \n 最終字符串被成功的打印到了屏幕上。
int main()
{
const char* str = "Hello write";
write(1, str, strlen(str)); // 返回值是寫入成功的字節(jié)數(shù)
close(1);
return 0;
}

結(jié)果分析:字符串的結(jié)尾依然不加 \n,但是這一次采用系統(tǒng)調(diào)用接口,最后仍然將標(biāo)準(zhǔn)輸出對應(yīng)的文件描述符進行關(guān)閉,這一次字符串被成功的打印了出來。
二、用戶緩沖區(qū)的引入
write 為什么能將不帶 \n 的字符串寫入到顯示器文件中。首先我們需要明確一點進程打開的每一個文件都有一個屬于自己的操作系統(tǒng)級別的文件緩沖區(qū),該緩沖區(qū)的存在,可以減少對外設(shè)的讀寫操作以提高計算機的效率。舉個栗子,在一個進程中向磁盤里的同一個文件進多次行寫入,文件緩沖區(qū)的存在,可以將每次寫入的內(nèi)容先存儲在文件緩沖區(qū)中,最后在程序退出或者調(diào)用 close 的時候,一次性將文件緩沖區(qū)中的所有內(nèi)容刷新到磁盤。如果沒有該文件緩沖區(qū),那在進程里對文件進行 n 次寫操做,就要對應(yīng) n 次向磁盤的寫操作,CPU 和外設(shè)之間是存在非常大的速度差的,這樣效率會非常低。
write 作為系統(tǒng)調(diào)用接口,它就是直接向文件緩沖區(qū)中寫入,最后在調(diào)用 close 接口或者程序退出的時候,會將文件緩沖區(qū)的內(nèi)容刷新到對應(yīng)的外設(shè)中。
printf、fprintf、fwrite 底層一定是封裝了 write 系統(tǒng)調(diào)用接口,那為什么使用 write 系統(tǒng)調(diào)用接口就可以將字符串寫入到顯示器,使用 C 庫函數(shù)沒能把字符串寫入到顯示器文件?原因在進度條的那篇文章中講過,我們使用的這些 C 庫函數(shù),是把字符串寫入到了緩沖區(qū)中,這個緩沖區(qū)和上面的文件緩沖區(qū)有所不同,這里說的緩沖區(qū)是 C 語言給我們提供的語言層面的緩沖區(qū),也叫做用戶級緩沖區(qū),\n 具有刷新用戶級緩沖區(qū)的作用,因此不加 \n 并且在程序結(jié)束前將顯示器對應(yīng)的文件描述符進行了關(guān)閉,最終就導(dǎo)致字符串在用戶級緩沖區(qū)中,沒有被刷新到文件緩沖區(qū),所以屏幕上就什么也沒有。這里我們可以肯定,在這些 C 庫函數(shù)中,并不是立即調(diào)用 write 接口,而是在遇到 \n 后才去調(diào)用 write 接口將用戶緩沖區(qū)的內(nèi)容刷新到文件緩沖區(qū)中。

總結(jié):使用 C 系統(tǒng)調(diào)用接口向文件中寫入,寫入的內(nèi)容先被存儲在用戶緩沖區(qū)中,在合適的時候(遇到 \n)才會進行刷新,這里刷新的本質(zhì)是調(diào)用 write 將數(shù)據(jù)從用戶緩沖區(qū)寫入內(nèi)核。
之前說的 exit 會刷新緩沖區(qū),其實就是刷新用戶緩沖區(qū),因為 exit 作為 C 庫函數(shù),可以看見用戶緩沖區(qū),而 _exit 作為系統(tǒng)調(diào)用接口,無法看到語言層面的用戶緩沖區(qū),因此也就無法刷新用戶緩沖區(qū)。
三、用戶緩沖區(qū)的刷新策略
- 無緩沖:直接刷新,數(shù)據(jù)不在用戶緩沖區(qū)中停留。
- 行緩沖:不刷新,直到碰到
\n。 - 全緩沖:緩沖區(qū)滿了才刷新。
所謂刷新就是調(diào)用 write 接口將數(shù)據(jù)寫入操作系統(tǒng)中的文件緩沖區(qū)。顯示器文件對應(yīng)采用的就是行緩沖,向磁盤文件中寫入采用的是全緩沖。進程在退出的時候也會刷新用戶緩沖區(qū),還可以調(diào)用 fflush 進行刷新。
四、為什么要有用戶緩沖區(qū)
- 解決效率問題,緩沖區(qū)就像菜鳥驛站,不需要我們自己坐火車坐飛機去送東西,而是直接交給菜鳥驛站,然后就可以干自己的事情了,菜鳥驛站可以選擇攢上一大批快遞然后統(tǒng)一寄送出去。用戶緩沖區(qū)的存在本質(zhì)上提高了 C 語言的效率,也就是提高了用戶的效率,因為 C 語言是程序員在使用,在使用 C 庫函數(shù)進行文件寫入時,大部分情況只需要把數(shù)據(jù)交給緩沖區(qū),然后就可以快速的返回,不需要每一次都親力親為的去和操作系統(tǒng)打交道。
- 配合格式化,有些和文件寫入相關(guān)的 C 庫函數(shù)是格式化輸出函數(shù),在我們看來,它可以寫入整形、符點型,但是最終都是以字符串的形式進行寫入。格式化就是將類型全都轉(zhuǎn)化成字符串,先寫入到用戶緩沖區(qū),用戶緩沖區(qū)中存的一定都是字符串。
用戶緩沖區(qū),有進也有出,將數(shù)據(jù)寫入到用戶緩沖區(qū)中就就叫做進,將用戶緩沖區(qū)中的數(shù)據(jù)刷新到內(nèi)核中的文件緩沖區(qū)中,被刷新的數(shù)據(jù)就可以從用戶緩沖區(qū)中刪掉,這就叫做出。用戶緩沖就像就像水流一樣源源不斷,流的概念就是因此而來。
小Tips:FILE 里面就有對應(yīng)打開文件的緩沖區(qū)字段和維護信息。每個被進程打開文件都有自己對應(yīng)的文件緩沖區(qū)。FILE 對象屬于用戶,用戶緩沖區(qū)可以看作是在堆上申請的一塊空間。
五、現(xiàn)象解釋
這下再來解釋上面代碼中有 fork 然后重定向,寫入了多次的原因。首先重定向后,將本來向顯示器文件寫入的內(nèi)容,寫到了磁盤文件,顯示器文件的緩沖區(qū)采用行緩沖,即遇到 \n 就會刷新,而磁盤文件采用的是全緩沖,當(dāng)緩沖區(qū)滿了才刷新。因此在重定向后,會把三條 C 庫函數(shù)寫入的內(nèi)容全部保存到緩沖區(qū)中,然后調(diào)用 fork 創(chuàng)建子進程,此時父子進程代碼共享,數(shù)據(jù)寫時拷貝,在程序退出的時候回去刷新用戶緩沖區(qū),上面說過,刷新就是將用戶緩沖區(qū)中的數(shù)據(jù)寫入到內(nèi)核,然后將用戶緩沖區(qū)中的內(nèi)容清空,上面還說過,緩沖區(qū)就是在堆上申請的一段空間,可以看作數(shù)據(jù)部分,因為要刪除數(shù)據(jù),所以就會進行寫時拷貝,此時之前父進程用戶緩沖區(qū)中的內(nèi)容就會給子進程拷貝一份,然后父子進程都執(zhí)行刷新動作,各自刷新自己的緩沖區(qū)數(shù)據(jù),這就是為什么最終出現(xiàn)多份的原因。沒有重定向,只向顯示器打印四條消息,是因為顯示器采用的是行刷新策略,在調(diào)用 fork 前,對應(yīng)的字符串就已經(jīng)被刷新出去了。在 fork 的時候,父進程的用戶緩沖區(qū)中是空的,什么也沒有。
磁盤文件全緩沖驗證:
int main()
{
const char* fstr = "Hello fwrite\n";
const char* str = "Hello write\n";
printf("Hello printf\n");
sleep(2);
fprintf(stdout, "Hello fprintf\n");
sleep(2);
fwrite(fstr, strlen(fstr), 1, stdout); // 返回值是寫入成功的快數(shù)
sleep(2);
write(1, str, strlen(str)); // 返回值是寫入成功的字節(jié)數(shù)
sleep(5);
fork();
return 0;
}

分析:最先將 write 內(nèi)容寫入到文件中,因為它是直接寫入到文件緩沖區(qū),而剩下的 C 庫函數(shù)對應(yīng)的內(nèi)容是統(tǒng)一一次全部刷新到內(nèi)核,即使每個字符串后面都有 \n,但最后還是統(tǒng)一全部刷新,這就證明了磁盤文件采用的是全刷新策略。
六、結(jié)語
以上就是Linux文件系統(tǒng)之緩沖區(qū)詳解的詳細(xì)內(nèi)容,更多關(guān)于Linux緩沖區(qū)的資料請關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
CentOS 7.2搭建VNC遠(yuǎn)程桌面服務(wù)的方法
本篇文章主要介紹了CentOS 7.2搭建VNC遠(yuǎn)程桌面服務(wù)的方法,小編覺得挺不錯的,現(xiàn)在分享給大家,也給大家做個參考。一起跟隨小編過來看看吧2018-03-03
Git 刪除遠(yuǎn)程服務(wù)器文件同時保留本地文件實例詳解
這篇文章主要介紹了Git 刪除遠(yuǎn)程服務(wù)器文件同時保留本地文件實例詳解的相關(guān)資料,需要的朋友可以參考下2017-05-05
Linux使用watch命令監(jiān)控Docker容器狀態(tài)的操作方法
在現(xiàn)代的開發(fā)和運維環(huán)境中,容器化技術(shù)已經(jīng)成為一種重要的趨勢,而Docker作為最流行的容器化平臺之一,Linux中的watch命令就是一個非常有用的工具,它可以幫助我們定期執(zhí)行指定的命令,并全屏顯示輸出,本文給大家介紹了在Linux中使用watch命令監(jiān)控Docker容器狀態(tài)2024-10-10
Linux內(nèi)核設(shè)備驅(qū)動之Linux內(nèi)核基礎(chǔ)筆記整理
今天小編就為大家分享一篇關(guān)于Linux內(nèi)核設(shè)備驅(qū)動之Linux內(nèi)核基礎(chǔ)筆記整理,小編覺得內(nèi)容挺不錯的,現(xiàn)在分享給大家,具有很好的參考價值,需要的朋友一起跟隨小編來看看吧2018-12-12
tomcat 5.5連接池配置,如何讓工程為默認(rèn)工程
把驅(qū)動程序拷貝到Tomcat 5.5\common\lib目錄下2009-06-06

