VinSong's Blog

Back

Buffered I/O#

Buffer I/O 是一種 abstraction 的概念,主要是為了減少 system call 的次數,本質上是一種 API 而非是 System interface,Buffer I/O 會在 user space allocate 一塊 buffer,然後把 buffer 裡面的東西透過 Unbuffer I/O 送出去

Standard Library of C#

Standard Library of C 就是一種提供 Buffered I/O 的 abstraction

  • Call fopen() 會得到一個 file object pointer 會放在 Virtual Memory

File object 會有一個 buffer,裡面還會記錄一些 File 相關的資訊,像是

  • File descriptor
  • Pointer to buffer
  • Buffer size
  • # of remaining chars, …etc.
File object in Standard Library of C
File object in Standard Library of C

Buffer I/O 最後依然會透過 Unbuffered I/O 出去,只是他可以做更多的控制操作(e.g. 累積後一次呼叫 system call)

  • Command line 裡面打 strace 可以 trace 所有 Buffer I/O
Unbuffered I/O
Unbuffered I/O
Buffered I/O
Buffered I/O

直接碰到 system call 是 high cost (time) 的,所以 C library 會先 allocate 一塊 buffer 去防止直接碰到 kernel

而這樣做的結果除了可以有效加快 I/O Efficiency,還有 Prefetch 的效果

只需要一個 write() 減少 system call
只需要一個 write() 減少 system call

fopen()#

FILE *fopen(const char *pathname, const char *type);
c

這個 funciton call 可以支援 atomic 的 open,會同時做 open + read/write 的動作,並且會回傳一個 FILE *,這個 FILE * 會放在 Virtual Memory 裡面,裡面會有 buffer、file descriptor、buffer size、remaining chars 等等資訊

以下是各種 flag 的代表意義

Restrictionrwar+w+a+
file must already exist
previous contents of file discarded
stream can be read
stream can be written
stream can be written only at end

freopen()#

FILE *freopen(const char *pathname, const char *type, FILE *fp);
c

這個 function 可以把某個已經 open 的 FILE * 重新指定到另一個 file,並且可以指定新的 type (通常是常見 e.g. stdout),下面這段 code 會把掛在 stdout 底下的 file 重新指定到 /tmp/logfile,並且可以同時做 append 的動作

freopen.c
FILE *fp;
fp = freopen ("/tmp/logfile", "a+", stdout);
printf(“Sent to stdout and redirected to /tmp/logfile”);
fclose(stdout);
c

fdopen()#

FILE *fdopen(int fildes, const char *type);
c

做 unbuffer I/O 後想重新指定型別,也可以把開好的 fd 來指定型別

  • 開啟的 type 一定要是前一個 open 的 type 的 subset,就是 permission 的 subset,e.g. r+ 可以指定成 r 但是不能指定成 w
  • 如果用這個 function call 用 w type 開啟檔案的話不會先行 truncate

fileno()#

int fileno(FILE *fp);
c

可以拿到 FILE object 的 file descriptor (fd)

fclose()#

int fclose(FILE *fp);
c

要做這個 function call 指定的 fstream 裡面的 buffer 必須有效,不然會失效,這個 function 的流程是

  1. Discard input buffer
  2. Flush output buffer
  3. Close file descriptor
  4. Deallocate file object
open_data.c
FILE *open_data(void)
{
    FILE *fp;
    char databuf[BUFSIZ];  /* setvbuf makes this the stdio buffer */

    if ((fp = fopen(DATAFILE, "r")) == NULL)
        return (NULL);
    if (setvbuf(fp, databuf, _IOLBF, BUFSIZ) != 0)
        return (NULL);

    return (fp);
}
c

databuf 是一個 local variable 在這個 function 結束後 call fclose(fp) 會失效

Buffering#

Buffer I/O 最主要目的是要減少 system call 的次數,而 buffer 機制就是可以達到這個目的方法。直接 call standard I/O 會自動 allocate buffer,但是也可以自行用 setbuf(), setvbuf() 設定

雖然說是 buffered I/O 但最終還是會往 Unbuffer I/O 送,由 Unbuffer I/O 做真正的 I/O

以下為常見的三種 buffer type

Fully Buffered#

Fully buffered 會等到 buffer 滿了才會送出去,通常是用在 diskpipesocket 等等,這些都是需要等到 buffer 滿了才會送出去的

Line Buffered#

每個 new line 就會送一次像是 terminal 就是一種 line buffered 環境,通常兩種情況要特殊處理

  • Full: buffer 滿了就一定得送出去
  • Flush: 系統要求一定要馬上 flush

下面這個 code,新一行雖然沒看到 \n 但 terminal 必須先輸出 $ 就是一種典型的 flush

char buf[100];
printf(“$ “); 
scanf(“%s”, buf); 
c

Unbuffered#

需要有錯誤就馬上輸出不能拖著 (e.g., stderr)

fflush()#

