Computer Systems: A Programmer's Perspective Chương 10 · System-Level I/O

Chương 10 · Vào/ra mức hệ thống Chapter 10 · System-Level I/O

Mọi thiết bị là một tệp, và một nhúm lời gọi hệ thống điều khiển tất cả Trong Linux, cái gì cũng là “tệp” — kể cả màn hình và mạng Every device is a file, and a handful of system calls drive them all

Bryant và O'Hallaron mở chương bằng một ý tưởng gọn và mạnh: trong Linux, mọi thiết bị vào/ra — mạng, đĩa, thiết bị đầu cuối — đều được mô hình hoá như tệp, và mọi hoạt động nhập xuất đều thực hiện bằng cách đọc và ghi các tệp đó. Nhờ vậy nhân (kernel) có thể phơi bày một giao diện bậc thấp duy nhất, gọi là vào/ra mức hệ thống (Unix I/O), cho phép làm mọi việc nhập xuất theo một cách đồng nhất và nhất quán.1

Trong Linux có một ý tưởng rất gọn: cái gì dính tới nhập xuất — đĩa cứng, màn hình, bàn phím, cả mạng — đều được coi như một “tệp”. Muốn nói chuyện với chúng, bạn chỉ cần biết vài lệnh để mở, đọc, ghi và đóng tệp. Nhớ nhé: coi mọi thứ là tệp thì chỉ cần học một bộ lệnh là dùng được hết.

Bryant and O'Hallaron open with a compact, powerful idea: in Linux every I/O device — networks, disks, terminals — is modeled as a file, and all input and output is performed by reading and writing those files. This lets the kernel export a single low-level interface, called Unix I/O, that performs all input and output in a uniform and consistent way.1

Điểm tinh tế là sự phân tầng. Nhân hiện thực mô hình Unix I/O và phơi nó ra qua các hàm như open, close, lseek, read, write và stat; bên trên đó, cả gói RIO lẫn thư viện vào/ra chuẩn (standard I/O) đều được xây “trên lưng” các hàm Unix I/O này.1 Vì thế hiểu tầng đáy không chỉ để viết mã hệ thống: nó giải thích vì sao printf lại có bộ đệm, vì sao đọc một socket có thể trả về ít byte hơn mong đợi, và vì sao chuyển hướng ls > foo.txt trong shell lại hoạt động.1

Luận đề của chương: vì Linux coi mọi thiết bị là tệp, một tập nhỏ lời gọi hệ thống (open/read/write/close) đủ để làm mọi việc nhập xuất; nhưng read và write có thể chuyển ít byte hơn yêu cầu — gọi là đọc/ghi thiếu (short count) — nên chương trình bền vững cần các hàm bọc như gói RIO, và hiểu cách nhân dùng ba bảng để biểu diễn tệp mở sẽ giải thích việc chia sẻ tệp lẫn chuyển hướng vào/ra.1

Ý chính: mọi thiết bị là tệp, nên chỉ cần mở–đọc–ghi–đóng là xong. Nhưng có lúc lệnh đọc/ghi chỉ làm được một phần, nên chương trình tốt phải biết làm lại cho đủ. Nhớ nhé: đọc/ghi không phải lúc nào cũng xong trong một lần — phải kiểm tra và lặp lại.

The chapter's thesis: because Linux models every device as a file, a small set of system calls (open/read/write/close) is enough for all I/O; but read and write may transfer fewer bytes than requested — a short count — so robust programs need wrapper functions such as the RIO package, and understanding how the kernel uses three tables to represent open files explains both file sharing and I/O redirection.1

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

Unix I/O — tệp là một dãy byte, mỗi tệp mở có một mô tả tệp Tệp chỉ là một dãy byte, và máy nhớ nó bằng một con số Unix I/O — a file is a byte sequence, each open file has a descriptor

Một tệp Linux đơn giản là một dãy m byte: B0, B1, …, Bm−1. Mọi thiết bị vào/ra đều được mô hình hoá như tệp, nên nhập xuất quy về bốn thao tác: mở, đổi vị trí tệp hiện tại, đọc/ghi, và đóng.1 Khi mở một tệp, nhân trả về một số nguyên không âm nhỏ gọi là mô tả tệp (file descriptor); từ đó về sau ứng dụng chỉ cần giữ con số này, còn nhân giữ mọi thông tin về tệp.1

Hãy hình dung một tệp chỉ là một dãy dài các byte xếp hàng. Muốn làm việc với nó, bạn “mở” tệp ra; máy đưa lại cho bạn một con số nhỏ để sau này gọi tên tệp đó. Rồi bạn đọc, ghi, và cuối cùng đóng lại. Nhớ nhé: mở tệp xong, máy cho bạn một con số — dùng con số đó để đọc và ghi.

