【APUE】Chapter3 File I/O

這章主要講了幾類unbuffered I/O函數的用法和設計思路。html

 

3.2 File Descriptorsnode

  fd本質上是非負整數,當咱們執行open或create的時候,kernel向進程返回一個fd。linux

  unix系統中有幾個特殊的fd:緩存

  0:standard input安全

  1:standard outputapp

  2:standard errorsocket

  這幾個帶有特殊含義的整數都有對應的可讀性強的符號表示:STDIN_FILENO, STDOUT_FILENO, STDERR_FILENOasync

  具體的定義都包含在unistd.h頭文件中:函數

  

 

3.3 open & openat Functions測試

  int open(const char *path, int oflag, ...)

  返回值爲file descriptor

  其中oflag中包含各類選項的組合;並要求「只讀、只寫、讀寫、搜索、執行」這五類選項必須有且只有一個。

  open函數還有一個特性:經過open或者openat返回的file descriptor必定保證是可用的最小的descriptor。寫一個小栗子以下:

#include <unistd.h>
#include <fcntl.h> 
#include <stdlib.h>
#include <stdio.h>

int main(void)
{
    int val1 = open("test_fd1",O_CREAT);
    printf("fd:%d\n",val1);
    int val2 = open("test_fd2",O_CREAT);
    printf("fd:%d\n",val2);
    exit(0);
}

  編譯運行後以下:

  

  因爲0,1,2都有特殊的含義,因此後開的兩個file descriptor爲三、4。

  書上還提到了文件名長度的問題:若是是比較老的一些系統,文件名的長度超過了14,則系統可能把超過14個長度的部分截斷了。

 

3.4 create Function

 

3.5 close Function

  關閉file deescriptor,一旦被close了,那麼Process中各類加在這個文件上的鎖也就被release了。

 

3.6 lseek Function

  與文件偏移量有關。

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

  若是成功返回new file offset,若是失敗則返回-1

  具體執行什麼操做須要根據whence參數來肯定:

    SEEK_SET:重置file的偏移量爲參數中offset的值

    SEEK_CUR:設置file的偏移量爲當前偏移量+offset的值,這裏offset可正可負

    SEEK_END:設置file的偏移量爲當前文件的size+offset的值,這裏offset依然可正可負

  若是要判斷文件的當前偏移量,lseek(fd, 0, SEEK_CUR)便可。

  lseek還能夠判斷當前的file是否可以被seeking(好比pipe FIFO或socket就不可以)代碼以下:

#include <stdio.h>
#include <unistd.h>
#include <stdlib.h> 

int main()
{
    if (lseek(STDIN_FILENO,0,SEEK_CUR)==-1) { 
        printf("cannot seek\n"); 
    }
    else { 
        printf("seek OK\n"); 
    } 
    exit(0);
}

  執行結果以下:

    

    從文件來的file descriptor就能夠seek;從pipie來的file descriptor就不可以seek。

    上述的代碼用lseek是否返回負1來判斷是否成功,那麼是否判斷lseek是否爲負數就能夠知道是否成功了呢?

    書上說這樣作的是不安全的,由於有些文件偏移量是容許爲負數的。

  這裏有一個問題,若是lseek操做後,文件的偏移量超過了文件的size怎麼辦?這種狀況也是容許的,若是偏移量超過了文件的size,那麼執行write操做就會將文件的size擴展,而且中間沒有執行寫操做的部分被灌以空白字符代替。例子以下:

#include "apue.h" 
#include <stdio.h>
#include <stdlib.h>
#include <fcntl.h>

char buf1[] = "abcdefghij";
char buf2[] = "ABCDEFGHIJ";

int main()
{
    int fd;
    fd = creat("file.hole", FILE_MODE);
    write(fd, buf1, 10);
    lseek(fd, 16384, SEEK_SET); /*offset now = 16384*/
    write(fd, buf2, 10); /*offset now = 16394*/
    exit(0);
}

  運行結果以下:

  

  文件大小是16394,文件的size被擴展了。

  既然文件可以隨着lseek而自動擴充size,那麼這種擴充是否是無限的?並非無限的。

  書上說大多數操做系統,提供了兩種方式操做file offsets:一種是32 bits offsets,另外一種是64 bits offsets

  2^32 = 4GB ,我猜這也就是爲何比較老的文件系統最大單個文件只能支持4GB的緣由了

  2^64 = 很是大的GB

  可是,即便系統提供了32 & 64兩種文件偏移量接口,可是最終可以支持多大的單體文件,還須要結合底層文件系統。

  

3.7 read Function

  ssize_t read(int fd, void *buf, size_t nbytes)

  read的操做從文件當前的offset執行,read返回前文件的offset會增長,增長的值就是讀入的bytes數;返回的是讀入的bytes數。

  通常來講,read讀入的bytes數就是nbytes;但有以下幾種狀況實際讀入的bytes是要小於nbytes的:

  (1)若是是regular file,而且文件已經讀到頭了

  (2)read from terminal device

  (3)read from a network

  (4)read from pipe of FIFO

  (5)interrupted by a signal and a partial amount of data has already been read

  

