The Linux Programming Interface Chương 4 · File I/O: The Universal I/O Model

Chương 4 · File I/O: Mô hình I/O phổ quát Chapter 4 · File I/O: The Universal I/O Model

Bốn lời gọi, một mô hình: cách UNIX đọc và ghi mọi thứ Chỉ bốn lệnh để đọc và ghi mọi loại tệp Four calls, one model: how UNIX reads and writes everything

Đây là chương đầu tiên thật sự chạm vào API của system call, và Kerrisk chọn điểm khởi đầu là tệp — vì tệp nằm ở trung tâm triết lý UNIX. Ông giới thiệu khái niệm file descriptor (bộ mô tả tệp), rồi bốn lời gọi hệ thống tạo nên cái gọi là mô hình I/O phổ quát: open(), read(), write() và close().1 Điều đáng kinh ngạc: cùng bốn lời gọi ấy hoạt động trên mọi loại tệp — tệp thường, pipe, FIFO, socket, terminal, thiết bị.1

Chương này bắt đầu dạy cách ra lệnh cho máy, mà điểm khởi đầu là tệp. Bạn sẽ gặp một con số nhỏ gọi là file descriptor (như số thứ tự của tệp đang mở), rồi bốn lệnh: mở, đọc, ghi, đóng. Hay ở chỗ: bốn lệnh ấy dùng được cho mọi loại tệp, kể cả bàn phím, màn hình hay mạng. Nhớ nhé: chỉ bốn lệnh — mở, đọc, ghi, đóng — là làm việc được với mọi loại tệp.

This is the first chapter that truly touches the system-call API, and Kerrisk starts with files — because files are central to the UNIX philosophy. He introduces the notion of a file descriptor, then the four system calls that make up the so-called universal I/O model: open(), read(), write() and close().1 The striking part: these same four calls work on every kind of file — regular files, pipes, FIFOs, sockets, terminals and devices.1

Kerrisk nói rõ chương này tập trung vào I/O trên tệp thường nằm trên đĩa, nhưng phần lớn nội dung vẫn áp dụng cho các chương sau, vì cùng những system call ấy được dùng cho mọi loại tệp. Chương 5 mở rộng thêm các chi tiết của file I/O; còn chuyện đệm (buffering) phức tạp đủ để dành hẳn một chương riêng — Chương 13 bàn về I/O buffering trong nhân và trong thư viện stdio.1

Luận đề của chương: UNIX thống nhất mọi thao tác nhập/xuất quanh một trừu tượng duy nhất — file descriptor — cùng bốn lời gọi hệ thống open(), read(), write(), close(); nhờ mỗi hệ thống tệp và trình điều khiển thiết bị đều hiện thực cùng bộ lời gọi ấy, ta có tính phổ quát của I/O: viết chương trình chỉ dùng chúng thì chạy được trên bất kỳ loại tệp nào.1

Ý chính: UNIX gom mọi việc đọc/ghi về chung một kiểu: dùng một con số (file descriptor) và bốn lệnh mở–đọc–ghi–đóng. Vì mọi loại tệp đều hiểu chung bốn lệnh ấy, chương trình của bạn không cần biết mình đang nói chuyện với tệp, bàn phím hay mạng. Nhớ nhé: một cách làm việc chung cho tất cả — đó là sức mạnh của UNIX.

The chapter's thesis: UNIX unifies all input/output around a single abstraction — the file descriptor — together with the four system calls open(), read(), write(), close(); because every file system and device driver implements that same set of calls, we get universality of I/O: a program written using only them works on any type of file.1

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

File descriptor và tính phổ quát của I/O Con số chỉ tệp, và vì sao mọi tệp dùng chung bốn lệnh File descriptors and the universality of I/O

Mọi system call thực hiện I/O đều quy chiếu tới tệp đang mở bằng một file descriptor — một số nguyên (thường nhỏ) không âm. File descriptor được dùng để chỉ tới mọi loại tệp đang mở, kể cả pipe, FIFO, socket, terminal, thiết bị và tệp thường. Mỗi tiến trình (process) có tập file descriptor riêng của mình.1

Khi chương trình muốn đọc hay ghi, nó không gọi tên tệp mà dùng một con số nhỏ gọi là file descriptor — giống như số vé gửi xe chỉ đúng tệp đang mở. Con số ấy dùng được cho mọi loại tệp, và mỗi chương trình giữ bộ số riêng. Nhớ nhé: file descriptor là con số thay cho tệp đang mở.

Every system call that performs I/O refers to an open file using a file descriptor — a (usually small) nonnegative integer. File descriptors are used to refer to all types of open files, including pipes, FIFOs, sockets, terminals, devices and regular files. Each process has its own set of file descriptors.1

Định nghĩa. Universality of I/O (tính phổ quát của I/O) là một đặc trưng phân biệt của mô hình I/O UNIX: cùng bốn lời gọi — open(), read(), write(), close() — được dùng để thao tác trên mọi loại tệp, kể cả thiết bị như terminal. Nó đạt được nhờ mỗi hệ thống tệp và mỗi trình điều khiển thiết bị đều hiện thực cùng bộ lời gọi I/O ấy; chi tiết riêng của từng loại được nhân xử lý bên trong.1