A Linux file is simply a sequence of m bytes: B0, B1, …, Bm−1. Because all I/O devices are modeled as files, I/O reduces to four operations: open, change the current file position, read/write, and close.1 When a file is opened, the kernel returns a small nonnegative integer called a file descriptor; from then on the application only keeps this number while the kernel keeps all information about the file.1

Định nghĩa. Mỗi tiến trình do shell tạo ra khởi đầu với ba tệp mở sẵn: vào chuẩn (standard input, mô tả tệp 0), ra chuẩn (standard output, mô tả tệp 1), và lỗi chuẩn (standard error, mô tả tệp 2). Tệp tiêu đề <unistd.h> định nghĩa các hằng STDIN_FILENO, STDOUT_FILENO, STDERR_FILENO để dùng thay cho các con số này.1

Nói đơn giản. Chương trình nào cũng bắt đầu với ba “ống” có sẵn: nơi nhận chữ gõ vào (số 0), nơi in kết quả ra (số 1), và nơi báo lỗi (số 2). Nhớ nhé: 0 là chỗ gõ vào, 1 là chỗ in ra, 2 là chỗ báo lỗi.

Definition. Each process created by a shell begins with three open files: standard input (descriptor 0), standard output (descriptor 1), and standard error (descriptor 2). The header <unistd.h> defines the constants STDIN_FILENO, STDOUT_FILENO, and STDERR_FILENO to use instead of these literal numbers.1

open(name, flags, mode) → fd
  fd = số nhỏ nhất chưa dùng
read(fd, buf, n) → #byte | 0=EOF | −1
write(fd, buf, n) → #byte | −1
close(fd) → 0 | −1

mở → được số
  đọc / ghi theo số đó
  đóng khi xong

open(name, flags, mode) → fd
  fd = smallest unused number
