Chương 5 · File I/O: Các chi tiết sâu hơn Chapter 5 · File I/O: Further Details
Bên dưới bốn lời gọi: nguyên tử tính, ba bảng của nhân, và các lời gọi I/O mở rộng Đào sâu hơn về đọc–ghi tệp: làm sao để an toàn và chính xác Beneath the four calls: atomicity, the kernel's three tables, and extended I/O
Chương 4 cho ta bốn lời gọi của mô hình I/O phổ quát. Chương 5 nối tiếp để hé lộ phần tinh tế ẩn bên dưới: khái niệm atomicity (tính nguyên tử) giúp loại trừ race condition; lời gọi đa dụng fcntl() để đọc và sửa open file status flags; ba cấu trúc dữ liệu mà nhân dùng để biểu diễn file descriptor và tệp đang mở; cách nhân bản file descriptor; rồi một loạt lời gọi I/O mở rộng — pread()/pwrite(), scatter-gather, truncate(), nonblocking I/O, và I/O trên tệp lớn.1
Chương trước dạy bốn lệnh cơ bản để đọc–ghi tệp. Chương này đi sâu hơn: làm sao để nhiều chương trình cùng ghi một tệp mà không giẫm lên nhau (tính nguyên tử), làm sao đổi cách một tệp đang mở hành xử (fcntl()), và vì sao hai “con số tệp” có thể trỏ chung một chỗ. Cuối cùng là vài lệnh đọc–ghi nâng cao. Nhớ nhé: chương này là phần “chi tiết sâu” của việc đọc–ghi tệp.
Chapter 4 gave us the four calls of the universal I/O model. Chapter 5 continues by revealing the subtlety beneath them: the concept of atomicity that eliminates race conditions; the multipurpose fcntl() call for reading and modifying open file status flags; the three data structures the kernel uses to represent file descriptors and open files; how to duplicate a file descriptor; and a set of extended I/O calls — pread()/pwrite(), scatter-gather, truncate(), nonblocking I/O, and I/O on large files.1
Kerrisk mở đầu chương bằng việc mở rộng thảo luận về open() sang khái niệm atomicity — một yêu cầu cần thiết cho sự vận hành đúng đắn của nhiều system call. Sau đó ông giới thiệu fcntl(), rồi khảo sát các cấu trúc dữ liệu của nhân để làm rõ nhiều điều tinh tế của file I/O sẽ gặp lại ở các chương sau. Ông cũng bàn các lời gọi mở rộng, nonblocking I/O, phần mở rộng cho tệp lớn, thư mục /dev/fd, và các hàm thư viện tạo tệp tạm với tên duy nhất sinh ngẫu nhiên.1
Luận đề của chương: sự tinh tế thật sự của file I/O không nằm ở bốn lời gọi, mà ở bên dưới chúng — ở tính nguyên tử giúp loại bỏ race condition, ở ba bảng của nhân (file descriptor table, open file description table, i-node table) lý giải vì sao các fd lại chia sẻ hay không chia sẻ trạng thái, và ở các lời gọi mở rộng cho ta kiểm soát chính xác offset, nhiều bộ đệm, kích thước tệp và hành vi chặn.1
Ý chính: điều khó của việc đọc–ghi tệp không nằm ở bốn lệnh, mà ở những chi tiết bên dưới: làm sao để thao tác diễn ra “một mạch” không bị chen ngang, cách nhân ghi nhớ tệp đang mở, và vài lệnh nâng cao cho những tình huống đặc biệt. Nhớ nhé: hiểu phần “bên dưới” thì mới viết được chương trình tệp chắc chắn.
The chapter's thesis: the real subtlety of file I/O lies not in the four calls but beneath them — in the atomicity that eliminates race conditions, in the kernel's three tables (the file descriptor table, the open file description table, and the i-node table) that explain why descriptors do or don't share state, and in the extended calls that give precise control over offset, multiple buffers, file size, and blocking behavior.1
Cụm khái niệm 01 Concept cluster 01
Nguyên tử tính, race condition, fcntl() và open file status flags Thao tác “một mạch”, và cách đổi tính chất của tệp đang mở Atomicity, race conditions, fcntl() and open file status flags
Atomicity (tính nguyên tử) là khái niệm sẽ gặp lại nhiều lần khi bàn về system call: nhân bảo đảm mọi bước của một system call được hoàn tất như một thao tác duy nhất, không bị tiến trình hay luồng khác chen vào.1 Nhờ đó ta tránh được race condition (race hazard) — tình huống mà kết quả phụ thuộc một cách bất ngờ vào thứ tự các tiến trình giành được CPU.1 Phần thứ hai của cụm này là fcntl(), lời gọi đa dụng dùng (trong nhiều việc khác) để đọc và sửa các open file status flags.12
“Nguyên tử” nghĩa là một thao tác diễn ra trọn vẹn trong một mạch, không ai chen ngang được nửa chừng. Nếu không có tính chất này, hai chương trình chạy cùng lúc có thể giẫm chân nhau và cho kết quả sai — gọi là race condition. Chương này còn giới thiệu lệnh fcntl() để hỏi và đổi vài “tính chất” của một tệp đang mở. Nhớ nhé: nguyên tử = một mạch không bị cắt ngang; thiếu nó thì sinh race condition.
Atomicity is a concept we meet repeatedly when discussing system calls: the kernel guarantees that all the steps of a system call complete as a single operation, without being interrupted by another process or thread.1 This lets us avoid race conditions (race hazards) — situations where the result depends in an unexpected way on the order in which processes gain access to the CPU(s).1 The second part of this cluster is fcntl(), the multipurpose call used (among much else) to fetch and modify the open file status flags.12
Định nghĩa. Một race condition xảy ra khi hai (hay nhiều) tiến trình thao tác trên tài nguyên dùng chung, và kết quả cuối phụ thuộc vào thứ tự lập lịch của chúng.1 Cách chữa trong chương này là biến chuỗi “kiểm tra rồi hành động” thành một bước nguyên tử duy nhất bằng các cờ của open(). fcntl() có chữ ký int fcntl(int fd, int cmd, ... /* arg */); — giá trị trả về khi thành công phụ thuộc vào cmd, hoặc −1 khi lỗi.12
Nói đơn giản. Race condition là “ai nhanh tay hơn thì thắng”, và vì không đoán được ai nhanh nên kết quả bấp bênh. Muốn chắc chắn, ta ghép bước kiểm tra và bước làm thành một, để không ai chen vào giữa. Lệnh fcntl() thì như một “bảng điều khiển” cho tệp đang mở. Nhớ nhé: gộp “kiểm tra” với “hành động” thành một bước là cách diệt race condition.
Definition. A race condition occurs when two (or more) processes operate on a shared resource and the final outcome depends on the order in which they are scheduled.1 The remedy in this chapter is to turn a “check then act” sequence into a single atomic step using open() flags. fcntl() has the signature int fcntl(int fd, int cmd, ... /* arg */); — its return value on success depends on cmd, or −1 on error.12
/* tạo độc quyền, nguyên tử */
fd = open(p, O_WRONLY |
O_CREAT | O_EXCL, 0644);
— — —
/* đọc rồi bật thêm O_APPEND */
flags = fcntl(fd, F_GETFL);
flags |= O_APPEND;
fcntl(fd, F_SETFL, flags);
tạo mới, chắc chắn mình tạo:
O_CREAT | O_EXCL
ghi luôn nối đuôi:
O_APPEND
đọc cờ: fcntl(F_GETFL)
đổi cờ: fcntl(F_SETFL)
/* atomic exclusive create */
fd = open(p, O_WRONLY |
O_CREAT | O_EXCL, 0644);
— — —
/* read, then add O_APPEND */
flags = fcntl(fd, F_GETFL);
flags |= O_APPEND;
fcntl(fd, F_SETFL, flags);
-
atomicity & race conditions
Nguyên tử tính diệt race condition “Một mạch” để không ai chen ngang Atomicity eliminates race conditions
Tính nguyên tử thiết yếu cho sự đúng đắn của nhiều thao tác. Kerrisk minh hoạ bằng hai tình huống file I/O nơi race condition phát sinh, rồi cho thấy chúng bị loại bỏ nhờ các cờ của
open()bảo đảm nguyên tử.1 Ý cốt lõi: nếu một tiến trình bị cắt ngang giữa bước “kiểm tra trạng thái” và bước “hành động dựa trên trạng thái đó”, một tiến trình khác có thể làm trạng thái thay đổi — khiến kết luận ban đầu sai.1Hãy hình dung hai người cùng muốn đặt chỗ một bàn trống. Nếu người A nhìn thấy “bàn trống” rồi dừng lại một chút, người B có thể chen vào ngồi mất. Khi A quay lại, bàn đã có người nhưng A vẫn tưởng mình đặt được. Thao tác nguyên tử giống như “nhìn và ngồi” trong một nhịp, không ai xen vào được. Nhớ nhé: khe hở giữa “nhìn” và “làm” chính là nơi race condition chui vào.
Atomicity is essential to the correctness of many operations. Kerrisk illustrates it with two file-I/O situations where a race condition arises, then shows them eliminated through
open()flags that guarantee atomicity.1 The core idea: if a process is interrupted between “checking some state” and “acting on that state”, another process can change the state in between — making the original conclusion wrong.1Bạn gặp nó ở đâuWhere you meet it Mỗi khi hai tiến trình cùng ghi vào một tệp log chung, hay cùng muốn chắc chắn mình là người tạo ra một tệp, bạn đang đứng trước đúng loại race condition này.1 Hai chương trình cùng ghi vào một tệp nhật ký là ví dụ kinh điển của tình huống này. Whenever two processes write to a shared log file, or both want to be sure they created a file, you are facing exactly this kind of race condition.1 -
O_EXCL & O_APPEND
Hai bảo đảm nguyên tử: tạo độc quyền và ghi nối Tạo tệp chắc chắn, và ghi luôn vào cuối Two atomic guarantees: exclusive create and append
Dùng một lời gọi
open()duy nhất vớiO_CREAT | O_EXCLbảo đảm bước kiểm tra sự tồn tại của tệp và bước tạo tệp được thực hiện như một thao tác nguyên tử; nếu tệp đã có,open()thất bại vớiEEXIST.13 Tình huống thứ hai là nhiều tiến trình cùng ghi nối (append) vào một tệp: tách thànhlseek()rồiwrite()sẽ dính race; mở tệp với cờO_APPENDbảo đảm việc dời tới cuối tệp và việc ghi diễn ra nguyên tử.13Muốn chắc chắn chính mình tạo ra tệp (chưa ai có trước): mở với
O_CREAT | O_EXCL— nếu tệp đã tồn tại thì báo lỗi ngay. Muốn nhiều chương trình cùng ghi nối vào cuối một tệp mà không đè nhau: mở vớiO_APPEND, hệ thống sẽ tự dời tới cuối và ghi trong một nhịp. Nhớ nhé: O_EXCL để tạo chắc chắn, O_APPEND để ghi nối an toàn.Using a single
open()call withO_CREAT | O_EXCLguarantees that the existence check and the file creation are performed as one atomic operation; if the file already exists,open()fails withEEXIST.13 The second situation is multiple processes appending to one file: splitting it intolseek()thenwrite()is racy; opening the file with theO_APPENDflag guarantees that seeking to the end and writing happen atomically.13Bạn gặp nó ở đâuWhere you meet it Một số hệ thống tệp (ví dụ NFS) không hỗ trợO_APPEND; khi đó nhân quay về chuỗi không-nguyên-tửlseek()+write(), kéo theo nguy cơ hỏng tệp như đã mô tả.1 Trên vài hệ thống tệp mạng (như NFS), O_APPEND không hoạt động, nên ghi nối lại có thể bị đè — cần lưu ý. Some file systems (e.g. NFS) don't supportO_APPEND; the kernel then reverts to the non-atomiclseek()+write()sequence, with the attendant risk of file corruption.1 -
fcntl: F_GETFL & F_SETFL
Đọc và sửa open file status flags bằng fcntl() Hỏi và đổi “tính chất” của tệp đang mở Reading and changing status flags with fcntl()
Một công dụng của
fcntl()là lấy lại (hoặc sửa) access mode và open file status flags của một tệp đang mở — chính các giá trị đã đặt qua đối sốflagscủaopen(). Dùngcmd = F_GETFLđể lấy; để kiểm tra access mode phải che (mask) bằngO_ACCMODEvìO_RDONLY(0),O_WRONLY(1),O_RDWR(2) không ứng với từng bit riêng.12 DùngF_SETFLđể sửa, nhưng chỉ sửa được một nhóm nhỏ:O_APPEND,O_NONBLOCK,O_NOATIME,O_ASYNCvàO_DIRECT; mọi cờ khác bị bỏ qua.1210Sau khi mở tệp, bạn vẫn hỏi lại được nó đang mở ở kiểu nào và bật những công tắc gì — bằng
fcntl(fd, F_GETFL). Bạn cũng đổi được vài công tắc như “ghi nối” hay “không chặn” bằngF_SETFL, nhưng không đổi được kiểu đọc/ghi hay việc tạo tệp. Nhớ nhé: F_GETFL để xem, F_SETFL để chỉnh vài công tắc cho phép.One use of
fcntl()is to retrieve (or modify) the access mode and open file status flags of an open file — the very values set viaopen()'sflagsargument. Usecmd = F_GETFLto retrieve them; to test the access mode you must mask withO_ACCMODE, becauseO_RDONLY(0),O_WRONLY(1) andO_RDWR(2) don't correspond to single bits.12 UseF_SETFLto modify them, but only a small group can be changed:O_APPEND,O_NONBLOCK,O_NOATIME,O_ASYNCandO_DIRECT; all other flags are ignored.1210Khi nào F_SETFL là bắt buộc
Sửa status flags bằng
fcntl()đặc biệt hữu ích khi: tệp không do chính chương trình mở (ví dụ một trong ba fd chuẩn), hoặc fd đến từ system call khácopen()— nhưpipe()trả về hai đầu ống, haysocket()trả về fd socket. Với những fd đó, ta không điều khiển đượcflagslúc mở, nên phải chỉnh sau bằngF_GETFLrồiF_SETFL.12Một sai lệch của Linux
SUSv3 yêu cầu chỉ các status flags đặt lúc
open()hay quaF_SETFLvề sau mới hiện trong kết quảF_GETFL. Nhưng Linux lệch ở một điểm: nếu chương trình được biên dịch với một trong các kỹ thuật mở tệp lớn (Mục 5.10), thìO_LARGEFILEsẽ luôn xuất hiện trong các cờ lấy về bằngF_GETFL.1
Cụm khái niệm 02 Concept cluster 02
Ba bảng của nhân và nhân bản file descriptor Nhân lưu tệp đang mở thế nào, và dup() nhân bản con số tệp The kernel's three tables and duplicating file descriptors
Thoạt nhìn, dường như file descriptor và tệp đang mở tương ứng một-một. Thực tế không phải vậy: có thể — và có ích — khi nhiều file descriptor cùng trỏ tới một open file description, dù trong cùng một tiến trình hay ở các tiến trình khác nhau. Để hiểu, cần xem ba cấu trúc dữ liệu mà nhân duy trì: bảng file descriptor của từng tiến trình, bảng open file description toàn hệ thống, và bảng i-node của hệ thống tệp.1
Ta hay tưởng mỗi “con số tệp” ứng với đúng một tệp đang mở. Thật ra nhiều con số có thể trỏ chung một chỗ. Để hiểu, hãy biết nhân giữ ba bảng: bảng con số của mỗi chương trình, bảng “tệp đang mở” dùng chung cho cả máy, và bảng i-node mô tả chính tệp trên đĩa. Nhớ nhé: một con số tệp đi qua ba bảng mới tới được tệp thật.
At first glance it may appear that there is a one-to-one correspondence between a file descriptor and an open file. In fact this is not the case: it is possible — and useful — for multiple descriptors to refer to one open file description, whether within the same process or in different processes. To understand this, we examine three data structures the kernel maintains: the per-process file descriptor table, the system-wide open file description table, and the file system i-node table.1
Định nghĩa. Một open file description (bản mô tả tệp đang mở, còn gọi là open file handle) là một mục trong bảng toàn hệ thống, lưu mọi thông tin của một lần mở tệp: file offset hiện tại, các status flags (từ đối số flags), access mode, thiết lập cho signal-driven I/O, và một tham chiếu tới i-node của tệp.1 Mỗi mục trong bảng file descriptor của tiến trình lại chứa một tập cờ điều khiển fd (đúng một cờ: close-on-exec) và một tham chiếu tới open file description.1
Nói đơn giản. “Open file description” là một phiếu ghi lại một lần mở tệp: đang đọc tới đâu (offset), bật công tắc gì (status flags), mở ở kiểu nào. Con số tệp của bạn chỉ là một mũi tên trỏ tới phiếu ấy; nhiều mũi tên có thể trỏ chung một phiếu. Nhớ nhé: offset và status flags nằm ở phiếu “tệp đang mở”, không nằm ở con số tệp.
Definition. An open file description (also called an open file handle) is an entry in a system-wide table storing everything about one open instance of a file: the current file offset, the status flags (from the flags argument), the access mode, signal-driven I/O settings, and a reference to the file's i-node.1 Each entry in a process's file descriptor table holds a set of fd flags (just one: close-on-exec) and a reference to an open file description.1
fd table (mỗi tiến trình)
fd 0 ─┐
fd 20 ┴─► open file descr.
open file descr. (toàn hệ thống)
[offset | status flags | i-node*]
i-node table → tệp trên đĩa
con số tệp → mũi tên
phiếu “tệp đang mở”:
đọc tới đâu + công tắc
i-node → tệp thật trên đĩa
nhiều mũi tên → 1 phiếu
fd table (per process)
fd 0 ─┐
fd 20 ┴─► open file descr.
open file descr. (system-wide)
[offset | status flags | i-node*]
i-node table → file on disk
-
the three kernel tables
fd table → open file description → i-node Ba bảng nối con số tệp tới tệp thật fd table → open file description → i-node
Mỗi tiến trình có bảng file descriptor riêng; mỗi mục trỏ tới một open file description trong bảng toàn hệ thống; và mỗi open file description trỏ tới một i-node trong bảng i-node. i-node ghi kiểu tệp (tệp thường, socket, FIFO…), quyền hạn, con trỏ tới danh sách khoá (lock) đang giữ trên tệp, và các thuộc tính như kích thước cùng các mốc thời gian.1 Chính ba tầng này giải thích vì sao hai fd có thể dùng chung một offset, hoặc trỏ tới cùng một tệp qua hai open file description riêng.1
Hình dung ba bảng xếp chồng: bảng con số của riêng chương trình, bảng “tệp đang mở” dùng chung cả máy, và bảng i-node tả tệp thật. Đi từ con số → phiếu tệp đang mở → i-node là ra tới tệp trên đĩa. Nhờ cách xếp này mà hai con số có thể trỏ chung một phiếu. Nhớ nhé: con số → phiếu → i-node, ba chặng mới tới tệp.
Each process has its own file descriptor table; each entry points to an open file description in the system-wide table; and each open file description points to an i-node in the i-node table. The i-node records the file type (regular file, socket, FIFO…), permissions, a pointer to the list of locks held on the file, and properties such as size and timestamps.1 It is these three levels that explain why two fds can share one offset, or point at the same file through two separate open file descriptions.1
Bạn gặp nó ở đâuWhere you meet it Hình 5-2 trong sách vẽ đúng ba bảng này cho hai tiến trình: hai fd của một tiến trình trỏ chung một open file description (dodup()), hai tiến trình chia sẻ một description (dofork()), và hai description riêng trỏ chung một i-node (do hai lầnopen()).1 Khi bạn chạy một chương trình con hay nhân bản con số tệp, chính ba bảng này quyết định ai chia sẻ gì với ai. Figure 5-2 in the book draws exactly these three tables for two processes: two fds of one process sharing an open file description (viadup()), two processes sharing one description (viafork()), and two separate descriptions pointing at one i-node (via twoopen()calls).1 -
shared vs private state
Cái gì chia sẻ, cái gì riêng tư Offset thì chung, cờ close-on-exec thì riêng What is shared, and what is private
Từ mô hình ba bảng rút ra ba hệ quả. (1) Hai fd cùng trỏ một open file description chia sẻ file offset: đổi offset qua fd này (do
read(),write()haylseek()) thì fd kia cũng thấy — kể cả khi hai fd ở hai tiến trình.1 (2) Tương tự, các open file status flags (nhưO_APPEND,O_NONBLOCK,O_ASYNC) cũng được chia sẻ, và lấy/đổi được quafcntl()F_GETFL/F_SETFL.12 (3) Ngược lại, file descriptor flags (cờ close-on-exec) là riêng tư cho từng fd; đổi nó không ảnh hưởng fd khác.1Nếu hai con số trỏ chung một phiếu “tệp đang mở”: vị trí đang đọc (offset) và các công tắc trạng thái là dùng chung — một bên dời thì bên kia thấy. Nhưng cờ “đóng khi chạy chương trình mới” (close-on-exec) lại của riêng từng con số. Nhớ nhé: offset và status flags là chung; cờ close-on-exec là riêng.
The three-table model yields three implications. (1) Two fds pointing at one open file description share a file offset: changing the offset through one fd (via
read(),write()orlseek()) is visible through the other — even across two processes.1 (2) Likewise the open file status flags (such asO_APPEND,O_NONBLOCK,O_ASYNC) are shared, and retrievable/changeable viafcntl()F_GETFL/F_SETFL.12 (3) By contrast, the file descriptor flags (the close-on-exec flag) are private to each fd; changing one affects no other.1Bạn gặp nó ở đâuWhere you meet it Hai lầnopen()riêng trên cùng một tệp tạo ra hai open file description khác nhau, nên mỗi fd có offset riêng — khác hẳn trường hợpdup().1 Mở cùng một tệp hai lần bằng hai lệnh open() thì mỗi con số có vị trí đọc riêng, không ảnh hưởng nhau. Two separateopen()calls on the same file create two different open file descriptions, so each fd has its own offset — quite unlike thedup()case.1 -
dup, dup2, dup3, F_DUPFD
Nhân bản fd: dup(), dup2() và họ hàng Tạo con số tệp thứ hai trỏ cùng một chỗ Duplicating fds: dup(), dup2() and kin
dup(oldfd)tạo một fd mới trỏ tới cùng open file description vớioldfd; fd mới là số chưa dùng nhỏ nhất.14dup2(oldfd, newfd)thì buộc fd mới mang đúng sốnewfd; nếunewfdđang mở, nó được đóng lại trước (lỗi khi đóng bị bỏ qua âm thầm).14 Nếuoldfdkhông hợp lệ,dup2()thất bại vớiEBADF; còn nếuoldfdhợp lệ vàoldfd == newfd,dup2()không làm gì và trả vềnewfd.14Đây chính là cơ chế đằng sau
2>&1trong shell: shell nhân bản con số tệp để báo lỗi (2) trỏ cùng chỗ với đầu ra (1).dup()cho bạn con số trống nhỏ nhất;dup2()cho bạn chọn đúng con số muốn. Nhớ nhé: dup() lấy số nhỏ nhất, dup2() lấy đúng số bạn chỉ định.dup(oldfd)makes a new fd pointing at the same open file description asoldfd; the new fd is the lowest unused number.14dup2(oldfd, newfd)forces the new fd to be exactlynewfd; ifnewfdis open, it is closed first (an error on that close is silently ignored).14 Ifoldfdis invalid,dup2()fails withEBADF; ifoldfdis valid andoldfd == newfd,dup2()does nothing and returnsnewfd.14F_DUPFD và dup3()
fcntl(oldfd, F_DUPFD, startfd)nhân bảnoldfdthành fd chưa dùng nhỏ nhất lớn hơn hoặc bằngstartfd— tiện khi muốn fd mới rơi vào một khoảng số nhất định.dup3(oldfd, newfd, flags)giốngdup2()nhưng thêm đối sốflags; hiện hỗ trợO_CLOEXECđể bật close-on-exec cho fd mới.dup3()mới có từ Linux 2.6.27 và là đặc thù Linux.14Cái gì theo sang fd nhân bản
fd nhân bản chia sẻ cùng file offset và status flags với fd gốc (vì chung open file description), nhưng có tập cờ fd riêng, và cờ close-on-exec (
FD_CLOEXEC) của nó luôn bị tắt. Từ Linux 2.6.24 còn có thao tácF_DUPFD_CLOEXEC: nhưF_DUPFDnhưng đồng thời bật close-on-exec cho fd mới.124
Cụm khái niệm 03 Concept cluster 03
Các lời gọi I/O mở rộng: pread/pwrite, scatter-gather, truncate, nonblocking, tệp lớn Những lệnh đọc–ghi nâng cao cho tình huống đặc biệt The extended I/O calls: pread/pwrite, scatter-gather, truncate, nonblocking, large files
Phần cuối chương giới thiệu các system call mở rộng chức năng đọc–ghi quy ước. pread()/pwrite() thực hiện I/O tại một vị trí chỉ định mà không đụng tới file offset; readv()/writev() chuyển dữ liệu từ/tới nhiều bộ đệm trong một lời gọi; truncate()/ftruncate() đặt lại kích thước tệp; nonblocking I/O tránh việc bị chặn; và các phần mở rộng LFS cho phép thao tác trên tệp rất lớn.1
Phần cuối gom các lệnh đọc–ghi nâng cao: đọc–ghi ngay tại một vị trí định sẵn mà không làm xê dịch vị trí đang đọc; gửi nhiều bộ đệm trong một lần; cắt ngắn hoặc nới dài tệp; không chờ khi chưa có dữ liệu; và xử lý được tệp cực lớn. Nhớ nhé: đây là hộp đồ nghề cho những tình huống mà bốn lệnh cơ bản chưa đủ.
The final part of the chapter introduces system calls that extend conventional read/write. pread()/pwrite() perform I/O at a specified location without touching the file offset; readv()/writev() transfer data from/to multiple buffers in a single call; truncate()/ftruncate() reset a file's size; nonblocking I/O avoids blocking; and the LFS extensions allow operations on very large files.1
Định nghĩa. Scatter-gather I/O là việc đọc hoặc ghi dữ liệu qua một mảng bộ đệm trong một system call duy nhất, mô tả bởi mảng struct iovec gồm iov_base (địa chỉ bộ đệm) và iov_len (số byte).16 readv() làm scatter input — đọc một dãy byte liền mạch từ tệp rồi “rải” vào các bộ đệm; writev() làm gather output — “gom” các bộ đệm rồi ghi liền mạch ra tệp. Cả hai hoàn tất nguyên tử.16
Nói đơn giản. Thay vì gọi nhiều lần, mỗi lần một bộ đệm, scatter-gather cho phép gửi/nhận cả một danh sách bộ đệm trong một lệnh. “Scatter” là rải dữ liệu đọc được vào nhiều hộp; “gather” là gom nhiều hộp rồi ghi ra. Nhớ nhé: một lệnh, nhiều bộ đệm — gọn và nhanh hơn.
Definition. Scatter-gather I/O reads or writes data across an array of buffers in a single system call, described by an array of struct iovec holding iov_base (buffer address) and iov_len (byte count).16 readv() performs scatter input — it reads a contiguous run of bytes from the file and “scatters” them into the buffers; writev() performs gather output — it “gathers” the buffers and writes them contiguously. Both complete atomically.16
n = pread(fd, buf, len, offset);
n = pwrite(fd, buf, len, offset);
n = readv(fd, iov, iovcnt);
n = writev(fd, iov, iovcnt);
truncate(path, length);
ftruncate(fd, length);
đọc ở vị trí định sẵn → pread
ghi ở vị trí định sẵn → pwrite
nhiều bộ đệm → readv / writev
đổi kích thước tệp →
truncate / ftruncate
n = pread(fd, buf, len, offset);
n = pwrite(fd, buf, len, offset);
n = readv(fd, iov, iovcnt);
n = writev(fd, iov, iovcnt);
truncate(path, length);
ftruncate(fd, length);
-
pread() & pwrite()
I/O tại một offset chỉ định Đọc–ghi ngay tại vị trí định sẵn I/O at a specified offset
pread()vàpwrite()hoạt động y nhưread()vàwrite(), chỉ khác là I/O diễn ra tại vị tríoffsetcho sẵn chứ không phải tại file offset hiện hành — và không làm thay đổi file offset.15 Gọipread()tương đương thực hiện nguyên tử chuỗi: lưu offset hiện tại,lseek()tớioffset,read(), rồi khôi phục offset cũ. Tệp phải seekable (gọi đượclseek()).15Hai lệnh này cho bạn đọc hoặc ghi đúng tại một vị trí bạn chỉ ra, mà không làm “con trỏ đọc” của tệp bị xê dịch. Nhờ vậy, lần đọc–ghi sau vẫn tiếp tục từ chỗ cũ như chưa có gì xảy ra. Nhớ nhé: pread/pwrite nhảy tới vị trí định sẵn mà không làm lạc chỗ đang đọc.
pread()andpwrite()work just likeread()andwrite(), except that the I/O is performed at the givenoffsetrather than at the current file offset — and the file offset is left unchanged.15 Callingpread()is equivalent to atomically performing: save the current offset,lseek()tooffset,read(), then restore the old offset. The file must be seekable (one on whichlseek()is permitted).15Bạn gặp nó ở đâuWhere you meet it Rất hữu ích trong ứng dụng đa luồng: mọi luồng của một tiến trình dùng chung bảng fd, nên file offset là chung;pread()/pwrite()cho nhiều luồng cùng I/O trên một fd mà không bị offset của luồng khác làm lệch.15 Khi nhiều luồng cùng đọc–ghi một tệp, pread/pwrite giúp chúng không giẫm lên vị trí đọc của nhau. Very useful in multithreaded applications: all threads of a process share the fd table, so the file offset is shared;pread()/pwrite()let multiple threads I/O on one fd without being thrown off by another thread's offset.15 -
readv() & writev()
Scatter-gather: nhiều bộ đệm trong một lời gọi Gửi hoặc nhận nhiều bộ đệm một lần Scatter-gather: many buffers in one call
Thay vì một bộ đệm,
readv()/writev()chuyển nhiều bộ đệm qua mảngiovgồmiovcntphần tử.readv()đổ đầy từng bộ đệm theo thứ tự —iov[0]đầy xong mới sangiov[1]— và hoàn tất nguyên tử.16 Lợi thế chính là tiện lợi và tốc độ: một lời gọiwritev()rẻ hơn nhiều lời gọiwrite(), lại ghi liền mạch nên không bị các tiến trình khác chèn dữ liệu vào giữa.16 SUSv3 cho phép đặt giới hạn số phần tử quaIOV_MAX; trên Linux giá trị này là 1024.1Hai lệnh này cho bạn liệt kê một danh sách “hộp” (bộ đệm) rồi đọc hoặc ghi cả danh sách trong một nhịp. Đọc thì đổ đầy hộp đầu trước rồi mới tới hộp sau; ghi thì nối nội dung các hộp lại rồi tuôn ra. Nhanh hơn và gọn hơn gọi nhiều lần. Nhớ nhé: một danh sách hộp, một lệnh — vừa nhanh vừa liền mạch.
Instead of a single buffer,
readv()/writev()transfer multiple buffers via the arrayiovofiovcntelements.readv()fills each buffer in order —iov[0]completely beforeiov[1]— and completes atomically.16 The main advantages are convenience and speed: a singlewritev()is cheaper than multiplewrite()calls, and writes contiguously so other processes can't interleave data in between.16 SUSv3 lets an implementation cap the element count viaIOV_MAX; on Linux that value is 1024.1Bạn gặp nó ở đâuWhere you meet it Linux 2.6.30 thêmpreadv()/pwritev()— kết hợp scatter-gather với I/O tại offset chỉ định; chúng phi chuẩn nhưng cũng có trên các BSD hiện đại.16 Phiên bản preadv/pwritev gộp cả hai tiện ích: nhiều bộ đệm, lại đặt đúng vị trí định sẵn. Linux 2.6.30 addedpreadv()/pwritev()— combining scatter-gather with I/O at a specified offset; they are nonstandard but also available on the modern BSDs.16 -
truncate() & ftruncate()
Đặt lại kích thước tệp Cắt ngắn hoặc nới dài một tệp Resetting a file's size
truncate()vàftruncate()đặt kích thước tệp về đúnglength. Nếu tệp đang dài hơnlength, phần dư bị mất; nếu đang ngắn hơn, tệp được nới ra bằng cách đệm thêm một dãy byte 0 — hoặc một file hole.17 Khác biệt:truncate()nhận pathname (tệp phải truy cập được và ghi được; nếu là symbolic link thì được dereference), cònftruncate()nhận một fd đã mở để ghi và không làm đổi file offset.17Hai lệnh này đặt lại độ dài của tệp theo con số bạn đưa. Lớn hơn hiện tại thì tệp được nới ra (phần mới toàn số 0); nhỏ hơn thì phần thừa bị cắt bỏ. Một lệnh làm việc qua tên tệp, lệnh kia qua con số tệp đã mở. Nhớ nhé: truncate đặt độ dài tệp — cắt bớt hoặc kéo dài.
truncate()andftruncate()set a file's size to exactlylength. If the file is longer thanlength, the excess is lost; if it is shorter, it is extended by padding with a sequence of null bytes — or a file hole.17 The difference:truncate()takes a pathname (the file must be accessible and writable; a symbolic link is dereferenced), whileftruncate()takes an fd opened for writing and does not change the file offset.17Bạn gặp nó ở đâuWhere you meet ittruncate()là system call duy nhất có thể đổi nội dung một tệp mà không cần trước hết lấy một file descriptor quaopen()(hay cách khác).17 Điểm đặc biệt: truncate() đổi được tệp chỉ bằng tên, không cần mở tệp trước.truncate()is the only system call that can change a file's contents without first obtaining a file descriptor viaopen()(or some other means).17 -
O_NONBLOCK
Nonblocking I/O: không chờ khi chưa sẵn Không đứng chờ nếu dữ liệu chưa tới Nonblocking I/O: don't wait when not ready
Cờ
O_NONBLOCKphục vụ hai mục đích. Một, nếu không thể mở tệp ngay,open()trả về lỗi thay vì chặn (ví dụ mở một FIFO).13 Hai, sau khi mở thành công, các thao tác I/O cũng nonblocking: nếu không hoàn tất ngay được, hoặc chuyển một phần, hoặc thất bại vớiEAGAIN/EWOULDBLOCK(trên Linux hai hằng này đồng nghĩa).13 Nonblocking dùng được với thiết bị, pipe, FIFO và socket.1Bình thường, đọc khi chưa có dữ liệu thì chương trình phải đứng chờ. Bật chế độ nonblocking thì thay vì chờ, nó quay lại ngay và báo “chưa có gì”, để bạn làm việc khác rồi thử lại sau. Nhớ nhé: nonblocking = thà quay lại tay không còn hơn đứng chờ mãi.
The
O_NONBLOCKflag serves two purposes. First, if the file can't be opened immediately,open()returns an error instead of blocking (e.g. opening a FIFO).13 Second, after a successful open, I/O operations are also nonblocking: if one can't complete immediately, it either transfers partially or fails withEAGAIN/EWOULDBLOCK(on Linux these two constants are synonymous).13 Nonblocking mode works with devices, pipes, FIFOs and sockets.1Thường bị bỏ qua với tệp thường
O_NONBLOCKnói chung bị bỏ qua với tệp thường, vì buffer cache của nhân bảo đảm I/O trên tệp thường không chặn (Mục 13.1). Ngoại lệ: khi dùng mandatory file locking (Mục 55.4) thìO_NONBLOCKvẫn có hiệu lực. Về lịch sử, System V có cờO_NDELAYngữ nghĩa gần giống; trên LinuxO_NDELAYđược định nghĩa nhưng đồng nghĩa vớiO_NONBLOCK.13 -
large files · /dev/fd · temp files
Tệp lớn, /dev/fd và tệp tạm Tệp cực lớn, con số tệp dưới dạng tên, và tệp tạm Large files, /dev/fd, and temporary files
Kiểu
off_tgiữ file offset thường là số nguyên có dấu; trên kiến trúc 32-bit, điều này giới hạn kích thước tệp ở 2³¹−1 byte (2 GB). Để vượt qua, cách được khuyến nghị là định nghĩa macro_FILE_OFFSET_BITSbằng 64 khi biên dịch, biến mọi hàm và kiểu 32-bit liên quan thành phiên bản 64-bit mà không phải sửa mã nguồn; cách cũ hơn là transitional LFS API (_LARGEFILE64_SOURCE,open64(),off64_t).18 Nhân còn cung cấp thư mục ảo/dev/fd: mở/dev/fd/ntương đương nhân bản fdn.1Trên máy 32-bit, mặc định tệp không vượt quá 2 GB. Muốn làm việc với tệp lớn hơn, chỉ cần biên dịch chương trình với một tuỳ chọn đặc biệt (
_FILE_OFFSET_BITS=64). Ngoài ra có thư mục/dev/fdcho phép gọi các con số tệp đang mở như thể chúng là tên tệp. Nhớ nhé: bật _FILE_OFFSET_BITS=64 là cách gọn nhất để mở được tệp lớn.The
off_ttype that holds a file offset is typically a signed integer; on 32-bit architectures this limits file size to 2³¹−1 bytes (2 GB). To get past this, the recommended way is to define the macro_FILE_OFFSET_BITSto 64 at compile time, turning all relevant 32-bit functions and types into their 64-bit counterparts with no source changes; the older way is the transitional LFS API (_LARGEFILE64_SOURCE,open64(),off64_t).18 The kernel also provides the virtual directory/dev/fd: opening/dev/fd/nis equivalent to duplicating fdn.1Bạn gặp nó ở đâuWhere you meet it Để tạo tệp tạm với tên duy nhất,mkstemp(template)thay 6 ký tựXXXXXXcuối template, tạo tệp vớiO_EXCL(chỉ chủ sở hữu đọc/ghi) và trả về một fd;tmpfile()trả về mộtFILE*mở để đọc–ghi và tự xoá khi đóng (nóunlink()ngay sau khi mở).19 Cần một tệp nháp dùng xong bỏ? mkstemp() tạo tên không trùng; tmpfile() tạo tệp tự biến mất khi đóng. To create a uniquely named temporary file,mkstemp(template)replaces the trailing 6XXXXXXcharacters, creates the file withO_EXCL(owner read/write only) and returns an fd;tmpfile()returns aFILE*opened for reading and writing that is deleted on close (itunlink()s it right after opening).19
Bốn ngộ nhận thường gặp Four common misconceptions
“dup() tạo một tệp độc lập, có offset riêng” “dup() makes an independent file with its own offset”
Ngộ nhận: fd nhân bản là một bản sao độc lập, có vị trí đọc riêng.
Thực tế: dup()/dup2() cho fd mới trỏ chung một open file description với fd gốc, nên hai fd chia sẻ file offset và status flags; chỉ cờ fd (close-on-exec) là riêng.14
Myth: a duplicated fd is an independent copy with its own offset.
Reality: dup()/dup2() give a new fd onto one open file description shared with the original, so both fds share the file offset and status flags; only the fd flag (close-on-exec) is private.14
“lseek() rồi write() là đủ để ghi nối an toàn” “lseek() then write() is enough to append safely”
Ngộ nhận: cứ lseek() tới cuối rồi write() là ghi nối được an toàn.
Thực tế: hai bước tách rời có thể bị cắt ngang giữa chừng, sinh race condition và đè dữ liệu; muốn ghi nối nguyên tử phải mở tệp với O_APPEND.13
Myth: an lseek() to the end followed by write() is a safe append.
Reality: the two separate steps can be interrupted in between, creating a race condition that overwrites data; for atomic appends the file must be opened with O_APPEND.13
“F_SETFL đổi được mọi cờ, kể cả access mode” “F_SETFL can change any flag, even the access mode”
Ngộ nhận: dùng fcntl() F_SETFL thì đổi được mọi cờ của tệp đang mở.
Thực tế: chỉ đổi được O_APPEND, O_NONBLOCK, O_NOATIME, O_ASYNC, O_DIRECT; access mode và các creation flag không đổi được sau khi mở, mọi cờ khác bị bỏ qua.12
Myth: fcntl() F_SETFL can change any flag of an open file.
Reality: only O_APPEND, O_NONBLOCK, O_NOATIME, O_ASYNC, O_DIRECT can be changed; the access mode and creation flags cannot be changed after opening, and all other flags are ignored.12
“O_NONBLOCK khiến read() trên tệp thường trả về ngay” “O_NONBLOCK makes read() on a regular file return at once”
Ngộ nhận: bật O_NONBLOCK thì đọc tệp thường cũng thành nonblocking.
Thực tế: với tệp thường, O_NONBLOCK nói chung bị bỏ qua vì buffer cache bảo đảm I/O không chặn; nó chỉ thật sự có tác dụng với thiết bị, pipe, FIFO, socket (hoặc khi dùng mandatory locking).13
Myth: turning on O_NONBLOCK also makes reads on regular files nonblocking.
Reality: for regular files O_NONBLOCK is generally ignored, because the buffer cache ensures I/O doesn't block; it only really matters for devices, pipes, FIFOs and sockets (or with mandatory locking).13
Tự kiểm Self-check
Năm câu hỏi để biết bạn đã nắm các chi tiết sâu của file I/O Năm câu hỏi kiểm tra nhanh Five questions to check you grasped the further details of file I/O
Trả lời được những câu này nghĩa là bạn đã theo trọn mạch từ nguyên tử tính và fcntl(), qua ba bảng của nhân và dup(), tới các lời gọi I/O mở rộ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 atomicity and fcntl(), through the kernel's three tables and dup(), to the extended I/O calls.
-
Atomicity nghĩa là gì, và vì sao
O_CREAT | O_EXCLlại loại bỏ được race condition khi tạo tệp?Gợi ý sâu hơn: vì sao tách thành “kiểm tra tồn tại” rồi “tạo tệp” là không an toàn?
-
fcntl()F_GETFL/F_SETFLlàm gì, và những status flag nào đổi được sau khi mở?Gợi ý sâu hơn: vì sao phải che bằng
O_ACCMODEđể kiểm tra access mode? -
Ba bảng của nhân là gì, và chúng giải thích ra sao việc hai fd có thể chia sẻ một file offset?
Gợi ý sâu hơn: cái gì được chia sẻ giữa các fd trỏ chung một open file description, cái gì thì riêng tư?
-
dup()vàdup2()khác nhau thế nào, và fd nhân bản chia sẻ gì với fd gốc?Gợi ý sâu hơn: cờ close-on-exec của fd nhân bản mặc định bật hay tắt?
-
pread()/pwrite()vàreadv()/writev()cho ta điều gì màread()/write()thường không có?Gợi ý sâu hơn: vì sao
pread()đặc biệt hữu ích trong ứng dụng đa luồng, và vì saoO_NONBLOCKthường vô hiệu với tệp thường?
“Nguyên tử” nghĩa là gì, và vì sao nó giúp tránh lỗi khi nhiều chương trình cùng làm việc?
Lệnh
fcntl()dùng để làm gì với một tệp đang mở?Vì sao hai “con số tệp” có thể trỏ chung một chỗ và dùng chung vị trí đọc?
dup()dùng để làm gì, và nó liên quan thế nào tới2>&1trong terminal?Lệnh nào giúp đọc–ghi ngay tại một vị trí định sẵn mà không làm lạc chỗ đang đọc?
What does atomicity mean, and why does
O_CREAT | O_EXCLeliminate the race condition when creating a file?What do
fcntl()F_GETFL/F_SETFLdo, and which status flags can be changed after opening?What are the kernel's three tables, and how do they explain how two fds can share one file offset?
How do
dup()anddup2()differ, and what does a duplicated fd share with the original?What do
pread()/pwrite()andreadv()/writev()give us that ordinaryread()/write()don't?
Khép lại chương Closing the chapter
Sự tinh tế của file I/O sống bên dưới bốn lời gọi:
O_EXCLvàO_APPENDdùng tính nguyên tử để diệt race condition; ba bảng của nhân lý giải vì sao các fd chia sẻ offset và status flags nhưng giữ riêng cờ close-on-exec; và các lời gọi mở rộng —pread(),readv(),truncate()— trả lại cho người lập trình quyền kiểm soát chính xác.1Điều khó của việc đọc–ghi tệp nằm ở phần bên dưới: làm thao tác “một mạch” để khỏi giẫm chân nhau, hiểu cách nhân ghi nhớ tệp đang mở, và biết dùng vài lệnh nâng cao đúng lúc. Nhớ nhé: nắm được phần “bên dưới” này là viết được chương trình tệp vững vàng.
The subtlety of file I/O lives beneath the four calls:
O_EXCLandO_APPENDuse atomicity to kill race conditions; the kernel's three tables explain why fds share an offset and status flags yet keep the close-on-exec flag private; and the extended calls —pread(),readv(),truncate()— hand the programmer precise control.1