The Linux Programming Interface Chương 13 · File I/O Buffering

Chương 13 · Đệm vào/ra tệp Chapter 13 · File I/O Buffering

Dữ liệu không đi thẳng ra đĩa — nó nằm lại trong hai tầng đệm Vì sao ghi tệp lại nhanh, và dữ liệu thật ra đang nằm ở đâu Data doesn't go straight to disk — it rests in two layers of buffers

Vì tốc độ và hiệu quả, các lời gọi hệ thống vào/ra (tức là nhân) và thư viện C chuẩn (các hàm stdio) đều đệm (buffer) dữ liệu khi thao tác trên tệp đĩa.1 Chương này mô tả cả hai tầng đệm và xét xem chúng ảnh hưởng ra sao tới hiệu năng ứng dụng; đồng thời trình bày các kỹ thuật để tác động và tắt bớt từng tầng, cùng một kỹ thuật tên direct I/O — bỏ qua buffer cache của nhân trong một số tình huống.1

Khi bạn ghi vào tệp, dữ liệu không chạy thẳng xuống đĩa ngay. Nó dừng lại ở các “kho tạm” (gọi là buffer — vùng đệm): một kho của nhân hệ điều hành, và một kho nữa của thư viện C. Nhờ gom lại rồi ghi một thể nên máy chạy nhanh hơn nhiều. Chương này chỉ cho bạn hai kho đó nằm đâu và cách điều khiển chúng. Nhớ nhé: dữ liệu ghi ra tệp nằm tạm trong vùng đệm trước, chưa xuống đĩa ngay.

In the interests of speed and efficiency, the I/O system calls (i.e., the kernel) and the standard C library's I/O functions (the stdio functions) buffer data when operating on disk files.1 This chapter describes both types of buffering and considers how they affect application performance; it also presents techniques for influencing and disabling each type, and a technique called direct I/O that bypasses the kernel buffer cache in certain circumstances.1

Hệ quả then chốt mà Kerrisk nhấn mạnh ngay từ đầu: khi write() trả về, dữ liệu chưa chắc đã ở trên đĩa — ta nói lời gọi hệ thống “không đồng bộ” (not synchronized) với thao tác đĩa; nhân sẽ ghi (flush) vùng đệm của nó xuống đĩa vào một lúc nào đó sau.1 Hai sợi chỉ chạy suốt chương là hiệu năng (đệm theo khối lớn để gọi hệ thống ít đi) và toàn vẹn dữ liệu (buộc flush để dữ liệu thật sự an toàn trước khi tiếp tục). Mọi công cụ trong chương — fflush(), fsync(), fdatasync(), sync(), O_SYNC, posix_fadvise(), O_DIRECT — đều đứng ở một trong hai sợi chỉ đó.1

Luận đề của chương: vào/ra tệp đi qua hai tầng đệm độc lập — vùng đệm stdio trong không gian người dùng và buffer cache của nhân — được dựng lên để đổi lấy tốc độ (gom nhiều thao tác nhỏ thành ít lời gọi hệ thống và ít lần truy cập đĩa); muốn vừa nhanh vừa an toàn, lập trình viên phải hiểu và chủ động điều khiển cả hai tầng.1

Ý chính: dữ liệu vào/ra tệp đi qua hai kho tạm để máy chạy nhanh hơn; nhưng nhanh thì có rủi ro mất dữ liệu nếu máy tắt đột ngột, nên ta cần biết cách “đẩy” dữ liệu xuống đĩa khi cần. Nhớ nhé: đệm làm máy nhanh; muốn chắc dữ liệu đã xuống đĩa thì phải chủ động ép nó đi.

The chapter's thesis: file I/O passes through two independent buffering layers — the stdio buffer in user space and the kernel's buffer cache — built to buy speed (batching many small operations into few system calls and few disk accesses); to be both fast and safe, the programmer must understand and deliberately control both layers.1

Cụm khái niệm 01 Concept cluster 01

Buffer cache của nhân — vì sao read() và write() lại nhanh Kho tạm của nhân — vì sao đọc ghi tệp nhanh thế The kernel buffer cache — why read() and write() are fast

Khi làm việc với tệp đĩa, các lời gọi read() và write() không trực tiếp khởi động thao tác đĩa. Thay vào đó, chúng chỉ chép dữ liệu giữa một vùng đệm trong không gian người dùng và một vùng đệm trong buffer cache của nhân.1 Ví dụ, lời gọi write(fd, "abc", 3) chép 3 byte từ bộ nhớ người dùng vào buffer cache rồi write() trả về ngay; mãi sau nhân mới ghi (flush) vùng đệm ấy xuống đĩa.1 Vì thế ta nói lời gọi hệ thống “không đồng bộ” với thao tác đĩa. Chiều đọc cũng vậy: nhân đọc dữ liệu từ đĩa vào vùng đệm, các lời read() lấy từ đó cho tới khi cạn.1

Khi bạn ghi vào tệp, dữ liệu không rơi thẳng xuống đĩa. Nhân hệ điều hành chép nó vào một “kho tạm” trong bộ nhớ (buffer cache) rồi báo “xong” ngay, còn việc ghi đĩa thật thì làm sau. Khi bạn đọc tệp, nhân cũng lấy sẵn một mảng vào kho tạm để lần sau đọc cho nhanh. Đĩa chậm, bộ nhớ nhanh — nên gom vào bộ nhớ trước là mẹo để đọc ghi mượt. Nhớ nhé: nhân giữ dữ liệu trong bộ nhớ trước, đĩa chậm nên để sau.