read(fd, buf, n) → #bytes | 0=EOF | −1
write(fd, buf, n) → #bytes | −1
close(fd) → 0 | −1

  1. file & descriptor

    Tệp và mô tả tệp — kiểu tệp, vị trí tệp, và điểm kết thúc EOF Tệp và con số đại diện cho nó File & descriptor — type, position, and the EOF condition

    Mỗi tệp có một kiểu: tệp thường (regular), thư mục (directory), socket, cùng vài kiểu khác ngoài phạm vi chương. Nhân giữ cho mỗi tệp mở một vị trí tệp (file position) k, khởi đầu bằng 0, là độ dời byte tính từ đầu tệp.1 Khi đọc tại vị trí k ≥ m (kích thước tệp), ta gặp điều kiện hết tệp (end-of-file, EOF); không có “ký tự EOF” thật sự ở cuối tệp.1

    Mỗi tệp thuộc một loại: tệp dữ liệu thường, thư mục, hay đầu nối mạng (socket). Máy luôn nhớ bạn đang đọc tới đâu trong tệp bằng một cái “dấu trang”. Khi dấu trang chạm cuối tệp thì gọi là hết tệp. Nhớ nhé: máy tự nhớ bạn đọc tới đâu; đọc hết thì nó báo “hết tệp”.

    Each file has a type: regular, directory, socket, plus a few others beyond the chapter's scope. The kernel maintains a file position k for each open file, initially 0, a byte offset from the start of the file.1 Reading at position k ≥ m (the file size) triggers the end-of-file (EOF) condition; there is no explicit “EOF character” at the end of a file.1

    Bạn gặp nó ở đâuWhere you meet it Mô tả tệp mà open trả về luôn là số nhỏ nhất chưa được tiến trình dùng — một chi tiết quan trọng khi làm chuyển hướng ở cụm sau.1 Con số máy đưa lại luôn là số nhỏ nhất còn trống — nhớ điều này khi học chuyển hướng. The descriptor open returns is always the smallest one not currently in use by the process — a detail that matters for redirection later.1

    Dòng văn bản và dấu xuống dòng

    Một tệp văn bản Linux là dãy các dòng, mỗi dòng kết thúc bằng ký tự xuống dòng ‘\n’ — chính là ký tự line feed (LF) của ASCII, giá trị 0x0a. Với nhân, không có khác biệt giữa tệp văn bản và tệp nhị phân.1

    Vì sao đóng tệp quan trọng

    Khi tiến trình kết thúc vì bất kỳ lý do gì, nhân tự đóng mọi tệp mở và giải phóng tài nguyên; nhưng đóng tường minh vẫn cần để trả mô tả tệp về kho dùng chung. Đóng một mô tả đã đóng rồi là một lỗi.1

  2. open & close

    open và close — cờ truy cập, mặt nạ quyền, và umask Mở tệp: đọc, ghi, hay tạo mới open & close — access flags, permission masks, and umask

    Lời gọi open(name, flags, mode) biến tên tệp thành mô tả tệp. Tham số flags cho biết ý định truy cập: O_RDONLY (chỉ đọc), O_WRONLY (chỉ ghi), O_RDWR (đọc và ghi), có thể OR thêm O_CREAT (tạo nếu chưa có), O_TRUNC (cắt rỗng nếu đã có), O_APPEND (ghi luôn nối vào cuối).12

    Để mở tệp, bạn nói rõ muốn làm gì: chỉ đọc, chỉ ghi, hay cả hai; có cần tạo tệp mới không; có xoá sạch nội dung cũ không; hay chỉ muốn ghi nối thêm vào cuối. Nhớ nhé: khi mở tệp, hãy nói rõ bạn định đọc hay ghi, và tệp đã có hay cần tạo mới.

    The call open(name, flags, mode) converts a filename to a descriptor. The flags argument states the access intent: O_RDONLY, O_WRONLY, O_RDWR, optionally ORed with O_CREAT (create if absent), O_TRUNC (truncate if present), O_APPEND (set position to end before each write).12

    Bạn gặp nó ở đâuWhere you meet it Khi tạo tệp mới, quyền của nó được tính bằng mode & ~umask: umask của tiến trình tắt bớt những bit quyền mà bạn không muốn bật mặc định.1 Khi một tệp mới bị thiếu quyền ghi cho người khác — đó là do umask đã tắt bớt. When a new file's permissions come out as mode & ~umask: the process's umask turns off permission bits you don't want on by default.1

    Các bit quyền

    Tham số mode đặt bit quyền truy cập, với tên biểu tượng như S_IRUSR/S_IWUSR/S_IXUSR (chủ đọc/ghi/chạy), rồi nhóm ...GRP cho nhóm và ...OTH cho mọi người — tất cả định nghĩa trong sys/stat.h.1

    ssize_t so với size_t

    Trên x86-64, size_t là unsigned long còn ssize_t (signed size) là long; read trả kiểu có dấu vì phải trả về −1 khi lỗi — và chính khả năng trả −1 này làm giảm kích thước đọc tối đa đi một nửa.1

  3. short counts

    read/write và đọc/ghi thiếu — chuyển ít byte hơn yêu cầu Đọc/ghi có khi chỉ làm được một phần read/write & short counts — fewer bytes than requested

    Ứng dụng nhập xuất bằng read và write. read(fd, buf, n) chép tối đa n byte từ vị trí hiện tại vào bộ nhớ, trả về số byte đọc được, 0 nếu EOF, −1 nếu lỗi.1 Nhưng đôi khi chúng chuyển ít hơn số byte yêu cầu — gọi là đọc/ghi thiếu (short count) — và điều này không phải lỗi.13

    Để nhập xuất, bạn dùng hai lệnh read và write. Có lúc bạn xin đọc 100 byte mà máy chỉ đưa 20 — đó không phải lỗi, chỉ là nó làm được bấy nhiêu lúc này thôi. Nhớ nhé: xin nhiều mà được ít chưa chắc là lỗi — cứ gọi lại cho đủ.

    Applications do I/O with read and write. read(fd, buf, n) copies at most n bytes from the current position into memory, returning the count read, 0 on EOF, −1 on error.1 But sometimes they transfer fewer bytes than requested — a short count — and this is not an error.13

    Bạn gặp nó ở đâuWhere you meet it Đọc/ghi thiếu xảy ra khi gặp EOF, khi đọc từng dòng từ thiết bị đầu cuối, hoặc khi đọc/ghi socket mạng và ống (pipe). Đọc tệp đĩa thì bạn sẽ không gặp short count trừ lúc EOF.1 Hay gặp nhất khi dữ liệu đến từ mạng hoặc bàn phím, chứ đọc tệp trên đĩa thì gần như không. Short counts occur on EOF, when reading lines from a terminal, or when reading/writing network sockets and pipes. On disk files you won't see them except at EOF.1

    Vì sao mạng hay gặp

    Với socket, các ràng buộc đệm bên trong và độ trễ mạng dài có thể khiến read và write trả về short count. Muốn viết ứng dụng mạng bền vững như máy chủ web, bạn phải lặp lại read/write cho tới khi chuyển đủ số byte.1

    lseek

    Ứng dụng có thể đặt lại vị trí tệp tường minh bằng lseek; sách xếp hàm này ngoài phạm vi, nhưng nó chính là cơ chế mà fseek của vào/ra chuẩn dùng bên dưới.1

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

Gói RIO, siêu dữ liệu tệp, và đọc nội dung thư mục Làm lại cho đủ, và cách xem thông tin tệp, thư mục The RIO package, file metadata, and reading directories

Để xử lý đọc/ghi thiếu một cách tự động, sách phát triển gói RIO (Robust I/O — vào/ra bền vững), cung cấp hai nhóm hàm: không đệm (unbuffered) chuyển dữ liệu thẳng giữa bộ nhớ và tệp, hợp cho dữ liệu nhị phân trên mạng; và có đệm (buffered) đọc hiệu quả từng dòng văn bản lẫn dữ liệu nhị phân từ một bộ đệm ở mức ứng dụng.1 Song song, các hàm stat/fstat lấy siêu dữ liệu (metadata) của tệp, và họ hàm readdir đọc nội dung thư mục.1

