.
.
.
.
.
目錄
(一) 一起學 Unix 環境高級編程 (APUE) 之 標准IO
(二) 一起學 Unix 環境高級編程 (APUE) 之 文件 IO
(三) 一起學 Unix 環境高級編程 (APUE) 之 文件和目錄
(四) 一起學 Unix 環境高級編程 (APUE) 之 系統數據文件和信息
(五) 一起學 Unix 環境高級編程 (APUE) 之 進程環境
(六) 一起學 Unix 環境高級編程 (APUE) 之 進程控制
(七) 一起學 Unix 環境高級編程 (APUE) 之 進程關系 和 守護進程
(八) 一起學 Unix 環境高級編程 (APUE) 之 信號
(九) 一起學 Unix 環境高級編程 (APUE) 之 線程
(十) 一起學 Unix 環境高級編程 (APUE) 之 線程控制
(十一) 一起學 Unix 環境高級編程 (APUE) 之 高級 IO
(十二) 一起學 Unix 環境高級編程 (APUE) 之 進程間通信(IPC)
(十三) [終篇] 一起學 Unix 環境高級編程 (APUE) 之 網絡 IPC:套接字
最近在學習 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 手冊為准。
大家在學習的過程中一定要使用 Linux 系統,每學習到一個新的函數或知識點的時候要自己運行起來。
從 LZ 的經驗來看,僅僅看 LZ 寫的文字和你自己手動做實驗得出的結果是不同的。只看不練的話跟沒看過一樣,什么都是看着簡單,寫起來才知道哪里有坑。
如果你因為工作等關系實在無法在電腦上安裝 Linux 系統,那么在虛擬機里安裝一個 Linux 也是很必要的。
如果你真的想學好 APUE,那么最好使用真正的 Linux。APUE 就是讓你在 Linux 下面寫程序用的,只有使用它才能體會到你平時使用的各種程序和命令是如何實現的,只有是使用它你才知道你需要什么程序,你自己可以寫什么程序。邊使用邊學習你才能體會到 Linux 的種種設計思想,才能融入到整個 *nix 大家庭來。
廢話先寫到這里。預祝大家學習成功,記住,堅持就是勝利。
==============================華麗的分割線==============================
好了,通過簡單的介紹相信大家對 APUE 的結構已經有了一個大致的了解了,接下來就開始今天的正題:文件 IO。
前面提到了標准 IO(STDIO)和系統 IO(SYSIO),那么這里整理一下它們的差別。
| 類型 | 可移植性 | 實時性 | 吞吐量 | 功能 |
| STDIO | 高 | 低 | 高 | 受限 |
| SYSIO | 低 | 高 | 低 | 自由 |
這里我一個一個的解釋表格中的每一項,表格中的每一項都是兩者之間相對而言,使用哪種 IO 並沒有絕對的好壞之分,要根據實際的需求來決定應該使用哪個。
可移植性:
標准 IO 是 C89 支持的函數,所以使用了標准 IO 的程序無論在 Linux 平台還是換成了 Windows 平台,不用修改代碼是可以直接編譯運行的。
而系統 IO 是由內核直接提供的函數庫實現的,不同的操作系統平台上提供的 IO 操作接口是不同的,所以想要移植使用了系統 IO 的程序,必須按照目標平台的 IO 庫修改程序並重新調試。
所以你寫的程序將來可能在不同的平台上運行,那么最好使用標准 IO 庫;如果你的程序是專門針對於某個平台而開發的,那么使用系統 IO 庫能夠得到我們下面說的其它優勢。
實時性和吞吐量:
講這兩個概念之前我先給大家看一段代碼:
1 #include <stdio.h> 2 #include <unistd.h> 3 4 int main (void) 5 { 6 putchar('a'); 7 write(1, "b", 1); 8 9 putchar('a'); 10 write(1, "b", 1); 11 12 putchar('a'); 13 write(1, "b", 1); 14 15 printf("\n"); 16 17 return 0; 18 }
1 >$ gcc -Wall 1.c 2 >$ ./a.out 3 bbbaaa
輸出的結果為什么不是 ababab 呢,這就是因為標准 IO 具有合並系統調用的功能,putchar(3) 將本應該執行多次的 write(2) 動作合並成了一步來完成,所以 aaa 是作為一個字符串打印的,這一點我們可以通過 strace(1) 命令跟蹤系統調用來得出結論(下方第7行)。另外由於 stdout 默認使用的是行緩沖模式(下面會講緩沖),所以對 putchar(3) 的調用並沒有立即打印出來。
1 >$ strace ./a.out 2 # ... 此處省略 n 行不相關內容 3 mmap(NULL, 4096, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0x7f077a35f000 4 write(1, "b", 1b) = 1 5 write(1, "b", 1b) = 1 6 write(1, "b", 1b) = 1 7 write(1, "aaa\n", 4aaa 8 ) = 4 9 exit_group(0) 10 >$
到這里我們還是沒有說明白為什么標准 IO 吞吐量高,而系統 IO 實時性高。我再舉個簡單的栗子:門衛老大爺負責送信到郵局,他去一次郵局要花費 10 分鍾的時間,而每次最多能送 20 封信,每當信件累計到 20 封的時候他就要動身去郵局了。但是當他收到一封加急的郵件時,就會立即去一趟郵局。系統 IO 就好比每收到一封信時都要去一趟郵局,所以實時性高。而標准 IO 就好比要攢夠 20 封信才去一趟郵局,所以吞吐量高,因為用戶把信件交到老大爺的手上時就會立即返回,響應速度快,用戶體驗更好。而我們使用 fflush(3) 之類的函數強制刷新緩沖的時候,就相當於是老大爺收到了一封加急信件需要立即去一趟郵局送信。
至於這里所說的標准 IO 功能受限,是因為標准 IO 在各個平台上都是使用系統 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 獲取到具體失敗的原因。
error 是什么呢?它是標准 C 中定義的一個整形值,用來表示上次發生的錯誤。大家可以在頭文件中看看 errno 都定義了哪些值:
>$ vim /usr/include/asm-generic/errno.h
>$ vim /usr/include/asm-generic/errno-base.h
通常系統調用會給我們我返回一個整形值來表示是否出現了錯誤,當出現了錯誤的時候會設置 errno,通過 errno 我們就可以得知出現了什么錯誤了。
當然,直接給我們一個數字,我們自己再從頭文件中查找這個數字表示的意義,然后再打印出來給用戶看,似乎態麻煩了,沒有什么簡便的辦法嗎?
別擔心,其實標准庫已經為我們准備好專門的轉換函數了:perror(3) 和 strerror(3)
perror(3) 會自動讀取 errno 幫我們轉換成對應的文字描述,並且將它們輸出到標准錯誤流中。它的參數是一個字符串,用來讓我們自定義一些錯誤消息給用戶看,它的輸出格式就是 我們給傳遞的參數:errno 轉換的描述文字。
strerror(3) 函數也會將 errno 轉換為文字,不過它不會自動讀取 errno 當前的值,需要我們把 errno 傳遞給它。它也不會幫我們輸出到標准輸出中,而是將轉換完的字符串返回給我們。
如果大家是開發一個前台應用,一般可以使用 perror(3) 函數直接將錯誤輸出給用戶。
如果大家開發的是后台應用(如守護進程等),那么一般先使用 strerrno(3) 函數將 errno 轉換為字符串,然后再把這個字符串傳給日志系統記錄下來。
大家在使用 errno 這個全局變量的時候要導入 errno.h 頭文件:
#include <errno.h>
在使用 strerror(3) 函數時不要忘記導入 string.h 頭文件,否則會報段錯誤!
#include <string.h>
其實現在的很多 *nix 系統中,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,這樣即省去了編譯時手動寫參數的麻煩,也避免了代碼中的丑陋。
10.fflush(3)
1 fflush - flush a stream 2 3 #include <stdio.h> 4 5 int fflush(FILE *stream);
fflush(3) 函數的作用是刷新緩沖區,提到這個函數就要講講緩沖區了。
緩沖區的作用是為了合並系統調用,在上面講 STDIO 與 SYSIO 的區別時大家已經看到什么是合並系統調用了。
Linux 系統中有三種緩沖形式:無緩沖、行緩沖和全緩沖。
無緩沖:需要立刻輸出時使用,例如 stderr;
行緩沖:遇到換行符時進行刷新、緩沖區滿了的時候刷新、強制刷新(fflush(3));而標准輸出(stdout)是行緩沖,因為涉及到終端設備;
全緩沖:只有緩沖區滿了的時候和強制刷新(fflush(3))時才會刷新,這是 Linux 默認的緩沖模式,但終端設備除外,終端設備使用行緩沖模式;
當數據被放入緩沖區的時候是不會通過系統調用(read(3)、write(3))送到內核中的,只有緩沖區被刷新的時候數據才會通過系統調用進入內核。而刷新緩沖區就是 fflush(3) 函數的作用。
fflush(3) 的參數是具體要刷新的流,當參數為 NULL 時會刷新所有的輸出流。
好了,時間不早了,今天先寫到這里。
== 補充 == 2016.02.20 ==============================
有園友提問:函數括號里的數字表示的是什么意思?
其實這個數字表示的是該函數在 man 手冊的第幾章里可以查詢到。
man 手冊一共有 8 章,每一章保存不一樣的內容,這八章的內容分別是:
第一章:shell 命令。如:ls、vim,查詢方法:>$ man ls
第二章:系統調用。如:open、close,查詢方法:>$ man 2 open 或 >$ man close。因為第一章也有 open,所以 man 的參數中要加章節號;因為第一章中沒有 close,所以查詢 close 不需要加章節號。
第三章:庫函數。如:printf、fopen,查詢方法:>$ man 3 printf 或 >$ man fopen
第四章:/dev 下的文件。如:zero。
第五章:一些配置文件的格式。如:/etc/shadow,查詢方法:>$ man shadow
第六章:預留給游戲的,由游戲自己定義。如:sol。
第七章:附件和變量。如 iso-8859-1。
第八章:只能由 root 執行的系統管理命令。如 mkfs。
因為有些函數的名字會在不同的章節出現,這種函數在查詢 man 手冊的時候如果不加上章節號默認會查詢低編號的章節。
比如 printf,如果你輸入的查詢命令是 >$ man printf,那么打開的並不是 printf 標准庫函數的手冊,而是 printf shell 命令的手冊,想要查詢 printf 函數的手冊就需要在 man 命令后面加上該函數所在的章節數,正確的命令是 >$ man 3 printf,這樣就會打開 man 手冊第三章的 printf 函數。
另外,如果想查看每個章節中都有哪些內容,可以使用 >$ man -aw 命令得到 man 手冊的安裝路徑,然后找到對應的 man 章節的目錄,再用 zcat 命令查看那些 gz 文件。
以上補充內容參考《linux man 手冊各個章節的意義和用法》。