Nói đơn giản. Tính phổ quát nghĩa là: dù bạn đang làm việc với tệp trên đĩa, với bàn phím hay với một thiết bị, bạn vẫn dùng đúng bốn lệnh mở–đọc–ghi–đóng. Nhân lo phần khác nhau giúp bạn. Nhớ nhé: một bộ lệnh cho tất cả, phần khác biệt để nhân lo.

Definition. Universality of I/O is a distinguishing feature of the UNIX I/O model: the same four calls — open(), read(), write(), close() — are used to perform I/O on all types of files, including devices such as terminals. It is achieved by ensuring that every file system and device driver implements that same set of I/O calls; the specifics of each type are handled within the kernel.1

fd = open(pathname, flags, mode)
n = read(fd, buffer, count)
n = write(fd, buffer, count)
status = close(fd)
— — —
0 stdin · 1 stdout · 2 stderr

mở → lấy số (fd)
đọc → read(fd, …)
ghi → write(fd, …)
đóng → close(fd)
0,1,2 = vào, ra, báo lỗi

fd = open(pathname, flags, mode)
n = read(fd, buffer, count)
n = write(fd, buffer, count)
status = close(fd)
— — —
0 stdin · 1 stdout · 2 stderr

  1. the file descriptor

    File descriptor — tay cầm để nắm một tệp đang mở File descriptor — cái tay cầm của tệp The file descriptor — a handle on an open file

    Thay vì truyền tên tệp cho từng thao tác, chương trình mở tệp một lần để nhận về một file descriptor, rồi dùng số đó cho mọi read()/write() tiếp theo. Vì là số nguyên không âm, fd rất rẻ để lưu và truyền. Cùng một fd có thể chỉ tới tệp đĩa, pipe, socket hay terminal — người lập trình thường không cần biết bên dưới là gì.1

    Mở tệp một lần, bạn được một con số. Sau đó mọi việc đọc/ghi chỉ cần đưa con số ấy ra, không cần nhắc lại tên tệp. Con số đó có thể đang chỉ tệp trên đĩa, hay bàn phím, hay kết nối mạng — cách dùng vẫn y hệt. Nhớ nhé: mở một lần, lấy số, rồi làm việc qua con số đó.

    Rather than passing a filename to each operation, a program opens a file once to obtain a file descriptor, then uses that number for every subsequent read()/write(). Being a nonnegative integer, an fd is cheap to store and pass. The same fd may refer to a disk file, a pipe, a socket or a terminal — the programmer usually need not know which lies beneath.1

    Bạn gặp nó ở đâuWhere you meet it Mỗi khi một chương trình C gọi open() và nhận về 3, rồi read(3, …), nó đang dùng đúng cơ chế này.2 Chương trình gọi mở tệp rồi nhận số 3, sau đó đọc qua số 3 — đó chính là file descriptor. Each time a C program calls open() and gets back 3, then read(3, …), it is using exactly this mechanism.2
  2. standard descriptors 0,1,2

    Ba file descriptor chuẩn — 0, 1, 2 Ba con số có sẵn: 0, 1, 2 The three standard descriptors — 0, 1, 2

    Theo quy ước, hầu hết chương trình trông đợi dùng được ba file descriptor chuẩn: 0 là standard input (stdin), 1 là standard output (stdout), 2 là standard error (stderr). Ba fd này được shell mở sẵn thay cho chương trình trước khi nó chạy; trong shell tương tác, chúng thường trỏ tới terminal.1 Trong chương trình, ta có thể dùng các số 0, 1, 2 hoặc — tốt hơn — các tên chuẩn POSIX STDIN_FILENO, STDOUT_FILENO, STDERR_FILENO định nghĩa trong <unistd.h>.1

    Khi một chương trình khởi chạy, nó đã có sẵn ba con số: 0 để nhận dữ liệu vào, 1 để in ra, 2 để báo lỗi. Shell mở sẵn ba con số này; thường chúng gắn với màn hình và bàn phím. Trong mã, nên gọi bằng tên dễ nhớ thay vì số trần. Nhớ nhé: 0 vào, 1 ra, 2 báo lỗi — có sẵn ngay khi chạy.

    By convention, most programs expect to use three standard file descriptors: 0 is standard input (stdin), 1 is standard output (stdout), 2 is standard error (stderr). These three are opened on the program's behalf by the shell before it starts; in an interactive shell they normally refer to the terminal.1 In a program we can use the numbers 0, 1, 2 or — preferably — the POSIX standard names STDIN_FILENO, STDOUT_FILENO, STDERR_FILENO defined in <unistd.h>.1

    Bạn gặp nó ở đâuWhere you meet it Khi bạn gõ ./prog > out.txt 2> err.txt, shell đổi fd 1 và 2 của tiến trình trước khi khởi chạy nó — đúng cơ chế redirection.1 Dấu “>” trong terminal chính là đổi hướng con số 1 (đầu ra) sang một tệp. When you type ./prog > out.txt 2> err.txt, the shell redirects the process's fd 1 and 2 before launching it — exactly the redirection mechanism.1
  3. ioctl: the escape hatch

    Khi mô hình phổ quát không đủ — ioctl() Việc đặc biệt nằm ngoài bốn lệnh — ioctl() When the universal model is not enough — ioctl()

    Tính phổ quát có giới hạn: một số thao tác đặc thù của thiết bị hay hệ thống tệp không nằm gọn trong bốn lời gọi. Với những trường hợp ấy, có một lời gọi “bắt-tất” là ioctl() — một cơ chế đa dụng cho các thao tác tệp và thiết bị nằm ngoài mô hình I/O phổ quát (xem Mục 4.8).17 Nhờ ioctl(), mô hình phổ quát không phải cố nhồi mọi tính năng kỳ quặc vào read()/write().1

    Bốn lệnh chung lo được hầu hết việc, nhưng vài thiết bị có chức năng riêng rất lạ (ví dụ chỉnh tốc độ cổng nối tiếp). Những việc đó giao cho một lệnh đặc biệt tên ioctl(), như một ô “việc khác” để không làm rối bốn lệnh chính. Nhớ nhé: việc lạ không hợp bốn lệnh thì gọi ioctl().

    Universality has limits: some device- or file-system-specific operations don't fit neatly into the four calls. For those, there is a catchall call, ioctl() — a general-purpose mechanism for file and device operations that fall outside the universal I/O model (see Section 4.8).17 Thanks to ioctl(), the universal model need not cram every odd feature into read()/write().1

    Chữ ký của ioctl()

    int ioctl(int fd, int request, ... /* argp */); — fd là file descriptor của thiết bị/tệp; request chọn thao tác điều khiển; đối số thứ ba argp (ký hiệu bằng dấu ba chấm của C) có thể thuộc kiểu bất kỳ, thường là con trỏ tới số nguyên hoặc cấu trúc. Giá trị trả về khi thành công phụ thuộc vào request, hoặc −1 khi lỗi.17

    Về chuẩn hoá

    SUSv3 chỉ đặc tả ioctl() cho các thao tác điều khiển thiết bị STREAMS (một tính năng của System V mà nhân Linux dòng chính không hỗ trợ). Phần lớn thao tác ioctl() khác trong sách không được SUSv3 đặc tả, nhưng ioctl() đã có trong UNIX từ rất sớm nên vẫn phổ biến rộng rãi.17

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

