Please enable Javascript to view the contents

Linux: 一切皆文件; peekfd: 偷看一切文件读写

 ·  ☕ 3 分钟

img

内容简介

Linux 大部分数据流动,包括进程间通讯,socket…… 均通过文件描述符(fd) 读写实现。在 troubleshooting 时,如果可以偷看到 fd 的流量,那么很多问题可以加速证明/证伪。本文介绍一个老工具 peekfd ,可以在一定环境中完成这个任务。

我遇到的问题

我在 《小编码,我输给 AI 了 —— 简记一次父子进程互锁的坑,自己挖的》 中说了一个场景。下面是进程父子关系图,我想用 kill -QUIT $ (为何不 jstack ?) 查看 jvm stack 。理想情况下, kill -QUIT $test_case_jvm 会让 test cases jvm 打印 stack dump 到 stdout,然后 maven jvm 也会复制一份打印出来。但现实是 maven jvmtest cases jvm 的 stack dump 输出吃了没吐出来。

[maven jvm] 
     |
     | -- [test cases jvm] (waiting for [curl] exit)
            |
            | -- [curl] (stdout pipe buffer full, waiting for pipe writeable)
1
2
3
4
5
6
7
$ sudo ll /proc/$test_case_jvm/fd
lr-x------ 1 1201 1202 64 7月   4 09:33 0 -> pipe:[1095338147]
l-wx------ 1 1201 1202 64 7月   4 09:33 1 -> pipe:[1095338148]
l-wx------ 1 1201 1202 64 7月   4 09:33 2 -> pipe:[1095338149]
lrwx------ 1 1201 1202 64 7月   4 09:33 3 -> socket:[1095340467]
lrwx------ 1 1201 1202 64 7月   4 09:33 4 -> socket:[1095340468]
lrwx------ 1 1201 1202 64 7月   4 09:33 5 -> socket:[1095340471]

还有计吗?当然有。 test cases jvm 是通过写 pipe 去分享 stdout 给 maven jvm 的。 pipe 的写说到底还是个 fd(file descriptor) 如果我们有个类似 tcpdump 的工具,可以看到这个 fd 的流量,就可以获得我们要的数据了。

尝试 cat fd

一开始,我想用 cat test cases jvm 的 stdout fd(fd 2) 的方法:

1
sudo cat /proc/$$test_case_jvm/fd/1

去偷看。但很快发现,stack dump 的结果是不完整的,每隔一行丢失一行。想想原因也简单,Linux pipe 支持多 reader ,但每个 buffer 块只给一个 reader 消费。

还有计吗?当然有。

peekfd

我的英语词汇其实有限。很多是在现在公司学习的(心怀感恩!)。我以前不了解 peek 这个英文单词的意思。第一次注意到这个单词是 java steam 的 peek。 然后就是 Envoy Proxy 的 Listener Filter 中用到的 libc 的 recv(int sockfd, void buf[.len], size_t len, int flags) 中的 MSG_PEEK flag

看看英文单词 peek 的定义:

to look, especially for a short time or while trying to avoid being seen:

Close your eyes. Don’t peek. I have a surprise for you.

I peeked out the window to see who was there.

The children peeked over the wall to see where the ball had gone.

The film peeks behind the scenes of a multinational corporation.

-- Cambridge Dictionary

take 1

我们用 jdk 自带的 java 应用 jconsole 看看效果。

1
jconsole | grep "not match at all"

在另外的终端中:

1
peekfd $(pgrep jconsole) 1 # 1 is fd of stdout

在另外的终端中:

1
kill -QUIT `pgrep console`

结果是,peekfd 什么也没输出!失望吧!爱折腾的我,当然不会就这样放弃。我自以为是懂点 kernel、 懂点 gdb 的人。

take 2

看看 peekfd 源码吧,就一个 c file:

https://github.com/acg/psmisc/blob/0d03fe8ca1d494d1cc48ca26aa0d780ad0cd4898/src/peekfd.c#L95

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
void attach(pid_t pid) {
	if (num_attached_pids >= MAX_ATTACHED_PIDS)
		return;
	attached_pids[num_attached_pids] = pid;
	if (ptrace(PTRACE_ATTACH, pid, 0, 0) == -1) {
		fprintf(stderr, _("Error attaching to pid %i\n"), pid);
		return;
	}
	num_attached_pids++;
}

用了 ptrace ,ptrace 是指定线程的,而 jvm 是多线程的,所以我得指定整个进程。

$ peekfd --help
Usage: peekfd [-8] [-n] [-c] [-d] [-V] [-h] <pid> [<fd> ..]
    -8, --eight-bit-clean        output 8 bit clean streams.
    -n, --no-headers             don't display read/write from fd headers.
    -c, --follow                 peek at any new child processes too.
    -t, --tgid                   peek at all threads where tgid equals <pid>.
    -d, --duplicates-removed     remove duplicate read/writes from the output.
    -V, --version                prints version info.
    -h, --help                   prints this help.

--tgid 好像可以救命,试试呗:

1
peekfd --follow --tgid `pgrep jconsole` 1 # 1 is fd of stdout

不知道为何,最近人品好像有点问题,还是没偷看到!

take 3

我自以为是懂点 jvm 的人,知道 thead dump 是由一个叫 VM Thread 的线程写 stdout 的。那么:

1
2
export vm_thread_id=$(ps -T -p `pgrep jconsole` | grep 'VM Thread' | cut -d ' ' -f 2)
peekfd $vm_thread_id 1 # 1 is fd of stdout

正所谓:功夫不负折腾人,只是家娃没人陪。终于这次可以抓取到 stdout 了。

更有意思的是, socket 也是 fd ,也可以用 peekfd 看 socket 的读写数据!

结语

peekfd 用了 ptrace ,可能比较性能影响,所以不建议用于生产环境中。peekfd 是个老掉牙的工具了,藏在 psmisc 这个工具包中。知名度很低很低。再次证明一点,解决疑难大问题,用小工具。

有趣的 peekfd 资料

分享

Mark Zhu
作者
Mark Zhu
An old developer