int fflush(FILE *fp);`
c
  • fp 裡面的 buffer 內容透過 Unbuffer I/O 送出去
  • 如果設定 fp == NULL 就會把所有 output buffer 送出去
  • 送是送到 buffer cache 而不是直接進去 DISK 還要呼叫 sysc(), fsysc(), fdatasysc()

setbuf()#

void setbuf(FILE *fp, char *buf);
c

對一個 file object 指定要使用某段 memory 作為 buffer,但缺點是不能指定 buffer size(固定是 BUFSIZ)跟 buffer type

  • setbuf(fp, NULL); 就是把 buffer 關掉

setvbuf()#

int setvbuf(FILE *fp, char *buf, int mode, size_t size);
c

可以指定 buffer type, buffer size,也可以選擇是否讓 LibC allocate buffer,有三種 mode 可以選擇 - _IOFBF: Fully buffered

  • _IOLBF: Line buffered
  • _IONBF: Unbuffered

這類型的 setbuf() 操作應該出現在 fopen() 之後,任何對 fp 的 I/O 操作之前,否則會失效

ftell()#

long ftell(FILE *fp);
c

找尋目前 offset

fseek()#

int fseek(FILE *fp, long offset, int whence);
c

在 file object 裡面指定 offset 的功能,跟 lseek() 很像但是有一個差別就是 fseek() 會先把 buffer flush 掉,然後再去做 offset 的動作

  • whence 可以是 SEEK_SET, SEEK_CUR, SEEK_END
  • offset 可以是正數或負數
  • Binary file SEEK_END 無用因為 buffer 性質會 append 一堆奇怪東西
  • Text file SEEK_SET 就是 0 或著 ftell() 的 return value

Unformatted I/O#

Formatted I/O 是指有格式的輸入輸出,像是 printf()scanf(),裡面的 % 就是格式化的符號,Unformatted I/O 就是沒有格式化的輸入輸出,像是 read()write()fread()fwrite()

  • Character-at-a-time I/O, e.g., getc()
    • Buffering handled by standard I/O lib
  • Line-at-a-time I/O, e.g., fgets()
    • Buffer limit might need to be specified.
  • Direct I/O, e.g., fread()
    • Read/write a number of objects of a specified size

Character-at-a-time I/O#

getchar(), getc(), fgetc()#

int getc(FILE *fp);
int getchar(void);
int fgetc(FILE *fp);
c

這些都是一次接一個 character 的 I/O,getc() 會有可能是 macro,getchar() 一定是 function,所以 getc() 會比 getchar() 快一點,EOF 或是 error 都會回傳 -1

getchar.c
int main() {
    char c;
    while ((c = getchar()) != EOF) 
        putchar(c);
}
c

因為 charunsigned 所以永遠不會停永遠不會是 -1,而如果用 signed char,就會常常有 -1 出現而不一定是在 EOF,所以 c 必須要是 int

putchar(), putc(), fputc()#

int putc(int c, FILE *fp);
int putchar(int c);
int fputc(int c, FILE *fp);
c

這些都是一次輸出一個的 I/O,putc() 會有可能是 macro,putchar() 一定是 function

fgets()#

char *fgets(char *buf, int size, FILE *fp);
c

一次讀一行,會把 \n 也讀進去,直到 buffer 滿了或是遇到 \n 或是 EOF 才會停下來

gets()#

char *gets(char *buf);
c

一次讀一行,會把 \n 也讀進去,但沒有指定 buffer size,會 overflow.

fputs(), puts()#

int fputs(const char *s, FILE *fp);
int puts(const char *s);
c

puts() 會自動在字串後面加上 \n,並且是一直寫到 stdout,而 fputs() 不會,而這兩個 function 都不會有 overflow 的問題,因為只要輸出空間夠大就可以了

fread(), fwrite()#

size_t fread(void *ptr, size_t size, size_t nobj, FILE *fp);
size_t fwrite(const void *ptr, size_t size, size_t nobj, FILE *fp);
c

是一種 Binary I/O 可以以 Object 為單位寫入讀取

fwrite.c
struct {
    short count;
    long total;
    char name[NAME_SIZE];
} item;
if (fwrite(&item, sizeof(item), fp) != 1)
    err_sys("fwrite error");
c

這段程式就是把 item 這個 struct 寫入到 fp 指定的檔案裡面,並且以 sizeof(item) 為單位寫入,寫入的數量是 1 個 object,如果寫入失敗就會回傳錯誤訊息

I/O Functions in Standard C Library#

以下這些都是 Standard C Library 提供的 Formatted I/O functions

Input Functions:#

  • int scanf(const char *format, …);
  • int fscanf(FILE *fp, const char *format, …);
  • int sscanf(char *buf, const char *format, …);

Output Functions:#

  • int printf(const char *format, …);
  • int fprintf(FILE *fp, const char *format, …);
  • int sprintf(char *buf, const char *format, …); Overflow is possible for sprintf() \0 appended at the end of the string. A better substitute: snprintf()
  • int vprintf(const char *format, va_list arg);
  • int vfprintf(FILE *fp, const char *format, va_list arg);
  • int vsprintf(char *buf, const char *format, va_list arg);

Standard I/O Performance#

FunctionUser CPU (s)System CPU (s)Clock Time (s)Bytes of Program Text
Best time using read/write0.010.186.67
fgets, fputs2.590.197.15139
getc, putc10.840.2712.07120
fgetc, fputc10.440.2711.42120
Single byte using read/write124.89161.65288.64

真正用 System CPU 的時間,Best user CPU time 跟其他 Standard Library 的其實一樣,因此 Standard library 是在節省 System CPU time

fwide()#

int fwide(FILE *fp, int mode)
c

Standard I/O in C lib 會有 orientation

  • single byte character
  • multiple byte character 比如某些 emoji 就是 multi-byte character,這個 function 可以指定 orientation,但是 orientation 一但被 set 就不能 reset

Back to the content

NTU PJ System Programming

2025 Fall

← Back to the content


NTU-SP 系統程式設計 Ch4 Buffered I/O
https://vinsong.csie.org/notes/sp/ch04-bufio.html
Author VinSong
Published at 2025年11月30日
回到 NTU-SP 系統程式設計 目錄