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
-
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àopentrả 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 descriptoropenreturns is always the smallest one not currently in use by the process — a detail that matters for redirection later.1Dò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
-
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ốflagscho biết ý định truy cập:O_RDONLY(chỉ đọc),O_WRONLY(chỉ ghi),O_RDWR(đọc và ghi), có thể OR thêmO_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. Theflagsargument states the access intent:O_RDONLY,O_WRONLY,O_RDWR, optionally ORed withO_CREAT(create if absent),O_TRUNC(truncate if present),O_APPEND(set position to end before each write).12Bạn gặp nó ở đâuWhere you meet it Khi tạo tệp mới, quyền của nó được tính bằngmode & ~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 asmode & ~umask: the process's umask turns off permission bits you don't want on by default.1Cá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...GRPcho nhóm và...OTHcho mọi người — tất cả định nghĩa trongsys/stat.h.1ssize_t so với size_t
Trên x86-64,
size_tlàunsigned longcònssize_t(signed size) làlong;readtrả 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 -
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
readvà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
readvà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
readandwrite.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.13Bạ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.1Vì 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
readvàwritetrả 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ạiread/writecho tới khi chuyển đủ số byte.1lseek
Ứ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àfseekcủ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
-
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.1Hai 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/writeagain, accumulate the transferred count, and advance the buffer pointer. If interrupted by a signal (errno isEINTR), they treat it as zero bytes and retry, so they tolerate interrupted system calls.1Bạ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.1Gốc gác của RIO
Các hàm RIO lấy cảm hứng từ
readline,readn,writenmà 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ớireadn/writencủa Stevens.17Đan xen tự do
Các lời gọi
rio_readnvàrio_writencó 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 -
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_readlinebchép một dòng từ bộ đệm nội bộ, tự gọireadđể nạp lại khi bộ đệm cạn; nó đọc tối đamaxlen−1byte, 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 đó.1Nế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_readlinebcopies a line from an internal buffer, callingreadto refill it when empty; it reads at mostmaxlen−1bytes, leaving room for the terminating NULL.rio_readnbreads binary data from the same buffer.1Bạ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ớirio_readnkhô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 unbufferedrio_readn.1Kích thước bộ đệm
Bộ đệm nội bộ
rio_bufcó kích thướcRIO_BUFSIZE= 8192 byte; cấu trúcrio_tcò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 -
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úcstruct stat;fstattươ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_ISSOCKcho biết kiểu tệp từst_mode.14Muố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ùngreaddirđể đ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 astruct stat;fstatis similar but takes a descriptor. The two most-used members:st_size(file size in bytes) andst_mode(encoding both permission bits and file type). Predicates likeS_ISREG,S_ISDIR,S_ISSOCKread the type fromst_mode.14Bạn gặp nó ở đâuWhere you meet it Máy chủ web dùngst_modevà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 usesst_modeandst_sizeto know whether a file is readable and how long it is before sending it to the client.1
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)
-
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
openhai 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 saufork, 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.1Mở 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
opentwice on the same filename, each descriptor gets its own entry, and thus its own file position, so the two reads are independent.1 But afterfork, 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.1Bạn gặp nó ở đâuWhere you meet it Saufork, 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. Afterfork, parent and child must both close their descriptors before the kernel deletes the entry — the reference count is then 2.1Bả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
-
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.txtkhiến shell chạylsvới ra chuẩn được chuyển vào tệpfoo.txt. Cơ chế làdup2(oldfd, newfd): chép mục bảng mô tảoldfdđè lên mụcnewfd; nếunewfdđang mở,dup2đóng nó trước.15Khi 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.txtrunslswith standard output redirected tofoo.txt. The mechanism isdup2(oldfd, newfd): copy descriptor entryoldfdover entrynewfd; ifnewfdwas open,dup2closes it first.15Bạn gặp nó ở đâuWhere you meet it Saudup2(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. Afterdup2(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.1Vì 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ìnhlsvẫ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.1CGI 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
-
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ểuFILEgó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.1Hầ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, andscanf/printf. It models an open file as a stream — a pointer to aFILEstructure wrapping a descriptor plus a stream buffer, whose purpose, like RIO's buffer, is to minimize expensive system calls.1Bạ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ùngscanfhayrio_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 usescanforrio_readlinebon binary files; G3 — use RIO for network sockets.1Ba 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.1Vì 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ênlseek, nhưnglseeklà 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.
-
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à
opentrả về được chọn theo quy tắc nào? -
Đọ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ó?
-
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_BUFSIZElà bao nhiêu byte? -
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á? -
dup2làm gì, và nó hiện thựcls > foo.txtra 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?
Trong Linux, cái gì được coi là “tệp”? Một chương trình mở sẵn những ống nào?
Khi xin đọc 100 byte mà chỉ được 20, đó có phải lỗi không? Nên làm gì?
Bộ hàm RIO giúp gì cho bạn? Vì sao đọc cả mẻ nhanh hơn đọc lẻ từng byte?
Mở cùng một tệp hai lần thì hai bên đọc chung một chỗ hay riêng?
Gõ
ls > foo.txtthì kết quả đi đâu, và vì sao?
Why does modeling every device as a file simplify I/O? What three files does a process start with?
What is a short count, and in what situations does it occur? Why is it not an error?
What problem does RIO solve? Distinguish unbuffered from buffered functions, and give one mixing restriction.
Which three tables does the kernel use for open files? Why do two opens of one file give two read positions?
What does
dup2do, and how does it implementls > 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