When working with disk files, the read() and write() system calls don't directly initiate disk access. Instead, they simply copy data between a user-space buffer and a buffer in the kernel buffer cache.1 For example, the call write(fd, "abc", 3) copies 3 bytes from user memory into the buffer cache, and write() returns at once; only later does the kernel flush that buffer to the disk.1 We therefore say the system call is not synchronized with the disk operation. Reading is symmetrical: the kernel reads data from disk into a buffer, and read() calls fetch from it until it is exhausted.1

Định nghĩa. Buffer cache là vùng bộ nhớ của nhân dùng chứa tạm dữ liệu các khối đĩa đã đọc hoặc chờ ghi. Mục đích của thiết kế này là để read() và write() chạy nhanh (không phải chờ thao tác đĩa chậm) và hiệu quả (giảm số lần truyền đĩa nhân phải thực hiện).1 Nhân Linux không đặt giới hạn trên cố định cho kích thước buffer cache: nó cấp phát bao nhiêu trang tùy nhu cầu, chỉ bị chặn bởi lượng bộ nhớ vật lý còn trống; khi bộ nhớ khan hiếm, nhân flush bớt trang đã sửa đổi xuống đĩa để lấy lại chỗ.1

Nói đơn giản. Buffer cache là vùng bộ nhớ mà nhân dùng làm kho chứa tạm các mẩu dữ liệu của đĩa. Nhờ kho này mà lệnh đọc/ghi trả lời nhanh, và nhân gộp được nhiều lần ghi đĩa thành ít lần hơn. Nhớ nhé: buffer cache là kho tạm giúp đọc ghi nhanh và ghi đĩa ít lần hơn.

Definition. The buffer cache is kernel memory holding copies of disk blocks that have been read or are waiting to be written. The aim of this design is to make read() and write() fast (no waiting on a slow disk operation) and efficient (fewer disk transfers for the kernel to perform).1 The Linux kernel imposes no fixed upper limit on the size of the buffer cache: it allocates as many pages as required, limited only by available physical memory; when memory is scarce, it flushes some modified pages to disk to free them.1

Sao chép tệp 100 triệu byte (ext2):
BUF_SIZE 1 → 107.43 s
BUF_SIZE 1024 → 2.05 s
BUF_SIZE 4096 → 2.05 s (gần tối ưu)
BUF_SIZE 65536 → 2.06 s

Chép tệp 100 triệu byte:
mỗi lần 1 byte → 107 giây
mỗi lần 4096 byte → 2 giây
→ gom khối lớn nhanh hơn ~50 lần

