【APUE】Chapter5 Standard I/O Library

5.1 Introduction安全

  這章介紹的standard I/O都是ISOC標準的。用這些standard I/O能夠不用考慮一些buffer allocation、I/O optimal-sized的細節,增長了易用性。可是也有一些問題。數據結構

 

5.2 Streams and FILE Objects架構

  1. Chapter3中提到的I/O routines的核心是file descriptor;而在standard I/O背景下,相應的概念換成了stream。函數

  2. standard I/O能夠設置signle character和multi character的不一樣模式。spa

 

5.3 Standard Inpupt, Standard Output, and Standard Error設計

  STDIN_FILENO, STDOUT_FILENO, STDERR_FILENO 也在standard I/O中有定義。頭文件在<stdio.h>中rest

 

5.4 Bufferingcode

  standard I/O中調用了readwrite這樣的calls。而standard I/O的設計目的,有一條就是儘可能減少調用readwrite的次數。orm

  buffering的方式分爲如下三種:blog

  1. Fully buffered

    把buffer寫滿了就往外輸出。若是強制往外輸出,能夠調用fflush函數。

  2. Line buffered

    根絕是否出現newline character判斷是否往外輸出buffer的內容。

    可是,有些時候Line buffered等不到newline character也會被動刷新輸出:

    (1)standard I/O留給Line buffer的容量寫滿了,等不到newline character出現就刷新輸出。這個比較好理解,滿了就輸出了。

    (2)當有來自unbuffered stream或line-buffered的input請求,會強制刷新輸出。這個書上講的沒理解,具體看到例子再說。

  3. Unbuffered

    這個比較好理解,不用buffer直接輸出。例子,通常的error stream都是unbuffered的方式。

  另,介紹了一個函數fflush(FILE *fp) 當fp==NULL的時候,刷新所有的output stream。

 

5.5 Opening a Stream

  記住倆函數:

  fopen(const char *restrict pathname, const char *restrict type) (char *restrict意思是參數只能是受限制的字符串,這裏只能是'w','r'這類的限制的幾個字符串)

  fclose(FILE *fp)

  

5.6 Reading and Writing a Stream

  讀單個字符的三個函數:

  int getc(FILE *fp)

  int fgetc(FILE *fp)

  int getchar(void)

  三個函數的返回值有以下幾種狀況:

  (1)若是執行成功且沒有到末尾,則返回next character

  (2)若是執行到文件末尾了,則返回EOF

  (3)若是出錯了,則返回error

  書上關於返回值有這樣的陳述「These three functions return the next character as an unsigned char converted to an int」。

  讀的是字符爲何返回值要採用整數的形式?

  由於,能夠區分正常字符、文件末尾、出錯三種狀況。若是是正常字符,轉換成int後都是正的;若是是EOF和error則都是負的。

  有的系統上EOF和error都是-1,那麼還怎麼區分是出錯了仍是讀到文件末尾了呢?

  還有如下兩個函數,判斷究竟是error爲真,仍是eof爲真:

  int ferror(FILE *fp)

  int feof(FILE *fp)

  若是是真的,則返回非零;不然,返回零。

 

5.7 Line-at-a-Time

  char *fgets(char *restrict buf, int n, FILE *restrict fp)

  一次讀一行,最多讀n-1個字符;超過n-1個字符的部分就被抹掉了。n-1的緣由是buffer必須是以null結束的

  int fputs(const char*restrict str, FILE *restrict fp)

  注意是否用補上換行符。

  

5.8 Standard I/O Efficiency

  比較兩種讀寫模式的效率,讀一個500M大小的文件,而後在寫到一樣的文件中;比較這一讀一寫的效率。

  代碼1:

 1 #include "apue.h"
 2 
 3 int main()
 4 {
 5     int c;
 6     while ((c=getc(stdin))!=EOF)
 7         if (putc(c, stdout)==EOF)
 8             err_sys("output error");
 9     if (ferror(stdin))
10         err_sys("input error");
11     exit(0);
12 }

  執行耗時以下:

  

  代碼2:

 1 int main()
 2 {
 3     char buf[MAXLINE];
 4     while (fgets(buf, MAXLINE, stdin)!=NULL)
 5     {
 6         if (fputs(buf,stdout)==EOF) { 
 7             err_sys("output error"); 
 8         } 
 9     }
10 
11     if (ferror(stdin)) {
12          err_sys("input error");
13     } 
14     exit(0);
15 }

  執行耗時以下:

  

  代碼1是逐個讀字符,代碼2是逐行讀字符。對比兩者執行結果,能夠得到如下的結論:

  (1)對比兩者的耗時,發現system time都差很少:這意味着kernel耗時都差很少。

  (2)差異在於user time不同:緣由就在於fgetc fputc讀寫一樣的字符串須要調用的系統read write次數多;而fgets fputs調用的read write次數少不少,因此real time上體現了效率提升了。

 

