多线程下 fork 与 exit 引发的连锁错误

这篇文章存在问题!!!

问题出现在这样的情景下:

编译完 loggerTest.cpp 后,执行后,程序被阻塞不能退出,在通过 ps aux|grep ./loggerTest 一看,好家伙,原来是 asyncLogging.cpp 中的rollFile 函数 fork 出来的进程都还没释放掉,而且都处在 S 状态,也就是被阻塞了!

本来 fork 出这个进程是为了 roll file 的,也就是把 log 文件进行压缩打包用的。代码片段如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
 ...
// 执行脚本,对原来的 日志包 解包,对每个日志文件重命名(0=>1,1=>2 ...),然后将 1~N 的日志文件重新压缩打包,父进程等待子进程退出,这阶段父进程不能持有任何锁
char *args[3];
char newfile[100];
memset(newfile, 0, sizeof(newfile));
memcpy(newfile, ret.c_str(), ret.size());
args[0] = "jiaoben";
args[1] = newfile;
args[2] = 0;
pid_t pid;
if ((pid = fork()) == 0)
execv("jiaoben", args);
wait(0); // 等待子进程结束
return ret;
}

可以看到本来的预期是 fork 出一个子进程后,立马 execv 让可执行文件 jiaoben 来覆盖原来的内容(包括页表,各种锁、条件变量的状态,当然还有内核数据结构)。jiaoben 这个可执行文件就是用来执行 python 脚本的,照理说执行完会立马退出的呀!

好在这个进程没退出,我们可以用 gdb 来追踪它的状态:

gdb 追踪状态

这是妥妥的连 execv 都没执行成功呀(因为 如果没有子进程 wait(0) 会直接返回 -1,所以子进程相当于继续执行下去了)!然后一看目录下,我晕,jiaoben.cpp 忘记编译了,所以 execv 执行失败,后端日志线程就回到 while 循环了,一直执行 _cond.waitForSecs(3); 这也就导致该进程在 ps 命令中看上去一直被阻塞了!

这也是血的教训呀!每次调用函数后一定要判断是否调用成功!不然有的苦头吃啊~~接下来,自然地将代码改成如下片段:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
 ...
// 执行脚本,对原来的 日志包 解包,对每个日志文件重命名(0=>1,1=>2 ...),然后将 1~N 的日志文件重新压缩打包,父进程等待子进程退出,这阶段父进程不能持有任何锁
char *args[3];
char newfile[40];
memset(newfile, 0, sizeof(newfile));
memcpy(newfile, ret.c_str(), ret.size());
args[0] = "jiaoben";
args[1] = newfile;
args[2] = 0;
pid_t pid;
if ((pid = fork()) == 0)
if(-1 == execv("jiaoben", args))
exit(1);
wait(0); // 等待子进程结束
return ret;
}

很自然呀!如果 execv 执行失败,让它退出就行了!但是好像并没那么容易,再次执行 loggerTest,发现连该进程都被阻塞了。当即用 ps aux|grep ./loggerTest 查看后,发现有两个名为loggerTest 的进程处在运行状态,退不出去!那没事,只要你还活着,我就能用 gdb attach 到你~~继续使用 gdb 去查看哪里出错了:

这一个 gdb 追踪的是原始的 ./loggerTest 进程,因为它有 2 个线程,而 fork 只能 fork 出一个线程。

原始 ./threadTest 进程

可以看到测试进程./loggerTest 的主线程一直在 AsyncLogging::stop 上:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
void stop() {
// 如果 _currentBuffer 中有数据是不能让 _running 变成 False 的,不然就丢失数据了
// 必须是 前端退出后,才能调用后端的 stop
assert(_running);
size_t size;
{
MutexGuard guard(_lock);
size = _currentBuffer->size();
}
while (size > 0) {
_cond.wakeup();
MutexGuard guard(_lock);
size = _currentBuffer->size();
}
_running = false;
_cond.wakeup();
_thread.join();
}

而这个函数一直停留在 stop 中的唯一理由就是一直在 while 循环内出不去,也就是 wakeup 没作用,日志后端线程根本不鸟这个 wakeup。

再来看看打包日志的进程,它又是为什么迟迟不肯退出呢?

打包日志的进程

可以看到它也一直停留在 AsyncLogging::stop 上。并且这个进程只有一个线程,那么这个 wakeup 就很搞笑了呀,因为根本没有接收者,或者说接收这个 wakeup 的人就是你自己,而你此时在 while 循环里永远也接收不到。再回到 roll file 的代码片段,它是一直阻塞等待着子进程退出才会继续执行下去的,然而此时子进程(打包日志的进程)永远也退不出,这就导致了 ./loggerTest 进程的后端日志线程永远被阻塞(可以通过 gdb 查看 thread 2 阻塞的位置就知道了 __GI__wait4),

./loggerTest 进程的 thread 2

进而导致前端日志线程退出时执行 AsyncLogging::stop 的 wakeup 却得不到后端日志线程的响应(因为它一直 wait(0) 等着子进程退出),这一系列连锁反应就导致了现在的尴尬局面~~

该怎么解决?那就是让 execv 一定要执行成功,在执行前判断脚本文件是否存在,如果不存在则不要在多线的情况下去 fork 了,很容易造成复杂的错误!

带来的启发

  • 在多线程环境下,调用 fork 后一定要立马确定 execv 成功,不然的话很容易出现复杂难以诊断的错误。因为 fork 在 linux 环境下只会 fork 出一个线程,这样本质上就和 锁,条件变量 等待同步工具在概念上矛盾了(同步工具用于多线程环境,而 fork 出来的是单线程进程)。
  • 不要以为调用了 exit 后 os 一定就会帮你释放掉资源,因为在 C++ 中,还存在全局对象析构这么一个环节,很有可能在这个环节 整个进程就阻塞住了,进而导致资源得不到释放,慢慢的把你的内存吃光!!!