這章主要講了幾類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)