程序師世界是廣大編程愛好者互助、分享、學習的平台,程序師世界有你更精彩!
首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫知識
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
 程式師世界 >> 編程語言 >> C語言 >> 關於C語言 >> (二) 一起學 APUE 之 文件 IO,apue文件io

(二) 一起學 APUE 之 文件 IO,apue文件io

編輯:關於C語言

(二) 一起學 APUE 之 文件 IO,apue文件io


.

.

.

.

.

昨天我們討論了標准 IO,今天主要說說系統 IO。

1.文件描述符

在 SYSIO 中貫穿了一個整形數,它就是文件描述符。對內核而言,所有打開的文件都是通過文件描述符引用的。它的作用與 STDIO 中的 FILE 結構體類似,但是它們的工作原理是完全不同的。它實際上是一個由內核保存的數組下標,所以不會是負數,下面我會用一張圖來表示它的作用。

圖1 SYSIO 文件描述符

圖是在 Ubuntu 下好不容易找到了一個畫圖軟件畫的,質量不怎麼樣,小伙伴們先湊合著看吧。

我解釋下圖上畫的東西。

圖片一共分為標准 IO 和系統 IO 兩部分,系統 IO 部分有一個數組,這個數組中的指針指向了內核中具體描述文件信息的結構體,而通過這些結構體再引用具體的文件(inode)。而操作系統提供給我們的文件描述符就是這個數組的下標。這個數組的長度是 1024,也就表示一個進程最多可以打開 1024 個文件,當然這個上限可以通過 ulimt(1) 命令修改,具體用法這裡不再贅述。

當產生一個文件描述符時,優先采用當前最小的可用數值。假設當前已經占用的文件描述符為1、2、3、5,那麼再次產生的文件描述符就是 4。

還要注意一點,上面這個文件描述符數組在每個進程中都會持有一份,所以理論上是每個進程最多可以打開 1024 個文件,而不是系統中所有的進程一共只能打開 1024 個文件。

 

2.fileno(3)

1 #include <stdio.h>
2 
3 int fileno(FILE *stream);
4 
5    Feature Test Macro Requirements for glibc (see feature_test_macros(7)):
6 
7        fileno(): _POSIX_C_SOURCE >= 1 || _XOPEN_SOURCE || _POSIX_SOURCE

 這個函數的作用是從 STDIO 的 FILE 結構體指針中獲得 SYSIO 的文件描述符。

 

3.fdopen(3)

1 #include <stdio.h>
2 
3 FILE *fdopen(int fd, const char *mode);
4 
5   Feature Test Macro Requirements for glibc (see feature_test_macros(7)):
6 
7      fdopen(): _POSIX_C_SOURCE >= 1 || _XOPEN_SOURCE || _POSIX_SOURCE

 這個函數和上面的 flieno(3) 函數的功能是反過來的,作用是把 SYSIO 的文件描述符轉換為 STDIO 的 FILE 結構體指針。mode 參數的作用與 fopen(3) 中的 mode 參數相同,這裡不再贅述。

雖然這兩個函數可以在 STDIO 與 SYSIO 之間互相轉換,但是並不推薦對同一個文件同時采用兩種方式操作。因為 STDIO 和 SYSIO 之間它們處理文件的私有數據是不同步的,如果同時使用兩種方式操作同一個文件則可能帶來不可預知的後果,具體可以參考上一篇博文中提到的那個合並系統調用的例子。

 

4.open(2)

1 open - open and possibly create a file or device
2 
3 #include <sys/types.h>
4 #include <sys/stat.h>
5 #include <fcntl.h>
6 
7 int open(const char *pathname, int flags);
8 int open(const char *pathname, int flags, mode_t mode);

 想要使用 SYSIO 操作文件或設備,要先通過 open(2) 函數獲得一個文件描述符。注意博文中在函數上方標識出來的頭文件大家在使用這個函數的時候一定要一個不少的全部包含到源代碼中

