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