輸入/輸出(I/O)是指在記憶體與外部設備之間複製數據的過程。
所有程式語言的 Run-time System 都提供 高階的 I/O 介面 來簡化 I/O 操作。例如:
在 Unix 系統中,檔案(File) 是由 m 個位元組(bytes) 組成的序列,
Unix 將所有 I/O 設備都視為檔案,所有的輸入與輸出都是透過 Read 與 Write 檔案來完成的。
這種將設備映射為檔案的設計,讓稱為 Unix I/O。
開啟檔案(Opening Files)
當需要存取 I/O 設備時,必須請求 kernel 開啟對應的檔案。核心會回傳一個非負整數,稱為 File Descriptor,用來識別該檔案,後續的操作都透過此描述符來進行。
kernel 會維護所有開啟檔案的資訊,而應用程式只需管理檔案描述符。
在 Unix Shell 中,每個新建立的 Process 都會有三個預設開啟的檔案:
標頭檔 <unistd.h> 定義了對應的常數:
檔案位置是 kernel 為每個開啟的檔案維護的一個指標 k,預設為 0,代表檔案的開頭。
應用程式可以透過 seek 操作來變更當前的檔案位置。
讀取
寫入
在 UNIX 系統中,process 可以透過 open() 系統呼叫來開啟現有的文件或建立新的文件。
在 UNIX 中,open() 函式會將檔名轉換為文件描述符,並回傳該描述符數字。
回傳的文件描述符一定是目前尚未被使用的最小數字。
int fd = open(const char *pathname, int flags, mode_t mode);
pathname
要開啟的檔案名稱(包含路徑)
flags
指定開啟檔案的模式(讀取、寫入、創建等)
mode(可選)
如果使用 O_CREAT,則需要設定新建文件的權限(如 0644)
open() 回傳值
umask:限制新檔案的預設權限
每個 Process 都有一個 umask 設定,它決定「新建檔案時」哪些權限會被取消。
在 Unix 系統中,應用程式透過 read 和 write 這兩個函式來執行輸入(讀取)與輸出(寫入)操作。
從檔案描述符當前位置讀取最多 n 個位元組到緩衝區 buf。
回傳值
將緩衝區 buf 內最多 n 個位元組寫入檔案描述符的當前位置。
回傳值
應用程式可以透過 lseek 函數來明確修改目前的檔案位置,但這部分超出了範圍。
ssize_t 和 size_t 有什麼不同?
你可能注意到 read 函數的輸入參數是 size_t,而返回值則是 ssize_t。那麼這兩種型別有什麼區別呢?
size_t:無符號整數(unsigned int),用來表示大小或計數。
ssize_t:有符號整數(signed int),用來表示可能會返回錯誤的大小。
read 函數使用 ssize_t 作為返回值,因為當發生錯誤時,它必須返回 -1。有趣的是,因為 ssize_t 需要保留一個負值(-1)來表示錯誤,這導致 read 最大可讀取的數據量減少了一半,從 4GB 降到 2GB。
(因為 read 假如成功是要回傳實際讀取的位元組數量)
read 或 write 可能傳輸的字節數比請求的少(叫短讀 short read 或短寫short write)
注:如果開發可靠的網路應用,應該用迴圈來確保所有數據都被完整讀取或寫入。
本節介紹 RIO(Robust I/O) 的 I/O 套件,它能夠自動處理短讀(short count)的問題。RIO 套件專為處理可能發生短讀的應用場景(例如網路程式)而設計。
RIO 提供了兩種類型的函式:
應用程式可以透過調用 rio_readn 和 rio_writen 函式,在記憶體與檔案之間直接傳輸數據。
ssize_t rio_readn(int fd, void *usrbuf, size_t n);
ssize_t rio_writen(int fd, void *usrbuf, size_t n);
rio_readn 和 rio_writen 的功能:
rio_readn:從檔案描述符 (fd) 讀取最多 n 個位元組到記憶體 (usrbuf)。
rio_writen:將 n 個位元組從記憶體 (usrbuf) 寫入檔案描述符 (fd)。
短讀與短寫的行為:
rio_readn 可能發生短讀(short read),但只會發生在遇到 EOF 時。
rio_writen 不會發生短寫(short write),它會確保所有資料都被寫入。
可交錯執行:
rio_readn 和 rio_writen 可以在同一個文件描述符上任意交錯執行,不會互相影響。
中斷處理與可攜性:
這些函式會自動重新啟動 read 和 write,如果它們因應用程式的訊號處理(signal handler)被中斷。
這種設計確保函式在不同的 Unix 系統上具有良好的可攜性(portability)。
這段的重點是透過封裝函式(如 rio_readlineb)來提升讀取文字行的效率,避免使用 read() 每次讀取 1 個字元導致效能低落。此外,對於包含文字行和二進位數據還提供了 rio_readn 的有緩衝版本,稱為 rio_readnb,它提供了一種適合處理混合文本與二進位數據(如 HTTP 回應)的高效讀取方式,並透過緩衝區機制減少系統呼叫次數。
下列為他所定義的結構:
#define RIO_BUFSIZE 8192
typedef struct {
int rio_fd;
int rio_cnt;
char *rio_bufptr; /* next unread byte in internal buf */
char rio_buf[RIO_BUFSIZE]; /* internal buffer */
} rio_t;
rio_fd : 為該檔案的文件表示符
rio_cnt : 在internal buf中還有幾個尚未讀取的位元
rio_bufptr: 指向 internal buf 中還未讀取的位元起始位置
rio_buf[RIO_BUFSIZE]: internal buffer
rio_readinitb
rio_readinitb 函式會在每個開啟的描述符上呼叫一次。它將描述符 fd 與位於 rp 地址的 rio_t 型態的讀取緩衝區關聯起來。
void rio_readinitb(rio_t *rp, int fd)
{
rp->rio_fd = fd;
rp->rio_cnt = 0;
rp->rio_bufptr = rp->rio_buf;
}
在介紹 rio_readlineb 和 rio_readnb 之前需要先了解到 rio_read 函數是作什麼用的,因為 rio_readlineb 和 rio_readnb 皆為封裝函數其中就包涵 rio_read 這個函數。
簡單來說 rio_read 這個函數會去把你所指定的文件描述符也就是( rio_fd )的資料去讀到 rio_buf 中,只是這個程式可能會有上述所說的短讀(short read)的問題,因此我們需要使用封裝函數 rio_readlineb 和 rio_readnb 來去解決。
rio_readlineb
ssize t rio readlineb(rio t *rp, void *usrbuf, size_t maxlen);
rio_readlineb 函式的話會從 rp 指向的檔案中讀取下一行文字(包含結尾的換行字元 \n),將其複製到記憶體位置 usrbuf,並在該行的最後加上空字元(null 字元,值為 0)。
rio_readlineb 函式最多會讀取 maxlen-1 個 bytes,並預留 1 個位元組來存放字串結尾的 null (\0) 字元。
如果讀取的文字行超過 maxlen-1 個位元組,則會截斷並在最後加上 null 字元。
特點確保讀取整行:不會發生部分行被截斷的問題(除非超過 maxlen)
ssize t rio readnb(rio t *rp, void *usrbuf, size_t n);
rio_readnb 函式則是最多讀取 n 個位元組,將其從文件 rp 複製到記憶體位置 usrbuf 且保證完整讀取只要沒有錯誤,就會嘗試讀取完整的 n 個字節。
rio_readlineb 和 rio_readnb 這兩個函式可以交錯使用(interleave)在同一個文件描述符(descriptor)上,但不能與無緩衝(unbuffered) 的 rio_readn 交錯使用。