Để khỏi phải tự lo chuyện đọc/ghi thiếu, sách viết sẵn bộ hàm tên là RIO: bạn gọi một lần, nó tự lặp lại cho tới khi chuyển đủ byte. Ngoài ra còn có hàm để hỏi “tệp này to bao nhiêu, loại gì” và hàm để liệt kê các tệp trong một thư mục. Nhớ nhé: RIO lo làm lại cho đủ, còn stat cho biết thông tin về tệp.

To handle short counts automatically, the book develops the RIO (Robust I/O) package with two families: unbuffered functions that transfer directly between memory and a file, good for binary data over networks; and buffered functions that efficiently read text lines and binary data from an application-level buffer.1 Alongside, stat/fstat fetch a file's metadata, and the readdir family reads directory contents.1

Định nghĩa. rio_readn và rio_writen là phiên bản bền vững của read/write: chúng tự khởi động lại nếu bị ngắt bởi trình xử lý tín hiệu. rio_readn chỉ trả về short count khi gặp EOF; rio_writen không bao giờ trả về short count.1

Nói đơn giản. Có hai hàm “chắc tay”: một để đọc đủ, một để ghi đủ. Hàm ghi thì luôn ghi đủ số byte bạn giao. Nhớ nhé: giao bao nhiêu, hàm ghi của RIO ghi đủ bấy nhiêu.

Definition. rio_readn and rio_writen are robust versions of read/write: they restart themselves if interrupted by a signal handler. rio_readn returns a short count only on EOF; rio_writen never returns a short count.1

typedef struct {
  int rio_fd;
  int rio_cnt;
  char *rio_bufptr;
  char rio_buf[8192];
} rio_t; // RIO_BUFSIZE = 8192

bộ đệm RIO
  = tệp + số byte chưa đọc
  + ô nhớ 8192 byte

