?前言:
在生活里,我們常和各種文件打交道,像用 Word 寫文檔、用播放器看視頻,這些操作背后都離不開文件的輸入輸出(I/O)。在 Linux 系統中,文件 I/O 操作更是復雜且關鍵。
接下來我們將深入探討Linux 基礎 IO,不僅包含了 C 語言文件 I/O 操作函數,如打開文件的fopen
、讀取文件的fread等常見接口
,還詳細講解了系統文件 I/O 的函數和原理,以及文件描述符、重定向、緩沖區等重要概念(內容太多了,分成兩篇博客介紹)。
目錄
?前言:
一. 文件
? ? ? ?1.相關概念
? ? ? ? ? ? 1.0 理解文件
? ? ???????1.1文件組成:
? ? ????????1.2文件路徑:
? ? ????????1.3文件訪問:
? ???????????1.4進程與文件關系:?
?編輯
? ? ? ? ? ? 1.5文件管理機制:
?2.系統調用接口
2.0 回顧C語言的函數接口
? ? ? ? 2.01 從文件的打開到文件的關閉
? ? ? ?打開文件:fopen()
讀取文件:fread()
文件修改:fwrite()
關閉文件:fclose()
? ? ? ?
???????? 2.02 文件的錯誤處理
?編輯
2.03 默認的IO流?
?2.1. open,write,read,close函數:
???????????2.11?open函數
第一個參數pathname?
?第二個參數flags
第三個參數mode
open的返回值
2.2 write函數?
2.3 read函數
2.4close函數?
?三、文件描述符fd
3.1文件描述符的概念
3.2文件描述符與FILE*的區別
四.文件描述符fd分配規則
?? ? ?4.1. 最小可用原則:
??????4.2演示:
一. 文件
? ? ? ?1.相關概念
? ? ? ? ? ? 1.0 理解文件
???狹義理解
- 文件在磁盤里
- 磁盤是永久性存儲介質,因此文件在磁盤上的存儲是永久性的(當然,時間久了肯定會有損害)
- 磁盤是外設(即是輸出設備也是輸入設備,有文件從內存里讀,就有文件向內存里寫)
- 對磁盤上所有文件的操作本質都是內存對外設的輸入和輸出,簡稱IO
? ?廣義理解
- 操作系統內一切皆文件
- 操作系統為了方便管理各種各樣的外設,選擇將他們都看成文件便于管理。
? ? ???????1.1文件組成:
????????????????文件由屬性和內容構成。
對于OKB的空文件是占用磁盤空間的,這是為啥呢???
大家可以想一想,我們平常在文件管理器上看到的KB大小,是不是指的文件大小,如果大小為0,為啥還會占用空間呢?很簡單,磁盤當中存儲的是我們文件的屬性和內存,即使是空文件他還是要有文件本身的屬性的。空文件在磁盤當中,存儲的是文件的屬性。
所以?文件=屬性+內容
? ? ????????1.2文件路徑:
????????????????(關于這一點我們在fopen那里會進一步講解)?
????????????????標定文件的唯一性需使用絕對路徑(路徑+文件名),未指定路徑時默認當前目錄。
? ? ????????1.3文件訪問:
????????????????只有被打開的文件才能被進程訪問,未被打開的文件存儲在磁盤中。(后一部分? ?由文件系統管理,在下一篇博客中會專門講解)
? ???????????1.4進程與文件關系:?
????????????????????????文件操作的本質是進程與被打開文件間的交互,由操作系統管理。多文件管理則是操作系統通過 `files_struct`統一 管理打開的文件,其中包含文件描述符表(fd數組),fd是數組索引。
就如圖所示,每一個被打開的文件,操作系統都會分配一個file_struct的結構,管理并存儲對應文件的屬性與內容,而進程則是在PCB當中保存著一個可以指向被打開文件數組的指針,然后通過特定文件描述符訪問或修改文件。
? ? ? ? ? ? 1.5文件管理機制:
????????????????????????操作系統通過“先描述再組織”管理文件,內核中每個文件對應一個數據結構(`struct file`),包含文件屬性和內容指針。
正如在1.4所說的,文件被操作系統用file_struct管理起來,如何管理不就是通過各種數據結構簡單而高效的管理嗎?這就是“再組織”的過程。通過對文件的結構的管理,不就實現了對文件的管理嗎。
?2.系統調用接口
2.0 回顧C語言的函數接口
在C語言中,文件I/O操作主要是通過C語言的標準庫提供的FILE *
指針和一系列文件操作函數來實現的。這些函數為開發人員提供了更高層的文件操作接口,使得文件讀寫變得簡單和方便。
? ? ? ? 2.01 從文件的打開到文件的關閉
? ? ? ?打開文件:fopen()
filename
:表示文件的路徑,文件可以是相對路徑或絕對路徑。mode
:文件打開的模式,決定文件的讀寫方式。- 返回值:該函數成功返回打開文件的指針,失敗著返回NULL。
"r" | 以只讀模式打開文件,文件必須存在。 |
"w" | 以寫模式打開文件,若文件已存在,且文件有內容,則文件會被清空。 沒有文件,則創建文件。 |
"a" | 以追加模式打開文件,若文件不存在則會創建文件。 |
"rb" | 以二進制模式只讀打開文件,文件必須存在。 |
"wb" | 以二進制模式寫入文件,若文件已存在則會被清空。 |
代碼示例
1 #include<stdio.h>2 #include<unistd.h>3 4 5 int main()6 {7 FILE* pf=fopen("text.txt","w");8 if(pf)9 printf("創建文件成功\n"); 10 return 0;11 }
~
?結果
讀取文件:fread()
size_t fread(void *ptr, size_t size, size_t nmemb, FILE *stream);
ptr
:指向內存的指針,用于存儲讀取的數據。size
:每個數據元素的大小(單位:字節)。count
:讀取的元素個數。stream
:文件指針,指定從哪個文件讀取數據。- 返回值:返回讀取成功的字節個數
代碼及結果
1 #include<stdio.h>2 #include<unistd.h>3 4 5 int main()6 {7 FILE* pf=fopen("text.txt","r");8 if(!pf)9 {10 perror("open file failed:\n");11 return 99;12 }13 printf("打開文件成功\n");14 char buffer[100];15 size_t n=fread(buffer,sizeof(char),100,pf);16 if(n) 17 {18 printf("讀取成功\n");19 //之所以把數組的最后置成'\0',那是因為C語言的接口只能識別以’\0‘為結尾的字符串。20 buffer[n]='\0';21 printf("這是text.txt文件的內容:%s\n",buffer);22 }23 return 0;24 }
文件修改:fwrite()
size_t fwrite(const void *ptr, size_t size, size_t nmemb,FILE *stream);
ptr
:指向內存中數據的指針。size
:每個數據元素的大小(單位:字節)。count
:寫入的元素個數。stream
:文件指針,指定將數據寫入哪個文件。?
代碼演示
1 #include<stdio.h>2 #include<unistd.h>3 4 5 int main()6 {7 8 FILE* pf=fopen("text.txt","r");9 if(!pf)10 {11 perror("open file failed:\n");12 return 99;13 }14 printf("打開文件成功\n");15 char buffer[100]={"這是一段用于演示的代碼,驗證fwrite的功能\n"};16 size_t n=fwrite(buffer,sizeof(char),100,pf);17 if(n)18 {19 printf("讀取成功\n");20 //之所以把數組的最后置成'\0',那是因為C語言的接口只能識別以’\0‘為結尾的字符串。21 buffer[n]='\0';22 printf("這是text.txt文件的內容:%s\n",buffer);23 }24 return 0;25 }
結果
注:fwrite
?函數本身既不是覆蓋寫也不是追加寫,它的寫入方式取決于文件的打開模式(是由'r'還是'w'決定的)。
關閉文件:fclose()
int fclose(FILE *fp);
- fp:一個已經打開的文件指針?
代碼示例
結果
? ? ? ?
???????? 2.02 文件的錯誤處理
? ? ? ? ? 在文件操作中,對于錯誤的處理是非常重要,C語言提供了兩個函數來幫助開發者檢測錯誤:ferror()
和feof()
ferror()與feof()
int feof(FILE *stream);int ferror(FILE *stream);
- ferror(FILE *stream):判斷是否發生了文件I/O錯誤。
- 函數返回值:無錯誤出現時返回0;有錯誤出現時,返回一個非零值。
- feof(FILE *stream):判斷文件是否到達了末尾。
- 函數返回值:如果沒有到文件尾,返回0;到達文件尾,返回一個非零值。
這兩個錯誤處理的函數,主要是判斷文件錯誤的類型。
2.03 默認的IO流?
都說Linux下一切皆文件,也就是說Linux下的任何東西都可以看作是文件(至少在操作系統看來),那么顯示器和鍵盤當然也可以看作是文件。我們能看到顯示器上的數據,是因為我們向“顯示器文件”寫入了數據,內存能獲取到我們敲擊鍵盤時對應的字符,是因為內存從“鍵盤文件”讀取了數據。
那么不有一個問題嗎?文件的打開不是要進程主動打開使用嗎?那為什么我們向“顯示器文件”寫入數據以及從“鍵盤文件”讀取數據前,不需要進行打開“顯示器文件”和“鍵盤文件”等相應操作?
那自然是操作系統為我們打開了基礎文件,任何進程在運行的時候都會默認打開三個輸入輸出流文件,即標準輸入、標準輸出以及標準錯誤,對應到C語言當中就是stdin、stdout以及stderr。
查閱man手冊
extern FILE *stdin;extern FILE *stdout;extern FILE *stderr;
?當我們的C程序被運行起來時,操作系統就會默認使用C語言的相關接口將這三個輸入輸出流打開,之后我們才能調用類似于scanf和printf之類的函數向鍵盤和顯示器進行相應的輸入輸出操作。
當然在默認的情況下,stdout,stderr對應的外設都是顯示器,stdin則是鍵盤。
代碼演示
1 #include<unistd.h>2 #include<sys/types.h>3 #include<stdio.h>4 5 6 int main()7 {8 fclose(stdout); 9 printf("這是一段用于演示,關閉標準輸出流后,無法向顯示器打印\n");10 return 0;11 }
結果
?其實不止是C語言當中有標準輸入流、標準輸出流和標準錯誤流,C++當中也有對應的cin、cout和cerr,其他所有語言當中都有類似的概念。
這也揭示了這種特性并不是某種語言所特有的,而是由操作系統所支持的。
?2.1. open,write,read,close函數:
????????操作文件除了C語言接口、C++接口或是其他語言的接口外,操作系統也有一套系統接口來進行文件的訪問。(各個語言對文件的訪問,本質上都是對系統接口封裝)。
我們在Linux平臺下運行C代碼時,C庫函數就是對Linux系統調用接口進行的封裝,
在Windows平臺下運行C代碼時,C庫函數就是對Windows系統調用接口進行的封裝,
這樣做使得語言有了跨平臺性,也方便進行二次開發。
(在不同的編譯環境當中,C語言的庫編寫方式不同,因此在語言庫的方面上保證C語言的可移植性)。
???????????2.11?open函數
?????????????man手冊原型
1 int open(const char *pathname, int flags);2 int open(const char *pathname, int flags, mode_t mode);
????????參數分析
? ? ? ? ?
pathname
:要打開的文件的路徑。flags
:文件打開模式,決定文件的訪問方式。mode
:文件權限,通常在文件創建時使用。
第一個參數pathname?
若pathname以路徑的方式給出,則當需要創建該文件時,就在pathname路徑下進行創建。
若pathname以文件名的方式給出,則當需要創建該文件時,默認在當前路徑下進行創建。(注意當前路徑的含義)?
還記得我們在“1.3文件路徑”中所說的區別?
什么是當前路徑?
我們知道,當fopen以寫入的方式打開一個文件時,若該文件不存在,則會自動在當前路徑創建該文件,那么這里所說的當前路徑指的是什么呢?我們通過代碼來驗證一下
代碼
1 #include<stdio.h>2 #include<unistd.h>3 4 5 int main()6 {7 FILE* pf=fopen("cwd.txt","w"); 8 fclose(pf);9 return 0;10 }
結果
這是我們處于“cwd.exe”進程的相同的路徑,結果與預想的差不多
那是否意味著這里所說的“當前路徑”是指“當前可執行程序所處的路徑”呢?
,?但如果我們處于與進程不同的路徑下再次運行程序結果會如何呢??我們返回上級路徑在測試一遍?
這時我們可以發現,該可執行程序運行后并沒有在FileIO目錄下創建cwd.txt,而是在我們當前所處的路徑下創建了cwd文本。
為了解釋這一現象,我們調出該進程的各項屬性
我們可以發現兩個明顯像是路徑的變量,cwd就是進程運行時我們所處的路徑,而exe就是該可執行程序的所處路徑?
總結:?實際上,我們這里所說的當前路徑不是指進程運行時所處的路徑,而是指該進程運行時我們所處的路徑。
?第二個參數flags
常見的flags
參數包括:
標志 | 描述 |
---|---|
O_RDONLY | 以只讀模式打開文件 |
O_WRONLY | 以只寫模式打開文件 |
O_RDWR | 以讀寫模式打開文件 |
O_CREAT | 文件不存在時創建文件 |
O_TRUNC | 如果文件已存在,清空文件內容 |
O_APPEND | 以追加模式打開文件 |
在C語言中我們經常用一個整形來傳遞選項,但是如果如果選項較多時,就會造成空間的浪費,這里我們可以通過使用一個比特位表示一個標志位,這樣一個int就可以同時傳遞至少32個標志位,此時的flag就可以看待成位圖的數據類型。而上面的參數就是一個又一個不同位置的宏,代表的是不同的位。
在打開文件的時候,利用“open”函數也可以達到“fopen”的效果,但是如果想要改變訪問文件的方式就得利用flag參數。
代碼:
1 #include<unistd.h>2 #include<stdio.h>3 #include <sys/types.h>4 #include <sys/stat.h>5 #include <fcntl.h>6 #include<string.h>7 8 9 int main()10 {11 int fd=open("open.txt",O_WRONLY | O_CREAT ,0664);12 if(fd == -1)13 {14 perror("open file falied\n");15 return -1;16 }17 18 const char str[]="這是一段用于檢測open函數參數的示例\n"; 19 write(fd,str,sizeof(str));20 return 0;21 }
結果
第三個參數mode
利用open函數的mode參數,可以對創建的文件進行權限管理,如果打開的文件已存在,那么該參數也無需去專門設置,設為0就好,但是不能不設置,否則會出現文件的權限錯誤。
例如,將mode設置為0666,則文件創建出來的權限如下:
按照之前的權限理解,我們創建的文件應該是具有所以的讀寫執行權限的。
但實際上創建出來文件的權限值還會受到umask(文件默認掩碼)的影響,實際創建出來文件的權限為:mode&(~umask)。umask的默認值一般為0002,當我們設置mode值為0666時實際創建出來文件的權限為0664。
若想創建出來文件的權限值不受umask的影響,則需要在創建文件前使用umask函數將文件默認掩碼設置為0。
當然我并不建議對系統默認的值進行太多的修改。
open的返回值
open函數的返回值是新打開文件的文件描述符。
當然啥是文件描述符在第三部分會有專門的介紹。
2.2 write函數?
接口
1 #include <unistd.h>2 ssize_t write(int fd, const void *buf, size_t count);
?man手冊
參數分析
fd
:文件描述符。buf
:指向的是要寫入數據的內存空間。count
:要寫入的字節數。
我們可以使用write函數,將buf位置開始向后count字節的數據寫入文件描述符為fd的文件當中。
- 如果數據寫入成功,實際寫入數據的字節個數被返回。
- 如果數據寫入失敗,-1被返回。
演示:
#include<stdio.h>
#include<unistd.h>
#include<sys/types.h>
#include<sys/stat.h>
#include<fcntl.h>
#include<string.h>int main()
{int fd=open("Write.txt",O_WRONLY | O_CREAT,0666);if(fd < 0){perror("open");return 1;}const char* message ="Hello world 0.6\n";for(int i=0;i<10;i++)write(fd,message,strlen(message));return 0;
}
注:在上面的例子當中,我們不能使用sizeof函數充當write的第三個參數,因為message是一個指針,它不是一個字符數組。?
注:?向文件寫入數據時,是先將數據寫入到對應文件的緩沖區當中,然后定期將緩沖區數據刷新到磁盤當中
2.3 read函數
接口:read()
:讀取文件
#include <unistd.h>
ssize_t read(int fd, void *buf, size_t count);
man手冊
參數:
fd
:文件描述符(由?open
?返回)。
buf
:數據讀取的緩沖區地址。
count
:期望讀取的字節數。返回值:實際讀取的字節數(可能小于?
count
),0 表示文件結束,-1 表示錯誤。
演示:
1 #include<stdio.h>2 #include<unistd.h>3 #include<sys/types.h>4 #include<sys/stat.h>5 #include<fcntl.h>6 #include<unistd.h>7 #define SIZE 10248 9 int main()10 {11 int fd=open("Write.txt",O_RDONLY,0666);12 if(fd < 0)13 {14 perror("open failed:\n");15 return 1;16 }17 char message[1024];18 ssize_t n=read(fd,message,sizeof(message));19 printf("這是讀取到的數據:%s",message);20 return 0;21 }
?
2.4close函數?
接口:close()
1 #include <unistd.h>
2 int close(int fd);
參數:要關閉的文件描述符。
返回值:0 成功,-1 失敗。
當然這個函數是在是太簡單了,這里就不在贅述。
演示:?
1 #include<stdio.h>2 #include<unistd.h>3 #include<sys/types.h>4 #include<sys/stat.h>5 #include<fcntl.h>6 #include<unistd.h>7 #define SIZE 10248 9 int main()10 {11 int fd=open("Write.txt",O_RDONLY,0666);12 if(fd < 0)13 {14 perror("open failed:\n");15 return 1;16 }17 char message[1024];18 ssize_t n=read(fd,message,sizeof(message));19 printf("這是讀取到的數據:%s",message);20 close(fd);21 return 0;22 }
?三、文件描述符fd
3.1文件描述符的概念
文件是由進程運行時打開的,一個進程可以打開多個文件,而系統當中又存在大量進程,這就導致了,在系統中任何時刻都可能存在大量已經打開的文件。
因此,操作系統務必要對這些已經打開的文件進行管理,操作系統會為每個已經打開的文件創建各自的struct file結構體,然后將這些結構體以雙鏈表的形式連接起來,之后操作系統對文件的管理也就變成了對這張雙向鏈表的增刪查改的操作。而在我們學習過進程相關概念后,我們也明白進程之間也是存在一個PCB結構體的。
而為了區分已經打開的文件哪些屬于特定的某一個進程,我們就還需要建立進程結構體和文件結構體之間的對應關系。
進程和文件之間的對應關系是如何建立的?
?我們知道,當一個程序運行起來時,操作系統會將該程序的代碼和數據加載到內存,然后為其創建對應的task_struct、mm_struct、頁表等相關的數據結構,并通過頁表建立虛擬內存和物理內存之間的映射關系。
而task_struct當中有一個指針,該指針指向一個名為files_struct的結構體,在該結構體當中就有一個名為fd_array的指針數組(又被稱為文件描述表),該數組的下標就是我們所謂的文件描述符。
當進程打開log.txt文件時,我們需要先將該文件從磁盤當中加載到內存,形成對應的struct file,將該struct file連入文件描述符表,并將該結構體的首地址填入到fd_array數組當中下標為3的位置,使得fd_array數組中下標為3的指針指向該struct file,最后返回該文件的文件描述符給調用進程即可。
這也是為啥文件描述符是一個整數,因為:它本質上是一個數組下標。
3.2文件描述符與FILE*的區別
雖然C語言提供了FILE *類型和相關的標準庫函數來處理文件操作,但底層實際上是通過文件描述符來進行管理的。理解FILE *與文件描述符的區別對于深入理解文件I/O非常重要。
- 特性?? ? ? ? ? ? FILE *(C標準庫)? ? ? ? ? ? ? ? ? 文件描述符(Linux操作系統)
- 類型?? ? ? ? ? ?由C標準庫提供的類型? ? ? ? ? ? ? ? ?操作系統內核使用整數值標識
- 管理者? ? ? ? ?由C標準庫管理? ? ? ? ? ? ? ? ? ? ? ? ? ?由操作系統內核管理
- 主要用途??? ?提供更高級別的文件操作接口? ? ?提供更低級別的文件操作接口
- 緩沖區管理??提供緩沖區管理,提高效率? ? ? ? 不提供緩沖區管理
- 數據訪問方式?適用于文本文件的高級操作? ? ? 適用于二進制文件和直接內存映射操作
四.文件描述符fd分配規則
?? ? ?4.1. 最小可用原則:
????????????????新打開的fd選擇當前未被使用的最小整數【如關閉fd (0)后新文件占用0】。
??????4.2演示:
1 #include<stdio.h>2 #include<unistd.h> 3 #include<sys/types.h> 4 #include<sys/stat.h> 5 #include<fcntl.h> 6 #include<unistd.h>7 8 int main()9 {10 int fd1=open("Write.txt",O_RDONLY,0666);11 printf("這是關閉其他文件描述符之前的fd:%d\n",fd1);12 close(0);13 close(fd1);14 close(2);15 int fd2=open("Write.txt",O_RDONLY,0666);16 printf("這是關閉其他文件描述符之后的fd:%d\n",fd2);17 return 0;18 }
解釋:
先打開一個文件,發現該文件的描述符是3【也很好理解,畢竟上面還有標準輸入/輸出/錯誤3者】,然后關閉標準輸入、標準錯誤與該文件描述符【不關閉二,是我們后面還要打印fd觀察現象】?,在打開文件,發現描述符fd變成了0。