資訊人筆記

Work hard, Have fun, Make history!

使用者工具

網站工具


course:nctu-高等unix程式設計:chapter3

File I/O and Standard I/O

0x00 Outline

  • Introduction
  • File I/O Function
  • File I/O Issues
  • Standard I/O Function

0x01 Introduction

Standard Input、Output、and Error

  • 當 shell 執行一個 program 時,會產生 standard input、standard output 和 standard error
  • standard input、standard output 和 standard error可以被piped或redirected

File I/O vs Standard I/O

  • File I/O(Unbuffered I/O)
    • System call,直接和 kernel 溝通
    • 通過 file descriptors 來識別 file
    • No buffering,直接存取檔案
  • Standard I/O(Buffered I/O)
    • User-level library
    • Uses streams
    • 通過 FILE 這個資料結構的 pointer 來識別檔案
    • Buffered
    • 預設有standard input、standard output 和 standard error
    • C語言中為stdin、stdout、stderr
    • 涉及fileno function
int fileno(FILE *stream);
//此函式會檢測參數的串流,若發生 error 回傳 -1,成功則回傳一個 file descriptor

Unbuffered I/O

  • 每個 read 和 write 都會在 kernel 中涉及 system call
  • 在 user space 的 program 或 libraries 沒有 buffered
  • 通常只有 open、read、write、lseek 和 close 五個 function

File Descriptors

  • 在 kernel 中,所有被開啟的檔案都會被 file descriptors 參照
  • file descriptors 是非負整數
  • 按照 shell 和 多數程式的慣例
    • file descriptors 的 0、1、2 分別對應到 standard input、standard output 和 standard error
    • 在 unistd.h 中定義了 STDIN_FILENO (0),STDOUT_FILENO (1),STDERR_FILENO (2)
  • 一個 process 可以使用的 file descriptors 範圍是 0 ~ OPEN_MAX-1
    • OPEN_MAX 可以用 setrlimit(2) function 改變,但需要 root 權限

0x02 File I/O Function

The open(2) Function

int open(const char *pathname, int flags, mode_t mode);
  • 開啟一個檔案
  • 給一個檔案路徑為參數,回傳目前沒被其他process使用的最小 file descriptor, 有error則回傳 -1
  • 第二參數至少要包含以下其中一個
    • O_RDONLY : read-only
    • O_WRONLY : write-only
    • O_RDWR : read/write
  • 其他第二參數可選,使用 | 結合,簡單列舉
    • O_APPEND : 檔案以 append mode 開啟,每次 write 前會先透過 lseek function 將指標設置到檔案結尾
    • O_CREAT : 檔案不存在時建立,檔案的 uid 為該 process 的 euid,gid 為該 process 的 egid 或 parent directory 的 gid (depending on file system type and mount options)
    • O_TRUNC : 如果檔案已經存在,是 regular file 且可寫入,則檔案會被 truncated 為長度 0
  • flags 使用 O_CREAT | O_EXCL,可以達到 automic creating of a non-exist file 的效果

在 NFS 中當有兩個以上 process 同時 append data 可能會損壞檔案,因為 NFS 本身沒有支援 append 方式,必須靠 client kernel 模擬,而無法應付 race condition

The creat(2) Function

int creat(const char *pathname, mode_t mode);
  • 等於是 open(const char *pathname, O_CREAT|O_WRONLY|O_TRUNC, mode_t mode)

The close(2) Function

int close(int fd);
  • 關閉 file descriptor
  • 回傳 0 表示成功, -1 表示 error
  • 當一個 process 終止後,所有它開啟的檔案會自動被關閉

The lseek(2) Function

off_t lseek(int fd, off_t offset, int whence);
  • 重新定位 file descriptior offset
  • 回傳新的 file offset,error 則回傳 -1
  • 通常 offset 為 32-bit long,另外有 lseek64() using off64_t
  • 第二參數 offset 單位為 character,若給 0 可以用來檢測檔案是否 seekable
  • 第三參數 whence包含
    • SEEK_SET : 指標設在檔案開頭為基準的 offset char
    • SEEK_CUR : 指標設在目前位置為基準的 offset char
    • SEEK_END : 指標設在檔案結尾為基準的 offset char,append mode 開啟 file 即為此方式
  • File hole(待補)

The read(2) Function

ssize_t read(int fd, void *buf, size_t count);
  • 從 file descriptor 讀取 count bytes 進 buffer
  • 回傳讀取的 byte 數,如果 EOF(End of File,已讀完) 則是 0,error 為 1
  • 每次讀取後 file offset 會往後移

The write(2) Function

