linux内核函数动态追踪

本文最后更新于: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

linux内核函数动态追踪
https://leon0625.github.io/2026/04/02/8f365e794da9/
作者
leon.liu
发布于
2026年4月2日
许可协议