typedef struct {
  int rio_fd;
  int rio_cnt;
  char *rio_bufptr;
  char rio_buf[8192];
} rio_t; // RIO_BUFSIZE = 8192

  1. rio unbuffered

    RIO không đệm — rio_readn và rio_writen lặp tới khi đủ Hàm đọc/ghi “chắc tay” không dùng bộ đệm RIO unbuffered — rio_readn and rio_writen loop to completion

    Bên trong, cả hai hàm dùng một vòng lặp: còn byte chưa chuyển thì gọi lại read/write, cộng dồn số byte đã chuyển và dời con trỏ bộ đệm. Nếu bị ngắt bởi tín hiệu (errno là EINTR), chúng xem như chuyển 0 byte và thử lại, nhờ vậy chịu được lời gọi hệ thống bị gián đoạn.1

    Hai hàm này cứ lặp: “còn thiếu thì gọi tiếp” cho tới khi đủ. Nếu giữa chừng bị tín hiệu cắt ngang, chúng bình tĩnh thử lại thay vì báo lỗi. Nhớ nhé: cứ lặp cho tới khi đủ, bị gián đoạn thì thử lại.

    Internally both use a loop: while bytes remain, call read/write again, accumulate the transferred count, and advance the buffer pointer. If interrupted by a signal (errno is EINTR), they treat it as zero bytes and retry, so they tolerate interrupted system calls.1

    Bạn gặp nó ở đâuWhere you meet it Dùng khi đọc/ghi dữ liệu nhị phân trực tiếp từ/tới mạng, nơi short count là chuyện thường ngày.1 Hợp nhất khi gửi/nhận dữ liệu qua mạng. Used to read/write binary data directly to and from networks, where short counts are routine.1

    Gốc gác của RIO

    Các hàm RIO lấy cảm hứng từ readline, readn, writen mà W. Richard Stevens mô tả trong sách lập trình mạng kinh điển; rio_readn/rio_writen đồng nhất với readn/writen của Stevens.17

    Đan xen tự do

    Các lời gọi rio_readn và rio_writen có thể đan xen tuỳ ý trên cùng một mô tả tệp, vì chúng không giữ trạng thái đệm riêng.1

  2. rio buffered

    RIO có đệm — rio_readlineb đọc từng dòng, an toàn luồng Đọc từng dòng nhanh nhờ có bộ đệm RIO buffered — rio_readlineb reads lines, thread-safe

    Đọc từng byte một để tìm dấu xuống dòng thì tốn kém vì mỗi byte là một lần bẫy vào nhân. Thay vào đó, rio_readlineb chép một dòng từ bộ đệm nội bộ, tự gọi read để nạp lại khi bộ đệm cạn; nó đọc tối đa maxlen−1 byte, chừa chỗ cho ký tự NULL kết chuỗi. rio_readnb đọc dữ liệu nhị phân từ cùng bộ đệm đó.1

    Nếu đọc lẻ từng byte để tìm hết dòng thì rất chậm. Hàm có đệm đọc cả một mẻ lớn vào một ô nhớ rồi cắt ra từng dòng cho bạn — nhanh hơn nhiều. Nhớ nhé: đọc cả mẻ rồi cắt dòng thì nhanh hơn đọc lẻ từng byte.

    Reading one byte at a time to find a newline is costly because each byte traps into the kernel. Instead rio_readlineb copies a line from an internal buffer, calling read to refill it when empty; it reads at most maxlen−1 bytes, leaving room for the terminating NULL. rio_readnb reads binary data from the same buffer.1

    Bạn gặp nó ở đâuWhere you meet it Các hàm có đệm của RIO an toàn luồng (thread-safe) và đan xen tuỳ ý trên cùng mô tả tệp — nhưng không được trộn với rio_readn không đệm.1 Dùng khi cần đọc văn bản theo dòng, kể cả trong chương trình nhiều luồng. RIO's buffered functions are thread-safe and interleave freely on one descriptor — but must not be mixed with the unbuffered rio_readn.1

    Kích thước bộ đệm

    Bộ đệm nội bộ rio_buf có kích thước RIO_BUFSIZE = 8192 byte; cấu trúc rio_t còn giữ mô tả tệp, số byte chưa đọc (rio_cnt), và con trỏ tới byte chưa đọc kế tiếp.1

    Cải tiến so với Stevens

    readline của Stevens dùng bộ đệm tĩnh nên không an toàn luồng và không dùng chung mô tả tệp với readn; RIO sửa cả hai khiếm khuyết này bằng rio_readlineb và rio_readnb tương thích lẫn nhau và an toàn luồng.17

  3. stat & readdir

    Siêu dữ liệu và thư mục — stat, st_mode/st_size, readdir Hỏi thông tin tệp, và liệt kê thư mục Metadata & directories — stat, st_mode/st_size, readdir

    stat(filename, &buf) điền vào một cấu trúc struct stat; fstat tương tự nhưng nhận mô tả tệp. Hai thành viên hay dùng nhất: st_size (kích thước tệp tính bằng byte) và st_mode (mã hoá cả bit quyền lẫn kiểu tệp). Các vị từ như S_ISREG, S_ISDIR, S_ISSOCK cho biết kiểu tệp từ st_mode.14

    Muốn biết một tệp to bao nhiêu, thuộc loại gì, ai được đọc? Dùng stat. Muốn biết trong một thư mục có những tệp nào? Dùng readdir để đi qua từng mục một. Nhớ nhé: stat hỏi về một tệp, readdir đi qua các tệp trong thư mục.

    stat(filename, &buf) fills a struct stat; fstat is similar but takes a descriptor. The two most-used members: st_size (file size in bytes) and st_mode (encoding both permission bits and file type). Predicates like S_ISREG, S_ISDIR, S_ISSOCK read the type from st_mode.14

    Bạn gặp nó ở đâuWhere you meet it Máy chủ web dùng st_mode và st_size để biết tệp có đọc được không và dài bao nhiêu trước khi gửi cho máy khách.1 Khi một trang web gửi đúng kích thước tệp cho trình duyệt — nó vừa hỏi stat đấy. A web server uses st_mode and st_size to know whether a file is readable and how long it is before sending it to the client.1

    readdir và dirent

    opendir trả về một dòng thư mục; mỗi readdir trả con trỏ tới một struct dirent gồm d_ino (số inode) và d_name (tên tệp, mảng 256 ký tự) — hai thành viên chuẩn trên mọi hệ thống; closedir đóng dòng.16

    Bẫy báo lỗi của readdir

    Khi hết mục, readdir trả NULL; khi lỗi cũng trả NULL. Cách duy nhất phân biệt là đặt errno = 0 trước vòng lặp rồi kiểm tra xem errno có bị đổi sau lời gọi hay không.16

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

Chia sẻ tệp, chuyển hướng vào/ra, và vào/ra chuẩn Ai đang dùng tệp nào, chuyển hướng, và nên dùng hàm gì File sharing, I/O redirection, and standard I/O

Nhân biểu diễn tệp mở bằng ba bảng liên kết nhau: bảng mô tả (descriptor table) riêng cho từng tiến trình; bảng tệp mở (open file table) dùng chung cho mọi tiến trình, mỗi mục giữ vị trí tệp hiện tại, một đếm tham chiếu (reference count), và con trỏ tới bảng v-node; và bảng v-node (v-node table) cũng dùng chung, mỗi mục chứa phần lớn thông tin của struct stat gồm st_mode và st_size.1