Mở một tệp: open() và đối số flags Mở tệp bằng open() và các công tắc flags Opening a file: open() and the flags argument

Lời gọi open() hoặc mở một tệp sẵn có, hoặc tạo và mở một tệp mới. Chữ ký của nó: int open(const char *pathname, int flags, ... /* mode_t mode */); — trả về file descriptor khi thành công, hoặc −1 khi lỗi (và đặt errno).12 Tệp cần mở được chỉ bởi pathname; nếu đó là symbolic link thì nó được dereference (lần theo).1

Lệnh open() dùng để mở một tệp có sẵn hoặc tạo tệp mới rồi mở. Bạn đưa cho nó đường dẫn tệp và một bộ “công tắc” (flags) nói rõ muốn mở để đọc, để ghi, hay tạo mới. Thành công thì nó trả về file descriptor; thất bại thì trả về −1. Nhớ nhé: open() đưa đường dẫn vào, trả con số ra; lỗi thì ra −1.

The open() call either opens an existing file or creates and opens a new one. Its signature: int open(const char *pathname, int flags, ... /* mode_t mode */); — returns a file descriptor on success, or −1 on error (setting errno).12 The file to open is identified by pathname; if it is a symbolic link, it is dereferenced.1

Định nghĩa. Đối số flags là một bit mask (mặt nạ bit). Nó luôn chứa đúng một access mode (chế độ truy cập): O_RDONLY (chỉ đọc), O_WRONLY (chỉ ghi), hoặc O_RDWR (đọc và ghi). Ngoài ra có thể OR (|) thêm các cờ tạo tệp và cờ trạng thái. Sách chia các hằng flags thành ba nhóm: file access mode flags, file creation flags, và open file status flags.12

Nói đơn giản. flags là một dãy công tắc bật/tắt gộp lại bằng dấu “|”. Bắt buộc chọn một trong ba kiểu mở: chỉ đọc, chỉ ghi, hoặc cả hai. Rồi tuỳ ý bật thêm công tắc như “tạo nếu chưa có” hay “ghi nối đuôi”. Nhớ nhé: flags = một kiểu mở bắt buộc, cộng các công tắc tuỳ chọn.

Definition. The flags argument is a bit mask. It always contains exactly one access mode: O_RDONLY (read only), O_WRONLY (write only), or O_RDWR (read and write). Additional file-creation and file-status flags may be OR-ed (|) in. The book groups the flags constants into three families: file access mode flags, file creation flags, and open file status flags.12

fd = open("f", O_RDONLY);
fd = open("f", O_WRONLY | O_CREAT
  | O_TRUNC, 0644);
fd = open("log", O_WRONLY | O_CREAT
  | O_APPEND, 0644);

chỉ đọc → O_RDONLY
tạo + ghi mới → O_WRONLY |
  O_CREAT | O_TRUNC
ghi nối đuôi → O_APPEND

fd = open("f", O_RDONLY);
fd = open("f", O_WRONLY | O_CREAT
  | O_TRUNC, 0644);
