檔案共享、重導向與標準 I/O (File Sharing, Redirection & Standard I/O)

Overview Table

小節 主題 核心結論
10.8 (p.942) Sharing Files kernel 用三張表表示 open files:descriptor table(每 process 一張)、open file table(全系統共享,含 file position 與 refcnt)、v-node table(全系統共享,含 stat 資訊)
10.8 (p.943) 同檔多開 對同一檔案 open 兩次 → 兩個 descriptor 指向不同 file table entry → 各自獨立的 file position
10.8 (p.944) fork 繼承 子行程複製父行程的 descriptor table → 父子指向同一 file table entry → 共享 file position、refcnt 加倍
10.9 (p.945) I/O Redirection dup2(oldfd, newfd) 把 descriptor table entry oldfd 複製到 newfd;shell 的 ls > foo.txt 即靠此實作
10.10 (p.947) Standard I/O libc 把 open file 抽象為 stream(FILE *)= descriptor + stream buffer,目的與 Rio buffer 相同:減少系統呼叫
10.11 (p.947) 該用哪套 I/O G1 儘量用 standard I/O;G2 別用 scanf/rio_readlineb 讀 binary;G3 socket 一律用 Rio
10.12 (p.949) Summary descriptor table → file table → v-node table 的三層結構,同時解釋 file sharing 與 redirection

10.8 Sharing Files:kernel 的三張表 (p.942)

kernel 用三個相互關聯的資料結構表示 open files,理解它們是理解 file sharing 的前提:

典型情況:無共享(Figure 10.12, p.943)

descriptor 1 與 4 透過不同 file table entry 指向兩個不同檔案:

Descriptor table      Open file table         v-node table
(每 process 一張)     (全 process 共享)        (全 process 共享)

fd 0                    File A
fd 1  ──────────────►  ┌──────────┐          ┌────────────┐
fd 2                   │ File pos │ ───────► │ File access│
fd 3                   │ refcnt=1 │          │ File size  │
fd 4  ────────┐        └──────────┘          │ File type  │
              │                              └────────────┘
              │         File B
              └──────► ┌──────────┐          ┌────────────┐
                       │ File pos │ ───────► │ File access│
                       │ refcnt=1 │          │ File size  │
                       └──────────┘          │ File type  │
                                             └────────────┘

情況二:同一檔案開兩次(Figure 10.13, p.943)

對同一 filename 呼叫 open 兩次 → 兩個 descriptor 各有自己的 file table entry(各自的 file position),但都指向同一個 v-node entry:

fd 3 ──► [File pos | refcnt=1] ──┐
                                 ├──► [同一個 v-node: File A]
fd 4 ──► [File pos | refcnt=1] ──┘

情況三:fork 之後的繼承(Figure 10.14, p.944)

fork 後子行程取得父行程 descriptor table 的複本;父子指向同一組 open file table entry:

Parent's table                Open file table
fd 3 ──────────┐
               ├────────►  [File pos | refcnt=2] ──► v-node
Child's table  │
fd 3 ──────────┘
Important

判斷「共不共享 file position」只看一件事:兩個 descriptor 指向的是同一個 file table entry(fork 繼承、dup2 複製 → 共享),還是不同 entry(各自 open → 獨立)。這是本章考題(Problems 10.2、10.3、10.5)的唯一判準。

Warning

「每個 descriptor 對應一個獨立檔案位置」只是典型情況的一般化敘述;fork 與 dup2 都是例外(共享同一 entry)。另外 file table、v-node table 是全系統共享,descriptor table 才是 per-process,方向別記反。


10.9 I/O Redirection 與 dup2 (p.945)

Linux shell 提供 I/O 重導向運算子,把 standard input/output 關聯到磁碟檔案,例如 linux> ls > foo.txtls 的 stdout 重導向到 foo.txt(Web server 執行 CGI 程式時做的是同類的事,見 11-Network-Programming/04-Web-Servers-and-Tiny)。實作方式之一是 dup2:

#include <unistd.h>
int dup2(int oldfd, int newfd);   // 成功回傳非負 descriptor,錯誤回傳 -1

dup2(4,1) 前後變化(Figure 10.12 → Figure 10.15, p.946)

呼叫前 fd 1(stdout)→ File A(終端機)、fd 4 → File B(磁碟檔),refcnt 皆為 1。呼叫 dup2(4,1) 後:

呼叫前:                          呼叫後 dup2(4,1):
fd 1 ──► File A [refcnt=1]        fd 1 ──┐        File A [refcnt=0 → 刪除]
fd 4 ──► File B [refcnt=1]               ├──► File B [refcnt=2]
                                  fd 4 ──┘
Tip

shell 實作 prog > file 的流程:fork → 子行程 open(file) 得到某 fd → dup2(fd, 1)execve(prog)。因為 execve 不會重設 descriptor table,重導向得以跨越程式載入而保留(參見 08-Exceptional-Control-Flow/04-Process-Control 與 Homework 10.9 的情境)。


