暗无天日

=============>DarkSun的个人博客

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定义的函数 readwrite 则是不带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