文件與fd
- 一、前置預備
- 二、復習c語言文件
- 三、系統文件認識
- 3.1 系統層面有關文件的接口(open):
- 3.2 簡單使用open參數
- 3.3 語言vs系統
- 3.4 進一步理解文件描述符:
- 四、內核中的文件
- 4.1 初步了解
- 4.2 理解一切皆文件:
- 4.3 IO基本過程
- 4.4 重定向
一、前置預備
1、首先我們先回憶一下,在linux的前幾章,文件=內容+屬性(時間等屬性)
2、在c語言中使用文件的形式是固定的—先打開文件,然后操作文件,最后關閉文件,那為什么在訪問文件之前我們必須先打開它了?
(1)首先我們需要知道文件沒有被打開時存放在哪里?----一般來說文件都存放于磁盤
(2)那是誰在訪問文件?----是不是當前我們寫的代碼所編譯形成的程序啊,這個程序我們也可以稱為一個進程
(3)既然是進程來訪問我們的文件,那根據我們前面所學的知識,進程是不是需要被加載到內存中然后被cpu來執行,當進程運行到打開文件這一行代碼時,cpu能夠直接去讀取磁盤上的文件嗎?—很顯然是不能的(馮洛伊曼),所以打開文件本質上就是文件加載到內存當中,那我們可以類比一下,進程被加載到內存時,進程=內核數據結構+代碼、數據,文件是不是可以認為,文件=內核數據結構+文件內容。
3、結論:我們研究打開的文件,是在研究:進程和文件的關系
4、研究文件系統可以分為兩部分來學習:
(1)沒有被打開的文件(磁盤)
(2)被打開的文件(內存)
二、復習c語言文件
1、在c語言或者c++中,一個進程會默認打開3個文件,就是標準輸入輸出流和錯誤流,對應的就是鍵盤、顯示器。但是我們知道鍵盤和顯示器都是硬件設備,我們能夠直接通過程序訪問硬件設備嗎?—很顯然不能,原因后面講解。(stdin / stdout / stderr)
2、是誰默認打開這三個標準輸入輸出流?----進程
下面就是c語言中對文件的兩種操作方式:
1、w(只寫),Truncate file to zero lengh or create text file for writing.(>)
(1)沒有該文件,創建該文件,并且寫入
(2)有該文件,但是沒有對文件寫入,則清空
(3)有該文件,并且有寫入,從0位置覆蓋式寫入
2、a(追加),顧名思義不會清空該文件內容,而是從結尾繼續添加寫入的內容(>>)
3、我們有幾種打印到屏幕的方式?:(純C)
4、我們知道用戶是不能直接訪問硬件的(磁盤,顯示器,鍵盤),要想訪問必須經過OS,所以我們所使用的C文件接口,底層一定封裝了對應的文件類系統調用
三、系統文件認識
在c語言或者c++中,文件的操作其實是給我們封裝好的,我們調用語言內的函數,然后它又調用系統接口,那我們現在就來看一下系統中文件的接口
3.1 系統層面有關文件的接口(open):
open中的參數:
(1)pathname:文件的文件名
(2)flags:常見的打開方式
O_RDONLY(只讀)、 O_WRONLY(只寫)、O_RDWR(讀寫),O_TRUNC(存在該文件就清空)、
O_CREAT(創建)、O_APPEND(若文件操作,追加),這些打開方式本質上是宏
第二個位置的操作方式可以傳多個,但是一般來說系統的接口是c語言的,不支持可變參數,這里是通過位圖的方式實現的,下面我們通過簡單的代碼來看下位圖是如何控制標記位的
(3)mode:權限位,在系統中文件管理與權限管理是兩個不同的模塊,如果文件已經存在,第三個參數沒有影響,但是如果我們是打開一個新的文件時,會創建一個新的文件,如果不帶第三個參數,那么該文件的權限是亂碼。
(4)系統中的open調用是有返回值的,它的返回值類型為int,當文件操作失敗返回-1,正常操作返回 !0,我們一般稱這個返回值叫做文件描述符。
由上圖我們可以看到,本質上系統中文件調用的接口只有兩個,兩個接口差別很微小,只有第三個參數的差別。(如果文件不存在,我們建議使用三參數open,如果文件已經存在則建議使用兩參數open)
3.2 簡單使用open參數
1、打開文件:
這里的umask不會改變系統的umask
2、touch的簡單實現:
3、關閉文件(close),讀(read),寫(write),
綜合使用文件接口:
3.3 語言vs系統
總結:語言上的fopen就是封裝的系統調用接口
3.4 進一步理解文件描述符:
1、文件描述符我們可以簡單的理解為該文件在當前程序操作的所有文件中的序號,一般來說我們打開的文件默認從3開始,因為系統默認打開了三個文件,stdin(0),stdout(1),stderr(2)。
2、字符串以\0結尾是c語言的規定,跟文件無關,所以寫文件時只寫字符長度,如果寫的時候讓長度+1,也就是想將\0也寫入,這個時候系統會默認\0為亂碼
四、內核中的文件
4.1 初步了解
上面我們簡單的介紹了系統中的文件調用,現在我們來理解一下內核中的文件:
1、在文件沒有啟動時它是以內容+屬性的形式存儲于磁盤空間內部
2、文件被打開是進程(task_struct)來打開的,也就是文件加載到內存中(創建struct file)
3、進程被CPU調度時會調用open系統調用
4、在內存中存在很多個struct file的結構體對象,它管理著文件的打開屬性,系統默認會打開3個文件(鍵盤,顯示器,顯示器),在OS層面上,有一個file list將所有結構體鏈接起來,對文件的管理,變成了對鏈表的增刪查改
5、進程也是由一個鏈表鏈接起來管理的(task_struct)
6、進程鏈表管理進程,file鏈表管理文件,OS層面是解耦合的,(一個進程-----多個文件)為了讓兩者相關聯,PCB內部存在一個指針(struct files_struct* files),OS會為每個進程里的該指針創建一個struct files_struct的結構體,其中會存在一個叫做struct file* fd_array[N]的數組(指針數組)
7、上面這個數組中下標對應的位置直接連接文件的結構體
8、文件描述符本質上是數組下標,進程和文件用指針來相關聯
9、OS層面,fd是唯一訪問文件的方式,FILE是C提供的訪問文件的結構體,它會有很多的屬性 --(其中有一個屬性必定對fd做封裝)
4.2 理解一切皆文件:
1、之前我們說過每一個硬件都有一個共同的名字叫做外設,我們如果整體來看的話可以發現所有設備可以擁有相同的屬性類別,但是屬性具體的值可以不一樣,怎么理解呢?例如:設備號,狀態值等,所以我們是可以將所有的外設用一個結構體去描述的,現在我們要訪問鍵盤、網卡、磁盤以及顯示器,對上述設備的訪問的方式一定是不同的,例如鍵盤只能讀,顯示器只能寫,那這個結構體怎么才能將所有設備統一呢?
2、在外設的角度上,所有的設備都其實實現了read和write方法,但上面我們剛說有些外設只需要其中一種,這個不必擔心,需要的方法我們不去實現它即可
3、站在linux操作系統上,對一個文件進行操作之前,我們都會創建一個struct file,這個結構體里會有一系列的調用外設的方法,我們來介紹其中最典型的兩個,就是
int (*read) ();
int (*write) ();
操作系統通過函數指針的形式來訪問外設的操作函數,如此現在我們就不需要關心鍵盤、網卡、顯示器、、、的具體函數實現,我們只需要調用接口即可,這一套機制我們稱為vfs(虛擬文件系統)
4、在struct file中又會提供文件描述符,所以我們對文件的操作轉化到了對文件描述符上的操作,系統調用也是通過對這些函數指針操作的
5、上面介紹的這種上層的struct file與外設的struct device這種技術實際上就是多態的實現原理
文本寫入 vs 二進制寫入
1、我們需要明白顯示器顯示12345,這里到底是一個整數還是5個數字字符?–其實這里顯示器顯示的是5個字符,所以實際上我們編寫的代碼中輸出所有整數類型,系統都會先轉化為字符,然后再在顯示器打印,系統就覺得太麻煩了,每次都需要我們自己轉換,并且我們寫的可能還會出現許多問題,故此系統封裝了一系列函數
2、c語言中的scanf與printf它叫做格式化輸入與輸出,它的作用就是將文件內的內容以固定格式來輸入輸出,其次計算機所謂的文本與二進制其實沒有本質差別,不管我們輸入的是字符,數字,符號,本質上計算機都是以二進制處理的,例如可執行文件,我們要知道可執行文件會被編譯成二進制,我們向二進制文件輸入文件,它會認識嗎?
3、那為什么c、c++還會封裝一系列函數呢?這是因為雖然系統給我們做了一層封裝,但是要是換系統了呢?上面我們都是基于linux系統來談的,封裝的好處就是語言的可移植性
4、為什么我們使用c語言的庫函數在linux或者win下都可以隨便使用?這是因為這些庫函數的代碼其實全部都封裝在庫內(glic)當我們需要調用庫內的函數時,系統會將該函數編譯成當前系統的版本,例如win版,linux版。所以在使用一門語言時,需要先安裝環境,安裝環境就是安裝庫
4.3 IO基本過程
1、input:當我們調用write函數時,我們通過文件描述符找到了文件操作表,又在文件操作表中通過函數指針調用寫的操作,但我們寫的內容并不是直接就寫入硬盤的,而是先寫入文件內核緩沖區,再刷新至硬盤文件空間,刷新是由os自主決定,意識是指緩沖區有多少內容或者多少時間執行一次刷新,由操作系統來決定
2、output:與上面的操作相反,但也是現將硬盤上的文件內容拷貝至緩沖區,然后os自主決定刷新
緩沖區(系統)的存在是因為內存的操作速度太快了,外設的操作速度非常的慢(例如我們要進行IO操作,如果沒有緩沖區,我們每進行一個字符的操作就都要進行一次外設的訪問,效率太低了,但現在我們有了緩沖區,也就是說我們現在可以向緩沖區寫入100或者1000的數據然后進行一次寫入寫出,效率大大的提升了),同時系統也會進行預加載,進一步提升IO地效率
所以write與read本質上是拷貝函數。
4.4 重定向
1、fd的分配規則:進程打開文件,需要給文件分配新的fd(最小的,沒有被使用的),如果系統默認的0,1,2被關閉,系統也會將0,1,2分配出去
1、在上面的代碼中如果沒有關閉1,這個文件的話,會默認打印3,4,5,6在顯示器上,這也符合我們的預期,但當我們關閉1后,本來應該向顯示器打印的內容,卻寫入到了log1.txt文件中,這是為什么呢?
這是因為printf函數默認會向stdout這個文件打印,我們知道printf也是通過文件描述符去操作文件的,但是printf只認1這個文件描述符,但此時1這個文件描述符已經被log1.txt給占用了,所以現在的操作變為了向log1.txt寫入了
所謂的重定向就是系統只認0,1,2,但是現在我們讓特定文件描述符內的內容做出修改,而系統毫不知情
系統的中重定向接口叫做dup2
系統實現重定向的方式非常簡單,0,1,2這三個位置本質上是指針數組,我們現在讓數組內3號位置的指針,拷貝到1號位置,這就是輸出重定向,這一些列操作都必須使用系統調用也就是dup2(oldfd,newfd)
例如輸出重定向就是dup2 (fd,1);