目錄
一、C語言中的文件操作
二、系統文件操作I/O
三、文件描述符fd
1、文件描述符的引入
2、對fd的理解
3、文件描述符的分配規則
四、重定向??
1、重定向的原理
2、重定向的系統調用dup2
五、Linux下一切皆文件
一、C語言中的文件操作
1、打開和關閉?
在C語言的文件操作中,我們要對一個文件進行寫入和讀寫的前提是打開文件。我們使用fopen來打開文件,打開失敗將會返回NULL ,而打開成功則返回文件的指針 FILE*。最后要進行的操作就是關閉(fclose)文件
函數原型:FILE *fopen(const char *path, char *mode)。path為文件名(也可以是文件路徑),mode為打開方式,它們都是字符串。
int fclose(FILE *fp)。
下面我們來看一看下面的代碼:
上面的代碼中,我打開了一個文件log.txt。但是我的當前目錄下并沒有這個文件。?
這個文件并不存在,但是我們要使用,那么fopen會在當前路徑下給我們創建出這個文件。那么這個當前路徑是什么呢??
簡單來說,當前路徑:一個進程運行起來的時候,每個進程都會去記錄自己當前所處的工作路徑。所以當前路徑也就是當前進程的工作路徑。
有了這個概念,我們就能理解了:test.c形成的可執行程序在運行后,會成為一個進程,該進程會通過調用系統接口幫助我們創建文件,因此新文件所在的路徑就是當前進程的工作路徑。
第一個紅色方框就是當前進程的工作路徑,exe就是當前的可執行文件。第二個紅色方框就是表示執行的是進程工作路徑下的那個可執行程序。
注:單純以w方式打開文件,會自動清空文件原有的數據。r+(讀寫)代表文件不存在則出錯,w+(讀寫)代表文件不存在則創建。(帶有+的表示讀寫)。a代表向文件中追加內容。
2、讀寫文件
我們知道在C語言中,我們可以通過fgets和fputs以字符串形式進行讀寫,也可以通過fprint和fscanf進行格式化讀寫。(下面的函數在C語言中我們已經學過了,這里就不一一演示了)。
int fputs (const char * str, FILE * stream )
char * fgets (char * str, int num, FILE * stream )
int fprintf (FILE * stream, const char * format, ... )
int fscanf (FILE * stream, const char * format, ... )
二、系統文件操作I/O
操作文件,除了上述C接口(當然,C++也有接口,其他語言也有),我們還可以采用系統接口來進行文件訪問。其實真正能夠直接訪問文件的只有操作系統,而各種編程語言能夠訪問文件的函數的本質都是去調用了操作系統提供的各種系統接口。
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);
pathname:打開文件名
flags : 標志位。(打開文件時,可以傳入多個參數選項,用一個或者多個常量進行“或”運算,構成 flags)?
O_RDONLY:只讀打開? ? ? ? O_WRONLY : 只寫打開? ? ? ? ?O_RDWR : 讀寫打開
O_CREAT : 若文件不存在,則創建它。需要使用mode選項,來指明新文件的訪問權限
O_APPEND : 追加寫
O_TRUNC:打開時,清空文件內容。
返回值:成功:新打開的文件的文件描述符? ? ? ? ?失敗:-1
~ 使用比特位傳遞選項
但是,flags是一個整型,他只表示一個參數,那么我們是怎么通過flags傳入多個參數呢?這里我們使用了一種數據結構叫做比特位:一個整數有32個比特位,所以我們可以通過比特位來傳遞選項。
下面我們通過一個例子,來看看是怎么實現的。
因此,我們可以使用 | (或)來幫助我們傳遞多個參數,以此實現不同的功能。?
mode參數
如果你使用O_CREAT參數創建一個新的文件,那么你還可以通過第三個參數mode來設置該文件的權限。
2、close
//所在頭文件
#include <unistd.h>//原型
int close(int fd);
3、read和write
文件打開后,我們就業對文件進行讀取或者寫入了。
write:寫入
#include <unistd.h>ssize_t write(int fd, const void *buf, size_t count);
fd:要寫入的文件? ? ? ? buf:要寫入的內容? ? ? ?count:所寫內容的大小。?
read:讀取
//頭文件
#include <unistd.h>ssize_t read(int fd, void *buf, size_t count);
fd:要讀取的文件? ?buf:存放讀取內容的數組? ? count:讀取的內容大小?
三、文件描述符fd
我們通過上面的學習,發現,open的返回值是一個整型,其實這個整型其實就是代表文件描述符fd。
1、文件描述符的引入
我們來看看下面的代碼
運行結果如下
我們知道fd是一個整型了,那么為什么是從3開始的呢?那0,1,2跑哪里去了呢?
在C語言階段,我們知道在程序運行時,操作系統會默認打開三個標準輸入輸出流:標準輸入,標準輸出,標準錯誤。對應到C語言當中就是stdin、stdout以及stderr。在C++中則是cin、cout、cerr,而在其他的語言中也有相應的輸入輸出流。
我們知道C語言中的stdin、stdout以及stderr這三個家伙實際上都是FILE*類型的,并不是int類型。因為FILE*是一個結構體指針,是C語言進行了封裝的,是為了方便用戶使用。而在操作系統層面,比如在Linux下,只認fd,而且只有操作系統才能直接訪問文件,那么各種語言為了既方便用戶使用,又要遵循操作系統的規則,必定在FILE結構體里面封裝了fd,這樣才能在系統層面去使用文件。
所有各種語言都有封裝自己的輸入輸出流,實際上這種特性并不是某種語言所特有的,而是由操作系統所支持的。
那么,說到這里,我們已經有一點感覺了,0,1,2哪去了呢?會不會是分別代表著標準輸入,標準輸出、標準錯誤呢?答案是肯定的。在Linux下0,1,2就是表示這個意思。
那么我們也就能夠理解了,0,1,2所表示的文件操作系統已經幫我們打開了!
下面通過代碼來驗證一下:
所以說,在系統層面,我們只能用0,1,2,3等整數來確定一個文件。?
2、對fd的理解
進程想要訪問文件,那么要先打開文件。而文件是由進程運行時打開的,一個進程可以打開多個文件,而系統當中又存在大量進程,也就是說,在系統中任何時刻都可能存在大量已經打開的文件。所以操作系統務必要對這些已經打開的文件進行管理。那么怎么進行管理呢?先描述,再組織!
操作系統會為每個已經打開的文件創建各自的struct file結構體(其中包含了該文件幾乎全部的內容),然后將這些結構體以雙鏈表的形式連接起來,之后操作系統對文件的管理也就變成了對這張雙鏈表的增刪查改等操作。
所以,為了區分被打開的文件各自屬于那個進程,操作系統必定會將進程與文件之間建立某種聯系。
那么進程和文件是怎么建立聯系的呢?
首先,我們來想一想fd為什么是連續的整數呢?我們學過的知識中有什么是和連續的整數有關且從0開始的呢?我們很容易就可以想到一個——數組的下標!沒錯,fd就是數組的下標!那么是什么數組的下標呢?
我們知道,當一個程序運行起來時,操作系統會將該程序的代碼和數據加載到內存,然后為其創建對應的task_struct,task_struct中的一個指針變量指向該進程的mm_struct(進程地址空間),通過頁表建立虛擬內存和物理內存之間的映射關系。
而task_struct當中有一個指針,該指針指向一個名為files_struct的結構體,在該結構體當中就有一個名為fd_array的指針數組,該數組的下標就是我們所謂的文件描述符。
當進程打開一個文件時,該文件從磁盤當中加載到內存,形成對應的struct file,OS將該struct file連入文件雙鏈表,并將該結構體的首地址填入到fd_array數組當中下標為3的位置,使得fd_array數組中下標為3的指針指向該struct file,最后返回該文件的 fd 給進程。
所以,我們只要有某一文件的文件描述符,就可以找到該文件相關的內容,進而對文件進行一系列輸入輸出操作。?
3、文件描述符的分配規則
一般情況下,操作系統默認為進程打開標準輸入,輸出,錯誤,分別對應fd為0,1,2。所以0,1,2位置已經被占用了,所以只能從3開始進行分配。之后打開的文件按順序fd為3,4,5 ......
若我們在打開新的文件前,先關閉文件描述符為0的文件,此后文件描述符的分配又會是怎樣的呢?
結果如下:
可以看到,新打開的文件獲取到的文件描述符變成了0。
我們再多打開幾個文件:
結果如下:第一個打開的文件獲取到的文件描述符變成了0,而之后打開文件獲取到的文件描述符還是從3開始依次遞增的。
所以:文件描述符是從最小但是沒有被使用的fd_array數組下標開始進行分配的。
四、重定向??
1、重定向的原理
~ 輸出重定向
運行結果如下:
根據運行結果,我們發現printf函數本應該將結果輸出到顯示器(標準輸出)讓我們看見,但是結果并沒有在顯示器上顯示出來,但是結果卻被打印到了 log.txt 里面。 這就是我們所說的輸出重定向。
輸出重定向:將我們本應該輸出到一個文件的數據重定向輸出到另一個文件中。
具體原理如下圖:
close的本質其實是將進程和文件的關聯關系解除。close(1)就是將1位置的指針設成NULL,但是語言層的 stdout(或者cout等)指向的是一個struct FILE類型的結構體,結構體中存儲文件描述符的變量的值仍然是1。
接著創建了一個新的文件log.txt,從0開始遍歷數組,發現1位置為空,所以將1位置的指針指向log.txt,這就建立了新的關聯關系。
所以當你使用C語言的printf向stdout寫入的時候,stdout的fd仍然是1,但是底層的1位置已經指向log.txt了,所以就寫到了log.txt里面。
~ 追加重定向
追加重定向就是在輸出重定向的基礎上,將“清空”的參數改成“追加”的參數。
2、重定向的系統調用dup2
上面是我們根據文件描述符的分配規則,來進行重定向的。下面我們使用系統調用接口dup2來幫助我們實現重定向。
功能: dup2會將fd_array[oldfd]的內容拷貝到fd_array[newfd]當中,如果有必要的話我們需要先使用關閉文件描述符為newfd的文件。?
返回值: dup2如果調用成功,返回newfd,否則返回-1。
我們使用下面的代碼舉例:
五、Linux下一切皆文件
廣義上的文件:站在操作系統Linux的角度,能夠被input讀取,或者能夠被output寫出的設備就叫做文件。
所以,顯示器、鍵盤、網卡、顯卡、磁盤等,幾乎所有的外設都可以稱為文件。
在Linux下,我們將文件分為:1、內存文件(文件已經打開,已經加載到了內存中)2、磁盤文件(文件還沒有被打開,沒有被加載到內存中)。
那么Linux下一切皆文件具體是怎么體現的呢?
首先,Linux內核是用C語言寫的。每個外設的硬件結構是不一樣的,那么我們通過操作系統訪問外設的方式肯定是不一樣的。但是,操作系統僅僅通過提供四個系統調用(open,close,write,read),就可以幫助用戶訪問顯示器、磁盤等文件。那么看似相同的方法是怎么訪問不同的硬件設備的呢?
我們在學習了C++或者Java等能夠面向對象的編程語言后,我們知道可以使用類來描述一個文件,然后使用多態達到使用相同接口而產生不同效果,所以這些語言可以做到上面的事。可是,Linux內核是使用C語言寫的,C語言可是沒有面向對象的特點的,也沒有多態的概念,那么它是怎么做到的呢?
任何一個被打開的文件的有自己的結構體對象struct file{ //各種文件的屬性 },不同的文件對應的讀寫方法不一樣,struct file對象里面可以有很多的(*readp)()、(*writep)()函數指針,通過函數指針指向具體的讀寫方法。
這樣,用戶就可以不用關心底層差別,統一使用文件的接口方式進行文件操作。
所以,在Linux下,一切皆文件!?