计算机系统篇之异常控制流(9):异常控制流 FAQ

Author: stormQ

Created: Monday, 21. September 2020 02:47PM

Last Modified: Monday, 21. September 2020 03:24PM



摘要

本文描述了异常控制流相关的常见问题,比如:1)孤儿进程的父进程一定是 init 进程吗?2)子进程停止时,内核一定会发送 SIGCHLD 信号给父进程吗?3)…

FAQ 1:孤儿进程的父进程一定是 init 进程吗?

验证思路:

构造这样一种情形:子进程在父进程终止后仍运行。此时,在子进程中获得其父进程 PID。如果父进程 PID 不是 1(即init进程的进程 ID),那么表明孤儿进程的父进程不一定是init进程。

验证过程:

源码,proc22_main.cpp:

#include <cstdio>
#include <cstdlib>
#include <unistd.h>

void foo(int exit_status)
{
    if (0 == fork())
    {
        for (int i = 0; i < 3; i++)
        {
            const auto self_pid = getpid();
            const auto parent_pid = getppid();
            std::printf("I'm child(pid:%d), parent PID: %d\n"
                self_pid, parent_pid);
            sleep(1);
        }
        std::exit(exit_status);
    }
}

int main()
{
    for (int i = 0; i < 5; i++)
    {
        foo(i);
    }
    return 0;
}

编译:

$ g++ -o proc22_main proc22_main.cpp -g

运行(On Ubuntu 16.04):

$ ./proc22_main 
I'm child(pid:10418), parent PID: 10417
I'
m child(pid:10419), parent PID: 10417
I'm child(pid:10420), parent PID: 10417
I'
m child(pid:10421), parent PID: 10417
I'm child(pid:10422), parent PID: 10417
$ I'
m child(pid:10418), parent PID: 3246
I'm child(pid:10420), parent PID: 3246
I'
m child(pid:10419), parent PID: 3246
I'm child(pid:10421), parent PID: 3246
I'
m child(pid:10422), parent PID: 3246
I'm child(pid:10418), parent PID: 3246
I'
m child(pid:10421), parent PID: 3246
I'm child(pid:10420), parent PID: 3246
I'
m child(pid:10422), parent PID: 3246
I'm child(pid:10419), parent PID: 3246

从上面的输出结果中可以看出,在父进程退出后,子进程(PID 为 10420)的父进程 PID 为 3246。

查看 PID 为 3246 的进程启动项:

$ cat /proc/3246/cmdline
/sbin/upstart--user

可以看出,在父进程退出后,子进程(PID 为 10420)的父进程为/sbin/upstart--user,而非init进程。

验证结论:

孤儿进程的父进程不一定是 init 进程。


FAQ 2:只要父进程收到一个 SIGCHLD 信号,就一定有子进程可以回收吗?

验证思路:

我们知道,只有当子进程已终止时,才可以对其进行回收。如果可以构造这样一种情形:父进程收到了一个SIGCHLD信号,但子进程尚未终止,那么表明父进程收到一个SIGCHLD信号时,不一定有子进程可以回收了。

验证过程:

源码,proc26_main.cpp:

#include <cstdio>
#include <cstdlib>
#include <unistd.h>
#include <errno.h>
#include <signal.h>
#include <cstring>

void foo(int exit_status)
{
    if (0 == fork())
    {
        const auto self_pid = getpid();
        const auto parent_pid = getppid();
        std::printf("I'm child(pid:%d), parent PID: %d\n"
            self_pid, parent_pid);
        for (int i = 0; i < 3; i++)
        {
            sleep(1000);
        }
        std::exit(exit_status);
    }
}

void sigchld_hanlder(int sig)
{
    if (SIGCHLD != sig)
    {
        return;
    }
    const char *msg = "catch SIGCHLD signal\n";
    write(STDOUT_FILENO, msg, strlen(msg));
}

bool SetSignalHanlder(int sig, void (*hanlder)(int))
{
  struct sigaction sa;
  std::memset(&sa, 0sizeof(sa));
  sa.sa_handler = hanlder;
  sa.sa_flags = SA_RESTART; // restart syscalls if possible
  return 0 == sigaction(sig, &sa, NULL);
}

int main()
{
    SetSignalHanlder(SIGCHLD, sigchld_hanlder);
    for (int i = 0; i < 1; i++)
    {
        foo(i);
    }
    while (true)
    {
        sleep(1);
    }
    return 0;
}

编译:

$ g++ -o proc26_main proc26_main.cpp -g

运行:

$ ./proc26_main 
I'm child(pid:16174), parent PID: 16173

在运行 proc26_main 后,向子进程(PID 为 16174)发送一个SIGTSTP信号,用于停止该进程。在另一个 shell 窗口中执行如下命令:

$ kill -SIGTSTP 16174

在执行上述命令后,父进程会打印如下内容:

catch SIGCHLD signal

上面的输出结果表示父进程收到了一个SIGCHLD信号。

此时,查看进程启动项中所有含proc26_main的进程:

$ pgrep -f "proc26_main"
16173
16174

可以看出,在父进程收到了一个SIGCHLD信号后,子进程(PID 为 16174)尚未终止。此时,子进程处于停止状态,不能被回收。

另外,需要注意:子进程处于停止状态时,再向其发送SIGTSTP信号,内核不会再发送SIGCHLD信号给父进程。

验证结论:

父进程收到一个SIGCHLD信号时,不一定有子进程可以回收了。


FAQ 3:子进程停止时,内核一定会发送 SIGCHLD 信号给父进程吗?

验证思路:

如果可以构造这样一种情形:子进程变成已停止状态时,父进程未收到SIGCHLD信号,那么表明子进程停止时,内核不一定会发送SIGCHLD信号给父进程。

验证过程:

源码,proc27_main.cpp:

#include <cstdio>
#include <cstdlib>
#include <unistd.h>
#include <errno.h>
#include <signal.h>
#include <cstring>

void foo(int exit_status)
{
    if (0 == fork())
    {
        const auto self_pid = getpid();
        const auto parent_pid = getppid();
        std::printf("I'm child(pid:%d), parent PID: %d\n"
            self_pid, parent_pid);
        for (int i = 0; i < 3; i++)
        {
            sleep(1000);
        }
        std::exit(exit_status);
    }
}

void sigchld_hanlder(int sig)
{
    if (SIGCHLD != sig)
    {
        return;
    }
    const char *msg = "catch SIGCHLD signal\n";
    write(STDOUT_FILENO, msg, strlen(msg));
}

void sigtstp_hanlder(int sig)
{
    if (SIGTSTP != sig)
    {
        return;
    }
    const char *msg = "catch SIGTSTP signal\n";
    write(STDOUT_FILENO, msg, strlen(msg));
}

bool SetSignalHanlder(int sig, void (*hanlder)(int))
{
  struct sigaction sa;
  std::memset(&sa, 0sizeof(sa));
  sa.sa_handler = hanlder;
  sa.sa_flags = SA_RESTART; // restart syscalls if possible
  return 0 == sigaction(sig, &sa, NULL);
}

int main()
{
    SetSignalHanlder(SIGCHLD, sigchld_hanlder);
    SetSignalHanlder(SIGTSTP, sigtstp_hanlder);
    for (int i = 0; i < 1; i++)
    {
        foo(i);
    }
    while (true)
    {
        sleep(1);
    }
    return 0;
}

编译:

g++ -o proc27_main proc27_main.cpp -g

运行:

$ ./proc27_main 
I'm child(pid:20917), parent PID: 20916

在运行 proc27_main 后,向子进程(PID 为 20917)发送一个SIGTSTP信号,用于停止该进程。在另一个 shell 窗口中执行如下命令:

$ kill -SIGTSTP 20917

在执行上述命令后,子进程会打印如下内容:

catch SIGTSTP signal

上面的输出结果表示子进程收到了一个SIGTSTP信号。

但是,父进程未打印catch SIGCHLD signal,表明父进程未收到SIGCHLD信号。

因此,可以得出结论:如果导致子进程停止的信号的处理程序不是默认行为时,子进程停止不会导致父进程收到SIGCHLD信号。 也就是说,子进程停止时,父进程会收到SIGCHLD信号的前提是导致子进程停止的信号的处理程序是默认行为。

验证结论:

子进程停止时,内核不一定会发送SIGCHLD信号给父进程。


FAQ 4:为什么不能用信号对其他进程中发生的事件计数?

原因:

由于同一种类型的待处理信号至多有一个,即信号不会排队等待。所以,不能用信号对其他进程中发生的事件计数。


下一篇:计算机系统篇之异常控制流(10):Chapter 8 Exceptional Control Flow 章节习题与解答

上一篇:计算机系统篇之异常控制流(7):利用 fork 和 execve 实现一个简易的 shell 程序

首页