5.9 Binary I/O

  size_t fread(void *restrict ptr, size_t size, size_t nobj, FILE *restrict fp)

  size_t fwrite(const void *restrict ptr, size_t size, size_t nobj, FILE *restrict fp)

  每次讀寫一個完整的數據結構:struct int float這樣的內容。

  這樣binary I/O的關鍵就在於這樣一個完整的數據結構佔多少byte。

  書上提醒,完整的數據結構到底佔多少byte跟編譯器complier有關,還跟不一樣的機器架構有關,即提醒此處可能有坑。

 

5.10 Positioning a Stream

  long ftell(FILE *fp) :返回current file position indicator if OK, -1L on error

  int fseek(FILE *fp, long offset, int  whence) :操做stream;經過whence參數控制操做的方式;成功返回0,出錯返回-1

  void rewind(FILE *fp): stream移動到文件的beginning

  類比Chapter3.6提出來的lseek()函數,lseek這個函數集成的功能比較多,能夠直接獲得fd對應的offset位置;而fseek因爲只能返回01,所以須要單拎出來ftell這個函數來返回stream的當前位置

  書上還介紹了兩個ISO C standard的函數:

  int fgetpos(FILE *restrict fp, fpos_t *restrict pos)

  int fsetpos(FILE *fp, const fpos_t *pos)

  書上對這兩個函數給出的tips是便於porting to non-Unix systems (便於移植)

 

5.11 Formatted I/O

  printf和scanf函數,用到再細看

 

5.12 Implementation Details

  1. 給出了Unix System中,standard I/O最終仍是調用了Chapter3中介紹的各類I/O routine來完成的。所以,每一個standard I/O stream必然對應一個file descriptor。靠int fileno(FILE *fp)來實現這個功能。

  2. 給出了一個簡易的buffering three standard stream的實現,具體以下:  

#include "apue.h"

void pr_stdio(const char *, FILE *);
int is_unbuffered(FILE *);
int is_linebuffered(FILE *);
int buffer_size(FILE *);

int main()
{
    FILE *fp;
    fputs("enter any character\n", stdout);
    if(getchar()==EOF)
        err_sys("getchar error");
    fputs("one line to standard error\n",stderr);

    pr_stdio("stdin", stdin);
    pr_stdio("stdout", stdout);
    pr_stdio("stderr", stderr);

    if ((fp=fopen("/etc/passwd","r"))==NULL) { 
        err_sys("fopen error"); 
    }
    if (getc(fp)==EOF)
        err_sys("getc error");
    pr_stdio("/etc/passwd",fp);
    exit(0);
}

void pr_stdio(const char *name, FILE *fp)
{
    printf("stream = %s, ", name);
    if (is_unbuffered(fp)) { 
        printf("unbuffered"); 
    } 
    else if (is_linebuffered(fp)) { 
        printf("line buffered"); 
    } 
    else
        printf("fully buffered");
    printf(", buffer size = %d\n", buffer_size(fp));
}

#if defined(_IO_UNBUFFERED)
int is_unbuffered(FILE *fp)
{
    return(fp->_flags & _IO_UNBUFFERED);
}

int is_linebuffered(FILE *fp)
{
    return(fp->_flags & _IO_LINE_BUF);
}

int buffer_size(FILE *fp)
{
    return(fp->_IO_buf_end - fp->_IO_buf_base);
}

#elif defined(__SNBF)
int is_unbuffered(FILE *fp)
{
    return(fp->_flags & __SNBF);
}

int is_linebuffered(FILE *fp)
{
    return(fp->_flags & __SLBF);
}

int buffer_size(FILE *fp)
{
    return(fp->_bf.size);
}

#elif defined(_IONBF)

#ifdef  _LP64
#define _flag __pad[4]
#define _flag __pad[1]
#define _base __pad[2]
#endif

int is_unbuffered(FILE *fp)
{
    return(fp->_flags & _IONBF);
}

int is_linebuffered(FILE *fp)
{
    return(fp->_flags & _IOLBF);
}

int buffer_size(FILE *fp)
{
#ifdef _LP64
    return(fp->_base - fp->_ptr);
#else
    return(BUFSIZ);
#endif
}

#else

#error unknown stdio implementation

#endif

  執行這個代碼,結果以下:

  

  (1)若是輸入輸出都是終端,則系統給出的buffer策略是line buffered

  (2)若是輸入輸出都是文件,則系統給出的buffer策略是fully buffered

  (3)若是是error輸出,則默認是unbuffered策略

  

5.13 Temporary Files

  1. 給了char *tmpnam(char *ptr) 和 FILE *tmpfile(void)的例子,使用方法。

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

#define MAXLINE 4096

