File IO
Unix File Structure
- actual file content is stored in disk
- a process must open a file before read/write
- a process may open same file mutiple time
- mutiple process may access same file at same time

Unix Support for File I/O
名詞解釋
- metadata: contain file information such as premission, where it is stored
- offset: an integer that counts the number of bytes from the
beginning of the file
- 記錄你現在讀/寫到哪裡,接下來的讀/寫就會從現在的 offset 開始
- must be positive for regular files - initialized to 0 when a file is opened
- 每次讀/寫多少,offset 就會增加多少(不會減少)
- offset 可能比檔案大小還大,下次
write()
就會擴張檔案
基本指令
開啟或建立檔案 : open(), openat()
https://man7.org/linux/man-pages/man2/open.2.html
// return lowest unopend file descriptor if success; -1 for an error
int open(const char *path, oflag, ... /* mode_tmode */ );
-
path : 要開啟或建立的檔案路徑
-
oflag : 用來指定 access modes (e.g. 讓檔案 read-only),以下三選一:
O_RDONLY
: read-onlyO_WRONLY
: write-onlyO_RDWR
: read and write
其他 optional flags (flag 之間用
|
隔開) :O_APPEND
: 從檔案最後開始 writeO_TRUNC
: 把檔案大小變成 0O_CREAT
: 如果檔案不存在就建立新的
-
mode : 建立新檔案時才能用,用來指定用戶的 permission
權限參數
// return lowest unopend file descriptor if success; -1 for an error
int openat(dirfd, const char *pathoflag, ... /* mode_tmode */)
基本上和 open 一樣,但把 fd 當成參數來看
- if path is absolute, dirfd is ignored
- if path is relative :
- if dirfd is
AT_FDCWD
, then path is 對於當前工作目錄的相對路徑 - if dirfd is referred to a given directory, then path is 對於指定目錄的相對路徑
- if dirfd is
int dirfd = open("..", O_RDONLY)
int fd = openat(dirfd, "test", O_RDWR | O_CREAT)
等同於
int fd = openat(AT_FDCWD, "../test", O_RDWR | O_CREAT)
關閉檔案 : close()
// return 0 if success; -1 for an error
int close(fd);
開啟或建立 write-only 檔案 : creat()
// return file descriptor if success; -1 for an error
int creat(const char *path, mode_t mode);
obsolete now
creat(path, mode)
等同於
open(path, O_WRONLY | O_CREAT | O_TRUNC, mode)
調整已打開檔案的 offset : lseek()
// returns new file offset if success; -1 for error
// off_t: signed int type
off_t lseek(int fd, off_t offset, int whence);
- whence : 有三種指定參數
SEEK_SET
: new offset = offset (直接從頭算)SEEK_CUR
: new offset = current offset + offsetSEEK_END
: new offset = size of the file + offset
- if fd refers to a pipe, FIFO, socket,
lseek()
return -1
讀取檔案 : read()
// return the number of bytes read if success; -1 for error; 0 if end of file (EOF)
// ssize_t: signed int type
// size_t: unsigned int type
ssize_t read(int fd, void *buf, size_t nbytes);
- read nbytes bytes data from an open file via the file descriptor fd into the memory buffer buf (buf is provided by user)
- 從 current offset 開始讀
寫入檔案 : write()
// return the number of bytes written if success; -1 for error
ssize_t write(int fd, void *buf, size_t nbytes);
- nbytes bytes data in the memory buffer buf is written to an open file via the file descriptor fd (buf is provided by user)
- 從 current offset 開始寫
- common error : 寫超過檔案大小、寫到硬碟爆炸
- If the
O_APPEND
option was specified when the file was opened, the file’s offset is set to the current end of file before each write operation
複製檔案 : dup(), dup2()
// returns the new file descriptor if successful; -1 for an error
int dup(fd);
int dup2(fd, fd2);
- new fd and original fd sharing same open table entry
- dup() : copy fd to a newly allocated file descriptor
- assign the lowest unopened fd
- dup2() : copy fd to fd2
- if fd2 is open, it will close fd2 before copying
更改檔案屬性 : fcntl()
https://man7.org/linux/man-pages/man2/fcntl.2.html https://blog.csdn.net/u012349696/article/details/50791364
// The value returned on a success call depends on cmd; -1 is returned on error
int fcntl(int fd, int cmd, ... /* int arg */ );
根據 cmd 來對 open file descriptor fd 做不同操作
舉例來說,可用於調整檔案的 status flag、設定 lock
獲取檔案的 status flag: F_GETFL
設定檔案的 status flag: F_SETFL

ioctl()
// -1 is returned on error; something else if OK
int ioctl(int fd, int request, ...);
Error Handling
// returns a pointer to the string that describes the error code passed in the argument errnum
char* sterror(int errnum)
// print a system error message based on the current error code
void perror(const char *s)
Atomic Operations
An atomic operation is an operation that will always be executed without any other process being able to read or change state that is read or changed during the operation — it is effectively executed as a single step.
當一個 process 需要的資料是整個系統共用的 (e.g. global variables, file status ..) 也就是這些資料也可以同時被其他 process 更動時,有可能會影響到 process 的執行結果

