常識:
1 文件包括屬性和內容
2 文件有打開和未打開文件,
3 本文先討論誰打開的文件,以及如何管理已經打開的文件
一 回憶c接口
1?fopen
??????? 我們在test.c里面用一下fopen函數,不存在打開的文件會默認創建,那為什么默認新建在當前目錄下,是因為cwd,而不是PWD,我們知道PWD是環境變量,如果我們去到其它工作目錄,PWD會變,但是CWD不變,此時再運行一下test.c,此時文件還是會創建在CWD存的路徑下,而不隨著環境變量改變而變化。
2 fwrite
????????fwrite函數可以往文件寫數據,值得一提的是fwrite的size參數,這個是要寫入的字節數,我們大部分時候都是往文件寫個字符串,例如"hello linux",有時候size傳strlen("hello linux"),有時候傳sizeof(hello linux),我們會發現sizeof多寫入的\0被文件識別為亂碼,這說明一個問題,字符串結尾有\0這個字符是c語言的規定,文件是不認這個\0是字符的。
二?文件操作和系統調用
????????fwrite庫函數一定封裝了系統調用,因為文件是在磁盤上的,fwirte要往硬件寫數據,那不就相當于訪問硬件的資源,由于操作系統不相信用戶,所以fwrite一定不是直接把數據弄給硬件,而是通過操作系統的接口將數據傳給硬件。接下里就來認識認識幾個系統調用。
????????1?認識open
??????? 從fopen的名字上看,我們也知道fopen封裝的是open,接下來就看看open的參數和返回值。man 2 open就可從手冊調出open的信息。
顯然,函數1是函數2的的子集合,我們只要說清楚了函數2,函數1的使用也就明白了。
參數1 文件名,不帶路徑應該是默認在當前路徑下找。
參數2 flags是什么呢?
????????
????????我們要給flags傳的是上面圖片中的宏,這些宏都表示一個一個的整數,接下來就介紹介紹這些宏的意義,以及如何使用。O_RDONLY表示open以只讀方式打開,O_WRONLY表示open以寫方式打開, ?而O_RDWR則表示以讀寫方式打開,這三個宏最好只出現一個,至于其它的宏O_APPEND,這個是表示向文件寫時以追加的方式去寫。
使用:
| :按位或?這個使用又是啥意思呢??
????????舉個例子,O_RDONLY可能是用0001來表示,O_WRONLY則是0010,O_RDWR則是0100來表示,同理得,O_APPEND就要用1000來表示,只用了一個比特位就能唯一表示一個宏,這樣的設計非常巧妙,首先我們可能會傳多個宏,設計者沒有用可模板參數來接收,而是只用一個整型,因為我們可以對傳的參數進行按位或,這樣只要對按位或的結果一分析,就知道你打開文件是要讀還是寫了,所以flag的類型就只是個樸素的int,可其實里面門道也不少。
參數3 權限初始化,因為我們open發現打開文件不存在,要創建文件,此時文件的權限是要指定的,不然會給一個初始值,但這個初始值如下圖。
? ? ? ?2?認識write和read
write和read就簡單多了。
write:往fd這個文件描述符對應的文件寫入buf數組中的元素,字節數為count。
read:從fd這個文件描述符對應的文件讀取count個字節的數據,寫入buf數組中。
????????現在我們就能解釋:現在我們就可以解釋為什么fwrite就傳一個"w"可以實現清空寫,以及創建文件,"w+"為什么能實現追加寫,就是因為在底層封裝了這些宏,然后操作系統識別到了,在調用系統調用的時候傳了給flag傳了不同的宏,至于返回值會在下面訪問文件的本質中提及。
三?訪問文件的本質
????????open的返回值-文件描述符,這個文件描述符怎么是int類型呢,我fopen用的可是FILE*,這兩者有什么關系嗎?
? ? ? ? 先來看看操作系統如何管理文件,首先操作系統打開的文件有很多,這些必然會被操作系統管理,操作系統管理文件,就像管理進程一樣,只要用一個file結構體描述文件即可,根本就不需要管文件內容,這樣在系統內核處,就又增加了一個數據結構,將所有的file結構體管理起來。誒,不對啊,文件不是進程打開的嗎,那不是應該進程管理嗎,如果僅僅是被進程管理,那如果進程出異常了,被kill了,這些文件不就丟失了?所以系統必須也要管理。如下圖:
? ? ? ? 好吧,既然上面是系統管理文件的方式,那進程呢怎么管理呢?
????????所以會有一個files_struct(這個和FILE*可不相同)來管理,這個結構體內部會有一個數組,數組每元素就是一個文件指針,而文件描述符就是數組下標,所以說一個文件描述符一定對應一個文件,當然多個文件描述符可以對應同一個文件(file結構體內部肯定是會有引用計數記錄的)。也就是說底層進程是通過下標來找文件指針,從而找到文件的,所以FILE*內部一定封裝了文件描述符,不然系統調用找不到文件。
????????我們發現所有語言寫的代碼運行起來都要默認打開三個文件,stdin,stdout,stderror,因為系統就要這樣做,系統設計這就認為開機后天然需要鍵盤,顯示器文件,所以就要打開,而所有語言寫的代碼不管寫了啥,形成進程后,就會把已經打開的文件填到files_struct內,所以程序一運行,該數組內就有了三個元素。
????????既然stderror和stdout都是指向顯示器文件,它們的區別是什么,我想也就是其內部的封裝的文件描述符不同,當我們close(1),printf就用不了了,但是perror還可以向顯示器打印。
四?重定向
? ? ? ? 周邊小知識:write寫的時候如果不close,一直寫,那就會一直往后寫,而不是覆蓋,open以追加方式寫,指的是第一次write寫的時候從哪開始寫。
1 文件描述符的分配規則
? ? ? ? 自數組開頭遍歷,在數組中最先遇到的空格位置的下標,就是被分配的文件描述符。先前已經說了系統會打開兩個文件(說打開三個是方便理解),這兩個文件是鍵盤,顯示器,而顯示器被打開了兩次,在files_struct內的數組就會有三個文件描述符,其中0存的是鍵盤文件指針,1,2存的都是顯示器文件指針,我前面說多個文件描述符可以對應同一個文件,這就是實實在在的例子。
2?手動實現重定向
????????輸出重定向就是:printf本來是要寫給顯示器的,但是由于close(1),然后又打開了myfile.txt后占用了一號位(這就驗證了文件描述符的分配規則)。
1 #include<stdio.h>2 #include<stdlib.h>3 #include<unistd.h>4 #include<string.h>5 #include <sys/types.h>6 #include <sys/stat.h>7 #include <fcntl.h>8 int main()9 {10 printf("我的id:%d\n",getpid());11 close(1);
W> 12 int fd = open("myfile.txt", O_CREAT|O_WRONLY,0666);13 printf("我的id:%d\n",getpid()); 14 return 0;15 }~
所以兩句printf就只有一句輸出,因為第二句printf就變成往myfile.txt輸出了。
????????這也側面說明在操作系統看來,往顯示器寫和普通的文件沒有區別,而printf內部用的stdout一定是封裝了1號文件描述符,這是編碼定死的,printf也不管這個標識符對應的文件變了沒,拿到就寫,就有了輸出重定向這種烏龍。
3 系統調用dup2
? ? ? ? 雖然可以先close,再打開文件實現重定向,但這樣的代碼還是不如直接調用系統調用那么優雅,直接上代碼,看看使用。結果一致。
問題1?oldfd和newfd誰覆蓋誰,顯然從先前的例子來看,是oldfd上的內容覆蓋到newfd的內容,本質是數組對應下標上元素,也就是文件指針的拷貝。
讀者可以嘗試進行輸入重定向
同理也就是對0號位置下標做手腳