10.10 Standard I/O (p.947)

C 語言定義一組更高階的 I/O 函式,稱為 standard I/O library(libc),是 Unix I/O 的高階替代:

功能 函式
開/關檔 fopen, fclose
讀寫 bytes fread, fwrite
讀寫字串 fgets, fputs
格式化 I/O scanf, printf
#include <stdio.h>
extern FILE *stdin;    /* standard input  (descriptor 0) */
extern FILE *stdout;   /* standard output (descriptor 1) */
extern FILE *stderr;   /* standard error  (descriptor 2) */

10.11 該用哪一套 I/O 函式?(p.947)

三層 I/O 套件的關係(Figure 10.16):

        ┌──────────────────────────────────────────┐
        │           C application program           │
        └──────┬──────────────────────────┬─────────┘
               │                          │
   ┌───────────▼──────────┐   ┌───────────▼──────────┐
   │  Standard I/O 函式    │   │      Rio 函式         │
   │ fopen fdopen fread   │   │ rio_readn rio_writen │
   │ fwrite fscanf fprintf│   │ rio_readinitb        │
   │ sscanf sprintf fgets │   │ rio_readlineb        │
   │ fputs fflush fseek   │   │ rio_readnb           │
   │ fclose               │   │                      │
   └───────────┬──────────┘   └───────────┬──────────┘
               │                          │
        ┌──────▼──────────────────────────▼─────────┐
        │        Unix I/O 函式(經 system call)      │
        │   open  read  write  lseek  stat  close   │
        │            (實作於 OS kernel)              │
        └────────────────────────────────────────────┘

三條使用準則 (p.948)

準則 內容
G1 儘量使用 standard I/O:磁碟與終端機裝置 I/O 的首選。多數 C 程式設計師終其職涯只用 standard I/O(例外:stat 在 standard I/O 沒有對應函式)
G2 不要用 scanfrio_readlineb 讀 binary 檔:它們是為文字檔設計的。binary 檔可能充斥與行結尾無關的 0xa bytes,會使程式以怪異、不可預測的方式失敗
G3 network socket 上一律用 Rio:standard I/O 用在網路上會有棘手問題(見下)

為什麼 socket 不能用 standard I/O (p.948–949)

Standard I/O stream 是 full duplex(同一 stream 可讀可寫),但有兩條文件記載不清的限制,與 socket 的限制彼此衝突:

FILE *fpin, *fpout;
fpin  = fdopen(sockfd, "r");
fpout = fdopen(sockfd, "w");
Tip

Socket 上的格式化 I/O 替代方案:輸出sprintf 先在記憶體格式化字串,再用 rio_writen 送出;輸入rio_readlineb 讀整行文字,再用 sscanf 抽取欄位。


10.12 Summary (p.949)


Exam/Test Patterns

情境/關鍵字 答案
同檔 open 兩次,Read(fd1)Read(fd2) 讀到什麼?(Problem 10.2) 兩個 fd 各有 file table entry → 各自 file position → fd2 仍讀第 1 個 byte,輸出 c = f(不是 o)
open 一次後 fork,子先讀 1 byte、父再讀(Problem 10.3) 父子共享同一 file table entry 與 file position → 父讀到第 2 個 byte,輸出 c = o
開兩次後 Read(fd2)Dup2(fd2,fd1)Read(fd1)(Problem 10.5) dup2 後 fd1 指向 fd2 的 entry(position 已在 1)→ 讀到第 2 個 byte,輸出 c = o
「如何把 stdin 重導向到 descriptor 5?」(Problem 10.4) dup2(5, 0),即 dup2(5, STDIN_FILENO);oldfd 在前、newfd 在後
dup2(4,1) 後 refcnt 變化 原 fd1 指向的檔案 refcnt 減 1(歸零則刪 entry);fd4 指向的檔案 refcnt 加 1(1→2)
「file table entry 何時被 kernel 刪除?」 refcnt 歸零時;fork 後父子都要 close 該 descriptor 才會刪除
「哪張表是 per-process?哪些共享?」 descriptor table 每 process 一張;open file table 與 v-node table 全系統共享
「stream buffer 的目的?」 與 Rio read buffer 相同:減少昂貴的 I/O 系統呼叫(getc 一次 read 填 buffer,之後從 buffer 取)
「binary 檔可以用 scanf/rio_readlineb 讀嗎?」 不行(G2):binary 檔的 0xa bytes 與行結尾無關,會導致不可預測的失敗
「socket 該用哪套 I/O?為什麼不用 standard I/O?」 用 Rio(G3);stream 的 I/O 順序限制需要 lseek,但 socket 上 lseek 非法;開雙 stream 又導致重複 close 同一 descriptor
「socket 上要格式化 I/O 怎麼辦?」 輸出:sprintf + rio_writen;輸入:rio_readlineb + sscanf
open 回傳哪個 descriptor?」 永遠回傳目前最小的未使用 descriptor(Problem 10.1/10.6 型考題基礎)