系統級I/O
一、UNIX I/O
在UNIX系統中有一個說法,一切皆文件。所有的I/O設備,如網絡、磁盤都被模型化為文件,而所有的輸入和輸出都被當做對相應文件的讀和寫來執行。這種將設備映射為文件的方式,允許UNIX內核引出一個簡單、低級的應用接口,稱為UNIX I/O,這使得所有的輸入和輸出都能以一種統一且一致的方式來執行。
- 打開文件 打開文件操作完成以後才能對文件進行一些列的操作,打開完成過以後會返回一個文件描述符,它在後續對此文件的所有操作中標識這個文件,內核記錄有關這個打開文件的所有信息。
- 改變當前的文件位置。
- 讀寫文件
- 關閉文件 應用完成了對文件的訪問之後,就通知內核關閉這個文件,內核釋放文件打開時創建的數據結構,並將這個描述符恢復到可用的描述符池中。進程終止,內核也會關閉所有打開的文件並釋放他們的存儲器資源。
二、打開和關閉文件
關於打開文件的基本操作,這裏就不再累述,就是關於幾個函數的解釋,在上面的三篇文章中有解釋。
int open(char *filename,int flags,mode_t mode);
其中打開標誌flags有三種基本標誌:O_RDONLY、O_WRONLY、O_RDWR。也可以和其他三種(O_CREAT、O_TRUNC、O_APPEND)組合使用。mode參數指定了新文件的訪問權限位。(這次終於看到完全的mode參數的使用方法了)
三、讀和寫文件
在系統I/O中讀寫文件用的系統函數為read()和write()函數來執行。
#include <unistd.h> ssize_t read(int fd,void * buf,size_t n); ssize_t write(int fd,void *buf,size_t n);
read函數從描述符為fd的當前文件位置拷貝最多n個字節到存儲器位置buf。返回值-1表示一個錯誤,而返回值0表示EOF。否則,返回值表示的是實際傳送的字節數量。而write函數從存儲器位置buf拷貝至多n個字節到描述符fd的當前文件位置。返回值要麽為-1要麽為寫入的字節數目。
/* $begin cpstdin */ #include "csapp.h" int main(void) { char c; while(Read(STDIN_FILENO, &c, 1) != 0) Write(STDOUT_FILENO, &c, 1); exit(0); } /* $end cpstdin */
關於在文件中定位使用的函數為lseek,在I/O庫中使用的函數為fseek。
(ps:size_t和ssize_t的區別,前者是unsigned int,而後者是int)
有些情況下,read和write傳送的字節比應用程序要求的要少,出現這種情況的原因如下:
- 讀時遇到EOF。此時read返回0來發出EOF信號。
- 從終端讀文本行。如果打開文件是與終端相關聯,那麽每個read函數將以此傳送一個文本行,返回的不足值等於文本行的大小。
- 讀和寫網絡套接字。可能會出現阻塞現象。(我一定會在進程間通信的時候弄清楚這個事情的前前後後,後後前前!!!)
實際上,除了EOF,在讀磁盤文件時,將不會遇到不足值,而且在寫磁盤文件時,也不會遇到不足值。然而,如果你想創建健壯的網絡應用,就必須反復調用read和write處理不足值,直到所有需要的字節都傳送完畢。(這一點在UNIX網絡編程中已經領略過了!!)
四、用RIO包健壯地讀寫
這個包會處理上面的不足,RIO提供了方便、健壯和高效的I/O。提供了兩類不同的函數:
- 無緩沖的輸入輸出函數 直接在存儲器和文件之間傳送數據,沒有應用級緩沖,它們對將二進制數據讀寫到網絡和從網絡讀寫二進制數據尤其有用。
- 帶緩沖的輸入函數
ssize_t rio_readn(int fd,void *usrbuf,size_t n); ssize_t rio_writen(int fd,void *usrbuf,size_t n);
對同一個描述符,可以任意交錯地調用rio_readn和rio_writen。一個問本行的末尾都有一個換行符,那麽像讀取一個文本中的行數怎麽辦,使用read讀取換行符這個方法不是很妥當,可以調用一個包裝函數(rio_readineb),它從一個內部讀緩沖區拷貝一個文本行,當緩沖區為空時,會自動地調用read重新填滿緩沖區。也就是說,這些函數都是緩沖區操作而言的。
五、讀取文件元數據
應用程序能夠通過調用stat和fstat函數檢索到關於文件的信息(有時也稱為文件的元數據)
#include <sys/stat.h> #include <unistd.h> int stat(const char *filename,struct stat *buf); int fstat(int fd,struct stat *buf);
若成功,返回0,若出錯則為-1.stat以一個文件名為輸入,並且填充buf結構體。fstat函數只不過是以文件描述符而不是文件名作為輸入。
struct stat { #if defined(__ARMEB__) unsigned short st_dev; unsigned short __pad1; #else unsigned long st_dev; #endif unsigned long st_ino; unsigned short st_mode; unsigned short st_nlink; unsigned short st_uid; unsigned short st_gid; #if defined(__ARMEB__) unsigned short st_rdev; unsigned short __pad2; #else unsigned long st_rdev; #endif unsigned long st_size; unsigned long st_blksize; unsigned long st_blocks; unsigned long st_atime; unsigned long st_atime_nsec; unsigned long st_mtime; unsigned long st_mtime_nsec; unsigned long st_ctime; unsigned long st_ctime_nsec; unsigned long __unused4; unsigned long __unused5; };
其中st_size成員包含了文件的字節大小。st_mode為文件訪問許可位。UNIX提供的宏指令根據st_mode成員來確定文件的類型:S_ISREG(),這是一個普通文件麽;S_ISDIR(),這是一個目錄文件麽;S_ISSOCK()這是一個網絡套接字麽。使用一下這個函數
#include <stdio.h> #include <stdlib.h> #include <string.h> #include <sys/stat.h> #include <sys/types.h> #include <unistd.h> int main() { int fd,size; struct stat buf_stat; memset(&buf_stat,0x00,sizeof(buf_stat)); fd=stat("stat.c",&buf_stat); printf("%d\n",(int)buf_stat.st_size); return 0; }
六、共享文件
內核用三個相關的數據結構來表示打開的文件:
- 描述符表(descriptor table)每個進程都有它獨立的描述符表,它的表項是由進程打開的文件描述符來索引的。每個打開的描述符表項指向文件表中的一個表項。
- 文件表(file table) 打開文件的描述符表項指向問價表中的一個表項。所有的進程共享這張表。每個文件表的表項組成包括由當前的文件位置、引用計數(既當前指向該表項的描述符表項數),以及一個指向v-node表中對應表項的指針。關閉一個描述符會減少相應的文件表表項中的應用計數。內核不會刪除這個文件表表項,直到它的引用計數為零。
- v-node表(v-node table)同文件表一樣,所有的進程共享這張v-node表,每個表項包含stat結構中的大多數信息,包括st_mode和st_size成員。
下面看幾張圖。
描述符1和4通過不同的打開文件表表項來引用兩個不同的文件。這是典型的情況,沒有共享文件,並且每個描述符對應一個不同的文件。
多個描述符也可以通過不同的文件表表項來應用同一個文件。如果同一個文件被open兩次,就會發生上面的情況。關鍵思想是每個描述符都有它自己的文件位置,所以對不同描述符的讀操作可以從文件的不同位置獲取數據。
父子進程也是可以共享文件的,在調用fork()之前,父進程如第一張圖,然後調用fork()之後,子進程有一個父進程描述符表的副本。父子進程共享相同的打開文件表集合,因此共享相同的文件位置。一個很重要的結果就是,在內核刪除相應文件表表項之前,父子進程必須都關閉了他們的描述符。
下圖展示了文件描述符、打開的文件句柄以及i-node之間的關系,圖中,兩個進程擁有諸多打開的文件描述符。在進程A中,文件描述符1和30都指向了同一個打開的文件句柄(標號23)。這可能是通過調用dup()、dup2()、fcntl()或者對同一個文件多次調用了open()函數而形成的。 進程A的文件描述符2和進程B的文件描述符2都指向了同一個打開的文件句柄(標號73)。這種情形可能是在調用fork()後出現的(即,進程A、B是父子進程關系),或者當某進程通過UNIX域套接字將一個打開的文件描述符傳遞給另一個進程時,也會發生。再者是不同的進程獨自去調用open函數打開了同一個文件,此時進程內部的描述符正好分配到與其他進程打開該文件的描述符一樣。 此外,進程A的描述符0和進程B的描述符3分別指向不同的打開文件句柄,但這些句柄均指向i-node表的相同條目(1976),換言之,指向同一個文件。發生這種情況是因為每個進程各自對同一個文件發起了open()調用。同一個進程兩次打開同一個文件,也會發生類似情況。
七、I/O重定向
函數為:
函數解釋:
(即:讓描述符oldfd實現newfd的功能)
eg,dup2(field,1) 將標準描述符輸出重定向到field描述符
假設在調用dup2(4,1)之前,我們的狀態圖10-11所示,其中描述符1(標準輸出)對應於文件A(比如一個終端),描述符4對應於文件B(比如一個磁盤文件)。A和B的引用計數都等於1。圖10-14顯示了調用dup2(4,1)之後的情況。兩個描述符現在都指向了文件B;文件A已經被關閉了,並且它的文件表和v-node表表項也已經被刪除了;文件B的引用計數已經增加了。從此之後,任何寫到標準輸出的數據都被重定向到文件B。
解析圖如下:
八、I/O使用的抉擇方法
上圖中展現了幾種I/O的關系模式,在應用程序中應該使用哪些函數呢?標準I/O函數是磁盤和終端設備I/O的首選。但是對網絡套接字上盡量使用健壯的RIO或者系統I/O
小結:
Unix內核使用三個相關的數據結構來表示打開的文件。描述符表中的表項指向打開文件表中的表項,而打開文件表中的表項又指向v-node表中的表項。每個進程都有它自己單獨的描述符表,而所有的進程共享同一打開文件表和v-node表。
系統級I/O