Để nhớ ai đang mở tệp nào, máy dùng ba bảng xâu chuỗi với nhau: một bảng riêng cho mỗi chương trình, và hai bảng dùng chung cho cả máy. Nhờ chúng, máy biết hai chương trình có đang đọc cùng một tệp hay không. Nhớ nhé: ba bảng của nhân cho biết ai đang mở tệp nào và đọc tới đâu.

The kernel represents open files with three linked tables: a per-process descriptor table; a shared open file table whose entries hold the current file position, a reference count, and a pointer into the v-node table; and a shared v-node table whose entries hold most of the struct stat info including st_mode and st_size.1

Định nghĩa. Đếm tham chiếu trong một mục bảng tệp mở là số mục mô tả đang trỏ vào nó. Đóng một mô tả làm giảm đếm tham chiếu đi 1; nhân chỉ xoá mục bảng tệp mở khi đếm tham chiếu về 0.1

Nói đơn giản. Mỗi tệp mở có một bộ đếm: bao nhiêu chỗ đang trỏ vào nó. Đóng một chỗ thì bớt một; về 0 mới thật sự dọn đi. Nhớ nhé: còn ai đang dùng thì tệp chưa bị dọn.

Definition. The reference count in an open-file-table entry is the number of descriptor entries pointing at it. Closing a descriptor decrements it by 1; the kernel deletes the entry only when the count reaches 0.1

dup2(oldfd, newfd):
  chép mục oldfd → newfd
  (đóng newfd trước nếu đang mở)
ls > foo.txt ≈ dup2(fd_foo, 1)

dup2: bắt số 1 (màn hình)
  trỏ sang tệp
→ in ra là vào tệp

dup2(oldfd, newfd):
  copy entry oldfd → newfd
  (close newfd first if open)
