2017年3月2日 星期四

Linux 檔案IO: 深入探討

檔案IO:  深入探討


atomicity and race condition


system call都是atomicity, kernel保證每個步驟在該操作內完成, 而不會受到其他process 或 thread中斷


open(pathname, O_CREAT | O_EXCL):
O_CREATE 和 O_EXCL的open()呼叫保證檢查和建立的步驟是在一個atomic操作中完成. 保證該檔案一定是由呼叫open()的process所建立的


Example:
若不使用O_CAEAT和O_EXCL的open()呼叫, 改使用呼叫兩次open()來執行程式. 第一次open(..., O_WRONLY)用來檢查該檔案存不存在. 第二次open(..., O_WRONLY | O_CREAT)用來建立檔案. 可能會造成以下的race condition.
Process A先呼叫open(.., O_WRONLY)來檢查檔案在不在. 發現檔案不在之後就被切換到Process B. Process B一樣先檢查檔案在不在, 發現不在之後接著就建立了新檔案.在切回Process A執行. Process A呼叫第二個open()就會以為是自己建立了檔案.
open(pathname, O_APPEND):
O_APPEND是指將offset移到檔案尾巴的下一個byte的位置.在寫入資料的時候,會把offset移到檔案尾巴. 基本上類似下面的程式碼

if (lseek(fd, 0, SEEKEND) == -1)
   errExit(“lseek”);
if (write(fd, buf, len) != len)
   fatal(“Partial/failed write”);
O_APPEND可以確保lseek()write()操作是連續的, 中間並不會被其他process或thread所中斷來以防race condition的情形產生.
PS: O_APPEND在NFS上是不支援的


Open File Status Flag


fcntl(): 用途之一是取得修改 存取模式(access mode)和開啟檔案狀態旗幟(open file status flag)
example1 (利用fcntl去檢查檔案是否使用同步寫入)
int flags, accessMode;
flags = fcntl(fd, F_GETFL);
if (flags & O_SYNC)
   printf("write are synchronized");


example2 利用fcntl()去檢查檔案的存取模式(read/write)
accessMode = flags & O_ACCMODE;
if (accessMode == O_WRONLY || accessMode == O_RDWR)
   printf("file is writable")
因為O_RDONLY, O_WRONLY和O_RDWR常數沒有對應的single bit, 所以我們必須要利用O_ACCMODE來幫我們判斷他的存取模式


example3 利用fcntl()去修改檔案的open file status flag
int flags;
flags = fcntl(fd, F_GETFL);
if (flags == -1)
   errExit("fcntl");
flags |= O_APPEND;
if (fcntl(fd, F_SETFL, flags) == -1)
   errExit("fcntl")


會使用fcntl()來修改open file status flag的情形有兩種
  1. 檔案不是由執行呼叫的程式開啟
  2. file descriptor不是由open()呼叫得到, 而是由系統呼叫得到. example, pipe, socket...etc


File descriptor和開啟檔案的關係


file descriptor對於開啟檔案的是多對一的關係
三個由kernel所維護的Data Structure
  • per-process file descriptor table:
    • 每一個process都會有一個table去紀錄現在開啟的file desciptor有哪些.每一筆紀錄單一file descriptor的相關訊息包含
      • 一組控制檔案敘述符的reference(就是指close-on-exec)
      • 一個指向開啟file descriptor的reference
  • open file description table:
    • kernel維護整個系統的open file description table. 目前系統有開啟的file descriptor的相關資訊, 每一筆包含
      • 目前檔案的offset
      • 開啟檔案設定時的flags(open()的flags設定)
      • 檔案存取模式(open()的唯讀唯寫讀寫設定)
      • signal-driver IO相關設定
      • 檔案的i-node object的reference
  • 檔案系統的inode table:
    • 每一個檔案系統都會有一個inode table去紀錄檔案系統中所有檔案的相關資訊, 這裡目前只討論的inode資訊有
      • 檔案類型及權限
      • 一個指向檔案鎖清單的指標
      • 檔案的各種屬性(ex. file size...etc)
存取檔案的時候kernel會在記憶體建立一個inode的副本. 紀錄各種檔案開啟時相關短暫屬性(ex. file lock...etc)


利用下圖解釋file descriptor, open file descriptor和i-nodes之間的關係
  • ProcessA的descriptor1和descriptor20都reference到同一個open file descriptor(23), 可能是由dup(), dup2或fcntl()所產生的結果
  • ProcessA的descriptor2和ProcessB的descriptor2都reference到open file descriptor(73). 可能是由fork()或裡用domain socket傳送給另一個 process所造成的現象
  • ProcessA的descriptor0和ProcessB的descriptor3都reference到不同的open file descriptor(0和86), 但最後卻都reference到inode table的同一筆紀錄.表示ProcessA和ProcessB都利用open()開啟同一個檔案
得到一些結論
  1. reference到同一份open file descriptor的兩個file descriptor會共用file offset. 若offset由file descriptor所變動(lseek())會影響另一個file descriptor.
  2. 除了offset之外也共用同一份status flag. 所以當一個file descriptor利用fcntl()作變動(O_APPEND, O_NONBLOCK, ...). 另一個file descriptor也會被影響.
  3. 但fd flags(close-on-exec flag)是跟著process file descriptor.修改後不會影響其他的Process的file descriptor.