fd = open("log", O_WRONLY | O_CREAT
  | O_APPEND, 0644);

  1. access mode flags

    Access mode — đọc, ghi, hay cả hai Chọn kiểu mở: đọc, ghi, hay cả hai Access mode — read, write, or both

    Mỗi lần mở phải chọn đúng một access mode trong ba giá trị O_RDONLY, O_WRONLY, O_RDWR.12 Có một cái bẫy đáng nhớ: UNIX thời đầu dùng số 0, 1, 2 thay cho ba tên này, nhưng các bản hiện đại định nghĩa chúng không trùng cách đó, nên O_RDONLY | O_WRONLY không tương đương O_RDWR — tổ hợp đó là một lỗi logic.1

    Mở tệp thì phải nói rõ: chỉ đọc, chỉ ghi, hay vừa đọc vừa ghi — đúng một trong ba. Đừng tưởng “gộp đọc với ghi” bằng dấu “|” là ra “cả hai”; muốn cả hai phải dùng đúng cờ O_RDWR. Nhớ nhé: cần đọc-ghi thì chọn O_RDWR, đừng cộng O_RDONLY với O_WRONLY.

    Each open must choose exactly one access mode among O_RDONLY, O_WRONLY, O_RDWR.12 One memorable trap: early UNIX used 0, 1, 2 instead of these names, but modern implementations define them not to have those values, so O_RDONLY | O_WRONLY is not equivalent to O_RDWR — that combination is a logical error.1

    Bạn gặp nó ở đâuWhere you meet it Access mode có thể lấy lại sau bằng thao tác fcntl() F_GETFL; nó nằm trong nhóm cờ không đổi được sau khi mở.12 Sau khi mở, bạn vẫn hỏi lại được tệp đang mở ở kiểu nào, nhưng không đổi kiểu đó được nữa. The access mode can be retrieved later via the fcntl() F_GETFL operation; it belongs to the group of flags that cannot be changed after opening.12
  2. creation flags & mode

    O_CREAT, O_EXCL, O_TRUNC và đối số mode Tạo mới, tạo độc quyền, cắt rỗng và quyền hạn O_CREAT, O_EXCL, O_TRUNC and the mode argument

    Các file creation flags điều khiển việc tạo tệp. O_CREAT tạo tệp mới nếu chưa tồn tại — và khi dùng nó, ta phải cấp đối số mode (kiểu mode_t) quy định quyền hạn của tệp mới, nếu không quyền sẽ nhận một giá trị ngẫu nhiên từ stack.12 O_EXCL dùng chung với O_CREAT để tạo tệp độc quyền: nếu tệp đã có, open() thất bại với errno = EEXIST; việc kiểm tra và tạo diễn ra nguyên tử (atomically).12 O_TRUNC cắt một tệp thường đang có về độ dài 0, xoá sạch dữ liệu cũ.1

    Muốn tạo tệp mới thì bật O_CREAT, và phải kèm quyền hạn (ví dụ 0644). Muốn chắc chắn mình là người tạo ra nó (tệp chưa từng tồn tại) thì thêm O_EXCL — nếu đã có sẵn, lệnh báo lỗi. Muốn xoá sạch nội dung cũ để ghi mới thì bật O_TRUNC. Nhớ nhé: O_CREAT tạo mới, O_EXCL chống trùng, O_TRUNC xoá sạch.

    The file creation flags control file creation. O_CREAT creates a new file if it doesn't exist — and when used, we must supply a mode argument (type mode_t) specifying the new file's permissions, otherwise the permissions take some random value from the stack.12 O_EXCL, used with O_CREAT, creates the file exclusively: if it already exists, open() fails with errno = EEXIST; the check and creation are performed atomically.12 O_TRUNC truncates an existing regular file to length 0, destroying its old data.1

    Bạn gặp nó ở đâuWhere you meet it Tổ hợp O_CREAT | O_EXCL cũng khiến open() thất bại nếu pathname là symbolic link — một bảo vệ an ninh quan trọng cho tiến trình có đặc quyền.12 O_CREAT kèm O_EXCL còn giúp tránh bị lừa ghi đè qua một liên kết tượng trưng độc hại. The O_CREAT | O_EXCL combination also makes open() fail if pathname is a symbolic link — an important security safeguard for privileged processes.12

    Nhóm cờ trạng thái (status flags)

    Nhóm thứ ba — open file status flags — gồm các cờ như O_APPEND (luôn ghi nối vào cuối tệp), O_NONBLOCK (mở ở chế độ không chặn), O_SYNC (ghi đồng bộ). Khác với cờ tạo tệp, chúng lấy lại và sửa được sau khi mở, qua fcntl() F_GETFL/F_SETFL.12

    Vài cờ phụ thuộc phiên bản nhân

    Một số cờ gắn với mốc nhân Linux cụ thể: O_CLOEXEC (đặt cờ close-on-exec, kể từ Linux 2.6.23), O_NOATIME (không cập nhật thời điểm truy cập khi đọc, kể từ Linux 2.6.8), O_DSYNC (toàn vẹn dữ liệu I/O đồng bộ, kể từ Linux 2.6.33). O_DIRECT, O_NOFOLLOW, O_NOATIME cần định nghĩa macro kiểm tra tính năng _GNU_SOURCE.12

  3. creat() & open() errors

    creat(), fd nhỏ nhất, và lỗi từ open() Lệnh creat() cũ, số nhỏ nhất, và các lỗi hay gặp creat(), the lowest fd, and errors from open()

    UNIX thời đầu, open() chỉ có hai đối số và không tạo được tệp mới, nên có lời gọi riêng creat(pathname, mode). Ngày nay nó tương đương open(pathname, O_WRONLY | O_CREAT | O_TRUNC, mode); vì open() linh hoạt hơn nên creat() đã lỗi thời, dù vẫn gặp trong mã cũ.12 SUSv3 bảo đảm: khi thành công, open() dùng file descriptor chưa dùng có số nhỏ nhất cho tiến trình — một đặc tính cho phép ta mở tệp vào đúng một fd cụ thể (ví dụ fd 0).18

    Ngày xưa open() không tạo được tệp mới, nên có lệnh riêng creat(). Giờ open() làm được tất cả nên creat() coi như cũ. Một điều hay: khi mở, hệ thống luôn trao cho bạn con số nhỏ nhất còn trống. Nhớ nhé: dùng open() là đủ; hệ thống luôn cấp con số trống nhỏ nhất.

    In early UNIX, open() had only two arguments and couldn't create a new file, so there was a separate call, creat(pathname, mode). Today it is equivalent to open(pathname, O_WRONLY | O_CREAT | O_TRUNC, mode); since open() is more flexible, creat() is now obsolete, though still seen in older code.12 SUSv3 guarantees that on success, open() uses the lowest-numbered unused file descriptor for the process — a property that lets us open a file onto a specific fd (e.g. fd 0).18

    Bạn gặp nó ở đâuWhere you meet it Khi open() thất bại, errno cho biết lý do: EACCES (không đủ quyền), EISDIR (mở thư mục để ghi), EEXIST (O_CREAT|O_EXCL mà tệp đã có), EMFILE/ENFILE (hết hạn mức fd), ENOENT (tệp không tồn tại mà không có O_CREAT), EROFS (tệp trên hệ chỉ-đọc), ETXTBSY (mở để ghi một tệp đang chạy).12 Mở hỏng thì errno nói lý do: không có quyền, tệp không tồn tại, mở nhầm thư mục, hết số cho phép mở… When open() fails, errno tells why: EACCES (permission denied), EISDIR (opening a directory for writing), EEXIST (O_CREAT|O_EXCL but file exists), EMFILE/ENFILE (fd limits reached), ENOENT (file missing without O_CREAT), EROFS (file on a read-only system), ETXTBSY (opening a running executable for writing).12

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

