Linux檔案IO及相關概念

2020-08-10 09:58:46

其餘相關內容可參考個人部落格

檔案描述符

Linux系統將所有裝置都當作檔案來處理,而Linux用檔案描述符來標識每個檔案物件。

檔案描述符在形式上是一個非負整數。實際上,它是一個索引值,指向內核爲每一個進程所維護的該進程開啓檔案的記錄表。當程式開啓一個現有檔案或者建立一個新檔案時,內核向進程返回一個檔案描述符。

標準檔案描述符

每個進程都有一張檔案描述符的表,進程剛被建立時,標準輸入、標準輸出、標準錯誤輸出裝置檔案被開啓,對應的檔案描述符0、1、2 記錄在表中。在進程中開啓其他檔案時,系統會返迴檔案描述符表中最小可用的檔案描述符,並將此檔案描述符記錄在表中

檔案描述符 縮寫 描述
0 STDIN 標準輸入
1 STDOUT 標準輸出
2 STDERR 標準錯誤

檔案描述符的複製

dup

int dup(int oldfd);
功能:複製舊的檔案描述符,自動分配一個可用的最小的檔案描述符
成功返回新的檔案描述符,失敗返回-1

函數說明:
它們參照相同的開啓檔案描述,因此共用檔案偏移量和檔案狀態標誌。 例如,如果在舊的檔案描述符之上使用lseek修改了檔案偏移,則新的也將更改。

關於檔案描述符與開啓檔案、檔案的關係在後續文章將會介紹,閱讀後能更容易理解上述說明

dup2

int dup2(int oldfd, int newfd);
功能:複製舊的檔案描述符,自動分配一個可用的最小的檔案描述符
成功返回新的檔案描述符,失敗返回-1

函數說明:

  1. dup2dup函數的升級版本,可以指定生成的檔案描述符(必須小於1024),如果這個指定的描述符已經打開了,那麼會原子地關閉和複製。
  2. oldfd是無效的,則newfd不會被關閉
  3. oldfd是無效的,且newfdoldfd相等,則dup2函數什麼也不幹 不乾,直接返回newfd

修改標準檔案描述符

標準檔案描述符0,1,2一旦被改變了就無法使用了,所以在重定向之前需要把他們三個儲存起來:

int new_stdout = dup(1);//在重定向之前儲存起來
dup2(new_stdout,1);//這樣就可以變回來了

寫例子的時候還有個小問題,我們重定向之後,printf到檔案當中,然後把stdout變回來,再printf一句話,這個時候可以看到終端上兩句話都列印出來了,那是因爲重定向輸出到檔案的時候緩衝區是全緩衝的,所以數據還在緩衝區當中,沒有寫到檔案當中呢,爲了避免這類問題,可以選擇使用系統呼叫(無緩衝區)

關於緩衝區的問題可繼續閱讀本文後續章節

exec後的檔案描述符

無論是fork還是system出子進程,如果父進程裡在open某個檔案後(包括socket fd)沒有設定FD_CLOEXEC標誌,就會引起各種不可預料的問題,特別是socket的fd本身又包括了本機ip,埠號等資訊資源,如果該socket fd被子進程繼承並佔用,或者未關閉,就會導致新的父進程重新啓動時不能正常使用這些網路埠,嚴重的就是裝置掉線。

開啓檔案後預設未將該標誌位置位,即預設在exec後不關閉檔案描述符,可進行如下設定:

int flags;
flags = fcntl(fd, F_GETFD);//獲得標誌
flags |= FD_CLOEXEC; //開啓標誌位
flags &= ~FD_CLOEXEC; //關閉標誌位
fcntl(fd, F_SETFD, flags);//設定標誌

其實open函數的flag提供了O_CLOEXEC標誌位,可直接設定(僅Linux 2.6.23後支援)。

檔案描述符和開啓檔案的關係

  1. 每個檔案描述符都指向一個開啓的檔案相對應
  2. 不同的檔案描述符可能指向同一個開啓的檔案
  3. 相同的檔案可能被不同的進程開啓,也可以在被同一個進程開啓多次

具體情況要具體分析,需要檢視由內核維護的3個數據結構:

  1. **進程級的檔案描述符表:**進程級的列表,也就是使用者區的一部分,進程每開啓一個檔案就會新建一個檔案描述符,同時只能通過檔案描述符的函數存取
  2. **系統級的開啓檔案表:**系統級的列表,對當前系統的所有進程都共用.
  3. **檔案系統的i-node表:**inode索引節點表。