Copy a 100-million-byte file (ext2):
BUF_SIZE 1 → 107.43 s
BUF_SIZE 1024 → 2.05 s
BUF_SIZE 4096 → 2.05 s (near optimal)
BUF_SIZE 65536 → 2.06 s

  1. not synchronized

    write() trả về ≠ dữ liệu đã trên đĩa Ghi xong chưa chắc đã xuống đĩa write() returning ≠ data on disk

    Điểm cốt lõi: khi write() trả về, nhân mới chỉ hứa rằng dữ liệu đã nằm trong buffer cache, chưa chắc đã ghi lên đĩa.1 Nếu trong khoảng đó có tiến trình khác đọc đúng vùng byte này, nhân sẽ cấp dữ liệu từ buffer cache (bản mới) chứ không phải từ nội dung đĩa (bản cũ) — nên về mặt logic chương trình không thấy khác biệt. Nhưng về mặt bền vững, dữ liệu chưa an toàn trước sự cố mất điện cho tới khi được flush.1

    Khi máy báo “ghi xong”, thật ra dữ liệu mới chỉ nằm trong bộ nhớ tạm của nhân, chưa chắc đã ở trên đĩa. Chương trình khác đọc lại thì vẫn thấy đúng dữ liệu mới, nhưng nếu mất điện ngay lúc đó thì có thể mất. Nhớ nhé: “ghi xong” nghĩa là vào bộ nhớ tạm, chưa phải đã nằm chắc trên đĩa.

    The crux: when write() returns, the kernel has only promised the data is in the buffer cache, not necessarily written to disk.1 If another process reads those bytes in the interim, the kernel supplies them from the buffer cache (the fresh copy), not from the (outdated) disk contents — so logically nothing looks different. But for durability, the data is not safe against a power failure until it is flushed.1

    Bạn gặp nó ở đâuWhere you meet it Một tiến trình ghi nhật ký của cơ sở dữ liệu (database journaling) phải chắc dữ liệu thật sự xuống đĩa trước khi báo “đã commit” — đó là lý do cần các lời gọi flush ở cụm 03.1 Phần mềm ngân hàng phải chắc dữ liệu đã xuống đĩa trước khi báo “xong giao dịch”. A database journaling process must be sure data really reached the disk before reporting “committed” — which is why the flush calls in cluster 03 exist.1
  2. buffer size

    Khối càng lớn, lời gọi hệ thống càng ít Gom khối lớn thì chạy nhanh hơn nhiều Bigger blocks, fewer system calls

    Dù ta ghi 1000 lần mỗi lần 1 byte hay ghi 1 lần 1000 byte, nhân vẫn thực hiện cùng số lần truy cập đĩa.1 Khác biệt nằm ở số lời gọi hệ thống: cách sau chỉ cần một, cách trước cần tới 1000 — mà mỗi lời gọi đều tốn thời gian (nhân phải bẫy lời gọi, kiểm tra tham số, chép dữ liệu giữa hai không gian). Bảng 13-1 trong sách (sao chép tệp 100 triệu byte trên ext2) cho thấy: với BUF_SIZE bằng 1 byte, mất 107,43 giây; với 4096 byte chỉ còn khoảng 2,05 giây và đạt gần mức tối ưu — vượt qua điểm đó hầu như không nhanh thêm.1

    Ghi 1000 lần mỗi lần 1 chữ cái, hay ghi 1 lần cả 1000 chữ — số lần đụng tới đĩa là như nhau. Nhưng mỗi lần ra lệnh cho nhân đều tốn công, nên gộp lại thành một lệnh lớn thì nhanh hơn hẳn: sách đo được chép tệp lớn giảm từ 107 giây xuống còn 2 giây khi dùng khối 4096 byte. Nhớ nhé: gom nhiều mẩu nhỏ thành khối lớn rồi hẵng ghi — nhanh hơn rất nhiều.

    Whether we do 1000 writes of a single byte or a single write of 1000 bytes, the kernel performs the same number of disk accesses.1 The difference is the number of system calls: the latter needs one, the former 1000 — and each call takes time (the kernel must trap it, validate arguments, copy data between the two spaces). The book's Table 13-1 (copying a 100-million-byte file on ext2) shows that with a BUF_SIZE of 1 byte it takes 107.43 seconds; with 4096 bytes it falls to about 2.05 seconds and reaches near-optimal performance — beyond that point there is almost no further gain.1

    Bạn gặp nó ở đâuWhere you meet it Đây chính là lý do thực dụng để dùng thư viện stdio: nó tự gom dữ liệu thành khối lớn giúp bạn, như cụm 02 mô tả.1 Chính vì vậy nên dùng thư viện stdio: nó tự gom khối lớn hộ bạn. This is the practical reason to use the stdio library: it batches data into large blocks for you, as cluster 02 describes.1

    Con số lời gọi

    Với khối 1 byte, chương trình thực hiện 100 triệu lời gọi read() và write(); với khối 4096 byte, số lần gọi mỗi hàm tụt xuống còn khoảng 24.000, và đạt gần hiệu năng tối ưu.1

    Phần lớn thời gian là đọc đĩa

    Bảng 13-2 (chỉ ghi, không đọc) cho thấy với khối 65.536 byte chỉ mất 0,09 giây, so với 2,06 giây ở Bảng 13-1 — chênh lệch ấy chứng tỏ phần lớn thời gian ở các khối lớn của Bảng 13-1 là do đọc đĩa.1

  3. read-ahead · page cache

    Đọc trước và page cache — tối ưu của nhân Nhân đọc sẵn phần tiếp theo cho nhanh Read-ahead and the page cache

    Với truy cập tuần tự, nhân thường thực hiện read-ahead (đọc trước): nạp sẵn các khối tiếp theo của tệp vào buffer cache trước khi tiến trình đọc tới, để lời read() kế tiếp không phải chờ đĩa.1 Kerrisk cũng lưu ý một điểm chính xác về cách gọi tên: từ nhân 2.4 trở đi, Linux không còn duy trì buffer cache tách riêng — các vùng đệm vào/ra của tệp được gộp vào page cache (nơi cũng chứa, chẳng hạn, các trang của tệp ánh xạ bộ nhớ). Dù vậy, sách vẫn dùng thuật ngữ “buffer cache” vì nó quen thuộc trong lịch sử các hệ UNIX.1

    Khi bạn đọc tệp từ đầu đến cuối, nhân đoán trước và nạp sẵn phần tiếp theo vào bộ nhớ, nên bạn gần như không phải chờ. Một lưu ý nhỏ: từ Linux 2.4, cái kho tạm này được gộp chung vào một kho lớn hơn tên “page cache”, nhưng người ta vẫn quen gọi là “buffer cache”. Nhớ nhé: nhân đọc trước phần kế tiếp, nên đọc tuần tự rất mượt.

    For sequential access, the kernel typically performs read-ahead: loading the next blocks of the file into the buffer cache before the reading process needs them, so the next read() need not wait on the disk.1 Kerrisk also notes a point about naming: from kernel 2.4 onward, Linux no longer maintains a separate buffer cache — file I/O buffers are included in the page cache (which also holds, for example, pages of memory-mapped files). Even so, the book uses the term “buffer cache” because it is historically common on UNIX implementations.1

    Bạn gặp nó ở đâuWhere you meet it Ở cụm 03, posix_fadvise() cho bạn báo cho nhân biết sẽ đọc tuần tự hay ngẫu nhiên — chính là điều chỉnh cơ chế read-ahead này.18 Lát nữa bạn sẽ thấy cách báo cho nhân biết mình sắp đọc kiểu gì để nó đọc trước cho khéo. In cluster 03, posix_fadvise() lets you tell the kernel whether you will read sequentially or randomly — tuning exactly this read-ahead mechanism.18

Cụm khái niệm 02 Concept cluster 02

Đệm trong thư viện stdio — tầng thứ hai, ở phía người dùng Kho tạm của thư viện C — tầng thứ hai Buffering in the stdio library — the second, user-space layer

Gom dữ liệu thành khối lớn để giảm số lời gọi hệ thống chính là việc mà các hàm stdio (như printf(), fprintf(), fscanf(), fgets(), fputs(), fputc(), fgetc()) làm khi thao tác trên tệp đĩa.1 Dùng thư viện stdio giải phóng ta khỏi việc tự tay đệm dữ liệu cho xuất (bằng write()) hay nhập (bằng read()). Đây là một tầng đệm nằm trong không gian người dùng, tách biệt và nằm phía trên buffer cache của nhân.1

Thư viện C (stdio) làm hộ bạn việc gom chữ lại thành khối lớn. Khi bạn gọi printf để in, nó không gửi đi ngay từng chữ, mà gom vào một kho tạm cho đầy rồi mới gửi một thể — nhờ vậy số lần nhờ nhân ít đi, chương trình nhanh hơn. Nhớ nhé: stdio tự gom chữ cho đầy kho rồi mới gửi, bạn khỏi phải lo.

