Recovering Live Data with GDB
原文地址: http://nullprogram.com/blog/2015/09/15/
我最近遇到个问题, 有一个 运行很长时间的程序 ,它的输出被卡在一个C语言FILE变量的buffer中了. 这个程序已经运行两天了,它会把结果直接输出来,但是最后几k的内容要等到程序完成它的清理动作并退出后才能输出来. 而这个清理动作耗时可能要花几天(甚至更多). 这个问题本身要修复很简单 — 这个清理的动作其实完全是没有必要的 — 但是我不想又要花两天的时间把结果重新再计算一遍.
下面这段代码简单模拟一下这个问题. 第一个循环代表了长时间的运算过程,而后面的无限循环代表了那个无尽的清理动作.
#include <stdio.h> int main(void) { /* Compute output. */ for (int i = 0; i < 10; i++) printf("%d/%d ", i, i * i); putchar('\n'); /* "Slow" cleanup operation ... */ for (;;) ; return 0; }
Buffered Output Review
printf
and putchar
这两个C库函数,都会以某种方式来缓存输出的内容. 这意味着不是每次调用这些函数都会实际输出数据.
另一方面, POSIX定义的函数 read
和 write
则是不带buffer的系统调用.
由于系统调用相对来说比较昂贵,因此一般会用带缓存的输入/输出来将大量针对小buffer的系统调用转换成一个针对大buffer的系统调用.
一般来说,若程序的标准输出为终端的话,它是按行来缓存的. 毕竟当程序完成了一行的输出内容后,用户很可能立即就想看到输出结果. 因此,若你编译该程序后是在终端上直接运行改程序的话,那么很可能在程序陷入无限循环之前就已经把结果输出来了.
$ cc -std=c99 example.c $ ./a.out 0/0 1/1 2/4 3/9 4/16 5/25 6/36 7/49 8/64 9/81
然而若把标准输出重定向到文件或管道中的话, 这个输出很有可能就会被缓存起来了,这个缓存大小一般为4KB. 这样一来,不管你等多长时间,改程序的输出始终都为空. 真正的输出内容被卡在进程内存中的一个FILE对象的buffer中了.
$ ./a.out > output.txt
修复这个问题的主流方法是调用 fflush
函数, 在开始一段耗时漫长而无输出结果的操作前可以用它来强行将buffer中的内容输出.
可惜,我早在两天前并没有想到这一出.
Debugger to the Rescue
幸运的是,有一个东西可以暂停正在运行的程序并帮我们维护程序的状态:那就是调试器.
第一步,找出进程号(上面输入 output.txt 的那个进程).
$ pgrep a.out 12934
现在让启动GDB,让它attach那个进程,这会暂停程序的运行.
$ gdb ./a.out Reading symbols from ./a.out...(no debugging symbols found)...done. gdb> attach 12934 Attaching to program: /tmp/a.out, process 12934 ... snip ... 0x0000000000400598 in main () gdb>
到了这一步,我就可以手工检查 stdout
的FILE结构,并从中抽取出buffer中的内容了.
不过最简单的方法是执行我一开始忘掉的条语句 fflush(stdout)
.
gdb> call fflush(stdout) $1 = 0 gdb> quit Detaching from program: /tmp/a.out, process 12934
程序依旧还在执行,我们已经得到结果了.
$ cat output.txt 0/0 1/1 2/4 3/9 4/16 5/25 6/36 7/49 8/64 9/81