參數列表:

  pathname:要打開的文件路徑。

  flags:指定文件的操作方式,多個選項之間用按位或( | )運算符鏈接。

    比選項,三選一:O_RDONLY, O_WRONLY, O_RDWR

    可選項:可選項有很多,這裡只介紹常用的,想要查看完全的可選項,可以查閱 man 手冊。

選項 說明 O_APPEND 追加到文件尾部。 O_CREAT 創建新文件。 O_DIRECT 最小化緩沖。buffer 時寫的加速機制,cache 是讀的加速機制。 O_DIRECTORY 強調一定要打開一個目錄,如果 pathname 不是目錄則會打開失敗。 O_LARGEFILE 打開大文件的時候要加這個,會將 off_t 定義為 64 bit,當然也可以在編譯的時候使用上一篇博文提到的宏定義來指定 off_t 的長度。 O_NOFOLLOW 如果 pathname 是符號鏈接則不展開,也就是說打開的是符號鏈接文件本身,而不是符號鏈接指向的文件。 O_NONBLOCK 非阻塞形式。阻塞是讀取不到數據時死等,非阻塞是嘗試讀取,無論能否讀取到數據都返回。

 

  mode:8 進制文件權限。當 flags 包含 O_CREAT 選項時必須傳這個參數,否則可以不用傳這個參數。當然系統在創建文件的時候不會直接這個參數,而是通過如下的公式計算得到最終的文件權限:

mode & ~(umask)

  具體的 umask 的值可以通過 umask(1) 命令獲得。通過這樣的公式進行計算可以避免程序中創建出權限過高的文件。

不知道小伙伴們注意到沒有,這個函數有一個有趣的地方。C 語言中是沒有函數重載這個概念的,那麼為什麼這兩個 open(2) 函數很像重載的函數呢?實際上它們是用可變長參數列表來實現的。

頓時讓我想起來一道面試題:如何確定一個函數是用重載實現的還是用變長參數實現的?答案是給它多傳幾個參數嘛,如果報錯了那一定是函數重載,否則就是變長參數實現的呗。

 

5.close(2)

1 close - close a file descriptor
2 
3 #include <unistd.h>
4 
5 int close(int fd);

 關閉文件描述符。

參數是要關閉的文件描述符。注意當一個文件描述符被關閉之後就不能再使用了,雖然 fd 這個變量的值沒有變,但是內核已經將相關的資源釋放了,這個 fd 相當於一個野指針了。

返回值:

  成功為0,失敗為-1。但很少對它的返回值做校驗,一般都認為不會失敗。

 

6.read(2)

1 read - read from a file descriptor
2 
3 #include <unistd.h>
4 
5 ssize_t read(int fd, void *buf, size_t count);

這是 SYSIO 讀取文件的函數,作用是從文件描述符 fd 中讀取 count 個字節的數據到 buf 所指向的空間。

返回值:返回成功讀取到的字節數;0 表示讀取到了文件末尾;-1 表示出現錯誤並設置 errno。

  注意 read(2) 函數與 STDIO 中的 fread(3) 函數的返回值是有區別的,fread(3) 返回的是成功讀取到了多少個對象,而 read(2) 函數返回的是成功讀取到的字節數量。

 

7.write(2)

1 write - write to a file descriptor
2 
3 #include <unistd.h>
4 
5 ssize_t write(int fd, const void *buf, size_t count);

 write(2) 是 SYSIO 向文件中寫入數據的函數,作用是將 buf 中 count 字節的數據寫入到文件描述符 fd 所對應的文件中。

返回值:返回成功寫入的字節數;0 並不表示寫入失敗,僅僅表示什麼都沒有寫入;-1 才表示出現錯誤並設置 errno。

  注意 write(2) 函數與 STDIO 中的 fwrite(3) 函數的返回值是有區別的,fwrite(3) 返回的是成功寫入了多少個對象,而 write(2) 函數返回的是成功寫入的字節數量。