舉例來說,如果我們用 O_APPEND
來打開一個檔案,那我們就不用在每次 write() 之前都去呼叫 lseek()
Unix 也提供了一些指令,以 atomic operation 的形式來代替執行 "lseek() + read()/write()"
從指定 offset 讀寫檔案 : pread(), pwrite()
#include <unistd.h>
// return the number of bytes read if success; -1 for error; 0 if end of file (EOF)
ssize_t pread(int fd, void *buf, size_t nbytes, off_t offset);
// return the number of bytes written if success; -1 for error
ssize_t pwrite(int fd, const void *buf, size_t nbytes, off_t offset)
pread() / pwrite() 不會更改檔案的 offset !
Delayed Write
https://blog.csdn.net/lra2003/article/details/82496164
Why: 我們不想要每次 write() 都需要存取磁碟
How: write() 的時候,先將資料存在 buffer cache,需要釋出空間時再將這些資料排入輸出隊列,等到該資料排到隊首時才真正的寫入磁碟
Cons: 降低資料更新速度,系統故障時,有些資料可能還未被寫入磁碟,造成資料遺失
Solve: 為了保證 cache 和磁碟中資料的 consistency (一致性),UNIX 提供了以下函數
同步資料 : sync(), fsync(), fdatasync()
void sync(void);
// For both fsync and fdatasync, 0 is returned on a success call; -1 is returned on error
int fsync(int fd);
int fdatasync(int fd);
- sync() : 把目前 cache 裡面的所有資料排入隊列,準備寫入磁碟,queuing 完後立即 return,不會等待資料被寫入
- fsync() : 把指定檔案 fd 的 data 和 metadata 和磁碟做同步,寫入完成才 return
- fdatasync() : 把指定檔案 fd 的 data 和磁碟做同步,寫入完成才 return
fsync() and fdatasync() function similarly to open() with O_SYNC
and O_DSYNC
, respectively.
Blocking/Nonblocking I/Os
System Call
- Fast system calls
- 經過一定時間就會返回,不會被外部資源阻擋
- e.g. read file from disk
- Slow system calls
- 無限等待直到某些外部事件發生才返回
- e.g. wait for network connection
- Blocking I/O: 等到執行完成後才會返回 (slow system calls),一般的 I/O 操作預設都是 blocking
- Nonblocking I/O: 執行後立即返回
- 如果沒有執行失敗,會立即返回錯誤
- 將 fd 設定成 nonblocking :
open()
時使用 flagO_NONBLOCK
或用fcntl()
更改
I/O Multiplexing
試想下圖的例子 :

Telnet Process reads from stdin and writes to the network ; reads from the network and write to the stdout
- 如果我們使用 Blocking I/O :
- 無法同時處理多個 I/O
- 任何一個 fd 都有可能會發生 blocking,卡住整個 process,導致效率下降
- 如果我們使用 Nonblocking I/O :
- polling(輪詢) — 如果有資料就讀取並處理;如果沒有就立即返回,繼續檢查
- 因此即便沒有讀到任何東西 CPU 也會持續進行 read(),導致資源的浪費
- I/O Multiplexing: 等到 data 準備好時才進行 read(),避免 busy-waiting

Comparison of I/O Models
select()
https://man7.org/linux/man-pages/man2/select.2.html
// returns: count of ready descriptors, 0 on timeout before any descriptors is ready, −1 on error
int select(int nfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout);
-
readfds, writefds, exceptfds : 分別代表要讀取的、要寫入的、要特別處理的
以上三個變數皆為指向 fd_set data type 的 pointer 如果沒有要使用該 set 可以設成NULL
fd_set 相關指令
// 建立一個 fd_set struct fd_set test_set; // 清空(初始化) fd_set void FD_ZERO(fd_set *fdset); // 將 fd 加入 fd_set void FD_SET(int fd, fd_set *fdset); // 將 fd 移出 fd_set void FD_CLR(int fd, fd_set *fdset); // returns nonzero if fd is in set, 0 otherwise int FD_ISSET(int fd, fd_set *fdset);
-
nfds : 數值為上面三個 sets 中最大的 fd,再+1
可以設成常數FD_SETSIZE
,在 linux 裡面是 1024
select()
會檢查三個 sets 裡面所有比 nfds 小的 fd -
timeout :
- 一直等 :
NULL
- 不要等 : 0
- 等指定時間 : 使用方法如下
struct timeval wait { time_t tv_sec; /* seconds */ suseconds_t tv_usec; /* microseconds */ }; select(nfds, readfds, writefds, exceptfds, &wait)
- 一直等 :
-
select() 會返回三個 sets 裡面已經 ==ready== 的 fd 總數 (如果一個 fd 同時在多個 sets 內,次數會累加) select() 會更新三個 sets 來告訴我們那些 fd 已經 ready
- 如果 read/write to fd 不會 block,那麼這個 fd 已經 ready
- 如果 fd 的 exception condition 待處理,那麼這個 fd 已經 ready
poll()
// returns: count of ready descriptors, 0 on timeout, −1 on error
int poll(struct pollfd fdarray[], nfds_t nfds, int timeout);
-
fdarray[] : 多個 pollfd data type 組成的陣列
struct pollfd { int fd; // the file descriptor to check, or < 0 to ignore short events; // events of interest on fd short revents; // events that occurred on fd };
event flags and revents flags for poll
-
nfds : fdarray[] 的大小
-
timeout : 等待多久才繼續執行
- 一直等 : -1
- 不要等 : 0
- 等指定時間 : >0 (milliseconds)