.
.
.
.
.
最近在學習 APUE,所以順便將每日所學記錄下來,一方面為了鞏固學習的知識,另一方面也為同樣在學習APUE的童鞋們提供一份參考。
本系列博文均根據學習《UNIX環境高級編程》一書總結而來,如有錯誤請多多指教。
APUE主要討論了三部分內容:文件IO、並發、進程間通信。
文件IO:
標准IO:優點是可移植性高,缺點是性能比系統 IO 差,且功能沒有系統 IO 豐富。
系統IO:因為是內核直接提供的系統調用函數,所以性能比標准 IO 高,但是可移植性比標准 IO 差。
並發:
信號 + 多進程;
多線程;
進程間通信:
FIFO:管道;
System V:又稱為 XSI,支持以下三種方式:
msg:消息隊列;
sem:信號量;
shm:共享存儲;
Socket:套接字(網絡通信);
本系列博文就是圍繞著這些內容進行學習和總結出來的,但是APUE一書講述的主要是 Unix 環境,而 LZ 用的是 Linux 環境,所以本系列博文的所有內容都是基於 Linux 環境的,僅供各位童鞋參考。LZ 盡量多講原理,少講函數的具體使用,在使用 LZ 提到的函數的時候,如與各位開發環境的 man 手冊沖突,則以 man 手冊為准。
==============================華麗的分割線==============================
好了,通過簡單的介紹相信大家對 APUE 的結構已經有了一個大致的了解了,接下來就開始今天的正題:文件 IO。
今天講的所有的 IO 操作都是標准 IO,如果是方言我會單獨標識出來。
首先要了解的一個概念是文件位置指針。
當我們打開一個文件要對它進行讀寫的時候,我們怎麼能知道要從哪裡開始讀(寫)文件呢?其實標准庫准備了一個工具輔助我們讀寫文件,它就是文件位置指針。當我們使用標准庫函數操作文件的時候,它會自動根據文件位置指針找到我們要操作的位置,也會隨著我們的讀寫操作而自動修改指向,而不用我們自己手動記錄和修改文件的操作位置。它使用起來非常方便,以至於你完全感覺不到它的存在,但是為了更好的理解文件 IO,你必須知道它的作用。
1.fopen(3)
1 fopen - stream open functions 2 3 #include <stdio.h> 4 5 FILE *fopen(const char *path, const char *mode);
這是今天要學習的第一個函數,在操作文件之前,我們需要通過 fopen() 函數將文件打開,通過這個函數我們可以告訴操作系統我們要操作的是哪個文件,以及用什麼樣的方式操作這個文件。
參數列表:
path:要操作的文件路徑。
mode:文件的打開方式,這個打開方式一共分為6種。
r:以只讀的方式打開文件,並且文件位置指針會被定位到文件首。如果要打開的文件不存在則報錯。
r+:以讀寫的方式打開文件,並且文件位置指針會被定位到文件首。如果要打開的文件不存在則報錯。
w:以只寫的方式打開文件,如果文件不存在則創建,如果文件已存在則被截斷為 0 字節,並且文件位置指針會被定位到文件首。
w+:以讀寫的方式打開文件,如果文件不存在則創建,如果文件已存在則被截斷為 0 字節,並且文件位置指針會被定位到文件首。
a:以追加的方式打開文件,如果文件不存在則創建,且文件位置指針會被定位到文件最後一個有效字符的後面(EOF,end of the file)。
a+:以讀和追加的方式打開文件,如果文件不存在則創建,且讀文件位置指針會被初始化到文件首,但是總是寫入到最後一個有效字符的後面(EOF,end of the file)。
返回值:
FILE 是一個由標准庫定義的結構體,各位童鞋不要企圖通過手動修改結構體裡的內容來實現文件的操作,一定要通過標准庫函數來操作文件。
這個函數返回一個 FILE 類型的指針,它作為我們打開文件的憑據,後面所有對這個文件的操作都需要使用這個指針,而且使用之後一定不要忘記調用 fclose(3) 函數釋放資源。
如果該函數返回了一個指向 NULL 的指針,則表示文件打開失敗了,可以通過 errno 獲取到具體失敗的原因。
2.fclose(3)
1 fclose - close a stream 2 3 #include <stdio.h> 4 5 int fclose(FILE *fp);
這個函數是與 fopen(3) 函數對應的,當我們使用完一個文件之後,需要調用 fclose(3) 函數釋放相關的資源,否則會造成內存洩漏。當一個 FILE 指針被 fclose(3) 函數成功釋放後,這個指針所指向的內容將不能再次被使用,如果需要再次打開文件還需要調用 fopen(3) 函數。
參數列表:
fp:fopen(3) 函數的返回值作為參數傳入即可。
3.fgets(3)
1 fgets - input of strings 2 3 #include <stdio.h> 4 5 int fgetc(FILE *stream); 6 7 char *fgets(char *s, int size, FILE *stream);
從輸入流 stream 中讀取一個字符串回填到 s 所指向的空間。
這裡出現了一個 stream 的概念,這個 stream 是什麼呢,它被成為“流”,其實就是操作系統對於可以像文件一樣操作的東西的一種抽象。它並非像自然界的小河流水一樣潺潺細流,而通常是要麼沒有數據,要麼一下子來一坨數據。當然 stream 也未必一定就是文件,比如系統為每個進程默認打開的三個 stream:stdin、stdout、stderr,它們本身就不是文件,就是與文件有著相同的操作方式,所以同樣被抽象成了“流”。
這個函數並沒有解決 gets(3) 函數可能會導致的數組越界問題,而是通過犧牲了獲取數據的正確性來保證程序不會出現數組越界的錯誤,實際上是掩蓋了 gets(3) 的問題。
該函數遇到如下四種情況會返回:
1.當讀入的數據量達到 size - 1 時;
2.當讀取的字符遇到 \n 時;
3.當讀取的字符遇到 EOF 時;
4.當讀取遇到錯誤時;
並且它會在讀取到的數據的最後面添加一個 \0 到 s 中。
返回值:
成功時返回 s。
返回 NULL 時表示出現了錯誤或者讀到了 strem 的末尾(EOF)。
4.fread(3)、fwrite(3)
1 fread, fwrite - binary stream input/output 2 3 #include <stdio.h> 4 5 size_t fread(void *ptr, size_t size, size_t nmemb, FILE *stream); 6 7 size_t fwrite(const void *ptr, size_t size, size_t nmemb, FILE *stream);
這兩個函數使用得最頻繁,用來讀寫 stream,通常是用來讀寫文件。
參數列表:
ptr:fread(3) 將從 stream 中讀取出來的數據回填到 ptr 所指向的位置;fwrite(3) 則將從 ptr 所只想的位置讀取數據寫入到 stream 中;
size:要讀取的每個對象所占用的字節數;
nmemb:要讀取出多少個對象;
stream:數據來源或去向;
返回值:
注意這兩個函數的返回值表示的是成功讀(寫)的對象的個數,而不是字節數!
例如:
read(buf, 1, 10, fp); // 讀取 10 個對象,每個對象 1 個字節
read(buf, 10, 1, fp); // 讀取 1 個對象,每個對象 10 個字節
當數據量充足的時候,這兩種方式是沒有區別的。
但是!!當數據量少於 size 個字節的整倍數時,第二種方法的的最後一個對象會讀取失敗。比如數據只有 45 個字節,那麼第二種方法的返回值為 4,因為它只能成功讀取 4 個對象。
所以通常第一種方式讀寫數據使用得比較普遍。
5.atoi(3)
1 atoi, atol, atoll, atoq - convert a string to an integer 2 3 #include <stdlib.h> 4 5 int atoi(const char *nptr); 6 long atol(const char *nptr); 7 long long atoll(const char *nptr); 8 long long atoq(const char *nptr);
atoi(3) 函數族在這裡提一下,主要是為了下面的 printf(3) 函數族做一個鋪墊。
這些函數的作用是方便的將一個字符串形式的數字轉換為對應的數字類型的數字。
上面這句話可能有點坳口,給你看個例子就懂了,下面是偽代碼。
1 char *str = "123abc456"; 2 int i = 0; 3 i = atoi(str);
i 的結果會變成 123。這些函數會轉換一個字符串中地一個非有效數字前面的數字。如果很不幸這個字符串中的第一個字符就不是一個有效數字時,那麼它們會返回 0。
6.printf(3)
1 printf, fprintf, sprintf, snprintf - formatted output conversion 2 3 #include <stdio.h> 4 5 int printf(const char *format, ...); 6 int fprintf(FILE *stream, const char *format, ...); 7 int sprintf(char *str, const char *format, ...); 8 int snprintf(char *str, size_t size, const char *format, ...);
printf(3) 函數大家一定不會陌生了,應該從寫 Hello World! 的時候就接觸到了的吧,所以我也不多介紹了,主要介紹兩個內容。
一個是面試常考的一個問題,用了這麼久的 printf(3) 函數,大家有沒有注意過它的返回值表示什麼呢?
printf(3) 的返回值表示成功打印的有效字符數量,不包括 \0。
另一個要說的就是剛才我們提到了 atoi(3) 函數族,它們負責將字符串轉換為數字,那麼有沒有什麼函數可以將數字轉換為字符串呢,其實通過 sprintf(3) 或 snprintf(3) 就可以了。
有了這兩個函數,不僅可以方便的將數字轉換為字符串,還可以將多個字符串任意拼接為一個完整的字符串。
這裡直接講解一下 snprintf(3) 函數。
參數列表:
str:拼接之後的結果會回填到這個指針所指向的位置;
size:size - 1 為回填到 str 中的最大長度,數據超過這個長度的部分則會被捨棄,然後會在拼接的字符串的尾部追加 \0;
format:格式化字符串,用法與 printf(3) 相同,這裡不再贅述;
...:格式化字符串的參數,用法與 printf(3) 相同;
這個函數與 fputs(3) 一樣,只是掩蓋了 sprintf(3) 可能會導致的數組越界問題,通過犧牲數據的正確性來保證程序不會出現數組越界的錯誤。
7.scanf(3)
1 scanf, fscanf, sscanf - input format conversion 2 3 #include <stdio.h> 4 5 int scanf(const char *format, ...); 6 int fscanf(FILE *stream, const char *format, ...); 7 int sscanf(const char *str, const char *format, ...);
scanf(3) 函數族相信也不用過多的介紹了,這裡唯一要強調的就是:scanf(3) 函數支持多種格式化參數,唯獨 %s 是不能安全使用的,可能會導致數組越界,所以當需要接收用戶輸入的時候可以使用 fgets(3) 等函數來替代。
8.fseek(3)
1 fgetpos, fseek, fsetpos, ftell, rewind - reposition a stream 2 3 #include <stdio.h> 4 5 int fseek(FILE *stream, long offset, int whence); 6 7 long ftell(FILE *stream); 8 9 void rewind(FILE *stream);
fseek(3) 函數族的函數用來控制和獲取文件位置指針所在的位置,從而能夠使我們靈活的讀寫文件。
介紹一下 fseek(3) 函數的參數列表:
stream:這個已經不需要多介紹了吧,就是准備修改文件位置指針的文件流;
offset:基於 whence 參數的偏移量;
whence:相對於文件的哪裡;有三個宏定義可以作為它的參數:SEEK_SET(文件首), SEEK_CUR(當前位置), or SEEK_END(文件尾);
返回值:
成功返回 0;失敗返回 -1,並且會設置 errno。
單獨看參數列表也許你還有所疑惑,那麼我寫點簡單的偽代碼作為例子:
1 fseek(fp, -10, SEEK_CUR); // 從當前位置向前偏移10個字節。 2 fseek(fp, 2GB, SEEK_SET); // 可以制造一個空洞文件,如迅雷剛開始下載時產生的文件。
ftell(3) 函數以字節為單位獲得文件指針的位置。
fseek(fp, 0, SEEK_END) + ftell(3) 可以計算出文件總字節大小。
還有一個值得大家注意的問題:
fseek(3) 和 ftell(3) 的參數和返回值使用了 long,所以取值范圍為 -2GB ~ (2GB-1),而 ftell(3) 只能表示 2G-1 之內的文件大小,所以可以使用 fseeko(3) 和 ftello(3) 函數替代它們,但它們只是方言(SUSv2, POSIX.1-2001.)。
由於這兩個函數比較古老,所以設計的時候認為 +-2GB 的取值范圍已經足夠用了,而沒有意識到科技發展如此迅速的今天,2GB 大小的文件已經完全不能滿足實際的需求了。
rewind(3) 函數將文件位置指針移動到文件起始位置,相當於:
1 (void) fseek(stream, 0L, SEEK_SET)
9.getline(3)
1 getline - delimited string input 2 3 #include <stdio.h> 4 5 ssize_t getline(char **lineptr, size_t *n, FILE *stream); 6 7 Feature Test Macro Requirements for glibc (see feature_test_macros(7)): 8 9 getline(): 10 Since glibc 2.10: 11 _POSIX_C_SOURCE >= 200809L || _XOPEN_SOURCE >= 700 12 Before glibc 2.10: 13 _GNU_SOURCE
這個函數是一個非常好用的函數,它能幫助我們一次獲取一行數據,而無論這個數據有多長。
參數列表:
lineptr:一個一級指針的地址,它會將讀取到的數據填寫到一級指針指向的位置,並將>該位置回填到該參數中。指針初始必須置為NULL,該函數會根據指針是否為 NULL 來決定是否需要分配新的內存。
n:是由該函數回填的申請的內存緩沖區的總大小,長度初始必須置為0。
雖然很好用,但是各位童鞋別高興得太早了,該函數僅支持 GNU 標准,所以是方言,大家還是自己封裝一個備用吧。
另外,想要使用這個函數必須在編譯的時候指定 -D_GNU_SOURCE 參數:
1 $> gcc -D_GNU_SOURCE
當然如果不想在編譯的時候添加參數,也可以在引用頭文件之前 #define _GNU_SOURCE,只是比較丑陋而已。
還有一個辦法,是在 makefile 中配置 CFLAGS += -D_GNU_SOURCE,這樣即省去了編譯時手動寫參數的麻煩,也避免了代碼中的丑陋。
好了,時間不早了,今天先寫到這裡。