第十章 系統級I/O
輸入/輸出(I/O)是在主存和外部裝置之間拷貝資料的過程,輸入操作時從I/O裝置拷貝資料到主存,而輸出操作是從主存拷貝資料到I/O裝置。
所有語言的運作時系統都提供執行I/O的較進階别的工具。
10.1 Unix I/O
一個Unix檔案就是一個m個位元組的序列,所有的I/O裝置都被模型化為檔案,所有的輸入和輸出都被當做對應的檔案的讀和寫操作來執行。
- 打開檔案
一個應用程式通過要求核心打開相應的檔案,來宣告他想要通路一個I/O裝置,核心傳回一個小的非負整數叫做描述符,他在後續對此檔案的所有操作中辨別這個檔案,核心記錄有關這個打開檔案的所有資訊,應用程式隻需記住這個描述符。
Unix外殼建立的每個程序開始時都有三個打開的檔案:标準輸入(描述符為0),标準輸出(1),标準錯誤(2)。
- 改變目前的檔案位置
對于每個打開的檔案,核心保持着一個檔案位置k,初始為0,這個檔案位置時從檔案開頭起始的位元組偏移量,應用程式能夠通過執行seek操作,顯式地設定檔案的目前位置為k。
- 讀寫檔案
一個讀操作就是從檔案拷貝n>0個位元組到存儲器,從檔案位置k開始,然後将k增加到k+n。給定一個大小為m位元組的檔案,當k≥m時執行讀操作會觸發一個EOF條件,檢測到這個條件。類似的寫操作就是存存儲器拷貝n>0個位元組到一個檔案,從目前檔案位置k開始然後更新k。
- 關閉檔案
當應用完成了對檔案的通路之後,他就通知核心關閉這個檔案,作為響應,核心釋放檔案打開是建立的資料結構,并将這個描述符恢複到可用的描述符池中,無論一個程序因為何種原因終止時,核心都會關閉所有打開的檔案并釋放他們的存儲器資源。
10.2 打開和關閉檔案
flags參數指明程序如何通路檔案
- O_RDONLY:隻讀
- O_WRONLY:隻寫
- O_RDWR:可讀可寫
flags參數也可以是一個或者更多位掩碼的或:
- O_CREAT:如果檔案不存在,就建立他的一個截斷的空檔案
- O_TRUNC:如果檔案已經存在就截斷它
- O_APPEND:在每次寫操作前,設定檔案位置到檔案的結尾處
通路權限位:

10.3 讀和寫檔案
應用程式是通過分别調用read和write函數來執行輸入和輸出的。
read函數從描述符為fd的目前檔案位置拷貝最多n個位元組到存儲器位置buf。傳回值-1表示一個錯誤而傳回值0表示EOF否則傳回值表示的是實際傳送的位元組數量。
write函數從存儲器位置buf拷貝至多n個位元組到描述符fd的目前檔案位置,read和write調用一次一個位元組地從标準輸入拷貝到标準輸出。
一次一個位元組地從标準輸入拷貝到标準輸出:
通過調用lseek函數,應用程式能夠顯示地修改目前檔案的位置。
ssize_t 和 size_t有些什麼差別
size_t 被定義為unsigned int,而ssize_t 則被定義為int,read函數傳回一個有符号的大小,而不是一個無符号的大小,這是因為出錯時他必須傳回-1
有些情況下,read和write傳送的位元組比應用程式要求的要少,這些不足值不表示有錯誤,原因如下:
- 讀時遇到了EOF
- 從終端讀文本行
- 讀和寫網絡套接字socket
除了EOF,在讀磁盤檔案時,将不會遇到不足值,而且在寫磁盤檔案時,也不會遇到不足值。
10.4 用RIO包健壯地讀寫
RIO包會自動為你處理上文中所述的不足值,RIO提供了兩類不同的函數:
- 無緩沖的輸入輸出函數。
這些函數直接在存儲器和檔案之間傳送資料,沒用應用級緩沖,它們對将二進制資料讀寫到網絡和從網絡讀寫二進制資料尤其重要有用。
- 帶緩沖的輸入函數
允許高效低從檔案中讀取文本行和二進制資料,内容緩存在應用及緩存區内。
10.4.1 RIO的無緩沖的輸入輸出函數
rio_readn函數從描述符fd的目前檔案位置最多傳送n個位元組到存儲器位置usrbuf,rio_writen函數從位置usrbuf傳送n個位元組到描述符fd,rio_readn函數在遇到EOF時隻能傳回一個不足值,rio_writen函數絕不會傳回不足值,對于同一個描述符可以交錯任意地調用rio_readn和rio_writen。
10.4.2 RIO的帶緩存的輸入函數
一個文本行就是一個又換行符結尾的ASCII碼字元序列
rio_readn 和 rio_writen函數:
RIO讀程式和核心是rio_read函數,該函數是unix read函數的帶緩沖的版本。
内部的rio_read函數:
對于一個應用程式,rio_read函數和Unix函數有同樣的語義,出錯時,傳回值-1,适當設定errno,EOF時,傳回值0,如果要求的位元組數超過了讀緩沖區内未讀的位元組數量,它會傳回一個不足值。
10.5 讀取檔案中繼資料
應用程式能夠通過調用stat和fstat函數,檢索到關于檔案的資訊(中繼資料)
stat函數以一個檔案名作為輸入,fstat函數以檔案描述符而不是檔案名作為輸入。
stat資料結構:
st_size成員包含了檔案的位元組數大小,st_mode成員則編碼了檔案通路許可位和檔案類型。普通檔案包括某種類型的二進制或文本資料,對于核心而言,文本檔案和二進制檔案毫無差別,目錄檔案包含關于其他檔案的資訊,套接字是一種用來通過網絡與其他程序通信的檔案
根據st_mode位确定檔案類型的宏指令:
10.6 共享檔案
核心用三個相關的資料結構來表示打開的檔案:
- 描述符表
每個程序都有他獨立的描述符表,它的表項是由程序打開的檔案描述符來索引的,每個打開的描述符表項指向檔案表中的一個表項。
- 檔案表
打開檔案的集合是由一張檔案表來展示的,所有的程序共享這張表,每個檔案表的表項組成包括有目前的檔案位置,引用計數(即目前指向該表項的描述符表項數)以及一個指向v-node表中對應表項的指針,關閉一個描述符會減少相應的檔案表表項中的引用計數。核心不會删除這個檔案表表項,直到它的引用計數為0。
- v-node表
同檔案表一樣,所有的程序共享這張v-node表,每個表項包含stat結構中的大多數資訊,包括st_mode和st_size成員。
典型的打開檔案的核心資料結構:
檔案共享:
子程序如何繼承父程序的打開檔案:
10.7 I/O重定向
I/O重定向操作符允許使用者将磁盤檔案和标準輸入輸出聯系起來。I/O重定向使用dup2函數工作。
dup2函數拷貝描述符表表項oldfd到描述符表表項newfd,覆寫描述符表表項newfd以前的内容,如果newfd已經打開了,dup2會在拷貝oldfd之前關閉newfd。
通過調用dup2(4,1)重定向标準輸出之後的核心資料結構:
10.8 标準I/O
ANSI C定義了一組進階輸入輸出函數稱為标準I/O庫,該庫(libc)提供了打開和關閉檔案的函數(fopen和fclose)、讀和寫位元組的函數(fread和fwrite)、讀和寫字元串的函數(fgets和fputs),以及複雜的格式化I/O函數(scanf和printf)。
标準的I/O庫将一個打開的檔案模型化為一個流,一個流就是一個指向FILE類型的結構的指針,每個ANSI C程式開始時都有三個大開的流stdin、stdout和stderr分别對應于标準輸入、标準輸出和标準錯誤。
類型為FILE的流是對檔案描述符和流緩沖區的抽象,流緩沖區目的和RIO讀緩沖區目的一樣:就是使開銷較高的unix I/O系統調用的數量盡可能的小。
10.9 綜合:我該使用哪些I/O函數
unix I/O、标準I/O和RIO之間的關系:
unix對網絡的抽象是一種稱為套接字的檔案類型,套接字也是用檔案描述符來引用的,在這種情況下稱為套接字描述符。
标準I/O流,從某種意義上而言是全雙工的,因為程式能夠在同一個流上執行輸入和輸出。
對流的限制和對套接字的先知,有時候會互相沖突:
- 限制一:跟在輸出函數之後的輸入函數
- 限制二:跟在輸入函數之後的輸出函數
對流I/O的第一個限制能夠通過采用在每個輸入操作前重新整理緩沖區這樣的規則來滿足,滿足第二個限制的唯一辦法是,對同一個打開的套接字描述符打開兩個流,一個用來讀,一個用來寫。
建議在網絡套接字上不要用标準I/O來進行輸入和輸出,而是使用健壯的RIO函數。
10.10 小結
unix提供了少量的系統級函數,它們允許用用程式打開、關閉、讀和寫檔案,提取檔案的中繼資料,以及執行I/O重定向,unix的讀和寫操作會出現不足值,應用程式必須能正确地預計和處理這種情況,應用程式不應該直接調用unix I/O 函數,而應該使用RIO包,RIO包通過反複執行讀寫操作,直到傳送完所有的請求資料,自動處理不足值。
unix核心是用三個相關的資料結構來表示打開的檔案,描述符表中的表項指向打開檔案表中的表項,而打開檔案表中的表項又指向v-node表中的表項,每個程序都有他自己單獨的描述符表,而所有的程序共享同一個打開檔案表和v-node表。
标準I/O庫是基于unix I/O實作的,并提供了一組強大的進階I/O例程。對于大多數應用程式而言,标準I/O更簡單,是優于unix I/O的選擇,然而,對于标準I/O和網絡檔案的一些互相不相容限制,unix I/O比标準I/O更适用于網絡應用程式。