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
-
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.1Khi 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.1Bạ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 -
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_SIZEbằ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.1Ghi 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_SIZEof 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.1Bạ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.1Con 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.1Phầ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
-
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.1Khi 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.1Bạ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)
-
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ủ ý.
stderrkhô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.1Mỗ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.
stderris 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.1Bạ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_IOLBFvà_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_IOLBFvs_IOFBFdifference.1 -
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ọisetbuf(fp, buf)tương đươngsetvbuf(fp, buf, (buf != NULL) ? _IOFBF : _IONBF, BUFSIZ)— tức là: truyềnbufkhácNULLthì đệm đầy đủ, truyềnNULLthì không đệm.16 HằngBUFSIZđị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àmsetbuffer(fp, buf, size)giốngsetbuf()nhưng cho phép tự chọn kích thước kho đệm.1Nếu thấy
setvbufhơi rườm, có hai lệnh gọn hơn:setbufchỉ cần đưa một kho là bật đệm đầy đủ, đưa “rỗng” là tắt đệm;setbufferthì 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 callsetbuf(fp, buf)is equivalent tosetvbuf(fp, buf, (buf != NULL) ? _IOFBF : _IONBF, BUFSIZ)— that is: pass a non-NULLbuffor full buffering, passNULLfor no buffering.16 TheBUFSIZconstant is defined in<stdio.h>; in the glibc implementation it is 8192 — a typical value.1 Thesetbuffer(fp, buf, size)function is likesetbuf()but lets you choose the buffer size.1Bạn gặp nó ở đâuWhere you meet it Nếu truyềnbufdo 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 ownbuf, 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 -
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àmfflush(stream).17 Nếustreamlà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. Ápfflush()lên một luồng nhập thì làm bỏ dữ liệu đã đệm sẵn.17Khi 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()) withfflush(stream).17 IfstreamisNULL,fflush()flushes all stdio streams.1 A stdio buffer is also flushed automatically when its stream is closed. Applyingfflush()to an input stream discards any buffered input.17Bạ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ọifflush(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 callfflush(stdout)explicitly.1Flush ngầm tới terminal
Ở nhiều hiện thực thư viện C, kể cả glibc, nếu
stdinvàstdouttrỏ tới terminal thì một lờifflush(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.1Rà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)
-
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ọisync()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_centisecsquy định tuổi tối đa của một vùng đệm “bẩn” trước khi bị flush.1Muốn đẩy đúng một tệp thì dùng
fsynchoặcfdatasync; muốn đẩy sạch tất cả thì gọisync. 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()andfdatasync()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_centisecssets the maximum age of a dirty buffer before it is flushed.1Bạ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ồifsync(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, thenfsync(fileno(fp))pushes the kernel buffer cache to disk.12 -
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_SYNCkhiến mọi lờiwrite()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ôngO_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).15Mở tệp với cờ
O_SYNCthì 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_SYNCflag makes every subsequentwrite()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 (withoutO_SYNC) to 1030 seconds (withO_SYNC) — a factor of more than 1000.1 SUSv3 also definesO_DSYNC(synchronized to the data-integrity level, likefdatasync()) andO_RSYNC(extending that behavior to read operations).15Bạn gặp nó ở đâuWhere you meet it Thay vì bậtO_SYNCcho mọi write, Kerrisk khuyên thiết kế để dùng khối ghi lớn, hoặc gọifsync()/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 onO_SYNCfor every write, Kerrisk advises designing for large write buffers, or making judiciousfsync()/fdatasync()calls only where needed.1Mộ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_SYNCmà thực chất cài nó nhưO_DSYNC; cácO_DSYNC/O_RSYNClúc đó cũng được glibc định nghĩa trùng vớiO_SYNC. Từ 2.6.33, Linux hiện thựcO_DSYNCđúng nghĩa.15Bộ đệ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_SYNCchỉ chuyển dữ liệu tới bộ đệm ấy. Nếu tắt cache đĩa (lệnhhdparm -W0), tác động củaO_SYNCcòn khắc nghiệt hơn nữa.1 -
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ạnPOSIX_FADV_SEQUENTIALbáo sẽ đọc tuần tự (Linux tăng cửa sổ read-ahead lên gấp đôi mặc định), cònPOSIX_FADV_RANDOMbáo đọc ngẫu nhiên (Linux tắt read-ahead).18 Linux cung cấpposix_fadvise()từ nhân 2.6.1Có 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 instancePOSIX_FADV_SEQUENTIALsignals sequential reading (Linux doubles the read-ahead window), whilePOSIX_FADV_RANDOMsignals random reading (Linux disables read-ahead).18 Linux providesposix_fadvise()since kernel 2.6.1Bạn gặp nó ở đâuWhere you meet it Từ nhân 2.4, mở tệp với cờO_DIRECTcho 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 withO_DIRECTallows 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.
-
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?
-
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?
-
Tầng đệm stdio khác tầng buffer cache của nhân ra sao? Ba kiểu
_IONBF,_IOLBF,_IOFBFmặ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?
-
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? -
Cờ
O_SYNClà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?
Khi máy báo “ghi xong”, dữ liệu đã nằm chắc trên đĩa chưa?
Ghi khối lớn và ghi từng chữ, cái nào nhanh hơn? Vì sao?
Thư viện stdio làm hộ bạn việc gì?
Muốn chắc dữ liệu đã xuống đĩa thì gọi lệnh nào?
Vì sao bật O_SYNC lại làm chương trình chậm hẳn?
When
write(fd, "abc", 3)returns, where is the data, and why do we say the call is not synchronized with the disk?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?
How does the stdio buffer differ from the kernel buffer cache? Which streams default to
_IONBF,_IOLBF,_IOFBF?How do
fsync()andfdatasync()differ regarding data integrity and file integrity?What does the
O_SYNCflag 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.1Dữ 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