10.1 File System Overview
File system tổ chức dữ liệu trên storage device thành cấu trúc cây với:
- Files: chuỗi byte có tên + metadata
- Directories: container chứa file/directory khác
- Mount points: gắn filesystem khác vào path
Filesystem layout (ext4 / XFS / FAT32)
Filesystems thực tế
- Linux: ext4 (default), XFS, btrfs, ZFS
- macOS: APFS
- Windows: NTFS
- Distributed: HDFS, S3, GCS, Ceph
- Network: NFS, SMB
10.2 Inode — "thẻ căn cước" của file
Mỗi file có 1 inode chứa metadata:
- File size
- Owner (UID), Group (GID)
- Permission (rwx cho user/group/other)
- Timestamps:
atime(access),mtime(modify),ctime(inode change) - Link count (số hardlink)
- File type (regular, directory, symlink, device, FIFO, socket)
- Pointers tới data blocks
Inode KHÔNG chứa filename! Filename nằm trong directory entry. Đó là cách hardlink hoạt động.
Cấu trúc pointer trong inode (ext4)
┌─────────────────────────────────┐
│ Inode metadata (size, perms..) │
├─────────────────────────────────┤
│ Direct[0..11]: 12 pointer │ → 12 × 4KB = 48 KB
├─────────────────────────────────┤
│ Single Indirect ─→ block │ → 1024 pointers × 4KB = 4 MB
│ of pointers │
├─────────────────────────────────┤
│ Double Indirect ─→ block ─→ │ → 1024² × 4KB = 4 GB
│ of blocks │
├─────────────────────────────────┤
│ Triple Indirect │ → 1024³ × 4KB = 4 TB
└─────────────────────────────────┘
Cấu trúc này cho phép file nhỏ chỉ tốn 1 inode + ít direct pointer; file lớn có nhiều tầng indirect.
Quan sát inode
ls -i myfile.txt # show inode number
# 1234567 myfile.txt
stat myfile.txt # full inode info
# File: myfile.txt
# Size: 100 Inode: 1234567 Links: 1
# Access: (0644/-rw-r--r--) Uid: ( 1000/ user)
# Access: 2025-05-09 ...
# Modify: 2025-05-09 ...
# Change: 2025-05-09 ...
df -i # số inode còn dư trên filesystem
touch báo "No space left" dù df hiện còn GB. Check với df -i.
10.3 Hard Link vs Symbolic Link
Hard Link
- 2+ filename trỏ vào cùng inode
- Không phân biệt "bản gốc" — đều là alias bình đẳng
- Inode link count tăng. Khi link count = 0, file thật sự bị xoá.
- Không cross filesystem
- Không hardlink directory (trên hầu hết FS)
Symbolic Link (Symlink)
- File đặc biệt chứa path đến file/dir khác
- Có "bản gốc" và "shortcut"
- Cross filesystem OK
- Nếu target bị xoá → symlink "dangling" (broken)
# Tạo hard link
ln file.txt hardlink.txt
ls -li file.txt hardlink.txt
# 12345 file.txt (cùng inode 12345)
# 12345 hardlink.txt
# Tạo symlink
ln -s file.txt softlink.txt
ls -li file.txt softlink.txt
# 12345 file.txt
# 67890 softlink.txt -> file.txt (inode khác)
# Xoá file gốc:
rm file.txt
cat hardlink.txt # vẫn đọc được — link count = 1, data còn
cat softlink.txt # error — broken symlink
10.4 File Descriptor (fd)
Khi process mở file, kernel trả về file descriptor — một số nguyên nhỏ (vd 3, 4, 5...). Process dùng fd để đọc/ghi/đóng file.
fd là index trong file descriptor table của process. Nhiều fd có thể trỏ vào cùng open file description (vd sau dup).
Số fd tối đa
ulimit -n # giới hạn fd của shell hiện tại — thường 1024 hoặc 4096
cat /proc/sys/fs/file-max # giới hạn toàn hệ thống
# Web server cần handle 100K connection → tăng:
ulimit -n 65535
close() khi xong.
10.5 stdin / stdout / stderr
Mỗi process khởi tạo có sẵn 3 fd:
- fd 0:
stdin— đầu vào (thường là keyboard) - fd 1:
stdout— đầu ra (thường là terminal) - fd 2:
stderr— đầu ra lỗi (thường là terminal)
Redirect trong shell
# Stdout to file
ls > out.txt # ghi đè
ls >> out.txt # append
# Stderr to file
ls /no > out.txt 2> err.txt
# Stderr to stdout (rồi cùng vào file)
ls /no > all.txt 2>&1
# Tất cả to /dev/null (vứt đi)
ls >/dev/null 2>&1
# Pipe: stdout của ls vào stdin của grep
ls -l | grep .txt
# Stdin from file
sort < input.txt
Cơ chế: shell dup2 để thay đổi fd 0/1/2 trỏ vào file/pipe trước khi exec.
10.6 open / read / write / close — file lifecycle
#include <fcntl.h>
#include <unistd.h>
int main() {
// 1. Open
int fd = open("data.txt", O_RDWR | O_CREAT, 0644);
if (fd < 0) { perror("open"); return 1; }
// 2. Write
const char *msg = "Hello\n";
ssize_t written = write(fd, msg, 6);
// 3. Seek (di chuyển offset)
lseek(fd, 0, SEEK_SET); // về đầu file
// 4. Read
char buf[100];
ssize_t n = read(fd, buf, sizeof(buf));
// 5. Close
close(fd);
return 0;
}
open() flags quan trọng
O_RDONLY,O_WRONLY,O_RDWRO_CREAT: tạo nếu chưa cóO_EXCL: dùng với O_CREAT — fail nếu đã tồn tại (atomic create)O_APPEND: ghi vào cuối file (atomic — tốt cho log)O_TRUNC: cắt file về 0 byte khi mởO_NONBLOCK: non-blocking I/O (xem 10.8)O_DIRECT: bypass page cache, đọc/ghi thẳng diskO_SYNC: write đảm bảo flush xuống disk
10.7 Blocking I/O
Mặc định, syscall I/O là blocking: thread đợi đến khi I/O xong.
// Server thread — 1 client 1 thread (blocking)
while (1) {
int client = accept(server_fd, ...); // BLOCK đến khi có connection
pthread_create(&t, NULL, handle, client);
}
void* handle(void* arg) {
int fd = *(int*)arg;
char buf[1024];
int n = read(fd, buf, 1024); // BLOCK đến khi client gửi data
write(fd, response, ...); // BLOCK đến khi gửi xong
close(fd);
}
Đơn giản nhưng scale kém: 10K client = 10K thread. Mỗi thread tốn 1MB stack → 10GB chỉ riêng stack. Đây là vấn đề C10K nổi tiếng — Apache prefork đụng phải.
10.8 Non-blocking I/O
Set fd thành non-blocking (O_NONBLOCK): syscall không đợi, trả về ngay.
fcntl(fd, F_SETFL, O_NONBLOCK);
int n = read(fd, buf, 1024);
if (n == -1) {
if (errno == EAGAIN) {
// Chưa có data — không phải error
// Có thể làm việc khác, retry sau
} else {
// Lỗi thật
}
}
Nhược: phải polling liên tục — vẫn tốn CPU. Cần cơ chế tốt hơn → I/O Multiplexing.
10.9 I/O Multiplexing — select / poll / epoll / kqueue
Ý tưởng: 1 thread quản lý nhiều fd, hỏi kernel "fd nào sẵn sàng đọc/ghi?". Kernel báo lại, thread chỉ thao tác fd đó.
select() — POSIX cổ
fd_set readfds;
FD_ZERO(&readfds);
FD_SET(fd1, &readfds);
FD_SET(fd2, &readfds);
int n = select(maxfd + 1, &readfds, NULL, NULL, NULL);
// n = số fd ready
// FD_ISSET(fd1, &readfds) → check từng fd
Nhược: giới hạn 1024 fd, copy fd_set cho mỗi call → O(n).
poll() — như select nhưng không giới hạn 1024
epoll (Linux) / kqueue (BSD/macOS) — modern
int epfd = epoll_create1(0);
// Đăng ký fd cần watch
struct epoll_event ev = {.events = EPOLLIN, .data.fd = client_fd};
epoll_ctl(epfd, EPOLL_CTL_ADD, client_fd, &ev);
// Đợi event
struct epoll_event events[100];
int n = epoll_wait(epfd, events, 100, -1);
// Xử lý fd ready
for (int i = 0; i < n; i++) {
if (events[i].events & EPOLLIN) {
read(events[i].data.fd, ...);
}
}
Tại sao epoll nhanh hơn select?
- O(1) thay vì O(n): chỉ trả về fd ready, không scan hết
- Không cần copy fd set mỗi lần
- Hỗ trợ edge-triggered: chỉ thông báo khi state đổi (avoid trigger lặp)
- Scale lên hàng trăm nghìn fd
nginx, Node.js, Redis, và mọi web server hiện đại đều dùng epoll/kqueue.
Pattern Reactor
Single-thread + epoll = pattern Reactor. Thread chính chạy event loop, dispatch event đến handler. Đây chính xác là cách Node.js libuv hoạt động (chương 3).
10.10 Async I/O — io_uring (mới nhất)
epoll hỏi "fd nào ready?" rồi mới đọc. Async I/O đi xa hơn: "kernel đọc giúp tôi, báo khi xong".
POSIX AIO (cũ, ít dùng)
API hỗn loạn, ít kernel support tốt.
io_uring (Linux 5.1+, 2019)
2 ring buffer giữa user và kernel: submission queue (SQ) và completion queue (CQ). User submit op vào SQ, kernel xử lý, push kết quả vào CQ. Ít syscall, batch nhiều op. Hiện đang là standard mới cho high-perf I/O.
Database, message broker, file server hiện đại đang chuyển sang io_uring.
10.11 Page Cache — kernel buffer cho file
Khi process read() file, kernel đọc từ disk vào page cache (vùng RAM kernel quản lý), rồi copy sang user buffer.
Lần đọc tiếp theo (cùng page) → trả từ cache, không đọc disk lại.
Đó là lý do file vừa đọc xong, đọc lại cực nhanh.
# Đọc file 1GB lần đầu — ~5s
time cat bigfile > /dev/null
# Lần 2 — <1s vì đã cache
time cat bigfile > /dev/null
# Xoá page cache
echo 3 | sudo tee /proc/sys/vm/drop_caches
Write-back cache
write() không lập tức vào disk. Kernel ghi vào page cache, mark dirty, return success ngay. Background kthread ghi xuống disk sau (vài giây).
Đó là lý do copy file nhanh nhưng tháo USB ngay sau khi copy lại nguy hiểm — data có thể chưa flush.
Quan sát page cache
free -h
# total used free shared buff/cache available
# Mem: 16G 4G 1G 1G 11G 11G
# (buff/cache 11G — phần lớn là page cache, có thể giải phóng nếu cần)
10.12 fsync() & Durability
Sau write(), data chỉ ở page cache. Mất điện = mất data. Để chắc chắn data persist trên disk:
write(fd, buf, n); // ghi vào page cache (nhanh)
fsync(fd); // CHỜ đến khi data thật sự xuống disk (chậm)
fsync tốn ~5-50ms (HDD), ~200µs (SSD). Database (MySQL, PostgreSQL, Redis) gọi fsync mỗi commit để đảm bảo durability.
Trade-off database thực tế
- fsync mỗi commit: an toàn nhất nhưng chậm. Default của PostgreSQL.
- fsync định kỳ (vd mỗi giây): nhanh hơn, có thể mất 1s data nếu crash. MySQL
innodb_flush_log_at_trx_commit=2, Redis AOFeverysec. - Không fsync: nhanh nhất, mất nhiều data nếu crash. Chỉ dùng cho cache.
fsync vs sync vs fdatasync
fsync(fd): ghi data + metadata của file xuống diskfdatasync(fd): chỉ ghi data + metadata cần (vd file size). Nhanh hơn fsync.sync(): flush mọi dirty page (cả filesystem)
10.13 Node.js File I/O — kết nối với chương 3
Node.js fs API có 2 cách: callback + promise.
const fs = require('fs').promises;
// Read file
const data = await fs.readFile('input.txt', 'utf8');
// Write file
await fs.writeFile('output.txt', data);
// Append
await fs.appendFile('log.txt', 'new entry\n');
// Stream (cho file lớn)
const fs = require('fs');
const readStream = fs.createReadStream('huge.csv');
readStream.on('data', (chunk) => { /* xử lý */ });
// Watch directory
fs.watch('./uploads', (event, filename) => {
console.log(event, filename);
});
Tại sao Node fs nhanh dù single-thread?
Như đã học chương 3: Node main thread không block. fs.readFile được delegate sang libuv thread pool
(default 4 thread). Khi đọc xong, callback đẩy vào event loop queue.
Tip: nếu app I/O-heavy, tăng UV_THREADPOOL_SIZE: UV_THREADPOOL_SIZE=16 node app.js.
Async I/O qua io_uring
Node 20+ có experimental fs.openAsBlob và 1 số API dùng io_uring trên Linux. Trong tương lai có thể bypass thread pool.
Bài tập
Tạo file. ln tạo hardlink. ls -li xem inode. Xoá file gốc — hardlink vẫn dùng được. Giải thích.
Tạo symlink. Xoá target. Cố cat symlink — error. Quan sát hành vi.
ls /proc/$$/fd liệt kê fd. lsof -p PID chi tiết. Mở file trong shell rồi kiểm tra.
Đọc file 100MB lần đầu — đo thời gian. Đọc lần 2 — nhanh hơn nhiều. echo 3 > /proc/sys/vm/drop_caches rồi đọc lần 3 — chậm lại.
Viết C program: write 1000 lần với fsync vs không fsync. So sánh thời gian. Trên SSD vs HDD nếu có.
Viết server C dùng epoll handle 100 client đồng thời (echo back). 1 thread, không thread pool.
Viết Node script copy file 1GB bằng readFile (load hết) vs createReadStream. So sánh memory usage qua process.memoryUsage().
🧪 Quiz cuối chương
Câu 1. Inode chứa gì?
Đáp án: metadata + data pointers. Filename nằm trong directory entry trỏ về inode. Đó là cách hardlink hoạt động.
Câu 2. Hard link và Symbolic link khác nhau?
Đáp án: cùng inode vs path. Hard link không cross filesystem; symlink có thể bị broken.
Câu 3. Khi process mở file, kernel trả về?
Đáp án: file descriptor. Giới hạn số fd là ulimit -n; web server hay phải tăng.
Câu 4. Tại sao epoll nhanh hơn select?
Đáp án: O(1). select scan hết fd_set; epoll register fd 1 lần, kernel chỉ trả ready ones.
Câu 5. Page cache trong Linux?
Đáp án: cache nội dung file. Đó là lý do file vừa đọc, đọc lại nhanh. free -h cột buff/cache.
Câu 6. fsync() dùng để?
Đáp án: ép flush xuống disk. Mất điện sau write nhưng không fsync = mất data.
Câu 7. stdin / stdout / stderr có fd lần lượt là?
Đáp án: 0, 1, 2. Mọi process khởi tạo có sẵn 3 fd này. Lý do 2>&1 redirect stderr sang stdout.
Câu 8. Lỗi "Too many open files" do?
Đáp án: hết fd. Mỗi connection, mỗi file mở tốn 1 fd. Web server cần code close() đúng chỗ.
Tổng kết chương 10 — và toàn bộ giáo trình OS
- ✅ Filesystem layout: superblock + inode table + data blocks
- ✅ Inode = metadata + data pointers; KHÔNG có filename (filename ở dir entry)
- ✅ Hard link (cùng inode) ≠ Symlink (file path đến target)
- ✅ File Descriptor = số nguyên index trong fd table của process
- ✅ stdin/stdout/stderr = fd 0/1/2; shell redirect dùng
dup2 - ✅ Blocking I/O đơn giản nhưng không scale; Non-blocking + I/O multiplexing là nền tảng web server hiện đại
- ✅ epoll/kqueue O(1) → scale lên hàng trăm nghìn fd. Pattern Reactor.
- ✅ io_uring: future of async I/O
- ✅ Page Cache: read/write đi qua RAM trước → fast; trade-off với durability
- ✅ fsync đảm bảo data xuống disk — đắt, dùng cho DB commit
- ✅ Node.js fs API: callback/promise; libuv thread pool cho I/O
Bạn đã đi qua 10 chương — từ kernel/syscall đến file system. Kết hợp với DSA, bạn đã có 2/6 trụ cột nền tảng IT. Sau OS, các trụ cột tiếp theo:
- Networking — TCP/IP, HTTP, DNS, sockets
- Database — SQL, indexing, transaction
- OOP & Design Patterns
- System Design — kiến trúc hệ thống lớn
Đề xuất thực hành ngay sau khi học OS:
- Đọc sách Operating Systems: Three Easy Pieces (free PDF) cho lý thuyết sâu hơn
- Cài Linux VM hoặc dùng macOS terminal — thử mọi command đã học
- Đọc man page:
man 2 fork,man 2 mmap,man 7 epoll - Build mini project: web server đơn giản dùng epoll, hoặc shell tự viết
Chúc bạn thành công ở mọi vòng phỏng vấn backend / SRE / DevOps!