Buffering data into large blocks to reduce system calls is exactly what the stdio functions (such as printf(), fprintf(), fscanf(), fgets(), fputs(), fputc(), fgetc()) do when operating on disk files.1 Using the stdio library relieves us of buffering data ourselves for output (with write()) or input (with read()). This is a buffering layer that lives in user space, separate from and on top of the kernel buffer cache.1

Định nghĩa. setvbuf(stream, buf, mode, size) điều khiển kiểu đệm mà stdio dùng cho một luồng (stream); phải gọi nó sau khi mở luồng và trước mọi thao tác stdio khác.16 Tham số mode nhận một trong ba giá trị: _IONBF (không đệm — mỗi lời gọi stdio sinh ngay một write()/read(), là mặc định của stderr), _IOLBF (đệm theo dòng — mặc định cho luồng gắn với thiết bị đầu cuối), và _IOFBF (đệm đầy đủ — mặc định cho luồng gắn với tệp đĩa).16

Nói đơn giản. Có một cái “công tắc” tên setvbuf để chọn cách gom: gom đầy cả kho rồi mới gửi (nhanh nhất, dùng cho tệp), gom theo từng dòng (dùng cho màn hình), hoặc không gom gì cả (gửi ngay từng cái, dùng cho báo lỗi). Nhớ nhé: setvbuf chọn cách gom — đầy kho, theo dòng, hay gửi ngay.

Definition. setvbuf(stream, buf, mode, size) controls the form of buffering stdio uses for a stream; it must be called after opening the stream and before any other stdio operation.16 The mode argument takes one of three values: _IONBF (unbuffered — each stdio call yields an immediate write()/read(), the default for stderr), _IOLBF (line-buffered — the default for streams referring to terminal devices), and _IOFBF (fully buffered — the default for streams referring to disk files).16

_IONBF → không đệm (stderr)
_IOLBF → đệm theo dòng (terminal)
_IOFBF → đệm đầy đủ (tệp đĩa)
setbuf(fp,buf) = setvbuf với BUFSIZ
BUFSIZ = 8192 (glibc)

3 kiểu gom:
không gom · theo dòng · đầy kho
kho mặc định ≈ 8192 byte

