如果你熟悉Linux的话,就有一个常识,Linux中所有的内存信息(进程)都是以文件形式保存在/proc目录下的,我们获取通过该目录下进程ID为名称的目录中有关该进程实时内存信息,包括网络,文件句柄、启动点、执行命令等等。本文虫虫以进程堆栈为例子,介绍通过/proc/进程号/stack文件的内容来实时跟踪进程堆栈信息。
进程阻塞
为了解决这个问题,让我们考虑一个进程阻塞的过程,以 TCP 服务器为例。
在最简单的形式中,我们可以拥有一个单 线程 TCP服务器,它只接收给定线程中的流量,然后处理其结果。
如上图所示,该流程有两个服务器阻塞点: Accept 阶段和read阶段,第一个阻塞直到客户端完成TCP握手;第二,完成TCP握手,直到数据开始读取。下面我们利用C 套接字实现一个简单认证握手的过程,来模拟accept阶段的第一个阻塞过程。
编译,然后运行该应用,然后知道发生阻塞,即监听完成等待连接握手。
查看进程内核堆栈跟踪
这时,我们可以浏览/proc信息并查看内核中发生了什么,并确定它在accept syscall上被阻止:
cat /proc/$(pidof accept.out)/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 /base.c#proc_pid_stack(由虚拟文件系统的调用/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 …/sys_linux_amd64.s:671 runtime.epollwait
Thread 17020 at …/sys_linux_amd64.s:532 runtime.futex
Thread 17021 at …/sys_linux_amd64.s:532 runtime.futex
Thread 17022 at …/sys_linux_amd64.s:532 runtime.futex
Thread 17023 at …/sys_linux_amd64.s:532 runtime.futex
Thread 17024 at …/sys_linux_amd64.s:532 runtime.futex
(dlv)goroutines
[4 goroutines]
Goroutine 1 – …net poll .go:173 internal/poll.runtime_pollWait (0x427146)
Goroutine 2 – …proc.go:303 runtime .gopark (0x42c74b)
Goroutine 3 – …proc.go:303 runtime.gopark (0x42c74b)
Goroutine 4 – …proc.go:303 runtime.gopark (0x42c74b)
(dlv)goroutine
(dlv) stack
0 0x000000000042c74b in runtime.gopark
at /usr/local/go/src/runtime/proc.go:303
1 0x0000000000427a99 in runtime.netpollblock
at /usr/local/go/src/runtime/netpoll.go:366
2 0x0000000000427146 in internal /poll.runtime_pollWait
at /usr/local/go/src/runtime/netpoll.go:173
3 0x000000000048e81a in internal/poll.(*pollDesc).wait
at /usr/local/go/src/internal/poll/fd_poll_runtime.go:85
4 0x000000000048e92d in internal/poll.(*pollDesc).waitRead
at /usr/local/go/src/internal/poll/fd_poll_runtime.go:90
5 0x000000000048fc20 in internal/poll.(*FD).Accept
at /usr/local/go/src/internal/poll/fd_unix.go:384
6 0x00000000004b6572 in net.(*netFD).accept
at /usr/local/go/src/net/fd_unix.go:238
7 0x00000000004c972e in net.(*TCPListener).accept
at /usr/local/go/src/net/tcpsock_posix.go:139
8 0x00000000004c86c7 in net.(*TCPListener).Accept
at /usr/local/go/src/net/tcpsock.go:260
9 0x00000000004d55f4 in main.main
at /tmp/tcp/main.go:16
10 0x000000000042c367 in runtime.main
at /usr/local/go/src/runtime/proc.go:201
11 0x0000000000456391 in runtime.goexit
at /usr/local/go/src/runtime/asm_amd64.s:1333
我们现在有了用户空间和内核空间堆栈,可以追踪Go应用程序线程的所有调用等信息。
总结
本文总使用 /proc/<pid>/stack(或等效的/proc/<pid>/task/<task_id/ stack)来追踪进程堆栈的信息,可以帮我们查看服务的调用信息等很重要的信息,可以帮助我们在系统调试或者其他方面使用,虫虫也间或介绍了几个有用工具,比如git仓库中的代码文件追踪git blame,golang程序进程栈的追踪工具。之前的文章中虫虫给大家介绍过strace等工具进程系统追踪的方法,其实上其底层也是调用了该堆栈的一些信息。关于/proc其实上有很多重要的信息,以后有机会虫虫会介绍更多的使用。欢迎大家关注虫虫,及时反馈和响应我。