文章目錄
- 一、先看現象
- 二、用戶緩沖區的引入
- 三、用戶緩沖區的刷新策略
- 四、為什么要有用戶緩沖區
- 五、現象解釋
- 六、結語
一、先看現象
#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); // 返回值是寫入成功的快數write(1, str, strlen(str)); // 返回值是寫入成功的字節數// fork();return 0;
}
結構分析:帶 fork 的輸出重定向最終把有一些內容向 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); // 返回值是寫入成功的快數close(1);// write(1, str, strlen(str)); // 返回值是寫入成功的字節數// fork();return 0;
}
結果分析:代碼中只使用了庫函數向顯示器中進行寫入,并且在字符串的結尾沒有加 \n
,在最后面將標準輸出對應的文件描述符進行了關閉,最終顯示器上什么也沒有。上一段代碼在字符串的結尾加上了 \n
最終字符串被成功的打印到了屏幕上。
int main()
{const char* str = "Hello write";write(1, str, strlen(str)); // 返回值是寫入成功的字節數close(1);return 0;
}
結果分析:字符串的結尾依然不加 \n
,但是這一次采用系統調用接口,最后仍然將標準輸出對應的文件描述符進行關閉,這一次字符串被成功的打印了出來。
二、用戶緩沖區的引入
write
為什么能將不帶 \n
的字符串寫入到顯示器文件中。首先我們需要明確一點進程打開的每一個文件都有一個屬于自己的操作系統級別的文件緩沖區,該緩沖區的存在,可以減少對外設的讀寫操作以提高計算機的效率。舉個栗子,在一個進程中向磁盤里的同一個文件進多次行寫入,文件緩沖區的存在,可以將每次寫入的內容先存儲在文件緩沖區中,最后在程序退出或者調用 close
的時候,一次性將文件緩沖區中的所有內容刷新到磁盤。如果沒有該文件緩沖區,那在進程里對文件進行 n 次寫操做,就要對應 n 次向磁盤的寫操作,CPU 和外設之間是存在非常大的速度差的,這樣效率會非常低。
write
作為系統調用接口,它就是直接向文件緩沖區中寫入,最后在調用 close
接口或者程序退出的時候,會將文件緩沖區的內容刷新到對應的外設中。
printf
、fprintf
、fwrite
底層一定是封裝了 write
系統調用接口,那為什么使用 write
系統調用接口就可以將字符串寫入到顯示器,使用 C 庫函數沒能把字符串寫入到顯示器文件?原因在進度條的那篇文章中講過,我們使用的這些 C 庫函數,是把字符串寫入到了緩沖區中,這個緩沖區和上面的文件緩沖區有所不同,這里說的緩沖區是 C 語言給我們提供的語言層面的緩沖區,也叫做用戶級緩沖區,\n
具有刷新用戶級緩沖區的作用,因此不加 \n
并且在程序結束前將顯示器對應的文件描述符進行了關閉,最終就導致字符串在用戶級緩沖區中,沒有被刷新到文件緩沖區,所以屏幕上就什么也沒有。這里我們可以肯定,在這些 C 庫函數中,并不是立即調用 write
接口,而是在遇到 \n
后才去調用 write
接口將用戶緩沖區的內容刷新到文件緩沖區中。
總結:使用 C 系統調用接口向文件中寫入,寫入的內容先被存儲在用戶緩沖區中,在合適的時候(遇到 \n
)才會進行刷新,這里刷新的本質是調用 write
將數據從用戶緩沖區寫入內核。
之前說的 exit
會刷新緩沖區,其實就是刷新用戶緩沖區,因為 exit
作為 C 庫函數,可以看見用戶緩沖區,而 _exit
作為系統調用接口,無法看到語言層面的用戶緩沖區,因此也就無法刷新用戶緩沖區。
三、用戶緩沖區的刷新策略
-
無緩沖:直接刷新,數據不在用戶緩沖區中停留。
-
行緩沖:不刷新,直到碰到
\n
。 -
全緩沖:緩沖區滿了才刷新。
所謂刷新就是調用 write
接口將數據寫入操作系統中的文件緩沖區。顯示器文件對應采用的就是行緩沖,向磁盤文件中寫入采用的是全緩沖。進程在退出的時候也會刷新用戶緩沖區,還可以調用 fflush
進行刷新。
四、為什么要有用戶緩沖區
-
解決效率問題,緩沖區就像菜鳥驛站,不需要我們自己坐火車坐飛機去送東西,而是直接交給菜鳥驛站,然后就可以干自己的事情了,菜鳥驛站可以選擇攢上一大批快遞然后統一寄送出去。用戶緩沖區的存在本質上提高了 C 語言的效率,也就是提高了用戶的效率,因為 C 語言是程序員在使用,在使用 C 庫函數進行文件寫入時,大部分情況只需要把數據交給緩沖區,然后就可以快速的返回,不需要每一次都親力親為的去和操作系統打交道。
-
配合格式化,有些和文件寫入相關的 C 庫函數是格式化輸出函數,在我們看來,它可以寫入整形、符點型,但是最終都是以字符串的形式進行寫入。格式化就是將類型全都轉化成字符串,先寫入到用戶緩沖區,用戶緩沖區中存的一定都是字符串。
用戶緩沖區,有進也有出,將數據寫入到用戶緩沖區中就就叫做進,將用戶緩沖區中的數據刷新到內核中的文件緩沖區中,被刷新的數據就可以從用戶緩沖區中刪掉,這就叫做出。用戶緩沖就像就像水流一樣源源不斷,流的概念就是因此而來。
小Tips:FILE
里面就有對應打開文件的緩沖區字段和維護信息。每個被進程打開文件都有自己對應的文件緩沖區。FILE
對象屬于用戶,用戶緩沖區可以看作是在堆上申請的一塊空間。
五、現象解釋
這下再來解釋上面代碼中有 fork 然后重定向,寫入了多次的原因。首先重定向后,將本來向顯示器文件寫入的內容,寫到了磁盤文件,顯示器文件的緩沖區采用行緩沖,即遇到 \n
就會刷新,而磁盤文件采用的是全緩沖,當緩沖區滿了才刷新。因此在重定向后,會把三條 C 庫函數寫入的內容全部保存到緩沖區中,然后調用 fork
創建子進程,此時父子進程代碼共享,數據寫時拷貝,在程序退出的時候回去刷新用戶緩沖區,上面說過,刷新就是將用戶緩沖區中的數據寫入到內核,然后將用戶緩沖區中的內容清空,上面還說過,緩沖區就是在堆上申請的一段空間,可以看作數據部分,因為要刪除數據,所以就會進行寫時拷貝,此時之前父進程用戶緩沖區中的內容就會給子進程拷貝一份,然后父子進程都執行刷新動作,各自刷新自己的緩沖區數據,這就是為什么最終出現多份的原因。沒有重定向,只向顯示器打印四條消息,是因為顯示器采用的是行刷新策略,在調用 fork
前,對應的字符串就已經被刷新出去了。在 fork
的時候,父進程的用戶緩沖區中是空的,什么也沒有。
磁盤文件全緩沖驗證:
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); // 返回值是寫入成功的快數sleep(2);write(1, str, strlen(str)); // 返回值是寫入成功的字節數sleep(5);fork();return 0;
}
分析:最先將 write
內容寫入到文件中,因為它是直接寫入到文件緩沖區,而剩下的 C 庫函數對應的內容是統一一次全部刷新到內核,即使每個字符串后面都有 \n
,但最后還是統一全部刷新,這就證明了磁盤文件采用的是全刷新策略。
六、結語
今天的分享到這里就結束啦!如果覺得文章還不錯的話,可以三連支持一下,春人的主頁還有很多有趣的文章,歡迎小伙伴們前去點評,您的支持就是春人前進的動力!