大家想一想,為什麼會出現寫入的值是 0 的情況呢?其實原因有很多,其中一個原因是當寫入的時候發生了阻塞,而阻塞中的 write(2) 系統調用恰巧被一個信號打斷了,那麼 write(2) 可能沒有寫入任何數據就返回了,所以返回值會是0。至於什麼是阻塞,什麼是信號,LZ 會在後面的博文中講解。

 

8.lseek(2)

1 lseek - reposition read/write file offset
2 
3 #include <sys/types.h>
4 #include <unistd.h>
5 
6 off_t lseek(int fd, off_t offset, int whence);

通過上一篇博文大家知道了文件位置指針這個概念,它是系統為了方便我們讀寫文件而設定的一個標記,隨著我們通過函數對文件的讀寫,它會自動相應的向文件尾部偏移。

那麼是不是說當我們讀取過文件的一段內容之後,就沒辦法回去再次讀取同一段內容了呢?

其實不是的,通過 lseek(2) 函數就可以讓我們隨心所欲的控制文件位置指針了。

參數列表:

  fd:要操作的文件描述符;

  offset:想對於 whence 的偏移量;

  whence:相對位置;三選一:SEEK_SET、SEEK_CUR、SEEK_END

    SEEK_SET 表示文件的起始位置;

    SEEK_CUR 表示文件位置指針當前所在位置;

    SEEK_END 表示文件末尾;

 返回值:

  成功時返回文件首相對於移動結束之後的文件位置指針所在位置的偏移量;失敗時返回 -1 並設置 errno;

這個函數的 offset 參數和返回值都對基本數據類型進行了封裝,這一點要比標准庫的 fseek(3) 更先進。

寫一段偽代碼來說明這個函數的使用方法。

1 lseek(fd, -1024, SEEK_CUR); // 從文件位置指針當前位置向前偏移 1024 個字節
2 lseek(fd, 1024, SEEK_SET); // 從文件起始位置向後偏移 1kb
3 lseek(fd, 1024UL*1024UL*1024UL*5UL, SEEK_SET); // 產生一個 5GB 大小的空洞文件

  

 9.time(1)

之前討論過 STDIO 與 SYSIO 的效率問題,所以在這裡聊一聊 time(1) 命令。

這個命令可不是用來查看系統當前時間的,想要查看系統時間得使用 date(1) 命令,這個不是我們今天要討論的內容,所以就不說了。

time(1) 命令的作用是監視一個程序的用戶時間,從而可以粗略的幫助我們分析這個程序的執行效率。

 1 while ((readlen = read(srcfd, buf, BUFSIZE)) > 0) {
 2         pos = 0;
 3         while (readlen > 0) {
 4                 writelen = write(destfd, buf+pos, readlen);
 5                 if (writelen < 0) {
 6                         err = errno;
 7                         goto e_write;
 8                 }
 9                 pos += writelen;
10                 readlen -= writelen;
11         }
12 }

 

 這是一個模仿 cp(1) 命令的程序的核心部分代碼,其中的 buf 是一個 char 數組,用來作為數據讀寫的緩存。當 buf 的容量不同時文件拷貝的效率也是不同的,因為過於頻繁的執行系統調用和使用過大的緩存都會使效率下降。如果通過不停的修改 buf 的容量的方式測試 buf 為多大的時候拷貝效率最高的話,就可以使用 time(1) 命令監視程序的執行時間。

>$ gcc -Wall mycp_sysio.c -o mycp_sysio
>$ time ./mycp_sysio rhel-server-6.4-x86_64-dvd.iso tmp.iso
real 1m30.014s
user 0m0.003s
sys 1m29.003s

sys 是程序在內核態消耗的時間,也就是執行系統調用所消耗的時間。 

user 是程序在用戶態消耗的時間,也就是程序本身的代碼所消耗的時間。

