本文最后更新于:2026年4月2日 晚上
我们看函数时,非常关心两件事:(1)怎么调到这个函数来的;(2)这个函数内部又会调用哪些函数(到哪儿去)。
除了一点点搜索代码,查看代码外,还可以使用内核的动态追踪工具来观察它们。
当前运行的系统为ubuntu 24.04(kernel 6.8.0)
动态查看调用栈 – 从哪儿来
(1) 检查是否有对应的事件
这里使用bpftrace来进行跟踪,首先第一步检查我们想要跟踪的函数是否有对应的事件
bpftrace -l | grep udp_enqueue 查看到有这个kprobe事件
1 2
| kfunc:vmlinux:__udp_enqueue_schedule_skb kprobe:__udp_enqueue_schedule_skb
|
使用bpftrace查看调用栈
执行如下命令
1
| bpftrace -e 'kprobe:__udp_enqueue_schedule_skb { printf("pid=%d comm=%s\n", pid, comm); print(kstack); }'
|
输出如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39
| Attaching 1 probe... pid=4982 comm=syncthing
__udp_enqueue_schedule_skb+1 udpv6_queue_rcv_skb+79 __udp6_lib_mcast_deliver+608 __udp6_lib_rcv+1508 udpv6_rcv+37 ip6_protocol_deliver_rcu+411 ip6_input_finish+68 ip6_input+63 ip6_mc_input+242 ipv6_rcv+415 __netif_receive_skb_one_core+99 __netif_receive_skb+21 process_backlog+142 __napi_poll+51 net_rx_action+385 handle_softirqs+219 __do_softirq+16 do_softirq.part.0+65 __local_bh_enable_ip+114 netif_rx+236 dev_loopback_xmit+134 ip6_finish_output2+1027 ip6_finish_output+252 ip6_output+117 ip6_local_out+68 ip6_send_skb+49 udp_v6_send_skb+562 udpv6_sendmsg+3189 inet6_sendmsg+118 ____sys_sendmsg+720 ___sys_sendmsg+154 __sys_sendmsg+137 __x64_sys_sendmsg+29 x64_sys_call+2334 do_syscall_64+127 entry_SYSCALL_64_after_hwframe+120
|
查看不同调用路径的调用次数
因为到达函数的路径不止一个,有时需要分析有多少调用,走了哪些路径,可以执行如下命令
1
| bpftrace -e 'kprobe:__udp_enqueue_schedule_skb { @[kstack(8)] = count(); }'
|
这里必须要限制栈深度了,因为软中断往下的调用栈对我们的分析没有实际意义。 这里限制记录栈的深度为8。
输出如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33
| Attaching 1 probe... ^C
@[ __udp_enqueue_schedule_skb+1 udpv6_queue_rcv_skb+79 __udp6_lib_mcast_deliver+608 __udp6_lib_rcv+1508 udpv6_rcv+37 ip6_protocol_deliver_rcu+411 ip6_input_finish+68 ip6_input+63 ]: 4 @[ __udp_enqueue_schedule_skb+1 udp_queue_rcv_skb+79 __udp4_lib_mcast_deliver+624 __udp4_lib_rcv+1555 udp_rcv+37 ip_protocol_deliver_rcu+216 ip_local_deliver_finish+119 ip_local_deliver+110 ]: 4 @[ __udp_enqueue_schedule_skb+1 udp_queue_rcv_skb+79 udp_unicast_rcv_skb+122 __udp4_lib_rcv+477 udp_rcv+37 ip_protocol_deliver_rcu+216 ip_local_deliver_finish+119 ip_local_deliver+110 ]: 7
|
查看函数有哪些子调用 – 到哪儿去
我们使用perf ftrace功能,执行如下命令:
1
| perf ftrace -G ip_rcv --graph-opts noirqs,depth=5
|
depth限制了调用图深度为5,熟练后也可以不限制,因为深度大了后会非常难看
noirqs限制不打印中断调用,不然会有大量的中断调用(irq_enter_rcu,irq_exit_rcu)嵌套在里面。
输出如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44
| # tracer: function_graph # # CPU DURATION FUNCTION CALLS # | | | | | | | 8) | finish_task_switch.isra.0() { 9) 0.491 us | _raw_spin_unlock(); 10) 0.381 us | irq_enter_rcu(); 11) 0.411 us | sched_core_idle_cpu(); 12) + 14.116 us | } 13) | ip_rcv() { 14) 1.342 us | irq_enter_rcu(); 15) 0.481 us | sched_core_idle_cpu(); 16) | ip_rcv_core() { 17) | sock_wfree() { 18) 0.551 us | tun_sock_write_space(); 19) 1.422 us | } 20) 2.314 us | } 21) 0.360 us | __rcu_read_lock(); 22) | nf_hook_slow() { 23) 0.370 us | ipv4_conntrack_defrag [nf_defrag_ipv4](); 24) | ipv4_conntrack_in [nf_conntrack]() { 25) | nf_conntrack_in [nf_conntrack]() { 26) 0.391 us | get_l4proto [nf_conntrack](); 27) 1.743 us | resolve_normal_ct [nf_conntrack](); 28) 3.227 us | nf_conntrack_handle_packet [nf_conntrack](); 29) 6.662 us | } 30) 7.324 us | } 31) | nf_nat_ipv4_pre_routing [nf_nat]() { 32) 0.421 us | nf_nat_inet_fn [nf_nat](); 33) 1.143 us | } 34) + 10.219 us | } 35) 0.360 us | __rcu_read_unlock(); 36) | ip_rcv_finish_core.isra.0() { 37) | tcp_v4_early_demux() { 38) | __inet_lookup_established() { 39) 0.391 us | inet_ehashfn(); 40) 1.743 us | } 41) | ipv4_dst_check() { 42) 0.370 us | __rcu_read_lock(); 43) 0.411 us | __rcu_read_unlock(); 44) 1.743 us | } 45) 4.739 us | } 46) 5.821 us | } 47) | ip_local_deliver() {
|
.isra.0这种后缀不用管它,它是gcc优化后生成的新的函数签名,看代码的话看前面就好。
人生苦短,远离bug
Leon, 2026-04-02