一、明確基本共識(shí)
-
文件等于內(nèi)容加屬性,內(nèi)容和和屬性都是數(shù)據(jù),不管是內(nèi)容還是屬性都要在磁盤中保存。
-
文件分為打開(kāi)的文件和沒(méi)打開(kāi)的文件。
-
打開(kāi)的文件本質(zhì)是進(jìn)程打開(kāi)的,要研究打開(kāi)的文件,本質(zhì)是研究進(jìn)程和文件的關(guān)系。
-
對(duì)文件的所有操作(打開(kāi)文件、讀取文件、向文件寫入)等,都是通過(guò)代碼來(lái)實(shí)現(xiàn)的,而代碼最終是由 CPU 去執(zhí)行的,根據(jù)馮諾依曼結(jié)構(gòu)體系,CPU 不能直接和外設(shè)打交道,因此一個(gè)被打開(kāi)的文件,第一步一定是先將其加載到內(nèi)存。
-
一個(gè)進(jìn)程能夠打開(kāi)多個(gè)文件,所以在操作系統(tǒng)內(nèi)部一定存在大量的被打開(kāi)的文件,操作系統(tǒng)還是通過(guò)先描述,再組織的方式對(duì)打開(kāi)的文件進(jìn)行管理。每個(gè)被打開(kāi)的文件都必須有自己的文件打開(kāi)對(duì)象,其中一一定包含了文件的很多屬性,將這些文件對(duì)象以某種特殊的數(shù)據(jù)結(jié)構(gòu)組織起來(lái),最終對(duì)文件的管理,就變成了對(duì)某種數(shù)據(jù)結(jié)構(gòu)的維護(hù)(增刪查改)。
-
沒(méi)打開(kāi)的文件一般都是在磁盤上放著,對(duì)于沒(méi)打開(kāi)的文件,由于沒(méi)打開(kāi)的文件非常多,所以對(duì)于沒(méi)打開(kāi)的文件我們最關(guān)心文件如何被分門別類的放置好,分門別類的放置好是為了快速的進(jìn)行增刪查改。
二、C語(yǔ)言文件接口回顧
2.1 文件的打開(kāi)操作
// 文件打開(kāi)接口
FILE *fopen(const char *path, const char *mode);
第一個(gè)參數(shù) path
,表示要打開(kāi)的文件路徑,或者文件名。如果只有文件名前面沒(méi)寫路徑,表示打開(kāi)當(dāng)前路徑下的文件。這里又涉及到當(dāng)前路徑,在前一篇文章中實(shí)現(xiàn) cd
指令的時(shí)候就講過(guò)什么是當(dāng)前路徑??偟膩?lái)說(shuō),當(dāng)前工作路徑是一個(gè)進(jìn)程 PCB
中維護(hù)的一個(gè)屬性。一個(gè)可執(zhí)行程序在被加載到內(nèi)存成為進(jìn)程創(chuàng)建出對(duì)應(yīng)的 PCB
對(duì)象的時(shí)候,PCB
對(duì)象中就維護(hù)了一個(gè)叫做 cwd
的屬性,該屬性就表示進(jìn)程當(dāng)前的工作路徑。
如果 fopen
函數(shù)的第一個(gè)參數(shù)只傳遞了文件名,最終在打開(kāi)文件的時(shí)候,操作系統(tǒng)會(huì)去 cwd
指向的工作路徑下查找該文件。
第二個(gè)參數(shù) mode
,這個(gè)參數(shù)有很多可選項(xiàng),今天只介紹個(gè)別選項(xiàng),關(guān)于所有選項(xiàng)的詳細(xì)介紹請(qǐng)看我之前的文章【C語(yǔ)言進(jìn)階】文件操作。
-
w選項(xiàng):只要是以
w
選項(xiàng)打開(kāi)的文件,在寫入之前都會(huì)對(duì)文件做清空處理,然后從頭開(kāi)始寫入。 -
a選項(xiàng):在文件結(jié)尾進(jìn)行追加寫。
小Tips:我們之前介紹的重定向,>
本質(zhì)上就對(duì)應(yīng)使用的是 w
選項(xiàng),>>
本質(zhì)上就對(duì)應(yīng)使用的是 a
選項(xiàng)。
2.2 文件的讀取寫入操作
和文件讀取寫入的相關(guān)接口,以及使用方法,今天也不過(guò)多介紹,詳細(xì)介紹請(qǐng)看我之前寫的文章【C語(yǔ)言進(jìn)階】文件操作。今天只想通過(guò) fwrite
接口跟大家明確一件事情。
// fwrite 接口聲明
size_t fwrite(const void *ptr, size_t size, size_t nmemb, FILE *stream);
int main()
{
FILE *fp = fopen("log.txt", "w");
if (fp == NULL)
{
// 打開(kāi)失敗
perror("fopen");
return errno;
}
// 打開(kāi)成功,對(duì)文件進(jìn)行相關(guān)的操作
// ...
char* str = "Hello Linux!";
fwrite(str, strlen(str), 1, fp);
// 操作結(jié)束,關(guān)閉文件
fclose(fp);
return 0;
}
fwrite
接口的第二個(gè)參數(shù) size
表示每一個(gè)要寫入的對(duì)象的大小。在向文件寫入字符串的時(shí)候,該參數(shù)是字符串的長(zhǎng)度還是字符串的長(zhǎng)度加一呢?因?yàn)?strlen
計(jì)算出來(lái)的字符串長(zhǎng)度是不包含結(jié)尾的 \0
,加一的小伙伴覺(jué)得要把 \0
也寫到文件里面,但是 \0
真的需要寫入文件嘛?其實(shí) \0
并不需要寫入文件中,因?yàn)樽址?\0
結(jié)尾只是 C 語(yǔ)言這么規(guī)定的,我們把一個(gè)字符串寫入文件后,可能通過(guò)其它的語(yǔ)言去讀取該文件,我們并不希望讀到與該字符串無(wú)關(guān)的內(nèi)容 \0
。下面是加一的結(jié)果:
\0
也是字符,只不過(guò)不可顯,在被寫入到文件后,vim
編輯器會(huì)把它識(shí)別成 ^@
,對(duì) Hello Linux
來(lái)說(shuō),^@
就是多余的無(wú)用字符。我們不希望它在文件中出現(xiàn)。
2.3 三個(gè)標(biāo)準(zhǔn)輸入輸出流
C程序在啟動(dòng)時(shí)候,默認(rèn)會(huì)打開(kāi)三個(gè)標(biāo)準(zhǔn)流文件:
-
stdin:標(biāo)準(zhǔn)輸入流——鍵盤文件
-
stdout:標(biāo)準(zhǔn)輸出流——顯示器文件
-
stderr:標(biāo)準(zhǔn)錯(cuò)誤流——顯示器文件
三、文件有關(guān)的系統(tǒng)調(diào)用
文件最初是在磁盤上的,磁盤是外部設(shè)備,訪問(wèn)磁盤文件其實(shí)是訪問(wèn)硬件,在計(jì)算機(jī)層狀結(jié)構(gòu)中,硬件是處于最底層的,操作系統(tǒng)幫我們把這些硬件管理起來(lái),并且操作系統(tǒng)是不相信用戶的,因此操作系統(tǒng)不允許我們直接去訪問(wèn)硬件,而是給我們提供了系統(tǒng)調(diào)用接口,幾乎所有的庫(kù)只要是訪問(wèn)硬件設(shè)備,必定要封裝系統(tǒng)調(diào)用。也就是說(shuō)我們平時(shí)在C語(yǔ)言里面使用的 fopen
、printf
、fprintf
、fscanf
等函數(shù)都一定是封裝了系統(tǒng)調(diào)用。
3.1 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);
-
第一參數(shù)
pathname
:表示要打開(kāi)或創(chuàng)建的目標(biāo)文件 -
第二個(gè)參數(shù)
flags
:標(biāo)志位選項(xiàng)。O_RDPPNLY
:只讀打開(kāi);O_WRONLY
:只寫打開(kāi);O_RDWR
:讀,寫打開(kāi)。這三個(gè)常量,必須指定一個(gè)且只能指定一個(gè)。O_CREAT
:若文件不存在,則創(chuàng)建它。需要使用mode
參數(shù),來(lái)指明新文件的訪問(wèn)權(quán)限。O_APPEND
:追加寫;O_TRUNC
:文件打開(kāi)的時(shí)候先清空。 -
第三個(gè)參數(shù)
mode
:新創(chuàng)建文件的默認(rèn)權(quán)限,要考慮權(quán)限掩碼,可以配合umask
系統(tǒng)調(diào)用接口來(lái)設(shè)置自己想要的效果。umask
系統(tǒng)調(diào)用產(chǎn)生的效果就只對(duì)當(dāng)前進(jìn)程創(chuàng)建的文件有關(guān)。 -
返回值:成功,返回新打開(kāi)的文件描述符,關(guān)于文件描述符是什么,將在后文為大家介紹;失敗,返回-1。
小Tips:open
函數(shù)具體使用哪個(gè),和具體的應(yīng)用場(chǎng)景有關(guān),如目標(biāo)文件不存在,需要 open
創(chuàng)建,則第三個(gè)參數(shù)表示創(chuàng)建文件的默認(rèn)權(quán)限。如果不需要?jiǎng)?chuàng)建新文件,使用兩個(gè)參數(shù)的 open
。
3.1.1 比特位級(jí)別的標(biāo)志位傳遞方式
#define ONE (1<<0) // 1
#define TWO (1<<1) // 2
#define FOU (1<<2) // 4
#define EIG (1<<3) // 8
void show(int flags)
{
if(flags & ONE) printf("function1\n");
if(flags & TWO) printf("function2\n");
if(flags & FOU) printf("function3\n");
if(flags & EIG) printf("function4\n");
return;
}
int main()
{
printf("--------------------------------------\n");
show(ONE);
printf("--------------------------------------\n");
show(ONE | TWO);
printf("--------------------------------------\n");
show(ONE | TWO | FOU );
printf("--------------------------------------\n");
show(ONE | TWO | FOU | EIG);
printf("--------------------------------------\n");
return 0;
}
小Tips:這種比特位級(jí)別的標(biāo)志位傳遞方式,使用戶可以在函數(shù)調(diào)用的時(shí)候采用按位或的方式傳遞多個(gè)選項(xiàng)實(shí)現(xiàn)不同的功能。open
函數(shù)的第二個(gè)參數(shù)就是采用這種方式就是這樣。
3.2 write
#include <unistd.h>
ssize_t write(int fd, const void *buf, size_t count);
-
第一個(gè)參數(shù)
fd
:表示待寫入文件的文件描述符。 -
第二個(gè)參數(shù)
buf
:指向待寫入的文件內(nèi)容。 -
第三個(gè)參數(shù)
count
:待寫入內(nèi)容的大小,單位是字節(jié)。 -
返回值:實(shí)際上寫入的字節(jié)數(shù)。
3.2.1 模擬實(shí)現(xiàn) w 選項(xiàng)
int main()
{
umask(0); // 將權(quán)限掩碼設(shè)置成全0
int fd = open("log.txt", O_WRONLY | O_CREAT | O_TRUNC, 0666); // 以讀的方式打開(kāi),若文件不存在就創(chuàng)建,打開(kāi)文件時(shí)清空
if(fd < 0)
{
printf("open file\n");
return errno;
}
const char* str = "aaa";
ssize_t ret = write(fd, str, strlen(str));
close(fd);
return 0;
}
3.2.2 模擬實(shí)現(xiàn) a 選項(xiàng)
int main()
{
umask(0); // 將權(quán)限掩碼設(shè)置成全0
int fd = open("log.txt", O_WRONLY | O_CREAT | O_APPEND, 0666); // 以讀的方式打開(kāi),若文件不存在就創(chuàng)建,以追加的方式進(jìn)行寫入
if(fd < 0)
{
printf("open file\n");
return errno;
}
const char* str = "aaa";
ssize_t ret = write(fd, str, strlen(str));
close(fd);
return 0;
}
3.3 read
#include <unistd.h>
ssize_t read(int fd, void *buf, size_t count);
-
第一個(gè)參數(shù)
fd
:要讀取文件的文件描述符。 -
第二個(gè)參數(shù)
buf
:指向一段空間,該空間用來(lái)存儲(chǔ)讀取到的內(nèi)容。 -
第三個(gè)參數(shù)
count
:參數(shù)二指向空間的大小。
四、訪問(wèn)文件的本質(zhì)
總結(jié):一個(gè)被打開(kāi)的文件,加載到內(nèi)存,會(huì)為該文件創(chuàng)建一個(gè) struct file
結(jié)構(gòu)體對(duì)象,操作系統(tǒng)對(duì)文件的管理本質(zhì)上就是對(duì) struct file
結(jié)構(gòu)體對(duì)象的管理,操作系統(tǒng)會(huì)將當(dāng)前所有被打開(kāi)文件的 struct file
對(duì)象以雙鏈表的形式組織起來(lái)。進(jìn)程的 PCB 對(duì)象中有一個(gè) struct files_struct
類型的指針,指向該類型的一個(gè)對(duì)象,該類型對(duì)象里面記錄了當(dāng)前進(jìn)程所打開(kāi)的所有文件新信息,其中中維護(hù)了一個(gè) struct file*
類型的數(shù)組,數(shù)組的內(nèi)容就指向了當(dāng)前進(jìn)程所打開(kāi)的文件結(jié)構(gòu)體對(duì)象,簡(jiǎn)言之就是指向了當(dāng)前進(jìn)程打開(kāi)的文件。我們將這個(gè)數(shù)組就叫做文件描述符表,數(shù)組的下標(biāo)就叫做文件描述符(因此文件描述符一定大于0)。open
函數(shù)的返回值其實(shí)就是文件描述符,即只要當(dāng)前進(jìn)程打開(kāi)一個(gè)新文件,操作系統(tǒng)就會(huì)按照從前往后的順序從該進(jìn)程的文件描述符表中分配一個(gè)數(shù)組下標(biāo),該下標(biāo)對(duì)應(yīng)的內(nèi)存空間中存儲(chǔ)的就是該文件結(jié)構(gòu)的地址。此后要對(duì)該文件進(jìn)行任何操作,只需要知道它對(duì)應(yīng)的數(shù)組下標(biāo)即可。
int main()
{
umask(0); // 將權(quán)限掩碼設(shè)置成全0
int fd1 = open("log1.txt", O_WRONLY | O_CREAT | O_APPEND, 0666); // 以讀的方式打開(kāi),若文件不存在就創(chuàng)建,以追加的方式進(jìn)行寫入
int fd2 = open("log2.txt", O_WRONLY | O_CREAT | O_APPEND, 0666);
int fd3 = open("log3.txt", O_WRONLY | O_CREAT | O_APPEND, 0666);
int fd4 = open("log4.txt", O_WRONLY | O_CREAT | O_APPEND, 0666);
printf("fd1: %d\n", fd1);
printf("fd2: %d\n", fd2);
printf("fd3: %d\n", fd3);
printf("fd4: %d\n", fd4);
return 0;
}
小Tips:通過(guò)結(jié)果可以看出,進(jìn)程新打開(kāi)的文件,其下標(biāo)只能從3,開(kāi)始,這是因?yàn)?C 程序在運(yùn)行起來(lái)的時(shí)候操作系統(tǒng)會(huì)默認(rèn)幫我們打開(kāi)三個(gè)流,標(biāo)準(zhǔn)輸入流 stdin
對(duì)應(yīng)鍵盤文件,下標(biāo)為0;標(biāo)準(zhǔn)輸出流 stdout
對(duì)應(yīng)顯示器文件,下標(biāo)為1;標(biāo)準(zhǔn)錯(cuò)誤流 stderr
對(duì)應(yīng)顯示器文件,下標(biāo)為2。從這里可以的出一個(gè)結(jié)論,默認(rèn)打開(kāi)三個(gè)標(biāo)準(zhǔn)輸入輸出流并不是 C 語(yǔ)言的特性,而是操作系統(tǒng)的特性,所有語(yǔ)言編寫的程序運(yùn)行起來(lái)后都會(huì)打開(kāi)。操作系統(tǒng)為什么要幫我們打開(kāi)呢?因?yàn)殡娔X在開(kāi)機(jī)的時(shí)候,鍵盤和顯示器就已經(jīng)被打開(kāi)了,我們?cè)诰幊痰臅r(shí)候,一般都會(huì)用鍵盤輸入和通過(guò)顯示器查看結(jié)果。
文件描述符對(duì)應(yīng)的分配規(guī)則:從0下標(biāo)開(kāi)始,尋找最小的沒(méi)有使用的數(shù)組位置,它的下標(biāo)就是新打開(kāi)文件的文件描述符。
4.1 再來(lái)認(rèn)識(shí) FILE
FILE
是 C 語(yǔ)言庫(kù)中自己封裝的一個(gè)結(jié)構(gòu)體,在 C 語(yǔ)言中,通過(guò) FILE
對(duì)象去描述文件??梢源_定,F(xiàn)ILE 中一定封裝了文件描述符。如下面代碼,FILE
中的 _fileno
屬性就是文件描述符。
int main()
{
printf("stdin->fd: %d\n", stdin->_fileno); // 標(biāo)準(zhǔn)輸入
printf("stdout->fd: %d\n", stdout->_fileno); //標(biāo)準(zhǔn)輸出
printf("stderr->fd: %d\n", stderr->_fileno); // 標(biāo)準(zhǔn)錯(cuò)誤
return 0;
}
4.2 再來(lái)理解關(guān)閉文件
一個(gè)文件可以被多個(gè)進(jìn)程同時(shí)打開(kāi),最常見(jiàn)的比如鍵盤文件,顯示器文件。在 struct file
對(duì)象中有一個(gè) f_count
字段,叫做當(dāng)前文件的引用計(jì)數(shù),記錄了當(dāng)前文件被多少個(gè)進(jìn)程打開(kāi)了,在進(jìn)程視角關(guān)閉文件就是調(diào)用 close
系統(tǒng)調(diào)用,將對(duì)應(yīng)下標(biāo)里面的內(nèi)容置為 NULL
,這是進(jìn)程系統(tǒng)需要執(zhí)行的工作。置空后操作系統(tǒng)會(huì)把該文件描述對(duì)應(yīng)文件結(jié)構(gòu)體對(duì)象中的 f_count
字段減減,然后判斷 f_count
是否為0,如果不為0就什么也不干,如果為0,操作系統(tǒng)才將對(duì)應(yīng)的 struct file
對(duì)象回收,這是文件系統(tǒng)執(zhí)行的工作。從這兒可以看出,文件描述符表的存在,將進(jìn)程系統(tǒng)和文件系統(tǒng)進(jìn)行了完美的解藕。這不禁讓我想起了前面的虛擬地址(進(jìn)程地址空間)和頁(yè)表的存在將進(jìn)程系統(tǒng)和內(nèi)存系統(tǒng)進(jìn)行解藕。Linux 操作系統(tǒng)的設(shè)計(jì)真的讓人拍案叫絕!
int main()
{
close(1); // 將 stdout 關(guān)閉
int ret = printf("stdin->fd: %d\n", stdin->_fileno);
printf("stdout->fd: %d\n", stdout->_fileno);
printf("stderr->fd: %d\n", stderr->_fileno);
fprintf(stderr, "printf ret: %d\n", ret);
return 0;
}
代碼分析:close(1)
表示將標(biāo)準(zhǔn)輸出關(guān)閉,1下標(biāo)指向顯示器文件,printf
就是向標(biāo)準(zhǔn)輸出中進(jìn)行寫入,關(guān)閉后,三條 printf
函數(shù)都沒(méi)有將內(nèi)容成功打印到顯示器上。根據(jù)上面的分析,雖然把標(biāo)準(zhǔn)輸出關(guān)了,但是標(biāo)準(zhǔn)錯(cuò)誤也指向顯示器,所以在調(diào)用 fprintf
向標(biāo)準(zhǔn)錯(cuò)誤中寫入時(shí),我們可以在顯示器上看到打印結(jié)果。其次,printf
執(zhí)行成功,返回值表示寫入的字符個(gè)數(shù),可以看出雖然我們通過(guò)系統(tǒng)調(diào)用直接把標(biāo)準(zhǔn)輸出給關(guān)了,但是 printf
還是認(rèn)為它寫入成功。
五、結(jié)語(yǔ)
今天的分享到這里就結(jié)束啦!如果覺(jué)得文章還不錯(cuò)的話,可以三連支持一下,春人的主頁(yè)還有很多有趣的文章,歡迎小伙伴們前去點(diǎn)評(píng),您的支持就是春人前進(jìn)的動(dòng)力!文章來(lái)源:http://www.zghlxwxcb.cn/news/detail-830473.html
文章來(lái)源地址http://www.zghlxwxcb.cn/news/detail-830473.html
到了這里,關(guān)于【Linux取經(jīng)路】文件系統(tǒng)之被打開(kāi)的文件——文件描述符的引入的文章就介紹完了。如果您還想了解更多內(nèi)容,請(qǐng)?jiān)谟疑辖撬阉鱐OY模板網(wǎng)以前的文章或繼續(xù)瀏覽下面的相關(guān)文章,希望大家以后多多支持TOY模板網(wǎng)!