int main()
{
    char name[L_tmpnam], line[MAXLINE];
    FILE *fp;

    printf("%s\n", tmpnam(NULL));
    
    tmpnam(name);
    printf("%s\n",name);

    fp = tmpfile();
    fputs("one line of output\n",fp);
    rewind(fp);
    fgets(line, sizeof(line), fp);
    fputs(line, stdout);
    exit(0);
}

  代碼執行結果以下:

  

  (1)前兩個printf是講tmpnam的用法,做用是生成個獨一無二的文件名;並展現了參數是NULL和不是NULL都是怎麼個用法。

  (2)後面一段是先用tmpfile()生成一個臨時文件,向文件中寫一行,再把寫進去的內容讀出來輸出到終端。這樣證實確實產生了臨時文件,而且隨着程序結束,臨時文件也沒有了。

  

  2. 介紹了更爲安全的兩個臨時文件函數char *mkdtemp(char *template) 和 int mkstemp(char *template)

    從緊上面1的代碼執行結果中能夠看到有個關於tmpnam的warning。這個warning的緣由是什麼呢?

    tmpnam函數有個弊端:tmpnam產生獨一無二的臨時文件名並非一個原子操做;可能有個time window,tmpnam產生文件名A的同時,另外一個應用產生相同文件名的文件了。

    所以,就須要引出mkstemp這樣的函數,保證是原子操做,即函數產生的文件名是獨一無二的。

    首先得明確一下臨時文件的產生過程:1. 生成一個獨一無二的臨時文件名 2.立刻unlink了

    爲何要這麼作呢?kernel判斷一個file是否能夠被delete主要取決於兩點:

      1. link數是0了

      2. 沒有process在使用這個file

    所以,按照上述臨時文件的原理,先生成文件名,而後unlink,則等着當前的process結束,或放棄對file的佔用,kernel就把file給delete了。

    這裏須要注意的是,unlink這個操做須要咱們本身來完成。

    看個例子:

 1 #include "apue.h"
 2 #include <errno.h>
 3 
 4 void make_temp(char *template)
 5 {
 6     int fd;
 7     struct stat sbuf;
 8 
 9     if ((fd = mkstemp(template))<0) { 
10         err_sys("can't create temp file"); 
11     } 
12     printf("temp name = %s\n", template);
13     close(fd);
14 
15     if (stat(template,&sbuf)<0) { 
16         if(errno==ENOENT)
17             printf("file doesn't exist\n");
18         else
19             err_sys("stat failed");
20     } 
21     else
22     {
23         printf("file exists\n");
24         unlink(template);
25     }
26 }
27 
28 int main()
29 {
30     char good_template[] = "/tmp/dirXXXXXX";
31     char *bad_template = "/tmp/dirXXXXXX";
32 
33     printf("tring to crate first temp file..\n");
34     make_temp(good_template);
35     printf("trying to create second temp file...\n");
36     make_temp(bad_template);
37     exit(0);
38 }

    代碼執行結果以下:

    

    從這段代碼中能夠看到,臨時文件確實是存在的(「file exists」能夠說明),而且須要咱們手工unlink。

    這裏還涉及到一個細節,常量字符串不能修改,若是把常量字符串送到mkstemp函數中,會報段錯誤。由於,常量字符串在只讀segment上面,mkstemp函數要修改這個常量字符串就會報錯。

 