read(), write(), close() và lseek() Đọc, ghi, đóng và nhảy tới vị trí trong tệp read(), write(), close() and lseek()

Ba lời gọi còn lại của mô hình phổ quát đều nhận file descriptor làm đối số đầu tiên. read() đưa dữ liệu từ tệp vào bộ đệm trong bộ nhớ; write() đưa dữ liệu từ bộ đệm ra tệp; close() giải phóng file descriptor. Thêm vào đó, lseek() không chuyển dữ liệu mà chỉ đổi file offset — vị trí nơi lần đọc/ghi kế tiếp sẽ bắt đầu.1

Còn ba lệnh nữa, đều bắt đầu bằng con số file descriptor. read() lấy dữ liệu từ tệp về bộ nhớ; write() đẩy dữ liệu từ bộ nhớ ra tệp; close() đóng tệp. Riêng lseek() không đọc ghi gì, chỉ dời “con trỏ” tới chỗ khác trong tệp. Nhớ nhé: read lấy vào, write đẩy ra, close đóng, lseek dời chỗ.

The remaining universal-model calls all take a file descriptor as their first argument. read() brings data from a file into a memory buffer; write() sends data from a buffer out to a file; close() releases the file descriptor. In addition, lseek() moves no data but changes the file offset — the position where the next read or write will begin.1

Định nghĩa. File offset (độ dời tệp), còn gọi là read-write offset hay con trỏ, là vị trí trong tệp nơi read() hoặc write() kế tiếp sẽ bắt đầu, tính theo số byte tính từ đầu tệp. Byte đầu tiên ở offset 0. Khi mở, offset đặt về 0, rồi tự động tăng theo mỗi lần đọc/ghi, nên các lần gọi liên tiếp quét tuần tự qua tệp.16

Nói đơn giản. File offset là “con trỏ” cho biết lần đọc/ghi tới sẽ bắt đầu ở đâu trong tệp, đếm từ byte số 0. Mỗi lần đọc/ghi xong, con trỏ tự nhảy tới sau phần vừa xử lý, nên bạn đi dần từ đầu tới cuối tệp. Nhớ nhé: offset là chỗ đang đứng trong tệp, tự tiến sau mỗi lần đọc/ghi.

Definition. The file offset (also called the read-write offset or pointer) is the location in a file where the next read() or write() will begin, measured in bytes from the start of the file. The first byte is at offset 0. On open, the offset is set to 0, then automatically advanced by each read or write, so successive calls progress sequentially through the file.16

ssize_t read(fd, buffer, count)
ssize_t write(fd, buffer, count)
int close(fd)
off_t lseek(fd, offset, whence)
whence: SEEK_SET / SEEK_CUR / SEEK_END

read → trả số byte đọc, 0 = hết tệp
write → trả số byte ghi
close → 0 ổn, −1 lỗi
lseek → dời con trỏ
  đầu / hiện tại / cuối

