Missing Semester 计算机教育中缺失的一课 Lecture 04 Debugging and Profiling
前言
断点调试真的比 cph cout 强太多了!!
一、调试 Debugging
1. gdb 调试
直接在练习中说吧。
#include <iostream>
#include <string>
#include <vector>
int g_balance = 100;
// ---------- Exercise 1: breakpoints and stepping ----------
int compute_subtotal(const std::vector<int>& prices) {
int subtotal = 0;
for (int price : prices) {
subtotal += price;
}
return subtotal;
}
int apply_discount(int subtotal, int percent) {
int discount = subtotal * percent / 100;
return subtotal - discount;
}
int add_shipping(int amount) {
const int free_shipping_threshold = 100;
const int shipping_fee = 12;
if (amount >= free_shipping_threshold) {
return amount;
}
return amount + shipping_fee;
}
int calculate_order(const std::vector<int>& prices, int discount_percent) {
int subtotal = compute_subtotal(prices);
int discounted = apply_discount(subtotal, discount_percent);
int total = add_shipping(discounted);
return total;
}
void run_basic_demo() {
std::vector<int> prices{30, 45, 20};
int discount_percent = 10;
int total = calculate_order(prices, discount_percent);
std::cout << "final total = " << total << '\n';
}
// ---------- Exercise 2: conditional breakpoints ----------
int transaction_amount(int id) {
return id * 17 - 8;
}
void process_transaction(int id) {
int amount = transaction_amount(id);
int fee = amount > 100 ? 5 : 1;
int net = amount - fee;
std::cout << "transaction " << id
<< ": amount=" << amount
<< ", net=" << net << '\n';
}
void run_condition_demo() {
for (int id = 1; id <= 10; ++id) {
process_transaction(id);
}
}
// ---------- Exercise 3: watchpoints ----------
void update_balance(int round) {
if (round == 4) {
g_balance = -999; // Intentional corruption.
} else {
g_balance += 10;
}
}
void run_watch_demo() {
std::cout << "initial balance = " << g_balance << '\n';
for (int round = 0; round < 7; ++round) {
update_balance(round);
std::cout << "round " << round
<< ", balance = " << g_balance << '\n';
}
}
// ---------- Exercise 4: crash and backtrace ----------
int read_number(const int* pointer) {
return *pointer; // Intentional null-pointer dereference.
}
int decode_packet(const int* payload) {
int value = read_number(payload);
return value + 1;
}
int handle_packet(bool malformed) {
int valid_payload = 41;
const int* payload = malformed ? nullptr : &valid_payload;
return decode_packet(payload);
}
void run_crash_demo() {
int result = handle_packet(true);
std::cout << "result = " << result << '\n';
}
// ---------- Program entry ----------
void print_usage(const char* program_name) {
std::cout << "Usage: " << program_name
<< " {basic|condition|watch|crash}\n";
}
int main(int argc, char* argv[]) {
if (argc != 2) {
print_usage(argv[0]);
return 1;
}
std::string mode = argv[1];
if (mode == "basic") {
run_basic_demo();
} else if (mode == "condition") {
run_condition_demo();
} else if (mode == "watch") {
run_watch_demo();
} else if (mode == "crash") {
run_crash_demo();
} else {
print_usage(argv[0]);
return 1;
}
return 0;
}
首先,对于以上的 cpp 代码,如果想用 gdb 调试,需要在编译时使用 -g参数写入完整调试信息。
❯ ./gdb basic
final total = 98
❯ ./gdb condition
transaction 1: amount=9, net=8
transaction 2: amount=26, net=25
transaction 3: amount=43, net=42
transaction 4: amount=60, net=59
transaction 5: amount=77, net=76
transaction 6: amount=94, net=93
transaction 7: amount=111, net=106
transaction 8: amount=128, net=123
transaction 9: amount=145, net=140
transaction 10: amount=162, net=157
❯ ./gdb watch
initial balance = 100
round 0, balance = 110
round 1, balance = 120
round 2, balance = 130
round 3, balance = 140
round 4, balance = -999
round 5, balance = -989
round 6, balance = -979
❯ ./gdb crash
[1] 145115 segmentation fault (core dumped) ./gdb crash
在分别运行四种模式后,可以发现前三个正常结束,第四个报了段错误,而且第三个的余额从第四轮开始突然变成 -999 了。接下来就是想办法调试问题。
❯ gdb ./gdb
GNU gdb (GDB) 17.2
Copyright (C) 2025 Free Software Foundation, Inc.
License GPLv3+: GNU GPL version 3 or later <http://gnu.org/licenses/gpl.html>
This is free software: you are free to change and redistribute it.
There is NO WARRANTY, to the extent permitted by law.
Type "show copying" and "show warranty" for details.
This GDB was configured as "x86_64-pc-linux-gnu".
Type "show configuration" for configuration details.
For bug reporting instructions, please see:
<https://www.gnu.org/software/gdb/bugs/>.
Find the GDB manual and other documentation resources online at:
<http://www.gnu.org/software/gdb/documentation/>.
For help, type "help".
Type "apropos word" to search for commands related to "word"...
Reading symbols from ./gdb...
(gdb)
在运行 gdb 后,此时就进入了调试界面,此时再输入 quit 就是退出。之后,输入 list 就是列出源码,默认从 main 函数开始。如果要查看之后的源码,可以直接按回车执行上一条命令。查看之前的源码就是 list -,查看指定行就是 list row,查看范围行就是 list l,r,查看指定函数就直接输入函数名字即可。不过一般就不在 gdb 中查看了,因为这个命令确实不太方便。
(gdb) run basic
Starting program: /home/Bluuue/Desktop/gdb basic
[Thread debugging using libthread_db enabled]
Using host libthread_db library "/usr/lib/libthread
_db.so.1".
final total = 98
[Inferior 1 (process 147525) exited normally]
(gdb) set args basic
(gdb) show args
Argument list to give program being debugged when i
t is started is "basic".
(gdb) run
Starting program: /home/Bluuue/Desktop/gdb basic
[Thread debugging using libthread_db enabled]
Using host libthread_db library "/usr/lib/libthread
_db.so.1".
final total = 98
[Inferior 1 (process 147604) exited normally]
run 命令就相当于运行程序。如果不想每次都手动输入参数,可以用 set args xxx 提前保存参数,使用 show args 查看,然后直接运行 run 就默认带参了。清空参数就可以直接 set args,后面不加任何东西即可。
(gdb) break calculate_order
Breakpoint 1 at 0x555555556320: file gdb.cpp, line 33.
(gdb) break 36
Breakpoint 2 at 0x55555555634e: file gdb.cpp, line 36.
(gdb) i b
Num Type Disp Enb Address What
1 breakpoint keep y 0x0000555555556320 in calculate_order(std::vector<int, std::allocator<int> > const&, int) at gdb.cpp:33
2 breakpoint keep y 0x000055555555634e in calculate_order(std::vector<int, std::allocator<int> > const&, int) at gdb.cpp:36
break 可以在指定位置设置断点,可以接受函数名称或行号。info breakpoint 可以查看当前所有断点的信息,包括位置和是否启动。
(gdb) run
Starting program: /home/Bluuue/Desktop/gdb basic
[Thread debugging using libthread_db enabled]
Using host libthread_db library "/usr/lib/libthread_db.so.1".
Breakpoint 1, calculate_order (
prices=std::vector of length 3, capacity 3 = {...},
discount_percent=10) at gdb.cpp:33
33 int subtotal = compute_subtotal(prices);
(gdb) backtrace
#0 calculate_order (
prices=std::vector of length 3, capacity 3 = {...},
discount_percent=10) at gdb.cpp:33
#1 0x00005555555563ad in run_basic_demo () at gdb.cpp:42
#2 0x00005555555567d2 in main (argc=2, argv=0x7fffffffdf38)
at gdb.cpp:126
(gdb) delete 2
之后再输入 run 运行,程序就会在断点的这一行停止运行,此时再运行 list 给出的就是当前断点附近的源码。此时输入 backtrace 就可以查看当前的调用栈,之后输入 continue 就可以继续执行。输入 delete id 就可以删除指定编号的断点,直接 delete 就是删除所有断点。输入 disable id 就可以暂时禁用断点,输入 enable id 就是重新启用。
Breakpoint 5, calculate_order (
prices=std::vector of length 3, capacity 3 = {...},
discount_percent=10) at gdb.cpp:33
33 int subtotal = compute_subtotal(prices);
(gdb) info args
prices = std::vector of length 3, capacity 3 = {30, 45, 20}
discount_percent = 10
当断点停在 calculate_order 这里时,此时输入 info args 就可以查看当前函数的参数,vector 也可以自动展开。
(gdb) print discount_percent
$1 = 10
(gdb) print discount_percent + 5
$2 = 15
(gdb) print discount_percent == 10
$3 = true
(gdb) p prices
$4 = std::vector of length 3, capacity 3 = {30, 45, 20}
(gdb) p prices.size()
$5 = 3
(gdb) p prices[1]
$6 = 45
(gdb) p $2
$7 = 15
print 命令可以用来查看变量信息或计算表达式的值,其中表达式也可以是 a.size() 这种。对于前面的 $d,gdb 会自动保存之前的打印结果,这个也是可以使用 print 打印的。
(gdb) b 36
Breakpoint 6 at 0x55555555634e: file gdb.cpp, line 36.
(gdb) continue
Continuing.
Breakpoint 6, calculate_order (
prices=std::vector of length 3, capacity 3 = {...},
discount_percent=10) at gdb.cpp:36
36 return total;
(gdb) info locals
subtotal = 95
discounted = 86
total = 98
info locals 可以查看当前的局部变量信息。
(gdb) next
34 int discounted = apply_discount(subtotal, discount_percent);
next 可以完整执行当前停留的这行代码,然后停在下一行之前。
(gdb) ptype subtotal
type = int
(gdb) p/d total
$8 = 98
(gdb) p/x total
$9 = 0x62
(gdb) p/t total
$10 = 1100010
ptype 可以输出变量的类型,p/d 以十进制输出变量,p/x 是十六进制,p/t 是二进制。
Breakpoint 1, calculate_order (
prices=std::vector of length 3, capacity 3 = {...},
discount_percent=10) at gdb.cpp:33
33 int subtotal = compute_subtotal(prices);
(gdb) step
compute_subtotal (prices=std::vector of length 3, capacity 3 = {...})
at gdb.cpp:9
9 int compute_subtotal(const std::vector<int>& prices) {
(gdb) bt
#0 compute_subtotal (
prices=std::vector of length 3, capacity 3 = {...}) at gdb.cpp:9
#1 0x000055555555632c in calculate_order (
prices=std::vector of length 3, capacity 3 = {...},
discount_percent=10) at gdb.cpp:33
#2 0x00005555555563ad in run_basic_demo () at gdb.cpp:42
#3 0x00005555555567d2 in main (argc=2, argv=0x7fffffffdf38)
at gdb.cpp:126
(gdb) finish
Run till exit from #0 compute_subtotal (
prices=std::vector of length 3, capacity 3 = {...}) at gdb.cpp:11
0x000055555555632c in calculate_order (
prices=std::vector of length 3, capacity 3 = {...},
discount_percent=10) at gdb.cpp:33
33 int subtotal = compute_subtotal(prices);
Value returned is $5 = 95
和 next 不同,step 在碰到函数时会进入这个函数然后停止。如果不想在当前函数里待着了,就可以用 finish 直接执行完当前函数返回。
(gdb) run
Starting program: /home/Bluuue/Desktop/gdb crash
[Thread debugging using libthread_db enabled]
Using host libthread_db library "/usr/lib/libthread_db.so.1".
Program received signal SIGSEGV, Segmentation fault.
0x000055555555662c in read_number (pointer=0x0) at gdb.cpp:91
91 return *pointer; // Intentional null-pointer dereference.
(gdb) frame
#0 0x000055555555662c in read_number (pointer=0x0) at gdb.cpp:91
91 return *pointer; // Intentional null-pointer dereference.
(gdb) bt full
#0 0x000055555555662c in read_number (pointer=0x0) at gdb.cpp:91
No locals.
#1 0x0000555555556648 in decode_packet (payload=0x0) at gdb.cpp:95
value = 1216096691
#2 0x0000555555556699 in handle_packet (malformed=true) at gdb.cpp:102
valid_payload = 41
payload = 0x0
#3 0x00005555555566c1 in run_crash_demo () at gdb.cpp:106
result = 0
#4 0x0000555555556835 in main (argc=2, argv=0x7fffffffdf38)
at gdb.cpp:132
mode = "crash"
(gdb) frame 1
#1 0x0000555555556648 in decode_packet (payload=0x0) at gdb.cpp:95
95 int value = read_number(payload);
在传入 crash 参数执行时,此时程序会发生段错误并报错。此时可以用 frame 查看当前所在的函数以及当前执行到的行。其中,#0 表示当前栈帧的编号为 0,这个也可以使用 bt full 查看所有调用栈及其参数和局部变量。除此之外,还可以使用 frame 1 切换栈帧,然后打印这个函数的局部变量。这样就可以层层往上检查,看是哪个函数传入了错误的指针。
(gdb) frame 0
#0 0x000055555555662c in read_number (pointer=0x0) at gdb.cpp:91
91 return *pointer; // Intentional null-pointer dereference.
(gdb) up
#1 0x0000555555556648 in decode_packet (payload=0x0) at gdb.cpp:95
95 int value = read_number(payload);
当然,不一定每次往上走都要用 frame 切换,可以直接使用 up 往上走。同理,也可以使用 down 往下走。这样就可以检查出程序到底是在哪发生错误的了。
(gdb) b 64 if id==7
Breakpoint 1 at 0x251d: file gdb.cpp, line 64.
(gdb) run
Starting program: /home/Bluuue/Desktop/gdb condition
[Thread debugging using libthread_db enabled]
Using host libthread_db library "/usr/lib/libthread_db.so.1".
transaction 1: amount=9, net=8
transaction 2: amount=26, net=25
transaction 3: amount=43, net=42
transaction 4: amount=60, net=59
transaction 5: amount=77, net=76
transaction 6: amount=94, net=93
Breakpoint 1, run_condition_demo () at gdb.cpp:64
64 process_transaction(id);
(gdb) b 64
Breakpoint 2 at 0x55555555651d: file gdb.cpp, line 64.
(gdb) i b
Num Type Disp Enb Address What
2 breakpoint keep y 0x000055555555651d in run_condition_
demo() at gdb.cpp:64
(gdb) condition 2 id == 7
在使用 condition 参数运行后,对于循环语句,如果只想让其在某个条件下停止,可以使用 b xx if xxx 条件断点,这样就不用每次手动 step 到想停下的时刻了。如果想要给已有的断点添加条件,可以使用 condition id xxx 给指定编号的断点添加条件。如果想要删除条件,直接 condition id 即可。
(gdb) watch g_balance
Hardware watchpoint 3: g_balance
(gdb) i b
Num Type Disp Enb Address What
3 hw watchpoint keep y g_balance
(gdb) run
Starting program: /home/Bluuue/Desktop/gdb watch
[Thread debugging using libthread_db enabled]
Using host libthread_db library "/usr/lib/libthread_db.so.1".
initial balance = 100
Hardware watchpoint 3: g_balance
Old value = 100
New value = 110
update_balance (round=0) at gdb.cpp:76
76 }
在使用 watch 参数运行后,此时可以发现全局变量 g_balance 会突然变成 -999。在一些大型项目中问题可能藏在很深的地方,如果想要找变量发生改变的位置,可以使用 watch xxx 监视变量,每次会在发生改变的地方停止。
(gdb) i b
Num Type Disp Enb Address What
3 hw watchpoint keep y g_balance
breakpoint already hit 5 times
(gdb) condition 3 g_balance<0
(gdb) run
Starting program: /home/Bluuue/Desktop/gdb watch
[Thread debugging using libthread_db enabled]
Using host libthread_db library "/usr/lib/libthread_db.so.1".
initial balance = 100
round 0, balance = 110
round 1, balance = 120
round 2, balance = 130
round 3, balance = 140
Hardware watchpoint 3: g_balance
Old value = 140
New value = -999
update_balance (round=4) at gdb.cpp:76
76 }
当然,也可以像断点一样添加条件,然后在出现问题的地方停止。此外,rwatch 是变量被读取时停止,awatch 是访问时停止。需要注意,在监视变量时需要注意生命周期问题,不能在函数外部监视局部变量。
2. Record-Replay 调试
当程序不一定每次运行都报错时,此时就没法每次运行直到发生问题再开始调试。那么就可以使用 rr 来记录运行时的数据,然后在发生错误时逆向回去检查问题。
#include <iostream>
struct EngineState {
int total = 120;
int divisor = 6;
int last_step = -1;
};
void apply_work(EngineState& state, int step) {
state.total += (step + 1) * 10;
}
void maybe_corrupt(EngineState& state, int step) {
if (step == 4) {
state.divisor = 0; // Intentional bug: corruption happens here.
}
}
void process_step(EngineState& state, int step) {
apply_work(state, step);
maybe_corrupt(state, step);
state.last_step = step;
std::cout << "step=" << step
<< " total=" << state.total
<< " divisor=" << state.divisor << '\n';
}
int finalize(const EngineState& state) {
// The program crashes here, several function calls after the corruption.
return state.total / state.divisor;
}
int main() {
std::cout << std::unitbuf; // 崩溃前立即刷新输出。
EngineState state;
for (int step = 0; step < 8; ++step) {
process_step(state, step);
}
std::cout << "final score = " << finalize(state) << '\n';
return 0;
}
在编译后,需要先使用 rr record xxx 运行并记录此次运行的状态。在输入 rr replay 后,就可以进入调试界面,此时虽然显示 rr,但本质还是 gdb。
(rr) continue
Continuing.
step=0 total=130 divisor=6
step=1 total=150 divisor=6
step=2 total=180 divisor=6
step=3 total=220 divisor=6
step=4 total=270 divisor=0
step=5 total=330 divisor=0
step=6 total=400 divisor=0
step=7 total=480 divisor=0
final score =
Program received signal SIGFPE, Arithmetic exception.
0x000055939c80129f in finalize (state=...) at rr.cpp:31
31 return state.total / state.divisor;
之后输入 continue 后,程序会停在发生问题的地方。此时仍然可以像 gdb 一样查看参数或局部变量,可以发现不管运行多少遍,变量的内存地址都是一样的,这就是 rr 保存运行状态的功能。
(rr) next
15 state.divisor = 0; // Intentional bug: corruption h
appens here.
(rr) next
17 }
(rr) p state
$2 = (EngineState &) @0x7fff1914424c: {total = 270, divisor = 0,
last_step = 3}
(rr) reverse-step
15 state.divisor = 0; // Intentional bug: corruption h
appens here.
(rr) p state
$3 = (EngineState &) @0x7fff1914424c: {total = 270, divisor = 6,
last_step = 3}
(rr) s 3
24 std::cout << "step=" << step
(rr) bt
#0 process_step (state=..., step=4) at rr.cpp:24
#1 0x000055939c801303 in main () at rr.cpp:40
(rr) rs 3
15 state.divisor = 0; // Intentional bug: corruption h
appens here.
(rr) bt
#0 maybe_corrupt (state=..., step=4) at rr.cpp:15
#1 0x000055939c8011f5 in process_step (state=..., step=4)
at rr.cpp:21
#2 0x000055939c801303 in main () at rr.cpp:40
reverse-step 顾名思义,可以反向回到程序运行的上一步,简写 rs。在打上条件断点并逐步调试后,可以发现程序在此时出现了问题,打印变量就可以看到回到了之前的状态。如果想多次返回,可以使用 rs x 直接倒退 x 步。 除此之外,和 step 和 next 的关系一样,reverse-next 会跳过上一步执行的函数。类似的,reverse-finish 就是从函数内部直接返回到函数即将被调用的时刻,注意不是返回开头而是返回上一级。
(rr) set $addr=&state.divisor
(rr) watch *$addr
Hardware watchpoint 7: *$addr
(rr) p *$addr
$9 = 0
(rr) rc
Continuing.
Program received signal SIGFPE, Arithmetic exception.
0x000055939c80129f in finalize (state=...) at rr.cpp:31
31 return state.total / state.divisor;
(rr) rc
Continuing.
Hardware watchpoint 7: *$addr
Old value = 0
New value = 6
0x000055939c8011ba in maybe_corrupt (state=..., step=4)
at rr.cpp:15
15 state.divisor = 0; // Intentional bug: corruption h
appens here.
(rr) bt
#0 0x000055939c8011ba in maybe_corrupt (state=..., step=4)
at rr.cpp:15
#1 0x000055939c8011f5 in process_step (state=..., step=4)
at rr.cpp:21
#2 0x000055939c801303 in main () at rr.cpp:40
当运行到报错的时候,此时就需要通过 watch 来监视 divisor 这个变量,找到其变成 0 的位置。由于每个函数里都会有 state 参数,所以直接写 watch state.divisor 会导致监视的是局部的引用参数,在返回到函数被调用前时会导致 gdb 无法读取。
正确的做法是,监视该变量的内存地址,这样就不会受引用参数的影响了。那么就可以先在 gdb 里临时用一个变量记录这个地址,然后直接监视这块内存即可。注意这里会因为 SIGFPE 报错信号停止一次,再执行一次即可。
3. 系统调用追踪
不管是 gdb 还是 rr,都只能用来调试程序内部的问题。而如果想要看程序在运行时系统发生了什么,就需要用到 system call trace 系统追踪了。
(1) strace
在输入 strace ls -l 2>&1 | less 后,此时就可以看到 ls -l 这个命令调用的系统命令了。可以发现,操作系统在运行该程序前, 调用了很多东西来进行预处理。
#include <cerrno>
#include <cstring>
#include <fcntl.h>
#include <iostream>
#include <unistd.h>
int main() {
const char terminal_message[] = "terminal output\n";
// 直接通过 write 向标准输出写数据。
ssize_t terminal_written =
write(STDOUT_FILENO, terminal_message,
sizeof(terminal_message) - 1);
if (terminal_written == -1) {
std::cerr << "write stdout failed: "
<< std::strerror(errno) << '\n';
return 1;
}
// 在当前工作目录打开或创建文件。
int fd = openat(
AT_FDCWD,
"strace_demo.txt",
O_WRONLY | O_CREAT | O_TRUNC,
0644
);
if (fd == -1) {
std::cerr << "openat failed: "
<< std::strerror(errno) << '\n';
return 1;
}
const char file_message[] = "file output\n";
ssize_t file_written =
write(fd, file_message, sizeof(file_message) - 1);
if (file_written == -1) {
std::cerr << "write file failed: "
<< std::strerror(errno) << '\n';
close(fd);
return 1;
}
if (close(fd) == -1) {
std::cerr << "close failed: "
<< std::strerror(errno) << '\n';
return 1;
}
return 0;
}
对于上述的练习文件,就是向终端写了一句话,然后创建并向文件写了这句话,接着关闭文件然后退出。在编译并运行该练习文件后,使用 strace 可以发现在运行 main 之前系统做了大量的预处理工作。需要注意的是,strace 默认输出在标准错误流中,重定向时需要注意。
write(1, "terminal output\n", 16) = 16
首先,write 命令是写数据的系统调用。第一个参数 1 表示标准输出的文件描述符,第二个参数是要写的东西,第三个参数 16 表示要写的字节数,返回值 16 表示成功写入 16 个字节。
openat(AT_FDCWD, "strace_demo.txt", O_WRONLY|O_CREAT|O_TRUNC, 0644) = 3
openat 命令是打开文件的系统调用。第一个参数 AT_FDCWD 表示以当前工作目录为基础,第二个参数就是要打开或创建的文件名。之后,O_WRONLY | O_CREAT | O_TRUNC 这三个打开方式分别对应只写、文件不存在时创建和已存在时清空。最后,0644 表示创建时的权限模式,就是 ls -l 查看的那个权限。
若系统调用成功,此时会返回文件描述符 3,这是因为 0,1,2 已经被三个流给占用了,所以现在这个文件就是下一个编号。
write(3, "file output\n", 12) = 12
close(3) = 0
之后,在有了新创建的文件的描述符后,就是将这句话写入这个文件。最后就是关闭这个文件,返回值 0 代表成功。
❯ strace -e trace=file ./strace
execve("./strace", ["./strace"], 0x7ffe46ff8c40 /* 90 vars */) = 0
access("/etc/ld.so.preload", R_OK) = -1 ENOENT (没有那个文件或目录)
openat(AT_FDCWD, "/etc/ld.so.cache", O_RDONLY|O_CLOEXEC) = 3
openat(AT_FDCWD, "/usr/lib/libstdc++.so.6", O_RDONLY|O_CLOEXEC) = 3
openat(AT_FDCWD, "/usr/lib/libm.so.6", O_RDONLY|O_CLOEXEC) = 3
openat(AT_FDCWD, "/usr/lib/libgcc_s.so.1", O_RDONLY|O_CLOEXEC) = 3
openat(AT_FDCWD, "/usr/lib/libc.so.6", O_RDONLY|O_CLOEXEC) = 3
terminal output
openat(AT_FDCWD, "strace_demo.txt", O_WRONLY|O_CREAT|O_TRUNC, 0644) = 3
+++ exited with 0 +++
-e 参数可以追踪一个表达式,之后的 trace=file 表示只关注文件相关的调用。除此之外,还可以 trace=openat 追踪指定命令或 trace=openat,write,... 一次性追踪多个命令。一般情况下,推荐通过 -o xxx.log 将调试信息保存到文件,然后通过 grep 查询。
❯ cat null.md
cat: null.md: 没有那个文件或目录
❯ strace -e trace=file cat null.md
...
openat(AT_FDCWD, "null.md", O_RDONLY) = -1 ENOENT (没有那个文件或目录)
...
+++ exited with 1 +++
❯ ls -l secret.md
-rw-r--r-- 1 Bluuue Bluuue 7 8月 6日 22:51 secret.md
❯ chmod 000 secret.md
'secret.md' 的模式已由 0644 (rw-r--r--) 更改为 0000 (---------)
❯ strace -e trace=file cat secret.md
...
openat(AT_FDCWD, "secret.md", O_RDONLY) = -1 EACCES (权限不够)
...
+++ exited with 1 +++
在这里,可以看到虽然上面同样提示了 “没有那个文件或目录”,但进程依然正常运行,而在下方提示时就直接退出了。而在下方可以发现,在更改权限后,此时再运行就可以发现报错信息了。这样,就可以调试程序内要求的文件是否存在、路径是否正确以及是否有权限了。
#!/usr/bin/env bash
echo "shell pid=$$"
cat input.txt
/bin/echo "all children finished"
❯ echo "This is child." > input.txt
❯ ./child.sh
shell pid=112392
This is child.
all children finished
❯ strace -f -e trace=process,file ./child.sh
...
shell pid=112582
...
strace: Process 112583 attached
[pid 112583] execve("/usr/bin/cat", ["cat", "input.txt"], 0x7fff877f06f0 /* 90 vars */) = 0
[pid 112583] access("/etc/ld.so.preload", R_OK) = -1 ENOENT (没有那个文件或目录)
[pid 112583] openat(AT_FDCWD, "/etc/ld.so.cache", O_RDONLY|O_CLOEXEC) = 3
[pid 112583] openat(AT_FDCWD, "/usr/lib/libc.so.6", O_RDONLY|O_CLOEXEC) = 3
[pid 112583] openat(AT_FDCWD, "/usr/lib/locale/locale-archive", O_RDONLY|O_CLOEXEC) = 3
[pid 112583] openat(AT_FDCWD, "input.txt", O_RDONLY) = 3
This is child.
[pid 112583] exit_group(0) = ?
[pid 112583] +++ exited with 0 +++
--- SIGCHLD {si_signo=SIGCHLD, si_code=CLD_EXITED, si_pid=112583, si_uid=1000, si_status=0, si_utime=0, si_stime=0} ---
wait4(-1, [{WIFEXITED(s) && WEXITSTATUS(s) == 0}], WNOHANG|WSTOPPED|WCONTINUED, {ru_utime={tv_sec=0, tv_usec=804}, ru_stime={tv_sec=0, tv_u
sec=0}, ...}) = 112583
kill(-112583, 0) = -1 ESRCH (没有那个进程)
wait4(-1, 0x7fff877eea04, WNOHANG|WSTOPPED|WCONTINUED, 0x7fff877eea20) = -1 ECHILD (没有子进程)
all children finished
exit_group(0) = ?
+++ exited with 0 +++
-f 参数可以追踪子进程,-ff 可以将每个进程的结果分别写入不同的文件,生成以进程编号为后缀的文件。此时可以发现,当前的进程为 112582。之后父进程为了运行 cat 又创建了编号为 112583 的子进程,后续的 openat 就都是在子进程中执行的。
#include <array>
#include <cerrno>
#include <cstring>
#include <iostream>
#include <unistd.h>
int main() {
std::cout << "pid=" << getpid() << '\n';
std::cout << "请输入一行文字:" << std::flush;
std::array<char, 128> buffer{};
// 程序会阻塞在这里,等待终端输入。
ssize_t bytes_read =
read(STDIN_FILENO, buffer.data(), buffer.size());
const char prefix[] = "程序收到:";
write(STDOUT_FILENO, prefix, sizeof(prefix) - 1);
write(STDOUT_FILENO, buffer.data(), bytes_read);
return 0;
}
❯ ./wait
pid=117093
请输入一行文字:
❯ sudo strace -p 117093 -e trace=read,write
strace: Process 117093 attached
read(0
在执行程序后,-p id 参数可以追踪正在运行的进程,查看为什么进程卡住等问题。此时由于在等待用户输入,所以会卡在 read 这里,输入后显示全部语句。如果不想查看了,可以直接 ctrl+c 解除附加。
❯ strace -T -e trace=clock_nanosleep,nanosleep sleep 3
clock_nanosleep(CLOCK_REALTIME, 0, {tv_sec=3, tv_nsec=1}, 0x7ffd8bb8
7f60) = 0 <3.000220>
+++ exited with 0 +++
❯ strace -t -e trace=clock_nanosleep,nanosleep sleep 3
23:40:22 clock_nanosleep(CLOCK_REALTIME, 0, {tv_sec=3, tv_nsec=1}, 0
x7ffe48014d30) = 0
23:40:25 +++ exited with 0 +++
❯ strace -c ./wait
pid=134086
请输入一行文字:hello
程序收到:hello
% time seconds usecs/call calls errors syscall
------ ----------- ----------- --------- --------- ----------------
0.00 0.000000 0 5 read
0.00 0.000000 0 4 write
0.00 0.000000 0 5 close
0.00 0.000000 0 6 fstat
0.00 0.000000 0 22 mmap
0.00 0.000000 0 6 mprotect
0.00 0.000000 0 1 munmap
0.00 0.000000 0 3 brk
0.00 0.000000 0 2 pread64
0.00 0.000000 0 1 1 access
0.00 0.000000 0 1 getpid
0.00 0.000000 0 1 execve
0.00 0.000000 0 1 arch_prctl
0.00 0.000000 0 1 futex
0.00 0.000000 0 1 set_tid_address
0.00 0.000000 0 5 openat
0.00 0.000000 0 1 set_robust_list
0.00 0.000000 0 1 prlimit64
0.00 0.000000 0 1 getrandom
0.00 0.000000 0 1 rseq
------ ----------- ----------- --------- --------- ----------------
100.00 0.000000 0 69 1 total
-T 参数可以输出系统调用的持续时间,而 -t 参数是输出调用的开始时间。此外,-c 参数可以输出每个系统调用的次数。
(2) bpftrace 和 eBPF
strace 通常由于调试某个指定进程,bpftrace 通常用于监视整个系统。这里,eBPF 是 linux 内核提供的一种程序运行机制,而 bpftrace 可以编写并加载 eBPF 追踪程序。
❯ sudo bpftrace -l 'tracepoint:syscalls:sys_enter_*'
这个命令的 -l 就是展示所有探针(probe)。探针很像断点,但区别是程序不会在此停下,而只是开始观测。在这里就是每当有线程准备执行所有通配出的系统调用时触发,然后执行给出的程序。
❯ sudo bpftrace -e '
tracepoint:syscalls:sys_enter_*
{
@calls[probe] = count();
}'
@calls[tracepoint:syscalls:sys_enter_read]: 67674
@calls[tracepoint:syscalls:sys_enter_write]: 83274
@calls[tracepoint:syscalls:sys_enter_poll]: 88990
@calls[tracepoint:syscalls:sys_enter_ioctl]: 102767
@calls[tracepoint:syscalls:sys_enter_futex]: 125291
@calls[tracepoint:syscalls:sys_enter_recvmsg]: 190599
这个命令就可以查看 read,write 被调用了多少次。在这里就是每当这些探针被触发时,执行 {} 内的命令,这个可以理解为 calls[probe]++。
❯ sudo bpftrace -e 'tracepoint:syscalls:sys_enter_openat
{
printf("%d %-16s %s\n", pid, comm, str(args.filename));
}'
236655 ls /etc/ld.so.cache
236655 ls /usr/lib/libcap.so.2
236655 ls /usr/lib/libc.so.6
236655 ls /usr/lib/locale/locale-archive
236655 ls /usr/share/locale/locale.alias
236655 ls /usr/share/locale/zh_CN/LC_TIME/coreutils.mo
236655 ls /usr/lib/gconv/gconv-modules.cache
236655 ls .
236655 ls /etc/nsswitch.conf
236655 ls /etc/passwd
236655 ls /etc/group
236655 ls /etc/ld.so.cache
236655 ls /usr/lib/libnss_systemd.so.2
236655 ls /usr/lib/libgcc_s.so.1
236655 ls /run/systemd/userdb/
236655 ls /proc/sys/kernel/random/boot_id
这个命令就是每当有线程调用 openat 系统调用时执行打印的命令。此时再在另一个终端运行譬如 ls -la 的命令,就可以看到这边输出 ls 的系统调用。对于 printf 中的参数,首先是该进程的 PID,之后紧跟着的 comm 是进程名,之后的是打开的路径。
❯ sudo bpftrace -e '
tracepoint:syscalls:sys_enter_*
/comm == "zsh"/
{
@calls[probe] = count();
}'
❯ sudo bpftrace -e '
tracepoint:syscalls:sys_enter_*
/pid == cpid/
{
@calls[probe] = count();
}' -c 'ls -la'
在之前的基础上,/comm == "zsh"/ 就可以过滤出进程名为 zsh 的事件。类似的,可以使用 /pid == 12345/ 只看该进程的事件。此时,就可以加上 -c 参数表示执行后续给出的命令,再结合 cpid 表示由 -c 启动的子进程,就可以查看该命令的事件了。
❯ sudo bpftrace -e '
tracepoint:syscalls:sys_enter_read
/pid == cpid/
{
@start[tid] = nsecs;
}
tracepoint:syscalls:sys_exit_read
/@start[tid]/
{
@latency_us = hist((nsecs - @start[tid]) / 1000);
delete(@start[tid]);
}
' -c 'cat /etc/hostname'
Attached 2 probes
bluuue
@latency_us:
[0] 1 |@@@@@@@@@@@@@@@@@@@@@@@@@@
|
[1] 0 |
|
[2, 4) 2 |@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@
@@@@@@@@@@|
此外,avg() 可以计算平均值,hist() 可以直接展示一个直方图。这个直方图的意思就是有 1 次 read 被调用小于 1 微秒,2 次在 2~4 微秒之间。
(3) 网络调试
❯ sudo tcpdump -i any port 80
tcpdump: WARNING: any: That device doesn't support promiscuous mode
(Promiscuous mode not supported on the "any" device)
tcpdump: verbose output suppressed, use -v[v]... for full protocol dec
ode
listening on any, link-type LINUX_SLL2 (Linux cooked v2), snapshot len
gth 262144 bytes
01:28:41.392601 lo In IP 104.20.23.154.80 > 192.168.43.145.60466:
Flags [S.], seq 3419164665, ack 2171666853, win 65483, options [mss 65
495,sackOK,TS val 1015227663 ecr 4124014184,nop,wscale 10], length 0
01:28:41.392636 lo In IP 104.20.23.154.80 > 192.168.43.145.60466:
Flags [.], ack 76, win 64, options [nop,nop,TS val 1015227663 ecr 4124
014184], length 0
01:28:42.197089 lo In IP 104.20.23.154.80 > 192.168.43.145.60466:
Flags [P.], seq 1:873, ack 76, win 64, options [nop,nop,TS val 1015228
467 ecr 4124014184], length 872: HTTP: HTTP/1.1 200 OK
01:28:42.237569 lo In IP 104.20.23.154.80 > 192.168.43.145.60466:
Flags [.], ack 77, win 64, options [nop,nop,TS val 1015228508 ecr 4124
014989], length 0
01:28:43.198601 lo In IP 104.20.23.154.80 > 192.168.43.145.60466:
Flags [F.], seq 873, ack 77, win 64, options [nop,nop,TS val 101522946
9 ecr 4124014989], length 0
^C
5 packets captured
10 packets received by filter
0 packets dropped by kernel
首先,tcpdump 命令可以进行基础抓包。-i 参数表示指定网络接口,any 就是所有可抓取的接口。之后,port 80 表示在所有接口抓出来的包的基础上,只保留端口为 80 的包。类似的,如果要按 ip 过滤就是 host 1.1.1.1,按协议就是 tcp,这些都可以自由组合。
❯ sudo tcpdump -i any port 80 -w capture.pcap
❯ wireshark capture.pcap
-w 就可以将抓到的包写入文件,保存下来以后分析。之后在安装了 Wireshark 之后,就可以使用图形界面分析查看了。
4. 内存调试
写过算法题的都知道,数组越界这种内存问题最难调试了,因为报错位置和出错位置完全不一样,此时就需要用到 AddressSanitizer 等内存调试工具了。
#include <iostream>
int main()
{
int* a=new int[3];
a[5]=42;
delete[] a;
}
❯ g++ -fsanitize=address -std=c++23 segfault.cpp -o segfault
❯ ./segfault
=================================================================
==177429==ERROR: AddressSanitizer: heap-buffer-overflow on address 0
x7b7a0e5e0024 at pc 0x5651750c21ce bp 0x7ffe11000d30 sp 0x7ffe11000d
20
...
对于上述出问题的代码,单纯 ❯ g++ -std=c++23 segfault.cpp -o segfault 是不会有问题的。此时,如果在编译时加上了 -fsanitize=address 参数,再运行就会发生报错。虽然这个编译选项固然好,但一般生产时不会一直开始这个参数,因为每次检查内存问题会产生明显的性能开销。
此外,还有 ThreadSanitizer 检查数据竞争问题,MemorySanitizer 检查是否读取了未初始化的内存,以及 UndefinedBehaviorSanitizer 检查是否存在未定义行为。这几个在算法竞赛里还挺重要的,毕竟经常脑子一抽写出一些很抽象的东西()
❯ valgrind --leak-check=full ./segfault
此外,以上工具都需要在使用时重新编译。而 Valgrind 可以直接运行可执行文件,不需要重新编译或链接。相应的,虽然好用但性能开销也很严重。
二、分析 Profiling
对于写好的程序,往往需要分析这个程序的性能。而分析性能最好的方式,就是真实跑一遍这个程序,而不是单纯用脑分析。
1. time 命令
❯ time curl https://www.bilibili.com/ &> /dev/null
curl https://www.bilibili.com/ &> /dev/null 0.02s user 0.01s system 14% cpu 0.169 total
time 命令可以输出一个程序用了多久跑完。这里,0.02s user 表示 CPU 花了 0.02s,0.01s system 表示内核花了 0.01s,而 0.169 total 表示现实世界花了 0.169s。这个数据就说明,内核和 CPU 运算只花了很少的时间,剩下大部分的时间都是在等待。
还需要注意的是,在多线程中,可能会出现 CPU 时间大于真实时间的情况,因为统计的是所有线程消耗 CPU 的总时间。
❯ hyperfine --warmup 3 'find . -name "*.md"' 'fd -e md'
Benchmark 1: find . -name "*.md"
Time (mean ± σ): 5.2 ms ± 1.9 ms [User: 2.0 ms, System: 3.8 ms]
Range (min … max): 1.8 ms … 11.2 ms 547 runs
Warning: Command took less than 5 ms to complete. Note that the results might be inaccurate because hyperfine can not calibrate the shell
startup time much more precise than this limit. You can try to use the `-N`/`--shell=none` option to disable the shell completely.
Benchmark 2: fd -e md
Time (mean ± σ): 15.0 ms ± 1.9 ms [User: 23.8 ms, System: 33.2 ms]
Range (min … max): 6.7 ms … 19.3 ms 174 runs
Summary
find . -name "*.md" ran
2.88 ± 1.14 times faster than fd -e md
当然,一次测试的时间其实不太能说明什么问题,多测几遍也可以发现每次的时间都不一样。而在工业环境中,cache 缓存和 CPU 频率等问题都影响单次测试的时间,所以为了严谨就需要多次测试。
此时,hyperfine 命令可以多次测试两个程序,然后输出一些统计数据便于分析。可以发现,最终显示 find 比 fd 快了三倍。
2. 资源监控
❯ htop
❯ btop
❯ sudo iotop
❯ free -h
❯ lsof -p 321587
❯ lsof -i :8080
❯ python -m http.server 4444
Serving HTTP on 0.0.0.0 port 4444 (http://0.0.0.0:4444/) ...
❯ ss -tlnp | grep 4444
LISTEN 0 0 0.0.0.0:4444 0.0.0.0:* users:(("python",pid=400855,fd=3))
❯ sudo nethogs
❯ sudo iftop
这些命令都是可以查看当前机器的状态,包括 CPU 使用、内存和线程等资源。htop 和 btop 可以查看 CPU 使用和内存等资源,iotop 可以查看磁盘读写,free 可以查看内存,lsof 可以查看文件、进程或端口占用,ss 可以查看网络连接,nethogs 可以查看哪些进程在占用网络带宽,iftop 可以查看流量去往了哪里。
3. 可视化分析
#include<iostream>
long long compute()
{
const int testTime=200000000;
long long sum=0;
for(int i=1;i<=testTime;i++)
{
sum+=i;
}
return sum;
}
long long dfs(int i)
{
if(i==0||i==1||i==2)
{
return 1;
}
return dfs(i-1)+dfs(i-2);
}
int main()
{
std::cout<<dfs(30)<<'\n';
std::cout<<compute()<<'\n';
return 0;
}
❯ perf stat ./program
832040
20000000100000000
Performance counter stats for './program':
0 context-switches:u # 0.0 cs/sec cs_per_second
0 cpu-migrations:u # 0.0 migrations/sec migrations_per_second
131 page-faults:u # 487.3 faults/sec page_faults_per_second
268.80 msec task-clock:u # nan CPUs CPUs_utilized
228 cpu_core/branch-misses/u # 0.0 % branch_miss_rate (96.69%)
206,354,129 cpu_core/branches/u # 767.7 M/sec branch_frequency (96.69%)
1,430,150,448 cpu_core/cpu-cycles/u # 5.3 GHz cycles_frequency (96.69%)
1,238,122,248 cpu_core/instructions/u # 0.9 instructions insn_per_cycle (96.69%
)
1,376,038 cpu_atom/branch-misses/u # 1.0 % branch_miss_rate (1.05%)
259,389,167 cpu_atom/branches/u # 965.0 M/sec branch_frequency (2.16%)
413,380,703 cpu_atom/cpu-cycles/u # 1.5 GHz cycles_frequency (3.28%)
1,153,461,725 cpu_atom/instructions/u # 2.8 instructions insn_per_cycle (3.31%)
TopdownL1 (cpu_core) # 0.0 % tma_bad_speculation
# 0.4 % tma_frontend_bound (96.69%)
# 80.3 % tma_backend_bound
# 19.3 % tma_retiring (96.69%)
TopdownL1 (cpu_atom) # 8.7 % tma_backend_bound (2.26%)
# 20.1 % tma_frontend_bound (1.15%)
# 27.1 % tma_bad_speculation
# 44.1 % tma_retiring (0.03%)
0.269862834 seconds time elapsed
0.269483000 seconds user
0.000000000 seconds sys
perf 命令可以输出一个程序运行时的很多数据,包括上下文切换、缺页率、分支预测失败率等。
❯ perf record -g ./program
832040
20000000100000000
[ perf record: Woken up 1 times to write data ]
[ perf record: Captured and wrote 0.109 MB perf.data (1011 samples) ]
❯ perf report
之前回答的是整体表现,而 record 可以将数据保存下来,再使用 report 查看 CPU 时间都花在哪个函数上。此外,还可以使用 script 画出火焰图,表示函数的调用层级和在采样中的 CPU 时间占比。
❯ valgrind --tool=callgrind ./program
==398639== Callgrind, a call-graph generating cache profiler
==398639== Copyright (C) 2002-2017, and GNU GPL'd, by Josef Weidendorfer et al.
==398639== Using Valgrind-3.25.1 and LibVEX; rerun with -h for copyright info
==398639== Command: ./program
==398639==
==398639== For interactive control, run 'callgrind_control -h'.
832040
20000000100000000
==398639==
==398639== Events : Ir
==398639== Collected : 1235453526
==398639==
==398639== I refs: 1,235,453,526
❯ callgrind_annotate callgrind.out.398639
--------------------------------------------------------------------------------
Profile data file 'callgrind.out.398639' (creator: callgrind-3.25.1)
--------------------------------------------------------------------------------
I1 cache:
D1 cache:
LL cache:
Timerange: Basic block 0 - 209345460
Trigger: Program termination
Profiled target: ./program (PID 398639, part 1)
Events recorded: Ir
Events shown: Ir
Event sort order: Ir
Thresholds: 99
Include dirs:
User annotated:
Auto-annotation: on
--------------------------------------------------------------------------------
Ir
--------------------------------------------------------------------------------
1,235,453,526 (100.0%) PROGRAM TOTALS
--------------------------------------------------------------------------------
Ir file:function
--------------------------------------------------------------------------------
1,200,000,011 (97.13%) ???:compute() [/home/Bluuue/Desktop/program]
32,645,930 ( 2.64%) ???:dfs(int)'2 [/home/Bluuue/Desktop/program]
跟之前一样,valgrind 就是暴力追踪一遍程序,来检测资源的使用,但会非常慢……
此外,valgrind --tool=massif ./program 可以支持查看程序什么时候用了多少的内存。
课后练习
1. 火焰图
Profile with perf record. Save this as slow.c. Compile with debug symbols: gcc -g -O2 slow.c -o slow -lm. Run perf record -g ./slow, then perf report to see where time is spent. Try generating a flame graph using the flamegraph scripts.
#include <math.h>
#include <stdio.h>
double slow_computation(int n) {
double result = 0;
for (int i = 0; i < n; i++) {
for (int j = 0; j < 1000; j++) {
result += sin(i * j) * cos(i + j);
}
}
return result;
}
int main() {
double r = 0;
for (int i = 0; i < 100; i++) {
r += slow_computation(1000);
}
printf("Result: %f\n", r);
return 0;
}
❯ perf script \
| ./FlameGraph/stackcollapse-perf.pl \
| ./FlameGraph/flamegraph.pl \
> flamegraph.svg
Filtering for events of type: cpu_core/cycles/Pu
❯ firefox flamegraph.svg
在使用 perf record 保存数据后,就可以这样生成火焰图。

2. CPU 亲和性 (Affinity)
Use htop to monitor your system while running a resource-intensive program. Try using taskset to limit which CPUs a process can use: taskset --cpu-list 0,2 stress -c 3. Why doesn’t stress use three CPUs?
CPU 亲和性就是规定一个进程或线程只能在特定 CPU 上运行。这是因为正常情况下线程可以在多个 CPU 之间不断迁移,如果一个线程刚把数据都放进 cache 里,此时再迁移到别的 CPU 就需要重新把数据加载进新 CPU 的 cache。
#include <atomic>
#include <chrono>
#include <cstdint>
#include <iostream>
#include <syncstream>
#include <thread>
#include <vector>
#include <sched.h>
#include <sys/syscall.h>
#include <unistd.h>
std::atomic<std::uint64_t> sink{0};
long get_tid()
{
return syscall(SYS_gettid);
}
void worker(int id)
{
using namespace std::chrono;
std::uint64_t x = id + 1;
auto finish = steady_clock::now() + seconds(10);
auto next_print = steady_clock::now();
while (steady_clock::now() < finish)
{
// 制造持续的 CPU 计算负载
for (int i = 0; i < 5'000'000; ++i)
{
x = x * 1664525ULL + 1013904223ULL;
}
if (steady_clock::now() >= next_print)
{
std::osyncstream(std::cout)
<< "worker=" << id
<< " tid=" << get_tid()
<< " cpu=" << sched_getcpu()
<< '\n';
next_print += seconds(1);
}
}
sink.fetch_xor(x, std::memory_order_relaxed);
}
int main()
{
std::vector<std::thread> threads;
for (int i = 0; i < 3; ++i)
{
threads.emplace_back(worker, i);
}
for (auto& t : threads)
{
t.join();
}
std::cout << "done, sink=" << sink.load() << '\n';
}
❯ g++ -std=c++23 -O2 -pthread affinity_demo.cpp -o affinity_demo
❯ ./affinity_demo
worker=1 tid=20902 cpu=2
worker=0 tid=20901 cpu=0
worker=2 tid=20903 cpu=31
worker=2 tid=20903 cpu=8
worker=1 tid=20902 cpu=2
worker=0 tid=20901 cpu=11
worker=1 tid=20902 cpu=2
...
done, sink=15086526823471386176
编译并运行后,此时 worker 就是创建的三个工作线程,tid 就是该线程的 ID,此时就能看到同一个线程工作的 CPU 一直在变化。
❯ taskset -c 0,2 ./affinity_demo
worker=0 tid=24377 cpu=2
worker=2 tid=24379 cpu=0
worker=1 tid=24378 cpu=0
worker=0 tid=24377 cpu=2
worker=1 tid=24378 cpu=0
worker=2 tid=24379 cpu=0
...
taskset 命令可以让程序只在给定 CPU 上运行。所以可以发现此时这些线程就只能在 0 和 2 之间迁移了,用 htop 也可以看到上方只有 0 和 2 在 100%。
需要注意的是,stress -c 3 是启动三个 CPU worker。但由于之前限制了只能在 0 和 2 之间运行,所以最多只能同时执行 2 个 worker。
总结
这课干货太多了……

Lecture 04 Debugging and Profiling
浙公网安备 33010602011771号