文章目錄
- 前言
- 一、C語言中的文件接口
- 1. 文件指針(句柄)FILE*
- 以寫方式打開文件,若文件不存在會新建一個文件
- W寫入方式,在==打開文件之前==都會將文件內容全部清空
- 追加寫方式,其用法與寫方法一致,不同在于a方法可以在文件結尾寫入
- 二、認識文件系統調用
- Linux下的系統調用open()
- 第一個參數為文件路徑
- 第二個參數為操作文件的方式
- 第三個可選參數是更改創建文件的默認權限:
- 三、訪問文件的本質
- 四、重定向與緩沖區
- 自定義重定向系統調用接口dup2
- 再談“一切皆文件”
- 1. 外設設備與文件系統的關系
- 2. 擴展思想:
- 總結
前言
在計算機系統中,文件由內容數據和元數據屬性共同構成。文件的完整生命周期分為兩個階段:
文件狀態 | 存儲位置 | 管理方式 |
---|---|---|
未打開文件 | 磁盤存儲介質 | 文件系統通過inode管理 |
已打開文件 | 內存 | 內核通過file結構體管理 |
- 所有文件操作本質上都是進程與文件系統的交互
- 打開文件需要將文件屬性加載到內存
- 文件內容采用按需加載策略(延遲加載)
研究文件系統本質是研究進程和文件之間的關系(文件是由進程打開的);未打開的文件存在磁盤上(存儲介質),文件要被打開(屬性)必須先要加載到內存;
一、C語言中的文件接口
基本輸入輸出 stdio.h
訪問磁盤的過程稱之為IO的過程,
1. 文件指針(句柄)FILE*
//C標準庫通過FILE結構體封裝文件描述符FILE *fopen(const char *path, const char *mode)
// mode參數決定了你的訪問權限
mode | 說明 | 特性 |
---|---|---|
“w” | 寫模式(清空文件) | 文件不存在時創建 |
“a” | 追加模式 | 保留原內容,末尾寫入 |
“r” | 讀寫模式 | 文件必須存在 |
以寫方式打開文件,若文件不存在會新建一個文件
若沒有指定路徑,程序會在默認當前路徑下創建,當前路徑指的是進程的當前路徑(使用ls /proc/[pid]
查看到當前進程的cwd)。
同樣的,修改當前進程的工作目錄就可以改變創建文件的默認路徑。
chdir("home/ys") //修改進程工作路徑為home/ys
W寫入方式,在打開文件之前都會將文件內容全部清空
上一個程序疑問:strlen要不要+1?
我們知道寫入字符串時需要將\0也寫入,我們試驗之后發現文本中多了@^這樣的亂碼,推測這就是\0,只不過vim文本編輯器將其解釋成了亂碼符號。結論是strlen不需要+1,文件系統沒有規定字符串必須以\0結尾。
追加寫方式,其用法與寫方法一致,不同在于a方法可以在文件結尾寫入
二、認識文件系統調用
c語言程序在啟動時,會默認打開三個標準輸入輸出流文件:
stdin:鍵盤設備
stdout:顯示器文件
stderr:顯示器文件
文件其實是在磁盤上的,由于磁盤是外部設備,訪問文件實際上是訪問磁盤這樣的硬件。不同的語言有不同的文件操作方式,但在底層用的是都是一樣的實現方式——都需要調用系統接口open、read、write。
庫函數(fopen,printf,fscanf等)訪問硬件設備一定會通過系統調用來訪問。
Linux下的系統調用open()
第一個參數為文件路徑
- 若pathname以路徑的方式給出,則當需要創建該文件時,就在pathname路徑下進行創建。
- 若pathname以文件名的方式給出,則當需要創建該文件時,默認在當前路徑下進行創建。(注意當前路徑的含義)
第二個參數為操作文件的方式
方式 | 含義 |
---|---|
O_RDONLY | 以只讀的方式打開文件 |
O_WRNOLY | 以只寫的方式打開文件 |
O_APPEND | 以追加的方式打開文件 |
O_RDWR | 以讀寫的方式打開文件 |
O_CREAT | 當目標文件不存在時,創建文件 |
1. O_WRONLY是寫方式,但是它并不會新建文件
2. O_CREAT打開文件時清空文件
3. O_APPEND 追加寫選項
寫入:
const char* message = "hello";
write(fd,message,strlen(message));
//write并不會對文件進行清空式寫入。
int fd = open("log.txt",O_WRONLY|O_CREAT|O_APPEND,0666); //追加
write(fd,message,strlen(message),);
第三個可選參數是更改創建文件的默認權限:
//eg:
int fd = open("log.txt",O_WRONLY|O_CREAT);
創建權限錯誤,所以新建文件時需要告訴接口權限是什么。
int fd = open("log.txt",O_WRONLY|O_CREAT,0666);
這里創建出來的并不是666而是664,應該要想到之前學到的權限掩碼(0002)的知識!
比特位級別的傳參方式原理:
使用位圖的方式,一次向操作系統傳遞多個標志位
三、訪問文件的本質
可以將其類比系統管理進程(struct_task),Linux系統中一切皆文件,因此管理進程勢必要通過先描述再組織的方法進行。要描述一個被打開的文件(struct_file),往往需要包含文件路徑、文件基本屬性(權限、大小、讀寫位置、訪問用戶的信息等)、文件的內核緩沖區信息、下一個struct_file的指針。
一個進程可能會打開多個文件,那么進程與文件之間又是如何關聯的?(1:n)
進程PCB中會存在一個結構體指針struct files_struct *files
指向了一個結構體,該結構體存放了一個存放各種文件PCB指針的數組;因為是數組,所以這也解釋了為什么open接口返回的是int類型的值了,進程根據這個下標就可以訪問對應文件。
如果嘗試打印一下返回值,發現文件描述符默認是從3開始的,那么0,1,2是什么文件呢?那就是標準輸入輸出錯誤流了!(stdin \ stdout \stderr
)
int fd = open("demo.txt",O_WRONLY |O_CREAT,0666);
cout << fd << endl; //3cout << stdin->_fileno << endl;//0
cout << stdout->_fileno << endl;//1
cout << stderr->_fileno << endl;//2
既然一切皆文件,那么輸出流也是文件,因此我們可以使用以下代碼向標準輸出流文件中寫入message信息:
const char* message = "hello";
write(1,message,strlen(message));// 1 就是標準輸出流stdout
從標準輸入流文件中讀取buffer大小的字符放在buffer[1024]數組中 :
char buffer[1024];
read(0,buffer,sizeof(buffer));
printf("echo: %s\n",buffer);
四、重定向與緩沖區
文件描述符對應的分配規則是什么?
從0下標開始,尋找沒有被使用的數組位置,它的下標就是新文件的文件描述符值。
假設我們有一個空文件log.txt,有如下代碼,含義是將msg中的strlen長度的數據輸出到顯示器。
const char* msg = "hello linux\n";
write(1,msg,strlen(msg));
但如果先關閉了1描述符(即關閉標準輸出流),除了顯示器無法顯示外
close(1);
int fd = open("log.txt", O_RDONLY | O_CREAT, 0666);//1
const char* msg = "hello linux\n";
write(1,msg,strlen(msg));//此時寫入的就是1號文件描述符,即log.txt 文件
log.txt中居然存有數據。
這一工作,稱為輸出重定向。根據上面的知識可以意識到關閉了1描述符后,那么這里就是空著的,當使用open接口新建log.txt時,根據文件描述符分配規則,自然1號位就成為了log.txt的fd描述符,所以將本來要寫入stdout的數據寫入到了log.txt中。
自定義重定向系統調用接口dup2
int dup2(int oldfd,int newfd)
把oldfd復制到newfd
//oldfd 相當于 原本的 3 描述符
//newfd 相當于 原本的 1 描述符int fd = open("log.txt", O_WRONLY|O_CREAT, 0666);
dup2(fd, 1);
這里要注意的是,重定向中的拷貝,不是將文件描述符表中的下標進行拷貝,而是對下標處的內容(文件結構體指針)進行拷貝!
使用dup2在打開文件log.txt后,進行了輸出重定向,將原本輸出到顯示器的內容寫入到了log.txt文件中。再次更改代碼open的宏參數(O_TRUNC -> O_APPEND),就成為了追加重定向操作。結果如下所示:
同樣的,可以修改代碼讓其重定向標準輸入流至文件(默認read從stdin文件讀數據,重定向后,從log.txt文件中讀)。這一過程稱為輸入重定向。
以上是使用dup2重定向系統調用函數write、read,前面提到c語言printf、fprintf底層也是這樣的文件描述符表的結構,那是否可以控制c語言中的輸入輸出呢?
dup2(fd,1);
printf("hello printf\n");
fprintf(stdout,"hello printf\n");
回想之前的章節介紹到echo指令,可以進行輸出重定向,cat指令可以進行輸入重定向
echo "hello" > log.txt //輸出重定向
cat < log.txt //輸入重定向
echo "hello" >> log.txt //追加重定向
進程的替換不會影響文件的訪問(包括重定向操作)——復習進程替換
stdout與stderr都是可以向顯示器打印,為什么要有2?他們倆的區別是什么?
有如下代碼,表示將字符串分別輸出到1(標準輸出流)和2(標準錯誤流)中。
$ ./mytest 1>normal.log 2>err.log
//將stdout的數據重定向至normal.log
//將stderr的數據重定向至err.log
實際上,1和2是相同的實現方式,只不過在使用中,相較于正常結果而言,更關注的是它的錯誤信息,而正常運行的信息往往很多,不便錯誤的篩查與糾正。因此,為了將錯誤信息分離出來,才有了標準錯誤流。
一個衍生用法:
$ ./mytest >normal.log 2>&1
再談“一切皆文件”
1. 外設設備與文件系統的關系
在這之前我們知道:所有操作計算機的動作都是由進程執行的,包括文件的訪問,每一種外設都要有描述他們的結構體對象(struct_dev)。
此外,每一種外設都有其相獨特的讀寫方法,縱然每個外設對應的訪問實現方式不同(各家外設設備驅動的不同),而對于操作系統來看,這些外設無非都是一些需要進行讀寫的文件,而能夠直接進行文件訪問讀寫的就是進程(open接口),打開新的文件就會創建一個新的struct_file,這個結構體是不是很熟悉?在這個結構體中,就存在著能夠指向該文件具體實現自身讀寫行為的指針(struct fils_operations*),例如(指向了不同磁盤的讀寫方法,不同鍵盤的讀寫方法)。
- 在Linux中,將struct_file這一層的邏輯關系稱為虛擬文件系統(VFS)。
外設差異化被封裝在驅動中:不同廠商的驅動實現自己的讀寫邏輯(如
razer_keyboard_read
和logitech_keyboard_read
),但必須遵循操作系統定義的接口。
?操作系統通過抽象層統一接口:上層應用只需調用read()、write()
等標準接口,無需關心底層是羅技還是雷蛇設備。
2. 擴展思想:
這種設計模式與 面向對象編程中的多態性高度相似:
?基類(抽象接口) ?:操作系統定義的設備驅動接口(如file_operations)。
?派生類(具體實現) ?:廠商驅動的讀寫函數(如雷蛇、羅技的實現)。
運行時多態 :通過函數指針動態綁定到具體實現。
通過這種機制,操作系統實現了外設的 ??“高內聚、低耦合”?
,使得硬件廠商可以自由創新,同時保持軟件生態的兼容性。
總結
👍 ?感謝各位大佬觀看。如果本文有幫助,請點贊收藏支持~