複製file descriptor


執行下列指令
$ ls > result.log 2>&1


上面這個指令會把ls的stdout導到result.log. 而後面是把ls的stderr改成跟stdout相同的open file descriptor.結論就是ls的stdout和stderr會寫到result.log中.


但下列指令
$ ls 2>&1 > result.log
卻只會把stdout導到result.log但stderr卻沒有. 為什麼呢?
shell讀取指令的順序是由左到右. 左邊的指令會先作再做右邊的.


$ ls > result.log 2>&1
$ ls 2>&1 > results.log


在c code裡面有幾種複製file descriptor的方式
  • dup()
  • dup2()
  • dup3()
  • fcntl(F_DUPFD)


dup()
int dup(int oldfd):
  • 建立oldfd的副本.找一個最小且沒有在使用的file descriptor指向oldfd所指的open file descriptor並回傳.


example (將stderr利用dup導到/tmp/results.log):
int new_fd, err_fd;
err_fd = open("/tmp/results.log", O_CREAT | O_WRONLY); //err_fd = 3
close(2);
new_fd = dup(err_fd); //new_df = 2


close(2)是先將stderr的file descriptor關閉.
dup(err_fd)會建立err_fd(3)的副本,並挑選一個最小且未始用的file descriptor, 就是剛剛關閉的2.


dup2()
int dup2(int oldfd, int newfd):
  • 可以想像成是close()dup()的簡化, 且因為是atomic的操作所以close()dup()並不會給其他process或thread所中斷
  • 若oldfd不是有效的file descriptor的話, 會失敗且不會關閉newfd
example:
dup2(err_fd, 2);


dup3()
int dup3(int oldfd, int newfd, int flags):
  • 支援一個flag O_CLOEXEC, 讓你複製file descriptor可以把這個參數開啟
  • Linux 2.6.27推出, Linux特有的功能


fcntl(F_DUPFD)
int fcntl(oldfd, F_DUPFD, startfd):
  • 會建立oldfd的副本並回傳大於等於startfd且未被使用的最小file descriptor.通常是我們想讓新的file descriptor落在某個區間使用.
  • 可以將F_DUPFD改成F_DUPFD_CLOEXEC就變成支援O_CLOEXEC開起.


指定偏移的檔案IO
在進行IO的時候可以指定offset, 完成後再回到原本的offset
  • ssize_t pread(int fd, void *buf, size_t count, off_t offset)
  • ssize_t pwrite(int fd, const volid *buf, size_t count, off_t offset)
pread()可以等同於以下的code且是atomic(不可中斷):
off_t orig;
orig = lseek(fd, 0, SRRK_CUR);
lseek(fd, offset, SEEL_SET);
s = read(fd, buf, len);
lseek(fd, orig, SEEK_SET);
單一pread()pwrite()成本都比read/write搭配lseek()成本還低


分散式輸入和集中式輸出(Scatter-Gather IO)


iovec:
  • 放資料的data structure, 結構如下
struct iovec {
void *iov_base; //buffer起使位置
size_t iov_len; //buffer的長度
}


Scatter input
從file descriptor所reference的檔案讀取資料並將這些資料分散放置在iov設定的buffer中


readv():
ssize_t readv(int fd, const struct iovec *iov, int iovcnt):
  • buffer由iov array所定義.
  • iovcnt只有多少個iov要讀
  • *iov是一個放資料的buffer, 是iovec結構
  • atomic操作
  • 當操作成功後會回傳讀取的byte數. 呼叫者必須要自行檢查是否讀取了所需的資料.


example:
# define STR_SIZE 100;
struct iovec iov[3];
struct stat myStruct;
int x;
char str[STR_SIZE];


iov[0].iov_base = &myStruct;
iov[0].iov_len = sizeof(struct stat);
iov[1].iov_base = &x;
iov[1].iov_len = sizeof(x);
iov[2].iov_base = str;
iov[2].iov_len = STR_SIZE;


numRead = readv(fd, iov, 3);


Gather output
集中串聯在iov指定的全部buffer資料, 並依序將他寫入由fd所reference的檔案.


writev()
ssize_t writev(int fd, const struct iovec *iov, int iovcnt)
  • buffer由iov array所定義.
  • iovcnt只有多少個iov要讀
  • *iov是一個放資料的buffer, 是iovec結構
  • atomic操作
  • 當操作成功後會回傳寫入的byte數.
readv()和writev()主要的好處
  • 便利
  • 速度
指定offset的scatter-gather IO
ssize_t preadv(int fd, const struct iovec *iov, int iovcnt, off_t offset)
ssize_t pwritev(int fd, const struct iovec *iov, int iovcnt, off_t offset)


截斷檔案:
  • int truncate(const char *pathname, off_t length)
    • truncate()唯一可以改變檔案內容而不用事先open()來取得fd
  • int ftruncate(int fd, off_t length)
這兩個function可以將檔案設定為length大小

  • 若原本檔案的長度大於lenght => 資料遺失
  • 若原本檔案的長度小於lenght => 找連續空byte來填

沒有留言:

張貼留言