ls > foo.txt ≈ dup2(fd_foo, 1)

  1. three tables

    Ba bảng nhân — và vì sao fork chia sẻ vị trí tệp Ba bảng, và vì sao tiến trình con dùng chung tệp Three kernel tables — and why fork shares the file position

    Thông thường, hai mô tả trỏ vào hai mục bảng tệp mở khác nhau — không chia sẻ. Nếu bạn gọi open hai lần cùng một tên tệp, mỗi mô tả có một mục bảng tệp mở riêng, và do đó một vị trí tệp riêng, nên hai lần đọc độc lập nhau.1 Nhưng sau fork, tiến trình con nhận bản sao bảng mô tả của cha và dùng chung các mục bảng tệp mở — nên dùng chung cả vị trí tệp.1

    Mở hai lần cùng một tệp thì mỗi lần có “dấu trang” riêng — đọc cái này không ảnh hưởng cái kia. Nhưng khi một chương trình đẻ ra chương trình con, hai bên dùng chung dấu trang. Nhớ nhé: mở hai lần = hai dấu trang riêng; đẻ con = dùng chung dấu trang.

    Typically two descriptors point at two different open-file-table entries — no sharing. If you call open twice on the same filename, each descriptor gets its own entry, and thus its own file position, so the two reads are independent.1 But after fork, the child gets a copy of the parent's descriptor table and shares the open-file-table entries — so it shares the file position too.1

    Bạn gặp nó ở đâuWhere you meet it Sau fork, cha và con đều phải đóng mô tả của mình thì nhân mới xoá mục bảng tệp mở — vì đếm tham chiếu khi đó là 2.1 Sau khi đẻ con, cả cha lẫn con phải đóng tệp thì nó mới thật sự được dọn. After fork, parent and child must both close their descriptors before the kernel deletes the entry — the reference count is then 2.1

    Bảng mô tả riêng, bảng tệp mở chung

    Bảng mô tả thuộc riêng mỗi tiến trình, chỉ số bằng các mô tả tệp mở của nó; còn bảng tệp mở và bảng v-node dùng chung cho mọi tiến trình. Đây là lý do fork có thể dùng chung vị trí tệp dù mỗi tiến trình có bảng mô tả riêng.1

    Vị trí tệp nằm ở đâu

    Vị trí tệp hiện tại nằm trong mục bảng tệp mở, không nằm trong bảng mô tả — nên cùng một mục bảng tệp mở nghĩa là cùng một vị trí đọc/ghi.1

  2. dup2 redirection

    Chuyển hướng vào/ra — dup2 chép mục bảng mô tả Chuyển hướng — bắt “chỗ in ra” trỏ vào tệp I/O redirection — dup2 copies a descriptor entry

    Shell cung cấp toán tử chuyển hướng: gõ ls > foo.txt khiến shell chạy ls với ra chuẩn được chuyển vào tệp foo.txt. Cơ chế là dup2(oldfd, newfd): chép mục bảng mô tả oldfd đè lên mục newfd; nếu newfd đang mở, dup2 đóng nó trước.15

    Khi bạn gõ ls > foo.txt, thay vì in lên màn hình, kết quả chảy vào tệp. Bí quyết là bắt “chỗ in ra” (số 1) trỏ sang tệp. Nhớ nhé: chuyển hướng chỉ là đổi chỗ số 1 trỏ tới — từ màn hình sang tệp.

    The shell provides redirection operators: typing ls > foo.txt runs ls with standard output redirected to foo.txt. The mechanism is dup2(oldfd, newfd): copy descriptor entry oldfd over entry newfd; if newfd was open, dup2 closes it first.15

    Bạn gặp nó ở đâuWhere you meet it Sau dup2(4,1), cả mô tả 1 lẫn 4 đều trỏ vào tệp B; tệp A bị đóng (đếm tham chiếu về 0) và mục của nó bị xoá; từ đó mọi thứ ghi ra ra chuẩn đều chảy vào tệp B.1 Máy chủ web cũng làm kiểu này khi chạy chương trình CGI cho khách. After dup2(4,1), both descriptors 1 and 4 point at file B; file A is closed (its count reaches 0) and its entry deleted; from then on everything written to standard output flows to file B.1

    Vì sao đè lên số 1

    Vì mô tả tệp 1 là ra chuẩn, nên khi chép mục của foo.txt đè lên vị trí 1, chương trình ls vẫn ghi “ra chuẩn” như thường nhưng dữ liệu lại đi vào tệp — chương trình không hề biết mình đã bị chuyển hướng.1

    CGI của máy chủ web

    Sách lưu ý máy chủ web làm một kiểu chuyển hướng tương tự khi chạy chương trình CGI thay mặt máy khách — một mảnh ghép sẽ gặp lại ở Chương 11.1

  3. standard I/O

    Vào/ra chuẩn — stream, FILE, và nên dùng hàm nào Vào/ra chuẩn, và chọn hàm nào cho đúng Standard I/O — streams, FILE, and which to use

    Ngôn ngữ C định nghĩa vào/ra chuẩn (standard I/O), một tầng cao hơn với fopen/fclose, fread/fwrite, fgets/fputs, và scanf/printf. Nó mô hình tệp mở như một stream — con trỏ tới một cấu trúc kiểu FILE gói một mô tả tệp cùng một bộ đệm stream, mục đích giống bộ đệm RIO: giảm số lời gọi hệ thống tốn kém.1

    Hầu hết lập trình viên C dùng bộ hàm bậc cao hơn: fopen, printf, scanf… Chúng tiện và có sẵn bộ đệm để chạy nhanh. Nhớ nhé: với tệp trên đĩa và màn hình, cứ dùng vào/ra chuẩn cho tiện.

    C defines standard I/O, a higher layer with fopen/fclose, fread/fwrite, fgets/fputs, and scanf/printf. It models an open file as a stream — a pointer to a FILE structure wrapping a descriptor plus a stream buffer, whose purpose, like RIO's buffer, is to minimize expensive system calls.1

    Bạn gặp nó ở đâuWhere you meet it Sách khuyến nghị: G1 — dùng vào/ra chuẩn bất cứ khi nào có thể (cho đĩa và đầu cuối); G2 — đừng dùng scanf hay rio_readlineb để đọc tệp nhị phân; G3 — dùng hàm RIO cho socket mạng.1 Quy tắc: tệp đĩa thì dùng vào/ra chuẩn; dữ liệu mạng thì dùng RIO; đừng đọc tệp nhị phân bằng hàm đọc dòng. The book advises: G1 — use standard I/O whenever possible (disk and terminal); G2 — don't use scanf or rio_readlineb on binary files; G3 — use RIO for network sockets.1

    Ba stream có sẵn

    Mỗi chương trình C bắt đầu với ba stream mở sẵn: stdin (mô tả 0), stdout (mô tả 1), stderr (mô tả 2) — song song với ba mô tả tệp chuẩn ở tầng Unix I/O.1

    Vì sao standard I/O khó cho socket

    Stream có hai hạn chế ít được ghi chép: hàm nhập không được theo ngay sau hàm xuất nếu thiếu fflush/fseek…, và ngược lại. Cách giải quyết dựa trên lseek, nhưng lseek là bất hợp lệ trên socket — nên sách khuyên dùng RIO cho mạng.1

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

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

Ngộ nhận: gọi đọc một lần là có đủ dữ liệu.

Thực tế: đọc/ghi thiếu (short count) là bình thường khi gặp EOF, đọc đầu cuối, hay socket/pipe — không phải lỗi; mã bền vững phải lặp lại cho tới khi đủ, đó chính là việc RIO làm.13

Myth: one read call yields all the data.

Reality: short counts are normal on EOF, terminals, or sockets/pipes — not errors; robust code loops until complete, which is exactly what RIO does.13

“Mở cùng tệp hai lần thì dùng chung vị trí đọc” “Opening the same file twice shares the read position”

