The Linux Programming Interface Chương 18 · Directories and Links

Chương 18 · Thư mục và liên kết Chapter 18 · Directories and Links

Tên không nằm trong tệp — tên sống trong thư mục, nối vào i-node bằng liên kết Tên tệp ở trong thư mục, không ở trong tệp A name does not live in the file — it lives in a directory, linked to an i-node

Chương 18 khép lại loạt chủ đề về tệp. Ông Kerrisk bắt đầu từ một sự thật dễ bị bỏ qua: i-node không chứa tên tệp. Một thư mục (directory) được lưu trên hệ thống tệp gần như y hệt một tệp thường, nhưng nó là một bảng ánh xạ các tên tệp sang các số i-node.1 Từ sự tách rời giữa tên và tệp này mọc ra toàn bộ chương: hard link, symbolic link, và bộ lời gọi để tạo, xoá, đổi tên, duyệt tệp và thư mục. Nửa sau bàn về hai thuộc tính thư mục của mỗi tiến trình — thư mục gốc (root directory) và thư mục làm việc hiện tại — và cách chúng định đoạt việc phân giải (resolve) một pathname.1

Nhiều người nghĩ tên tệp nằm bên trong tệp. Không phải. Tên tệp nằm trong thư mục — một cuốn danh bạ ghép “tên” với một con số chỉ tới tệp thật (số i-node).1 Hiểu điều đó rồi thì xoá tệp, tạo lối tắt, đổi tên… đều sáng ra. Nhớ nhé: tên tệp ở trong danh bạ (thư mục), còn tệp thật ở một nơi khác.

Chapter 18 concludes the file-related topics. Mr Kerrisk begins from a fact that is easy to miss: an i-node does not contain a filename. A directory is stored on the file system much like an ordinary file, but it is a table mapping filenames to i-node numbers.1 From this separation of name and file grows the whole chapter: hard links, symbolic links, and the system calls to create, remove, rename and scan files and directories. The second half covers two directory-related attributes of every process — its root directory and its current working directory — and how they decide the resolution of a pathname.1

Ông Kerrisk nhấn mạnh một hệ quả thực dụng của việc i-node tách khỏi tên: vì nhiều tên (trong cùng hay khác thư mục) có thể cùng trỏ tới một i-node, câu hỏi “làm sao tìm tên tệp ứng với một file descriptor?” nói chung không có lời đáp một cách khả chuyển — một descriptor trỏ tới i-node, và i-node có thể có nhiều tên, thậm chí (như ở mục 18.3) không còn tên nào.1 Toàn chương, vì vậy, là bài học về việc tách khái niệm “định danh” (i-node number) khỏi khái niệm “nhãn do con người đặt” (filename) — một thiết kế gọn nhưng gây không ít ngộ nhận.13

Luận đề của chương: vì i-node không giữ tên, một tệp chỉ là một i-node được một hay nhiều hard link (mục trong thư mục) gọi tên; điều này giải thích vì sao rm xoá tên chứ không phải tệp, vì sao symbolic link (một tệp chứa đường dẫn tới tệp khác) khác hẳn hard link, và vì sao mọi thao tác trên tệp đều quy về việc phân giải một pathname bắt đầu từ thư mục gốc hoặc thư mục làm việc hiện tại.1

Ý chính: tệp thật là i-node; cái tên chỉ là một mục trong danh bạ trỏ tới nó. Xoá tên không chắc đã xoá tệp; và có hai kiểu “liên kết”: hard link (tên trực tiếp) và symbolic link (lối tắt chứa đường đi). Nhớ nhé: một tệp có thể có nhiều tên; xoá hết tên thì tệp mới mất.

The chapter's thesis: because an i-node holds no name, a file is just an i-node named by one or more hard links (directory entries); this explains why rm removes a name rather than the file, why a symbolic link (a file holding the pathname of another file) is quite unlike a hard link, and why every file operation reduces to resolving a pathname that starts at the root directory or the current working directory.1

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

Tên và tệp — thư mục, hard link, và symbolic link Thư mục chứa tên, còn liên kết thì có hai loại Names and files — directories, hard links, and symbolic links

Một thư mục được lưu như một tệp thường, nhưng khác ở hai điểm: i-node của nó mang một kiểu tệp khác (đánh dấu là thư mục), và dữ liệu của nó là một bảng gồm cặp tên tệp + số i-node.1 Trên hầu hết hệ thống tệp Linux bản địa, tên tệp dài tối đa 255 ký tự.1 Vì i-node không chứa tên, nhiều mục thư mục có thể cùng trỏ một i-node — đó chính là các hard link.1

Thư mục giống một cuốn danh bạ: mỗi dòng ghi một cái tên và một con số chỉ tới tệp thật (số i-node). Vì tệp thật không giữ tên, nhiều dòng danh bạ có thể cùng trỏ về một tệp — ta gọi mỗi dòng đó là một hard link. Nhớ nhé: thư mục là danh bạ tên → số i-node.

A directory is stored like an ordinary file, but it differs in two ways: its i-node is marked with a different file type (a directory), and its data is a table of filename + i-node number pairs.1 On most native Linux file systems, a filename can be up to 255 characters long.1 Because the i-node holds no name, several directory entries can point to one i-node — those are hard links.1

Định nghĩa. Một hard link là một mục thư mục ánh xạ một tên sang một số i-node; mọi tên (link) của một tệp đều ngang hàng, không tên nào ưu tiên hơn tên nào.1 Một symbolic link (soft link) là một tệp kiểu đặc biệt mà dữ liệu chính là tên của một tệp khác; symbolic link không được tính vào số liên kết (link count) của tệp đích, nên khi đích bị xoá nó trở thành liên kết treo (dangling link).41