ssize_t write(int fd, const void *buf, size_t count);
  • 從 buffer 讀取 count bytes 寫進 file descriptor 指向的檔案
  • 回傳寫入的 byte 數,error 為 1
  • 每次寫入後 file offset 會往後移

0x03 File I/O Issues

I/O Efficiency

  • 基本上 buffer 越大,I/O 所花時間越短

File Sharing

  • 同個檔案可以被多個 process 同時開啟
  • kernal 會為開啟的檔案維護數份 data structure
    • 每個 process 會在 process table 有一項條目
    • 每項 process table 的條目又包含一張 table ,記錄已開啟的 file descriptors
    • file descriptors table 中的每筆紀錄會有 file pointer 指向一張 file table,代表著一個被開啟的檔案
    • 每個 file table 會關聯到一個 v-node 的資料結構

Kernal Data Structure

Open The Same File

Atomic Operation

  • 如果有兩個執行緒都 lseek 到檔案尾端並執行 write 則會發生 error,任何需要兩次以上 system call 的操作都不能稱作 Atomic Operation

pread(2) And pwrite(2)

ssize_t pread(int fd, void *buf, size_t count, off_t offset);
ssize_t pwrite(int fd, const void *buf, size_t count, off_t offset);
  • 將 seek 和 read、write 結合成 atomic operation,兩件事無法被打斷分離
  • 有自己的 offset,在 read、write 時 File offset 不會被改變

dup(2) And dup2(2)

int dup(int oldfd);
int dup2(int oldfd, int newfd);
  • 複製出一個新的 file descriptor
  • 回傳複製出的 fd,有 error 則回傳 -1
  • dup 使用未被使用的最小號碼為新的 file descriptor
  • dup2 把舊的 fd 複製到新的 fd
    • 如果有需要會先關閉新的 fd
    • 如果 oldfd 是無效的,這個 system call 會 fail,且 newfd 不會被關閉
    • 如果 oldfd 是有效的,且 oldfd 值和 newfd 相同,則 dup2 會直接回傳 newfd
  • dup(fd); 等價於 fnctl(fd, F_DUPED, 0);
  • dup2(fd, fd2); 等價於 fnctl(fd, F_DUPED, fd2);
    • dup2 是 atomic operation

sync(2), fsync(2) And fdatasync(2)

void sync(void);
int fsync(int fd);
int fdatasync(int fd);
  • return 0, error 則為 -1
  • 一般 write 時,更新的只是記憶體和快取,並不會立即寫到硬碟中
  • 因為 write 不會等到資料寫入硬碟後才 return,所以若在 write 後,寫入硬碟之前停電或壞軌,可能導致資料遺失,儘管時間差很短
  • 使用 sync 等方法將資料寫入硬碟中,這些方法都會等到資料確實寫入後才 return
  • 要寫入的部分除了 filedata 外,還包含 metadata (檔案大小、最後存取時間等屬性)
  • fsync 會將 filedata 和 metadata 一起寫入硬碟,要兩次 I/O
  • fdatasync 一般只會將 filedata 寫入,僅僅在有必要時才會寫入 metadata,以減少 I/O 時間成本
    • 有必要的定義為會影響檔案資料正常運作的,例如寫入資料後檔案大小(st_size)改變,若 metadata 未同步則可能錯估邊界而讀不到內容,而像最後存取時間(atime)/修改時間(mtime)
  • sync for all files, fsync and fdatasync for specific file

fnctl(2)

int fcntl(int fd, int cmd, ... /* arg */ );
  • 操作 file descriptor,改變開啟檔案的屬性
  • 常見 command 包含
    • F_DUPFD : duplicate the file descriptor
    • F_GETFD/F_SETFD : get or set the file descriptor flag
    • F_GETFL/F_SETFL : get or set the file status flags,像是 O_RDONLY、O_WRONLY、O_RDWR、O_APPEND
  • |= Enable flag
  • &= Disable flag
#include "apue.h"
#include <fcntl.h>
void set_fl(int fd, int flags)
/* flags are file status flags to turn on */
{
  int val;
 
  if ((val = fcntl(fd, F_GETFL, 0)) < 0)
    err_sys("fcntl F_GETFL error");
 
  val |= flags; /* turn on flags */
 
  if (fcntl(fd, F_SETFL, val) < 0)
    err_sys("fcntl F_SETFL error");
}

ioctl(2)

  int ioctl(int d, int request, ...);

/dev/fd

  • 較現代的系統提供了 /dev/fd 資料夾
  • 這是一個 virtual file system
  • 假設 fd n 已經被開啟,則開啟 /dev/fd/n 等同於複製 fd n
    • fd = open(“/dev/fd/0”, mode);
    • fd = dup(0);
  • 主要用途在 shell,使程式可以像處理 regular file 一樣處理 standard in/out