Ngộ nhận: hai mô tả cùng tên tệp thì đọc nối tiếp nhau.

Thực tế: hai lần open tạo hai mục bảng tệp mở riêng, mỗi mục một vị trí tệp riêng; chỉ các mô tả do fork sao chép mới dùng chung vị trí.1

Myth: two descriptors of one filename read in sequence.

Reality: two open calls make two separate open-file-table entries, each with its own file position; only descriptors copied by fork share a position.1

“Vào/ra chuẩn dùng tốt cho socket mạng” “Standard I/O is fine for network sockets”

Ngộ nhận: cứ fopen socket rồi printf/scanf như tệp thường.

Thực tế: các hạn chế của stream phải nhờ lseek để lách, mà lseek lại bất hợp lệ trên socket; sách khuyên dùng hàm RIO cho mạng.1

Myth: just fopen a socket and use printf/scanf like a regular file.

Reality: stream restrictions rely on lseek to work around, yet lseek is illegal on sockets; the book recommends the RIO functions for networks.1

Tự kiểm Self-check

Năm câu hỏi để biết bạn đã nắm vào/ra mức hệ thống Năm câu hỏi kiểm tra nhanh Five questions to check you grasped system-level I/O

Trả lời được những câu này nghĩa là bạn đã hiểu mô hình Unix I/O, đọc/ghi thiếu, ba bảng nhân, và lựa chọn hàm I/O.

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

Answering these means you grasped the Unix I/O model, short counts, the three kernel tables, and choosing I/O functions.

  1. Vì sao Linux coi mọi thiết bị là tệp lại giúp đơn giản hoá nhập xuất? Ba tệp mở sẵn của một tiến trình là gì?

    Gợi ý sâu hơn: mô tả tệp mà open trả về được chọn theo quy tắc nào?

  2. Đọc/ghi thiếu (short count) là gì, và nó xảy ra trong những tình huống nào? Vì sao không coi nó là lỗi?

    Gợi ý sâu hơn: vì sao đọc tệp đĩa gần như không gặp short count, còn socket thì có?

  3. Gói RIO giải quyết vấn đề gì? Phân biệt hàm không đệm với hàm có đệm, và nêu một ràng buộc khi trộn chúng.

    Gợi ý sâu hơn: kích thước bộ đệm nội bộ RIO_BUFSIZE là bao nhiêu byte?

  4. Nhân dùng ba bảng nào để biểu diễn tệp mở? Vì sao mở một tệp hai lần lại cho hai vị trí đọc khác nhau?

    Gợi ý sâu hơn: sau fork, vì sao cha và con phải cùng đóng thì tệp mới được xoá?

  5. dup2 làm gì, và nó hiện thực ls > foo.txt ra sao? Theo sách, nên dùng hàm I/O nào cho socket mạng?

    Gợi ý sâu hơn: vì sao vào/ra chuẩn khó dùng cho socket, liên quan gì tới lseek?

  1. Trong Linux, cái gì được coi là “tệp”? Một chương trình mở sẵn những ống nào?

  2. Khi xin đọc 100 byte mà chỉ được 20, đó có phải lỗi không? Nên làm gì?

  3. Bộ hàm RIO giúp gì cho bạn? Vì sao đọc cả mẻ nhanh hơn đọc lẻ từng byte?

  4. Mở cùng một tệp hai lần thì hai bên đọc chung một chỗ hay riêng?

  5. Gõ ls > foo.txt thì kết quả đi đâu, và vì sao?

  1. Why does modeling every device as a file simplify I/O? What three files does a process start with?

  2. What is a short count, and in what situations does it occur? Why is it not an error?

  3. What problem does RIO solve? Distinguish unbuffered from buffered functions, and give one mixing restriction.

  4. Which three tables does the kernel use for open files? Why do two opens of one file give two read positions?

  5. What does dup2 do, and how does it implement ls > foo.txt? Which I/O functions does the book advise for sockets?

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

Vì Linux coi mọi thiết bị là một tệp, một nhúm lời gọi hệ thống đủ để làm mọi việc nhập xuất; nhưng đọc/ghi thiếu là chuyện bình thường, nên hãy để gói RIO lặp cho đủ, hiểu ba bảng nhân để nắm chia sẻ tệp và chuyển hướng, và dùng đúng hàm cho đúng việc.1

Trong Linux, cái gì cũng là tệp, nên chỉ cần mở–đọc–ghi–đóng. Chỉ nhớ rằng đọc/ghi có khi làm thiếu, nên phải kiểm tra và làm lại cho đủ. Nhớ nhé: coi mọi thứ là tệp, nhưng luôn kiểm tra xem đã đọc/ghi đủ chưa.

Because Linux treats every device as a file, a handful of system calls does all I/O; but short counts are normal, so let the RIO package loop to completion, understand the three kernel tables for sharing and redirection, and use the right function for the job.1