5.14 Memory Streams

  這個部分說的是:是否能夠像讀寫文件同樣操做內存中的一塊區域。下面這個函數就幫助user實現了這樣的功能。

  FILE *fmemopen(void *restrict buf, size_t size, const char *restrict type)

  直接看一段代碼:

 1 #include "apue.h"
 2 
 3 #define BSZ 48
 4 
 5 int pr_print(char buf[])
 6 {
 7     int i;
 8     for ( i=0; i<BSZ; ++i) printf("%c",buf[i]=='\0'?'#':buf[i]);
 9     printf("\n");
10 }
11 
12 int pr_offset(FILE *fp)
13 {
14     printf("    memeory stream offset: %ld\n",ftell(fp)); 
15 }
16 
17 int main()
18 {
19     FILE *fp;
20     char buf[BSZ];
21     
22     memset(buf, 'a', BSZ-2);
23     buf[BSZ-2]='\0';
24     buf[BSZ-1] = 'X';
25     printf("befor fmemopen initial buffer contents: ");pr_print(buf);
26     fp = fmemopen(buf, BSZ, "w+");
27     pr_offset(fp);
28     printf("after fmemopen initial buffer contents: ");pr_print(buf);
29     fprintf(fp, "hello, world"); /*此時fp標誌在buf的起始位置*/
30     //pr_offset(fp);
31     printf("before flush: %s\n",buf);
32     fflush(fp);
33     printf("after fflush:");
34     pr_print(buf); /*原本buf的內容是aaaa...\0#,可是因爲fprintf的操做hello world寫到前面buf的前面幾個位置,而且後面跟了一個\0*/
35     pr_offset(fp);
36     printf("len of string in buf = %ld\n", (long)strlen(buf));
37 
38     memset(buf, 'b', BSZ-2);
39     buf[BSZ-2] = '\0';
40     buf[BSZ-1] = 'X';
41     fprintf(fp, "hello, world");
42     pr_offset(fp);
43     fseek(fp, 0, SEEK_SET);
44     printf("after fseek:");
45     pr_offset(fp);
46     pr_print(buf);
47     printf("len of string in buf = %ld\n", (long)strlen(buf));
48 
49     memset(buf, 'c', BSZ-2);
50     buf[BSZ-2] = '\0';
51     buf[BSZ-1] = 'X';
52     fprintf(fp, "hello, world");
53     fclose(fp);
54     printf("after fclose:");
55     pr_print(buf);
56     printf("len of string in buf = %ld\n", (long)strlen(buf));
57 
58     return(0);
59 }

    代碼執行結果以下:

    

    上述代碼在書上5.15的例子基礎上作了修改,緣由是更好的理解各類操做對實際內存數據的影響。

    這裏有個點須要明確:printf函數在輸出內存中字符串的時候,如何判斷字符串結束了?首次碰見'\0'就認爲字符串結束了

    下面按照執行順序來分析爲:

    1. memset()以後,以buf爲起始地址的內存字符串都被賦值爲'a'了。

    2. 執行fmemopen,將buf以w+的方式關聯到一個FILE *類型變量fp上。這個時候,看書上P171的闡述,以‘w+’這種方式打開的時候,""truncate to 0 length and open for writing"。

      (1)爲了驗證書上這句話,我調用了DIY的函數pr_offset,來查看此時current file position(具體ftell函數,能夠參考書上P158),stream offset確實是0。

      (2)那麼stream offset是0意味着什麼呢?我調用了DIY的函數pr_print函數,逐個輸出buf開始的BSZ個字符:發現原來的首個字符'a'變成了'\0',客觀上在我實驗的環境中就是這樣。

    3. 執行fprintf函數,向fp指向的buf寫「hello, world」12個byte。可是這個寫並不會立刻寫進去:

      (1)執行fflush以前,buf的首個字符仍是'\0',所以用printf函數輸出buf,天然什麼都不會輸出。

      (2)執行fflush以後,fp的內容被寫進去了;調用pr_print函數能夠看到"hello, world"被寫進去了,而且緊接着還寫進去了一個'\0';此時fp的offset變成了12,並且用strlen函數去統計buf的長度也是12。因而可知fprintf的工做方式,並且能夠看到strlen的實現也差很少是根據第一個碰見的'\0'來統計字符串長度的。

    4. 如今另起爐竈,把buf後面的BSZ長度的字符都設成'b'了。

      (1)仍是調用fprintf往fp裏面再寫一次「hello, world」;注意,此時fp的offset是12,跟執行memset沒有關係,此時執行過第二次寫「hello, world」以後,fp的offset變成了24。

      (2)此時,執行fseek,將fp的offset移動到0,並用pr_offset函數驗證。

      (3)再次輸出buf起始的BSZ個字符,發現「hello, world"確實從第13個位置寫到了第24個位置,而且在末尾跟上了一個'\0',驗證了上面的分析。

    5. 再次處理一下,把buf後面的BSZ長度的字符都設成'c'了。

      (1)先明確一點,此時fp的offset雖然恢復0了,可是fp關聯的memory stream的amount of data仍是24個字符,這一點比較關鍵。

      (2)此時仍是fprintf往fp裏面寫"hello, world"12個字符,隨後把fp關掉。

      (3)此時,再用pr_print逐個輸出buf起始的BSZ個字符,發現了一個與以前不一樣的地方,即"hello, world"的後面沒有在跟着一個'\0'。回想(1)中提到的,此時fp關聯的memory steam的amount仍是48,若是從0開始寫12個byte,並不會改變整個memory stream的amount,所以後面就沒有再跟着'\0'了,這也就解釋了最後的一組輸出。

    6. 若是咱們將上述代碼的30行的屏蔽符號去掉,在執行代碼,會獲得如下結果:

    

    這裏只須要注意before flush後面的輸出便可:爲何沒有用fflush,仍是刷出來了buf呢?

    我猜,這是由於ftell(fp)中有刷新fp的操做,因此至關於隱藏着調用了一個fflush了。

  這個memory streams一開始看的並不清晰,主要是不明確printf的實現原理,遇到'\0'不輸出後面的type了。因此,掌握這些基礎函數的原理對提升工做效率比較有幫助。

相關文章
相關標籤/搜索