3.8 write Function

  

3.9 I/O Efficiency

  書上列了這麼一段程序:

#include "apue.h"

#define    BUFFSIZE    4096

int
main(void)
{
    int        n;
    char    buf[BUFFSIZE];

    while ((n = read(STDIN_FILENO, buf, BUFFSIZE)) > 0)
        if (write(STDOUT_FILENO, buf, n) != n)
            err_sys("write error");

    if (n < 0)
        err_sys("read error");

    exit(0);
}

  上述的代碼主要測試BUFFSIZE的大小與讀寫的效率,主要利用Linux Shell的測試的效率。

  準備了一個504M的文本文件,以下:

  

  用以下命令測試不一樣BUFFERSIZE下的讀寫時間:

time -p ./a.out < ./loadlog_tsinghua.txt > o

  先通過標準輸入讀入txt文件,再通過標準輸出寫入文件o中。

  測試BUFFSIZE爲不一樣的值:

  524288

  

  8192

  

  4096

  

  2048

  

  1024

  

  32

  

  經過上述的比較,選擇一個合適的buffer size是能夠提升讀寫效率的。

  最優的讀寫速度集中在4096這個buffer size左右,這個與測試的linux環境系統的block size有關係。

 

3.10 File Sharing

  這個部分主要針對這樣一個問題:不一樣的process可能對同一個文件進程寫或者讀操做;假設上述操做都是可行的,那麼背後的機制大概是什麼樣的。

  書上舉了一個通常性的文件共享結構(可能與實際的不徹底相同,可是能夠幫助理解原理

  

  (1)每一個進程都有一個process table entry,裏面的內容都是file descriptos

  (2)每一個file descriptor中包含兩部份內容:

      a. fd flags:文件操做方式

      b. file pointer:指向一個file table entry

  (3)每一個file  table entry中包含以下的內容:

      a. file status flags:指的是read, write, append, sync, nonblocking等

      b. 當前的文件offset

      c. 指向v-node table entry的指針

  經過上面的結構分析:一個文件只能有一個v-node table entry;可是不一樣的file descritpor指向同一個v-node table entry。這樣就實現了文件在不一樣進程中的共享。

  經過上述的結構,從新分析以前提到的幾個讀寫操做函數:

    (1)執行了write操做後,current file offset增長的數量就是寫入的數量

    (2)若是文件以O_APPEND的flag被打開,執行了write的時候,current file offset先從i-node中獲取當前文件的size,而後再執行write操做,這樣就保證了必定是append的

    (3)lseek的操做只修改了file table entry中的current file offset,並不會產生I/O操做

  除此以外,還有一個問題須要注意:能夠有多個file descriptor指向同一個file table entry的(詳情見dup函數);所以也帶來一個問題,process table entry中的file flags和file table entry中的file status flags的影響面是不一樣的:

   (1)file flags影響的只是單個進程中的單個file descriptor

   (2)file status flags影響的是全部連到這個file table entry的進程的fd

  如何對file descriptor flags和file status flags都進行有效的操做呢?這就裏就有一個管家級的函數fcntl,後面會提到。

 

 

3.11 Atomic Operations

  原子操做這個概念以前就見過了,書上給出了更好的定義:指的是包含好幾步的operation,要麼一塊兒執行完了,要麼都不執行,不存在只執行了部分步驟的過程。

  這部分只給出了簡單的例子,多個process對同一個文件執行寫操做,那麼就容易產生不一樣步的現象。

  或者兩個進程都要對同一個文件執行append的操做:以前說過了append必然要通過lseek的過程,這裏就可能存在不一樣步。

  後續4.15和14.3章節會給出更具體的例子。

  

  

3.12 dup and dup2 Functions

  int dup(int fd)

  這個函數的做用是:複製已有的file descriptor。

  函數的返回值,仍是lowest-numbered available file descriptor。

  那麼,若是須要指定返回的file descriptor的值怎麼辦呢?還有另外一個相似功能的函數:int dup2(int fd, int fd2)

  其中fd2是但願獲得的的file descriptor值,分以下幾種狀況:

    (1)若是fd2已經打開了,那麼先關上,再返回fd2;此時fd2就與fd指向同一個fie entry table

    (2)若是fd與fd2相等,則返回的就是fd2

    (3)不然fd2的FD_CLOEXEC fle descriptor flag被清空了

  總的來講,最後達到的效果以下圖所示:

  

  這種狀況屬於同一個process中對同一個文件產生了不一樣的file descriptor。

 

3.13 sync, fsync, and fdatasync Functions

  這裏先介紹UNIX system的文件系統的一種delay-write的機制:在執行寫操做的時候,通常先把內容寫到buffer中,再queue到隊列中,最後在某個時候把內容寫入到disk中

  上面描述的內容的核心就是:這裏執行完write操做,並非等着內容真的寫到disk上才返回的;若是須要強寫同步的環境,則須要考慮delay-write的影響。爲了更好的理解這一部分的幾個函數,須要對unix系統io操做的概貌有一個簡略的瞭解。

  在網上找了一個有全貌的blog(http://blog.csdn.net/ybxuwei/article/details/22727565

  

  int sync(void)

    這是一個相似全局update催促函數,把kernel's blocks buffer(內核緩衝區)flush,可是不等待write完成了再回來。

  int fsync(void)

    只針對single file,fsync把kernel's block buffer內容flush,而且必須等着內容寫入後才返回,嚴格寫同步。

  int fdatasync(void)

    跟fsync功能相似,只不過只等着文件數據部分更新,不等着文件屬性信息更新完。

  

3.14 fcntl Function

  int fcntl(int fd, int cmd, ... );

  這是個比較綜合的函數,主要操做與file descriptor相關的內容。

  cmd表示命令的格式,由各類宏定義符號表示,好比F_GETFL表示就是返回file descriptor對應的file status flags的值(這裏須要注意,file descriptor有個指針指向file table entry, file status flags是屬於file table entry裏的值;具體見上面的關係圖,就能夠搞清楚了

  示例代碼以下:

#include <unistd.h>
#include <fcntl.h>
#include <stdio.h>
#include <stdlib.h> 

int main(int argc, char *argv[])
{
    int val;
    val = fcntl(atoi(argv[1]), F_GETFL, 0); /*F_GETFL返回file status flags*/
    switch (val & O_ACCMODE)
    {
        case O_RDONLY:
            printf("read only");
            break; 
        case O_WRONLY:
            printf("write only");
            break;
        case O_RDWR:
            printf("read write");
            break;
        default: 
            printf("error\n");
    }
    if (val & O_APPEND) { 
        printf(", append"); 
    }  
    if (val & O_NONBLOCK) { 
        printf(", nonblcking"); 
    } 
    if (val & O_SYNC) { 
        printf(", synchronous writes"); 
    } 
    putchar('\n');
    exit(0);
}

  代碼執行結果以下:

  

  上面的例子體驗了一下fcntl操做file descriptor的效果。

  書上在這一部分還對delay-write的機制進行了討論,若是用同步機制fsync會形成什麼影響。

  修改3.5.c的代碼以下(主要功能仍是從STDIN讀再從STDOUT寫,測試讀寫時間):

#include "apue.h" 
#include <unistd.h>
#include <stdlib.h>
#include <errno.h> 
#include <fcntl.h>

#define BUFFSIZE 524288

void set_fl(int fd, int flags)
{
    int val;
    if ((val=fcntl(fd, F_GETFL,0))<0) { 
        err_sys("fcntl F_GETFL error"); 
    } 
    val |= flags;
    if (fcntl(fd, F_GETFL, val)<0) { 
        err_sys("fcntl F_GETFL error"); 
    } 
}

int main(int argc, char *argv[])
{
    /*set_fl(STDOUT_FILENO, O_SYNC);*/
    int n;
    char buf[BUFFSIZE];
    
    while ((n=read(STDIN_FILENO, buf, BUFFSIZE))>0) { 
        write(STDOUT_FILENO, buf, n);
        fsync(STDOUT_FILENO);
    } 
    if (n<0) { 
        err_sys("read error"); 
    } 
}

  這部分代碼主要增長了write同步的功能,有兩種實現方法:

  (1)利用fcntl函數設置O_SYNC的flags

  (2)更直接一些,直接在每次執行write緊跟着調用fsync(fd)

  另外還有一個因素是BUFFSIZE取多少的問題,咱們下面一個個分類討論。

  1. 測試O_SYNC是否有用?

    這個不上圖了,通過測試,把O_SYNC設置上了,並無對讀寫速度形成影響。

  2. 測試fsync是否會對執行速度形成影響?

    首先將BUFFSIZE設爲4096,執行的時間太長了,沒等到執行完,截圖以下:

    

    咱們作實驗的文件大小是504M而每次寫的buffer是0.004M,這樣須要等待的次數是巨大的。最起碼在我實驗的系統上是等不起這樣的同步的。

    若是咱們只等數據呢?把fdatasync(fd)呢?效果仍是然並卵,因此這樣的方式真的是不科學的。

    下面再把BUFFSIZE的數值改到比較大的524,288,結果以下:

    

    最起碼說明,若是BUFFSIZE比較大的時候,由於同步等待的次數少了(504M的文件大小0.524M的buffer已經能夠比4096能夠接受的多了

 

    看書看到這裏,大概瞭解了爲何要有delay-write的機制,還有以前提到的緩存機制。

    這一章開頭的說的read() write()是unbuffered I/O並非徹底真的不帶緩存,直接往disk上幹:而是說在用戶層沒有緩存;這兩者在kernel層仍是設有緩存的。好比,kernel的緩存是100byte,每次write寫的是10byte,則把kernel的緩存寫了10次滿了以後,才真的輸出到disk上。若是在用戶層增長一個緩存層,50bytes爲buffer size,則系統層調用兩次write就能夠執行真正的I/O操做了,減小了調用調用wrtie()次數。

    對於這個問題,還須要後面的章節繼續加深理解;

    目前搜到了這篇文章,講的比較透徹(http://www.360doc.com/content/11/0521/11/5455634_118306098.shtml

相關文章
相關標籤/搜索