如果你熟悉Linux的话,就有一个常识,Linux中所有的内存信息(进程)都是以文件形式保存在/proc目录下的,我们获取通过该目录下进程ID为名称的目录中有关该进程实时内存信息,包括网络,文件句柄、启动点、执行命令等等。本文虫虫以进程堆栈为例子,介绍通过/proc/进程号/stack文件的内容来实时跟踪进程堆栈信息。
进程阻塞
为了解决这个问题,让我们考虑一个进程阻塞的过程,以TCP服务器为例。
在最简单的形式中,我们可以拥有一个单线程TCP服务器,它只接收给定线程中的流量,然后处理其结果。
如上图所示,该流程有两个服务器阻塞点:Accept阶段和read阶段,第一个阻塞直到客户端完成TCP握手;第二,完成TCP握手,直到数据开始读取。下面我们利用C 套接字实现一个简单认证握手的过程,来模拟accept阶段的第一个阻塞过程。
编译,然后运行该应用,然后知道发生阻塞,即监听完成等待连接握手。
查看进程内核堆栈跟踪
这时,我们可以浏览/proc信息并查看内核中发生了什么,并确定它在accept syscall上被阻止:
cat /proc/$(pidof acce)/stack
[<0>] inet_csk_accept+0x246/0x380
[<0>] inet_accept+0x45/0x170
[<0>] SYSC_accept4+0xff/0x210
[<0>] SyS_accept+0x10/0x20
[<0>] do_syscall_64+0x73/0x130
[<0>] entry_SYSCALL_64_after_hwframe+0x3d/0xa2
[<0>] 0xffffffffffffffff
可能看起来像一个奇怪的堆栈跟踪,但结构非常简单。
每一行代表一个被调用的函数(从查看堆栈调用),第一部分[<0>],是函数的内核地址,而第二部分,do_syscall_64 + ...对应偏移量的符号名。
当fs/proc (由虚拟文件系统的调用/proc的方法)遍历堆栈帧时,看到它将[<0>]硬编码为要实际的地址,至于为啥屏蔽了该实际地址可能是处于安全的原因,该函数源码:
在源代码的git仓,我们使用git blame 对seq_printf审查,可以看到该部分[<0>]硬编码代码是又Linus 教主去年添加的哦
查看printk格式说明符的文档,可以看到非常专业的格式:
B说明符导致符号名称带有偏移量,应在打印堆栈回溯时使用。使用K说明符,用于打印应该对非特权用户隐藏的内核指针。意思是,之前你可以检索内核地址,但是现在已经屏蔽显示了,可能是为了安全的缘故。
多线程异步应用的堆栈
虽然很清楚为什么在上面的例子中了解内核中的堆栈跟踪是有用的,但是对于使用异步IO的多线程应用服务来说(就像大多数现代Web服务器那样)。
我们使用golang实现一个和上部分中TCP 监听程序的例子:
上面的代码中我们没有使用goroutine,但是Go运行时最终会设置一个事件池文件,它允许我们监视多个文件描述符而不是单个的阻塞。
通过查看以上应用进程运行时内核被阻塞的系统调用:
find /proc/$(pidof gosocket)/task -name "stack" |xargs -I{} /bin/sh -c 'echo {} ; cat {}'
...
请注意,与C应用不同,我们看到了由gosocket应用的PID标识的任务组下的多个个任务的堆栈。由于Go在启动时将运行多个线程(这样我们可以调度goroutine来运行实际线程的轮询),我们可以查看所有线程中的堆栈,得到整体的堆栈信息(每个线程都是一个任务,所以各自都有自己的堆栈)。
为了进一步深入追踪,我们用dlv(github:/ derekparker/delve),可以看到有一个进程futex_wait阻塞了 5个线程,而另一个线程被ep_poll阻塞(异步IO上的实际块):
dlv attach $(pidof gosocket)
(dlv) threads
* Thread 17019 at ... run
Thread 17020 at ... run
Thread 17021 at ... run
Thread 17022 at ... run
Thread 17023 at ... run
Thread 17024 at ... run
(dlv)goroutines
[4 goroutines]
Goroutine 1 - ...ne internal (0x427146)
Goroutine 2 - ... run (0x42c74b)
Goroutine 3 - ... run (0x42c74b)
Goroutine 4 - ... run (0x42c74b)
(dlv)goroutine
(dlv) stack
0 0x000000000042c74b in run
at /usr/local/go/src/runtime/
1 0x0000000000427a99 in run
at /usr/local/go/src/runtime
2 0x0000000000427146 in internal
at /usr/local/go/src/runtime/ne
3 0x000000000048e81a in internal/poll.(*pollDesc).wait
at /usr/local/go/src/internal/poll
4 0x000000000048e92d in internal/poll.(*pollDesc).waitRead
at /usr/local/go/src/internal/poll
5 0x000000000048fc20 in internal/poll.(*FD).Accept
at /usr/local/go/src/internal/poll
6 0x00000000004b6572 in net.(*netFD).accept
at /usr/local/go/src/ne
7 0x00000000004c972e in net.(*TCPListener).accept
at /usr/local/go/src/ne
8 0x00000000004c86c7 in net.(*TCPListener).Accept
at /usr/local/go/src/ne
9 0x00000000004d55f4 in main.main
at /tmp/tc
10 0x000000000042c367 in run
at /usr/local/go/src/runtime
11 0x0000000000456391 in run
at /usr/local/go/src/runtime
我们现在有了用户空间和内核空间堆栈,可以追踪Go应用程序线程的所有调用等信息。
总结
本文总使用 /proc/<pid>/stack(或等效的/proc/<pid>/task/<task_id/ stack)来追踪进程堆栈的信息,可以帮我们查看服务的调用信息等很重要的信息,可以帮助我们在系统调试或者其他方面使用,虫虫也间或介绍了几个有用工具,比如git仓库中的代码文件追踪git blame,golang程序进程栈的追踪工具。之前的文章中虫虫给大家介绍过strace等工具进程系统追踪的方法,其实上其底层也是调用了该堆栈的一些信息。关于/proc其实上有很多重要的信息,以后有机会虫虫会介绍更多的使用。欢迎大家关注虫虫,及时反馈和响应我。