ssize_t read(fd, buffer, count)
ssize_t write(fd, buffer, count)
int close(fd)
off_t lseek(fd, offset, whence)
whence: SEEK_SET / SEEK_CUR / SEEK_END

  1. read()

    read() — đọc, nhưng có thể đọc ít hơn ta xin read() — có khi đọc ít hơn mình yêu cầu read() — reads, but may read fewer bytes than asked

    ssize_t read(int fd, void *buffer, size_t count); đọc tối đa count byte từ tệp vào buffer, trả về số byte thực sự đọc được, 0 nếu gặp cuối tệp (EOF), hoặc −1 khi lỗi.13 Một lời gọi read() có thể đọc ít hơn số byte yêu cầu — với tệp thường là vì gần cuối tệp; với pipe, socket hay terminal thì vì nhiều lý do khác (ví dụ terminal mặc định đọc tới hết dòng).13 Đáng nhớ: read() không tự thêm byte kết chuỗi \0; nếu cần, ta phải tự đặt vào.1

    Lệnh read() lấy tối đa số byte bạn xin, bỏ vào vùng nhớ bạn chỉ, rồi báo đã lấy được bao nhiêu. Nếu trả về 0 nghĩa là hết tệp. Lưu ý: nó không tự gắn dấu kết chuỗi, nên nếu coi dữ liệu là chuỗi chữ thì bạn phải tự thêm dấu \0. Nhớ nhé: read trả về số byte thật, 0 là hết; muốn thành chuỗi thì tự thêm \0.

    ssize_t read(int fd, void *buffer, size_t count); reads at most count bytes from the file into buffer, returning the number of bytes actually read, 0 on end-of-file (EOF), or −1 on error.13 A single read() may read fewer bytes than requested — for a regular file because we were near the end; for a pipe, socket or terminal for various other reasons (e.g. a terminal reads up to the next newline by default).13 Note: read() does not append a terminating \0 byte; if we need one, we must place it ourselves.1

    Bạn gặp nó ở đâuWhere you meet it Buffer phải đủ dài ít nhất count byte, vì system call không tự cấp bộ nhớ — khác với nhiều hàm thư viện.1 Bạn phải tự chuẩn bị sẵn vùng nhớ đủ chỗ trước khi gọi read. The buffer must be at least count bytes long, since the system call allocates no memory for it — unlike many library functions.1
  2. write()

    write() — ghi, nhưng thành công chưa chắc đã lên đĩa write() — ghi xong chưa chắc đã nằm trên đĩa write() — success doesn't mean it's on disk yet

    ssize_t write(int fd, void *buffer, size_t count); ghi tối đa count byte từ buffer ra tệp, trả về số byte thực sự ghi, có thể nhỏ hơn count (một partial write) — ví dụ khi đĩa đầy hoặc chạm hạn mức kích thước tệp của tiến trình.14 Một điểm then chốt: với tệp đĩa, write() trả về thành công không bảo đảm dữ liệu đã nằm trên đĩa, vì nhân đệm (buffer) các thao tác I/O đĩa để giảm truy cập đĩa và tăng tốc — chi tiết ở Chương 13.14

    write() đẩy dữ liệu từ bộ nhớ ra tệp và báo đã ghi được bao nhiêu byte (có khi ít hơn mong đợi, ví dụ đĩa đầy). Điều bất ngờ: khi nó báo “xong”, dữ liệu có thể vẫn đang nằm trong vùng đệm của nhân chứ chưa thật sự lên đĩa. Nhớ nhé: write báo xong nghĩa là đã giao cho nhân, chưa chắc đã nằm trên đĩa.

    ssize_t write(int fd, void *buffer, size_t count); writes up to count bytes from buffer to the file, returning the number of bytes actually written, which may be less than count (a partial write) — e.g. when the disk fills or the process's file-size limit is reached.14 A key point: for a disk file, a successful return from write() does not guarantee the data is on disk, because the kernel buffers disk I/O to reduce disk activity and expedite it — details in Chapter 13.14

    Bạn gặp nó ở đâuWhere you meet it Chương trình copy trong Listing 4-1 lặp read() rồi write() cho tới khi read() trả 0 — bộ khung chuẩn để sao chép mọi loại tệp.1 Chép tệp chính là: cứ đọc một khối rồi ghi khối đó, lặp cho tới khi hết. The copy program in Listing 4-1 loops read() then write() until read() returns 0 — the standard skeleton for copying any type of file.1
  3. close()

    close() — trả lại tài nguyên, và nên kiểm tra lỗi close() — nhớ đóng, và nhớ kiểm tra lỗi close() — returns the resource, and should be checked

    int close(int fd); đóng một file descriptor đang mở, giải phóng nó để tiến trình dùng lại; trả về 0 khi thành công, −1 khi lỗi.15 Khi một tiến trình kết thúc, mọi fd của nó tự động được đóng; nhưng đóng tường minh các fd không còn dùng vẫn là thói quen tốt, vì file descriptor là tài nguyên hữu hạn — nhất là với chương trình chạy lâu như shell hay máy chủ mạng.15 Như mọi system call, nên bọc close() trong kiểm tra lỗi.1

    close() đóng tệp và trả lại con số cho hệ thống dùng lại. Khi chương trình thoát, mọi tệp tự đóng, nhưng đóng sớm những cái không cần nữa vẫn tốt, vì số lượng tệp mở được là có hạn. Và nhớ kiểm tra xem đóng có lỗi không. Nhớ nhé: dùng xong thì đóng, và vẫn kiểm tra lỗi của close.

    int close(int fd); closes an open file descriptor, freeing it for reuse by the process; it returns 0 on success, −1 on error.15 When a process terminates, all its fds are closed automatically; but explicitly closing unneeded fds is good practice, since file descriptors are a finite resource — especially in long-lived programs like shells or network servers.15 As with any system call, close() should be wrapped in error checking.1

    Bạn gặp nó ở đâuWhere you meet it NFS (Network File System) là ví dụ lỗi riêng của một hệ tệp: nếu NFS commit thất bại (dữ liệu không tới được đĩa ở xa), lỗi đó được báo lại cho ứng dụng ngay tại lời gọi close().15 Với tệp mạng NFS, có khi đến lúc close mới lộ ra lỗi “dữ liệu chưa ghi được” — nên đừng bỏ qua. NFS (Network File System) gives an example of a file-system-specific error: if an NFS commit fails (the data didn't reach the remote disk), that error is reported to the application at the close() call.15
  4. lseek() & file holes

    lseek() — dời con trỏ, và chuyện “lỗ” trong tệp lseek() — nhảy chỗ, và cái “lỗ” trong tệp lseek() — moving the offset, and file holes

    off_t lseek(int fd, off_t offset, int whence); đặt lại file offset theo offset và whence: SEEK_SET tính từ đầu tệp, SEEK_CUR tương đối so với vị trí hiện tại, SEEK_END tính từ cuối tệp. Nó trả về offset mới, hoặc −1 khi lỗi.16 Lời gọi curr = lseek(fd, 0, SEEK_CUR); lấy vị trí hiện tại mà không dời. Không thể lseek() trên pipe, FIFO, socket hay terminal — khi đó lỗi với errno = ESPIPE.16

    lseek() dời con trỏ tới chỗ bạn muốn: đếm từ đầu tệp, từ chỗ đang đứng, hay từ cuối tệp. Nó chỉ chỉnh con số vị trí chứ không chạm đĩa. Lưu ý: không dùng được cho những thứ như đường ống hay bàn phím. Nhớ nhé: lseek chỉ dời vị trí trong tệp, và chỉ dùng cho tệp có thể nhảy chỗ.

    off_t lseek(int fd, off_t offset, int whence); resets the file offset according to offset and whence: SEEK_SET from the file's start, SEEK_CUR relative to the current position, SEEK_END from the end of the file. It returns the new offset, or −1 on error.16 The call curr = lseek(fd, 0, SEEK_CUR); retrieves the current position without moving it. You cannot lseek() on a pipe, FIFO, socket or terminal — doing so fails with errno = ESPIPE.16

    Bạn gặp nó ở đâuWhere you meet it Đối số offset thuộc kiểu off_t — một kiểu nguyên có dấu theo SUSv3 — nên với SEEK_CUR hay SEEK_END nó có thể âm hoặc dương; với SEEK_SET thì phải không âm.16 Vì offset có dấu, bạn có thể lùi lại (số âm) so với vị trí hiện tại hoặc so với cuối tệp. The offset argument is of type off_t — a signed integer type per SUSv3 — so with SEEK_CUR or SEEK_END it may be negative or positive; with SEEK_SET it must be nonnegative.16

    File hole — lỗ trong tệp

    Có thể lseek() vượt quá cuối tệp rồi ghi. Khoảng giữa phần cuối cũ và dữ liệu mới ghi gọi là file hole (lỗ tệp). Đọc từ lỗ trả về các byte 0 (null), nhưng lỗ không chiếm chỗ trên đĩa: hệ tệp chỉ cấp khối đĩa khi có dữ liệu thật được ghi vào. Nhờ vậy, tệp thưa (sparse) tốn ít đĩa hơn nhiều; các tệp core dump thường chứa lỗ lớn.16

    Ví dụ trong sách

    Listing 4-3 (seek_io.c) minh hoạ: sau touch tfile rồi ./seek_io tfile s100000 wabc, chương trình nhảy tới offset 100000 và ghi "abc"; đọc lại trong vùng lỗ (R5) cho ra năm byte đều bằng 0. Hầu hết hệ tệp UNIX bản xứ hỗ trợ lỗ, nhưng nhiều hệ không bản xứ (như VFAT của Microsoft) thì không.16

Bốn ngộ nhận thường gặp Four common misconceptions

“read(n) luôn trả về đủ n byte” “read(n) always returns n bytes”

Ngộ nhận: xin count byte thì chắc chắn nhận đủ count byte.

Thực tế: read() có thể trả về ít hơn — gần cuối tệp, hoặc với pipe/socket/terminal — nên phải luôn kiểm giá trị trả về; 0 nghĩa là EOF.13

Myth: asking for count bytes always yields count bytes.

Reality: read() may return fewer — near EOF, or on a pipe/socket/terminal — so always check the return value; 0 means EOF.13

“write() trả về thành công nghĩa là dữ liệu đã trên đĩa” “write() success means data is on disk”

Ngộ nhận: write() trả về là dữ liệu đã an toàn trên đĩa.

Thực tế: với tệp đĩa, nhân đệm I/O; thành công chỉ nghĩa là dữ liệu đã giao cho nhân, chưa chắc đã ghi vật lý (Chương 13 bàn cách buộc đồng bộ).14

Myth: a write() return means the data is safely on disk.

Reality: for disk files the kernel buffers I/O; success only means the data was handed to the kernel, not physically written (Chapter 13 covers forcing synchronization).14

“O_RDONLY | O_WRONLY nghĩa là đọc-ghi” “O_RDONLY | O_WRONLY means read-write”

Ngộ nhận: gộp cờ chỉ-đọc với chỉ-ghi ra chế độ đọc-ghi.

Thực tế: các bản hiện đại không định nghĩa chúng theo kiểu 0/1/2, nên tổ hợp đó là lỗi logic; muốn đọc-ghi phải dùng O_RDWR.12

Myth: OR-ing read-only with write-only gives read-write.

Reality: modern implementations don't define them as 0/1/2, so that combination is a logical error; for read-write use O_RDWR.12

“lseek() di chuyển đầu đọc/ghi của đĩa” “lseek() moves the disk's read/write head”

Ngộ nhận: lseek() khiến đĩa quay/đầu đọc dời tới vị trí.

Thực tế: lseek() chỉ chỉnh bản ghi file offset trong nhân, không gây truy cập thiết bị vật lý nào cả; nó cũng không dùng được cho pipe/terminal (ESPIPE).16

Myth: lseek() makes the disk head move to a position.

Reality: lseek() merely adjusts the kernel's record of the file offset, causing no physical device access at all; nor does it work on a pipe/terminal (ESPIPE).16

Tự kiểm Self-check

Năm câu hỏi để biết bạn đã nắm mô hình I/O phổ quát Năm câu hỏi kiểm tra nhanh Five questions to check you grasped the universal I/O model

Trả lời được những câu này nghĩa là bạn đã theo trọn mạch từ file descriptor, qua open() và đối số flags, tới bốn thao tác và lseek().

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 file descriptors, through open() and its flags, to the four operations and lseek().

  1. File descriptor là gì, và ba fd chuẩn 0, 1, 2 trỏ tới đâu khi chương trình khởi chạy?

    Gợi ý sâu hơn: tên POSIX nào trong <unistd.h> nên dùng thay cho ba số trần đó?

  2. “Tính phổ quát của I/O” nghĩa là gì, và ioctl() giữ vai trò gì trong bức tranh ấy?

    Gợi ý sâu hơn: vì sao cần một lời gọi “bắt-tất” thay vì nhồi mọi thứ vào read()/write()?

  3. Đối số flags của open() gồm những nhóm nào, và vì sao O_RDONLY | O_WRONLY là một lỗi?

    Gợi ý sâu hơn: O_CREAT | O_EXCL bảo đảm điều gì, và “nguyên tử” ở đây nghĩa là sao?

  4. Vì sao read() có thể trả về ít byte hơn yêu cầu, và vì sao write() thành công chưa chắc dữ liệu đã trên đĩa?

    Gợi ý sâu hơn: một partial write có thể xảy ra vì những lý do nào?

  5. lseek() làm gì, ba giá trị whence khác nhau ra sao, và “file hole” là gì?

    Gợi ý sâu hơn: vì sao file hole không chiếm chỗ trên đĩa, và vì sao không lseek() được trên pipe?

  1. File descriptor là gì, và ba con số 0, 1, 2 dùng để làm gì?

  2. Vì sao chỉ bốn lệnh mà làm việc được với mọi loại tệp?

  3. Lệnh nào dùng để mở tệp, và vài “công tắc” hay gặp của nó là gì?

  4. Khi read() trả về 0 thì nghĩa là gì?

  5. lseek() dùng để làm gì trong một tệp?

  1. What is a file descriptor, and where do the three standard descriptors 0, 1, 2 point when a program starts?

  2. What does “universality of I/O” mean, and what role does ioctl() play in that picture?

  3. What families make up open()'s flags argument, and why is O_RDONLY | O_WRONLY an error?

  4. Why can read() return fewer bytes than requested, and why doesn't a successful write() mean the data is on disk?

  5. What does lseek() do, how do the three whence values differ, and what is a “file hole”?

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

Để thao tác trên một tệp, trước hết phải lấy một file descriptor bằng open(); rồi dùng read() và write(); xong thì giải phóng bằng close().1 Vì mọi loại tệp và trình điều khiển thiết bị đều hiện thực cùng giao diện ấy, ta có tính phổ quát của I/O — một chương trình dùng được với bất kỳ loại tệp nào mà không cần mã riêng cho từng loại.1

Muốn làm việc với tệp: mở để lấy số, đọc, ghi, rồi đóng. Vì mọi loại tệp đều hiểu chung bốn lệnh ấy, một chương trình chạy được với mọi loại tệp. Nhớ nhé: nắm vững mở–đọc–ghi–đóng là nắm được xương sống của cả cuốn sách.

To operate on a file, first obtain a file descriptor with open(); then use read() and write(); when done, release it with close().1 Because all file types and device drivers implement that same interface, we get universality of I/O — one program works with any type of file without code specific to each.1