檔案描述符表 開啓檔案表 i-node表
記錄內容 檔案描述符操作標誌(目前內核僅定義了一個close-on-exec標誌) 當前檔案偏移量 檔案型別
對開啓檔案控制代碼的參照 開啓檔案時使用的狀態標識(open的flags參數) 檔案鎖
檔案存取模式(open時設定的O_RDONLY等標誌) 檔案擁有者的UID,GID
對該檔案i-node物件的參照 檔案的時間戳:ctime,mtime,atime
檔案型別(例如:常規檔案、通訊端或FIFO) 鏈接數,即有多少檔名指向這個inode
存取許可權 讀寫執行許可權
一個指針,指向該檔案所持有的鎖列表 檔案數據block的位置
檔案的各種屬性,包括大小以及各種時間戳 檔案的各種屬性,包括大小以及各種時間戳
與信號驅動相關的設定

範例如下圖所示:

  • 在進程A中,檔案描述符1和30都指向了同一個開啓的檔案控制代碼(標號23)。這可能是通過呼叫dup、dup2
  • 進程A的檔案描述符2和進程B的檔案描述符2都指向了同一個開啓的檔案控制代碼(標號73)。這種情形可能是在呼叫fork後出現的
  • 進程A的描述符0和進程B的描述符3分別指向不同的開啓檔案控制代碼,但這些控制代碼均指向i-node表的相同條目(1976),發生這種情況是因爲每個進程各自對同一個檔案發起了open呼叫,同一個進程兩次開啓同一個檔案,也會發生類似情況

檔案描述符限制

系統級限制

檢視方式:

sysctl -a | grep -i file-max --color
cat /proc/sys/fs/file-max

sysctl命令和proc檔案系統中檢視到的數值是一樣的,這屬於系統級限制,它是限制所有使用者開啓檔案描述符的總和

使用者級限制

每個進程的最大檔案描述符限制:

ulimit -n

修改方式

  1. 修改使用者級限制:

    ulimit -SHn 10240
    

    以上的修改只對當前對談起作用,是臨時性的,如果需要永久修改,則要修改/etc/security/limits.conf檔案:

    * soft nofile 100001
    * hard nofile 100002
    

    soft 指的是當前系統生效的設定值,hard 表明系統中所能設定的最大值

  2. 修改系統級限制:

    [root@VM-0-4-centos ~]# cat /proc/sys/fs/file-max
    350000
    [root@VM-0-4-centos ~]# echo 50000 > /proc/sys/fs/file-max 
    [root@VM-0-4-centos ~]# cat /proc/sys/fs/file-max
    50000
    [root@VM-0-4-centos ~]# sysctl -a | grep -i file-max --color
    fs.file-max = 50000
    
    

    以上是臨時修改,重新啓動後失效,永久修改如下

    fs.file-max=400000新增到/etc/sysctl.conf中,使用sysctl -p即可

緩衝區

出於速度和效率考慮,系統IO呼叫和標準 C語言庫的IO函數均會對數據進行緩衝,接下來將分類介紹:

系統IO呼叫緩衝

readwrite在操作磁碟檔案的時候不會直接發起磁碟存取,而是在使用者空間緩衝區和內核緩衝區快取記憶體之間複製數據。

write(fd,"abc",3);

上面的語句將3個位元組的數據從使用者空間記憶體傳遞到內核空間的緩衝區中,隨後write返回,在後續的某個時刻,內核會將其緩衝區中的數據寫入(重新整理至)磁碟,在此期間如果有另一進程存取這幾個位元組,直接從快取記憶體中提供這些數據。對輸入而言同理。
這一設計不需要readwrite等待磁碟操作,也減少了內核進行磁碟傳輸的次數。例如:讓磁碟寫1000次,每次寫入一個位元組,還是一次寫入1000個位元組,內核存取磁碟的次數都是相同的,因爲有緩衝區的存在,但是我們更趨向於後者,因爲只有一次系統呼叫,所以這部分是程式設計師需要思考的,這部分的緩衝也就是下面 下麪提到的stdio庫的緩衝了。

簡單來說,就是在write系統呼叫和實際的磁碟之間還有一層由內核維護的緩衝。

stdio庫的緩衝

在操作磁碟檔案的時候,雖然有內核維護的緩衝來減少存取磁碟的次數以節省開銷,但是還有一部分開銷是由系統呼叫產生的,也就是程式中確定每次write或者read多少個位元組,而stdio庫的緩衝就是幫程式設計師幹這件事的,分爲以下三類:

  1. 無緩衝
    每個stdio庫函數立即呼叫write或者read

  2. 行緩衝
    只帶終端裝置的流預設爲這一緩衝型別。對於輸出流,在輸出一個換行符(除非緩衝區已經填滿)前將緩衝數據,遇到換行符會重新整理緩衝區。對於輸入流,每次讀取一行數據

  3. 全緩衝
    單次讀寫數據(通過write和read)的大小和緩衝區相同,只帶磁碟的流預設採用此模式。

