#include<stdio.h>
#include<unistd.h>int main()
{printf("XXXXXXX");sleep(3);return 0;
}
為什么以下代碼是先顯示"XXXXXXX",屏幕才顯示"XXXXXXX"sleep三秒。
#include<stdio.h>
#include<unistd.h>int main()
{printf("XXXXXXX\n");sleep(3);return 0;
}
1.什么是緩沖區
舉個例子:
假設有兩個人,張三和李四。他們兩個是網友,有一天呢李四過生日,張三就準備把自己給他準備的禮物給愛他,但是張三和李四距離太遠,以前呢張三騎著自己的小單車就出發了,經過兩個月的長途跋涉終于送給了李四。張三兩個月沒上班,一想,虧死了。
后來,有了菜鳥驛站,張三需要給李四禮物就不用騎單車了,張三只需要把快遞交給樓下的菜鳥驛站,至于菜鳥驛站什么時候發,快遞什么時候到都不需要張三管,張三就可以繼續上班了,快遞到了先是放在李四樓下的菜鳥驛站,至于李四什么時候在家,菜鳥驛站再送。
至此,我們其實就可以大概猜得出,菜鳥驛站其實就是緩沖區的意思,而張三就是程序員,李四就是硬件。張三家樓下的菜鳥驛站就是C語言FILE結構體里面的一個長數組,他就是C語言下的緩沖區,我們的printf實際上就是先把數據寫到它里面去。(下面會詳細說FILE)
struct _IO_FILE {// ...其他字段...char* _IO_buf_base; // 緩沖區起始位置char* _IO_buf_end; // 緩沖區結束位置// ...其他字段...
};
李四樓下的緩沖區就是內核里面的緩沖區,在struct file里,struct?file里不止有文件的屬性和內容信息,還有緩沖區。?
struct file {// ...const struct file_operations *f_op; // 文件操作函數指針struct address_space *f_mapping; // 指向address_space結構// ...
};
其中,f_mapping
字段指向一個struct address_space
,這是文件與頁緩存之間的橋梁。?
?
2 .為什么要引?緩沖區機制
3.緩沖類型
緩沖區:這種緩沖?式要求填滿整個緩沖區后才進?I/O系統調?操作。對于磁盤?件的操作通常使?全緩沖的?式訪問。?緩沖區:在?緩沖情況下,當在輸?和輸出中遇到換?符時,標準I/O庫函數將會執?系統調?操作。當所操作的流涉及?個終端時(例如標準輸?和標準輸出),使??緩沖?式。因為標準I/O庫每?的緩沖區?度是固定的,所以只要填滿了緩沖區,即使還沒有遇到換?符,也會執?I/O系統調?操作,默認?緩沖區的??為1024。?緩沖區:?緩沖區是指標準I/O庫不對字符進?緩存,直接調?系統調?。標準出錯流stderr通常是不帶緩沖區的,這使得出錯信息能夠盡快地顯?出來。
1.? 緩沖區滿時;2.? 執?flush語句;
include <stdio.h>
#include <string.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
int main() {close(1);int fd = open("log.txt", O_WRONLY | O_CREAT | O_TRUNC, 0666);if (fd < 0) {perror("open");return 0;}printf("hello world: %d\n", fd);close(fd);return 0;
}
這里相當于把stdout用log.txt替換
[ljh@VM-8-12-centos buffer]$ ./myfile
[ljh@VM-8-12-centos buffer]$ ls
log.txt makefile myfile myfile.c
[ljh@VM-8-12-centos buffer]$ cat log.txt
[ljh@VM-8-12-centos buffer]$
當調用printf()
時,數據被寫入stdout
對應的FILE
結構體管理的緩沖區,這個緩沖區完全在用戶空間,由 C 標準庫 (libc
) 維護默認情況下,重定向到文件的流是全緩沖的,緩沖區大小通常為 4KB 或 8KB。
只有當 C 庫緩沖區刷新時 (通過fflush()
或緩沖區滿),才會調用write()
系統調用,write()
將數據從用戶空間復制到內核空間的頁緩存 (Page Cache),頁緩存由內核管理,位于物理內存中
include <stdio.h>
#include <string.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
int main() {close(1);int fd = open("log.txt", O_WRONLY | O_CREAT | O_TRUNC, 0666);if (fd < 0) {perror("open");return 0;}printf("hello world: %d\n", fd);fflush(stdout);close(fd);return 0;
}
#include <stdio.h>
#include <string.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
int main() {close(2);int fd = open("log.txt", O_WRONLY | O_CREAT | O_TRUNC, 0666);if (fd < 0) {perror("open");return 0;}perror("hello world");close(fd);return 0;
}
4.FILE
上面我們提到了FILE,這里我們詳細來聊聊。
4.1 FILE
結構體的本質與作用
FILE
是 C 標準庫中定義的結構體類型,用于封裝文件操作的相關信息,是用戶空間與系統調用之間的橋梁。它本質上是對文件描述符(fd)的一層封裝,同時管理 I/O 緩沖區,以提升 I/O 操作效率。
4.2 FILE
結構體的核心字段
?在 GNU C 庫(glibc)中,FILE
結構體的關鍵字段如下:
struct _IO_FILE {int _fileno; // 封裝的文件描述符(fd)char* _IO_buf_base; // 緩沖區起始地址char* _IO_buf_end; // 緩沖區結束地址char* _IO_read_ptr; // 讀緩沖區當前位置char* _IO_write_ptr; // 寫緩沖區當前位置int _flags; // 文件狀態標志(如讀寫模式、是否關閉等)const struct _IO_jump_t* _vtable; // 函數指針表,指向I/O操作函數// 其他字段(省略)
};
-
_fileno
:
直接關聯系統調用的文件描述符,是FILE
與內核交互的橋梁。例如,printf
最終會通過_fileno
對應的 fd 調用write
系統調用。 -
緩沖區相關指針:
_IO_buf_base
和_IO_buf_end
:標記緩沖區的物理范圍。_IO_read_ptr
和_IO_write_ptr
:記錄當前讀寫位置,用于控制數據在緩沖區中的流動。
-
_vtable
:
指向函數表,包含一系列 I/O 操作的實現(如讀、寫、刷新緩沖區等),體現了面向對象的設計思想。
4.3?FILE
與系統調用的關系
用戶空間視角:通過FILE*
指針調用printf
、fscanf
等庫函數,數據先存入FILE
的緩沖區。
內核空間視角:當緩沖區刷新時(如調用fflush
、緩沖區滿或程序結束),庫函數通過_fileno
調用write
、read
等系統調用,將數據傳入內核緩沖區(頁緩存)
4.4 為什么需要FILE
結構體?
- 跨平臺兼容性:封裝不同系統的文件操作細節(如 Windows 和 Linux 的文件描述符機制差異)。
- 緩沖區優化:通過用戶空間緩沖區減少系統調用次數,提升 I/O 效率。
- 高層抽象:提供更易用的接口(如
printf
的格式化輸出),隱藏底層系統調用的復雜性
FILE
結構體是 C 語言 I/O 體系的核心,它通過封裝文件描述符和管理用戶空間緩沖區,在高效性和易用性之間取得平衡。理解FILE
的內部結構,有助于深入掌握 C 語言 I/O 操作的本質,以及緩沖區刷新、重定向等關鍵機制。
5.一個關鍵問題
為了讓我們更好的理解上面的內容,我們來看以下代碼:
#include <stdio.h>
#include <string.h>
#include <unistd.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(msg1), 1, stdout);write(1, msg2, strlen(msg2));fork();return 0;
}
這個代碼看起來簡單,卻是檢測是否理解緩沖區的關鍵!!!
問題:
為什么運行出結果:
而重定向時:
為什么結果不一樣呢???
其實關鍵的原因就在于,顯示器文件和該文本文件的緩沖區刷新方式不同。
顯示器是行刷新,而文本是滿刷新。
所以在顯示器上,一行一行刷新,到子進程時緩沖區也沒東西了。
在重定向時,開始數據進入緩沖區,但是一直沒刷新,write是系統調用,直接輸入到內核緩沖區了所以先打印,到了fork時,子進程繼承了父進程緩沖區的東西,文件退出時,自動ffulsh,所以就得到了我們看見的現象。
- 一般 C 庫函數寫入文件時是全緩沖的,而寫入顯示器是行緩沖。
- printf fwrite 庫函數 + 會自帶緩沖區(進度條例子就可以說明),當發生重定向到普通文件時,數據的緩沖方式由行緩沖變成了全緩沖。
- 而我們放在緩沖區中的數據,就不會被立即刷新,甚至 fork 之后
- 但是進程退出之后,會統一刷新,寫入文件當中。
- 但是 fork 的時候,父子數據會發生寫時拷貝,所以當你父進程準備刷新的時候,子進程也就有了同樣的一份數據,隨即產生兩份數據。
- write 沒有變化,說明沒有所謂的緩沖區。
6.簡單設計一下libc庫
#pragma once#define SIZE 1024#define FLUSH_NONE 0
#define FLUSH_LINE 1
#define FLUSH_FULL 2struct IO_FILE
{int flag; // 刷新方式int fileno; // 文件描述符char outbuffer[SIZE];int cap;int size;// TODO
};typedef struct IO_FILE mFILE;mFILE *mfopen(const char *filename, const char *mode);
int mwrite(const void *ptr, int num, mFILE *stream);
void mfflush(mFILE *stream);
void mfclose(mFILE *stream);
my_stdio.c
#include "my_stdio.h"
#include <string.h>
#include <stdlib.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <fcntl.h>
#include <unistd.h>mFILE *mfopen(const char *filename, const char *mode)
{int fd = -1;if(strcmp(mode, "r") == 0){fd = open(filename, O_RDONLY);}else if(strcmp(mode, "w")== 0){fd = open(filename, O_CREAT|O_WRONLY|O_TRUNC, 0666);}else if(strcmp(mode, "a") == 0){fd = open(filename, O_CREAT|O_WRONLY|O_APPEND, 0666);}if(fd < 0) return NULL;mFILE *mf = (mFILE*)malloc(sizeof(mFILE));if(!mf){close(fd);return NULL;}mf->fileno = fd;mf->flag = FLUSH_LINE;mf->size = 0;mf->cap = SIZE;return mf;
}void mfflush(mFILE *stream)
{if(stream->size > 0){// 寫到內核文件的文件緩沖區中write(stream->fileno, stream->outbuffer, stream->size);// 刷新到外設fsync(stream->fileno);stream->size = 0;}
}int mfwrite(const void *ptr, int num, mFILE *stream)
{// 1. 拷貝memcpy(stream->outbuffer + stream->size, ptr, num);stream->size += num;// 2. 檢測是否要刷新if(stream->flag == FLUSH_LINE && stream->size > 0 && stream->outbuffer[stream->size - 1] == '\n'){mfflush(stream);}return num;
}void mfclose(mFILE *stream)
{if(stream->size > 0){mfflush(stream);}close(stream->fileno);
}
main.c
#include "my_stdio.h"
#include <stdio.h>
#include <string.h>
#include <unistd.h>int main()
{mFILE *fp = mfopen("./log.txt", "a");if(fp == NULL){return 1;}int cnt = 10;while(cnt){printf("write %d\n", cnt);char buffer[64];snprintf(buffer, sizeof(buffer),"hello message, number is : %d", cnt);cnt--;mfwrite(buffer, strlen(buffer), fp);mfflush(fp);sleep(1);}mfclose(fp);
}