_IONBF → unbuffered (stderr)
_IOLBF → line-buffered (terminal)
_IOFBF → fully buffered (disk file)
setbuf(fp,buf) = setvbuf with BUFSIZ
BUFSIZ = 8192 (glibc)

  1. three modes

    Ba kiểu đệm — và vì sao mặc định khác nhau Ba kiểu gom, mỗi nơi một mặc định Three buffering modes — and why defaults differ

    Các mặc định được chọn rất có chủ ý. stderr không đệm (_IONBF) để thông báo lỗi hiện ra ngay lập tức.1 Luồng tới thiết bị đầu cuối thì đệm theo dòng (_IOLBF): với luồng xuất, dữ liệu được giữ lại cho tới khi gặp ký tự xuống dòng (trừ khi kho đầy trước); với luồng nhập, dữ liệu được đọc từng dòng một.1 Luồng tới tệp đĩa thì đệm đầy đủ (_IOFBF): dữ liệu được đọc/ghi theo đơn vị bằng đúng kích thước kho đệm — đây là mặc định cho đĩa, nơi hiệu năng quan trọng nhất.1

    Mỗi nơi có mặc định riêng cho hợp lý: báo lỗi thì gửi ngay (để thấy liền), màn hình thì gom theo dòng (gõ xong một dòng là hiện), còn tệp trên đĩa thì gom cho đầy kho rồi mới ghi (để nhanh). Nhớ nhé: lỗi gửi ngay, màn hình theo dòng, tệp thì đầy kho mới ghi.

    The defaults are chosen deliberately. stderr is unbuffered (_IONBF) so error messages appear immediately.1 A stream to a terminal is line-buffered (_IOLBF): for output, data is held until a newline is output (unless the buffer fills first); for input, data is read a line at a time.1 A stream to a disk file is fully buffered (_IOFBF): data is read or written in units equal to the buffer size — the default for disk, where performance matters most.1

    Bạn gặp nó ở đâuWhere you meet it Khi chạy chương trình trong terminal thấy chữ hiện từng dòng, nhưng ghi ra tệp lại thấy gộp một cục — đó chính là sự khác biệt _IOLBF và _IOFBF.1 Chạy ở màn hình thấy chữ ra từng dòng, ghi ra tệp thì ra một cục — là do kiểu gom khác nhau. When a program's output appears line by line on a terminal but in one lump in a file, that is exactly the _IOLBF vs _IOFBF difference.1
  2. setbuf · setbuffer

    setbuf và setbuffer — lớp vỏ gọn hơn Hai lệnh gọn hơn để đặt kho đệm setbuf and setbuffer — simpler wrappers

    Hai hàm tiện lợi được xây trên setvbuf(). Lời gọi setbuf(fp, buf) tương đương setvbuf(fp, buf, (buf != NULL) ? _IOFBF : _IONBF, BUFSIZ) — tức là: truyền buf khác NULL thì đệm đầy đủ, truyền NULL thì không đệm.16 Hằng BUFSIZ định nghĩa trong <stdio.h>; trong hiện thực glibc nó bằng 8192 — một giá trị tiêu biểu.1 Hàm setbuffer(fp, buf, size) giống setbuf() nhưng cho phép tự chọn kích thước kho đệm.1

    Nếu thấy setvbuf hơi rườm, có hai lệnh gọn hơn: setbuf chỉ cần đưa một kho là bật đệm đầy đủ, đưa “rỗng” là tắt đệm; setbuffer thì cho bạn chọn luôn cỡ kho. Kho mặc định thường khoảng 8192 byte. Nhớ nhé: setbuf và setbuffer là bản gọn của setvbuf.

    Two convenience functions are built on setvbuf(). The call setbuf(fp, buf) is equivalent to setvbuf(fp, buf, (buf != NULL) ? _IOFBF : _IONBF, BUFSIZ) — that is: pass a non-NULL buf for full buffering, pass NULL for no buffering.16 The BUFSIZ constant is defined in <stdio.h>; in the glibc implementation it is 8192 — a typical value.1 The setbuffer(fp, buf, size) function is like setbuf() but lets you choose the buffer size.1

    Bạn gặp nó ở đâuWhere you meet it Nếu truyền buf do bạn tự cấp, nó phải là bộ nhớ tĩnh hoặc cấp trên heap (malloc()) — không được là biến cục bộ trên stack, vì khi hàm trả về, khung stack bị giải phóng và sinh lỗi loạn.16 Nếu tự đưa kho riêng, nhớ cấp bằng malloc, đừng dùng biến tạm trong hàm — nếu không sẽ lỗi. If you pass your own buf, it must be static or heap memory (malloc()) — not a local variable on the stack, since when the function returns its stack frame is freed and chaos results.16
  3. fflush

    fflush() — ép kho stdio xuống nhân fflush() — đẩy kho tạm của stdio đi fflush() — force the stdio buffer to the kernel

    Bất kể kiểu đệm nào, bất cứ lúc nào ta cũng có thể ép dữ liệu trong kho stdio của một luồng xuất được ghi (flush — tức chuyển tới vùng đệm của nhân qua write()) bằng hàm fflush(stream).17 Nếu stream là NULL, fflush() flush mọi luồng stdio.1 Kho stdio cũng được flush tự động khi luồng tương ứng được đóng. Áp fflush() lên một luồng nhập thì làm bỏ dữ liệu đã đệm sẵn.17

    Khi muốn “tống” hết chữ đang nằm trong kho tạm của stdio đi ngay (chưa cần đầy kho), bạn gọi fflush. Đưa “rỗng” thì nó tống hết mọi kho. Lúc đóng tệp nó cũng tự tống nốt. Nhớ nhé: fflush là nút “gửi ngay” cho kho của stdio.

    Regardless of the buffering mode, at any time we can force the data in a stdio output stream's buffer to be flushed (written to the kernel buffer via write()) with fflush(stream).17 If stream is NULL, fflush() flushes all stdio streams.1 A stdio buffer is also flushed automatically when its stream is closed. Applying fflush() to an input stream discards any buffered input.17

    Bạn gặp nó ở đâuWhere you meet it Để một dấu nhắc không có ký tự xuống dòng (ví dụ printf("Date: ")) hiện ra trước khi chương trình chờ nhập, chương trình khả chuyển nên gọi fflush(stdout) tường minh.1 Muốn dòng hỏi “Nhập tên: ” hiện ra trước khi gõ, hãy gọi fflush(stdout). To make a prompt without a trailing newline (e.g. printf("Date: ")) appear before the program waits for input, portable programs should call fflush(stdout) explicitly.1

    Flush ngầm tới terminal

    Ở nhiều hiện thực thư viện C, kể cả glibc, nếu stdin và stdout trỏ tới terminal thì một lời fflush(stdout) ngầm được thực hiện mỗi khi đọc từ stdin. Hành vi này không có trong SUSv3/C99 và không phải thư viện nào cũng làm.1

    Ràng buộc đọc–ghi của C99

    C99 đặt ra: với luồng mở để vừa đọc vừa ghi, một thao tác xuất không được theo ngay sau một thao tác nhập mà không có fflush() hoặc một hàm định vị tệp (fseek(), fsetpos(), rewind()) xen giữa, và ngược lại.1

Cụm khái niệm 03 Concept cluster 03

Điều khiển đệm của nhân — toàn vẹn dữ liệu và cái giá của nó Ép dữ liệu xuống đĩa — và cái giá phải trả Controlling kernel buffering — integrity and its cost

Ta có thể buộc nhân flush các vùng đệm của tệp xuất xuống đĩa.1 SUSv3 định nghĩa hai mức “hoàn tất vào/ra đồng bộ” (synchronized I/O completion): toàn vẹn dữ liệu (data integrity) bảo đảm đủ thông tin để sau này đọc lại được dữ liệu; toàn vẹn tệp (file integrity) là một siêu tập — chuyển cả những metadata (siêu dữ liệu, “dữ liệu về dữ liệu”) không cần thiết cho việc đọc lại, như dấu thời gian sửa đổi cuối.19 Khác biệt này chính là chỗ phân biệt hai lời gọi hệ thống chủ chốt.1

Khi cần chắc dữ liệu đã an toàn trên đĩa, ta ra lệnh cho nhân “đẩy xuống ngay”. Có hai mức: mức nhẹ chỉ bảo đảm phần dữ liệu (và thông tin tối thiểu để đọc lại); mức đầy đủ thì đẩy cả những thông tin phụ như giờ sửa tệp. Nhớ nhé: có mức đẩy nhẹ (chỉ dữ liệu) và mức đẩy đủ (cả thông tin phụ).

We can force the kernel to flush a file's output buffers to disk.1 SUSv3 defines two levels of synchronized I/O completion: data integrity ensures enough information is transferred to later retrieve the data; file integrity is a superset — it also transfers metadata (“data about data”) not needed for retrieval, such as the last-modification timestamp.19 This distinction is exactly what separates the two key system calls.1

