七叶笔记 » golang编程 » 使用/proc信息跟踪进程的堆栈

使用/proc信息跟踪进程的堆栈

如果你熟悉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其实上有很多重要的信息,以后有机会虫虫会介绍更多的使用。欢迎大家关注虫虫,及时反馈和响应我。

相关文章