1. 程式人生 > >Linux環境程式設計-檔案I/O

Linux環境程式設計-檔案I/O

這一章的IO是基礎IO,不帶緩衝的IO,每個read和write都呼叫核心中的一個系統呼叫,包含open,read,write,lseek以及close幾個函式。 只要設計多個程序間的共享資源,原子操作就很重要,這一章通過檔案IO與相關函式來討論,進一步討論多個程序間如何共享檔案,以及說明dup,fcntl,sync等函式

檔案描述符

對於核心而言,所有開啟的檔案都通過檔案描述符來引用,檔案描述符是一個非負整數,當開啟或者建立一個檔案的時候,核心向程序返回一個檔案描述符,這個描述符將用於讀寫等操作來標識這個檔案。按照管理,檔案描述符0,1,2分別與標準輸入,標準輸出以及標準錯誤相關,不過我們通常將他們替換成STDIN_FILNO,STDOUT_FILNO與STDERR_FILNO,這樣做是為了提高可讀性。 對於linux來說,檔案描述符的變化範圍幾乎是無限的,他只受制於系統配置的儲存器總量,整形字長等因素,所以一般來說最多開啟65535個

open與openat

這兩個函式用於開啟或者建立一個檔案 int open(const char *pathname, int flags, / *mode_t mode */); int openat(int dirfd, const char *pathname,/ * int flags, mode_t mode * /); 最後一個引數是可變的,只有在建立一個檔案的時候才關心最後一個引數 第一個引數path是要開啟或者建立的檔名字,關於這個名字是絕對路徑名還是相對路徑名的區別在下面兩個函式的區別的時候在做討論。 第二個引數是這個函式的選項,以下幾個是必須選定一個的 O_RDONLY(只讀開啟) O_WRONLY(只寫開啟)O_RDWR(讀寫開啟) O_EXEC(只執行開啟) O_SEARCH(只搜尋開啟) 下面的是非必選項 O_APPEND:每次都追加到檔案的尾端,通過設定偏移量到檔案末尾實現 O_CLOEXEC:把這個常量設定為檔案描述符標誌 O_CREAT:若檔案不存在就建立,要是設定了這個的話一定要設定第三個引數賦許可權 O_NOCTTY:如果path引用終端裝置就不將該裝置分配作為此裝置的控制終端 O_NOFOLLOW: 如果path一個符號連結,就報錯 O_NONBLOCK:如果檔案是一個FIFO,塊特殊字元檔案,那就設定為非阻塞方式 open與openat返回的是最小的未使用描述符,我們可以用這個特性來關閉某些描述符然後重定向

open與openat的區別

根據上面函式原型我們可以看出來,openat多了一個fd引數,共有三種可能

  1. path引數指定的是絕對路徑名,在這種情況下fd被忽略,兩個函式等同
  2. path指定的是相對路徑名,fd引數指出了相對路徑名在檔案系統中的開始地址
  3. path引數指定相對路徑名,fd引數具有特殊值AT_FDCWD在這種情況下,路徑名在當前工作目錄中獲取,openat函式操作上與open函式類似

那麼為什麼會有openat函式呢,openat函式是為了解決兩個問題,第一個是讓執行緒可以使用相對路徑名開啟目錄中的檔案,而不再只能開啟當前工作目錄。第二個是可以避免TOCTTOU錯誤,TOCTTOU錯誤的基本思想是:如果有兩個基於檔案的函式呼叫,其中第二個呼叫依賴第一個的結果,那麼程式是脆弱的,因為兩個函式並不是原子操作,所以兩個函式之間檔案改變了就造成了呼叫不再有效。

檔名和路徑截斷

檔名最大值是由NAME_MAX設定的,可是如果NAME_MAX是14,我們試圖在目錄中建立一個檔名包含15個字元的話,那麼在早期system V情況下是截斷成14個字元並且不返回任何出錯狀態,在BSD類的系統下就返回出錯狀態,可是如果我們的檔名恰好是14個字元的話我們就不知道他是不是截斷過的檔名了,這樣會引起混亂。 在posix中常量_POSIX_NO_TRUNC決定要截斷路徑名還是出錯,如果常量有效,那麼整個路徑名超過NAME_MAX。

creat與close

creat creat是用於建立一個新檔案 int creat(const char *pathname, mode_t mode); 用法與open函式大致相同 其實我們可以發現open函式可以完成creat的功能 creat函式等價於open(path,O_WRONLY | O_TRUNC | O_CREAT) 而且我們可以發現creat建立的是隻寫,當我們要讀的時候我們要先creat再close再open 這樣太麻煩了我們可以直接open的時候選擇O_CREAT建立然後O_RDWR給讀寫許可權即可

close close函式用於關閉一個檔案 int close(int fd); 當關閉一個檔案的時候還會釋放該程序上所有記錄鎖 當一個程序終止的時候 ,核心會關閉它開啟的所有檔案。

lseek與檔案偏移量

每一個開啟的檔案都有一個當前的檔案偏移量,它通常是一個非負整數,用以度量從檔案開始處計算的位元組數,通常,讀寫操作都是從當前檔案偏移量開始的,並使偏移量增加所讀寫的位元組數,按照系統預設的情況,當開啟一個檔案時除非是O_APPEND是追加開啟否則偏移量都是被設定0

lseek

lseek函式可以用來設定一個檔案的偏移量 off_t lseek(int fd, off_t offset, int whence); 對引數offset的解釋與引數whence的值有關

  • 若whence是SEEK_SET(0)被叫做絕對偏移量,那麼該檔案的偏移量設定為距檔案開始處offset個位元組
  • 若whence是SEEK_CUR(1)相對於當前位置的偏移量,那麼該檔案的偏移量設定為當前值加上offset,offset可正可負
  • 若whence是SEEK_END(2)相對於末尾位置的偏移量,那麼該檔案的偏移量設定為末尾值加上offset,offset可正可負