Định nghĩa. fsync(fd) buộc dữ liệu đã đệm và tất cả metadata gắn với mô tả tệp fd xuống đĩa — đưa tệp về trạng thái toàn vẹn tệp; nó chỉ trả về sau khi việc truyền tới thiết bị đĩa (hoặc ít nhất là bộ nhớ đệm phần cứng của đĩa) hoàn tất.2 fdatasync(fd) làm tương tự nhưng chỉ đưa tệp về mức toàn vẹn dữ liệu: nó có thể giảm số thao tác đĩa từ hai (như fsync()) xuống một, vì không buộc ghi những metadata không cần thiết (như dấu thời gian).13

Nói đơn giản. fsync đẩy cả dữ liệu lẫn mọi thông tin phụ của tệp xuống đĩa rồi mới trả về. fdatasync gọn hơn: chỉ đẩy dữ liệu và thông tin tối thiểu, nên thường nhanh hơn vì ít đụng đĩa hơn. Nhớ nhé: fsync đẩy đủ, fdatasync đẩy gọn và thường nhanh hơn.

Definition. fsync(fd) forces buffered data and all metadata associated with the open file fd to disk — bringing the file to the synchronized file integrity state; it returns only after the transfer to the disk device (or at least its hardware cache) has completed.2 fdatasync(fd) is similar but brings the file only to the data integrity state: it can cut disk operations from two (as with fsync()) to one, since it need not write unnecessary metadata (such as timestamps).13

fsync(fd) → dữ liệu + mọi metadata
fdatasync(fd) → dữ liệu + metadata tối thiểu
sync() → mọi vùng đệm của nhân
open(…, O_SYNC) → mỗi write() đồng bộ
O_DSYNC ~ fdatasync · O_RSYNC (đọc)

fsync → đẩy đủ
fdatasync → đẩy gọn
sync → đẩy tất cả
O_SYNC → ghi lần nào xuống đĩa lần đó

