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_modest_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 提供兩類函式:

為何要學 RIO?

  1. 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, 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_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

使用範例:逐行複製 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 版本的差異(Aside, p.939)

RIO 源自 Stevens 網路程式設計經典中的 readline/readn/writenrio_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_modest_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 最後存取 / 修改 / 變更時間
巨集 判斷
S_ISREG(m) 是否為 regular file(一般檔案)
S_ISDIR(m) 是否為 directory(目錄)
S_ISSOCK(m) 是否為 network socket
類型用巨集、權限用位元 AND

檔案類型必須用 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 並釋放資源
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 的錯誤 vs. end-of-stream 無法從回傳值區分

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_readnrio_writen 可;rio_readlinebrio_readnb 可;緩衝與無緩衝讀取不可混用
RIO 相對 Stevens readline 的改進 (1) 緩衝/無緩衝讀取互相相容 (2) thread-safe(無 static buffer)
rio_read 補緩衝時 read 回傳 short count 不是錯誤,只是部分填滿緩衝
想知道檔案大小與類型 stat/fstatst_size(bytes)、st_mode
stat vs. fstat 差異 statfilename,fstatfile 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)