Bỏ qua điều hướng, tới nội dung chính
Học C
Bài 41.326 phút đọc

Tầng thấp so với stdio

Sau bài này bạn sẽ làm được

  • So sánh vào ra tầng thấp với stdio trên năm tiêu chí
  • Đo ảnh hưởng kích thước bộ đệm tới hiệu năng
  • Chuyển đổi giữa fd và FILE bằng fileno và fdopen
  • Chọn đúng tầng cho từng loại vào ra

open/read/write nói thẳng với nhân. fopen/fread/fwrite là một lớp thư viện có bộ đệm nằm trên. Bài này đo chênh lệch giữa chúng, và con số giải thích vì sao bộ đệm tồn tại.

#Hai tầng vào ra

open/read/writefopen/fread/fwrite
TầngLời gọi hệ thống trực tiếpThư viện chuẩn C
Bộ đệmKhôngCó
Khả chuyểnPOSIXChuẩn C, mọi nền tảng
Kiểuint fdFILE *
Kiểm soátChi tiếtÍt hơn
Dùng chosocket, ống, thiết bị, vào ra lớntệp thường, vào ra nhỏ
Bộ đệm của stdio
Thư viện chuẩn giữ một vùng đệm trong bộ nhớ tiến trình. fwrite chép vào đệm và trả về ngay; chỉ khi đệm đầy (hoặc bạn fflush) nó mới gọi một write thật. Nhờ vậy nghìn lần fputc chỉ thành vài lời gọi hệ thống.

#Đo hiệu năng bộ đệm

perf.c, đọc 20 MB với các kích thước bộ đệm
int main(int argc, char **argv) {
    int bufsz = (argc > 1) ? atoi(argv[1]) : 4096;

    int fd = open("big.dat", O_RDONLY);
    char *buf = malloc(bufsz);

    long tong = 0, lan = 0;
    ssize_t n;
    clock_t t0 = clock();
    while ((n = read(fd, buf, bufsz)) > 0) {
        tong += n;
        ++lan;
    }
    clock_t t1 = clock();

    printf("buffer %6d byte: %ld lan read, %ld byte, %.0f ms\n",
           bufsz, lan, tong, 1000.0 * (t1 - t0) / CLOCKS_PER_SEC);

    close(fd);
    free(buf);
    return 0;
}
terminal
for sz in 1 512 4096 65536; do ./perf.exe $sz; done
buffer      1 byte: 20971520 lan read, 20971520 byte, 53547 ms
buffer    512 byte:    40960 lan read, 20971520 byte,   133 ms
buffer   4096 byte:     5120 lan read, 20971520 byte,    34 ms
buffer  65536 byte:      320 lan read, 20971520 byte,    22 ms

#Chuyển đổi fd và FILE

fdopen.c, đi lại giữa hai tầng
#include <fcntl.h>
#include <unistd.h>
#include <stdio.h>

int main(void) {
    int fd = open("fd.txt", O_WRONLY | O_CREAT | O_TRUNC, 0644);

    FILE *f = fdopen(fd, "w");       /* fd -> FILE*: boc de dung fprintf */
    fprintf(f, "ghi qua FILE* boc quanh fd\n");
    fclose(f);                        /* dong ca FILE va fd ben trong */

    FILE *r = fopen("fd.txt", "r");
    int rfd = fileno(r);             /* FILE* -> fd: lay so fd ben trong */
    printf("fileno cua FILE* = %d\n", rfd);

    char buf[64];
    fgets(buf, sizeof buf, r);
    printf("noi dung: %s", buf);
    fclose(r);
    return 0;
}
terminal
gcc -O2 -Wall -Wextra fdopen.c -o fdopen.exe && ./fdopen.exe
fileno cua FILE* = 3
noi dung: ghi qua FILE* boc quanh fd

#Chọn tầng nào

Tự làm thử

  1. Đo thời gian đọc một tệp lớn với bộ đệm 1 byte, 512 byte, 4 KB, 64 KB.
  2. Trên Linux, chạy strace -c để đếm số read ở mỗi kích thước bộ đệm.
  3. So sánh đọc bằng read với đọc bằng fread cùng logic.
  4. Bọc một fd bằng fdopen và dùng fprintf ghi vào nó.
  5. Lấy fileno của một FILE* rồi gọi fstat để đọc kích thước tệp.
  6. Trộn fwrite và write trên cùng tệp và quan sát thứ tự byte sai khi quên fflush.

Trình chấm điểm tự động sẽ được bổ sung ở giai đoạn sau. Hiện tại bạn tự chạy thử trên máy.

Tóm tắt

  • Tầng thấp nói thẳng với nhân, không đệm; stdio là lớp có bộ đệm trên nó.
  • Đọc từng byte chậm hơn nghìn lần vì mỗi syscall tốn; bộ đệm 4 KB trở lên là đủ.
  • fdopen bọc fd thành FILE*, fileno lấy fd từ FILE*; đừng trộn hai tầng trên một tệp.
  • Dùng stdio cho tệp thường, tầng thấp cho socket và ống và vào ra lớn.
  • Nhớ fflush trước fork và exec, hoặc dùng tầng thấp khi làm việc với tiến trình.