梅開二度
在 C 語法下就早已知悉基礎(chǔ) IO ,其實(shí)就是耳熟能詳?shù)奈募僮鳎f到文件操作腦子里又是一堆耳熟能詳?shù)暮瘮?shù)接口:
以一個(gè)簡單的寫入操作為例,運(yùn)行程序后當(dāng)前路徑下會(huì)生成對應(yīng)文件,文件當(dāng)中就是我們寫入的內(nèi)容:
int main()
{
FILE* fp = fopen("log.txt", "w");
if (fp == NULL){
perror("fopen");
return 1;
}
int count = 5;
while (count){
fputs("hello worldn", fp);
count--;
}
fclose(fp);
return 0;
}
當(dāng)前路徑
文件操作我們打開文件時(shí),如果 fopen 對象是一個(gè)未創(chuàng)建的對象,那么就會(huì)自動(dòng)在當(dāng)前路徑生成一個(gè)該文件,這里就牽涉到一個(gè)當(dāng)前路徑 color{red} {當(dāng)前路徑}當(dāng)前路徑的概念。
比如我們在剛剛寫入后的 log.txt 文件進(jìn)行讀取:
int main()
{
FILE* fp = fopen("log.txt", "r");
if (fp == NULL){
perror("fopen");
return 1;
}
char buffer[64];
for (int i = 0; i < 5; i++){
fgets(buffer, sizeof(buffer), fp);
printf("%s", buffer);
}
fclose(fp);
return 0;
}
該情況下,我們在總目錄下運(yùn)行可執(zhí)行程序 myproc,那么該可執(zhí)行程序創(chuàng)建的 log.txt 文件會(huì)出現(xiàn)在總目錄下:
這是否意味著 “當(dāng)前路徑” 就是指的 “當(dāng)前可執(zhí)行程序所處的路徑”?
我們不妨直接去查看他的路徑對吧,我們用ps -axj | head -1&&ps -axj | grep myproc | grep -v grep可以查看可執(zhí)行程序的 PID :
然后我們再利用 PID 來查看執(zhí)行路徑sudo ls /proc/8189 -al,因?yàn)槲以诳偰夸?~ 下,因此這里我使用弄了 sudo 命令進(jìn)行管理員權(quán)限查找:
這里的cwd和exe是軟鏈接,我們下文細(xì)談,所以實(shí)際上,當(dāng)前路徑不是指可執(zhí)行程序所處的路徑,而是指該可執(zhí)行程序運(yùn)行成為進(jìn)程時(shí)我們所處的路徑
三大輸入輸出流
我們一直貫徹一個(gè)理念就是 Linux 下 一切皆文件,我們?nèi)庋劭梢姷娘@示屏輸出的數(shù)據(jù),本質(zhì)是電腦讀取鍵入的字符,電腦從“電腦文件” 讀取字符,電腦再對“顯示器文件”進(jìn)行輸出
那么問題來了,在我們對這些“文件”進(jìn)行讀寫之前,為什么我們沒有一個(gè)文件打開的操作呢?
要知道打開文件一定是進(jìn)程運(yùn)行的時(shí)候打開的,而任何進(jìn)程在運(yùn)行的時(shí)候都會(huì)默認(rèn)打開三個(gè)輸入輸出流,即標(biāo)準(zhǔn)輸入流、標(biāo)準(zhǔn)輸出流、標(biāo)準(zhǔn)錯(cuò)誤流,就是 C 當(dāng)中的 stdin、stdout、stderr;C++當(dāng)中的 cin、cout、cerr,其他所有語言都有類似的概念。實(shí)際上這種特性并不是某種語言所特有的,而是由操作系統(tǒng)所支持的
其中,標(biāo)準(zhǔn)輸入流對應(yīng)的設(shè)備就是鍵盤,標(biāo)準(zhǔn)輸出流和標(biāo)準(zhǔn)錯(cuò)誤流對應(yīng)的設(shè)備都是顯示器。查看 man 手冊我們不難發(fā)現(xiàn),stdin、stdout、stderr 這仨 byd 其實(shí)就是 FILE* 類型的
extern FILE *stdout;
extern FILE *stderr;
我們之所以可以調(diào)用 scanf 、printf 這類的函數(shù)向鍵盤顯示器進(jìn)行輸入輸出操作,其實(shí)就是程序運(yùn)行時(shí),操作系統(tǒng)默認(rèn)使用 C 的接口將這三個(gè)輸入輸出流打開。試想我們使用 fputs 函數(shù)時(shí),將其第二個(gè)參數(shù)設(shè)置為 stdout,此時(shí) fputs 函數(shù)會(huì)不會(huì)直接將數(shù)據(jù)顯示到顯示器上呢?
答案是肯定的,因?yàn)榇藭r(shí)就是用 fputs 向顯示器文件進(jìn)行了寫入操作
系統(tǒng)文件 I/O
相比 C,C++ 這些語言的接口,操作系統(tǒng)也有一套文件操作的接口,而且操作系統(tǒng)的接口更加貼近底層,而其他語言的接口本質(zhì)上也是對操作系統(tǒng)的接口的封裝,我們在 Linux、Windows 平臺(tái)下運(yùn)行 C 代碼時(shí),C 庫函數(shù)就是對 Linux、Windows 系統(tǒng)調(diào)用接口進(jìn)行的封裝,這樣做使得語言有了跨平臺(tái)性,也方便進(jìn)行二次開發(fā)
open
函數(shù)原型:
1.pathname 表示要打開或創(chuàng)建的目標(biāo)文件。
若pathname以路徑的方式給出,則當(dāng)需要?jiǎng)?chuàng)建該文件時(shí),就在pathname路徑下進(jìn)行創(chuàng)建。 若pathname以文件名的方式給出,則當(dāng)需要?jiǎng)?chuàng)建該文件時(shí),默認(rèn)在當(dāng)前路徑下進(jìn)行創(chuàng)建,注意當(dāng)前路徑的含義
2.flags 表示打開文件的方式。
flags 的可調(diào)用參數(shù)有如下這些:
flags 可以同時(shí)傳入多個(gè)參數(shù)選項(xiàng),這些選項(xiàng)用 “或” 運(yùn)算符連接。例如以只寫的方式打開文件時(shí),文件不存在就應(yīng)該自動(dòng)創(chuàng)建文件,則參數(shù)設(shè)置如下
我們基于與運(yùn)算的最根本原因是因?yàn)椋?這些宏定義選項(xiàng)的共同點(diǎn)就是它們的二進(jìn)制序列當(dāng)中有且只有一個(gè)比特位是 1 color{red} {這些宏定義選項(xiàng)的共同點(diǎn)就是它們的二進(jìn)制序列當(dāng)中有且只有一個(gè)比特位是 1}這些宏定義選項(xiàng)的共同點(diǎn)就是它們的二進(jìn)制序列當(dāng)中有且只有一個(gè)比特位是1,除了 O_RDONLY 序列為全 0,表示他為默認(rèn)選項(xiàng),且為 1 的比特位是各不相同的,這樣一來函數(shù)內(nèi)部就可以通過使用與運(yùn)算來判斷是否設(shè)置了某一選項(xiàng)
if (arg2&O_RDONLY){
//設(shè)置了O_RDONLY選項(xiàng)
}
if (arg2&O_WRONLY){
//設(shè)置了O_WRONLY選項(xiàng)
}
if (arg2&O_RDWR){
//設(shè)置了O_RDWR選項(xiàng)
}
if (arg2&O_CREAT){
//設(shè)置了O_CREAT選項(xiàng)
}
//...
}
3.mode,表示創(chuàng)建文件的默認(rèn)權(quán)限,在不創(chuàng)建文件時(shí),此選項(xiàng)可以不設(shè)置。
我們將mode設(shè)置為 0666,則文件創(chuàng)建出來的權(quán)限如下,按理說本來應(yīng)該是 :
但是不要忘了,Linux 系統(tǒng)設(shè)有 umask 權(quán)限掩碼,文件的真正權(quán)限計(jì)算方法是:mode &( ~umask),umask 的默認(rèn)值應(yīng)該是 0002,所以在我們自己設(shè)置的權(quán)限下應(yīng)該減去 umask 得到 0664,即:
當(dāng)然,如果想繞開 umask ,直接使用我們第一手的設(shè)置,那么我們可以直接將 umask 進(jìn)行置 0 操作
open 返回值
open 的返回值其實(shí)是新打開文件的文件描述符 fd,我們這里嘗試一次打開多個(gè)文件,然后分別打印它們的文件描述符:
#include
#include
#include
int main()
{
umask(0);
int fd1 = open("log1.txt", O_RDONLY | O_CREAT, 0666);
int fd2 = open("log2.txt", O_RDONLY | O_CREAT, 0666);
int fd3 = open("log3.txt", O_RDONLY | O_CREAT, 0666);
int fd4 = open("log4.txt", O_RDONLY | O_CREAT, 0666);
int fd5 = open("log5.txt", O_RDONLY | O_CREAT, 0666);
printf("fd1:%dn", fd1);
printf("fd2:%dn", fd2);
printf("fd3:%dn", fd3);
printf("fd4:%dn", fd4);
printf("fd5:%dn", fd5);
return 0;
}
我們又知道系統(tǒng)是無法打開一個(gè)不存在的文件 fd 會(huì)返回 -1,打開成功時(shí)如圖所示每個(gè)文件的 fd 從 3 開始且都是連續(xù)遞增的,那么問題來了:0~2 哪里去了?
所謂的文件描述符本質(zhì)上是一個(gè)指針數(shù)組的下標(biāo),指針數(shù)組當(dāng)中的每一個(gè)指針都指向一個(gè)被打開文件的文件信息,通過對應(yīng)文件的文件描述符就可以找到對應(yīng)的文件信息
open函數(shù)打開文件成功時(shí)數(shù)組當(dāng)中的指針個(gè)數(shù)增加,然后返回該指針在數(shù)組中的下標(biāo),而當(dāng)文件打開失敗時(shí)直接返回 -1,因此,成功打開多個(gè)文件時(shí)所獲得的文件描述符就是連續(xù)且遞增的
而 Linux 進(jìn)程默認(rèn)情況下會(huì)有 3 個(gè)缺省打開的文件描述符,分別就是標(biāo)準(zhǔn)輸入0、標(biāo)準(zhǔn)輸出1、標(biāo)準(zhǔn)錯(cuò)誤2,這就是為什么成功打開文件時(shí)所得到的文件描述符會(huì)從3開始
close
系統(tǒng)接口中使用close函數(shù)關(guān)閉文件,close函數(shù)的函數(shù)原型如下:
若關(guān)閉文件成功則返回 0,若關(guān)閉文件失敗則返回 -1
write
系統(tǒng)接口中使用write函數(shù)向文件寫入信息,write函數(shù)的函數(shù)原型如下:
write函數(shù)將 buf 位置開始向后 count 字節(jié)的數(shù)據(jù)寫入文件描述符為 fd 的文件當(dāng)中;如果數(shù)據(jù)寫入成功,返回寫入數(shù)據(jù)的字節(jié)個(gè)數(shù),如果數(shù)據(jù)寫入失敗,返回 -1。
read
系統(tǒng)接口中使用read函數(shù)從文件讀取信息,read函數(shù)的函數(shù)原型如下:
read 函數(shù)從文件描述符為 fd 的文件讀取 count 字節(jié)的數(shù)據(jù)到 buf 位置當(dāng)中。如果數(shù)據(jù)讀取成功,實(shí)際讀取數(shù)據(jù)的字節(jié)個(gè)數(shù)被返回;如果數(shù)據(jù)讀取失敗,返回 -1
文件描述符fd
我們知道文件只能在進(jìn)程執(zhí)行時(shí)才能打開,且一個(gè)進(jìn)程可打開多個(gè)文件,系統(tǒng)中存在大量的進(jìn)程,這就表示系統(tǒng)可以在任何時(shí)刻存在大量已經(jīng)打開的文件
Linux 思想面對批量的處理時(shí)總會(huì)采取 “先描述后組織” 的思想,系統(tǒng)會(huì)為大量的文件描述一個(gè) file struct 的結(jié)構(gòu)體,里面存放著這些文件的主要信息,然后將結(jié)構(gòu)體以雙鏈表的形式進(jìn)行組織,相當(dāng)于將文件的管理具象成對雙鏈表的增刪查改。
但是在大量進(jìn)程和大量已打開的文件里,我們要找到每個(gè)文件的歸屬進(jìn)程系統(tǒng)就應(yīng)該建立對應(yīng)關(guān)系
對應(yīng)關(guān)系
當(dāng)一個(gè)程序運(yùn)行起來時(shí),操作系統(tǒng)會(huì)將該程序的代碼和數(shù)據(jù)加載到內(nèi)存,然后為其創(chuàng)建對應(yīng)的task_struct、mm_struct、頁表等相關(guān)的數(shù)據(jù)結(jié)構(gòu),并通過頁表建立虛擬內(nèi)存和物理內(nèi)存之間的映射關(guān)系
首先在 task_struct 里有一個(gè)指針,他指向一個(gè)名為 file_struct 的結(jié)構(gòu)體,在這個(gè)結(jié)構(gòu)體里面又有一個(gè) fd_array 的指針數(shù)組,這個(gè)數(shù)組的下標(biāo)就是我們所謂的文件描述符。比如進(jìn)程打開 log.txt 時(shí)會(huì)先加載進(jìn)內(nèi)存形成 struct file ,然后將 struct file 放入一個(gè)文件的雙鏈表里,struct file 的首地址再放入鏈表中下標(biāo)為 3 處的地方,最后返回他的文件描述符即可。
向文件寫入數(shù)據(jù)時(shí),是先將數(shù)據(jù)寫入到對應(yīng)文件的緩沖區(qū)當(dāng)中,然后定期將緩沖區(qū)數(shù)據(jù)刷新,數(shù)據(jù)才能進(jìn)入磁盤。
那么為什么進(jìn)程創(chuàng)建時(shí)會(huì)默認(rèn)打開0、1、2 呢?
我們知道操作系統(tǒng)能夠識(shí)別硬件,操作系統(tǒng)能管理硬件也意味著鍵盤,顯示器這些東西都有自己對應(yīng)的 struct_file ,將這 3 個(gè) struct_file 放入雙鏈表,就會(huì)對應(yīng)填入到下標(biāo)為 0,1,2 的位置,就默認(rèn)打開了標(biāo)準(zhǔn)輸入流、輸出流、錯(cuò)誤流。
內(nèi)存文件
磁盤文件和內(nèi)存文件之間的關(guān)系就像程序和進(jìn)程的關(guān)系一樣,當(dāng)程序運(yùn)行起來后便成了進(jìn)程,而當(dāng)磁盤文件加載到內(nèi)存后便成了內(nèi)存文件。
磁盤文件分為了文件內(nèi)容和文件屬性兩部分,也將文件屬性叫做元信息,文件加載到內(nèi)存時(shí),一般先加載文件的屬性信息,當(dāng)需要對文件內(nèi)容進(jìn)行讀取、輸入或輸出等操作時(shí),再加載文件數(shù)據(jù)
分配規(guī)則
我們還是用最開始的代碼做解釋:
#include
#include
#include
int main()
{
umask(0);
int fd1 = open("log1.txt", O_RDONLY | O_CREAT, 0666);
int fd2 = open("log2.txt", O_RDONLY | O_CREAT, 0666);
int fd3 = open("log3.txt", O_RDONLY | O_CREAT, 0666);
int fd4 = open("log4.txt", O_RDONLY | O_CREAT, 0666);
int fd5 = open("log5.txt", O_RDONLY | O_CREAT, 0666);
printf("fd1:%dn", fd1);
printf("fd2:%dn", fd2);
printf("fd3:%dn", fd3);
printf("fd4:%dn", fd4);
printf("fd5:%dn", fd5);
return 0;
}
然而文件描述符是從最小的 0 開始且未被分配的開始分配的,比如我關(guān)閉了 0,2 的流,那么新打開 3 個(gè)文件就是不是 3,4 5 而是 0,2,3 了
close(2);//關(guān)閉描述符為 0,2 的文件
重定向
原理
到這里其實(shí)不難理解重定向的原理是修改文件描述符下標(biāo)對應(yīng)的 struct file* 內(nèi)容,比如我們說過的輸出重定向就是將一個(gè)本應(yīng)該輸出到一個(gè)文件的數(shù)據(jù)輸出到另一個(gè)文件
比如想讓本應(yīng)該輸出到顯示器的數(shù)據(jù)輸出到 log.txt 文件當(dāng)中,那么可以在打開 log.txt 文件之前將文件描述符為 1 的文件關(guān)閉,也就是將“顯示器文件”關(guān)閉,這樣一來,當(dāng)我們后續(xù)打開 log.txt 文件時(shí)所分配到的文件描述符就是 1
#include
#include
#include
#include
int main()
{
close(1);
int fd = open("log.txt", O_WRONLY | O_CREAT, 0666);//輸出重定向
if (fd < 0){
perror("open");
return 1;
}
printf("hello worldn");
printf("hello worldn");
printf("hello worldn");
printf("hello worldn");
printf("hello worldn");
fflush(stdout);
close(fd);
return 0;
}
這里 printf 是默認(rèn)向 stdout 輸出數(shù)據(jù)的,而 stdout 指向的 FILE 結(jié)構(gòu)體中存儲(chǔ)的文件描述符就是1,因此 printf 實(shí)際上就是向文件描述符為1的文件輸出數(shù)據(jù)。C 的數(shù)據(jù)并不是立馬寫到了內(nèi)存操作系統(tǒng)里面,而是寫到了緩沖區(qū)當(dāng)中,所以使用 printf 打印完后需要使用 fflush 將緩沖區(qū)當(dāng)中的數(shù)據(jù)刷新到文件中
可以看出,我執(zhí)行 file 程序時(shí)并沒有任何結(jié)果,但是打印 log.txt 時(shí)卻得到了我想要的結(jié)果,因此就證明了上面的觀點(diǎn):
但是又有一個(gè)問題:標(biāo)準(zhǔn)輸出流和標(biāo)準(zhǔn)錯(cuò)誤流對應(yīng)的都是顯示器,它們有什么區(qū)別嗎?
答案是有的, 我們以代碼為例:
int main()
{
printf("hello printfn"); //stdout
perror("perror"); //stderr
fprintf(stdout, "stdout:hello fprintfn"); //stdout
fprintf(stderr, "stderr:hello fprintfn"); //stderr
return 0;
}
結(jié)果一定會(huì)成功的輸出四行內(nèi)容,然后再對他進(jìn)行重定向到 log.txt 中:
很明顯這里 log.txt 文件當(dāng)中只有向標(biāo)準(zhǔn)輸出流輸出的兩行字符串,而向標(biāo)準(zhǔn)錯(cuò)誤流輸出的兩行數(shù)據(jù)并沒有重定向到文件當(dāng)中,而是仍然輸出到了顯示器上。實(shí)際上我們使用重定向時(shí),是對輸出流進(jìn)行了重定向,而對錯(cuò)誤流無影響 color{red} {實(shí)際上我們使用重定向時(shí),是對輸出流進(jìn)行了重定向,而對錯(cuò)誤流無影響}實(shí)際上我們使用重定向時(shí),是對輸出流進(jìn)行了重定向,而對錯(cuò)誤流無影響。
dup2
要完成重定向我們只需對 fd_array 數(shù)組當(dāng)中元素的拷貝即可,Linux 中對于重定向給出了一個(gè)接口:==dup2 ==,我們可以使用這個(gè)接口完成重定向:
dup2 會(huì)將 fd_array[oldfd] 的內(nèi)容拷貝到 fd_array[newfd] 當(dāng)中,如果有必要的話我們需要先使用關(guān)閉文件描述符為 newfd 的文件,dup2 函數(shù)返回值如果調(diào)用成功返回 newfd,否則返回 -1。
需要的是:
- 如果 oldfd 不是有效的文件描述符,則 dup2 調(diào)用失敗,并且此時(shí)文件描述符為 newfd 的文件沒有被關(guān)閉
- oldfd 是一個(gè)有效的文件描述符,但是 newfd 和 oldfd 具有相同的值,則 dup2 不做任何操作,并返回 newfd
比如:
#include
#include
#include
#include
int main()
{
int fd = open("log.txt", O_WRONLY | O_CREAT, 0666);
if (fd < 0){
perror("open");
return 1;
}
close(1);
dup2(fd, 1);
printf("hello printfn");
fprintf(stdout, "hello fprintfn");
return 0;
}
就像這樣,數(shù)據(jù)會(huì)被傳到 log.txt 里面。
重定向模擬實(shí)現(xiàn)
在我們自己實(shí)現(xiàn) shell 的基礎(chǔ)上,是可以自己實(shí)現(xiàn)重定向功能的。對于獲取到的命令進(jìn)行判斷,若命令當(dāng)中包含重定向符號 >、>> 或是 <,則該命令需要進(jìn)行處理
設(shè)置 type 變量,type 為 0 表示命令當(dāng)中為輸出重定向,type 為 1 表示追加重定向,type為 2 表示輸入重定向。若 type 值為 0 或者 1,則使用 dup2 接口實(shí)現(xiàn)目標(biāo)文件與標(biāo)準(zhǔn)輸出流的重定向;若 type 值為 2,則使用 dup2 接口實(shí)現(xiàn)目標(biāo)文件與標(biāo)準(zhǔn)輸入流的重定向
#include
#include
#include
#include
#include
#include
#include
#include
#define LEN 1024 //命令最大長度
#define NUM 32 //命令拆分后的最大個(gè)數(shù)
int main()
{
int type = 0; //0 >, 1 >>, 2 <
char cmd[LEN]; //存儲(chǔ)命令
char* myargv[NUM]; //存儲(chǔ)命令拆分后的結(jié)果
char hostname[32]; //主機(jī)名
char pwd[128]; //當(dāng)前目錄
while (1){
//獲取命令提示信息
struct passwd* pass = getpwuid(getuid());
gethostname(hostname, sizeof(hostname)-1);
getcwd(pwd, sizeof(pwd)-1);
int len = strlen(pwd);
char* p = pwd + len - 1;
while (*p != '/'){
p--;
}
p++;
//打印命令提示信息
printf("[%s@%s %s]$ ", pass->pw_name, hostname, p);
//讀取命令
fgets(cmd, LEN, stdin);
cmd[strlen(cmd) - 1] = '?';
//實(shí)現(xiàn)重定向功能
char* start = cmd;
while (*start != '?'){
if (*start == '>'){
type = 0; //遇到一個(gè)'>',輸出重定向
*start = '?';
start++;
if (*start == '>'){
type = 1; //遇到第二個(gè)'>',追加重定向
start++;
}
break;
}
if (*start == '<'){
type = 2; //遇到'<',輸入重定向
*start = '?';
start++;
break;
}
start++;
}
if (*start != '?'){ //start位置不為'?',說明命令包含重定向內(nèi)容
while (isspace(*start)) //跳過重定向符號后面的空格
start++;
}
else{
start = NULL; //start設(shè)置為NULL,標(biāo)識(shí)命令當(dāng)中不含重定向內(nèi)容
}
//拆分命令
myargv[0] = strtok(cmd, " ");
int i = 1;
while (myargv[i] = strtok(NULL, " ")){
i++;
}
pid_t id = fork(); //創(chuàng)建子進(jìn)程執(zhí)行命令
if (id == 0){
//child
if (start != NULL){
if (type == 0){ //輸出重定向
int fd = open(start, O_WRONLY | O_CREAT | O_TRUNC, 0664); //以寫的方式打開文件(清空原文件內(nèi)容)
if (fd < 0){
error("open");
exit(2);
}
close(1);
dup2(fd, 1); //重定向
}
else if (type == 1){ //追加重定向
int fd = open(start, O_WRONLY | O_APPEND | O_CREAT, 0664); //以追加的方式打開文件
if (fd < 0){
perror("open");
exit(2);
}
close(1);
dup2(fd, 1); //重定向
}
else{ //輸入重定向
int fd = open(start, O_RDONLY); //以讀的方式打開文件
if (fd < 0){
perror("open");
exit(2);
}
close(0);
dup2(fd, 0); //重定向
}
}
execvp(myargv[0], myargv); //child進(jìn)行程序替換
exit(1); //替換失敗的退出碼設(shè)置為1
}
//shell
int status = 0;
pid_t ret = waitpid(id, &status, 0); //shell等待child退出
if (ret > 0){
printf("exit code:%dn", WEXITSTATUS(status)); //打印child的退出碼
}
}
return 0;
}
效果如圖:
FILE 的文件描述符
訪問文件的本質(zhì)都是通過文件描述符進(jìn)行訪問的,而且?guī)旌瘮?shù)又是對系統(tǒng)接口的封裝,所以在庫函數(shù)中的 FILE 結(jié)構(gòu)體也必定存在文件描述符 fd
我們在 / u s r / i n c l u d e / s t d i o . h color{red} {/usr/include/stdio.h}/usr/include/stdio.h 頭文件中可以看到下面這句代碼,也就是說 FILE 實(shí)際上就是struct _IO_FILE 結(jié)構(gòu)體的一個(gè)別名。
接下來轉(zhuǎn)到 struct _IO_FILE 結(jié)構(gòu)體的定義,其中我們可以看到一個(gè)名為_fileno的成員,這個(gè)成員實(shí)際上就是封裝的文件描述符
int _flags; /* High-order word is _IO_MAGIC; rest is flags. */
#define _IO_file_flags _flags
//緩沖區(qū)相關(guān) /* 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 0 int _blksize;
#else int _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 };
那我們再來聊聊文件函數(shù)的底層:
fopen 函數(shù)會(huì)為用戶在上層調(diào)申請 FILE 結(jié)構(gòu)體,返回 FILE* 結(jié)構(gòu)體指針,底層上調(diào)用 open 函數(shù)獲取文件的 fd,并將 fd 交給 _fileno 填充,這樣就完成了文件的打開操作。其他的比如 fread、fwrite、fputs、fgets ,都會(huì)先根據(jù)我們傳入的文件指針找到對應(yīng)的FILE結(jié)構(gòu)體,然后在FILE結(jié)構(gòu)體當(dāng)中找到文件描述符,最后通過文件描述符對文件進(jìn)行的一系列操作
我們以三種輸出函數(shù)為例:
#include
int main()
{
//c
printf("hello printfn");
fputs("hello fputsn", stdout);
//system
write(1, "hello writen", 12);
fork();
return 0;
}
看到這結(jié)果是不是覺得淦!好怪。按照代碼邏輯的話,這里應(yīng)該只會(huì)打印出三個(gè)句子對應(yīng)三個(gè)函數(shù),但是為什么這里有兩個(gè)函數(shù)出現(xiàn)了兩次呢?
不難發(fā)現(xiàn),這里重復(fù)的兩個(gè)函數(shù)都是 C 庫函數(shù),我們就要牽扯到三種緩沖方式了:
無緩沖
行緩沖(對顯示器進(jìn)行刷新數(shù)據(jù))
全緩沖(對磁盤文件寫入數(shù)據(jù))
直接執(zhí)行可執(zhí)行程序,將數(shù)據(jù)打印到顯示器時(shí)所采用的就是行緩沖,因?yàn)榇a當(dāng)中每句話后面都有 n,所以當(dāng)我們執(zhí)行完對應(yīng)代碼后就立即將數(shù)據(jù)刷新到了顯示器上
如果將運(yùn)行結(jié)果重定向到 log.txt 文件時(shí),數(shù)據(jù)的刷新策略就變?yōu)榱巳彌_,此時(shí)使用 printf 和 fputs 打印的數(shù)據(jù)都打印到了C語言自帶的緩沖區(qū)當(dāng)中,之后 fork 創(chuàng)建子進(jìn)程時(shí),由于進(jìn)程間具有獨(dú)立性,而之后當(dāng)父進(jìn)程或是子進(jìn)程對要刷新緩沖區(qū)內(nèi)容時(shí),本質(zhì)就是對父子進(jìn)程共享的數(shù)據(jù)進(jìn)行了修改,此時(shí)就需要對數(shù)據(jù)進(jìn)行寫時(shí)拷貝,至此緩沖區(qū)當(dāng)中的數(shù)據(jù)就變成了兩份,一份父進(jìn)程的,一份子進(jìn)程的,所以重定向到 log.txt 文件當(dāng)中 printf 和 puts 函數(shù)打印的數(shù)據(jù)就有兩份。但由于 write 是系統(tǒng)接口,我們可以將 write 看作是沒有緩沖區(qū)的,因此 write 打印的數(shù)據(jù)就只打印了一份
這個(gè)緩沖區(qū)是誰提供的?
他是 C 自帶的,如果說這個(gè)緩沖區(qū)是操作系統(tǒng)提供的,那么 printf、fputs 和 write 打印的數(shù)據(jù)重定向到文件后都應(yīng)該打印兩次
這個(gè)緩沖區(qū)在哪?
printf 是將數(shù)據(jù)打印到 stdout 里面,而 stdout 就是一個(gè) FILE* 指針,在 FILE 結(jié)構(gòu)體當(dāng)中還有一大部分成員是用于記錄緩沖區(qū)相關(guān)的信息的,我們來看看底層代碼:
/* 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. */
抗疫知道這里緩沖區(qū)是由 C 提供,在 FILE 結(jié)構(gòu)體當(dāng)中進(jìn)行維護(hù),F(xiàn)ILE 結(jié)構(gòu)體當(dāng)中不僅保存了對應(yīng)文件的文件描述符還保存了用戶緩沖區(qū)的相關(guān)信息
操作系統(tǒng)有緩沖區(qū)嗎?
答案是一定的,其實(shí)我們數(shù)據(jù)并不是直接刷新到顯示器和磁盤上的,而是先刷新到操作系統(tǒng)的緩沖區(qū)里面,再經(jīng)過緩沖區(qū)加載到顯示器和磁盤上,當(dāng)然這里我們先不關(guān)心操作系統(tǒng)的刷新策略。
因?yàn)椴僮飨到y(tǒng)是進(jìn)行軟硬件資源管理的軟件,所以要將數(shù)據(jù)刷新到具體外設(shè)硬件上,就必須要經(jīng)過操作系統(tǒng),看一下層狀結(jié)構(gòu)圖也許會(huì)更清楚:
inode
磁盤文件由兩部分構(gòu)成,分別是文件內(nèi)容和文件屬性。比如文件名、文件大小以及文件創(chuàng)建時(shí)間等信息都是文件屬性,文件屬性又被稱為元信息
在命令行當(dāng)中輸入ls -l,即可顯示當(dāng)前目錄下各文件的屬性信息,各種文件屬性排列如下:
在 Linux 操作系統(tǒng)中,文件的元信息和內(nèi)容是分離存儲(chǔ)的,其中保存元信息的結(jié)構(gòu)稱之為 i n o d e color{red} {其中保存元信息的結(jié)構(gòu)稱之為 inode}其中保存元信息的結(jié)構(gòu)稱之為inode,因?yàn)橄到y(tǒng)當(dāng)中可能存在大量的文件,所以我們需要給每個(gè)文件的屬性搞一個(gè)唯一的編號,即 inode 號
也就是說,inode 是一個(gè)文件的屬性集合,Linux 中幾乎每個(gè)文件都有一個(gè) inode,為了區(qū)分系統(tǒng)當(dāng)中大量的 inode,我們?yōu)槊總€(gè) inode 設(shè)置了 inode 編號,ls -i 命令即可查看當(dāng)前目錄下的文件和他的 inode 編號:
無論是文件內(nèi)容還是文件屬性,他們都是存儲(chǔ)在磁盤里面的
磁盤
盤是一種永久性存儲(chǔ)介質(zhì),在計(jì)算機(jī)中,磁盤幾乎是唯一的機(jī)械設(shè)備。與磁盤相對應(yīng)的就是內(nèi)存,內(nèi)存是掉電易失存儲(chǔ)介質(zhì),目前所有的普通文件都是在磁盤中存儲(chǔ)的,磁盤在馮諾依曼體系結(jié)構(gòu)當(dāng)中既可以充當(dāng)輸入設(shè)備,又可以充當(dāng)輸出設(shè)備:
尋址方案
對磁盤讀寫時(shí),一般有以下 3 個(gè)步驟:
確定讀寫信息的盤面
確定讀寫信息的柱面
確定讀寫信息的扇區(qū)
分區(qū)與存儲(chǔ)介質(zhì)
要理解文件系統(tǒng),我們必須要先將磁盤結(jié)構(gòu)理解為線性的存儲(chǔ)介質(zhì),比如說小學(xué)英語的磁帶,你扯出磁帶條條時(shí)有沒有想過復(fù)讀機(jī)讀取磁帶的信息,放完重來必要做倒帶的操作才能從頭開始,這就非常貼切線性結(jié)構(gòu)了。
磁盤分區(qū)
磁盤也被稱為塊設(shè)備,以扇區(qū)為單位,一個(gè)扇區(qū)的大小通常為512字節(jié)。如果以大小為 512G 的磁盤為例,該磁盤就可被分為十億多個(gè)扇區(qū):
計(jì)為了更好的管理磁盤,磁盤進(jìn)行了分區(qū),原理類似于將整個(gè)國家劃分為省市區(qū)縣進(jìn)行管理,使用分區(qū)編輯器在磁盤上劃分幾個(gè)邏輯部分,盤片一旦劃分成數(shù)個(gè)分區(qū),不同的目錄與文件就可以存儲(chǔ)進(jìn)不同的分區(qū),分區(qū)越多,文件的性質(zhì)區(qū)分越細(xì),Windows 磁盤就被分為 C 盤和 D 盤
Linux 也是可以查看文件的分區(qū)信息:
格式化
磁盤分區(qū)完成后就會(huì)進(jìn)行格式化,格式化后每個(gè)分區(qū) inode 數(shù)就會(huì)被確定下來,所以說格式化是對分區(qū)進(jìn)行初始化的一種操作,會(huì)導(dǎo)致所有資源被清除,本質(zhì)上是對分區(qū)后各個(gè)區(qū)域?qū)懭牍芾硇畔?/p>
其中的管理信息內(nèi)容是由文件系統(tǒng)決定的,不同的文件系統(tǒng)格式化時(shí)管理信息是不同的,常見的文件系統(tǒng)有 EXT2、EXT3、XFS、NTFS 等
EXT2 存儲(chǔ)方案
而對于每一個(gè)分區(qū)來說,分區(qū)的頭部有一個(gè)啟動(dòng)塊(Boot Block),對于該分區(qū)的其余區(qū)域,EXT2 文件系統(tǒng)會(huì)根據(jù)分區(qū)大小劃分為一個(gè)個(gè)的塊組(Block Group)
啟動(dòng)塊的大小是確定的,而塊組的大小是由格式化的時(shí)候確定的,并且不可以更改。
其次,每個(gè)組塊都有著相同的組成結(jié)構(gòu),每個(gè)組塊都由超級塊(Super Block)、塊組描述符表(Group Descriptor Table)、塊位圖(Block Bitmap)、inode位圖(inode Bitmap)、inode表(inode Table)以及數(shù)據(jù)表(Data Block)組成:
Super Block: 存放文件系統(tǒng)本身的結(jié)構(gòu)信息。主要有:Data Block和inode的總量、未使用的Data Block和inode的數(shù)量、一個(gè)Data Block和inode的大小、最近一次掛載時(shí)間。Super Block的信息被破壞,可以說整個(gè)文件系統(tǒng)結(jié)構(gòu)就被破壞了
Group Descriptor Table: 塊組描述符表,描述該分區(qū)當(dāng)中塊組的屬性信息
Block Bitmap: 塊位圖中記錄著 Data Block 中哪個(gè)數(shù)據(jù)塊已經(jīng)被占用或沒有被占用
inode Bitmap: inode 位圖中記錄著每個(gè)inode 是否空閑可用
inode Table: 文件屬性,即每個(gè)文件的inode。
Data Blocks: 文件內(nèi)容
因?yàn)?super block 極為重要,所以一般在其他塊組中會(huì)存在冗余,方便損壞后拷貝恢復(fù)
此時(shí)我們就可以理解文件創(chuàng)建了:
- 先通過遍歷 inode 位圖找到一個(gè)空閑的 inode
- 再在 inode 表當(dāng)中找到對應(yīng)的 inode,并將文件的屬性信息填充進(jìn) inode 結(jié)構(gòu)中。
- 將該文件的文件名和inode指針添加到目錄文件的數(shù)據(jù)塊中
文件寫入也是同理:
- 通過 inode 編號找到對應(yīng)的 inode 結(jié)構(gòu)
- 通過 inode 結(jié)構(gòu)找到存儲(chǔ)該文件內(nèi)容的數(shù)據(jù)塊,并將數(shù)據(jù)寫入數(shù)據(jù)塊
- 若不存在數(shù)據(jù)塊或申請的數(shù)據(jù)塊已被寫滿,則通過遍歷塊位圖的方式找到一個(gè)空閑的塊號,并在數(shù)據(jù)區(qū)當(dāng)中找到對應(yīng)的空閑塊,再將數(shù)據(jù)寫入數(shù)據(jù)塊,最后還需要建立數(shù)據(jù)塊和 inode 結(jié)構(gòu)的對應(yīng)關(guān)系(對應(yīng)關(guān)系是通過數(shù)組進(jìn)行維護(hù)的,該數(shù)組一般可以存儲(chǔ) 15 個(gè)元素,其中前 12 個(gè)元素分別對應(yīng)文件使用的 12 個(gè)數(shù)據(jù)塊,剩余的三個(gè)元素分別是一級索引、二級索引和三級索引,當(dāng)該文件使用數(shù)據(jù)塊的個(gè)數(shù)超過12個(gè)時(shí),可以用這三個(gè)索引進(jìn)行數(shù)據(jù)塊擴(kuò)充)
文件刪除也是同理:
其實(shí)刪除并不會(huì)真正將文件信息刪除,而只是將其 inode 和數(shù)據(jù)塊號置為無效,所以刪除文件后短時(shí)間內(nèi)是可以恢復(fù)的。
短時(shí)間內(nèi)是個(gè)什么意思呢,因?yàn)槲募?yīng)的 inode 號和數(shù)據(jù)塊號被置為了無效,后續(xù)創(chuàng)建其他文件或是對其他文件進(jìn)行寫入操作申請 inode 號和數(shù)據(jù)塊號時(shí),可能會(huì)將該無效了的 inode號和數(shù)據(jù)塊號分配出去,此時(shí)刪除文件的數(shù)據(jù)就會(huì)被覆蓋,也就無法恢復(fù)文件了
這也就就是了為什么拷貝文件的時(shí)候很慢,而刪除文件的時(shí)候很快
文件目錄也是同理:
Linux下一切皆文件,目錄當(dāng)然也會(huì)被看作為文件。目錄有自己的屬性信息,他 inode 結(jié)構(gòu)中存儲(chǔ)的是目錄的屬性信息,比如目錄的大小、目錄的擁有者等;目錄的數(shù)據(jù)塊當(dāng)中存儲(chǔ)的就是該目錄下的文件名以及對應(yīng)文件的 inode 指針。
注意: 文件名并沒有存儲(chǔ)在自己的 inode 結(jié)構(gòu)當(dāng)中,而是存儲(chǔ)在該文件所處目錄文件的文件內(nèi)容當(dāng)中。因?yàn)橄到y(tǒng)并不關(guān)心文件名,他只關(guān)心文件的 inode ,而文件名和 inode 指針存儲(chǔ)在其目錄文件的文件內(nèi)容當(dāng)中后,目錄通過文件名和文件的 inode 指針即可將文件名和文件內(nèi)容及其屬性連接起來
軟鏈接
文件軟鏈接的創(chuàng)建可以通過這個(gè)命令:
效果如下:
我們可以通過ls -i可以看到軟鏈接的 inode 與源文件的 inode 是不同的,并且軟鏈接的大小比源文件的大小要小得多!
刪除源文件后軟鏈接文件不能獨(dú)立存在,雖然仍保留文件名,但卻不能執(zhí)行或是查看軟鏈接的內(nèi)容
硬鏈接
文件硬鏈接的創(chuàng)建可以通過這個(gè)命令:
效果如下:
我們可以通過ls -i可以看到硬鏈接的 inode 與源文件的 inode 是相同的,并且硬鏈接文件的大小與源文件的大小也是相同的,特別注意的是,當(dāng)創(chuàng)建了一個(gè)硬鏈接文件后,該硬鏈接文件和源文件的硬鏈接數(shù)都變成了 2
所以硬鏈接文件就是源文件的一個(gè)別名 color{red} {源文件的一個(gè)別名}源文件的一個(gè)別名,一個(gè)文件有幾個(gè)文件名,該文件的硬鏈接數(shù)就是幾,這里 inode 為 659031 的文件有 myproc 和 myproc-h 兩個(gè)文件名,因此該文件的硬鏈接數(shù)為 2
與軟連接不同的是,當(dāng)硬鏈接的源文件被刪除后,硬鏈接文件仍能正常執(zhí)行,只是文件的鏈接數(shù)減少了一個(gè),但是硬鏈接可以同步修改多個(gè)不在或者同在一個(gè)目錄下的文件名,其中一個(gè)修改后,所有與其有硬鏈接的文件都會(huì)一起被修改
-
接口
+關(guān)注
關(guān)注
33文章
8961瀏覽量
153260 -
Linux
+關(guān)注
關(guān)注
87文章
11469瀏覽量
212889 -
程序
+關(guān)注
關(guān)注
117文章
3824瀏覽量
82491 -
文件系統(tǒng)
+關(guān)注
關(guān)注
0文章
294瀏覽量
20302 -
函數(shù)
+關(guān)注
關(guān)注
3文章
4372瀏覽量
64285
發(fā)布評論請先 登錄
Linux系統(tǒng)中網(wǎng)絡(luò)I/O性能改進(jìn)方法的研究
Java I/O 的相關(guān)方法分析

Linux 系統(tǒng)應(yīng)用編程之標(biāo)準(zhǔn)I/O詳解
學(xué)會(huì)處理Linux內(nèi)核訪問外設(shè)I/O資源的方式
如何更改 Linux 的 I/O 調(diào)度器

Linux I/O多路復(fù)用
Linux中如何使用信號驅(qū)動(dòng)式I/O?

深入理解Linux傳統(tǒng)的System Call I/O

Linux磁盤I/O的性能指標(biāo)和查看性能工具
Linux I/O重定向詳解
深入理解 Linux 的 I/O 系統(tǒng)

評論