一、問題引入
我們先來看看下面的代碼:我們使用了C語言接口和系統調用接口來進行文件操作。在代碼的最后,我們還使用fork函數創建了一個子進程。
#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
#include<sys/wait.h>
#include<sys/types.h>
#include<string.h>int main()
{fprintf(stdout,"hello fprintf\n");const char*s="hello fputs\n";fputs(s,stdout);printf("hello printf\n");const char* ss="hello write\n";write(1,ss,strlen(ss));frok();return 0;
}
代碼運行結果如下:
結果沒有什么問題啊?結果很正確。但是我們再來看看下面的操作:我們對其進行輸出重定向。然后,查看log.txt的代碼。
我們驚奇地發現,文件里面的內容和打印到顯示器的內容是不一樣的!我們再仔細觀察,發現,C語言的函數都打印了兩次,而系統調用接口只打印了一次。為什么呢??
這種現象就和fork函數以及我們下面要講的緩沖區有關了。
二、緩沖區
2.1、什么是緩沖區
緩沖區的本質就是一段內存空間。
我們知道,內存的速度比磁盤的速度快了幾個數量級。所以數據如果直接從內存寫到磁盤,那么訪問外設效率比較低,那就太消耗時間了。所以緩沖區的意義就是通過減少與外設的IO次數,來節省進程進行數據IO的時間。
所以C語言中就提供了緩沖區。而有了緩沖區的存在,可以提高整機效率,并提高用戶的響應速度。
2.2、刷新策略
~ 立即刷新。
~ 行刷新(行緩沖)。(常見的對顯示器進行數據刷新)以\n為標志
~ 滿刷新(全緩沖)。(常見的對磁盤文件寫入數據)
特殊情況:1、用戶強制刷新(fflush) ? ? ? ? ?2、進程退出
注:所有的設備,永遠都傾向于全緩沖,即緩沖區滿了才刷新,因為這樣只需要更少的IO操作,更少次的外設訪問,效率更高。
當然,我們要根據實際情況去改變刷新策略。如:顯示器是直接給用戶看的,一方面要照顧效率,一方面要照顧用戶體驗。所以顯示器一般使用行刷新。
2.3、緩沖區由誰提供
從上面的例子,我們發現直接往顯示器上打印的結果為4條,往文件打印的結果為7條,這跟緩沖區有關,同時這也說明了緩沖區一定不在Linux內核中,為什么?因為write是系統接口,如果在內核中,write也應該打印兩次。所以緩沖區是由C標準庫提供的。
我們之前所說的所有緩沖區都指的是用戶級語言層面提供的緩沖區。stdout,stdin,stderr對應的類型——FILE*,FILE是一個結構體,里面封裝了fd,同時還包括了一個緩沖區。
從源碼出發,我們可以來看一看FILE結構體:
2.4、重看問題
有了緩沖區的概念,我們就來解釋解釋問題引入中的現象。
首先,我們要先知道,代碼運行完了,并不代表數據已經刷新了。上面代碼中,使用C語言函數的操作在執行完了后,先將數據寫入了緩沖區中,并沒有直接向顯示器上打印。
第一次運行,沒有重定向操作,就是直接向顯示器打印,而顯示器的刷新策略是行刷新,且每個代碼后面都有\n,所以在調用fork之前,代碼不僅執行完了,而且數據都已經刷新了。所以fork對結果沒有影響。
第二次運行,我們有了重定向操作,于是函數就由向顯示器打印變成了向磁盤文件打印。所以刷新策略也由行刷新變成了滿刷新。那么\n就已經沒有意義了。所以代碼在運行到fork時,之前的代碼雖然已經運行完成了,但是數據還沒有刷新到文件中。數據還在當前進程對應的C標準庫中的緩沖區中,且該數據屬于父進程。
于是最后,我們fork創建了子進程。接著,父進程或子進程退出,這時數據會強制刷新出來。我們假設父進程先退出:父進程退出后,其數據強制刷新,而刷新的過程也是一種寫入,所以這時,為了父子進程的數據不會相互影響,就會發生寫時拷貝!這樣數據就會有兩份,于是父子進程各自退出時都會刷新各自的數據。(當然,如果子進程先退出也是同樣的)
所以,簡單總結來說:重定向導致刷新策略發生了改變(由行緩沖變成了全緩沖)。同時發生了寫時拷貝,父子進程各自刷新。
三、緩沖區的簡單實現
有了緩沖區的一些基本概念。我們可以自己實現一個簡單的帶有緩沖區的struct file。
主函數:
int main
{MyFILE* fp = fopen_("log.txt", "r");if(fp==NULL){printf("open file fail");return 0;}fputs_("hello world", fp);fclose_(fp);return 0;
}
struct file
struct MyFILE_
{int fd;char buff[NUM];int end;//當前緩沖區的結尾
};typedef struct MyFILE_ MyFILE;
?fopen函數的簡單實現
MyFILE* fopen_(const char* pathname, const char* mode)
{assert(pathname);assert(mode);MyFILE* fp = NULL;if(strcmp(mode, "w")==0){int fd = open(pathname, O_WRONLY|O_TRUNC|O_CREAT);if(fd>0){MyFILE* fp=(MyFILE*)malloc(sizeof(MyFILE));memset(fp,'\0',sizeof(MyFILE));fp->fd = fd;}}else if(strcmp(mode, "w+")==0){}else if(strcmp(mode,"r")==0){}else if(strcmp(mode,"r+")==0){}else if(strcmp(mode,"a")==0){}else if(strcmp(mode,"a+")==0){}else {}return fp;
}
fputs函數的簡單實現
void fputs_(const char* message, MyFILE* fp)
{assert(message);assert(fp);strcpy(fp->buff+fp->end, message);fp->end += strlen(message);if(fp->fd==0){}else if(fp->fd==1){if(fp->buff[fp->end-1]== '\n'){write(fp->fd, fp->buff,fp->end);fp->end = 0;}}else if(fp->fd==2){}else {}
}
fclose函數簡單實現和fflush函數
void fclose_(MyFILE* fp)
{assert(fp);fflush_(fp);close(fp->fd);free(fp);
}void fflush_(MyFILE* fp)
{assert(fp);if(fp->end != 0){write(fp->fd, fp->buff, fp->end);syncfs(fp->fd);fp-> end = 0;}
}