目錄
- 一、初步理解系統層面的文件
- 1. 文件操作的本質
- 2. 進程管理文件核心思想
- 二、系統調用層
- 1. 打開關閉文件函數
- 2. 讀寫文件函數
- 三、操作系統文件管理
- 1. 文件管理機制
- 2. 硬件管理機制
- 四、理解重定向
- 1. 文件描述符分配規則
- 2. 重定向系統調用
- 3. 重定向命令行調用
- 五、理解緩沖區
- 1. 緩沖區介紹
- 2. 緩沖區刷新策略
- 3. 有趣現象
一、初步理解系統層面的文件
1. 文件操作的本質
在C語言里文件操作時,fopen打開文件,本質是cpu執行代碼到這一行,進程幫我們創建相應的內核數據結構和相關初始化,打開文件本質是進程打開文件
2. 進程管理文件核心思想
一個進程可以打開多個文件,系統中有許多進程,所以大多數情況下,OS內部,一定存在大量的被打開的文件,同時,操作系統也要進行這些文件的管理
操作系統管理文件與管理進程的方式類似,先描述在組織,管理相應的結構體(類似于進程的pcb)
文件 = 屬性 + 內容
二、系統調用層
1. 打開關閉文件函數
函數原型
#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);int creat(const char *pathname, mode_t mode);#include <unistd.h>int close(int fd);//關閉一個打開的文件,參數fd為打開文件時open的返回值#include <sys/types.h>
#include <sys/stat.h>mode_t umask(mode_t mask); //動態更改當前進程創建文件時的權限掩碼
參數說明
pathname為要打開文件的名稱
falgs參數是用bit位來進行標志的傳遞,即位圖,其含義為代碼打開方式(只讀,只寫,讀寫,追加……),falgs常用選項如下:
O_APPEND 寫的時候追加寫入
O_TRUNC 寫的時候清空文件
O_WRONLY 寫方式打開
O_RDONLY 寫方式打開
O-CREAT 打開時若沒有文件,則在進程當前工作路徑下創建文件
mode為新創建文件時的權限,該權限會由系統的權限掩碼計算后再給設置到新文件
可以通過umask函數在進程內更改該進程創建文件時的權限掩碼
umask
計算:最終權限 = mode & ~umask- 示例:
open("file", O_CREAT, 0666)
+ umask=002 → 實際權限664
返回值
返回值fd,一個整數,稱為文件描述符,對應一個文件內核數據結構(在下文操作系統管理中詳細說明),在后續的文件寫入或者輸出時,傳參的fd都是文件描述符
返回值小于零打開失敗
返回值非零時,打開對應文件會返回對應的值,前3個默認打開,分別是
0:標準輸入 stdin 鍵盤
1:標準輸出 stdout 顯示器
2:標準錯誤 stderror 顯示器
前3個進程啟動時默認打開,一般情況下我們自己打開或者創建文件返回值從3開始依次增加
1和2都對應著顯示器,為什么同時默認打開1和2
1和2對應的文件都是顯示器文件,區別就是,當我們進行標準輸出重定向時,只會將1號文件進行重定向,2號不會被重定向。
默認同時打開的主要原因就是我們輸出信息時,有正確的信息也有錯誤信息,只需做一次輸出重定向就可以將錯誤信息和正確信息分離開,標準輸出重定向只會將1號文件重定向,2號文件不會改變,依舊輸出在顯示器上。
#include<stdio.h>#include<sys/types.h>#include<sys/stat.h>#include<fcntl.h>#include<unistd.h> int main(){fprintf(stdout,"hello stdout \n");fprintf(stdout,"hello stdout \n");fprintf(stdout,"hello stdout \n");fprintf(stderr,"hello stderr \n");fprintf(stderr,"hello stderr \n");fprintf(stderr,"hello stderr \n");return 0;}
運行結果
也可以用兩次重定向(在后文重定向中說明)將正確消息和錯誤消息分開放在不同的文件里,方便我們的調試查看信息。
2. 讀寫文件函數
#include <unistd.h>ssize_t write(int fd, const void *buf, size_t count);
//向文件fd里面寫入buf指向的內容,寫入內容大小為count,返回值大于0表示實際寫入的字節,等于0表示什么都沒寫,返回-1表示失敗,并且設置錯誤碼ssize_t read(int fd, void *buf, size_t count);//向buf里面讀入fd文件的內容,讀入內容大小為count,返回值大于0表示實際讀取的字節,等于0表示讀到了文件結尾,返回-1表示失敗,并且設置錯誤碼#include <sys/types.h>#include <sys/stat.h>#include <unistd.h>int stat(const char *path, struct stat *buf); //通過路徑獲取文件屬性//buf為輸出型參數,文件屬性存入buf指向的結構體,實現函數功能,成功返回0,失敗返回-1,并且設置錯誤碼int fstat(int fd, struct stat *buf);//通過文件描述符獲取文件屬性int lstat(const char *path, struct stat *buf); //通過路徑獲取文件屬性
內核中的文件屬性(struct stat):
struct stat {dev_t st_dev; // 設備IDino_t st_ino; // inode號mode_t st_mode; // 文件類型和權限nlink_t st_nlink; // 硬鏈接數uid_t st_uid; // 所有者UIDgid_t st_gid; // 組GIDoff_t st_size; // 文件大小(字節)// ... 時間戳等字段
};
三、操作系統文件管理
1. 文件管理機制
核心思想:先描述,在組織
內核級管理
每打開一個文件時,操作系統要創建相應的內核數據結構(struct file),并且會創建文件內核級的緩存(開辟的一塊空間)。內核數據結構中存在指針指向該緩存,并且會用文件的屬性去初始化內核數據結構,將文件的內容加載到文件緩存里。
一個進程可以打卡多個文件,操作系統需要建立進程和文件的對應關系,所以在進程的pcb(task_struct)中存在一個struct files_struct * files指針(指向文件描述符表),struct files_struct數據結構中包含struct file* fd_array[N],一個文件指針的數組,對應該進程所打開的文件的內核數據結構file,該數組下標就是打開文件時所返回的文件描述符。
操作系統提供的系統調用可以用文件描述符快速找到文件對應的內核數據結構進行操作,在進行讀寫時,都必須在合適的時候讓OS把文件的內容讀寫在緩沖區,在進行刷新
在操作系統內,訪問文件只認文件描述符fd
語言級管理
在C語言中,通過封裝系統調用設計出來一系列的文件操作函數(fprintf,fscnaf,fopen),在配合C語言封裝的文件結構體struct FILE,我們常常定義的文件指針FILE*就是這種結構的指針。
struct FILE中封裝這文件描述符,語言級的緩沖區,打開方式等信息。
如圖,int _fileno 為文件描述符,_falgs為文件打開方式, _IO_write_end為該緩沖區的結束……
C語言中所有的文件操作都是對系統調用的封裝。
由于在不同的系統中系統調用是不同的,因此我們寫的含有系統調的代碼不具備跨平臺性,但是我們用C語言庫中的函數,他是具備跨平臺性的,因為我們在不同的平臺下有不同的C語言標準庫,他們底層封裝的系統調用是對應系統的系統調用。
文件打開流程
- 創建
struct file
對象 - 分配內核緩沖區(可延遲加載數據)
- 查進程的文件描述符表空閑的下標
- 存儲file對象地址于文件描述符表中
- 返回fd下標
2. 硬件管理機制
在Linux系統中,一切皆文件
硬件設備都會有自己的共同的屬性(名稱,廠商,生產日期等),這些屬性都被封裝在結構體中(struct device),同時,不用的硬件也有自己獨特的操作方法(驅動程序中實現),比如像顯示器上輸出,從鍵盤鼠標內讀取數據等,這些方法都是驅動程序中一個個的函數。
在Linux系統中,打開或者使用某一個外設時,會像管理文件一樣管理硬件,創建一個struct file(文件內核數據結構),存放著對應硬件設備使用的函數的函數指針,還有指向屬性結構體(struct device)的指針和屬于該硬件的緩沖區的指針,向硬件設備中讀或者寫數據時,先在緩沖區操作,然后將緩沖區內容刷新到設備或者內存。
不同的設備的驅動程序中,類似操作的函數參數要設計相同,因為在struct file中函數指針只有一套,但要調用不同設備的方法,參數相同才可以兼容
源碼中部分函數指針:
由于struct file在進行文件管理時(硬件也看做文件),即有屬性(struct device,文件屬性),還有方法(操作底層方法指針表,或者操作文件的方法列表),因此這也是一種類的實現,并且用相同的函數名(函數指針)來操作不同類型的硬件設備或者文件,也是用C語言實現的多態技術,這種管理技術在Linux系統中也叫做vfs(virtual file system)
四、理解重定向
1. 文件描述符分配規則
打開文件時,查文件描述符表從0開始分配,還沒有被使用的最小的下標將被分配
重定向的本質是在內核中改變文件描述符表特定下標的內容,與上層無關
2. 重定向系統調用
#include <unistd.h>int dup(int oldfd);int dup2(int oldfd, int newfd);//將文件描述符表中oldfd下標對應的內容拷貝到newfd對應下標的位置
舉例
#include<stdio.h>#include<sys/types.h>#include<sys/stat.h>#include<fcntl.h>#include<unistd.h> int main(){const char* filename = "file.txt";//打開文件,不存在文件時新建,以寫入方式打開,每次打開時清空文件int fd = open(filename, O_CREAT | O_WRONLY | O_TRUNC, 0666);//重定向dup2(fd, 1);//默認向stdout中寫入printf("hello Linux\n");//指定向stdout中寫入fprintf(stdout, "hello Linux\n");return 0;}
運行結果:
可以看到,本應當向標準輸出stdout中輸出的內容輸出到文件file.txt中了。
因為我們使用了重定向將該進程中文件描述符表里的fd(值為3,因為0,1,2默認被使用,從3開始分配)下標的內容拷貝到1下標位置,因此1號下標本來存放的標準輸出文件對象指針被我們修改為“file.txt”文件的文件對象指針。
在底層的文件描述符表中,1號下標處為我們重定向的file.txt文件對象指針,但是上層stdout(C語言提供的FILE*指針,里面封裝這文件描述符等信息)里面的文件描述符等其他信息都沒有改變,在調用printf,fprintf時都向stdout中打印,stdout中封裝著_fileno = 1,操作系統就會打印到文件描述符表里將下標為1處的文件指針所指的文件對象的緩沖區中,即向我們重定向的文件file.txt中打印。
3. 重定向命令行調用
簡單調用
在指定被重定向的文件時,默認為標準輸出重定向,即文件描述符為1的文件
可執行程序 > filename ,表示標準輸出重定向到filename,不存在該文件時就新建
可執行程序 >> filename, 和> 大致相同,唯一區別就是>的重定向默認清空文件,>>是追加的打印,不會在打開文件時清空,
< filename 表示標準輸入重定向
舉例:
echo hello Linux > log.txt
該命令可以將本來向顯示器打印的 hello Linux 打印到log.txt中
復雜調用
指令或可執行程序 1>filename1 2>filename2 ……,表示將文件描述符為1的文件重定向到filename1, 文件描述符為2的文件重定向到filename2
舉例:
指令或可執行程序 1>filename 2>&1,將1號重定向到filename,在&1(1號下標的內容,即filename的地址)放入下標為2,即1,2同時指向filename
五、理解緩沖區
1. 緩沖區介紹
緩沖區就是一段內存空間,可以給上層提供高效的IO體驗,間接提高整體的效率,
緩沖區有用戶級緩沖區(語言提供,維護的)和內核級緩沖區(操作系統提供,維護的),緩沖區的優點有解耦(用戶只需將數據交到緩沖區,緩沖區會自己刷新到下一個目標位置,一般不需要我們在進行操作,設計),提高效率。每一個打開的文件都有自己的緩沖區,語言級的緩沖區在struct FILE中(C語言中的文件指針FILE*),內核級的緩沖區在文件對象(struct file)中。
2. 緩沖區刷新策略
1.立即刷新,fflush(stdout),fsync(fd),這類函數調用可以立即刷新緩沖區,可以認為是無緩沖
2.行刷新,顯示器通常采用行刷新
3.全緩沖,緩沖區寫滿,才刷新,普通文件通常采用全緩沖
4.進程退出,系統會自動刷新
3. 有趣現象
有下面2段代碼
代碼1:
#include<stdio.h>
#include<string.h>
#include<sys/types.h>
#include<sys/stat.h>
#include<fcntl.h>
#include<unistd.h>int main()
{printf("hello printf\n");fprintf(stdout, "hello fprintf\n");const char* message = "hello write\n";write(1, message, strlen(message));return 0;
}
執行結果
代碼2:
#include<stdio.h>
#include<string.h>
#include<sys/types.h>
#include<sys/stat.h>
#include<fcntl.h>
#include<unistd.h>int main()
{printf("hello printf\n");fprintf(stdout, "hello fprintf\n");const char* message = "hello write\n";write(1, message, strlen(message));fork();return 0;
}
執行結果
現象:
重定向后先輸出的是write,后輸出printf和fprintf,
當創建子進程時,printf和fprintf被輸出兩遍
原因:
我們在命令行進行了重定向執行程序時,會使得原本向stdout輸出的內容輸出到file.txt文件中,這會改變刷新策略,顯示器是按行刷新,普通文件是寫滿緩沖區或者進程退出在刷新,因此當"hello printf\n"和"hello fprintf\n"被寫入用戶緩沖區時,不會直接刷新,而調用write時,該調用是系統調用,無用戶緩沖區,直接寫入內核緩沖區,內核緩沖區直接進行刷新,因此第一行是"hello write",后面程序退出時在將用戶緩沖區的內容刷新,先刷新到內核緩沖區,在刷新到文件,
圖片轉存中…(img-MnhIhwH7-1753197796549)]
現象:
重定向后先輸出的是write,后輸出printf和fprintf,
當創建子進程時,printf和fprintf被輸出兩遍
原因:
我們在命令行進行了重定向執行程序時,會使得原本向stdout輸出的內容輸出到file.txt文件中,這會改變刷新策略,顯示器是按行刷新,普通文件是寫滿緩沖區或者進程退出在刷新,因此當"hello printf\n"和"hello fprintf\n"被寫入用戶緩沖區時,不會直接刷新,而調用write時,該調用是系統調用,無用戶緩沖區,直接寫入內核緩沖區,內核緩沖區直接進行刷新,因此第一行是"hello write",后面程序退出時在將用戶緩沖區的內容刷新,先刷新到內核緩沖區,在刷新到文件,
創建子進程后,用戶緩沖區沒有寫滿還沒有被刷新,內核緩沖區已刷新,父子進程各有自己的用戶緩沖區,父子進程各自刷新一次,所以出現了printf和fprintf打印2次