0x04 Standard I/O Function

Standard I/O

  • 處理 buffer 分配
  • Perform I/O in optimal-sized chunks
  • 較容易使用
  • FILE Structure
    • 將開啟的檔案都視為串流
    • 串流和 file descriptors 相關
    • 維護 buffer states

Buffering

  • Fully buffered
    • Characters written to or read from a fully buffered stream are transmitted to or from the file in blocks of arbitrary size.
    • 一般 disk 中的檔案都是藉由 standard I/O library 做 fully buffered
    • 開始串流時通常使用 malloc function 來得到可使用的 buffer
    • 真正 I/O 的時機
      • Reading : buffer 是空的,需要填入時
      • Writing : buffer 是滿的,需要被 flushing,當 buffer 滿時會自動 flush,或是手動呼叫 fflush() 進行
  • Line buffered
    • Characters written to a line buffered stream are transmitted to the file in blocks when a newline character is encountered.
    • 一般使用在 terminal device
    • 真正 I/O 的時機在 standard I/O library 遭遇換行字元時
    • 因為 buffer 大小固定,有可能在還沒遭遇換行字元時就已經滿了,此時也會進行 I/O
    • 當有 input 來自 line buffered 或 unbuffered 時,line buffered output stream 都會先被 flush
  • Unbuffered
    • Characters written to or read from an unbuffered stream are transmitted individually to or from the file as soon as possible.
    • stdand error stream 使用

Newly opened streams are normally fully buffered, with one exception: a stream connected to an interactive device such as a terminal is initially line buffered.

int fflush(FILE *stream);

Function for Setting Buffer (3)

void setbuf(FILE *stream, char *buf);
int setvbuf(FILE *stream, char *buf, int mode, size_t size);
  • return 0 if OK,nonzero on error
  • Buffering mode
    • _IOFBF: fully buffered
    • _IOLBF: line buffered
    • _IONBF: unbuffered

Function for Open and Close Files (3)

FILE *fopen(const char *path, const char *mode);
FILE *fdopen(int fd, const char *mode);
FILE *freopen(const char *path, const char *mode, FILE *stream);
int fclose(FILE *fp);
  • return file pointer if OK,NULL on error
  • mode
    • r or rb: open for reading
    • w or wb: truncate to 0 length or create for writing
    • a or ab: append, open for writing at EOF or create for writing
    • r+, r+b, or rb+: open for reading and writing
    • w+, w+b, or wb+: equivalent to w or wb plus reading
    • a+, a+b, or ab+: equivalent to a or ab plus reading

Function for Read and Write By Character (3)

//read character
int getc(FILE *stream);
int getchar(void);
  • return next char if OK, EOF on EOF or error
//Test EOF or Error
int feof(FILE *stream);
int ferror(FILE *stream);
  • return nonzero(true) if condition is true ,or zero(false)
//write character
int putc(int c, FILE *stream);
int putchar(int c);
  • return c if OK,EOF on error

Function for Read and Write By Line

//read line
char *fgets(char *s, int size, FILE *stream);
char *gets(char *s);
  • reutrn buf if OK,NULL on EOF or error
//write line
int fputs(const char *s, FILE *stream);
int puts(const char *s);
  • return non-negative value if OK,EOF on error

Binary I/O

size_t fread(void *ptr, size_t size, size_t nmemb, FILE *stream );
size_t fwrite(const void *ptr, size_t size, size_t nmemb, FILE *stream);
  • return number of object read or write

Positioning a Stream

int fseek(FILE *stream, long offset, int whence);
long ftell(FILE *stream);
void rewind(FILE *stream);
  • fseek 用法類似 lseek
  • ftell 會得到目前 file position indicator 指向 stream 中位置的值
  • rewind 會將 stream file position indicator 設到檔案開頭,效用等同於 (void) fseek(stream, 0L, SEEK_SET)

Temporary Files

char *tmpnam(char *str);
FILE *tmpfile(void);
  • 用於建立臨時文件
  • tmpnam 回傳一個指標指向獨特的路徑名稱,NULL on error
  • tmpfile 回傳 file pointer if OK,NULL on error
  • 不建議使用 tmpnam
    • 若參數 str 為 NULL,回傳的 char 會保留在內部 buffer,下次再呼叫 tmpnam 時會被覆蓋
    • tmpnam 回傳的文件路徑名稱一般為 /tmp/fileXXXXXX,不是 FILE pointer,在開檔讀寫還需另外處理

0x05 參考資料

course/nctu-高等unix程式設計/chapter3.txt · 上一次變更: 127.0.0.1