[溫故而知新] 《Linux/Unix系統程式設計手冊》——檔案I/O

小灰馬發表於2016-10-23

本文對檔案IO這一塊做一些梳理,記錄思考的一些問題和一些待解決的問題,後續會繼續更新。

I hear and I forget,I see and I remember,I do and I understand.

Part 1 :通用IO

/**
相關標頭檔案:
<fcntl.h>
<unistd.h>

檔案IO的幾個系統呼叫
fd = open(pathname, flags, mode)
numRead = read(fd, buffer, count)
numWritten = write(fd, buffer, count)
status = close(fd)
*/

C標準庫的函式真是簡潔,跟OOP 形成鮮明的對比就是從引數的傳遞方式,比如open函式,對於flag是通過位運算來進行各種引數的判斷,如果是像Java這種比較囉嗦的語言,實現起來估計就會是一個類,然後裡面各種方法過載,然後各種引數。當然兩種方式各有優缺點。

思考的一些問題:

  1. 對於 open 函式,返回的是一個檔案描述符(file descriptor) , 是一個int型結果,為什麼不是返回一個具體的結構體呢?
    首先如果讓我自己來實現這個系統api,必須有個結構體來記錄開啟的檔案的相關資訊,比如當前讀取到哪個位置了,檔案的路徑等。從使用者角度來講,大多數情況關心的只是如何對檔案進行IO,遮蔽掉底層的實現顯然是比較合理的。
    既然返回的是一個int型的,那麼fd可以認為就是個索引而已,核心中必須有個結構來維護程式開啟的檔案列表。

  2. 對於 read函式我們關心的是讀了多少資料,這些資料讀完放哪,而函式只能有一個返回值,所以buffer作為函式引數傳遞了。
    有個問題待確認,在彙編層面,系統呼叫中傳遞的陣列引數是如何進行的?//TODO
    目前簡單的猜測,傳遞陣列實際傳的只是個指標,然後在系統呼叫時切到核心態後,把程式的虛擬地址進行轉換為實體地址然後進行IO,而這一步轉換是如何進行的?

  3. open呼叫成功,其返回值為程式未用檔案描述符中數值最小者。
    原因猜測,一個程式的檔案描述符是有限的,所以已經關閉的描述符可以重複利用。

  4. 關於 O_CLOEXEC 的flag
    //TODO

  5. open函式的O_CREAT 標識,用來在開啟檔案不存在的時候也進行建立,但是這裡有個問題,如果open的時候沒有傳mode,也就是許可權沒有傳的話,親測,建立出來的檔案的許可權是個隨機值(書中說的是棧中的隨機值,沒有具體去考證如何從棧中取值的)。 然而這裡為什麼不直接返回失敗呢?//TODO

  6. open函式的O_CREAT標識,可以用來建立檔案,那麼為什麼不用creat函式呢?
    好吧,O_CREAT可以和另一個引數 O_EXCL 配合,達到的效果是,判斷檔案是否存在,如果存在則呼叫失敗。也就是檢查檔案存在和建立檔案是一個原子操作。
    實際上 creat() 等價於
    open(pathname, O_WRONLY|O_CREAT|O_TRUNC, mode)

  7. 系統呼叫的read(),write() 實際上只是在傳遞的引數buffer和核心的緩衝區進行資料拷貝而已,並不是實際的通過磁碟IO然後拷貝到buffer中。那麼,觸發磁碟IO的時機是什麼?
    對於寫操作,如果沒有手動刷,核心有個專門的執行緒幹這個事情,檢查是否為髒緩衝(超過一定時間,比如30s)是的話就刷緩衝。對於寫操作,如果是核心緩衝區滿了是不是也刷緩衝?核心的策略是如何的?//TODO

  8. open 的幾個引數
    O_NOATIME //不修改訪問時間,對於一些讀操作可以優化IO,因為可以少一次把檔案的後設資料刷到磁碟的操作
    O_NOFOLLOW //對於一些有特權的程式非常有用,不跟隨符號連結,保證安全性。
    O_ASYNC //TODO
    O_NONBLOCK //非阻塞IO,有些型別的檔案,open後者後續的讀寫會造成阻塞,加入這個標誌會變為非阻塞,open可能會直接失敗返回,而對於讀,可能只讀了部分資料,對於寫呢?//TODO

Part 2 : 檔案I/O緩衝

