天天看點

标準I/O庫的緩沖模式

問題描述

有時候,代碼中明明執行了printf語句列印到終端,卻沒有看到輸出的内容。

寫檔案的時候,明明成功執行了fwrite, fprintf語句,檔案卻沒有寫入相應的内容。

想搞清楚這些問題産生的原因,需要了解标準I/O庫的緩沖模式。

标準I/O與unbuffered I/O

linux對I/O檔案操作分為不帶緩存I/O(unbuffered I/O)和帶緩存I/O(即标準I/O)

《APUE》中對術語unbuffered的定義: “The term unbuffered means that each read or write invokes a system call in the kernel”

這篇文章講了unbuffered I/O和标準I/O的差別https://blog.csdn.net/qq_33366098/article/details/77923722,以下引用其中的描述:

不帶緩存I/O,是指每次read, write都會進入核心,執行一次系統調用,不帶緩存不是指直接對磁盤進行讀寫。比如read,write函數,它們屬于系統調用,在使用者态沒有緩存,但是在核心是有緩存器的。如核心緩存未滿,寫入的資料還是在核心緩存,并沒有真正寫入硬碟。需要等待緩存寫滿或者核心需要重用該緩存以存放其他磁盤塊資料時,才進行實際硬碟讀寫,這種方式被稱為延遲寫(delayed write)

帶緩存I/O也叫标準I/O。标準I/O會在使用者态建立一個緩存區,以盡可能減少read和write調用的次數,提高效率。

unbuffered I/O操作資料流向:資料->核心緩存區->磁盤

标準I/O操作資料流向:資料->流緩存區->核心緩存區->磁盤

緩沖

标準I/O庫提供三種模式的緩沖: 全緩沖、行緩沖、不帶緩沖

全緩沖(fully buffered)

這種情況下,在填滿标準I/O緩沖區後才進行實際I/O操作。對于駐留在磁盤上的檔案通常用全緩沖。

行緩沖(line buffered)

這種情況下,當輸入或輸出遇到換行符,或者緩沖區已滿時進行實際I/O操作。當流涉及一個終端時,通常使用行緩沖。

不帶緩沖(unbuffered)

這種情況下,标準I/O庫不對字元進行緩沖存儲。例如标準出錯流stderr通常是不帶緩沖的,這使得出錯資訊可以盡快顯示。

注:

1.這裡的實際I/O操作不是指讀寫硬碟操作,而是指執行read, write系統調用。

2.緩沖類型與具體的标準I/O函數無關,與讀寫的檔案類型有關。

舉例說明

例1 全緩沖

#include<stdio.h>
#include<unistd.h>
int main() {
	char str[] = "hello world";
	FILE *fp = fopen("./text", "w+");	// 省略了判空操作^_^
	fprintf(fp, "%s\n", str);
    // fflush(fp); 
	for( ; ; ) {						
		sleep(10);
	}
	return 0;
}
           

編譯并運作程式,進入死循環後,觀察text檔案發現内容為空,hello world字元串并沒有寫入。

這是因為對于磁盤上的檔案預設是全緩沖的。因為寫入的字元串長度小于緩沖區大小(我的ubuntu機器上,為4096位元組),是以不會直接寫入檔案。

如需要立即輸出,可以在for循環之前調用fflush函數,将緩沖區的内容寫入磁盤。

這解釋了為什麼有時明明成功執行了fwrite,fprintf語句,檔案卻沒有寫入相應的内容。

例2 行緩沖

#include<stdio.h>
#include<unistd.h>
int main() {
	fprintf(stdout, "hello world");
	for( ; ; ) {						
		sleep(10);
	}
	return 0;
}
           

編譯并運作程式,發現終端沒有輸出,即使fprintf已經執行。

這是因為涉及終端的流預設是行緩沖的,當輸入或輸出遇到換行符時才進行實際I/O操作。

如果需要執行fprintf後立即列印,隻需在"hello world"後添加換行符’\n’

這個例子解釋了為什麼有時代碼中執行了printf語句列印到終端,卻沒有看到輸出的内容。

例3 無緩沖

#include<stdio.h>
#include<unistd.h>
int main() {
	fprintf(stderr, "hello world");
	for( ; ; ) {						
		sleep(10);
	}
	return 0;
}
           

編譯并運作程式, 終端立即輸出hello world。可以看出标準錯誤是不帶緩沖的。目的是使出錯資訊可以盡快顯示。

例4 緩沖類型與讀寫的檔案類型有關

#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
#include<sys/wait.h>

int main()
{
	pid_t pid;
	printf("before fork\n");

	if((pid = fork()) < 0) {	// fork失敗直接傳回-1
		exit(-1);
	} else if(pid > 0) {		// 父程序
		wait(NULL);
	}
	printf("pid = %d, hello\n", getpid());
	return 0;
}
           

編譯并執行程式, 得到:

$ ./a.out

before fork

pid = 6033, hello

pid = 6032, hello

$ ./a.out > output.txt #将輸出重定向到output.txt檔案

$ cat output.txt

before fork

pid = 6077, hello

before fork

pid = 6076, hello

發現兩次輸出的内容不同,将輸出重定向到檔案時,會多列印一行"before fork",原因如下:

如果标準輸出連到終端裝置,預設是行緩沖的。"before fork"隻輸出一次,原因是調用第一個printf後,标準輸出緩沖區由換行符沖洗,“before fork”被立即列印。

如果将标準輸出重定向到檔案,預設是全緩沖的。"befork fork"會輸出兩次,原因是調用第一個printf後資料“before fork”仍舊在緩沖區,然後調用fork函數,将父程序資料空間複制到子程序,此時該緩沖區也被複制到子程序。最後當父子程序終止時,各自沖洗其緩沖區的副本。

這個例子說明,緩沖類型與讀寫的檔案類型有關,與具體I/O函數無關。

其他說明

以下内容直接摘抄自APUE:

ISO C要求的緩沖特征

當且僅當标準輸入和标準輸出不涉及互動裝置時,才是全緩沖的。

标準出錯不可能是全緩沖的。

但标準中沒有規定,當标準輸入和輸出涉及互動裝置時,到底是行緩沖的還是不帶緩沖的;也沒有規定标準出錯是不帶緩沖的還是行緩沖的。很多系統預設使用下列類型的緩沖:

标準出錯是不帶緩沖的。

若涉及終端裝置的其他流,則是行緩沖的,否則是全緩沖的。

标準I/O庫的不足之處

效率不高。當使用fgets, fputs函數,通常需要複制兩次資料:一次是核心和标準I/O緩沖之間,一次是标準I/O緩沖和使用者程式的行緩沖區之間。

參考資料

《UNIX環境進階程式設計第二版》

https://blog.csdn.net/qq_33366098/article/details/77923722

https://www.yanbinghu.com/2019/12/01/27836.html

繼續閱讀