若lseek成功執行,則返回新的檔案偏移量,如果檔案描述符是一個管道或者套接字那麼lseek就返回-1,並將errno設定為ESPIPE 這裡要切記一點由於lseek可以為負數所以在返回比較的時候比較是否為-1而不是小於零 lseek有以下特點:

  1. lseek僅將當前檔案偏移量記錄在核心中,不引起任何的IO操作
  2. 檔案偏移量可以大於當前的檔案長度,在這種情況下,對該檔案的下一次寫將加長該檔案,並在檔案稱構成一個空洞,所有沒被寫過的位元組都讀作0,並且他們並不佔用儲存區,這種檔案被叫做空洞檔案。

read與write

read與write函式主管對檔案的讀和寫操作 read ssize_t read(int fd, void *buf, size_t count); 第一個引數fd是要操作的檔案描述符 第二個引數buf是要讀到的區域 第三個引數count是要讀的個數 成功返回讀到的位元組數,若已經讀到末尾就返回0 有多種情況可使實際讀到的位元組數少於要求讀到的位元組數

  • 讀普通檔案的時候,在讀到要求位元組數之前已經讀到了檔案末尾
  • 當從終端讀的時候,通常一次最多讀一行
  • 當從網路讀的時候,網路中的緩衝機制可能造成返回值小於要求的位元組數
  • 當從管道或FIFO讀的時候,如果管道中包含的位元組少於所需的位元組數,那返回實際讀到的位元組個數
  • 當從某些面向記錄的裝置(比如磁帶)讀時,一次最多返回一個記錄
  • 當一訊號造成中斷的時候,而已經讀了部分資料

write write函式用來向開啟檔案寫入資料 ssize_t write(int fd, const void *buf, size_t count); 若成功返回寫的位元組數,失敗返回-1 write通常失敗的原因是磁碟寫滿或者超過了一個給定程序的檔案長度限制 對於普通檔案來說,寫操作從檔案當前偏移量開始,預設偏移量是0,O_APPEND則是指向檔案末尾,從末尾追加寫入。

檔案共享

接下來我們來看看在不同程序間共享開啟檔案,這裡我們一定先要理解檔案系統

檔案系統

核心使用三中國資料結構來表示開啟的檔案,他們之間的關係決定了在檔案共享方面一個程序對另一個程序產生的影響 每一個程序在程序表中有一個記錄項,記錄項中包含一張開啟的檔案描述符表,其實就是程序PCB(Linux下是task_struct)中維護了一張檔案描述符表,然後這個表中每一個檔案描述符都匹配一個檔案指標,指向開啟的檔案的檔案表項,這個檔案表項包含:

  • 檔案的狀態標誌(讀寫新增,同步阻塞等)
  • 當前檔案的偏移量
  • 只想該檔案的v節點

其中檔案的v節點包含了檔案型別核對此檔案進行各種操作函式的指標以及該檔案的i節點。我們用下圖來說明這幾個資料結構的關係 在這裡插入圖片描述 當我們瞭解了檔案系統後,接下來我們來看看如何檔案共享以及需要注意的問題 當我們用兩個不同的程序開啟一個檔案的時候,首先兩個程序會建立兩張檔案表項,並且他們有自己的偏移量, 在這裡插入圖片描述 我們假定第一個程序在檔案描述符3上開啟該檔案,而另一個程序在檔案描述符4上開啟該檔案. 開啟該檔案的每個程序都獲得各自的一個檔案表項,但對一個給定的檔案只有一個v節點表項.之所以每個程序都獲得自己的檔案表項,是因為這可以使每一個程序都有它自己的對該檔案的當前偏移量.給出了這些資料之後,現在對前面所述的操作進一步說明.

  1. 在完成每個write後,在檔案表項中的當前檔案偏移量既增加所寫入的位元組數. 如果這導致當前檔案偏移量超出了自己當前檔案長度. 則將i節點表項中的當前檔案長度設定為當前檔案偏移量(檔案加長).
  2. 如果用O_APPEND標誌開啟一個檔案,則相應標誌也被設定到檔案表項的檔案狀態標誌中. 每次對這種具有追加寫標誌的檔案執行寫操作時,檔案表項中的當前檔案偏移量首先會被設定為i節點表項中的檔案長度,這就使得每次寫入的資料都追加到檔案的當前尾端處.
  3. 若一個檔案用lseek定位到檔案當前的尾端,則檔案表項中的當前檔案偏移量被設定為i節點表項中的當前檔案長度.
  4. lessk函式只修改檔案表項中的當前檔案偏移量,不進行任何I/O操作.

dup與sync

dup dup函式族是用於複製一個現有的檔案描述符(重定向) int dup(int oldfd); int dup2(int oldfd, int newfd); 由dup函式返回的新檔案描述符一定是當前可用檔案描述符中的最小值,對於dup2,可以用fd2引數指定的新描述符的值如果fd2已經開啟,就將他關閉,如果fd等於fd2,則dup2返回否都,而不關閉它。這些函式返回的新檔案描述符與引數fd共享一個檔案表項 sync 當我們向檔案寫入資料的時候,核心先將資料複製到緩衝區,然後排入佇列,晚些時候再寫入磁碟,這種被叫做延遲寫。當核心需要重用緩衝區來存放其他磁碟塊資料的時候,他會把所有延遲寫資料寫入磁碟,sync只是將所有修改過的塊緩衝區排入寫佇列,然後陷入阻塞等待。 int syncfs(int fd); 成功就返回0,失敗返回-1