real 是用戶等待的總時間,是 sys + user + CPU 調度時間,所以 real 時間會稍微比 sys + user 時間長一點。一個程序從提高響應素的的方式提高用戶體驗,一般指的就是提高 real 時間。

 

 10.文件共享

 文件共享就是指多個進程共同處理同一個文件,就是 圖1 中第二個文件表項和第三個文件表項共同指向同一個 inode 的圖示,不過這兩個文件表項來自於不同的進程表項時才叫做文件共享。

 

 11.原子操作

 通俗來講,原子操作就是將多個動作一氣呵成的做完,中間不會被打斷,要麼執行完所有的步驟,要麼一步也不會執行。這裡用創建臨時文件來舉個栗子。

1 tmpnam, tmpnam_r - create a name for a temporary file
2 
3 #include <stdio.h>
4 
5 char *tmpnam(char *s);

 

如果我們需要創建一個臨時文件,那麼首先需要又操作系統提供一個文件名,然後再創建這個文件。

tmpnam(3) 函數就是用來獲得臨時文件的文件名的。為什麼要通過這個函數由操作系統來為我們生成文件名呢?就是因為系統中進程比較多,臨時文件也比較多,怕文件重名嘛。

但是這個函數只負責生成一個目前系統中不存在的臨時文件名,並不負責創建一個文件,所以創建文件的任務要由我們自己使用 fopen(3) 或 open(2) 等手段創建。

假設在我們拿到這個文件名的時候,臨時文件還沒有在磁盤上真正創建,另一個進程拿到了一個與我們相同的文件名,那麼這個時候再創建文件就是有問題的了。

這就是因為獲得文件名與創建文件這個動作不原子造成的,如果獲得唯一的文件名和創建文件這個動作一氣呵成中間不會被打斷,則這個問題就不會發生,我們創建好文件之後另一個進程就再也拿不到相同的文件名了。

1 tmpfile - create a temporary file
2 
3 #include <stdio.h>
4 
5 FILE *tmpfile(void);

 

既然使用 tmpnam(3) 函數無法原子的創建臨時文件,那麼就沒有原子的方式來避免上面描述的問題了嗎?當然有辦法,那就是使用 tmpfile(3) 函數來創建臨時文件。

tmpfile(3) 函數是獲得文件名和創建臨時文件的動作一氣呵成的,它直接會返回一個創建好的 FILE 結構體指針給我們,這樣一來媽媽再也不用擔心我們的文件名會被別人搶占了。:)

當然系統中有許多地方需要原子操作,不僅僅是創建臨時文件,所以系統還有其它函數提供了原子操作,遇到的時候我們再講解,這裡不再詳述。

 

12.dup(2)、dup2(2)

1 dup, dup2 - duplicate a file descriptor
2 
3 #include <unistd.h>
4 
5 int dup(int oldfd);
6 int dup2(int oldfd, int newfd);

 

這兩個函數是用來復制文件描述符的,就是 圖1 中 文件描述符 3 和 6 指向了同一個文件表項的情況。

舉個栗子,用 dup(2) 實現輸出的重定向。

 1 #include <stdio.h>
 2 #include <unistd.h>
 3 
 4 int main (void)
 5 {                                                                               
 6         /* 要求在不改變下面的內容的情況下,使輸出的內容到文件中  */
 7 
 8         puts("dup test.");
 9 
10         return 0;
11 }

 