fsync(fd) → data + all metadata
fdatasync(fd) → data + minimal metadata
sync() → all kernel buffers
open(…, O_SYNC) → every write() synchronous
O_DSYNC ~ fdatasync · O_RSYNC (reads)

  1. fsync · fdatasync · sync

    Ba cách ép flush — và pdflush âm thầm Ba lệnh đẩy dữ liệu xuống đĩa Three ways to flush — and the quiet pdflush

    Ngoài fsync() và fdatasync() tác động lên một tệp, lời gọi sync() buộc mọi vùng đệm của nhân chứa thông tin tệp đã cập nhật (dữ liệu, khối con trỏ, metadata…) xuống đĩa; trong hiện thực Linux, sync() chỉ trả về sau khi dữ liệu đã được truyền tới thiết bị đĩa (hoặc bộ đệm phần cứng của nó).14 Ngay cả khi ta không gọi gì, một luồng nhân chạy nền vẫn bảo đảm các vùng đệm đã sửa được flush trong vòng 30 giây — ở Linux 2.6 do luồng pdflush đảm nhiệm; tệp /proc/sys/vm/dirty_expire_centisecs quy định tuổi tối đa của một vùng đệm “bẩn” trước khi bị flush.1

    Muốn đẩy đúng một tệp thì dùng fsync hoặc fdatasync; muốn đẩy sạch tất cả thì gọi sync. Mà kể cả bạn không làm gì, nhân cũng tự đẩy mọi thứ còn tồn trong vòng khoảng 30 giây. Nhớ nhé: fsync/fdatasync đẩy một tệp, sync đẩy tất cả, và nhân cũng tự đẩy sau ~30 giây.

    Besides fsync() and fdatasync() acting on one file, sync() forces all kernel buffers containing updated file information (data, pointer blocks, metadata…) to disk; in the Linux implementation, sync() returns only after the data has been transferred to the disk device (or at least its hardware cache).14 Even if we call nothing, a background kernel thread ensures modified buffers are flushed within 30 seconds — in Linux 2.6 this is the pdflush thread; the file /proc/sys/vm/dirty_expire_centisecs sets the maximum age of a dirty buffer before it is flushed.1

    Bạn gặp nó ở đâuWhere you meet it Để một tệp stdio thật sự xuống đĩa, phải flush cả hai tầng theo thứ tự: fflush(fp) đẩy kho stdio xuống nhân, rồi fsync(fileno(fp)) đẩy buffer cache của nhân xuống đĩa.12 Muốn chắc tệp mở bằng stdio đã xuống đĩa: gọi fflush trước, rồi fsync sau. For a stdio file to truly reach disk, flush both layers in order: fflush(fp) pushes the stdio buffer to the kernel, then fsync(fileno(fp)) pushes the kernel buffer cache to disk.12
  2. O_SYNC · O_DSYNC · O_RSYNC

    O_SYNC — mọi write() tự đồng bộ, rất đắt O_SYNC — ghi lần nào xuống đĩa lần đó O_SYNC — every write() synchronous, and costly

    Mở tệp với cờ O_SYNC khiến mọi lời write() sau đó tự động flush dữ liệu và metadata xuống đĩa (mức toàn vẹn tệp).5 Tiện nhưng rất đắt: Bảng 13-3 trong sách (ghi 1 triệu byte trên ext2) cho thấy với khối 1 byte, thời gian thực tăng từ 0,73 giây (không O_SYNC) lên 1030 giây (có O_SYNC) — gấp hơn 1000 lần.1 SUSv3 còn có O_DSYNC (đồng bộ theo mức toàn vẹn dữ liệu, như fdatasync()) và O_RSYNC (mở rộng hành vi ấy sang thao tác đọc).15

    Mở tệp với cờ O_SYNC thì cứ ghi là xuống đĩa luôn, chắc chắn nhưng chậm khủng khiếp: sách đo thấy chậm hơn gấp hơn nghìn lần khi ghi từng byte một. Nên dùng dè. Nhớ nhé: O_SYNC an toàn tuyệt đối nhưng chậm gấp cả nghìn lần, phải cân nhắc.

    Opening a file with the O_SYNC flag makes every subsequent write() automatically flush data and metadata to disk (file-integrity level).5 Convenient but very costly: the book's Table 13-3 (writing 1 million bytes on ext2) shows that with a 1-byte buffer, elapsed time rises from 0.73 seconds (without O_SYNC) to 1030 seconds (with O_SYNC) — a factor of more than 1000.1 SUSv3 also defines O_DSYNC (synchronized to the data-integrity level, like fdatasync()) and O_RSYNC (extending that behavior to read operations).15

    Bạn gặp nó ở đâuWhere you meet it Thay vì bật O_SYNC cho mọi write, Kerrisk khuyên thiết kế để dùng khối ghi lớn, hoặc gọi fsync()/fdatasync() thưa thớt đúng chỗ cần.1 Thay vì bật O_SYNC khắp nơi, nên ghi khối lớn và chỉ gọi fsync đúng lúc cần. Rather than turning on O_SYNC for every write, Kerrisk advises designing for large write buffers, or making judicious fsync()/fdatasync() calls only where needed.1

    Một lưu ý về phiên bản nhân

    Trước nhân 2.6.33, Linux không hiện thực đầy đủ ngữ nghĩa O_SYNC mà thực chất cài nó như O_DSYNC; các O_DSYNC/O_RSYNC lúc đó cũng được glibc định nghĩa trùng với O_SYNC. Từ 2.6.33, Linux hiện thực O_DSYNC đúng nghĩa.15

    Bộ đệm phần cứng của đĩa

    Bảng 13-3 còn bỏ qua một yếu tố: ổ đĩa hiện đại có bộ nhớ đệm lớn, nên mặc định O_SYNC chỉ chuyển dữ liệu tới bộ đệm ấy. Nếu tắt cache đĩa (lệnh hdparm -W0), tác động của O_SYNC còn khắc nghiệt hơn nữa.1

  3. posix_fadvise · O_DIRECT

    Mách nước cho nhân, và bỏ qua buffer cache Báo trước cho nhân, hoặc bỏ qua kho tạm Advising the kernel, and bypassing the cache

    Ở chiều ngược lại — tối ưu thay vì ép an toàn — posix_fadvise(fd, offset, len, advice) cho tiến trình báo cho nhân biết kiểu truy cập dự kiến của mình, để nhân tối ưu dùng buffer cache; nhân có thể (nhưng không bắt buộc) dùng thông tin ấy, và lời gọi này không làm thay đổi ngữ nghĩa chương trình.8 Chẳng hạn POSIX_FADV_SEQUENTIAL báo sẽ đọc tuần tự (Linux tăng cửa sổ read-ahead lên gấp đôi mặc định), còn POSIX_FADV_RANDOM báo đọc ngẫu nhiên (Linux tắt read-ahead).18 Linux cung cấp posix_fadvise() từ nhân 2.6.1

    Có một lệnh để “mách nước” cho nhân: bạn báo trước sẽ đọc tệp kiểu tuần tự hay nhảy lung tung, nhân sẽ chuẩn bị cho khéo. Ngoài ra còn một cách đặc biệt (O_DIRECT) để bỏ hẳn kho tạm, nhưng chỉ hợp với vài phần mềm chuyên dụng. Nhớ nhé: posix_fadvise để mách nước cho nhân; O_DIRECT để bỏ qua kho tạm.

    In the other direction — optimizing rather than forcing safety — posix_fadvise(fd, offset, len, advice) lets a process tell the kernel its expected access pattern, so the kernel can optimize its use of the buffer cache; the kernel may (but need not) use the information, and the call does not change program semantics.8 For instance POSIX_FADV_SEQUENTIAL signals sequential reading (Linux doubles the read-ahead window), while POSIX_FADV_RANDOM signals random reading (Linux disables read-ahead).18 Linux provides posix_fadvise() since kernel 2.6.1

    Bạn gặp nó ở đâuWhere you meet it Từ nhân 2.4, mở tệp với cờ O_DIRECT cho phép bỏ qua buffer cache (direct I/O) — chỉ nên dùng cho ứng dụng tự quản cache của riêng nó, như hệ cơ sở dữ liệu.15 O_DIRECT (từ Linux 2.4) cho phép ghi thẳng, bỏ kho tạm — chỉ hợp với database tự lo cache. Since kernel 2.4, opening a file with O_DIRECT allows bypassing the buffer cache (direct I/O) — only for applications that manage their own cache, like database systems.15

Ba ngộ nhận thường gặp Three common misconceptions

“write() trả về nghĩa là dữ liệu đã trên đĩa” “write() returning means the data is on disk”

Ngộ nhận: gọi write() xong là dữ liệu đã an toàn trên đĩa.

Thực tế: write() chỉ chép dữ liệu vào buffer cache rồi trả về; lời gọi “không đồng bộ” với thao tác đĩa, dữ liệu có thể mất nếu mất điện trước khi flush. Muốn chắc, dùng fsync() hay O_SYNC.12

Myth: once write() returns, the data is safely on disk.

Reality: write() only copies data into the buffer cache and returns; the call is not synchronized with the disk, and data can be lost on a power failure before the flush. For certainty, use fsync() or O_SYNC.12

“Direct I/O (O_DIRECT) luôn nhanh hơn” “Direct I/O (O_DIRECT) is always faster”

