昨天,面百度,其中有一個問題是:open的返回值是什麼?從你調用open函數到Linux下出現了一個文件,這中間發生了什麼?
第二個問題,當時就懵逼了,後來仔細想想,發現自己其實看過,其實講文件描述符相關~爲此,自己又重新看了APUE第二章,同時對應看了CSAPP第10章,因此做一個總結~
IO是什麼?
i/o是input,output的縮寫,也就是輸入輸出的意思。在計算機中,I/O就是在主存和外部設備(如磁盤,終端,網絡)之間拷貝數據的過程。
輸入:從IO設備拷貝數據到主存 IO->主存
輸出:從主存拷貝數據到IO設備 主存->IO
在Unix/Linux系統中,我們可以使用由內核提供的系統調用函數實現IO,比如read,write。。。(由於Unix和Linux在很多地方都相似,我們下面統稱Linux)
在Linux中,一個文件就是一個字節序列。而Linux有一個設計思想:Linux下一切皆文件。
因此,我們上述所講的IO設備(如磁盤,終端,網絡)都可以看作是文件,因此所有的輸入輸出都可以看作是對文件的讀和寫,這樣的一層抽象保證了所有的輸入輸出都能以一種統一和一致的方式來執行。
那麼它是如何實現的呢?概括來說,它是通過一個叫文件描述符的東西來進行操作的,具體如下:
相關函數接口
一般來說,打開文件,讀寫文件,關閉文件,對文件操作等,只需要用到5個函數,open,read,write,lseek,close,我們也可以通過man手冊來進行查閱~
ps:creat函數也可以用於創建文件,而open是打開一個已有文件或者創建一個新文件
ssize_t read(int fd, void *buf, size_t count);
ssize_t write(int fd, const void *buf, size_t count);
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);
off_t lseek(int fd, off_t offset, int whence);
int close(int fd);
函數聲明如上,具體函數是如何使用,CSAPP,APUE都有着詳細的說明,本文不再贅述,我們重點談談上面所講的文件描述符。
當我們利用open,creat函數創建一個文件成功,它們會返回一個整型數字,這就是文件描述符fd,後面所有的讀寫等操作都必須用到這個fd,我們看上面的函數,除了創建函數等,其他函數第一個參數都是fd也印證了我們所說的這一點。
共享文件
Linux系統支持在不同進程之間共享打開的文件,內核維護了3種數據結構表示打開的文件。
- 文件描述符表
- 文件表
- v-node表
下面我們重點來講一講這三者的概念以及彼此的關係,這對我們理解文件IO,共享有着關鍵性的作用。
文件描述符表
每個進程都具有它獨立的文件描述符表,每個描述符佔有一項,與每個文件描述符相關聯的是:
- 文件描述符標誌
- 指向文件表的指針
其中,0,1,2分別對應標準輸入,標準輸出,標準錯誤,因此,從標準輸入讀可以使用read(0, buf, sizeof(buf))
文件表
內核爲每個打開文件都維護一個文件表,所有進程都共享這張表。它的表項有很多,包括文件位置,引用計數(即當前有多少個指向該表的描述表項數),以及一個指向v-node中對應表項的指針。
關閉一個描述符(close(fd))會減少對應的文件表表項的引用計數,直到這個引用計數爲0,內核纔會刪除這個文件件表項。
注意到:它的表項有很多,對於共享文件,我們主要關注引用計數,實際上APUE寫的更爲詳細:
在此,我們忽略這些。
v-node表
每個打開文件(設備也是文件)都有一個v-node結構,這個v-node結點包含了文件類型等等。對於大多數文件,v結點還包含了該文件的i-node結點(索引結點),這些信息是打開文件時候從磁盤上讀入內存的。
和文件表一樣,所有進程共享這張v-node表。
三者關係
CSAPP給出幾道題幫我們理解這些概念:
其中10.2題就對應第二張圖,我們對同一個filename文件“foobar.txt”打開兩次,就有兩個文件描述符fd1,fd2,但是因爲每個fd都有自己的文件表表項,因此對於foobar.txt都有它自己文件位置,所以放到
fd2讀完,最後結果是c = f。
而10.3題則對應第三張圖,子進程會繼承父進程的描述符表,同時所有進程共享同一個打開文件表,因此父子進程都指向同一個打開文件表表項:
當子進程讀取一個字符,文件偏移位置+1,父進程會讀取第二個字節,因此結果是 c = o
重定向
在這篇理解重定向之dup,dup2博客裏面我總結了重定向,同時給出如何實現重定向以及恢復重定向。
但是,我當時理解不夠,所以有些地方不太準,因此,我在這兒對於某些不太準備的地方做出解釋:
以dup2(oldfd, newfd)爲例,它是將oldfd拷貝到newfd中,如果newfd打開就會先關閉newfd再拷貝fd。實際上呢,就是將newfd所對應的指針指向oldfd的指向的地方。
以dup(4,1)爲例:
與C語言中標準IO的區別
ANSI也定義一組高級IO函數,稱爲標準IO庫,也可以用來讀寫,比如printf,scanf,fread,fwrite等,那麼它們有什麼區別?該如何選擇?
最主要的區別就是C語言讀取文件自帶一個緩衝區,而read,write這些是不帶緩衝區的,實際上包括fread,fwrite這些c語言函數必然調用read這些系統調用。
標準C將文件抽象爲流,所謂的流就是指向FILE*類型的指針,和unix類似,它也有三個流對應標準輸入輸出錯誤。
類似爲FILE的流是對文件描述符和流緩衝區的抽象,所謂的緩衝區目的實際上就是儘可能少使用開銷較大的系統調用,比如我們想用getc返回讀取文件的下一個字符,我們可以只調用一次read來填充緩衝區,因此讀取字符只需要到緩衝區內讀,而非系統調用。
如何選擇
實際上大部分時候,我們會選擇標準IO,因爲它自帶緩衝區,不需要頻繁使用系統調用(這是有代價的),從而提高效率,當然在某些時候需要用到fflush函數刷新緩衝區。
然而對於網絡這樣是不行的,前面我們說過Linux下一切皆文件,因此對於網絡的IO,我們抽象爲一種稱爲套接字的文件類型,和其他文件一樣,對套接字文件操作也是基於文件描述符的,應用進程通過對套接字描述符讀寫與其他計算機上的進程通信。
然而不幸的是,標準IO和網絡文件有很多不兼容性,因此我們更多的會去選擇使用read,write這種低級IO。