puts(3) 函數是將參數字符串寫入到標准輸出 stdout(文件描述符是 1) 中,而標准輸出默認目標是我們的 shell。如果想要讓 puts(3) 的參數輸出到一個文件中,實現思路是:首先打開一個文件獲得一個新的文件描述符,然後關閉標准輸出文件描述符(1),然後使用 dup(2) 函數族復制產生一個新的文件描述符,此時的 1 號文件描述符就不是標准輸出的文件描述符了,而是我們自己創建的文件的描述符了。還記得我們之前提到過嗎,文件描述符優先使用可用范圍內最小的。進程中當前打開的文件描述符有標准輸入(0)、標准輸出(1)、標准錯誤(2)和我們自己打開的文件(3),當我們關閉了 1 號文件描述符後,當前可用的最小文件描述符是 1,所以新復制的文件描述符就是 1。而標准庫函數 puts(3) 在調用系統調用 write(2) 函數向 1 號文件描述符打印時,正好是打印到了我們所指定的文件中。

 1 #include <stdio.h>
 2 #include <unistd.h>
 3 #include <fcntl.h>
 4 
 5 #include <sys/types.h>
 6 #include <sys/stat.h>
 7 
 8 int main (void)
 9 {
10     int fd = -1;
11 
12     fd = open("tmp", O_WRONLY | O_CREAT | O_TRUNC, 0664);
13     /* if error */
14 
15     #if 0
16     close(1); // 關閉標准輸出
17     dup(fd);
18     #endif
19     dup2(fd, 1);
20     close(fd);
21 
22     /* 要求在不改變下面的內容的情況下,使輸出的內容到文件中  */
23 
24     puts("dup test.");
25 
26     return 0;
27 }

 

由於題目的要求是 puts(3) 上面注釋以下的內容都不能修改,原則上 1 號文件描述符在這裡使用完畢也需要 close(2),所以這裡造成了一個內存洩漏,但並不影響對 dum(2) 函數族的解釋和測試。

上面的代碼用 close(2) + dup(2) 的方式或者 dup2(2) 的方式都可以實現。

dup(2) 和 dup2(2) 的作用是相同的,區別是 dum2(2) 函數可以用第二個參數指定新的文件描述符的編號。

如果新的文件描述符已經被打開則先關閉它再重新打開。

如果兩個參數相同,則 dup2(2) 函數會返回原來的文件描述符,而不會關閉它。

另外一點比較重要,close(2) + dup(2) 的方式不原子,而 dup2(2) 這兩步動作是原子的,在並發的情況下可能會出現問題。後面的博文我們會通過信號和多線程來討論並發。

 

13.sync(2)

1 sync, syncfs - commit buffer cache to disk
2 
3 #include <unistd.h>
4 
5 void sync(void);

 

sync(2) 函數族的函數作用是全局催促,將 buffer 和 cache 刷新和同步到 disk,一般在設備即將卸載的時候使用。這個函數族的函數不是很常用,所以用到的時候再具體討論,這裡不再詳述。

 

14.fcntl(2)

1 fcntl - manipulate file descriptor
2 
3 #include <unistd.h>
4 #include <fcntl.h>
5 
6 int fcntl(int fd, int cmd, ... /* arg */ );

這是一個管家級別的函數,根據不同的 cmd 和 arg 讀取或修改對已經打開的文件的操作方式。具體的命令和參數請查閱 man 手冊,這裡不再花費大量篇幅贅述。

 

15.ioctl(2)

1 ioctl - control device
2 
3 #include <sys/ioctl.h>
4 
5 int ioctl(int d, int request, ...);

Linux 的一切皆文件的設計原理將所有的設備都抽象為一個文件,當一個設備的某些操作不能被抽象成打開、關閉、讀寫、跳過等動作時,其它的動作都通過 ioctl(2) 函數控制。

例如將聲卡設備抽象為一個文件,錄制音頻和播放音頻的動作則可以被抽象為對聲卡文件的讀、寫操作。但是像配置頻率、音色等功能無法被抽象為對文件的操作形式,那麼就需要通過 ioctl(2) 函數對聲卡設備進行控制,具體的控制命令則由驅動程序提供。

16. /dev/fd

/dev/fd 是一個虛擬目錄,它裡面是當前進程所使用的文件描述符信息。如果用 ls(1) 查看,則裡面顯示的是 ls(1) 這個進程所使用的文件描述符信息。而打開裡面的文件則相當於復制文件描述符。

 

文件 IO 部分到此就結束了,如有問題歡迎大家斧正。:)

  1. 上一頁:
  2. 下一頁:
Copyright © 程式師世界 All Rights Reserved