?
目錄
0. 文件先前理解
1. C文件接口
1.1 寫文件
1.2?讀文件
1.3?輸出信息到顯示器
1.4 總結 and stdin & stdout & stderr
2. 系統調用文件I/O
2.1 系統接口使用示例
2.2?接口介紹
2.3?open函數返回值
3. 文件描述符fd及重定向
3.1?0 & 1 & 2
3.2 文件描述符fd的理解
3.3 文件描述符Linux內核源碼分析
3.4?文件描述符的分配規則
3.5?重定向
3.6 使用 dup2 系統調用
4. 在minishell中添加重定向功能
5. 有關FILE緩沖區及Linux一切皆文件理解
5.1 一切皆文件及C語言實現運行時多態
5.2 緩沖區
5.3?C標準庫FILE結構體及緩沖區
5.4 大致模擬緩沖區設計
0. 文件先前理解
文件 = 文件內容 + 文件屬性
因此,對文件進行的操作無非:對內容,對屬性
文件本質存放在磁盤上,訪問文件,先寫代碼 -> 編譯 -> exe -> 運行 ->訪問文件,其本質是進程在訪問文件!!!
????????要向硬件寫入,只有操作系統具備權利,而普通用戶寫入,則需要使用操作系統提供的文件類系統調用接口,而語言層面對系統調用接口進行了封裝,由于不同操作系統平臺提供的系統調用接口不同,語言把所有平臺的代碼都實現條件編譯—動態裁剪,使其語言具有跨平臺性!如果語言不提供對文件的系統接口的封裝,所有訪問文件的操作,都必須使用OS的接口,一旦使用系統接口,編寫所謂的文件代碼,就無法在其他平臺安全運行,就不再具有跨平臺性。
學習OS層面的文件系統,本質是為了更好的理解不同語言底層操作實際是相通的!
顯示器是硬件,而printf向顯示器打印,本質也是一種寫入!
Linux下,一切皆文件,又該如何理解?
文件而言:
曾經理解的文件,站在寫程序的角度,將文件打開,加載到內存,進行read,write
顯示器:printf,cout -> 一種write
鍵盤:scanf,cin -> 一種read
普通文件 -> fopen/fread -> 你的進程內部(內存) -> fwrite -> 文件中
? ? ? ? ?????????????????input? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? output
站在系統的角度,能夠被input讀取,或者output寫出的設備就叫做文件!
狹義的文件——普通磁盤文件
廣義上的文件——顯示器,鍵盤,網卡,聲卡,顯卡,磁盤。幾乎所有外色,都可以稱之為文件
1. C文件接口
打開文件:fopen
打開方式: r,r+,w,w+,a,a+...
fopen以當前路徑打開文件或者相對路徑打開文件
當前路徑默認是:當一個進程運行起來,在其pcb結構體中,沒個進程都會記錄當前所處的工作路徑—cwd(創建工作的目錄)即為該進程的當前路徑!
1.1 寫文件
//打開文件
FILE * fopen ( const char * filename, const char * mode );
//關閉文件
int fclose ( FILE * stream );
//fwrite:
size_t?fwrite(?const?void?*buffer,?size_t?size,?size_t?count,?FILE?*stream?);
buffer—要寫入的數據? size—一個元素的大小 count—幾個元素 stream—文件流
#include <stdio.h> #include <string.h> int main() {FILE* fp = fopen("myfile", "w");if (!fp) {printf("fopen error!\n");}const char* msg = "hello Linux!\n";int count = 5;while (count--) {fwrite(msg, strlen(msg), 1, fp);}fclose(fp);return 0; }
注意:這里寫入字符串不計算'\0',因為字符串結束標志是C語言提供的,而不是操作系統所具有的!
1.2?讀文件
//fread:
size_t?fread(?void?*buffer,?size_t?size,?size_t?count,?FILE?*stream?);
#include <stdio.h> #include <string.h> int main() {FILE* fp = fopen("myfile", "r");if (!fp) {printf("fopen error!\n");}char buf[1024];const char* msg = "hello Linux!\n";while (1) {size_t s = fread(buf, 1, strlen(msg), fp);if (s > 0) {buf[s] = 0;printf("%s", buf);}if (feof(fp)) {break;}}fclose(fp);return 0; }
1.3?輸出信息到顯示器
將寫入的數據流入stdout,即輸出到顯示器
#include <stdio.h> #include <string.h> int main() {const char* msg = "hello fwrite\n";fwrite(msg, strlen(msg), 1, stdout);printf("hello printf\n");fprintf(stdout, "hello fprintf\n");return 0; }
1.4 總結 and stdin & stdout & stderr
C默認會打開三個輸入輸出流,分別是stdin, stdout, stderr
仔細觀察發現,這三個流的類型都是FILE*, fopen返回值類型,文件指針
打開文件的方式:
r? ? ? ? ? Open text file for reading. The stream is positioned at the beginning of the file.
r+ ???????Open for reading and writing. The stream is positioned at the beginning of the file.
w ????????Truncate(縮短) file to zero length or create text file for writing. The stream is? ? ? ? ? ? ? ? ? ? ? ? ? positioned at the beginning of the file.
w+ ??????Open for reading and writing.The file is created if it does not exist, otherwise it is? ? ? ? ? ? ? ? ? ? truncated. The stream is positioned at the beginning of the file.
a? ? ? ? ? Open for appending (writing at end of file). The file is created if it does not exist.? ? ? ? ? ? ? ? ? ? The?stream is positioned at the end of the file.
a+ ???????Open for reading and appending (writing at end of file). The file is created if it does? ? ? ? ? ? ? ? not exist. The initial file position for reading is at the beginning of the file, but output? ? ? ? ? ? ? is always appended to the end of the file.
如上,是我們之前學的文件相關操作。還有 fseek ftell rewind 的函數,在C部分已經有所涉獵,請同學們自 行復習。
2. 系統調用文件I/O
操作文件,除了上述C接口(當然,C++也有接口,其他語言也有),我們還可以采用系統接口來進行文件訪問, 先來直接以代碼的形式,實現和上面一模一樣的代碼!
2.1 系統接口使用示例
open close read write O_CREAT O_TRUNC O_WRONLY O_RDONLY O_RDWR...
寫文件:
#include <stdio.h> #include <sys/types.h> #include <sys/stat.h> #include <fcntl.h> #include <unistd.h> #include <string.h> int main() {umask(0);int fd = open("myfile", O_WRONLY | O_CREAT, 0644);if (fd < 0) {perror("open");return 1;}int count = 5;const char* msg = "hello Linux!\n";int len = strlen(msg);while (count--) {write(fd, msg, len);//fd: 后續, msg:緩沖區首地址, len: 本次讀取,期望寫入多少個字節的數//據。 返回值:實際寫了多少字節數據}close(fd);return 0; }
讀文件:
#include <stdio.h> #include <sys/types.h> #include <sys/stat.h> #include <fcntl.h> #include <unistd.h> #include <string.h> int main() {int fd = open("myfile", O_RDONLY);if (fd < 0) {perror("open");return 1;}const char* msg = "hello Linux!\n";char buf[1024];while (1) {ssize_t s = read(fd, buf, strlen(msg));//類比writeif (s > 0) {printf("%s", buf);}else {break;}}close(fd);return 0; }
在應用層看到一個很簡單的動作,在系統接口層面甚至OS層面,可能要做非常多的動作!
2.2?接口介紹
open ——?man open
#include<sys/types.h> #include<sys/stat.h> #include<fcntl.h> int open(const char *pathname, int flags); int open(const char *pathname, int flags, mode_t mode); pathname: 要打開或創建的目標文件 flags: 打開文件時,可以傳入多個參數選項,用下面的一個或者多個常量進行“或”運算,構成flags。 參數: O_RDONLY: 只讀打開 O_WRONLY: 只寫打開 O_RDWR : 讀,寫打開 這三個常量,必須指定一個且只能指定一個 O_CREAT : 若文件不存在,則創建它。需要使用mode選項,來指明新文件的訪問權限 O_APPEND: 追加寫 返回值:成功:新打開的文件描述符失敗:-1
mode_t理解:直接 man 手冊,比什么都清楚。
open 函數具體使用哪個,和具體應用場景相關,如目標文件不存在,需要open創建,則第三個參數表示創建文件的默認權限,否則,使用兩個參數的open。
2.3?open函數返回值
在認識返回值之前,先來認識一下兩個概念: 系統調用 和 庫函數
- 上面的 fopen fclose fread fwrite 都是C標準庫當中的函數,我們稱之為庫函數(libc)
- 而, open close read write lseek 都屬于系統提供的接口,稱之為系統調用接口
- 操作系統概念時,畫的一張圖
系統調用接口和庫函數的關系,一目了然。 所以,可以認為,f#系列的函數,都是對系統調用的封裝,方便二次開發。
3. 文件描述符fd及重定向
由上述系統調用open可知成功返回新打開的文件描述符(file descriptor)(fp)
#include <stdio.h> #include <sys/types.h> #include <sys/stat.h> #include <fcntl.h> #include <unistd.h> #include <string.h> int main() {//未做差錯處理int fd1 = open("myfile1", o_rdonly);int fd2 = open("myfile2", o_rdonly);int fd3 = open("myfile3", o_rdonly);int fd4 = open("myfile4", o_rdonly);printf("open sucess, fd: %d", fd1);printf("open sucess, fd: %d", fd2);printf("open sucess, fd: %d", fd3);printf("open sucess, fd: %d", fd4);close(fd1);close(fd2);close(fd3);close(fd4);return 0; }
?而打開多個文件,發現其fd從3開始依次遞增!0,1,2去哪里了?
可知文件描述符就是一個小整數,如何理解文件描述符?
3.1?0 & 1 & 2
- Linux進程默認情況下會有3個缺省打開的文件描述符,分別是標準輸入0, 標準輸出1, 標準錯誤2.
- 0,1,2對應的物理設備一般是:鍵盤,顯示器,顯示器
- 所以輸入輸出還可以采用如下方式:
#include <stdio.h> #include <sys/types.h> #include <sys/stat.h> #include <fcntl.h> #include <string.h> int main() {char buf[1024];ssize_t s = read(0, buf, sizeof(buf));if (s > 0) {buf[s] = 0;write(1, buf, strlen(buf));write(2, buf, strlen(buf));}return 0; }
3.2 文件描述符fd的理解
FILE* fopen(const char* path, const char* mode);
C標準庫提供的文件操作可知,FILE是一個由C標準庫提供的結構體
由上述可知,C語言庫函數內部一定封裝了系統調用接口,lib在系統調用之上,站在系統角度,操作系統并不認識C語言內部的FILE結構體,只認識fd,因此FILE結構體內部,必定封裝了fd!!
所以,fd是什么?
? ? ? ? 進程想要訪問文件,必須先打開文件
? ? ? ? 一般而言,進程 : 打開文件 = 1 :n
文件要被訪問,前提是加載到內存中,才能直接被訪問!
可知一個進程可以打開多個文件,多個進程運行時,會存在大量的被打開的文件,所以,操作系統需要把這些被各個進程打開的文件管理起來,如何管理?先描述,在組織!!!
?
而現在知道,文件描述符就是從0開始的小整數。當我們打開文件時,操作系統在內存中要創建相應的數據結構來描述目標文件。于是就有了file結構體。表示一個已經打開的文件對象。而進程執行open系統調用,所以必須讓進 程和文件關聯起來。每個進程都有一個指針*files, 指向一張表files_struct,該表最重要的部分就是包涵一個指針數組,每個元素都是一個指向打開文件的指針!所以,本質上,文件描述符就是該數組的下標。所以,只要拿著文件描述符,就可以找到對應的文件
3.3 文件描述符Linux內核源碼分析
文件:磁盤文件(未被打開的文件)????????? ? ? ? 內存文件(被進程打開的文件)
????????因此沒每個進程的PCB中都一個文件表指針struct files_struct * files,指向一個struct files_struct文件表結構體,該文件表結構體內一定包含一個struct file* fd_array[]文件指針數組,其數組每個對應存取指向一個被打開文件結構體file的指針,file記錄了該文件的所有屬性及所有內容。
?
?fwrite() -> FILE* -> fd -> write -> write(fd, ...) -> 自己執行操作系統內部的write方法 -> 能找到進程的task_struct -> *files -> files_struct -> fd_array[] -> fd_array[fd] -> struct file -> 內存文件被找到 -> 操作
#include<stdio.h> #include<stdlib.h>struct File {void(*read_p)();void(*write_p)(); };void readByKeyBoard() {cout << "從鍵盤讀取" << endl; } void writeByKeyBoard() {cout << "nothing to do!" << endl; }void readByFile() {cout << "從文件讀取" << endl; } void writeFile() {cout << "往文件寫入" << endl; }void TestFile(struct File file) {file.read_p();file.write_p(); }int main() {struct File fileByKB;fileByKB.read_p = readByKeyBoard;fileByKB.write_p = writeByKeyBoard;struct File fileByFILE;fileByFILE.read_p = readByFile;fileByFILE.write_p = writeFile;TestFile(fileByFILE);TestFile(fileByKB);return 0; }
每個文件被加載到內存,其文件管理創建對應的file結構體,結構體內部會根據驅動填充相應的函數指針write,read等操作函數的地址,因為磁盤、顯示器、鍵盤、網卡等不用硬件的讀寫方法不同,因此為了實現多態調用,C語言可以采用函數指針,實例化對象時填充不同的函數地址,以實現多態調用!
3.4?文件描述符的分配規則
直接看代碼:
#include <stdio.h> #include <sys/types.h> #include <sys/stat.h> #include <fcntl.h> int main() {int fd = open("myfile", O_RDONLY);if(fd < 0){perror("open");return 1;}printf("fd: %d\n", fd);close(fd);return 0; }
輸出發現是 fd: 3
關閉0或者2,在看
#include <stdio.h> #include <sys/types.h> #include <sys/stat.h> #include <fcntl.h> int main() {close(0);//close(2);int fd = open("myfile", O_RDONLY);if (fd < 0) {perror("open");return 1;}printf("fd: %d\n", fd);close(fd);return 0; }
發現是結果是: fd: 0 或者 fd 2 可見,文件描述符的分配規則:在files_struct數組當中,找到當前沒有被使用的最小的一個下標,作為新的文件描述符。
3.5?重定向
那如果關閉1呢?看代碼:
#include <stdio.h> #include <sys/types.h> #include <sys/stat.h> #include <fcntl.h> #include <stdlib.h> int main() {close(1);int fd = open("myfile", O_WRONLY | O_CREAT, 00644);if (fd < 0) {perror("open");return 1;}printf("fd: %d\n", fd);fflush(stdout);close(fd);exit(0); }
此時,發現,本來應該輸出到顯示器上的內容,輸出到了文件 myfile 當中,其中,fd=1。這種現象叫做輸出重定向。常見的重定向有:>, >>, <
?重定向的本質,其實是在OS內部,更改fd對應的內容的指向!!!
3.6 使用 dup2 系統調用
函數原型如下:
#include<unistd.h>?
????????int dup2(int oldfd, int newfd);
這里的oldfd copy 給 newfd,必要時會釋放oldfd
?
代碼如下:
#include <stdio.h> #include <unistd.h> #include <fcntl.h> int main() {int fd = open("./log", O_CREAT | O_RDWR);if (fd < 0) {perror("open");return 1;}close(1);dup2(fd, 1);for (;;) {char buf[1024] = { 0 };ssize_t read_size = read(0, buf, sizeof(buf) - 1);if (read_size < 0) {perror("read");break;}printf("%s", buf);fflush(stdout);}return 0; }
?
4. 在minishell中添加重定向功能
#include<stdio.h> #include<stdlib.h> #include<string.h> #include<unistd.h> #include<sys/wait.h> #include<sys/types.h> #include<sys/stat.h> #include<fcntl.h> #include<assert.h>#define NUM 1024 #define SIZE 32 char cmd_line[NUM];void dealStr(char* str, char** argv) {char* dealstr = NULL;const char* sep = " ";size_t i = 0;for (dealstr = strtok(str, sep); dealstr != NULL; dealstr = strtok(NULL, sep)) {if (i < SIZE) {argv[i] = dealstr;i++;}}if(strcmp(argv[0], "ls") == 0){argv[i] = (char*)"--color=auto";}argv[++i] = NULL; }#define INPUT_REDIR 1 #define OUTPUT_REDIR 2 #define APPEND_REDIR 3 #define NONE_REDIR 0 int redir_status = NONE_REDIR;char* CheckRedir(char* start){assert(start);//ls -a -l\0char* end = start + strlen(start) - 1;while(end >= start){if(*end == '>'){if(*(end - 1) == '>'){redir_status = APPEND_REDIR;*(end-1) = '\0';end++;break;}redir_status = OUTPUT_REDIR;*end = '\0';end++;break;//ls -a -l>myfile.txt//ls -a -l>>myfile.txt}else if(*end == '<'){redir_status = INPUT_REDIR;*end = '\0';end++;break;}else{end--;}}if(end >= start){return end;//要打開文件}else{return NULL;} }//環境變量保存的是地址,而拿不到環境變量本質是地址內容被清空,MY_VAL=NULL char buffer[64];//shell運行原理:通過讓子進程執行命令,父進程等待&&解析命令 int main(){//extern char** environ;while(1){//1.打印提示信息printf("[root@localhost myshell]#");fflush(stdout);//2.獲取用戶輸入memset(cmd_line, '\0', sizeof cmd_line);char *g_argv[SIZE] = { NULL };if(fgets(cmd_line, sizeof cmd_line, stdin) == NULL){continue;}cmd_line[strlen(cmd_line) - 1] = '\0';//printf("echo:%s\n", cmd_line);//"ls -a -l > log.txt" -> "ls -a -l\0log.txt"char* sep = CheckRedir(cmd_line);//3.命令行字符串分割dealStr(cmd_line, g_argv); //4.TODO 內置命令,讓父進程(shell)自己執行的命令,叫做內置命令,內建命令// 內建命令本質就是shell中的一個函數調用if(strcmp("cd", g_argv[0]) == 0){//not child execute, father executeif(g_argv[1] != NULL)chdir(g_argv[1]);continue;}if(strcmp("export", g_argv[0]) == 0 && g_argv[1] != NULL){strcpy(buffer, g_argv[1]);putenv(buffer);//int checkPutenv = putenv(buffer);//if(checkPutenv == 0){// printf("Putenv sucess\n");// //for(size_t i = 0; environ[i]; i++){// //printf("%s\n", environ[i]);// // }//}else{// printf("Putenc fail\n");//}continue;}//5.forkpid_t id = fork();if(id < 0){perror("fork");exit(1);}else if(id == 0){if(sep != NULL){int fd = -1;switch(redir_status){case INPUT_REDIR:fd = open(sep, O_RDONLY);dup2(fd, 0);break;case OUTPUT_REDIR:fd = open(sep, O_WRONLY | O_TRUNC | O_CREAT, 0666);dup2(fd, 1);break;case APPEND_REDIR:fd = open(sep, O_WRONLY | O_APPEND);dup2(fd, 1);break;default:printf("bug?\n");break;}}//printf("getenv : %s\n", getenv("MY_VAL"));//execvpe(g_argv[0], g_argv, environ);//子進程繼承父進程環境變量execvp(g_argv[0], g_argv);exit(1);}int status = 0;pid_t ret = waitpid(id, &status, 0);if(ret > 0){printf("exit code:%d -> result:%s\n",WEXITSTATUS(status), strerror(WEXITSTATUS(status)));}else{printf("wait fail!\n");exit(1);}}//end whilereturn 0; }
5. 有關FILE緩沖區及Linux一切皆文件理解
Linux設計則學——一切皆文件——體現在操作系統的軟件設計層面的
5.1 一切皆文件及C語言實現運行時多態
Linux是C語言寫的!如何用C語言實現面向對象,甚至是運行時多態?
可以使用結構體及函數指針實現面向對象!!!及運行時多態!!!
在Linux下,根據馮諾依曼體系結構,大部分硬件都輸入IO設備,IO設備主要用于輸入輸出,因此根據其驅動層提供的讀寫方法,即可實現一切皆文件的概念!
?所有的底層設備,都可以有自己的read和write方法的具體實現,但是,其具體的代碼一定是不一樣的!而通過函數指針使其根據硬件指向不同的讀寫方法,在調用時,就像在使用同一個函數!
5.2 緩沖區
1. 什么是緩沖區??
? ? ? ? 就是一段內存空間(這個空間社提供? 用戶(char buffer[64], scanf(buffer))? 語言? OS?)
2. 為什么要有緩沖區
? ? ? ? 提高整機效率,主要是為了提高用戶的響應速度!
寫透模式(WT模式): 只要寫就刷新,會進行頻繁的IO操作,成本高,且效率慢!
回寫模式(WB模式):提供緩沖區,定義刷新策略,IO操作次數降低,效率高!
緩沖區的刷新策略:
????????1. 立即刷新
????????2. 行刷新(行緩沖)
????????3. 滿刷新(全緩沖)
特殊情況:
????????1. 用戶強制刷新(fflush)
????????2. 進程退出
緩沖策略 = 一般 + 特殊, 緩沖策略是可控制的!
一般而言:行緩沖設備文件 —— 顯示器? ? ? ? ? ?全緩沖設備文件 —— 磁盤文件
所有的設備,永遠都傾向于全緩沖! —— 緩沖區滿了,才刷新 --> 需要更少次的IO操作 --> 更少次的外設訪問 --> 提高效率!
和外部設備進行IO的時候,數據量的大小不是主要矛盾,而和外設預備IO的過程是最耗費時間的!其他刷新策略是結合具體情況所做的妥協!
顯示器:需要直接給用戶看的,一方面照顧效率,一方面照顧用戶體驗,極端情況,是可以自定義規則的!
5.3?C標準庫FILE結構體及緩沖區
因為IO相關函數與系統調用接口對應,并且庫函數封裝系統調用,所以本質上,訪問文件都是通過fd訪問的。 所以C庫當中的FILE結構體內部,必定封裝了fd。
來段代碼在研究一下:
#include <stdio.h> #include <string.h> int main() {const char* msg0 = "hello printf\n";const char* msg1 = "hello fwrite\n";const char* msg2 = "hello write\n";printf("%s", msg0);fwrite(msg1, strlen(msg0), 1, stdout);write(1, msg2, strlen(msg2));fork();return 0; }
運行出結果:
hello printf hello fwrite hello write
但如果對進程實現輸出重定向呢? ./hello > file , 我們發現結果變成了:
hello write hello printf hello fwrite hello printf hello fwrite
我們發現 printf 和 fwrite (庫函數)都輸出了2次,而 write 只輸出了一次(系統調用)。為什么呢?肯定和 fork有關!
- 一般C庫函數寫入文件時是全緩沖的,而寫入顯示器是行緩沖。
- printf fwrite 庫函數會自帶緩沖區(進度條例子就可以說明),當發生重定向到普通文件時,數據 的緩沖方式由行緩沖變成了全緩沖。
- 而我們放在緩沖區中的數據,就不會被立即刷新,甚至fork之后
- 但是進程退出之后,會統一刷新,寫入文件當中。
- 但是fork的時候,父子數據會發生寫時拷貝,所以當你父進程準備刷新的時候,子進程也就有了同樣的 一份數據,隨即產生兩份數據。
- write 沒有變化,說明沒有所謂的緩沖。
綜上: printf fwrite 庫函數會自帶緩沖區,而 write 系統調用沒有帶緩沖區。另外,我們這里所說的緩沖區, 都是用戶級緩沖區。其實為了提升整機性能,OS也會提供相關內核級緩沖區,不過不再我們討論范圍之內。 那這個緩沖區誰提供呢? printf fwrite 是庫函數, write 是系統調用,庫函數在系統調用的“上層”, 是對系統 調用的“封裝”,但是 write 沒有緩沖區,而 printf fwrite 有,足以說明,該緩沖區是二次加上的,又因為是 C,所以由C標準庫提供。
如果有興趣,可以看看FILE結構體:
在 / usr / include / libio.h struct _IO_FILE {int _flags; /* High-order word is _IO_MAGIC; rest is flags. */ #define _IO_file_flags _flags//緩沖區相關/* The following pointers correspond to the C++ streambuf protocol. *//* Note: Tk uses the _IO_read_ptr and _IO_read_end fields directly. */char* _IO_read_ptr; /* Current read pointer */char* _IO_read_end; /* End of get area. */char* _IO_read_base; /* Start of putback+get area. */char* _IO_write_base; /* Start of put area. */char* _IO_write_ptr; /* Current put pointer. */char* _IO_write_end; /* End of put area. */char* _IO_buf_base; /* Start of reserve area. */char* _IO_buf_end; /* End of reserve area. *//* The following fields are used to support backing up and undo. */char* _IO_save_base; /* Pointer to start of non-current get area. */char* _IO_backup_base; /* Pointer to first valid character of backup area */char* _IO_save_end; /* Pointer to end of non-current get area. */struct _IO_marker* _markers;struct _IO_FILE* _chain;int _fileno; //封裝的文件描述符 #if 0int _blksize; #elseint _flags2; #endif_IO_off_t _old_offset; /* This used to be _offset but it's too small. */ #define __HAVE_COLUMN /* temporary *//* 1+column number of pbase(); 0 is unknown. */unsigned short _cur_column;signed char _vtable_offset;char _shortbuf[1];/* char* _save_gptr; char* _save_egptr; */_IO_lock_t* _lock; #ifdef _IO_USE_OLD_IO_FILE };
5.4 大致模擬緩沖區設計
#include<stdio.h> #include<string.h> #include<unistd.h> #include<sys/types.h> #include<sys/stat.h> #include<fcntl.h> #include<assert.h> #include<stdlib.h>#define NUM 1024 typedef struct MyFILE_{int fd;char buffer[NUM];int end;//當前緩沖區結尾 }MFILE;MFILE* fopen_(const char* pathname, const char* mode){assert(pathname && mode);MFILE* fp = NULL;if(strcmp(mode, "r") == 0){}else if(strcmp(mode, "r+") == 0){}else if(strcmp(mode, "w") == 0){int fd = open(pathname, O_WRONLY | O_CREAT | O_TRUNC, 0666);if(fd >= 0){fp = (MFILE*)malloc(sizeof(MFILE));memset(fp, 0, sizeof(MFILE));fp->fd = fd;}}else if(strcmp(mode, "w+") == 0){}else if(strcmp(mode, "a") == 0){}else if(strcmp(mode, "a+") == 0){}else{}return fp; }void fputs_(const char* message, MFILE* fp){assert(message && fp);strcpy(fp->buffer + fp->end, message);fp->end += strlen(message);//for debugfprintf(stderr, "%s\n", fp->buffer);//暫時未刷新//制定刷新策略,刷新策略是由誰來執行的呢?//答案是用戶通過執行C標準庫中的代碼邏輯,來完成刷新//效率提高體現,幾行代碼就減少頻繁的IO操作執行次數(注:不是數據量)//stdinif(fp->fd == 0){//stdout}else if(fp->fd == 1){if(fp->buffer[fp->end-1] == '\n'){ // //for debug // fprintf(stderr, "fllush : %s", fp->buffer);write(fp->fd, fp->buffer, fp->end);fp->end = 0;}//stderr}else if(fp->fd == 2){}else{} }void fflush_(MFILE* fp){assert(fp);if(fp->end != 0){//暫且認為刷新了 -- 其實是把數據寫到了內核中//如果想將數據由內核刷新到外設,系統調用sync-syncfswrite(fp->fd, fp->buffer, fp->end);syncfs(fp->fd);//將數據寫入磁盤fp->end = 0;} }void fclose_(MFILE* fp){assert(fp);fflush_(fp);close(fp->fd);free(fp); } int main(){//close(1);MFILE* fp = fopen_("./log.txt", "w");if(fp == NULL){printf("open file error\n");return 1;}fputs_("one:hello world\n", fp);//fputs_("two:hello world\n", fp);//fputs_("three:hello world", fp);//fputs_("four:hello world\n", fp);//fputs_("five:hello world", fp);//行刷新策略 one one:two tree tree:four fivefork();//fork時,上下文數據寫時拷貝,在緩沖區的one刷新兩次fclose_(fp);return 0; }