RIO 套件與檔案中繼資料 (RIO Package & File Metadata)
Overview Table
| 主題 | 核心 API | 解決的問題 | 書頁 |
|---|---|---|---|
| RIO 無緩衝 I/O | rio_readn / rio_writen |
short count 自動重試、EINTR 自動重啟 |
p.933-935 |
| RIO 緩衝輸入 | rio_readinitb / rio_readlineb / rio_readnb |
逐行讀取免除每 byte 一次 trap to kernel | p.934-938 |
| 內部引擎 | rio_read(static) |
帶應用層緩衝的 read 替身,語意與 Linux read 相同 |
p.936-937 |
| 檔案中繼資料 | stat / fstat + struct stat |
取得 metadata(st_mode、st_size 等) |
p.939-940 |
| 目錄讀取 | opendir / readdir / closedir |
以 directory stream 逐項列舉目錄項目 | p.941-942 |
10.5 RIO(Robust I/O)套件總覽 (p.933)
Unix read/write 在網路程式等情境會回傳 short count(實際傳輸位元組數少於要求數),應用程式必須反覆呼叫直到傳完。RIO 套件自動處理 short count,提供健壯、方便、高效的 I/O,是後續網路程式(Ch.11、Ch.12)的基礎。RIO 提供兩類函式:
- Unbuffered 輸入輸出函式:資料直接在記憶體與檔案間傳輸,無應用層緩衝;特別適合網路上的二進位資料讀寫。
- Buffered 輸入函式:內容先快取在應用層緩衝區(類似 standard I/O 的
printf緩衝),可高效讀取文字行與二進位資料;且與 Stevens 的版本不同,RIO 緩衝函式是 thread-safe(見 12-Concurrent-Programming/04-Parallelism-and-Thread-Safety),可在同一 descriptor 上任意交錯(先讀幾行文字、再讀二進位、再讀文字)。
- Ch.11/12 的網路應用全部使用 RIO;2. 研讀其原始碼能加深對 Unix I/O 的整體理解。
10.5.1 RIO 無緩衝輸入輸出函式 (p.933-935)
#include "csapp.h"
ssize_t rio_readn(int fd, void *usrbuf, size_t n);
ssize_t rio_writen(int fd, void *usrbuf, size_t n);
// 回傳:成功為已傳輸位元組數;EOF 回傳 0(僅 rio_readn);錯誤回傳 -1
rio_readn:從fd的目前檔案位置傳送最多 n bytes 到usrbuf;只有遇到 EOF 才可能回傳 short count。rio_writen:從usrbuf傳送 n bytes 到fd;永不回傳 short count。- 兩者可在同一 descriptor 上任意交錯呼叫。
- 實作核心(Figure 10.4):以
nleft迴圈補足;若被 signal handler 返回中斷(errno == EINTR),手動重啟read/write(將本輪計數視為 0 再試),以達最大可攜性。
rio_readn(fd, usrbuf, n) 迴圈流程:
nleft = n
│
▼
┌─────────────── nleft > 0 ? ──────────── no ──► return n - nleft
│ yes
▼
nread = read(fd, bufp, nleft)
│
├─ nread < 0 ─┬─ errno==EINTR ──► nread=0(重試,回到迴圈頂)
│ └─ 其他錯誤 ───────► return -1
├─ nread == 0 ──► break(EOF)──► return n - nleft(short count)
└─ nread > 0 ──► nleft -= nread; bufp += nread; 回到迴圈頂
rio_readn 回傳值 < n 不一定是錯誤
回傳值介於 0 與 n 之間表示提早遇到 EOF;回傳 -1 才是錯誤(errno 由 read 設定)。rio_writen 則保證要嘛寫滿 n、要嘛 -1。
10.5.2 RIO 緩衝輸入函式 (p.934-938)
動機:若用 read 一次讀 1 byte 找換行符來計算文字檔行數,每個 byte 都要 trap 進 kernel,極度低效。較佳作法是由 wrapper(rio_readlineb)從內部讀取緩衝區複製文字行,緩衝空了才呼叫 read 補滿。針對文字行與二進位資料混合的檔案(如 Section 11.5.3 的 HTTP response),另提供共用同一緩衝區的 rio_readnb。
#include "csapp.h"
void rio_readinitb(rio_t *rp, int fd); // 無回傳值
ssize_t rio_readlineb(rio_t *rp, void *usrbuf, size_t maxlen);
ssize_t rio_readnb(rio_t *rp, void *usrbuf, size_t n);
// 回傳:成功為已讀位元組數;EOF 回傳 0;錯誤回傳 -1
rio_readinitb:每個開啟的 descriptor 呼叫一次,把fd與位於rp的rio_t讀取緩衝區關聯起來(設定空緩衝)。rio_readlineb:讀取下一個文字行(含結尾換行符\n)到usrbuf,並以 NULL(0)字元結尾;最多讀maxlen-1bytes,超長的行會被截斷並補 NULL。rio_readnb:從rp讀最多 n bytes 原始資料到usrbuf。rio_readlineb與rio_readnb可任意交錯;但不可與無緩衝的rio_readn交錯使用同一 descriptor。
rio_t 讀取緩衝區結構(Figure 10.6, p.936)
#define RIO_BUFSIZE 8192
typedef struct {
int rio_fd; /* 此內部緩衝所屬的 descriptor */
int rio_cnt; /* 內部緩衝中尚未讀取的 bytes */
char *rio_bufptr; /* 內部緩衝中下一個未讀 byte */
char rio_buf[RIO_BUFSIZE]; /* 內部緩衝區 */
} rio_t;
rio_readinitb 初始化:rp->rio_fd = fd; rp->rio_cnt = 0; rp->rio_bufptr = rp->rio_buf;
內部引擎 rio_read(Figure 10.7, p.936-937)
rio_read 是 Linux read 的緩衝版,對應用程式而言語意與 read 完全相同:錯誤回傳 -1 並設 errno;EOF 回傳 0;若要求的 bytes 超過緩衝中未讀 bytes 則回傳 short count。
rio_read(rp, usrbuf, n) 資料流:
kernel rio_t(應用層) 使用者
┌──────────────┐ read() ┌──────────────────────┐ memcpy ┌─────────┐
│ 檔案 / 網路 │ ──────────► │ rio_buf[8192] │ ─────────► │ usrbuf │
└──────────────┘ 緩衝空時 │ ▲rio_bufptr │ min(n, └─────────┘
才補滿 │ rio_cnt 未讀計數 │ rio_cnt)
└──────────────────────┘
while (rio_cnt <= 0): rio_cnt = read(rio_fd, rio_buf, sizeof(rio_buf))
<0 且 errno!=EINTR → return -1 (EINTR 則重試)
==0 → return 0 (EOF)
>0 → rio_bufptr = rio_buf(重設指標)
cnt = min(n, rio_cnt); memcpy; rio_bufptr += cnt; rio_cnt -= cnt; return cnt
- 補緩衝時
read回傳 short count 不是錯誤,只是緩衝被部分填滿。 - 因語意與
read相同,以rio_read取代read即可搭出各種緩衝讀取函式:rio_readnb=rio_readn的程式結構 +rio_read取代read(Figure 10.8)。rio_readlineb= 最多呼叫rio_readmaxlen-1 次,每次取 1 byte(實際來自緩衝,不 trap kernel),逐 byte 檢查是否為\n;EOF 時若已讀到部分資料則回傳該行,完全沒讀到才回傳 0。
使用範例:逐行複製 stdin 到 stdout(Figure 10.5, p.936)
int main(int argc, char **argv) {
int n; rio_t rio; char buf[MAXLINE];
Rio_readinitb(&rio, STDIN_FILENO);
while ((n = Rio_readlineb(&rio, buf, MAXLINE)) != 0)
Rio_writen(STDOUT_FILENO, buf, n);
}
RIO 源自 Stevens 網路程式設計經典中的 readline/readn/writen。rio_readn/rio_writen 與 Stevens 版完全相同;但 Stevens 的 readline 有兩個缺陷:(1) readline 有緩衝而 readn 無,不能混用於同一 descriptor;(2) 使用 static buffer,非 thread-safe(Stevens 需另提供 readline_r)。RIO 以互相相容且 thread-safe 的 rio_readlineb/rio_readnb 修正了這兩點。
RIO 函式比較表
| 函式 | 緩衝 | 用途 | 回傳 short count 的時機 | 可交錯對象 |
|---|---|---|---|---|
rio_readn |
無 | 二進位資料(網路) | 僅 EOF | rio_writen |
rio_writen |
無 | 任何輸出 | 永不 | rio_readn |
rio_readlineb |
有 | 文字行(含 \n,補 NULL) |
EOF 或行滿 maxlen-1 |
rio_readnb |
rio_readnb |
有 | 二進位資料(與文字混合檔) | 僅 EOF | rio_readlineb |
10.6 讀取檔案中繼資料 (p.939-940)
應用程式可用 stat/fstat 取得檔案的 metadata(中繼資料):
#include <unistd.h>
#include <sys/stat.h>
int stat(const char *filename, struct stat *buf); // 以檔名查詢
int fstat(int fd, struct stat *buf); // 以 descriptor 查詢
// 回傳:成功 0,錯誤 -1
struct stat 主要成員(Figure 10.9;本書重點為 st_mode 與 st_size,Web 伺服器 Section 11.5 會用到):
| 成員 | 型別 | 意義 |
|---|---|---|
st_dev |
dev_t |
裝置 |
st_ino |
ino_t |
inode 編號 |
st_mode |
mode_t |
權限位元 + 檔案類型 |
st_nlink |
nlink_t |
hard link 數 |
st_uid / st_gid |
uid_t/gid_t |
擁有者 user/group ID |
st_rdev |
dev_t |
裝置類型(若為 device inode) |
st_size |
off_t |
檔案大小(bytes) |
st_blksize / st_blocks |
unsigned long |
檔案系統 I/O 區塊大小 / 已配置區塊數 |
st_atime / st_mtime / st_ctime |
time_t |
最後存取 / 修改 / 變更時間 |
st_mode同時編碼權限位元(如S_IRUSR,見 10-System-IO/01-Unix-IO-and-Files 的 Figure 10.2)與檔案類型。- Linux 在
sys/stat.h定義巨集判定式 (macro predicates) 判斷檔案類型:
| 巨集 | 判斷 |
|---|---|
S_ISREG(m) |
是否為 regular file(一般檔案) |
S_ISDIR(m) |
是否為 directory(目錄) |
S_ISSOCK(m) |
是否為 network socket |
- 用法(Figure 10.10
statcheck.c):Stat(argv[1], &stat)後,以S_ISREG(stat.st_mode)判類型、以stat.st_mode & S_IRUSR檢查擁有者讀取權限。
檔案類型必須用 S_ISxxx(m) 巨集判斷(類型是編碼欄位,不是單一 bit);權限則用 st_mode & S_IRUSR 這類位元遮罩檢查。
10.7 讀取目錄內容 (p.941-942)
應用程式以 readdir 系列函式讀取目錄內容:
#include <sys/types.h>
#include <dirent.h>
DIR *opendir(const char *name); // 成功回傳 stream handle 指標,錯誤 NULL
struct dirent *readdir(DIR *dirp); // 下一個目錄項目;無更多項目或錯誤時 NULL
int closedir(DIR *dirp); // 成功 0,錯誤 -1;關閉 stream 並釋放資源
opendir接收 pathname,回傳指向 directory stream 的指標。stream 是「有序項目列表」的抽象,此處是目錄項目 (directory entries) 的列表。- 每次呼叫
readdir回傳 stream 中下一個目錄項目的指標;沒有更多項目時回傳 NULL。 - 目錄項目結構(只有這兩個成員是所有系統通用的標準;部分 Linux 版本有額外成員):
struct dirent {
ino_t d_ino; /* inode 編號(檔案位置) */
char d_name[256]; /* 檔名 */
};
opendir("dir") ──► DIR *streamp ──► readdir ──► dirent{d_ino,d_name}
│ │ (重複呼叫,逐項前進)
│ └──► NULL:end-of-stream 或 error
└──► closedir(streamp) 釋放資源
readdir 出錯時回傳 NULL 並設 errno——與「讀完了」的 NULL 相同。唯一的區分方法:呼叫前先 errno = 0,迴圈結束後檢查 errno != 0 即為錯誤(Figure 10.11 readdir.c 的標準寫法)。
Exam/Test Patterns
| 情境 / 關鍵字 | 答案 |
|---|---|
rio_readn 回傳值 < n 且 ≥ 0 |
遇到 EOF 的 short count,不是錯誤;-1 才是錯誤 |
rio_writen 會不會回傳 short count? |
永不;成功必回傳 n,否則 -1 |
read 被 signal handler 中斷 |
errno == EINTR,RIO 將計數視為 0 自動重啟呼叫 |
為何逐 byte read 數行數很慢? |
每 byte 一次 trap to kernel;改用 rio_readlineb 從應用層緩衝取 byte |
rio_readlineb(rp, buf, maxlen) 最多讀幾個 bytes? |
maxlen-1 bytes(留 1 byte 給結尾 NULL);超長行被截斷 |
rio_readlineb 讀到的行包含什麼? |
含結尾 \n,再補 NULL;EOF 前有資料則回傳部分行,無資料回傳 0 |
| 哪些 RIO 函式可在同一 descriptor 交錯? | rio_readn↔rio_writen 可;rio_readlineb↔rio_readnb 可;緩衝與無緩衝讀取不可混用 |
RIO 相對 Stevens readline 的改進 |
(1) 緩衝/無緩衝讀取互相相容 (2) thread-safe(無 static buffer) |
rio_read 補緩衝時 read 回傳 short count |
不是錯誤,只是部分填滿緩衝 |
| 想知道檔案大小與類型 | stat/fstat → st_size(bytes)、st_mode |
stat vs. fstat 差異 |
stat 吃 filename,fstat 吃 file descriptor,其餘相同 |
| 判斷是否為目錄 | S_ISDIR(st.st_mode);regular file 用 S_ISREG、socket 用 S_ISSOCK |
| 檢查擁有者可讀 | st.st_mode & S_IRUSR |
struct dirent 跨系統保證的成員 |
只有 d_ino(inode)與 d_name(檔名) |
readdir 回傳 NULL 如何區分錯誤與讀完? |
呼叫前設 errno = 0,結束後 errno != 0 即錯誤 |
rio_t 內部緩衝大小 |
RIO_BUFSIZE = 8192 bytes |
rio_read 一次最多複製多少到 usrbuf? |
min(n, rp->rio_cnt) |