Hệ điều hành cho lập trình viên
Sê-ri 45 bài đo thật hệ điều hành trong container: syscall, tiến trình, bộ nhớ, lịch CPU, I/O, cgroup — mỗi bài một phép đo, một sơ đồ, một lần tự sửa mình.
45/45 phần đã đăng
Hệ điều hành
1
Chi phí một syscall
Một lời gọi hàm thường tốn 0,8ns; một syscall getpid bẫy vào nhân tốn ~113ns (gấp 140 lần). Nhưng clock_gettime chỉ 15ns — vì nó KHÔNG bẫy, nó chạy trong vDSO. Đo thật bằng C và strace.
03/09/2026
· 7 phút đọc
2
fork tốn gì
fork() không sao chép dữ liệu nhờ copy-on-write (fork 1GB 1,6ms so với memcpy 490ms), nhưng nó vẫn tốn O(bộ nhớ) vì phải sao chép bảng trang: 33us ở 1MB lên 1613us ở 1GB. Đo thật.
03/09/2026
· 8 phút đọc
3
Copy-on-write khi fork
Sau fork, con ghi 0% thì chiếm riêng 0MB, ghi 100% thì 400MB — bộ nhớ được chia sẻ đúng tới khi ghi. Nhưng RSS đứng yên 400MB ở mọi mức: nó đếm cả trang chung. Đo thật bằng smaps.
03/09/2026
· 7 phút đọc
4
Chi phí tạo luồng
Tạo một luồng tốn 42us, tạo một tiến trình 96us — nhanh hơn ~2 lần, không phải 100 lần. Và 1000 luồng làm VmSize +8GB nhưng RAM thật chỉ +8MB: 8MB mỗi luồng là địa chỉ ảo. Đo thật.
03/09/2026
· 7 phút đọc
5
Chuyển ngữ cảnh
Ghim hai luồng vào một nhân, đổi ngữ cảnh tốn 0,5us. Bỏ ghim để dùng hai nhân — chậm gấp 16 lần (8,7us), vì đó là cross-core chứ không phải đổi ngữ cảnh. Đo thật bằng ping-pong.
03/09/2026
· 7 phút đọc
6
Trạng thái tiến trình
Dựng từng trạng thái R/S/D/Z/T rồi đọc /proc: dd đồng bộ nằm trong D tới 72% thời gian. Và load average không vọt lên ngay khi có tải — vì nó là trung bình trượt 1/5/15 phút. Đo thật.
03/09/2026
· 7 phút đọc
7
Lỗi trang
Chạm 200MB anon chỉ sinh 102 lỗi trang, không phải 51200 — vì trang lớn THP. Và đọc file lạnh từ đĩa chỉ đếm 1 major fault dù chậm gấp 27 lần cache — vì readahead. Đo thật bằng getrusage.
03/09/2026
· 7 phút đọc
8
mmap so với read
Đọc file chạm mọi byte: mmap và read bằng nhau (~3700 MB/s). Nhưng chạm thưa 1 byte/trang, mmap nhanh gấp 5 lần (190 vs 40 GB/s) — vì read phải chép cả file vào buffer. Đo thật.
03/09/2026
· 7 phút đọc
9
Page cache
Đọc lạnh từ đĩa 93ms, đọc ấm từ cache 18ms — nhanh gấp 5 lần. Nhưng đo hớ: ghi file bằng dd đã nạp sẵn nó vào cache, nên 'lần đầu' của tôi vốn đã ấm. Phải drop_caches. Đo thật.
03/09/2026
· 7 phút đọc
10
fsync tốn gì
write() không fsync đạt ~984.000 ghi/giây — nhưng đó là tốc độ cache, dữ liệu chưa bền. Thêm fsync ép xuống đĩa: sụp còn ~2.578 ghi/giây (388us mỗi lần). Gộp fsync cứu lại tốc độ. Đo thật.
03/09/2026
· 7 phút đọc
11
Ghi đệm và write-back
Ghi 300MB không fsync: Dirty nằm nguyên 300MB suốt 30 giây rồi mới về 0 — nhân đẩy trang bẩn xuống đĩa đúng mốc dirty_expire. Cửa sổ 30 giây đó là lý do phải fsync. Đo thật bằng /proc/meminfo.
03/09/2026
· 7 phút đọc
12
Đọc trước
Đọc tuần tự 6701 MB/s, ngẫu nhiên 295 MB/s. Nhưng đọc tuần tự mà TẮT readahead chỉ 134 MB/s — chậm 50 lần, dù không hề seek. Tuần tự nhanh vì readahead, không phải vì SSD không seek. Đo thật.
03/09/2026
· 7 phút đọc
13
Bộ nhớ ảo và RSS
Cấp phát 2GB tức thì, VmSize +2GB nhưng VmRSS +0. Và đọc hết 2GB đó vẫn +0 RAM — vì đọc bộ nhớ mới chạm zero-page chung; chỉ GHI mới cấp RAM thật. Đo thật bằng /proc/self/status.
03/09/2026
· 7 phút đọc
14
Overcommit bộ nhớ
malloc 512 GB trên một máy 8 GB vẫn báo THÀNH CÔNG — vì overcommit chỉ hứa, chưa giao RAM. Sự thật lộ khi chạm trang: OOM killer giết ở giới hạn thật. Đo thật.
03/09/2026
· 8 phút đọc
15
OOM killer
Khi hết RAM, nhân giết ai? Không phải kẻ vừa xin làm tràn, mà KẺ TO NHẤT — một tiến trình ngoài cuộc đang ngồi yên. Đo thật bằng oom_score và dmesg.
03/09/2026
· 9 phút đọc
16
Huge pages
Truy cập ngẫu nhiên trên 1 GB: trang 4KB mất 138 ns/lần, trang lớn 2MB chỉ 114 ns — chênh đó là chi phí lỗi TLB. Và một lần THP=always khiến 'nền 4KB' của tôi âm thầm là 2MB. Đo thật.
03/09/2026
· 8 phút đọc
17
Swap
Nạp lại một trang đã bị đẩy ra swap tốn ~704 ns, chậm hơn đọc RAM 58 lần — dù bộ đếm ru_majflt và pswpin đều báo 0. Tin đồng hồ hơn bộ đếm. Đo thật bằng MADV_PAGEOUT.
03/09/2026
· 9 phút đọc
18
Bộ nhớ chia sẻ
Chuyển 1 GB giữa hai tiến trình: pipe 3,1 GB/s, bộ nhớ chia sẻ có ring 9,8 GB/s (~3×). Nhưng shm một đệm lại chậm hơn pipe, và một số 15 GB/s dối đã suýt lừa tôi. Đo thật.
03/09/2026
· 9 phút đọc
19
Pipe và bộ đệm
Pipe báo dung lượng 64 KB, nhưng ghi khối 4097 byte chỉ nhét được 44 KB trước khi chặn — cách ghi định hình con số. Và khi đầy, write() chặn đúng tới khi bên đọc rút. Đo thật.
03/09/2026
· 8 phút đọc
20
Mutex và futex
Khóa mutex khi không ai tranh chỉ tốn 5,4 ns (thuần userspace, không syscall). Tranh chấp 8 luồng chậm ~4 lần — nhưng strace cho thấy gần như không có syscall futex nào. Đo thật.
03/09/2026
· 8 phút đọc
21
Lát thời gian CFS
Mỗi lượt một task chạy ~3 ms rồi bị đẩy ra — và con số đó cố định dù 2 hay 8 task tranh CPU. Nhưng chỉ khi ghim cùng một CPU mới thấy; và kernel này là EEVDF, không phải CFS. Đo thật.
03/09/2026
· 8 phút đọc
22
nice và ưu tiên
Hai tiến trình tranh một CPU chia phần theo nice: 0 vs 5 ra 75/25, 0 vs 19 ra 98,5/1,5. Nhưng phần chia chỉ phụ thuộc HIỆU nice — nice 10 vs 15 chia y hệt 0 vs 5. Đo thật.
03/09/2026
· 8 phút đọc
23
Đóng vào một core
Ghim tiến trình vào một nhân để giữ cache ấm — nghe hợp lý, nhưng đo ra ghim không nhanh hơn tự do: máy rảnh, dưới tải, kể cả khi ép dời liên tục. Vì scheduler đã tự giữ. Đo thật.
03/09/2026
· 8 phút đọc
24
cgroup bóp CPU
Cấp cho container nguyên 1 CPU mà nó vẫn đứng hình 85 ms mỗi 100 ms — vì 4 luồng đốt chung quota nhanh gấp 4 rồi cả bốn bị đóng băng. Đo thật hiện tượng CFS throttling.
03/09/2026
· 7 phút đọc
25
cgroup giới hạn bộ nhớ
Container ở 255/256 MB — sắp OOM? Không hẳn: nếu 'đầy' là page cache thì container khỏe re, oom_kill=0; nếu là anon thì exit 137. Cùng con số, kết cục trái ngược. Đo thật.
03/09/2026
· 7 phút đọc
26
Nguồn đồng hồ
Đo một khoảng thời gian bằng đồng hồ treo tường (realtime) rồi bị NTP chỉnh giữa chừng: kết quả ra -10 giây, một khoảng âm bất khả. Monotonic thì không bao giờ nhảy. Đo thật.
03/09/2026
· 7 phút đọc
27
Độ phân giải timer
clock_getres báo 1 nano giây, nhưng xin ngủ 1 micro giây thì ngủ thật ~84 micro giây — gấp 84 lần. Có một sàn ~80us do độ trễ đánh thức, không phải độ phân giải đồng hồ. Đo thật.
03/09/2026
· 7 phút đọc
28
Tín hiệu
Giao một tín hiệu tới handler tốn ~500 ns. Nhưng cú đắt hơn là EINTR: một tín hiệu tới giữa nanosleep(10 giây) làm nó trả về sau 0,055 giây — và nếu bỏ giá trị trả về, bạn không hề biết. Đo thật.
03/09/2026
· 8 phút đọc
29
Zombie và reaping
Một tiến trình zombie chiếm RSS ~0 KB — đo bộ nhớ thì tưởng vô hại. Nhưng nó giữ một slot PID, và đủ zombie thì cạn PID, fork() thất bại khắp máy dù RAM còn đầy. Đo thật.
03/09/2026
· 7 phút đọc
30
Cạn file descriptor
Hai lỗi gần như y hệt: 'Too many open files' (EMFILE, mỗi tiến trình) và '...in system' (ENFILE, toàn hệ thống). Vá nhầm bằng ulimit thì gõ cả buổi không hết. Đo thật.
03/09/2026
· 7 phút đọc
31
epoll so với poll
Theo dõi 10.000 fd: poll tốn 247.670 ns mỗi lần gọi, epoll chỉ 133 ns — nhanh ~1860 lần. Nhưng epoll không phải O(1) thần kỳ, mà là O(số fd sẵn sàng). Đo thật.
03/09/2026
· 7 phút đọc
32
Direct I/O
O_DIRECT bỏ qua page cache — tưởng nhanh hơn, nhưng đọc lại từ cache (buffered ấm) nhanh gấp 5 lần. O_DIRECT là I/O không cache, không phải I/O nhanh. Và một môi trường suýt lừa cả phép đo. Đo thật.
03/09/2026
· 8 phút đọc
33
sendfile
sendfile gửi cả file 512 MB trong ĐÚNG 1 syscall thay vì 16.384 read+write. Nhưng trên loopback cache ấm, thông lượng thô của nó không hơn read+write đệm 64K — vì tôi đo nhầm đại lượng. Đo thật.
03/09/2026
· 7 phút đọc
34
Ngăn xếp và heap
Cấp phát trên stack tốn ~1 ns cho mọi kích cỡ (chỉ dời con trỏ), malloc chậm 6-14 lần. Nhưng stack giới hạn 8MB và tràn thì crash không báo, còn heap trả NULL. Đo thật.
03/09/2026
· 7 phút đọc
35
malloc và arena
Ép malloc về một arena chỉ làm 8 luồng chậm nhẹ (556 xuống 434 triệu op/s) — tưởng arena không quan trọng. Nhưng tắt luôn tcache thì nó sập còn 23. Hai lớp chống tranh khóa. Đo thật.
03/09/2026
· 8 phút đọc
36
False sharing
Hai luồng ghi hai biến RIÊNG, không hề chia sẻ gì trong code — nhưng để chung một cache line thì chậm ~3,7 lần. Tranh chấp vô hình ở tầng mã nguồn. Đo thật.
03/09/2026
· 7 phút đọc
37
Đọc /proc
Đọc /proc/self/stat tốn ~1,5 us — tưởng mọi file /proc đều rẻ vậy. Nhưng smaps tốn 12 us và còn phình theo bộ nhớ tiến trình, vì nhân phải đi bộ bảng trang. Đo thật.
03/09/2026
· 7 phút đọc
38
Load average
Load average trên Linux đếm cả tiến trình kẹt chờ I/O — nên 4 tiến trình chờ đĩa đẩy load lên 4 dù CPU rảnh. Và nó dâng chậm vì làm mượt. Load cao ≠ CPU quá tải. Đo thật.
03/09/2026
· 7 phút đọc
39
Chi phí của strace
Mỗi syscall dưới strace tốn ~80 micro giây thay vì 126 nano giây — chậm 630 lần. Nên mọi số đo thời gian dưới strace đều sai. Chính công cụ đo suốt sê-ri này là kẻ nói dối lớn nhất về thời gian. Đo thật.
03/09/2026
· 7 phút đọc
40
Bộ đếm hiệu năng
Định đo cache-miss và branch-miss bằng perf, nhưng máy ảo không lộ PMU nên perf trả <not supported>. Phải quay về đồng hồ — nó vẫn lộ hiệu ứng cache chậm 200 lần. Đo thật.
03/09/2026
· 7 phút đọc
41
Số nguyên tử và CAS
Bộ đếm atomic nhanh hơn mutex 3 lần — nhưng nó KHÔNG scale: thông lượng tụt khi thêm luồng, vì dòng cache nảy giữa các nhân. Cách scale thật là không chia sẻ. Đo thật.
03/09/2026
· 7 phút đọc
42
Trang chỉ đọc
Ghi vào trang chỉ đọc gây SIGSEGV — nhưng cùng cái bẫy phần cứng đó, sau fork kernel lại lặng lẽ copy trang chứ không giết tiến trình. Đo thật trong container.
03/09/2026
· 9 phút đọc
43
fork rồi exec
fork một tiến trình 256MB chỉ tốn 133 micro giây, không phải chục mili giây — vì COW nhân bảng trang chứ không copy dữ liệu. Và mọi thứ vừa nhân bị exec vứt sạch. Đo thật trong container.
03/09/2026
· 9 phút đọc
44
Nhóm tiến trình
Giết PID tiến trình cha không giết được các con — chúng thành mồ côi và chạy tiếp. Muốn hạ cả cụm phải nhắm nhóm tiến trình: kill(-pgid) một phát hạ tất. Đo thật trong container.
03/09/2026
· 8 phút đọc
45
Giới hạn tài nguyên
Chạm giới hạn fd cho lỗi EMFILE tự nâng được không cần root; nhưng nâng xong lại đụng một trần khác (ENFILE toàn hệ thống) không phải của mình. Đo thật, và để errno nói thật đang chạm trần nào.
03/09/2026
· 8 phút đọc