第十章 系統級I/O
輸入輸出I/O是在主存和外部裝置(如磁盤,網絡和終端)之間拷貝資料的過程。
輸入就是從I/O裝置拷貝資料到主存,而輸出就是從主存拷貝資料到I/O裝置。
10.1 unix i/o
所有的I/O裝置,如網絡、磁盤和終端,都被模型化為檔案,而所有的輸入和輸出都被當做對相應的檔案的讀和寫來執行。這種将裝置優雅地映射為檔案的方式,允許Unix核心引出一個簡單、低級的的應用接口,稱為Unⅸ I/O,這使得所有的輸入和輸出都能以一種統一且一緻的方式來執行:
- 打開檔案。一個應用程式通過要求核心打開相應的檔案,來宣告它想要通路一個I/O裝置。核心傳回一個小的非負整數,叫做描述符,它在後續對此檔案的所有操作中辨別這個檔案。核心記錄有關這個打開檔案的所有資訊。應用程式隻需記住這個描述符。Unⅸ外殼建立的每個程序開始時都有三個打開的檔案:标準輸入(描述符為0)、标準輸出(描述符為1)和标準錯誤(描述符為2)。頭檔案可用來代替顯式的描述符值。
- 改變目前的檔案位置。對于每個打開的檔案,核心保持着一個檔案位置k,初始為0。這個檔案位置是從檔案開頭起始的位元組偏移量。應用程式能夠通過執行seek操作,顯式地設定檔案的目前位置為k。
- 讀寫檔案。一個讀操作就是從檔案拷貝n>0個位元組到存儲器,從目前檔案位置k開始,然後将k增加到k+n。給定一個大小為m位元組的檔案,當k>=m時執行讀操作會觸發―個稱為end-of-file(EOF)的條件,應用程式能檢測到這個條件。在檔案結尾處處并沒有明确的“EOF”符号。
- 關閉檔案。當應用完成了對檔案的通路之後,它就通知核心關閉這個檔案。作為響應,核心釋放檔案打開時建立的資料結構,并将這個描述符恢複到可用的描述符池中。無論一個程序因為何種原因終止時,核心都會關閉所有打開的檔案并釋放它們的存儲器資源。
10.2 打開和關閉檔案
- 程序是通過調用open函數來打開一個已存在的檔案或者建立一個新檔案
- flags參數表示程序打算如何通路這個檔案,它的值包括:
O_RDONLY O_WRONLY O_RDWR
- flags參數也可以是一個或者更多位掩碼的或,提供一些額外的訓示:
O_CREAT O_TRUNC:如果檔案已經存在,就截斷它。 O_APPEND
- mode參數指定了新檔案的通路權限位。符号名字如下圖。作為上下文的一部分,每個程序都有一個umask它是通過調用umask函數來設定的。當程序通過帶某個mode參數的open函數調用來建立一個新檔案時,檔案的通路權限位被設定為mode&umask。
資訊安全系統設計基礎第九周學習總結
10.3 讀和寫檔案
應用程式是通過分别調用系統函數 read和write函數來執行輸入和輸出的。
旁注:size_t是作為usigned int,而ssize_t是作為int。
在某些情況下,read和write傳送的位元組比應用程式要求的要少。出現這種情況的可能的原因有:
讀時遇到EOF。假設該檔案從目前檔案位置開始隻含有20個位元組,而應用程式要求我們以50個位元組的片進行讀取,這樣一來,這個read的傳回的值是20,在此之後的read則傳回0。 從終端讀文本行。如果打開的檔案是與終端相關聯的,那麼每個read函數将一次傳送一個文本行,傳回的不足值等于文本行的大小。 讀和寫socket。如果打開的檔案對應于網絡套接字,那麼内部緩沖限制和較長的網絡延遲會導緻read和write傳回不足值。
10.4 用rio包健壯地讀寫
RIO提供了兩類不同的函數:
無緩沖的輸入輸出函數 帶緩沖的輸入函數
1 rio的無緩沖的輸入輸出函數
- rio_readn函數從描述符fd的目前檔案位置最多傳送n個位元組到存儲器位置usrbuf。類似的rio_writen函數從位置usrbuf傳送n個位元組到描述符fd。rio_readn函數在遇到EOF時隻能傳回一個不足值。rio_writen函數絕不會傳回不足值。
- 注意:如果rio_readn和rio_writen函數被一個從應用信号處理程式的傳回中斷,那麼每個函數都會手動地重新開機read或write。
2 rio的帶緩沖的輸入函數
- 一個文本行就是一個由 換行符 結尾的ASCII碼字元序列。在Unix系統中,換行符是‘\n’,與ASCII碼換行符LF相同,數值為0x0a。假設我們要編寫一個程式來計算文本檔案中文本行的數量應該如何來實作呢?
一種方法是用read函數來一次一個位元組地從檔案傳送到使用者存儲器,檢查每個位元組來查找換行符。這種方法的問題就是效率不高,每次取檔案中的一個位元組都要求陷入核心。 一種更好的方法是調用一個包裝函數(rio_readlineb),它從一個内部緩沖區拷貝一個文本行,當緩沖區變空時,會自動的調用read系統調用來重新填滿緩沖區。
- 在帶緩沖區的版本中,每打開一個描述符都會調用一次rio_readinitb函數,它将描述符fd和位址rp處的一個類型為rio_t的讀緩沖區聯系起來。
- rio_readinitb函數從檔案rp讀取一個文本行(包括結尾的換行符),将它拷貝到存儲器位置usrbuf,并且用空字元來結束這個文本行。
- RIO讀程式的核心是rio_read函數,rio_read函數可以看成是Unix read函數的帶緩沖區的版本。當調用rio_read要求讀取n個位元組的時候,讀緩沖區内有rp->rio_cnt個未讀的位元組。如果緩沖區為空的時候,就會調用read系統函數去填滿緩沖區。這個read調用收到一個不足值的話并不是一個錯誤,隻不過讀緩沖區的是填充了一部分。
- 一旦緩沖區非空,rio_read就從讀緩沖區拷貝n和rp->rio_cnt中較小值個位元組到使用者緩沖區,并傳回拷貝位元組的數目。
- 對于應用程式來說,rio_read和系統調用read有着相同的語義。出錯時傳回-1;在EOF時,傳回0;如果要求的位元組超過了讀緩沖區内未讀的位元組的數目,它會傳回一個不足值。rio_readlineb函數多次調用rio_read函數。每次調用都從讀緩沖區傳回一個位元組,然後檢查這個位元組是否是結尾的換行符。
- rio_readlineb函數最多讀取(maxlen-1)個位元組,餘下的一個位元組留給結尾的空字元。超過maxlen-1位元組的文本行被截斷,并用一個空字元結束。
10.5 讀取檔案中繼資料
- 應用程式能夠通過調用stat和fstat函數,檢索到關于檔案的資訊。
- stat函數結構
- st_size成員包含了檔案的位元組數大小。st_mode成員則編碼了檔案通路許可位和檔案類型。Unix識别大量不同的檔案類型。普通檔案包括某種類型的二進制或文本資料。對于核心而言,文本檔案和二進制檔案毫無差別。
- 目錄檔案包含關于其他檔案的資訊。套接字是一種用來通過網絡與其他程序通信的檔案。Unix提供的宏指令根據st_mode成員來确定檔案的類型。
10.6 共享檔案
核心用三個相關資料結構來表示打開的檔案
描述符表, 檔案表, v-node表
10.7 i/o重定向
- Unix外殼提供了I/O重定向操作符,允許使用者将磁盤檔案和标準輸入輸出聯系起來。
- I/O重定向的工作方式: 一種是使用dup2函數。
- dup2函數拷貝描述符表表項oldfd到描述符表表項newfd,覆寫描述符表表項newfd以前的内容。如果newfd已經打開了,dup2會在拷貝oldfd之前關閉newfd。
10.8 标準i/o
- ANSI C定義了一組進階輸入輸出函數,成為标準I/O庫,為程式員提供了Unix I/O的較進階别的替代。這個庫(libc)提供了打開和關閉檔案的函數(fopen和fclose)、讀和寫位元組的函數(fread和fwrite)、讀和寫字元串的函數(fgets和fputs)、以及複雜的格式化I/O函數(printf和scanf)。
- 标準I/O庫将一個打開的檔案模型化為一個流。對于程式員而言,一個流就是一個指向FILE類型的結構的指針。每個ANSI C程式開始時都有三個打開的流stdin、stdout和stderr,分别對應于标準輸入、标準輸出和标準錯誤。
遇到的問題:我以習題10.1為例
在做習題10.1時,我建立檔案編譯,最開始時,不能識别頭檔案“csapp.h”:
我查了一下,此頭檔案是為了讓函數能正常運作,我就将它改了一下“stdio.h”,為了exit(0)能正常運作,又加上了”stdlib.h”,此外還要将open所需的頭檔案加上,不然會傳回出錯,再運作編譯發現不能識别“Open,Close,”,這是因為它們不能大寫,然後修改完成後如下:
運作發現:
為什麼打開檔案會出錯,這是因為我們并沒有“foo.txt”和”baz.txt”,用vim建立一下,再次編譯運作就有正确的結果了:
如果你将程式裡的close(fd1)删掉,你會發現fd2的結果變成了4,那麼出現這些結果的原因是什麼呢?
UNIX外殼在最開始建立每個程序是就将:标準輸入(描述符0),标準輸出(描述符1),标準錯誤(描述符2)的描述符賦給了stdin,而open函數總是傳回最低的未打開描述符,是以fd1的結果一定為3,而調用close函數會釋放描述符3,這樣fd2的結果就變成了3,而我們将程式裡的close(fd1)删掉,描述符3未被釋放,fd2的結果就變成了4.
參考資料:
《深入了解計算機系統》
另附部分實踐運作結果如下:
root@kali:~/fs# gcc ls1.c -o ls1
root@kali:~/fs# ./ls1
.
..
ls1
a.out
echostate.c
setecho.c
testioctl.c
spwd.c
ls2.c
ls1.c
filesize.c
fileinfo.c
who1.c
cp1.c
who2.c
error a
root@kali:~/fs# gcc ls2.c -o ls2
root@kali:~/fs# ./ls2
drwxr-xr-x 4 1000 inetsim 360 Nov 4 02:39 .
drwxrwxr-x 15 root root 500 Nov 4 02:25 ..
-rwxr-xr-x 1 root root 10103 Nov 4 02:39 ls2
-rwxr-xr-x 1 root root 7800 Nov 4 02:34 ls1
-rwxrwxr-x 1 1000 inetsim 7408 Nov 21 00:28 a.out
-rw-rw-r-- 1 1000 inetsim 502 Nov 20 23:52 echostate.c
-rw-rw-r-- 1 1000 inetsim 626 Nov 20 23:52 setecho.c
-rw-rw-r-- 1 1000 inetsim 337 Nov 20 23:49 testioctl.c
-rw-rw-r-- 1 1000 inetsim 1284 Sep 9 21:23 spwd.c
-rw-rw-r-- 1 1000 inetsim 2533 Sep 9 21:23 ls2.c
-rw-rw-r-- 1 1000 inetsim 551 Sep 9 21:23 ls1.c
-rw-rw-r-- 1 1000 inetsim 231 Sep 9 21:23 filesize.c
-rw-rw-r-- 1 1000 inetsim 809 Sep 9 21:23 fileinfo.c
-rw-rw-r-- 1 1000 inetsim 725 Sep 9 21:24 who1.c
-rw-rw-r-- 1 1000 inetsim 911 Sep 9 21:23 cp1.c
-rw-rw-r-- 1 1000 inetsim 725 Oct 31 10:59 who2.c
drwxrwxr-x 2 1000 inetsim 140 Sep 10 02:09 error
drwxr-xr-x 4 1000 inetsim 80 Nov 21 00:41 a