手動重新整理stdio緩衝區

int fflush(FILE *stream);
  1. 使用該庫函數強制將stdio輸出流中的數據重新整理到內核緩衝區中
  2. 應用於輸入流時,這將丟棄已緩衝的輸入數據。當程式下一次嘗試從流中讀取數據時,將重新裝載緩衝區
  3. streamNULL,則將重新整理所有的輸出緩衝區
  4. 當關閉流時,自動重新整理緩衝區

函數

open

int open(const char *pathname, int flags);
int open(const char *pathname, int flags, mode_t mode);
功能:開啓pathname所標識的檔案,並返迴檔案描述符,flags可以指定檔案的開啓方式,mode指定了存取許可權,如果flags中沒有建立檔案的標誌,mode可以忽略

flag取值:

標誌 用途
O_RDONLY 以只讀方式開啓
O_WRONLY 以只寫方式開啓
O_RDWR 以讀寫方式開啓
O_CLOEXEC 設定close-on-exec標誌,預設關閉,即exec後檔案描述符不關閉
O_CREAT 若檔案不存在則建立,需要指定mode
O_EXCL 結合O_CREAT標誌使用,專門用於建立檔案(若檔案已存在,則直接返回錯誤)
O_NONBLOCK 以非阻塞方式開啓
O_APPEND 總在檔案尾追加數據(若多個進程同時對同一檔案追加數據,可能導致檔案損壞)
O_TRUNC 截斷已有檔案,使其長度爲0
O_SYNC 以同步方式寫入檔案
O_ASYNC 當IO操作可行時,產生信號通知進程(此特性僅適用終端、僞終端、socket和管道)
O_DSYNC 提供同步的IO數據完整性,即write返回後,數據均已輸出到硬體
O_DIRECT 無緩衝的輸入輸出
O_DIRECTORY 如果pathname不是目錄,則失敗
O_LARGEFILE 在32位元系統中使用該標誌開啓大檔案
O_NOATIME 呼叫read時不修改檔案最近存取時間
O_NOCTTY 不要讓pathname(所指向的終端裝置)成爲控制終端
O_NOFOLLOW 對符號鏈接不予解除參照

補充:

  1. O_DIRECT和O_SYNC的區別

    O_DIRECT:繞過內核的頁面快取將數據寫入裝置,但是裝置本身也存在快取所以並不能保證數據就一定固化到磁碟上
    O_SYNC:檔案數據和所有檔案元數據同步寫入磁碟

  2. O_DSYNC、O_RSYNC、O_SYNC的區別

    • Linux中無O_RSYNC,glibc定義O_RSYNC具有與O_SYNC相同的值
    • 在寫操作中,O_DSYNC和O_SYNC均保證數據同步更新到檔案中,O_DSYNC將僅保證重新整理對檔案長度元數據的更新(而O_SYNC也重新整理最後的修改時間戳記元數據)

read

ssize_t read(int fd, void *buf, size_t count);
功能:從fd檔案中讀取至多count位元組的數據並儲存到buf中。
返回值爲實際讀取到的位元組數,如再無位元組可讀(例如讀到檔案結尾符EOF時),返回值爲0

write

ssize_t write(int fd, const void *buf, size_t count);
功能:從buf中讀取多達count位元組的數據寫入fd指代的已開啓的檔案中
返回值爲實際寫入檔案中的位元組數,有可能小於count

close

int close(int fd);
功能:釋放檔案描述符fd及相關的內核資源
成功返回0,失敗返回-1

lseek

off_t lseek(int fd, off_t offset, int whence);
功能:改變檔案偏移量
名稱 說明
參數 fd 檔案描述符
offset 指定了一個以位元組爲單位的數值
whence 表示參照哪個基點來解釋offset,取值如下:
SEEK_SET 檔案開頭
SEEK_CUR 當前偏移量
SEEK_END 檔案末尾
返回值 off_t 成功返回距檔案開頭的偏移量,失敗返回-1

lseek不適用於所有型別的檔案,不能用於如管道、FIFO、socket和終端

檔案空洞

如果程式的檔案偏移量已經跨越了檔案結尾,然後在執行I/O操作,將會發生read呼叫返回0,表示檔案結尾,write可以正常寫入數據。從檔案結尾到重新用write寫入數據的這段空間被稱爲檔案空洞,從程式設計角度看,檔案空洞中是存在位元組的,讀取空洞將會返回以0(空位元組)填充的緩衝區。然而檔案空洞不會佔用磁碟空間

  • ls命令可以檢視檔案在檔案系統中的大小(邏輯大小),這個大小是包含檔案空洞的空位元組大小的.
  • du命令可以檢視檔案在磁碟中實際佔用的空間,du -s test結果表示的是多少個1024位元組