/**
對於標準庫的緩衝(stdio的緩衝)
相關的函式有:
fprintf(), fscanf(), fgets(), fputs(), fgetc(), fputc()
這些最終都是通過系統呼叫read()和write() 進行IO。
設定標準庫的緩衝策略相關函式:
<stdio.h>
int setvbuf(FILE *stream, char *buf, int mode, size_t size)
緩衝策略有三種:
1. 不緩衝  _IONBF     io no buffer
2. 行緩衝  _IOLBF      io line buffer 遇到換行符或者緩衝區滿就調系統呼叫
3. 全緩衝  _IOFBF      io full buffer  緩衝區滿再呼叫系統呼叫

setvbuf兩個兄弟
void setbuf(FILE *stream, char *buf) => setvbuf(fp,buf, ( buf!=NULL) ? _IOFBF:_IONBF, BUFSIZ )
也就是緩衝區大小採用stdio.h中定義的預設緩衝區大小,緩衝模式預設為全緩衝

#defnie _BSD_SOURCE
void setbuffer(FILE *stream, char *buf,size_t size);
跟setbuf類似,緩衝模式為全緩衝,緩衝區大小自己配置。
*/

思考的一些問題:

  1. 對於檔案I/O的核心快取,對於寫緩衝,核心把緩衝刷到磁碟上的策略是什麼?
    如果程式沒有手動呼叫flush,那麼系統核心會有個執行緒在週期性執行檢查然後flush。髒緩衝區能被刷的條件是達到規定的“年齡”(在/proc/sys/vm/dirty_expire_centisecs ,單位為1%秒,一般是30秒),也就是30秒內沒有手動刷,系統的一條長期執行的核心執行緒下次檢查到了就會把它刷到磁碟去。

  2. stdio有setvbuf之類的設定緩衝策略的東西,核心呢?如何控制緩衝策略?//TODO

核心用於控制檔案IO緩衝的系統呼叫:
#include <unistd.h>
int fsync(int fd);
int fdatasync(int fd);
  1. fsync和fdatasync的區別?
    參考:http://blog.csdn.net/zbszhangbosen/article/details/7956558
    簡單來說,它們的共同點都是同步操作,需要等磁碟的IO,對於fsync會確保檔案的資料和檔案的後設資料(例如檔案的最近訪問時間、修改時間等)都同步寫完才返回(兩次磁碟操作),而fadatasync只保證檔案的資料同步寫,並不保證檔案的後設資料同步寫(一次磁碟操作)。

  2. 針對stdio,強制重新整理寫緩衝到系統核心的函式: fflush(FILE *stream)

  3. 開啟一個流同時用於輸入和輸出,C99兩項要求:

    • 一個輸出操作不能緊跟一個輸入操作,必須在兩者之間呼叫fflush() 函式或者一個檔案定位函式(fseek(),fsetpos(),rewind() )。 這裡是不是意味著這些定位函式會呼叫一次fflush()?//TODO
    • 一個輸入操作不能緊跟著一個輸出操作,必須在二者之間呼叫一個檔案定位函式,除非輸入操作已經到了檔案結尾。
      這兩項要求的目的是什麼?各種系統的實現如何?(試了下輸入後立即輸出和 輸出後立即輸入操作,暫時沒發現問題,猜測跟同步問題相關)//TODO
  4. open函式對於緩衝的控制flag
    O_SYNC和 O_DSYNC 作用於寫操作。
    O_SYNC flag, 相當於後續的輸出操作,會類似fsync一樣,同步寫檔案的後設資料和檔案的資料,對效能影響非常大。
    O_DSYNC flag, 這個與O_SYNC類似,不過它的語義跟fdatasync類似。
    O_RSYNC flag, 作用於讀操作,是與O_SYNCO_DSYNC相結合使用的。具體語義是,如果O_RSYNC|O_DSYNC ,那麼在讀操作之前,會執行像O_DSYNC一樣所有待處理的寫操作。
    這個標誌的使用場景呢?//TODO

  5. I/O緩衝小結,畫張圖出來//TODO

    • 緩衝有多處,stdio緩衝,核心緩衝,磁碟高速緩衝
    • 對於stdio緩衝,任意時刻可以呼叫fflush()刷緩衝; 或者在輸出之前,通過呼叫setbuf(stream,NULL)禁用掉stdio的緩衝,然後直接走系統呼叫。
    • read,write系統呼叫,並不是直接進行磁碟IO,而是在讀寫核心的緩衝區。任意時刻可以呼叫fsync之類的函式強刷核心緩衝到磁碟。也可以在open的時候設定O_SYNC之類的標誌來強刷緩衝。
    • 磁碟的緩衝控制
      禁用:hdparam -W0
      啟用://TODO
  6. 裸 I/O: O_DIRECT
    O_DIRECT 需定義_GNU_SOURCE
    裸I/O看起來好麻煩的樣子,看裸I/O的語義,就是可以不經過核心緩衝區,直接經過磁碟DMA進行IO,所以速度應該很慢。但是O_DIRECT和 O_SYNC有什麼區別呢?//TODO
    參考:http://stackoverflow.com/questions/5055859/how-are-the-o-sync-and-o-direct-flags-in-open2-different-alike
    目前還沒驗證答案是否正確,大概的意思是說O_DIRECT並沒有保證資料刷到磁碟上函式才返回,而O_SYNC 有這個保證(雖然刷到磁碟上還可能有緩衝)。而O_SYNC是會經過核心緩衝區的,O_DIRECT沒有經過記憶體緩衝區,所以O_DIRECT的使用,需要設定緩衝區,並且有各種記憶體對齊的要求:

    • 用於資料傳遞的緩衝區,記憶體邊界必須為block大小(不同環境的block大小不一樣)的整數倍。
    • 資料傳輸的起點,必須是block大小的整數倍。
    • 待傳遞的資料的長度,必須是block大小整數倍。
  7. posix_fadvise() //TODO
    給核心提供建議,優化效能。

