缺少 WUNTRACED 参数引发的惨案

在写简易 shell 程序的时候,因 waitpid 函数的使用不当导致程序出现了预料之外的行为。求助了各路高手,找了一天,最后终于定位到了源头,原来是很细小的问题导致。

问题描述

上代码 mystop.c ,没兴趣看可以先跳过,描述在下面。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
/*
* mystop.c - Another handy routine for testing tiny shell
*
* usage: mystop <n>
* Sleeps for <n> seconds and sends SIGTSTP to itself.
*
*/
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <signal.h>
extern int errno;
int main(int argc, char **argv)
{
int i, secs;
pid_t pid;

if (argc != 2) {
fprintf(stderr, "Usage: %s <n>\n", argv[0]);
exit(0);
}
secs = atoi(argv[1]);

for (i=0; i < secs; i++)
sleep(1);

pid = getpid();

if (kill(-pid, SIGTSTP) < 0)
fprintf(stderr, "kill (tstp) error");

exit(0);
}

在自制简易 shell 中调用 mystop 程序时,在 kill处阻塞了,没有正常返回到主进程。但是!!如果通过Ctrl + Z,键盘上触发SIGTSTP 信号,mystop 可以正常返回。这很使我迷惑。
what f** ??
image.png

问题分析

  1. 父进程(tsh)设置了sigchld_handler, sigtstp_handler, sigint_handler等几个信号处理函数。 在子进程停止或终止时会给父进程(tsh)发送SIGCHLD 信号,父进程将其捕获并 调用sigchld_handler及时清理掉僵死进程。sigtstp_handler 捕获SIGTSTP信号,将其转发给子进程。sigint_handler 同sigint_handler,将SIGINT 捕获并转发给子进程。
  2. 子进程调用 execv() 之前,通过setpgid() 设置了新进程组。且调用execv 之后信号处理函数会恢复默认处理。因此此前给父进程设置的sig_handler不会影响子进程。
  3. 键盘触发Ctrl + Z,信号可能是被父进程捕获转发给子进程;而子进程一定是自己内部调用kill(-pid, SIGTSTP)

  4. Mystop 调用 kill(-pid, SIGTSTP)时,因与父进程不在一个进程组因此父进程不会收到SIGTSTP,不会触发sigtstp_handler。

  5. Mystop 进程调用kill 时,父进程sigchld_handler能捕获收到SIGCHLD 信号。能捕获到 SIGCHLD,可以说明 Mystop 正常终止了,但为什么还阻塞在那儿了呢?越发迷惑 emmm…

尝试

测试了一会,也看了书,Google了一下没人遇到这样的奇葩问题。焦灼中man 了一下 waitpid

If the WUNTRACED option is set, children of
the current process that are stopped due to a SIGTTIN, SIGTTOU, SIGTSTP,
or SIGSTOP signal also have their status reported.

之前看书不够细心,没有注意太多 terminated 和 stopped 。误以为 waitpid 只会在有 terminated 状态时返回 terminated process 的 pid。看到 manual page 中,提到了 「stopped」。于是立马燃起了希望,加上 | WUNTRACED。果然解决问题

Finally

image.png
其实在 mystop 调用 kill(-pid, SIGTSTP)后,子进程立即变为stop 状态了。但waitpid 没加WUNTRACED 参数不能返回其 pid,因此没有正确将job的 state 更新。而父进程调用waitfg ,一直在等待 ForeGround进程改变状态。所以出现的效果就是之前看到的那样,好像是mystop 阻塞了。然而事实是 mystop 没有阻塞,已经是stop 状态了,阻塞在了 waitfg,主进程。

总结

  • exec函数将原先设置为要捕捉的信号都更改为默认动作,其他信号的状态则不变(一个进程原先要捕捉的信号,当其执行一个新程序后,就不能再捕捉了,因为信号捕捉函数的地址很可能在所执行的新程序文件中已无意义)。
  • When a child process terminates or stops, the kernel sends a SIGCHLD signal (number 17) to the parent. (Yes, stop will do too!)

  • WUNTRACED: Suspend execution of the calling process until a process in the wait set becomes either terminated or stopped. Return the PID of the terminated or stopped child that caused the return. The default behavior returns only for terminated children. This option is useful when you want to check for both terminated and stopped children.

纸上得来终觉浅,绝知此事要宫刑躬行。 体会到很多前辈说的那句话:学计算机的,看了书不能说明你掌握了。只有实践过后才能算基本掌握。(我加一句,学计算机的,不能完全相信自己的眼睛 (mystop: 我就说你冤枉我了 >_< )

image.png