Filesystem — từ tên file tới khối dữ liệu trên đĩa
Tên file không nằm trong file: dentry trỏ tới inode, inode giữ metadata và bản đồ block. Vì sao ls -l đắt hơn ls, và hardlink khác symlink chỗ nào.
TL;DR: Tên file không nằm trong file. Khi hệ điều hành mở report.txt, nó lật thư mục cha ra, tìm dòng report.txt → 482719, rồi dùng số inode 482719 để định vị metadata và khối dữ liệu thật. Thư mục chỉ là bảng ánh xạ tên sang số; inode mới giữ quyền, chủ sở hữu, thời gian, kích thước và bản đồ block. Từ nhận thức đó suy ra ba hệ quả dev gặp hằng ngày: vì sao ls rẻ còn ls -l đắt, hardlink và symlink khác nhau chỗ nào, và vì sao xoá file đang mở không thu hồi dung lượng ngay.
Một buổi trực production: ổ đĩa đầy, bạn xoá file log vài gigabyte, chạy df -h — con số không nhúc nhích. Ứng dụng ở terminal kia vẫn đang ghi vào file đó. Làm sao filesystem biết chuyện đó, và tại sao nó quan tâm?
Câu trả lời nằm ở chỗ tên, metadata và khối dữ liệu không sống cùng nhau. Chúng tách ra ba tầng, và mỗi lệnh ls, stat hay rm bạn chạy thực ra đang đọc một tầng khác nhau.
1. Analogy — mục lục thư viện
Hình dung một thư viện với hai hệ thống tra cứu tách bạch.
Ngăn kéo phiếu mục lục ở sảnh cho bạn tra theo tên sách — mỗi phiếu ghi "tên sách → mã kho F-0482". Đây là dentry: một cái tên ánh xạ sang một con số định vị. Tên sách không in bên trong cuốn sách; nó sống trong phiếu mục lục. Và một cuốn sách hoàn toàn có thể có nhiều phiếu khác tên mà vẫn trỏ về cùng mã kho.
Xuống kho, mã F-0482 dẫn tới thẻ kho. Thẻ ghi tình trạng sách, ngăn giá nào, ai đang mượn. Đây là inode: nó giữ metadata và danh sách vị trí lưu trữ thật. Tên sách không có trên thẻ kho.
| Trong thư viện | Trong filesystem |
|---|---|
| Phiếu mục lục (tên sách → mã kho) | Dentry — ánh xạ tên file → số inode |
| Mã kho | Số inode (inode number) |
| Thẻ kho (tình trạng, vị trí ngăn) | Inode — metadata + con trỏ block |
| Hai phiếu cùng mã kho, khác tên | Hardlink |
| Phiếu ghi "xem phiếu kia" | Symlink |
| Ngăn sách trên giá | Block dữ liệu |
Tên file sống trong thư mục — không trong inode. Inode chỉ biết mình là số mấy, metadata là gì, và dữ liệu nằm ở block nào.
2. Thư mục là bảng ánh xạ, còn inode giữ thứ gì?
Thư mục không phải đối tượng đặc biệt — nó vốn là một file bình thường có nội dung là danh sách cặp (tên → số inode). Mỗi cặp gọi là dentry. Khi bạn chạy ls /etc, kernel đọc nội dung file thư mục /etc rồi trả về danh sách tên — không đụng tới bất kỳ inode nào của các file con.
Inode giữ: kiểu file, quyền (rwx), chủ sở hữu (uid/gid), ba mốc thời gian (atime/mtime/ctime), kích thước, số hard link đang trỏ vào, và con trỏ tới các block dữ liệu. Không có trường tên nào. Muốn xem số inode của một file, dùng stat:
stat /etc/passwd
Output dạng minh hoạ:
File: /etc/passwd
Size: 2712 Blocks: 8 IO Block: 4096 regular file
Device: 8,1 Inode: 393218 Links: 1
Access: (0644/-rw-r--r--) Uid: ( 0/ root) Gid: ( 0/ root)
(Minh hoạ — số inode và kích thước thật tuỳ hệ thống.)
Sơ đồ đường đi từ tên file tới dữ liệu:

open(), stat(), readdir(), unlink() — bốn thao tác dùng xuyên bài này — đều là syscall. Kernel nhận yêu cầu, tra bảng inode trong kernel space rồi trả kết quả về user space. Bạn đã biết cơ chế đó từ bài syscall; ở đây ta thấy cụ thể kernel đang tra bảng gì.
3. Inode trỏ xuống block bằng cơ chế gì?
File nhỏ vài kilobyte thì inode cất vài con trỏ trực tiếp — mỗi con trỏ là địa chỉ một block 4 KB trên đĩa. File lớn hàng gigabyte thì số block lên tới hàng triệu, không nhồi hết vào inode kích thước cố định được.
Hệ thống truyền thống (ext2/ext3) dùng con trỏ gián tiếp nhiều tầng: một block dành riêng để liệt kê địa chỉ các block thật; nếu chưa đủ thì một block nữa liệt kê địa chỉ các block chứa danh sách — cứ thế lồng thêm tầng. Đọc một byte nằm sâu trong file lớn có thể cần hai, ba lần nhảy đĩa chỉ để duyệt chuỗi con trỏ.
Cách tiếp cận hiện đại của ext4 và btrfs là extent: thay vì liệt kê từng block một, inode ghi cặp (block bắt đầu, số block liên tiếp). File 1 MB nằm liền mạch thì chỉ cần một extent duy nhất thay vì 256 con trỏ rời. Và vì dữ liệu nằm liền mạch trên đĩa, HDD không cần nhảy chỗ — đây là cơ sở cho lý do đọc tuần tự nhanh hơn nhiều mà bài 01 và bài 02 nói tới. Filesystem cố cấp nguyên một vùng block liên tiếp ngay lúc ghi mới để extent ngắn nhất có thể.
4. Hardlink, symlink và tại sao xoá file đang mở không giải phóng dung lượng?
Hardlink là một dentry thứ hai trỏ vào cùng một inode. Không có gì sao chép; link count trong inode tăng từ 1 lên 2. Bạn xác nhận được bằng ls -li:
ln /etc/passwd /tmp/passwd-link
ls -li /etc/passwd /tmp/passwd-link
Output dạng minh hoạ:
393218 -rw-r--r-- 2 root root 2712 ... /etc/passwd
393218 -rw-r--r-- 2 root root 2712 ... /tmp/passwd-link
(Minh hoạ — số inode 393218 là ví dụ.)
Cột đầu là inode number — hai dòng cùng số, nghĩa là cùng một inode. Số 2 ở link count nói rằng có hai dentry đang trỏ vào. Xoá một cái chỉ giảm link count về 1; inode và dữ liệu vẫn sống.
Symlink khác hẳn: nó là một file riêng có nội dung là đường dẫn text. Kernel thấy symlink thì đọc nội dung rồi tra lại từ đầu. Hardlink không thể cross-filesystem vì inode number chỉ unique trong một filesystem; symlink có thể trỏ sang filesystem khác hoặc trỏ tới đường dẫn không tồn tại.
Xoá file đang mở: rm gỡ dentry khỏi thư mục và giảm link count về 0, nhưng kernel còn bộ đếm thứ hai — số file descriptor đang mở trỏ vào inode. Chỉ khi cả link count lẫn fd count cùng bằng 0 thì kernel mới thật sự thu hồi block. Tiến trình ghi log giữ fd count dương, nên df không thay đổi cho đến khi tiến trình đóng file.
5. Pitfall của riêng concept này
❌ Pitfall 1 — Hardlink qua filesystem.
Số inode chỉ unique trong phạm vi một filesystem. Tạo hardlink sang thiết bị khác thì lệnh ln báo lỗi ngay — cùng số inode trên hai ổ là hai thực thể hoàn toàn khác.
✅ Dùng symlink khi cần link cross-filesystem; symlink lưu đường dẫn text, không lưu số inode.
❌ Pitfall 2 — df -h còn trống nhưng không tạo được file mới (No space left on device).
Bảng inode có kích thước cố định từ lúc format. Hàng triệu file nhỏ có thể làm hết inode trước khi hết block — và df -h chỉ báo block.
✅ Kiểm tra df -i; nếu cột IUse% đạt 100% thì đây là nguyên nhân. Dọn thư mục chứa nhiều file nhỏ hoặc format lại với mật độ inode cao hơn.
❌ Pitfall 3 — Xoá file log nhưng ổ vẫn đầy.
rm chỉ giảm link count. Tiến trình đang mở file descriptor vào file đó giữ fd count dương; dung lượng chưa được thu hồi.
✅ Tìm tiến trình bằng lsof | grep deleted rồi gửi SIGHUP hoặc cho nó xoay vòng file. Khi fd đóng, dung lượng thu hồi ngay.
6. 📚 Đào sâu (tuỳ chọn)
Ext4 lưu tối đa 4 extent trực tiếp trong inode, mỗi extent là cặp (block bắt đầu, số block liên tiếp). File nhỏ thường chỉ cần 1–2 extent và không cần block phụ — khác hẳn con trỏ gián tiếp của ext2 phải duyệt thêm một, hai tầng block trung gian. File lớn hơn thì extent tree mọc thêm nút, nhưng vẫn ít bước hơn nhiều. Tài liệu: kernel docs Documentation/filesystems/ext4/ và man page ext4(5).
7. Liên hệ các bài khác
- HDD, SSD, NVMe — extent là câu trả lời cụ thể cho "làm sao dữ liệu nằm liền mạch tránh seek"; bài 01 giải thích từ phía phần cứng, bài này giải thích từ phía filesystem.
- Tuần tự vs ngẫu nhiên — file phân mảnh nhiều extent rải rác trên đĩa, mỗi extent là một lần nhảy chỗ — đây là lý do truy cập ngẫu nhiên đắt hơn tuần tự.
- Page cache và buffered I/O — kernel tra inode, lấy block address, nạp block vào page cache; lần đọc thứ hai bỏ qua đĩa vì page đã có trong cache.
- fsync và độ bền — sau khi kernel biết inode và block nào cần ghi, nó còn quyết định khi nào thực sự ghi xuống đĩa; fsync ép ghi ngay.
8. Tóm tắt
- Thư mục là file chứa danh sách cặp
(tên → số inode)— gọi là dentry. Tên file sống trong thư mục, không trong inode; inode giữ metadata và con trỏ block. lsđọc thư mục để lấy tên (1 inode),ls -lphải gọistat()cho từng entry — N file trong thư mục thì N lần đọc inode thêm.- Hardlink là dentry mới trỏ cùng inode, link count tăng; symlink là file riêng chứa đường dẫn text và có thể cross-filesystem.
- Kernel giải phóng block chỉ khi cả link count lẫn fd count về 0 — nên
rmfile đang mở không thu hồi dung lượng cho đến khi tiến trình đóng fd.
9. Tự kiểm tra
Q1Bạn chạy ls /home/alice và thấy report.txt. Kernel đã đọc bao nhiêu inode để in ra tên đó? Mô tả từng bước.▸
Đúng một inode — inode của chính thư mục /home/alice. Kernel mở thư mục (tra inode của nó để biết các block chứa nội dung thư mục), đọc danh sách dentry, rồi trả về tên. Inode của report.txt hay bất kỳ file con nào chưa được đụng tới.
Đó là lý do ls chạy nhanh không phụ thuộc số file: số lần đọc inode cố định là 1 (inode thư mục). Ngược lại ls -l gọi stat() cho mỗi entry — 10.000 file là 10.000 lần đọc inode.
Q2Vì sao không thể tạo hardlink từ /home/alice/report.txt sang /mnt/backup/link.txt nếu /mnt/backup nằm trên thiết bị khác?▸
Số inode chỉ unique trong phạm vi một filesystem. Inode số 482719 trên ổ A và inode số 482719 trên ổ B là hai thực thể hoàn toàn khác. Tạo hardlink cross-device nghĩa là một dentry trỏ tới một số inode — nhưng số đó trên filesystem nào? Câu hỏi không có câu trả lời hợp lệ trong thiết kế hiện tại.
Symlink không gặp giới hạn này vì nó không lưu số inode — nó lưu đường dẫn text. Kernel đọc đường dẫn rồi tra lại từ đầu, có thể đi qua nhiều filesystem khác nhau.
Q3Một tiến trình đang ghi log vào /var/log/app.log. Bạn chạy rm /var/log/app.log nhưng df -h vẫn không thay đổi. Giải thích cơ chế và cách thu hồi dung lượng mà không cần khởi động lại server.▸
rm xoá dentry trong thư mục và giảm link count về 0, nhưng kernel còn bộ đếm riêng: số file descriptor đang mở trỏ vào inode đó. Tiến trình ghi log vẫn giữ fd nên fd count dương. Kernel chỉ thu hồi block khi cả link count lẫn fd count cùng bằng 0.
Thu hồi không cần restart: tìm tiến trình bằng lsof | grep deleted, rồi gửi SIGHUP để nó đóng và mở lại file log. Ngay khi fd cũ đóng, fd count về 0 và kernel giải phóng block ngay lập tức.
Q4df -h báo ổ còn 10 GB trống nhưng lệnh touch /tmp/newfile lại trả lỗi No space left on device. Điều gì đang xảy ra và kiểm tra thế nào?▸
Filesystem hết inode, không phải hết block. Mỗi filesystem có bảng inode kích thước cố định xác định lúc format. Tạo hàng triệu file nhỏ thì bảng inode cạn trước khi hết block; touch cần cấp phát một inode mới — hết inode thì mọi thao tác tạo file đều thất bại với cùng lỗi đó.
Kiểm tra bằng df -i: nếu cột IUse% là 100% thì đây là nguyên nhân. Giải pháp ngắn hạn là dọn thư mục chứa nhiều file nhỏ (session cache, temp, message queue tồn đọng). Dài hạn hơn: format lại với mật độ inode cao hơn.
Q5Extent (ext4) giải quyết vấn đề gì của con trỏ gián tiếp (ext2)? Nêu cả trường hợp extent không giúp được gì.▸
Con trỏ gián tiếp của ext2 liệt kê từng block riêng lẻ, nên đọc file lớn cần duyệt một, hai thậm chí ba tầng block trung gian — mỗi tầng là một lần nhảy đĩa — chỉ để tìm địa chỉ block chứa dữ liệu thật. Extent thay thế bằng cặp (block bắt đầu, số block liên tiếp): file 1 MB liền mạch chỉ cần một extent, kernel tính ngay địa chỉ mà không cần tra thêm, và dữ liệu nằm liền mạch giúp HDD không phải nhảy chỗ.
Extent không giúp khi file bị phân mảnh nặng — nhiều đoạn dữ liệu rải rác thì cần nhiều extent, lợi thế "một cặp số" mất đi. Đây là lý do filesystem cố cấp block liền mạch lúc ghi mới; SSD ít bị ảnh hưởng vì không có seek, nhưng nhiều extent vẫn tăng overhead metadata khi tra.
Bài tiếp theo: Page cache — vì sao lần đọc thứ hai nhanh gấp trăm lần
Bài này đáng gửi cho bạn học cùng?
Copy link đã gắn nguồn — dán group, chat, hoặc LinkedIn.
Bài này có giúp bạn hiểu bản chất không?
Hỏi đáp về bài này
Chưa có câu hỏi
Có gì chưa rõ trong bài? Đặt câu hỏi đầu tiên — câu trả lời từ cộng đồng giúp bạn (và người sau).
Đặt câu hỏi đầu tiên