Ngộ nhận: bỏ qua buffer cache bằng O_DIRECT thì vào/ra nhanh hơn.

Thực tế: với phần lớn ứng dụng, direct I/O làm chậm đáng kể, vì mất hết tối ưu của nhân (read-ahead, gom khối, chia sẻ cache). Nó chỉ dành cho ứng dụng có yêu cầu vào/ra đặc thù, như database tự quản cache.15

Myth: bypassing the buffer cache with O_DIRECT makes I/O faster.

Reality: for most applications, direct I/O degrades performance considerably, since it loses the kernel's optimizations (read-ahead, clustering, shared cache). It is intended only for apps with specialized I/O requirements, such as databases managing their own cache.15

“Trộn printf() và write() thì thứ tự in luôn đúng” “Mixing printf() and write() keeps output in order”

Ngộ nhận: gọi printf() rồi write() thì đầu ra hiện đúng thứ tự gọi.

Thực tế: printf() gom vào kho stdio ở người dùng, còn write() đi thẳng vào nhân, nên đầu ra của printf() thường hiện sau. Cần gọi fflush() đúng chỗ khi trộn system call và stdio trên cùng tệp (xem fileno(), fdopen()).17

Myth: calling printf() then write() makes output appear in call order.

Reality: printf() buffers in the user-space stdio buffer while write() goes straight to the kernel, so the printf() output often appears later. Judicious fflush() is needed when mixing system calls and stdio on the same file (see fileno(), fdopen()).17

Tự kiểm Self-check

Năm câu hỏi để biết bạn đã nắm hai tầng đệm Năm câu hỏi kiểm tra nhanh Five questions to check you grasped the two buffering layers

Trả lời được những câu này nghĩa là bạn đã theo trọn mạch từ buffer cache của nhân, qua vùng đệm stdio, tới các cách ép flush và cái giá hiệu năng.

Nếu trả lời được, nghĩa là bạn đã hiểu chương này.

Answering these means you followed the whole line from the kernel buffer cache, through the stdio buffer, to the ways of forcing a flush and their performance cost.

  1. Khi write(fd, "abc", 3) trả về, dữ liệu đang ở đâu, và vì sao ta nói lời gọi “không đồng bộ” với đĩa?

    Gợi ý sâu hơn: nếu một tiến trình khác đọc đúng 3 byte đó ngay sau đó, nó lấy dữ liệu từ đâu?

  2. Vì sao ghi theo khối 4096 byte lại nhanh hơn ghi từng byte đến hàng chục lần, dù số lần truy cập đĩa như nhau?

    Gợi ý sâu hơn: Bảng 13-1 và 13-2 khác nhau ở điểm nào, và chênh lệch ấy nói lên phần lớn thời gian là do đâu?

  3. Tầng đệm stdio khác tầng buffer cache của nhân ra sao? Ba kiểu _IONBF, _IOLBF, _IOFBF mặc định cho những luồng nào?

    Gợi ý sâu hơn: để một tệp mở bằng stdio thật sự xuống đĩa, phải gọi những hàm nào, theo thứ tự nào?

  4. fsync() và fdatasync() khác nhau thế nào về toàn vẹn dữ liệu và toàn vẹn tệp?

    Gợi ý sâu hơn: vì sao fdatasync() có thể giảm số thao tác đĩa từ hai xuống một?

  5. Cờ O_SYNC làm gì, và Bảng 13-3 cho thấy cái giá hiệu năng của nó lớn cỡ nào?

    Gợi ý sâu hơn: thay vì O_SYNC, Kerrisk gợi ý hai cách thiết kế nào để vừa an toàn vừa đỡ chậm?

  1. Khi máy báo “ghi xong”, dữ liệu đã nằm chắc trên đĩa chưa?

  2. Ghi khối lớn và ghi từng chữ, cái nào nhanh hơn? Vì sao?

  3. Thư viện stdio làm hộ bạn việc gì?

  4. Muốn chắc dữ liệu đã xuống đĩa thì gọi lệnh nào?

  5. Vì sao bật O_SYNC lại làm chương trình chậm hẳn?

  1. When write(fd, "abc", 3) returns, where is the data, and why do we say the call is not synchronized with the disk?

  2. Why is writing in 4096-byte blocks tens of times faster than writing byte by byte, though the number of disk accesses is the same?

  3. How does the stdio buffer differ from the kernel buffer cache? Which streams default to _IONBF, _IOLBF, _IOFBF?

  4. How do fsync() and fdatasync() differ regarding data integrity and file integrity?

  5. What does the O_SYNC flag do, and how large is its performance cost in Table 13-3?

Khép lại chương Closing the chapter

Vào/ra tệp đi qua hai tầng đệm — vùng đệm stdio và buffer cache của nhân — dựng lên để đổi lấy tốc độ bằng cách gom thao tác. Nhưng tốc độ đi kèm rủi ro: khi write() trả về, dữ liệu chưa chắc trên đĩa, nên muốn vừa nhanh vừa an toàn, ta phải chủ động điều khiển cả hai tầng.1

Dữ liệu vào/ra tệp đi qua hai kho tạm, gom lại cho máy chạy nhanh; nhưng nhanh thì có thể mất nếu mất điện, nên lúc cần hãy chủ động ép dữ liệu xuống đĩa. Nhớ nhé: hiểu hai tầng đệm thì vừa viết được chương trình nhanh, vừa giữ được dữ liệu an toàn.

File I/O passes through two buffering layers — the stdio buffer and the kernel buffer cache — built to buy speed by batching operations. But speed carries risk: when write() returns, the data may not be on disk, so to be both fast and safe, we must deliberately control both layers.1