系統級 I/O練習題 (Practice - System-Level IO)
Related Concepts
- 10-System-IO/01-Unix-IO-and-Files
- 10-System-IO/02-Rio-Package-and-File-Metadata
- 10-System-IO/03-File-Sharing-Redirection-and-Standard-IO
| 關鍵字 | 答案 |
|---|---|
| open 回傳哪個 descriptor | 永遠是該 process 目前最小未開啟的非負整數 |
| read 回傳 0 | EOF(file position k ≥ 檔案大小 m);−1 才是錯誤;檔案裡沒有 EOF 字元 |
| short count 是不是錯誤 | 不是;成因:EOF、終端機一次一行、socket/pipe 緩衝與延遲 |
| rio_writen 何時 short count | 永不;rio_readn 只在 EOF |
| errno == EINTR | 被 signal handler 中斷,RIO 把本輪計數視為 0 自動重試 |
| 共不共享 file position | 看兩個 descriptor 指向同一個 file table entry(fork、dup2)還是不同 entry(各自 open) |
| dup2(oldfd, newfd) | 把 oldfd 的 descriptor entry 複製到 newfd;newfd 原本開著會先被 close |
| 三張表誰 per-process | descriptor table 每 process 一張;open file table 與 v-node table 全系統共享 |
| 新檔實際權限 | mode & ~umask |
| socket 用哪套 I/O | 一律 Rio;standard I/O 的順序限制需要 lseek,socket 上 lseek 非法 |
Question 1 - 最小描述子規則 [recall]
一個由 shell 建立的 process 執行
fd1 = Open("foo.txt", O_RDONLY, 0);,接著Close(fd1);,再執行fd2 = Open("baz.txt", O_RDONLY, 0);。
印出的fd2是多少?為什麼?
fd2 = 3。
open 永遠回傳該 process 目前最小未開啟的 descriptor。process 一出生 0(stdin)、1(stdout)、2(stderr)已被占用,第一次 open 得到 3;close(3) 之後 3 回到可用池,再 open 又拿到 3。(改編自書中 Practice Problem 10.1)
Question 2 - read 回傳值語意 [recall]
ssize_t read(int fd, void *buf, size_t n)的三種回傳值各代表什麼?檔案結尾有沒有「EOF 字元」?
回傳 −1 表錯誤;回傳 0 表 EOF(file position k ≥ 檔案大小 m);正值表實際傳輸的位元組數(可能小於 n,即 short count)。
檔案結尾沒有 EOF 字元——EOF 是 kernel 偵測到 k ≥ m 時觸發的條件,不是存在檔案裡的資料。
Question 3 - short count 成因 [recall]
read/write 傳輸的位元組數少於要求量稱為 short count。它是不是錯誤?列出三種會產生 short count 的情境。
不是錯誤。三種成因:(1) 讀到 EOF(如檔案只剩 20 bytes 卻要求 50,回傳 20,下一次回傳 0);(2) 從終端機讀 text line,每次 read 只傳一行;(3) 讀寫網路 socket 或 Linux pipe,受內部緩衝限制與網路延遲影響。
實務上:讀磁碟檔案除 EOF 外不會有 short count,寫磁碟檔案永遠不會。
Question 4 - open flags [recall]
說明
O_CREAT、O_TRUNC、O_APPEND三個 flag 的語意。O_APPEND是「open 時把位置移到檔尾一次」嗎?
O_CREAT:檔案不存在時建立一個截斷(空)的版本;O_TRUNC:檔案已存在時截斷之;O_APPEND:每次 write 之前都把 file position 設到檔案結尾。
不是只有 open 時一次——O_APPEND 的效果作用於每一次 write。
Question 5 - RIO 無緩衝函式 [recall]
rio_readn與rio_writen各在什麼情況下會回傳 short count?若read被 signal handler 中斷(errno == EINTR),RIO 如何處理?
rio_readn 只在遇到 EOF 時可能回傳 short count(回傳值介於 0 與 n);rio_writen 永不回傳 short count——要嘛寫滿 n,要嘛回傳 −1。
遇到 EINTR 時,RIO 把本輪計數視為 0 並自動重啟 read/write 呼叫,不把它當錯誤。
Question 6 - rio_readlineb 行為 [recall]
rio_readlineb(rp, usrbuf, maxlen)最多讀入幾個 bytes?讀到的行包含什麼?遇到超過 maxlen 的長行會怎樣?
最多讀 maxlen−1 bytes,保留 1 byte 給結尾的 NULL(0)字元。
正常情況下讀入的行含結尾換行符 \n,再補 NULL;超長的行會被截斷並補 NULL。使用前須先以 rio_readinitb 對該 descriptor 呼叫一次以關聯 rio_t 緩衝區(RIO_BUFSIZE = 8192)。
Question 7 - kernel 三張表 [recall]
kernel 用哪三個資料結構表示 open files?哪一張是 per-process,哪兩張全系統共享?file table entry 何時被刪除?
(1) Descriptor table:每個 process 一張,entry 指向 file table entry;(2) Open file table:全系統共享,含 file position 與 refcnt;(3) v-node table:全系統共享,含 stat 資訊(st_mode、st_size 等)。
close 使 refcnt 減 1;refcnt 歸零時 kernel 才刪除該 file table entry。
Question 8 - dup2 語意 [recall]
dup2(oldfd, newfd)做了什麼?若 newfd 原本已開啟會如何?要把 standard input 重導向到 descriptor 5,該怎麼呼叫?
dup2 把 descriptor table 的 entry oldfd 複製到 entry newfd,覆蓋 newfd 原本內容;若 newfd 原本已開啟,dup2 會先 close newfd 再複製。
重導向 stdin:dup2(5, 0),即 dup2(5, STDIN_FILENO)——oldfd(來源)在前、newfd(目的)在後。(書中 Practice Problem 10.4)
Question 9 - I/O 套件選用準則 [recall]
依 CSAPP 的三條準則:(1) 磁碟與終端機 I/O 首選哪套?(2) 為什麼不能用
scanf或rio_readlineb讀 binary 檔?(3) network socket 上該用哪套?
G1:儘量用 standard I/O(磁碟/終端機首選,但它無法存取檔案 metadata,如 st_size)。
G2:binary 檔可能充斥與行結尾無關的 0xa bytes,用行導向函式讀會以怪異、不可預測的方式失敗。
G3:socket 一律用 Rio——standard I/O 的輸入/輸出順序限制需要 lseek,而 socket 上 lseek 非法。
Question 10 - 同檔兩次 open [application]
檔案
foobar.txt內容為 6 個 ASCII 字元foobar。以下程式輸出為何?int fd1 = Open("foobar.txt", O_RDONLY, 0); int fd2 = Open("foobar.txt", O_RDONLY, 0); char c; Read(fd1, &c, 1); Read(fd2, &c, 1); printf("c = %c\n", c);
輸出 c = f。
同一檔案 open 兩次會得到兩個不同的 file table entry,各自維護獨立的 file position。fd1 讀走 f 只推進自己的 position;fd2 的 position 仍在 0,讀到的還是第 1 個 byte f。(書中 Practice Problem 10.2)
Question 11 - fork 後共享 position [application]
同樣的
foobar.txt(內容foobar),以下程式輸出為何?int fd = Open("foobar.txt", O_RDONLY, 0); char c; if (Fork() == 0) { Read(fd, &c, 1); exit(0); } Wait(NULL); Read(fd, &c, 1); printf("c = %c\n", c);
輸出 c = o。
fork 時子行程複製父的 descriptor table,父子的 fd 指向同一個 file table entry,因此共享 file position(refcnt 變 2)。子行程先讀走 f 把 position 推到 1,父行程等子結束後再讀,拿到第 2 個 byte o。(書中 Practice Problem 10.3)
Question 12 - dup2 後讀取 [application]
同樣的
foobar.txt(內容foobar),以下程式輸出為何?int fd1 = Open("foobar.txt", O_RDONLY, 0); int fd2 = Open("foobar.txt", O_RDONLY, 0); char c; Read(fd2, &c, 1); Dup2(fd2, fd1); Read(fd1, &c, 1); printf("c = %c\n", c);
輸出 c = o。
Read(fd2) 先把 fd2 的 file position 推到 1;Dup2(fd2, fd1) 把 fd2 的 descriptor entry 複製到 fd1,兩者從此指向同一個 file table entry(position = 1)。接著 Read(fd1) 從位置 1 讀到 o。(書中 Practice Problem 10.5)
Question 13 - dup2(4,1) 的表結構變化 [analysis]
初始狀態:fd 1(stdout)指向 File A(終端機,refcnt=1),fd 4 指向 File B(磁碟檔,refcnt=1)。呼叫
dup2(4, 1)後,分析:(a) 兩個檔案的 refcnt 各變成多少?(b) File A 的 file table entry 命運如何?(c) fd 1 與 fd 4 是否共享 file position?
(a) dup2 先 close fd 1,File A 的 refcnt 由 1 減為 0;fd 1 改指 File B 的 entry,File B refcnt 由 1 增為 2。
(b) refcnt 歸零,kernel 刪除 File A 的 file table entry(與對應 v-node 參照)。
(c) 共享——fd 1 與 fd 4 指向同一個 file table entry,任一方 read/write 移動 position 都影響另一方。從此寫到 stdout 的資料全部進入 File B,這正是 shell > 重導向的機制。
Question 14 - 為何 socket 不能用 standard I/O [analysis]
Standard I/O stream 是 full duplex,理論上一個 stream 就能對 socket 讀寫。請分析:(1) stream 的兩條 I/O 順序限制是什麼?(2) 為何在 socket 上無法滿足?(3) 對同一 socket descriptor 開兩個 stream(
fdopen一讀一寫)為什麼仍是災難配方?
(1) 限制一:output 之後接 input,中間必須 fflush/fseek/fsetpos/rewind;限制二:input 之後接 output,中間必須 fseek/fsetpos/rewind(除非 input 遇到 EOF)。
(2) 後三者都靠 lseek 重設 file position,而 socket 上呼叫 lseek 非法;限制一還能靠「每次輸入前先 flush」繞過,限制二繞不掉。
(3) 雙 stream 要各自 fclose 才不會 memory leak,但兩次 fclose 都會嘗試關同一個 socket descriptor,第二次必然失敗;在多執行緒程式中關閉已關閉的 descriptor 是災難配方。解法:socket 一律用 Rio(輸出 sprintf+rio_writen,輸入 rio_readlineb+sscanf)。
| 答題模式 | 要點 |
|---|---|
| 問新 descriptor 編號 | open 永遠回傳最小未開啟的非負整數;0/1/2 出生即占用 |
| 問 read 回傳 0 / short count | 0 = EOF(無 EOF 字元);short count 非錯誤(EOF、終端機一行、socket/pipe) |
| 問 RIO 回傳值 | rio_readn 僅 EOF short count;rio_writen 永不;EINTR 一律自動重試 |
| 問 rio_readlineb | 最多 maxlen−1 bytes,含 \n 補 NULL,超長截斷 |
| 問「讀到哪個字元」 | 唯一判準:兩 fd 指向同一 file table entry(fork/dup2 → 共享 position)或不同 entry(各自 open → 獨立 position) |
| 問 dup2(old, new) | 複製 old 的 entry 到 new;new 原開啟則先 close;來源在前、目的在後 |
| 問 refcnt | close/被 dup2 覆蓋 → 減 1,歸零刪 entry;fork/dup2 指入 → 加 1 |
| 問三張表歸屬 | descriptor table per-process;open file table 與 v-node table 全系統共享 |
| 問該用哪套 I/O | 磁碟/終端機用 standard I/O;binary 別用行導向函式;socket 一律 Rio |