Đọc syscall bằng strace — nhìn chương trình gọi kernel
Dùng strace quan sát syscall thật: đọc output, nhận diện nhóm (file, memory, process, network), và chẩn đoán chương trình chậm hay treo ở đâu.
TL;DR: strace là công cụ Linux ghi lại mọi system call một chương trình phát ra, kèm đối số và giá trị trả về. Nó dựa trên system call ptrace(2) — cơ chế cho một tiến trình theo dõi tiến trình khác. Mỗi dòng có dạng tên(đối_số...) = giá_trị_trả_về; lỗi hiện thành = -1 ENOENT (mô tả). Bốn nhóm hay gặp: file (openat, read, write, close), memory (mmap, brk, mprotect), process (execve, clone, wait4, exit_group), network (socket, connect, sendto). Dùng strace -c để đếm và xếp hạng syscall theo thời gian; dùng strace -p PID để soi một tiến trình đang treo — dòng cuối đứng im cho biết nó đang chờ ở đâu (thường read, futex, poll).
Ở ba bài trước, "system call" còn là khái niệm — bạn tin rằng chương trình gọi kernel, nhưng chưa thấy. Bài này biến nó thành thứ sờ được. Chỉ với một lệnh, bạn nhìn được toàn bộ cuộc đối thoại giữa chương trình và hệ điều hành: từng file nó mở, từng byte nó đọc, từng lần nó xin thêm bộ nhớ.
strace không chỉ để học. Nó là công cụ chẩn đoán hàng ngày: khi một service treo mà không log gì, khi một chương trình chậm bí ẩn, khi một lệnh báo "permission denied" mà bạn không rõ file nào — strace cho câu trả lời trong vài giây. Bài này dạy bạn đọc output của nó và rút ra kết luận.
1. Analogy — máy ghi âm cuộc gọi tổng đài
Ở bài 02, mỗi system call là một cú "gọi tổng đài" nhờ kernel làm việc. strace giống như đặt một máy ghi âm lên đường dây đó: nó không thay đổi cuộc gọi, chỉ chép lại từng câu — chương trình xin gì, kernel trả gì.
Bản ghi âm này cực kỳ hữu ích khi có sự cố. Chương trình "không mở được file"? Nghe lại băng, thấy nó gọi openat("config.yaml") và nhận về ENOENT — file không tồn tại, không phải bug code. Chương trình treo? Băng dừng ở read(...) và không có câu trả lời nào tiếp theo — nó đang ngồi chờ dữ liệu.
| Máy ghi âm tổng đài | strace |
|---|---|
| Đường dây điện thoại | Ranh giới user/kernel |
| Mỗi cuộc gọi | Mỗi system call |
| Câu hỏi của khách | Tên syscall + đối số |
| Câu trả lời của tổng đài | Giá trị trả về |
| "Không có ai nhấc máy" | Syscall block, chưa trả về (treo) |
| Nghe lại để tìm sự cố | Đọc trace để chẩn đoán |
strace = danh sách mọi lần chương trình vượt biên sang kernel, kèm hỏi gì và nhận gì. Nó không cho thấy tính toán bên trong user mode — chỉ những cú chạm tới hệ điều hành.
2. strace là gì — cơ chế ptrace
strace chạy (hoặc gắn vào) một chương trình và in ra mọi system call nó thực hiện. Dạng dùng cơ bản:
strace ./chuong_trinh # chay va trace tu dau
strace -f ./chuong_trinh # trace ca tien trinh con (fork/clone)
strace -p 12345 # gan vao tien trinh dang chay co PID 12345
strace -o log.txt ./ct # ghi trace ra file thay vi man hinh
Nó làm được điều này nhờ một system call tên ptrace(2) — chính là cơ chế mà debugger (như gdb) dùng. ptrace cho phép một tiến trình "cha theo dõi" (ở đây là strace) chặn tiến trình đích mỗi khi tiến trình đó vào và ra một system call. Tại mỗi điểm chặn, strace đọc thanh ghi của tiến trình đích để biết số hiệu syscall và đối số (nhớ bài 02: số hiệu ở rax, đối số ở rdi/rsi/rdx...), giải mã ra dạng người đọc được, rồi cho tiến trình chạy tiếp.
sequenceDiagram
participant T as strace (theo doi)
participant P as Tien trinh dich
participant K as Kernel
P->>K: bat dau mot syscall
K-->>T: ptrace chan (syscall-enter)
T->>T: doc rax/rdi/rsi -> in "openat(...)"
T->>K: cho tien trinh chay tiep
K->>K: chay handler syscall
K-->>T: ptrace chan (syscall-exit)
T->>T: doc rax -> in "= 3"
T->>P: cho chay tiep toi syscall keVì mỗi syscall giờ bị chặn hai lần (vào và ra), mỗi lần chặn buộc kernel chuyển hẳn sang tiến trình strace để đọc thanh ghi rồi chuyển ngược lại — đó là context switch (đổi hẳn tiến trình, đắt hơn nhiều so với mode switch của một syscall thường, xem bài 02). Nên chương trình chạy dưới strace chậm hơn đáng kể — có thể vài lần với chương trình gọi nhiều syscall. Điều này nghĩa là: (1) đừng strace một service production nhạy hiệu năng nếu không cần; (2) con số thời gian tuyệt đối trong trace không phản ánh tốc độ thật khi chạy không trace. Dùng nó để hiểu hành vi và tỉ lệ, không phải benchmark tuyệt đối.
3. Đọc một dòng strace
Cấu trúc mỗi dòng luôn giống nhau:
openat(AT_FDCWD, "/etc/hosts", O_RDONLY|O_CLOEXEC) = 3
\____/ \___________________________________/ \_/
ten doi so (giong tham so ham C) gia tri tra ve
- Tên:
openat— system call được gọi. - Đối số: giống tham số của hàm C tương ứng.
stracegiải mã cả hằng số (O_RDONLY) và chuỗi ("/etc/hosts") cho dễ đọc. - Giá trị trả về:
= 3—openattrả về một file descriptor (số 3; 0/1/2 là stdin/stdout/stderr đã dùng).
Lỗi hiện rất rõ:
openat(AT_FDCWD, "khong_ton_tai.txt", O_RDONLY) = -1 ENOENT (No such file or directory)
Nhớ bài 02: kernel trả -errno; strace dịch -2 thành tên hằng ENOENT và mô tả (No such file or directory). Đây là dòng bạn săn khi chương trình "không tìm thấy file" hay "permission denied" (EACCES).
Đây là đoạn đầu điển hình khi chạy strace ls (rút gọn — số và địa chỉ thật sẽ khác trên máy bạn):
execve("/usr/bin/ls", ["ls"], 0x7ffe... /* 30 vars */) = 0
brk(NULL) = 0x55e...
openat(AT_FDCWD, "/etc/ld.so.cache", O_RDONLY|O_CLOEXEC) = 3
fstat(3, {st_mode=S_IFREG|0644, st_size=41547, ...}) = 0
mmap(NULL, 41547, PROT_READ, MAP_PRIVATE, 3, 0) = 0x7f...
close(3) = 0
openat(AT_FDCWD, "/lib/x86_64-linux-gnu/libc.so.6", O_RDONLY|O_CLOEXEC) = 3
read(3, "\177ELF\2\1\1\3\0\0\0\0\0\0\0\0"..., 832) = 832
mmap(NULL, 2020..., PROT_READ, MAP_PRIVATE|MAP_DENYWRITE, 3, 0) = 0x7f...
mprotect(0x7f..., 16384, PROT_READ) = 0
close(3) = 0
write(1, "Documents Downloads file.txt\n", 31) = 31
exit_group(0) = ?
Ngay cả khi chưa hiểu từng syscall, bạn thấy được câu chuyện: chương trình khởi động (execve), nạp thư viện C (openat + read + mmap cái libc.so.6), làm việc của nó (write kết quả ra stdout), rồi thoát (exit_group).
4. Bốn nhóm syscall thường gặp
Không ai đọc từng dòng. Mẹo là phân nhóm để nắm bức tranh lớn:
| Nhóm | Syscall tiêu biểu | Chương trình đang làm gì |
|---|---|---|
| File / I/O | openat, read, write, close, fstat, lseek | Đọc/ghi file, kiểm tra metadata |
| Memory | mmap, munmap, brk, mprotect | Xin/trả bộ nhớ, ánh xạ file/thư viện, đổi quyền trang |
| Process | execve, clone, fork, wait4, exit_group, kill | Tạo/kết thúc tiến trình, chờ tiến trình con |
| Network | socket, connect, bind, listen, accept, sendto, recvfrom | Mở kết nối, gửi/nhận dữ liệu mạng |
strace có thể lọc theo nhóm sẵn:
strace -e trace=file ./ct # chi syscall lien quan file/path
strace -e trace=network ./ct # chi syscall mang
strace -e trace=memory ./ct # chi syscall bo nho
strace -e trace=%process ./ct # chi syscall vong doi tien trinh
Khi debug "chương trình mở nhầm file", strace -e trace=file cắt hết nhiễu, chỉ để lại các thao tác path — thường thấy ngay dòng ENOENT thủ phạm. Các nhóm này ánh xạ trực tiếp về các phần cứng/tài nguyên mà bài 01 nói user code phải xin qua kernel.
5. strace -c — đâu là syscall đắt nhất?
Với chương trình phát hàng nghìn syscall, đọc dòng-theo-dòng là bất khả thi. Cờ -c gom lại thành bảng tổng hợp: đếm mỗi loại syscall được gọi bao nhiêu lần, tổng thời gian, và số lỗi.
strace -c ls
% time seconds usecs/call calls errors syscall
------ ----------- ----------- --------- --------- ----------------
24.30 0.000112 7 16 mmap
18.20 0.000084 8 10 3 openat
15.10 0.000070 5 13 close
12.00 0.000055 4 12 read
9.80 0.000045 6 7 fstat
8.10 0.000037 12 3 write
...
------ ----------- ----------- --------- --------- ----------------
100.00 0.000461 78 3 total
Cách đọc bảng (con số cụ thể thay đổi theo máy — điều đáng chú ý là hình dạng):
- Cột calls: syscall nào bị gọi nhiều nhất. Nhiều
read/writebất thường gợi ý I/O không có buffer. - Cột errors: syscall nào trả lỗi. Ở đây
openatcó 3 lỗi — thường là tìm thư viện/config ở nhiều chỗ, có chỗENOENT(bình thường), nhưng số lỗi lớn bất thường đáng điều tra. - Cột % time / seconds: syscall nào ngốn thời gian nhất. Đây là nơi bắt đầu tối ưu — dù nhớ cảnh báo section 2 rằng thời gian dưới
stracebị phóng đại.
strace -c chính là công cụ trả lời câu "chương trình này chậm vì gọi quá nhiều syscall gì?" — nối thẳng với bài học chi phí syscall ở bài 02.
6. Chẩn đoán treo và chậm
Chương trình treo. Một service không phản hồi nhưng CPU gần 0%: nó không bận tính, mà đang chờ một syscall block. Gắn strace vào nó:
strace -p 12345
Nếu output đứng im ở một dòng chưa có =:
read(6,
thì tiến trình đang kẹt trong read trên fd 6, chờ dữ liệu không bao giờ tới (có thể là socket phía kia không gửi, hay pipe rỗng). Vài syscall "chờ" hay gặp:
read(.../recvfrom(...— chờ dữ liệu file/socket.futex(...— chờ một lock/điều kiện (thường là deadlock hoặc lock bị giữ; sẽ học ở module đồng bộ).poll(.../epoll_wait(...— chờ sự kiện I/O trên nhiều fd.wait4(...— cha đang chờ tiến trình con kết thúc.
Dòng cuối đứng im chính là chỗ nó treo — thông tin vàng mà không log nào cho bạn.
Chương trình chậm. Chạy strace -c (chậm nhiều syscall?) hoặc strace -T (in thời gian mỗi call trong <...>):
strace -T ./ct
read(3, "x", 1) = 1 <0.000004>
read(3, "y", 1) = 1 <0.000005>
read(3, "z", 1) = 1 <0.000004>
... (hang nghin dong read 1 byte)
Nghìn dòng read(...1) = 1 là dấu hiệu kinh điển của đọc không buffer: mỗi byte một syscall, mỗi syscall một mode switch. Fix: đọc theo khối lớn (nhớ bài 02 — gom thành ít syscall lớn).
7. Thử sức — tự đọc một trace
Dưới là trace rút gọn của một chương trình. Trước khi đọc phân tích, hãy tự trả lời: chương trình này làm gì, và có gì bất thường không?
execve("/usr/bin/app", ["app"], ...) = 0
openat(AT_FDCWD, "/etc/app/config.yaml", O_RDONLY) = -1 ENOENT (No such file or directory)
openat(AT_FDCWD, "/home/u/.app.yaml", O_RDONLY) = -1 ENOENT (No such file or directory)
openat(AT_FDCWD, "./config.yaml", O_RDONLY) = 3
read(3, "port: 8080\n", 4096) = 11
close(3) = 0
socket(AF_INET, SOCK_STREAM, IPPROTO_TCP) = 4
connect(4, {sa_family=AF_INET, sin_port=htons(5432), ...},
Gợi ý câu hỏi: hai dòng ENOENT đầu có phải lỗi nghiêm trọng không? Dòng cuối cho biết gì?
Phân tích:
- Hai
openatđầu trảENOENTkhông phải lỗi — đây là chương trình tìm file config theo thứ tự ưu tiên (thư mục hệ thống, rồi home), không thấy thì thử chỗ tiếp. Đến./config.yamlmới thấy (= 3). Đây là mẫu "search path" rất thường gặp; đừng hoảng khi thấyENOENTkiểu này. - Nó
readconfig thấyport: 8080. - Rồi mở một
socketTCP (= 4) và gọiconnecttới cổng 5432 (cổng mặc định của PostgreSQL). Dòngconnectchưa có giá trị trả về — trace dừng ở đây, nghĩa là chương trình đang kẹt khi kết nối database. Nhiều khả năng database không chạy hoặc firewall chặn: đây chính là chỗ treo.
Chỉ từ trace, không cần đọc source, bạn kết luận được: chương trình đọc config OK, nhưng treo khi kết nối PostgreSQL ở cổng 5432.
8. Pitfall — những hiểu nhầm thường gặp
❌ Nhầm 1 — "Mọi dòng ENOENT/= -1 là bug cần sửa."
✅ Không. Rất nhiều lỗi trong trace là bình thường và có chủ đích: tìm thư viện/config qua nhiều đường dẫn, thử một tính năng kernel rồi fallback. Chỉ quan tâm lỗi ở chỗ không nên có — ví dụ read một fd hợp lệ mà trả EIO.
❌ Nhầm 2 — "Thời gian trong strace -c là tốc độ thật của chương trình."
✅ Không. strace chặn mỗi syscall hai lần nên làm chương trình chậm đi và phóng đại thời gian syscall. Dùng con số để so tỉ lệ giữa các syscall và tìm hot spot, không phải để đo tốc độ tuyệt đối. Benchmark thật thì chạy không có strace.
❌ Nhầm 3 — "strace cho thấy mọi thứ chương trình làm."
✅ Chỉ thấy system call — tức những lần chạm kernel. Toàn bộ tính toán trong user mode (vòng lặp, xử lý chuỗi, thuật toán) vô hình với strace. Một chương trình đốt CPU 100% trong một vòng lặp thuần user sẽ cho trace trống trơn — lúc đó cần perf hay profiler, không phải strace.
9. 📚 Deep Dive
Spec / tài liệu tham khảo:
- strace(1) — Linux man page — mọi cờ:
-c,-f,-e trace=,-T,-p,-t. Nguồn chính cho các lệnh trong bài. - ptrace(2) — Linux man page — cơ chế nền tảng
strace(vàgdb) dựa vào để chặn syscall của tiến trình khác. - syscalls(2) — Linux man page — tra nghĩa từng syscall bạn thấy trong trace; hoặc
man 2 <tên>(ví dụman 2 openat).
Ghi chú: Trên macOS công cụ tương đương là dtruss/dtrace; trên Windows là các ETW/Process Monitor. Bài này bám Linux vì đó là nền server phổ biến nhất.
10. Liên hệ các bài khác
- Bài 02 — System call là gì:
stracehiện đúng những syscall bạn đã học ở mức thanh ghi; giá trị trả về âm (-errno) chính là thứ hiện thành-1 ENOENT. - Bài 03 — Interrupt, trap & exception:
stracecũng in các signal (SIGSEGV,SIGPIPE) mà exception và sự kiện sinh ra, không chỉ syscall. - Bài 05 — Mini-challenge đếm syscall: bạn tự cầm
strace -cmổ xẻ một lệnh thật — áp dụng trực tiếp bài này. - Bài 01 — Kernel mode vs user mode: bốn nhóm syscall ánh xạ về các tài nguyên phần cứng mà user code phải xin qua kernel.
11. Tóm tắt
straceghi lại mọi system call của một chương trình, dựa trênptrace(2)— cơ chế một tiến trình theo dõi tiến trình khác (cùng nền vớigdb).- Mỗi dòng:
tên(đối_số) = kết_quả; lỗi hiện= -1 TÊNLỖI (mô tả), ví dụ-1 ENOENTkhi file không tồn tại. - Phân bốn nhóm để đọc nhanh: file (
openat/read/write), memory (mmap/brk), process (execve/clone/wait4), network (socket/connect/sendto). Lọc bằng-e trace=file|network|memory|%process. strace -cxếp hạng syscall theo calls / errors / thời gian — tìm hot spot và anti-pattern (nghìnread1 byte).strace -p PIDsoi tiến trình đang treo: dòng cuối đứng im (read,futex,poll,wait4) chỉ ra chỗ nó chờ.- Cảnh giác: nhiều
ENOENTlà bình thường (search path); thời gian dướistracebị phóng đại;stracekhông thấy tính toán thuần trong user mode.
12. Tự kiểm tra
Q1Một service không phản hồi nhưng CPU gần 0%. Vì sao strace -p PID là công cụ đúng, và dòng output đứng im nói lên điều gì?▸
strace -p PID toả sáng: nó gắn vào tiến trình đang chạy và in syscall kế tiếp. Nếu output đứng im ở một dòng chưa có dấu =, ví dụ read(6, hay futex(..., thì đó chính là syscall mà tiến trình đang kẹt trong đó: read/recvfrom = chờ dữ liệu file/socket; futex = chờ một lock (có thể deadlock); poll/epoll_wait = chờ sự kiện I/O; wait4 = chờ tiến trình con. Dòng cuối đứng im là chỗ treo — thông tin mà không log ứng dụng nào cho bạn.Q2Trong trace bạn thấy ba dòng openat trả -1 ENOENT liên tiếp rồi một dòng openat trả = 3. Đây có phải lỗi không? Giải thích.▸
ENOENT đầu là ba chỗ nó thử mà không thấy (thư mục hệ thống, home...), hoàn toàn bình thường; đến chỗ thứ tư nó thấy file và openat trả về file descriptor 3. Kết quả cuối là thành công. Bài học: đừng hoảng khi thấy ENOENT trong trace — chỉ quan tâm lỗi ở chỗ không nên có, ví dụ syscall cuối cùng của một chuỗi tìm kiếm cũng thất bại, hoặc lỗi trên một fd đã mở hợp lệ.Q3strace -c cho thấy syscall read được gọi 50.000 lần trong khi chương trình chỉ đọc một file nhỏ. Điều này gợi ý vấn đề gì, và fix ra sao?▸
read khổng lồ cho một file nhỏ là dấu hiệu kinh điển của đọc không buffer: chương trình đọc từng byte (hoặc từng đoạn rất nhỏ) một, mỗi lần là một system call. Nhớ bài 02: mỗi syscall là một mode switch tốn cỡ hàng trăm ns; 50.000 lần cộng dồn thành chi phí lớn hơn nhiều so với bản thân việc đọc dữ liệu. Fix: đọc theo khối lớn — gọi read với buffer vài KB mỗi lần, hoặc dùng thư viện I/O có buffer sẵn (như fread/BufferedReader). Gom nhiều thao tác nhỏ thành ít syscall lớn để khấu hao chi phí vượt biên.Q4Một chương trình đốt 100% CPU nhưng strace của nó gần như trống trơn (rất ít syscall). Vì sao, và bạn nên dùng công cụ gì thay thế?▸
strace chỉ thấy system call — những lần chương trình vượt biên sang kernel. Nếu chương trình đang đốt CPU trong một vòng lặp tính toán thuần user mode (xử lý chuỗi, thuật toán, busy-loop), nó không gọi kernel nên trace gần như trống. Điều đó tự nó là một manh mối: bottleneck nằm trong code user, không phải I/O hay syscall. Công cụ đúng là profiler CPU như perf (ví dụ perf top hay perf record), thứ lấy mẫu con trỏ lệnh để cho biết hàm nào ngốn CPU — chứ không phải strace. Chọn công cụ theo bản chất vấn đề: chờ I/O/syscall thì strace, đốt CPU thuần thì perf.Q5Vì sao không nên tin thời gian tuyệt đối mà strace -c báo cáo như tốc độ thật của chương trình?▸
strace dựa trên ptrace, nó chặn mỗi syscall hai lần (khi vào và khi ra kernel), mỗi lần chặn kéo theo một cú chuyển sang tiến trình strace để đọc thanh ghi và giải mã. Điều này thêm overhead lớn vào mỗi syscall, làm chương trình chạy chậm hơn nhiều lần so với khi không trace, và phóng đại thời gian mỗi syscall trong báo cáo. Con số vẫn hữu ích để so tỉ lệ giữa các syscall (cái nào ngốn nhiều nhất, cái nào bị gọi nhiều nhất) và tìm hot spot, nhưng không phản ánh tốc độ tuyệt đối khi chạy bình thường. Muốn đo thời gian thật, chạy chương trình không có strace và dùng công cụ benchmark riêng.Bài tiếp theo: Mini-challenge — mổ xẻ một lệnh bằng strace
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