Nói đơn giản. Hard link = một cái tên trực tiếp của tệp. Symbolic link = một lối tắt, bên trong chỉ ghi “đường đi” tới tệp khác. Xoá tệp gốc thì lối tắt trỏ vào hư không. Nhớ nhé: hard link là tên thật, symbolic link là tấm biển chỉ đường.

Definition. A hard link is a directory entry mapping a name to an i-node number; all names (links) of a file are equal, none has priority.1 A symbolic link (soft link) is a special file type whose data is the name of another file; a symbolic link is not counted in the target's link count, so when the target is removed it becomes a dangling link.41

i-node 1 → bad blocks
i-node 2 → thư mục gốc "/"
mục thư mục = { tên, số i-node }
hard link: cùng i-node, cùng FS
symlink: tệp chứa "đường/đi/tới"
link count 0 + 0 fd mở ⇒ xoá tệp

thư mục = danh bạ
  tên → số i-node
hard link = tên thật
symlink = lối tắt (ghi đường đi)
xoá hết tên ⇒ mất tệp

i-node 1 → bad blocks
i-node 2 → root directory "/"
dir entry = { name, i-node # }
hard link: same i-node, same FS
symlink: file holding "a/path"
link count 0 + 0 open fd ⇒ file freed

  1. directory

    Thư mục — bảng tên và số i-node Thư mục — cuốn danh bạ của hệ tệp The directory — a table of names and i-node numbers

    Bảng i-node đánh số từ 1 (không phải 0, vì số 0 trong trường i-node của một mục thư mục nghĩa là mục đó chưa dùng). I-node 1 ghi các khối hỏng (bad blocks); thư mục gốc "/" luôn ở i-node 2, để nhân biết điểm khởi đầu khi phân giải một pathname.1 Một tiến trình không được dùng read() hay write() trực tiếp lên thư mục; nó chỉ thay đổi nội dung thư mục một cách gián tiếp qua các lời gọi như open(), link(), mkdir(), unlink().1

    Mỗi tệp có một con số định danh gọi là số i-node, đánh số từ 1. Số 2 luôn dành cho thư mục gốc "/", để máy biết bắt đầu tìm từ đâu. Bạn không gõ thẳng vào “cuốn danh bạ” được; muốn thêm/bớt dòng phải nhờ các lệnh của hệ. Nhớ nhé: danh bạ do hệ giữ, ta chỉ nhờ hệ sửa giúp.

    The i-node table is numbered from 1 (not 0, since 0 in a directory entry's i-node field means the entry is unused). I-node 1 records bad blocks; the root directory "/" is always i-node 2, so the kernel knows where to start when resolving a pathname.1 A process may not use read() or write() directly on a directory; it changes a directory's contents only indirectly, through calls such as open(), link(), mkdir() and unlink().1

    Bạn gặp nó ở đâuWhere you meet it Hình 18-1 của sách vẽ /etc/passwd: thư mục "/" (i-node 2) trỏ "etc" sang i-node 7, thư mục /etc trỏ "passwd" sang i-node 6422 — một chuỗi tra bảng.1 Mỗi khi bạn mở /etc/passwd, máy lần theo danh bạ: từ "/" tới "etc", rồi tới "passwd". The book's Figure 18-1 traces /etc/passwd: directory "/" (i-node 2) maps "etc" to i-node 7, and /etc maps "passwd" to i-node 6422 — a chain of lookups.1
  2. hard link · link count

    Hard link — nhiều tên, một i-node Hard link — một tệp, nhiều tên Hard link — many names, one i-node

    Từ shell, lệnh ln tạo thêm hard link. Trong i-node có một số liên kết (link count): mỗi tên mới làm nó tăng 1, mỗi lần gỡ tên làm nó giảm 1; i-node và các khối dữ liệu chỉ được giải phóng khi số này về 0.1 Hard link có hai giới hạn: phải nằm cùng một hệ thống tệp (vì số i-node chỉ duy nhất trong một hệ tệp), và không được trỏ tới thư mục (để tránh vòng lặp làm rối nhiều chương trình).21

    Lệnh ln a b cho tệp "a" thêm một tên là "b" — hai tên, một tệp. Máy đếm xem tệp có bao nhiêu tên; còn tên thì tệp còn sống. Nhưng hard link phải ở cùng một ổ đĩa (hệ tệp), và không được dùng cho thư mục. Nhớ nhé: thêm hard link là thêm tên, không phải chép tệp.

    From the shell, the ln command creates extra hard links. The i-node holds a link count: each new name raises it by 1, each removed name lowers it by 1; the i-node and its data blocks are freed only when it reaches 0.1 Hard links have two limits: they must reside on the same file system (i-node numbers are unique only within one file system), and they cannot point to a directory (to prevent loops that would confuse many programs).21

    Bạn gặp nó ở đâuWhere you meet it Cột thứ ba của ls -li chính là số liên kết; sau ln abc xyz, số ấy của tệp lên 2 vì giờ có hai tên trỏ cùng i-node.1 Gõ ls -li, con số ở cột thứ ba cho biết tệp đang có mấy cái tên. The third column of ls -li is the link count; after ln abc xyz it rises to 2, since two names now point to the same i-node.1
  3. symbolic link · dangling link

    Symbolic link — một tệp chứa đường dẫn Symbolic link — lối tắt có thể treo Symbolic link — a file holding a pathname

    Symbolic link ra đời ở 4.2BSD; không có trong POSIX.1-1990 nhưng được đưa vào SUSv1 và còn trong SUSv3.1 Vì nó trỏ bằng tên chứ không bằng số i-node, nó vượt được cả hai giới hạn của hard link: liên kết sang hệ tệp khác, và trỏ vào thư mục.1 SUSv3 đòi hiện thực cho phép giải (dereference) ít nhất _POSIX_SYMLOOP_MAX = 8 cấp; trước nhân 2.6.18 Linux giới hạn 5 cấp, từ 2.6.18 theo mức tối thiểu 8 của SUSv3 và áp trần tổng cộng 40 lần giải cho một pathname, nhằm chặn vòng lặp và tràn ngăn xếp trong nhân.41

    Symbolic link là một tệp nhỏ, bên trong chỉ ghi đường đi tới tệp khác — như tấm biển "đi lối này". Nó có thể trỏ sang ổ khác, trỏ vào thư mục, và nếu đích biến mất thì nó "treo" (dangling). Máy sẽ không đi theo biển chỉ đường mãi mãi: có giới hạn số lần. Nhớ nhé: symbolic link chỉ là tấm biển, không phải tệp thật.

    Symbolic links appeared in 4.2BSD; they were not in POSIX.1-1990 but were incorporated into SUSv1 and remain in SUSv3.1 Because it points by name, not by i-node number, it overcomes both limits of hard links: it can link across file systems and point to directories.1 SUSv3 requires an implementation to allow dereferencing at least _POSIX_SYMLOOP_MAX = 8 levels; before kernel 2.6.18 Linux imposed a limit of 5, and from 2.6.18 it follows the SUSv3 minimum of 8 and caps the total at 40 dereferences per pathname, to prevent loops and kernel stack overflow.41

    Tối ưu lưu trong i-node

    Nếu chuỗi đường dẫn đủ ngắn để nhét vào phần i-node vốn dành cho con trỏ khối dữ liệu, ext2/ext3/ext4 lưu luôn chuỗi ở đó — khỏi cấp khối đĩa và đọc nhanh hơn. Tác giả kiểm 20.700 symbolic link trên một hệ: 97% dài từ 60 byte trở xuống.1

    Quyền của symbolic link bị bỏ qua

    Symbolic link luôn được tạo với mọi quyền bật; quyền và quyền sở hữu của nó bị bỏ qua cho hầu hết thao tác — thứ quyết định là quyền của tệp đích. Quyền sở hữu của chính link chỉ đáng kể khi xoá hoặc đổi tên nó trong thư mục có sticky bit.1

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

Các thao tác — tạo, xoá, đổi tên, và duyệt Những lệnh để tạo, xoá, đổi tên và liệt kê The verbs — create, remove, rename, and scan

Đã hiểu tên tách khỏi tệp, bộ lời gọi trở nên rất logic: link()/unlink() thêm và gỡ hard link; rename() chỉ dời một mục thư mục; symlink()/readlink() tạo và đọc lối tắt; mkdir()/rmdir()/remove() lo thư mục; còn opendir()/readdir() và nftw() dùng để duyệt nội dung.1

Hiểu "tên khác tệp" rồi thì mấy lệnh sau dễ nhớ: thêm/bớt tên, đổi tên, tạo lối tắt, tạo/xoá thư mục, và đọc xem trong thư mục có gì. Nhớ nhé: phần lớn các lệnh này chỉ sửa danh bạ tên.

Once you grasp that a name is separate from a file, the calls become very logical: link()/unlink() add and remove hard links; rename() merely moves a directory entry; symlink()/readlink() create and read a soft link; mkdir()/rmdir()/remove() handle directories; and opendir()/readdir() plus nftw() scan their contents.1

Định nghĩa. link(oldpath, newpath) tạo một hard link mới tên newpath cho tệp đã có oldpath; nếu newpath đã tồn tại thì không ghi đè mà báo lỗi EEXIST.2 unlink(pathname) gỡ một tên và, nếu đó là tên cuối, xoá luôn tệp — nhưng không xoá được thư mục (việc đó dành cho rmdir() hay remove()).3

Nói đơn giản. link = thêm một cái tên cho tệp. unlink = bỏ một cái tên; bỏ tên cuối cùng thì tệp mới mất. Muốn xoá thư mục phải dùng lệnh khác. Nhớ nhé: unlink gỡ tên, chưa chắc xoá tệp.

Definition. link(oldpath, newpath) creates a new hard link named newpath for the existing file oldpath; if newpath already exists it is not overwritten — an EEXIST error results.2 unlink(pathname) removes a name and, if it is the last one, deletes the file too — but it cannot remove a directory (that is for rmdir() or remove()).3

link(old,new) newpath tồn tại ⇒ EEXIST
unlink(path) gỡ tên; đích thư mục ⇒ EISDIR*
rename(old,new) chỉ dời mục; khác FS ⇒ EXDEV
symlink(f,link) readlink → KHÔNG thêm '\0'
mkdir → tạo . và .. ; rmdir cần rỗng
remove = file?unlink : rmdir

link = thêm tên
unlink = bỏ tên
rename = đổi/chuyển tên
symlink= tạo lối tắt
mkdir/rmdir = tạo/xoá thư mục

link(old,new) newpath exists ⇒ EEXIST
unlink(path) remove name; on dir ⇒ EISDIR*
rename(old,new) moves entry; cross-FS ⇒ EXDEV
symlink(f,link) readlink → NO trailing '\0'
mkdir → makes . and .. ; rmdir needs empty
remove = file?unlink : rmdir

  1. link() · unlink()

    link()/unlink() — và mẹo "tệp sống đến khi đóng" Thêm tên, bỏ tên, và tệp chưa mất ngay link()/unlink() — and "alive until closed"

    Ngoài số liên kết, nhân còn đếm số file descriptor đang mở của mỗi i-node. Nếu tên cuối bị gỡ nhưng còn tiến trình giữ descriptor mở, tệp chưa bị xoá cho tới khi mọi descriptor đóng.1 Đây là nền của một mẹo kinh điển: tạo tệp tạm, unlink() ngay lập tức, rồi vẫn dùng qua descriptor — tệp sẽ tự huỷ khi chương trình kết thúc (đúng như tmpfile() làm).3

    Hệ đếm cả số tên lẫn số chương trình đang mở tệp. Gỡ hết tên nhưng vẫn còn người đang mở thì tệp chưa mất — đợi đóng xong mới mất. Nhờ vậy có mẹo: tạo tệp tạm rồi xoá tên ngay, dùng tiếp cho tới khi thoát là tệp tự dọn. Nhớ nhé: tệp mất khi hết tên VÀ hết người đang mở.

    Besides the link count, the kernel also counts open file descriptors for each i-node. If the last name is removed but some process still holds an open descriptor, the file is not deleted until all descriptors are closed.1 This underlies a classic trick: create a temporary file, unlink() it immediately, yet keep using it via the descriptor — the file self-destructs when the program exits (exactly what tmpfile() does).3

    Bạn gặp nó ở đâuWhere you meet it Listing 18-1 của sách mở tệp, unlink ngay, ghi dữ liệu, rồi dùng df cho thấy dung lượng chỉ được trả lại sau khi close().1 Trên Linux, thư mục /proc/PID/fd cho thấy một tiến trình đang mở những tệp nào — kể cả tệp đã bị gỡ tên. The book's Listing 18-1 opens a file, unlinks it at once, writes data, then uses df to show the space is reclaimed only after close().1
  2. rename()

    rename() — chỉ dời một mục thư mục rename() — đổi tên mà không chép dữ liệu rename() — just moves a directory entry

    rename(oldpath, newpath) vừa đổi tên vừa chuyển tệp sang thư mục khác trên cùng hệ tệp; nó chỉ sửa các mục thư mục, không dời dữ liệu, không đổi số liên kết, và không động tới descriptor đang mở.6 Nếu newpath đã có thì bị ghi đè; nếu hai đường trỏ cùng một tệp thì không làm gì cả; đổi tên qua hệ tệp khác thất bại với lỗi EXDEV (khi đó phải sao chép rồi xoá — đúng việc lệnh mv làm).61

    rename đổi tên hoặc chuyển tệp sang thư mục khác — nhưng chỉ trong cùng một ổ. Nó chỉ sửa danh bạ, không bê dữ liệu đi, nên rất nhanh. Chuyển sang ổ khác thì không được, phải chép rồi xoá. Nhớ nhé: đổi tên là sửa danh bạ, không phải chép tệp.

    rename(oldpath, newpath) both renames a file and moves it to another directory on the same file system; it only edits directory entries — it moves no data, changes no link count, and does not affect open descriptors.6 If newpath exists it is overwritten; if both paths name the same file, nothing happens; renaming across file systems fails with EXDEV (then you must copy and delete — what mv does).61

    Bạn gặp nó ở đâuWhere you meet it rename() không giải symbolic link ở cả hai đối số; nếu oldpath là symlink thì chính symlink được đổi tên, không phải tệp đích.61 Khi bạn gõ mv a b trong cùng ổ đĩa, bên dưới là một lời gọi rename(). rename() dereferences symbolic links in neither argument; if oldpath is a symlink, the symlink itself is renamed, not its target.61
  3. symlink() · readlink()

    symlink()/readlink() — tạo và đọc lối tắt Tạo lối tắt và đọc xem nó trỏ đâu symlink()/readlink() — make and read a link

    symlink(filepath, linkpath) tạo một symbolic link mới linkpath trỏ tới filepath; đích không cần tồn tại lúc tạo (nếu không có, link treo ngay).4 Muốn lấy chuỗi đường dẫn mà link chứa (thay vì đi theo nó), dùng readlink(). Một cái bẫy quan trọng: readlink() không thêm byte null kết thúc vào buffer, nên ta phải tự xử lý độ dài; sách khuyên cấp buffer cỡ PATH_MAX.51

    symlink tạo lối tắt; readlink đọc xem lối tắt đó ghi đường đi gì (chứ không đi theo). Lưu ý: readlink không tự đóng chuỗi bằng ký tự kết thúc, bạn phải tự thêm. Nhớ nhé: readlink đọc tấm biển, không bước theo biển.

    symlink(filepath, linkpath) creates a new symbolic link linkpath pointing to filepath; the target need not exist at creation time (if it does not, the link dangles at once).4 To fetch the pathname the link holds (rather than follow it), use readlink(). A key trap: readlink() does not append a terminating null byte to the buffer, so you must handle the length yourself; the book suggests sizing the buffer at PATH_MAX.51

    Bạn gặp nó ở đâuWhere you meet it Listing 18-4 của sách dùng readlink() rồi realpath() để in cả đường link lẫn đường tuyệt đối sau khi giải hết link.1 Khi gõ ls -F, dấu @ sau tên chính là máy báo đó là một symbolic link. The book's Listing 18-4 uses readlink() then realpath() to print both the link string and the absolute path after all links are resolved.1
  4. mkdir() · rmdir() · remove()

    mkdir()/rmdir()/remove() — thư mục và dọn dẹp Tạo thư mục, xoá thư mục, xoá chung mkdir()/rmdir()/remove() — directories & cleanup

    mkdir(pathname, mode) tạo một thư mục mới và tự sinh hai mục . (chính nó) và .. (thư mục cha); nó chỉ tạo thành phần cuối của pathname, các cấp trên phải có sẵn, và báo EEXIST nếu đã tồn tại.7 rmdir(pathname) chỉ xoá được thư mục rỗng; nếu thành phần cuối là symbolic link thì không giải nó mà báo ENOTDIR.8 remove() là hàm thư viện tiện lợi: tệp thì gọi unlink(), thư mục thì gọi rmdir().1

    mkdir tạo một thư mục mới (kèm sẵn "." và ".."). rmdir chỉ xoá được thư mục trống. remove tiện hơn: tệp thì nó xoá như unlink, thư mục thì như rmdir. Nhớ nhé: muốn xoá thư mục thì phải dọn trống trước.

    mkdir(pathname, mode) creates a new directory and auto-creates two entries, . (itself) and .. (its parent); it creates only the last component of the pathname — higher levels must exist — and returns EEXIST if it already exists.7 rmdir(pathname) removes only an empty directory; if the last component is a symbolic link it is not dereferenced, and ENOTDIR results.8 remove() is a convenient library function: for a file it calls unlink(), for a directory rmdir().1

    Bạn gặp nó ở đâuWhere you meet it Giống unlink và rmdir, remove() không giải symbolic link; nếu pathname là symlink, nó xoá chính link chứ không xoá tệp đích.1 Lệnh rmdir foo báo lỗi nếu thư mục foo còn tệp bên trong — hãy xoá sạch trước. Like unlink and rmdir, remove() does not dereference symbolic links; if the pathname is a symlink, it removes the link, not the target.1
  5. opendir() · readdir()

    opendir()/readdir() — đọc nội dung một thư mục Liệt kê các tên trong một thư mục opendir()/readdir() — read one directory

    opendir() mở một thư mục và trả về một directory stream (con trỏ DIR); mỗi lần gọi readdir() trả về mục kế tiếp qua một struct dirent chứa số i-node và tên tệp.9 Cấu trúc này được cấp phát tĩnh và bị ghi đè sau mỗi lần gọi.10 Hai điểm dễ sai: thứ tự tên trả về không được sắp (tuỳ hệ tệp — muốn sắp dùng scandir(3)); và khi hết thư mục lẫn khi lỗi, readdir() đều trả NULL, nên phải xoá errno trước lúc gọi để phân biệt.101

    opendir mở một thư mục; readdir đọc từng tên một cho tới hết. Lưu ý: tên trả về lộn xộn, không theo a-b-c; muốn sắp thứ tự thì dùng công cụ khác. Nhớ nhé: readdir trả tên không theo thứ tự.

    opendir() opens a directory and returns a directory stream (a DIR pointer); each call to readdir() returns the next entry via a struct dirent holding the i-node number and filename.9 That structure is statically allocated and overwritten on each call.10 Two easy slips: the names come back unsorted (it depends on the file system — use scandir(3) to sort); and on both end-of-directory and error readdir() returns NULL, so you must clear errno before calling to tell them apart.101

    Bạn gặp nó ở đâuWhere you meet it Listing 18-2 của sách dùng opendir/readdir/closedir để in nội dung một thư mục, bỏ qua "." và "..".1 Mỗi khi gõ ls, bên dưới chính là một vòng lặp readdir() như thế. The book's Listing 18-2 uses opendir/readdir/closedir to print a directory's contents, skipping "." and "..".1
  6. nftw()

    nftw() — duyệt cả một cây thư mục Đi khắp một cây thư mục, xử lý từng tệp nftw() — walk an entire directory tree

    nftw() duyệt đệ quy toàn bộ cây dưới một thư mục, gọi một hàm do ta viết (func) cho mỗi tệp; nó là bản nâng cấp của ftw() cũ, xử lý symbolic link rõ ràng hơn.11 Mặc định nó đi tiền thứ tự (preorder) và có giải symbolic link. Hành vi đổi theo cờ trong đối số flags: FTW_DEPTH đi hậu thứ tự (xử lý tệp trong thư mục trước thư mục), FTW_PHYS không giải symlink, FTW_MOUNT không vượt sang hệ tệp khác, FTW_CHDIR chuyển vào từng thư mục trước khi xử lý.111

    nftw giúp "đi dạo" khắp một thư mục và mọi thư mục con, làm gì đó với từng tệp gặp được. Bạn bật các cờ để chỉnh cách đi: theo chiều sâu, đi theo hay không đi theo lối tắt, có vượt ổ đĩa hay không. Nhớ nhé: nftw đi khắp cây, bạn chỉ cần viết phần xử lý mỗi tệp.

    nftw() recursively walks the whole tree under a directory, calling a function you write (func) for each file; it is an enhancement of the older ftw(), with clearer handling of symbolic links.11 By default it does a preorder traversal and does dereference symbolic links. Behaviour changes via flags in the flags argument: FTW_DEPTH does a postorder walk (files before their directory), FTW_PHYS does not dereference symlinks, FTW_MOUNT stays within one file system, and FTW_CHDIR chdirs into each directory before processing.111

    Bạn gặp nó ở đâuWhere you meet it Listing 18-3 của sách in cây thư mục thụt đầu dòng; chạy có và không có FTW_PHYS cho thấy khi nào symbolic link được giải.1 Những lệnh như find hay du làm công việc tương tự: đi khắp cây rồi xử lý từng tệp. The book's Listing 18-3 prints an indented directory tree; running it with and without FTW_PHYS shows when symbolic links are resolved.1

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

Nơi một tên được phân giải — cwd, root, và dirfd Máy tìm tệp bắt đầu từ đâu Where a name resolves — cwd, root, and dirfd

Mỗi tiến trình có hai thuộc tính thư mục định đoạt việc phân giải pathname: thư mục gốc (điểm bắt đầu cho đường tuyệt đối, mặc định là "/") và thư mục làm việc hiện tại (cwd — điểm bắt đầu cho đường tương đối). getcwd() đọc cwd; chdir()/fchdir() đổi nó; chroot() đổi thư mục gốc; họ lời gọi openat() cho phép phân giải tương đối với một thư mục được chỉ qua file descriptor; còn realpath(), dirname()/basename() giúp xử lý chuỗi đường dẫn.1

Mỗi chương trình có hai mốc: "gốc" (ứng với dấu "/") và "nơi mình đang đứng" (cwd). Đường bắt đầu bằng "/" tính từ gốc; đường không có "/" đầu tính từ nơi đang đứng. Có các lệnh để hỏi và đổi hai mốc này. Nhớ nhé: "/" tính từ gốc, còn lại tính từ chỗ bạn đứng.

Every process has two directory attributes that decide pathname resolution: its root directory (the start for absolute paths, "/" by default) and its current working directory (the cwd — the start for relative paths). getcwd() reads the cwd; chdir()/fchdir() change it; chroot() changes the root directory; the openat() family resolves relative to a directory given by a file descriptor; and realpath(), dirname()/basename() help process pathname strings.1

Định nghĩa. Thư mục làm việc hiện tại (current working directory) là điểm khởi đầu để phân giải các pathname tương đối của tiến trình; tiến trình con thừa kế cwd từ cha.1 Thư mục gốc (root directory) là điểm khởi đầu để phân giải pathname tuyệt đối; một tiến trình đặc quyền có thể đổi nó bằng chroot() để tạo một "chroot jail" giam chương trình vào một nhánh của cây tệp.14

Nói đơn giản. cwd là "chỗ mình đang đứng"; root là "gốc" ứng với "/". Đổi gốc bằng chroot giống như nhốt chương trình trong một căn phòng, nó không thấy được phần còn lại của ngôi nhà. Nhớ nhé: chroot đổi "gốc", nhốt chương trình vào một góc cây tệp.

Definition. The current working directory is the start point for resolving a process's relative pathnames; a child inherits its cwd from its parent.1 The root directory is the start point for resolving absolute pathnames; a privileged process can change it with chroot() to set up a "chroot jail" confining a program to one branch of the file tree.14

đường "/a/b" → tính từ root directory
đường "a/b" → tính từ cwd
getcwd(buf,size) Linux/x86-32 ≤ 4096
chdir(path) / fchdir(fd)
openat(dirfd,path,…) AT_FDCWD = cwd
chroot(path) cần CAP_SYS_CHROOT

"/..." → từ gốc
"..." → từ chỗ đang đứng
getcwd = hỏi "đang ở đâu"
chdir = đi sang thư mục khác
chroot = đổi "gốc"

"/a/b" → from the root directory
"a/b" → from the cwd
getcwd(buf,size) Linux/x86-32 ≤ 4096
chdir(path) / fchdir(fd)
openat(dirfd,path,…) AT_FDCWD = cwd
chroot(path) needs CAP_SYS_CHROOT

  1. getcwd() · chdir() · fchdir()

    cwd — đọc bằng getcwd(), đổi bằng chdir() Hỏi "đang ở đâu" và "đi chỗ khác" The cwd — read it, change it

    getcwd(buf, size) chép đường tuyệt đối của cwd vào buf; thường ta cấp buffer cỡ PATH_MAX. Trên Linux/x86-32, getcwd() trả tối đa 4096 byte (PATH_MAX); nếu đường dài hơn, nó báo lỗi ERANGE thay vì cắt xén âm thầm.12 chdir(pathname) đổi cwd theo đường cho trước (có giải symlink); fchdir(fd) làm điều tương tự nhưng qua một descriptor của thư mục — tiện để "nhớ chỗ cũ rồi quay lại".1

    getcwd cho biết bạn đang đứng ở thư mục nào. chdir chuyển sang thư mục khác. Có thể mở sẵn thư mục hiện tại rồi dùng fchdir để quay về sau. Nhớ nhé: getcwd là hỏi đường, chdir là bước đi.

    getcwd(buf, size) copies the cwd's absolute path into buf; we usually size the buffer at PATH_MAX. On Linux/x86-32, getcwd() returns at most 4096 bytes (PATH_MAX); if the path is longer it fails with ERANGE rather than silently truncating.12 chdir(pathname) changes the cwd to the given path (dereferencing symlinks); fchdir(fd) does the same via a directory descriptor — handy to "remember where we are, then return".1

    Bạn gặp nó ở đâuWhere you meet it Với quyền thích hợp, ta đọc cwd của tiến trình khác qua liên kết tượng trưng Linux-chuyên-biệt /proc/PID/cwd bằng readlink().1 Lệnh pwd trong shell chính là gọi getcwd() để in ra chỗ bạn đang đứng. With suitable privilege, you can read another process's cwd via the Linux-specific symbolic link /proc/PID/cwd using readlink().1
  2. openat() · dirfd

    openat() — phân giải tương đối với một dirfd Mở tệp "từ một thư mục đã mở sẵn" openat() — resolve relative to a dirfd

    Từ nhân 2.6.16, Linux thêm một loạt lời gọi dạng …at() (như openat()) nhận thêm đối số dirfd: nếu pathname là tương đối, nó được phân giải tương đối với thư mục mà dirfd trỏ tới, thay vì với cwd (giá trị đặc biệt AT_FDCWD nghĩa là "dùng cwd").13 Lợi ích chính là tránh một số điều kiện tranh đua (race condition): một thành phần trong phần thư mục của pathname có thể bị đổi song song; mở sẵn descriptor cho thư mục đích rồi dùng openat() sẽ loại được loại lỗi đó, và còn giúp mô phỏng "thư mục làm việc riêng cho từng luồng".131

    Thường một tệp "a/b" được tìm từ chỗ bạn đang đứng. openat cho phép tìm nó từ một thư mục bạn đã mở sẵn. Nhờ vậy tránh được lỗi khi ai đó đổi thư mục giữa chừng, và mỗi luồng có thể có "chỗ đứng" riêng. Nhớ nhé: openat = mở tệp tính từ một thư mục đã cầm sẵn.

    From kernel 2.6.16, Linux added a family of …at() calls (such as openat()) taking an extra dirfd argument: if the pathname is relative, it is resolved relative to the directory dirfd refers to, rather than to the cwd (the special value AT_FDCWD means "use the cwd").13 The main benefit is avoiding certain race conditions: a component in the directory part of a pathname could be changed in parallel; opening a descriptor for the target directory and using openat() removes that class of bug, and also helps emulate "per-thread working directories".131

    Họ …at() và macro cần bật

    Bảng 18-2 liệt kê cả họ: faccessat(), fchmodat(), fstatat(), linkat(), mkdirat(), openat(), readlinkat(), renameat(), symlinkat(), unlinkat()… Để lộ khai báo, phải định nghĩa _XOPEN_SOURCE ≥ 700 hoặc _POSIX_C_SOURCE ≥ 200809 trước khi include header.116

    Cờ AT_SYMLINK_NOFOLLOW

    Nhiều lời gọi …at() còn nhận một đối số flags không có ở bản truyền thống; cờ hay gặp nhất là AT_SYMLINK_NOFOLLOW, buộc thao tác lên chính symbolic link thay vì tệp đích. Cả họ được chuẩn hoá trong SUSv4 (không có trong SUSv3).131

  3. chroot()

    chroot() — đổi thư mục gốc của tiến trình chroot() — nhốt chương trình vào một góc chroot() — change the process's root

    chroot(pathname) đổi thư mục gốc của tiến trình thành pathname; sau đó mọi đường tuyệt đối đều tính từ đó — một "chroot jail".14 Cần đặc quyền CAP_SYS_CHROOT.14 Nhưng ông Kerrisk cảnh báo rõ: chroot không được thiết kế như cơ chế giam an toàn tuyệt đối. chroot() không đổi cwd, nên thường phải chdir("/") kèm theo; và nếu tiến trình còn giữ descriptor của một thư mục ngoài "nhà tù", nó có thể dùng fchdir() rồi chroot() lần nữa để thoát ra.141

    chroot đổi "gốc" của chương trình, khiến nó chỉ thấy một nhánh của cây tệp — như bị nhốt trong một phòng. Nhưng đây không phải khoá an toàn tuyệt đối: nếu còn cầm "chìa khoá" (descriptor) của phòng ngoài, chương trình có thể lẻn ra. Nhớ nhé: chroot là rào chắn, không phải két sắt.

    chroot(pathname) changes the process's root directory to pathname; thereafter all absolute paths are counted from there — a "chroot jail".14 It needs the CAP_SYS_CHROOT capability.14 But Mr Kerrisk warns plainly: chroot is not conceived as a fully secure jail. chroot() does not change the cwd, so a chdir("/") usually must follow; and if the process still holds a descriptor for a directory outside the jail, it can use fchdir() then chroot() again to break out.141

    Bạn gặp nó ở đâuWhere you meet it Ví dụ kinh điển là máy chủ FTP: khi đăng nhập ẩn danh, chương trình ftp dùng chroot() để giới hạn người dùng trong thư mục dành cho khách.1 Lệnh chroot của shell cũng dùng để chạy lệnh bên trong một "gốc" mới. The classic example is an FTP server: for anonymous logins the ftp program uses chroot() to confine the user to the guest directory.1
  4. realpath() · dirname() · basename()

    realpath() và dirname()/basename() — xử lý chuỗi đường Rút gọn đường và tách phần thư mục/tên realpath() and dirname()/basename()

    realpath(pathname, resolved_path) giải hết mọi symbolic link và mọi tham chiếu . và .. để cho ra đường tuyệt đối tương ứng; buffer đích nên cỡ PATH_MAX.15 Còn dirname() và basename() tách một chuỗi đường thành phần thư mục và phần tên: ví dụ "/home/britta/prog.c" cho ra "/home/britta" và "prog.c".1 Lưu ý: cả hai có thể sửa chuỗi đầu vào và trả về con trỏ tới vùng cấp phát tĩnh, nên nếu cần giữ nguyên chuỗi gốc thì phải truyền một bản sao.1

    realpath biến một đường rối (có lối tắt, có "." và "..") thành đường "sạch" tuyệt đối. dirname lấy phần thư mục, basename lấy phần tên. Hai hàm sau có thể sửa chuỗi bạn đưa vào, nên hãy đưa bản sao. Nhớ nhé: realpath dọn đường, dirname/basename cắt đường làm đôi.

    realpath(pathname, resolved_path) dereferences all symbolic links and resolves all . and .. references to yield the corresponding absolute pathname; the target buffer should be PATH_MAX bytes.15 And dirname() and basename() split a pathname string into its directory and filename parts: e.g. "/home/britta/prog.c" yields "/home/britta" and "prog.c".1 Note: both may modify the input string and return pointers to statically allocated storage, so pass a copy if you must keep the original.1

    Bạn gặp nó ở đâuWhere you meet it Listing 18-5 của sách chạy dirname/basename trên nhiều đường (Bảng 18-3): "/usr/bin/zip" → "/usr/bin" + "zip"; "/" → "/" + "/".1 Khi một chương trình cần tách "thư mục chứa" và "tên tệp" từ một đường dẫn, nó dùng đúng hai hàm này. The book's Listing 18-5 runs dirname/basename over many paths (Table 18-3): "/usr/bin/zip" → "/usr/bin" + "zip"; "/" → "/" + "/".1

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

“Gỡ tên (rm) là xoá tệp ngay lập tức” “Removing a name (rm) deletes the file at once”

Ngộ nhận: unlink()/rm là xoá tệp tức thì.

Thực tế: nó gỡ một tên; tệp chỉ thực sự mất khi số liên kết về 0 và không còn descriptor nào mở — nên một tệp đã bị gỡ tên vẫn có thể đang được dùng.31

Myth: unlink()/rm deletes the file instantly.

Reality: it removes a name; the file is truly gone only when the link count reaches 0 and no descriptor is still open — so an unlinked file may still be in use.31

“Symbolic link làm tăng số liên kết của tệp đích” “A symbolic link raises the target's link count”

Ngộ nhận: tạo symlink thì link count của đích tăng lên.

Thực tế: symbolic link không được tính vào link count của tệp đích — trong Hình 18-2, i-node 61 có hai hard link nên link count là 2, chứ không phải 3 dù có một symlink trỏ tới.14

Myth: creating a symlink increments the target's link count.

Reality: a symbolic link is not counted in the target's link count — in Figure 18-2, i-node 61 has two hard links so its link count is 2, not 3, despite a symlink pointing to it.14

“readdir() trả về tên theo thứ tự a-b-c” “readdir() returns names in sorted order”

Ngộ nhận: các tên từ readdir() đã được sắp.

Thực tế: thứ tự là tuỳ cách hệ tệp lưu, không sắp; muốn danh sách đã sắp thì dùng scandir(3). (Và readlink() cũng không tự thêm byte null.)101

Myth: the names from readdir() are sorted.

Reality: the order is however the file system stores them — unsorted; for a sorted list use scandir(3). (And readlink() likewise does not add a null byte.)101

Tự kiểm Self-check

Năm câu hỏi để biết bạn đã nắm thư mục và liên kết Năm câu hỏi kiểm tra nhanh Five questions to check you grasped directories and links

Trả lời được những câu này nghĩa là bạn đã theo trọn mạch: từ việc tên tách khỏi tệp, qua bộ lời gọi tạo/xoá/đổi tên/duyệt, tới cách một pathname được phân giải.

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 a name being separate from a file, through the create/remove/rename/scan calls, to how a pathname is resolved.

  1. Vì sao nói i-node không chứa tên tệp, và điều đó khiến hard link khả dĩ như thế nào?

    Gợi ý sâu hơn: vì sao câu hỏi "tìm tên tệp ứng với một file descriptor" nói chung không có lời đáp khả chuyển?

  2. Hard link và symbolic link khác nhau ở những điểm cốt lõi nào (giới hạn hệ tệp, thư mục, link count)?

    Gợi ý sâu hơn: trong Hình 18-2, vì sao link count của i-node 61 là 2 chứ không phải 3?

  3. Khi nào unlink() thực sự giải phóng i-node và các khối dữ liệu của một tệp?

    Gợi ý sâu hơn: mẹo "tạo tệp tạm rồi unlink ngay" dựa vào tính chất nào của nhân?

  4. rename() làm gì ở mức mục thư mục, và vì sao nó thất bại khi chuyển qua hệ tệp khác?

    Gợi ý sâu hơn: những lời gọi nào trong chương KHÔNG giải symbolic link, và vì sao điều đó quan trọng?

  5. Thư mục gốc và cwd mỗi cái quyết định phân giải loại pathname nào, và openat() thêm điều gì?

    Gợi ý sâu hơn: vì sao chroot() không phải một cơ chế giam an toàn tuyệt đối?

  1. Tên tệp nằm ở đâu — trong tệp hay trong thư mục?

  2. Hard link và symbolic link, cái nào là "tên thật", cái nào là "lối tắt"?

  3. Xoá tên một tệp có chắc là xoá luôn tệp không?

  4. Đổi tên (rename) có chép dữ liệu tệp đi không?

  5. Đường bắt đầu bằng "/" được tính từ đâu, còn đường không có "/" đầu thì từ đâu?

  1. Why does an i-node hold no filename, and how does that make hard links possible?

  2. What are the core differences between a hard link and a symbolic link (file-system limit, directories, link count)?

  3. When does unlink() actually free a file's i-node and data blocks?

  4. What does rename() do at the directory-entry level, and why does it fail across file systems?

  5. Which pathname kind does each of the root directory and the cwd resolve, and what does openat() add?

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

Một i-node không mang tên; tên sống trong thư mục, và mỗi tên chỉ là một hard link trỏ vào i-node. Từ đó, rm gỡ tên chứ không xoá tệp, symbolic link là một tệp chứa đường đi, và mọi pathname đều được phân giải từ thư mục gốc hoặc thư mục làm việc hiện tại.1

Tệp thật là i-node; cái tên chỉ là một dòng trong danh bạ. Xoá tên chưa chắc mất tệp, và máy luôn tìm tệp bắt đầu từ gốc "/" hoặc từ chỗ bạn đang đứng. Nhớ nhé: tên không phải là tệp — nắm được điều này thì cả chương sáng tỏ.

An i-node carries no name; names live in directories, and each name is just a hard link into an i-node. From that, rm removes a name rather than the file, a symbolic link is a file holding a pathname, and every pathname is resolved from the root directory or the current working directory.1