Part 3 庫函式和系統呼叫混用

/**
有的函式對於檔案傳遞的是 FILE* 指標,有的是一個int型的檔案描述符
#include <stdio.h>
int fileno(FILE *stream)
FILE *fdopen(int fd, const char* mode);
兩個函式的作用相反。
*/

Part 4 檔案操作控制

lseek()函式
off_t lseek(int fd, off_t offset, int whence)

用來定位檔案的讀寫位置,並不是所有型別的檔案都支援,比如像 socket,終端就不支援lseek。
lseek() 只是調整核心中與檔案相關的檔案描述符結構,並沒有物理裝置訪問。
lseek() 的 offset是帶符號的,也就是可以從檔案最後往前讀。

思考的幾個問題

  1. 為什麼是lseek() 而不是 seek()?
    返回值是long型。

  2. 檔案空洞//TODO

  3. lseek() 到檔案最後開始寫,和open的時候帶上O_APPEND的區別?
    區別在於O_APPEND能保證原子性的語義,也就是說保證每次寫都是從檔案的最後開始寫。而如果有兩個程式同時lseek()到檔案最後然後寫,有可能導致寫覆蓋。

  4. int ioctl() //TODO 百寶箱
    像這種百寶箱類的函式,引數一般都是一個資源、一個cmd、變長的其它引數。

  5. int fcntl(int fd, int cmd, ...)//TODO 又是百寶箱…

    • 讀取和修改檔案狀態標誌
      能讀取的狀態標誌:
      O_SYNC
      O_RDONLY
      O_WRONLY
      O_RDWR
      //TODO 還有哪些
    • O_RDONLY,O_WRONLY,O_RDWR為什麼沒有與檔案狀態標誌的位元位一一對應,原因很簡單,它們有交叉關係……
      能修改的標誌:
      O_APPEND
      O_NONBLOCK
      O_NOACTIME
      O_ASYNC
      O_DIRECT
    • 兩個程式修改狀態標誌會相互影響嗎?參考下面的檔案描述符與檔案的關係。
  6. 檔案描述符與檔案的關係
    書中一張神圖解決所有問題//TODO

  7. 如何讀寫大檔案,在32位的機器上,off_t最大是2G
    一種推薦做法是定義一個巨集,_FILE_OFFSET_BITS 64
    然後,之前的IO函式都會變為64位的版本,比如open()->open64()
    所以那些都是巨集定義。

  8. 建立臨時檔案的幾種種方法:

    • int mkstemp(char* template)
      該呼叫會加上O_EXCL標誌,模版引數類似”/tmp/abcXXXXXX”,核心會替換最後6個X並且保證檔名唯一,如何做到?//TODO
    • tmpnam(),tempnam(),mktemp() 能用於生成唯一檔名,區別是什麼?又為什麼說會有安全漏洞?有安全漏洞那麼哪些場景下可用?//TODO
    • FILE* tmpfile(void)
      檔案流關閉後自動刪除該檔案,如何做到?//TODO
      內部呼叫unlink()來刪除檔名??//TODO
      程式退出後自動關閉所有開啟的檔案描述符,然後就刪除臨時檔案?
  9. 一些好用的api //TODO

readv    //read vector
writev   //write vector

pread    //position read
pwrite   //position write

相關文章