Linux基础——信号

目录

[1. 什么是信号?](#1. 什么是信号?)

[2. 信号的产生](#2. 信号的产生)

①键盘的组合键

②kill命令

④产生异常

⑤软件条件

[⑥进程等待中的core dump](#⑥进程等待中的core dump)

[3. 信号的保存](#3. 信号的保存)

①信号的发送与接收

②为什么需要保存信号?

③信号是如何保存的

[4. 信号的捕捉处理](#4. 信号的捕捉处理)

①信号的处理与处理时机

②再谈进程地址空间

③信号的捕捉

④可重入函数与不可重入函数

⑤从信号角度谈谈C++中的volatile

⑥SIGCHLD


在介绍信号之前,我们先用一个浅显的例子来理解信号

我们点了一个外卖,

外卖小哥打了个电话------产生了一个信号

此时可能在打游戏,等待一个合适的时间处理------信号被记录下来

打完游戏后,去取外卖------处理信号

1. 什么是信号?

对于信号的概念,我们还是举几个生活中的例子,比如

照明信号弹,上下课铃声,发令枪,集合哨子,闹钟,外卖电话等等

从这里我们就可以提出几点

  1. 我们是怎么认识这些信号的?------有人教 -> 我们记住了这些常见的信号

  2. 即便没有信号产生,我们也知道信号产生之后该干什么

  3. 信号产生了并不代表我们要立刻处理这个信号,可以挑选一个合适的时间去处理,因为我们有可能正在做更重要的事。因此,信号产生后会为他创建一个时间窗口,在处理信号时,我们需要在这个时间窗口内记住有信号来过。

我们类比到OS中来,进程就是这里的"我们",即

  1. 进程必须能够识别 + 处理信号------即使信号没有产生,也要有具备处理信号的能力------信号的处理能力属于进程内置功能的一部分

  2. 当进程真的收到一个具体信号时,进程可能并不会立即处理这个信号,而是留待合适的时候处理

注:信号的处理方式分为:1. 默认动作; 2. 忽略; 3. 自定义动作

  1. 一个信号从产生到信号开始被处理,一定会有一个时间窗口,进程具有临时保存哪些信号已经产生了的能力

我们可以使用

bash 复制代码
kill -l

来查看所有信号,即

每个信号的序号及其完整的英文名称:

  1. SIGHUP - Hangup (挂断)

  2. SIGINT - Interrupt (中断)

  3. SIGQUIT - Quit (退出)

  4. SIGILL - Illegal Instruction (非法指令)

  5. SIGTRAP - Trace/Breakpoint Trap (跟踪/断点陷阱)

  6. SIGABRT - Abort (中止)

  7. SIGBUS - Bus Error (总线错误)

  8. SIGFPE - Floating-Point Exception (浮点异常)

  9. SIGKILL - Kill (杀死进程)

  10. SIGUSR1 - User-Defined Signal 1 (用户定义信号1)

  11. SIGSEGV - Segmentation Fault (段错误)

  12. SIGUSR2 - User-Defined Signal 2 (用户定义信号2)

  13. SIGPIPE - Broken Pipe (管道破裂)

  14. SIGALRM - Alarm Clock (闹钟)

  15. SIGTERM - Termination (终止)

  16. SIGSTKFLT - Stack Fault (栈错误)

  17. SIGCHLD - Child Status Changed (子进程状态改变)

  18. SIGCONT - Continue (继续)

  19. SIGSTOP - Stop (Cannot be caught or ignored) (停止)

  20. SIGTSTP - Terminal Stop Signal (终端停止信号)

  21. SIGTTIN - Background Read from TTY (后台读取TTY)

  22. SIGTTOU - Background Write to TTY (后台写入TTY)

  23. SIGURG - Urgent Condition on Socket (套接字紧急情况)

  24. SIGXCPU - CPU Time Limit Exceeded (CPU时间限制超出)

  25. SIGXFSZ - File Size Limit Exceeded (文件大小限制超出)

  26. SIGVTALRM - Virtual Alarm Clock (虚拟闹钟)

  27. SIGPROF - Profiling Timer Expired (性能分析时间到期)

  28. SIGWINCH - Window Size Change (窗口大小改变)

  29. SIGIO - I/O Now Possible (I/O操作可能)

  30. SIGPWR - Power Failure (电源故障)

  31. SIGSYS - Bad System Call (错误系统调用)

其中1~31号信号为普通信号,34~64号信号为实时信号(需要立即处理)

我们之前已经知道了,ctrl+c 能够直接杀掉前台进程,这是为什么呢?------键盘输入是前台输入,首先会被前台进程捕获到,而 ctrl+c 本质上被进程解释成收到了2号信号,我们可以使用signal接口来验证,接口如下

signal() 的行为在不同的 UNIX 版本之间有所不同,并且在不同版本的 Linux 中也有历史上的变化。建议避免使用 signal(),而应使用 sigaction(2)

signal() 将信号 signum 的处理方式设置为 handler,该处理方式可以是 SIG_IGNSIG_DFL,或者是程序员定义的函数的地址(即"信号处理函数")。

如果信号 signum 被传递给进程,则会发生以下情况之一:

  • 如果处理方式设置为 SIG_IGN,则信号将被忽略。

  • 如果处理方式设置为 SIG_DFL,则会执行与该信号相关的默认操作(请参见 signal(7))。

  • 如果处理方式设置为一个函数,则首先会将处理方式重置为 SIG_DFL,或者信号会被阻塞(请参见下面的可移植性部分),然后调用 handler,并将 signum 作为参数传递。如果调用处理函数导致信号被阻塞,则在返回处理函数后,该信号会被解锁。

信号 SIGKILLSIGSTOP 不能被捕获或忽略。

测试代码如下

cpp 复制代码
int main()
{
    // 默认处理
    signal(SIGINT, SIG_DFL);

    while (true)
    {
        lg(Debug, "this is a test process\n");
        sleep(3);
    }

    return 0;
}

运行如下

可以看到,对2号信号的默认动作就是终止自己,接下来我们使用自定义动作试试,测试代码如下

cpp 复制代码
// signo - 信号编号
void myhandler(int signo)
{
    lg(Info, "get a signal, signo : %d\n", signo);
}

int main()
{
    signal(SIGINT, myhandler);

    while (true)
    {
        lg(Debug, "this is a test process\n");
        sleep(3);
    }

    return 0;
}

运行有

可以看到,我们确实收到了2号信号,且由于我们采取了自定义操作,将处理方式变为了向屏幕打印而不是终止。那我们为什么要将 signal 函数放在代码最前面呢?------设置一次后,再往后都有效。

接下来我们讲讲键盘数据是如何输入给OS的,以及 ctrl+c 是如何变成信号的,这就需要我们谈到硬件了,我们用画图来解释,如图

如图所示,键盘输入内容后,键盘被按下后,键盘通过针脚向CPU发送硬件中断,随后OS根据中断号,将其作为索引在中断向量表中执行对应的方法,最后这个读方法会让OS将键盘中的内容读取到内部的文件缓冲区中。

在这个过程中,如果在最后一步,OS将键盘中内容拷贝到内部文件缓冲区前,OS会先判断数据,即如果识别到数据是 ctrl+c 就会将其转化成2号信号发送给对应进程。我们学习的信号,就是使用软件方式模拟硬件中断,从而给进程发送信号。

2. 信号的产生

信号的产生一般分为如下几种

①键盘的组合键

我们可以使用键盘的组合键向前台进程发送信号,比如:

ctrl+c表示发送2号信号(SIGINT);

ctrl+\表示发送3号信号(SIGQUIT);

ctrl+z表示发送19号信号(SIGSTOP)。

②kill命令

举个例子

bash 复制代码
kill -19 23541

就表示向pid为23541的进程发送19号信号(SIGSTOP)。

③系统调用。如:kill, raise, abort等,测试代码如下

cpp 复制代码
void Help(string proc)
{
    lg(Fatal, "Please follow the format: %s signo pid\n", proc.c_str());
}

int main(int argc, char* argv[])
{
    if (argc != 3)
    {
        Help(argv[0]);
        exit(1);
    }

    int signo = stoi(argv[1]);
    pid_t pid = stoi(argv[2]);

    int n = kill(pid, signo);
    if (n == -1)
    {
        lg(Fatal, "kill error : %s", strerror(errno));
        exit(2);
    }

    return 0;
}

运行效果

测试代码2

cpp 复制代码
int main()
{
    int cnt = 5;
    while (true)
    {
        cout << "this is a test process, my pid : " << getpid() << endl;
        sleep(1);
        
        cnt--;
        if (cnt == 0) raise(9);
    }

    return 0;
}

运行效果

abort类似于raise,不同点在于发送的是6号信号。

④产生异常

我们举个例子,测试代码如下

cpp 复制代码
int main()
{

    sleep(1);
    lg(Debug, "point error before\n");
    int *p;
    *p = 5;

    sleep(1);
    lg(Debug, "point error after\n");

    sleep(1);
    return 0;
}

运行效果

可以发现,进程由于使用了野指针崩溃了,我们对其捕捉并自定义有

cpp 复制代码
void handler(int signo)
{
    lg(Info, "process %d get a signal, signo : %d\n", getpid(), signo);
}

int main()
{
    signal(SIGSEGV, handler);

    sleep(1);
    lg(Debug, "point error before\n");
    int *p;
    *p = 5;

    sleep(1);
    lg(Debug, "point error after\n");

    sleep(1);
    return 0;
}

运行有

为什么这里一直调用了handler函数呢?信号又为什么一直触发呢?------进程未退出(未变成Z状态)。而我们之前学到过try ... catch{} 肯定与信号有关,不过它发出的信号应该是很温和的,因为我们可以自己对这种情况进行处理。此外,还有除0错误等硬件异常。但是,异常并不只由硬件产生,比如在使用管道的时候,写端正在写入,读端被关闭也会产生异常。

⑤软件条件

在Linux中,我们可以使用alarm接口来向进程发出14号信号(SIGALRM),它的接口如下

alarm() 函数安排在指定的秒数后将一个 SIGALRM 信号发送给调用进程。

如果秒数为零,任何待处理的警报将被取消。

无论如何,之前设置的任何 alarm() 都会被取消。

测试代码如下

cpp 复制代码
int main()
{
    int n = alarm(5);

    while (true)
    {
        lg(Info, "this is a process, my pid : %d\n", getpid());
        sleep(1);
    }

    return 0;
}

运行效果如下

我们可以通过重复设置闹钟,使代码每5s响一次,每几秒执行一次自己的任务。修改后代码如下

cpp 复制代码
void handler(int signo)
{
    lg(Info, "pocess %d get a signal, signo : %d\n", getpid(), signo);
    alarm(5);
}

int main()
{
    signal(SIGALRM, handler);

    int n = alarm(5);

    while (true)
    {
        lg(Info, "this is a process\n");
        sleep(1);
    }

    return 0;
}

运行效果如下

⑥进程等待中的core dump

之前我们进程控制中提到,对于一个进程来说,其正常退出与被信号杀死退出是不一样的,具体表现在 waitpid 的第二个参数status,图解如下

这个core dump具体大致分为两种形式,我们可以在man 7 signal 中查看到

我们可以用下面的代码来验证

cpp 复制代码
int main()
{
    pid_t id = fork();

    // child
    if (id == 0)
    {
        while (true)
        {
            lg(Info, "this is child, my pid: %d", getpid());
            sleep(1);
        }

        exit(0);
    }

    // father
    int status = 0;
    pid_t rid = waitpid(id, &status, 0);
    if (rid == id)
    {
        lg(Info, "child quit! rid: %d, exit code: %d, exit signal: %d, core dump: %d\n", rid, (status>>8)&0xFF, (status&0x7F), (status>>7)&1);
    }

    return 0;
}

我们向其发送2号信号有

向其发送8号信号有

可以看到,两种退出方式中的core dump各不相同,而在我们发送信号后,会生成一个core.xxx的文件,如图

这里的32734其实就是运行时进程的pid。

当我们打开系统中的core dump功能时,一旦进程出现异常,OS会将进程在内存中的运行信息 dump(转储) 到进程的当前目录中,形成 core.pid 文件,我们将其称为核心转储(core dump)

形成 core dump 属于运行时出错,而出错了我们一定是想知道在哪一行出了问题。而我们可以在gdb调试中使用 core_file core.pid 来直接定位到出错行。

但是这个功能在云服务器中是默认关闭的,为什么?------因为如果服务器因为出问题陷入了停机就会重启,而重启之后又停机的话,会在这个过程中产生大量的core dump文件从而冲击到磁盘。

3. 信号的保存

①信号的发送与接收

在讲信号的保存前,我们先来讲讲信号是如何发送与接收的。

之前我们说过,OS可以使用 kill 命令来向指定的进程发送对应信号。那么对于一个进程来说,我怎么知道自己有没有收到一个信号呢?其实OS给进程发信号,其实就是给进程的PCB中发信号,我们可以在PCB(task_struct)中查看到

其实OS是将一个 int 的32位作为一个位图,并使用它来管理普通信号(1~31号)。那就意味着如果一次性发十几个信号的话会遗失一部分。

注:实时信号不会遗失,且会立刻执行,虽然它的设计理念与普通信号相同,但是它们实际是使用双链表来管理的。

我们来谈谈这个位图,一个 int 可以表示成 0000 0000 0000 0000 0000 0000 0000 0000,其中

  1. 比特位的内容是0还是1,表示是否收到该信号

  2. 比特位的位置(第几个)表示信号的编号

  3. 所谓的向进程"发信号",其本质就是在OS去修改PCB中的信号位图对应的比特位!

②为什么需要保存信号?

因为进程在收到信号后,可能不会在现在处理这个信号,而信号不被处理,就会产生一个时间窗口

③信号是如何保存的

我们先来介绍一些信号的常见概念

  1. 实际执行信号的处理动作称为信号递达(Delivery)。

  2. 信号从产生到递达之间的状态,称为信号未决(Pending)。

  3. 进程可以选择阻塞(Block)某个信号。

  4. 被阻塞的信号产生时将保持在未决状态,直到进程解除对此信号的阻塞,才执行递达的动作。

注:阻塞和忽略是不同的,只要信号被阻塞就不会递达,而忽略是在递达之后可选的一种处理动作。

因为普通信号一共有31种,每一种信号都要有自己的处理办法,那么我们可以猜测一下,OS中一定有下面这种类似的设计

cpp 复制代码
typedef void (*handler_t)(int); // -- 对应操作的函数指针
handler_t handler[31];          // -- 31种信号的操作集

实际上,在Linux中是这样设计的,如图

即在PCB中,对于信号方面有三个表,分别是阻塞表,保存表,处理表。

在阻塞表中,对应位置的数字为1则表示该信号被阻塞,数字为0则则表示该信号未被阻塞;

在保存表中,对应x位置的数字被设置成1时,表示进程收到了x号信号;

在处理表中,对应位置的处理一般分为SIG_DFL(默认处理),SIG_IGN(忽略处理),自定义处理,而在内核中我们可以看到SIG_DFL与SIG_IGN分别是由0和1强制类型转换来的。

注:阻塞和忽略是不同的,举个简单的例子,忽略就是已读不回,而阻塞是未读。

那我们有没有办法来修改这几个表呢?------有的,我们可以使用信号集操作函数来修改block表与pending表。

cpp 复制代码
int sigemptyset(sigset_t *set);
int sigfillset(sigset_t *set);
int sigaddset (sigset_t *set, int signo);
int sigdelset(sigset_t *set, int signo);
int sigismember(const sigset_t *set, int signo);

我们简单介绍一下上面这些函数

  1. 函数 sigemptyset 初始化set所指向的信号集,使其中所有信号的对应bit清零,表示该信号集不包含任何有效信号。

  2. 函数 sigfillset 初始化set所指向的信号集,使其中所有信号的对应bit置位,表示该信号集的有效信号包括系统支持的所有信号。

注: 在使用 sigset_ t 类型的变量之前,一定要调用 sigemptyset 或 sigfillset 做初始化, 使信号集处于确定的状态。

  1. 初始化 sigset_t 变量之后就可以调用 sigaddset 和 sigdelset 在该信号集中添加或删除某种有效信号。

  2. sigismember 是一个布尔函数, 用于判断一个信号集的有效信号中是否包含某种信号,若包含则返回1,不包含则返回0,出错返回-1。

在介绍完后,我们讲讲如何修改 Block 表和pending表,我们需要使用

cpp 复制代码
int sigprocmask(int how, const sigset_t *set, sigset_t *oset);

sigprocmask() 函数用于获取和/或更改调用线程的信号屏蔽字。信号屏蔽字是指当前对于调用者被阻塞(即无法接收)的信号集合(有关更多详细信息,请参阅 signal(7))。

此调用的行为取决于 how 参数的值,具体如下:

  • SIG_BLOCK
    • 被阻塞的信号集合是当前集合与 set 参数的并集。
  • SIG_UNBLOCK
    • 从当前被阻塞的信号集合中移除 set 中的信号。尝试解除一个未被阻塞的信号的阻塞是允许的。
  • SIG_SETMASK
    • 被阻塞的信号集合被设置为 set 参数指定的集合。

如果 oldset 非空,则信号屏蔽字之前的值会被存储在 oldset 中。

如果 set 为空,则信号屏蔽字保持不变(即 how 被忽略),但如果 oldset 非空,当前信号屏蔽字的值仍会被存储在 oldset 中。

在多线程进程中,sigprocmask() 的使用未指定;请参见 pthread_sigmask(3)

还有

cpp 复制代码
int sigpending(sigset_t *set);

sigpending() 函数返回调用线程(即调用该函数的线程)待处理的信号集合(也就是那些在阻塞期间被引发但尚未交付的信号)。待处理信号的掩码会返回在 set 中。

测试代码如下

cpp 复制代码
void PrintPending(sigset_t &pending)
{
    // 打印1~31号信号
    cout << "pending: ";
    for (int signo = 1; signo <= 31; signo++)
    {
        if (sigismember(&pending, signo))
        {
            cout << "1";
        }
        else
        {
            cout << "0";
        }
    }
    cout << endl;
}

int main()
{
    // 1. 我们想阻塞2号信号
    sigset_t set, oset;
    sigemptyset(&set);
    sigemptyset(&oset);
    sigaddset(&set, 2); // 到这里只是预备好了资源,并没有设置进内核中

    // 2. 设置进内核
    sigprocmask(SIG_SETMASK, &set, &oset); // 已经将2号信号设置为阻塞了!

    // 3. 打印pending
    sigset_t pending;
    int cnt = 10;
    while (true)
    {
        int n = sigpending(&pending);
        if (n < 0)
        {
            lg(Fatal, "sigpending error!\n");
            continue;
        }
        PrintPending(pending);

        // 10秒后解除阻塞2号信号
        sleep(1);
        cnt--;
        if (cnt == 0)
        {
            lg(Info, "unblock the signo 2\n");
            sigprocmask(SIG_SETMASK, &oset, nullptr);
        }
    }

    return 0;
}

运行效果如下

接下来,我们对2号信号进行自定义捕捉,有

那么我们可以屏蔽所有信号吗?------当然不行,前面我们提到过的9号与19号信号都是不可被屏蔽的!

4. 信号的捕捉处理

①信号的处理与处理时机

我们先来看看信号是如何被处理的,在这里我们可以使用一张图来描述

在这个过程中我们也可以看到,当进程从内核态返回到用户态的时候,进行信号的检测与处理。那么什么时候才会切换身份呢?我们举几个例子

  1. 调用系统调用。OS是会自动做身份切换的,即 用户身份 ↔ 内核身份

  2. 汇编指令 int 80。这条汇编指令会使用户态陷入内核态

②再谈进程地址空间

在一个正常运转的OS中,用户页表有几份呢?------因为进程具有独立性,因此有几个进程就有几个用户级页表。那么内核页表有几份呢?------仅有1份!因为每个进程看到的3~4GB(即内核空间)的东西是一样的,整个OS中进程再怎么切换,内核空间的内容是不会变的。

对于进程来说,自己调用系统中的方法,就是在我进程地址空间中的代码区执行的;

对于OS来说,在任意一个时刻,都有进程正在运作,它不管什么时候想执行OS的代码,就可以马上执行。

因此,对于一个进程的工作状态来说,内核态就是能够访问OS的代码与数据,用户态就是只能访问自己的代码与数据。

③信号的捕捉

我们已经知道,信号在收到后会保存在三个表中,而信号的捕捉实际上就是在研究 pending 表什么时候由1->0,我们可以使用 sigaction 函数来对每个信号的行为,其接口如下

cpp 复制代码
int sigaction(int signum, const struct sigaction *act, struct sigaction *oldact);

其中,signum 表示信号序号,act 是一个输入型参数,oldact 是一个输出型参数,而这个struct sigaction 结构体如下

cpp 复制代码
struct sigaction 
{
    void     (*sa_handler)(int);
    void     (*sa_sigaction)(int, siginfo_t *, void *);
    sigset_t   sa_mask;
    int        sa_flags;
    void     (*sa_restorer)(void);
};

测试代码如下

cpp 复制代码
void handler(int signo)
{
    lg(Info, "catch a signal, signal number: %d", signo);
}

int main()
{
    struct sigaction act, oact;
    memset(&act, 0, sizeof(act));
    memset(&oact, 0, sizeof(oact));

    act.sa_handler = handler;
    sigaction(2, &act, &oact);
    while(true)
    {
        lg(Info, "this is a process: %d", getpid());
        
        sleep(1);
    }

    return 0;
}

运行效果如下

要研究 pending 表中的信号什么时候由1->0,我们在 handler 中加上一个打印函数来验证,即

cpp 复制代码
void PrintPending(sigset_t &pending)
{
    cout << "pending: ";
    for (int signo = 1; signo <= 31; signo++)
    {
        if (sigismember(&pending, signo))
        {
            cout << "1";
        }
        else
        {
            cout << "0";
        }
    }
    cout << endl;
}

void handler(int signo)
{
    sigset_t set;
    sigpending(&set);

    PrintPending(set);
    lg(Info, "catch a signal, signal number: %d", signo);
}

int main()
{
    struct sigaction act, oact;
    memset(&act, 0, sizeof(act));
    memset(&oact, 0, sizeof(oact));

    act.sa_handler = handler;
    sigaction(2, &act, &oact);
    while(true)
    {
        lg(Info, "this is a process: %d", getpid());

        sleep(1);
    }

    return 0;
}

运行效果如下

从结果中,我们可以看到在打印 pending 表时,信号已经变为0,结论就是在执行捕捉方法时,是先将 pending 表清零,再调用 handler 方法。且在收到2号信号后,如果当前正在处于捕捉状态,2号信号的block表会设置成1,捕捉结束后变为0,这样可以防止出现各种嵌套调用,我们可以以下代码来测试

cpp 复制代码
void PrintPending(sigset_t &pending)
{
    cout << "pending: ";
    for (int signo = 1; signo <= 31; signo++)
    {
        if (sigismember(&pending, signo))
        {
            cout << "1";
        }
        else
        {
            cout << "0";
        }
    }
    cout << endl;
}

void handler(int signo)
{
    sigset_t set;
    sigpending(&set);

    lg(Info, "catch a signal, signal number: %d", signo);
    while (true)
    {
        PrintPending(set);
        sleep(1);
    }
}

int main()
{
    struct sigaction act, oact;
    memset(&act, 0, sizeof(act));
    memset(&oact, 0, sizeof(oact));

    act.sa_handler = handler;
    sigaction(2, &act, &oact);
    while(true)
    {
        lg(Info, "this is a process: %d", getpid());

        sleep(1);
    }

    return 0;
}

运行效果如下

可以看到,正在捕捉的时候,如果再传来相同的信号会被block表中的1阻塞。那在处理2号信号时,2号信号被自动屏蔽了,我们可不可以屏蔽更多信号呢?------可以设置 sigaction 结构体中的 sa_mask 参数即可!

④可重入函数与不可重入函数

假如有这样一个场景,我们想向链表中插入一个节点,即

如果现在 main 函数中调用了一次 insert (newnode1),如果在执行了①后,此时来了一条信号(这个信号的处理方法也是调用 insert(newnode2) ),执行后 head 节点指向 newnode2,newnode2 指向 node1,此时回到 main 函数的执行流,执行②此时就会将 head->next = newnode1,即

由于 insert 函数被 main 和 sighandler 执行流重复进入,导致了节点丢失与内存泄漏。

如果一个函数被重复进入的情况下出错了或者可能会出错,我们将其称为不可重入函数。反之,称为可重入函数。

目前,我们使用的大部分函数都是不可重入函数!

⑤从信号角度谈谈C++中的volatile

我们先来看下面这段代码

cpp 复制代码
int flag = 0;

void handler(int signo)
{
    lg(Info, "catch a signal, signo: %d", signo);
    flag = 1;
}

int main()
{
    signal(2, handler);
    while(!flag); // flag 0, !flag真

    lg(Info, "process quit success!");

    return 0;
}

g++ 可能会对这段代码进行优化,在优化条件下,flag 变量可能会被直接优化到CPU的寄存器中,比如我们使用 -O1进行优化,有如下情况

在未优化情况下,我们使用2号信号可以直接退出,其情况如下

使用优化后,我们不能退出,其情况如下

因为优化,导致我们在内存中不可见了!所以,我们可以在定义 flag 时加上 volatile 关键字,其核心作用就是防止编译器过度优化,保持内存的可见性!加上此关键字后我们再次运行有

⑥SIGCHLD

我们之前提到过,子进程在退出的时候并非是静悄悄地退出的。实际上子进程在退出的时候,会主动向父进程发送SIGCHLD(17号)信号,我们使用下面的代码验证

cpp 复制代码
void handler(int signo)
{
    lg(Info, "%d proccess catch a signal, signo: %d", getpid(), signo);
}

int main()
{
    signal(17, handler);

    pid_t id = fork();
    if(id == 0) // child
    {
        while(true)
        {
            lg(Info, "this is child process, pid: %d, ppid: %d", getpid(), getppid());
            sleep(1);
            break;
        }
        exit(0);
    }
    // father
    while(true)
    {
        lg(Info, "this is father process, pid: %d", getpid());
        sleep(1);
    }

    return 0;
}

运行效果如下

所以,父进程在进行等待的时候,我们可以采用基于信号的方式进行等待。进程等待有如下的好处

  1. 获取子进程的退出状态,释放子进程的僵尸

  2. 即使不知道父子进程谁先运行,但一定是 father 进程先退出

我们还需要让父进程调用wait/waitpid接口,即父进程必须保证自己是一直在运行的。既然如此,我们也可以试着将子进程的进程等待写入信号捕捉中,如下

cpp 复制代码
void handler(int signo)
{
    pid_t rid = waitpid(-1, nullptr, 0);
    lg(Info, "%d proccess catch a signal, signo: %d", getpid(), signo);
}

如过有多个子进程可以使用 while 循环捕捉,即

cpp 复制代码
void handler(int signo)
{
    pid_t rid;
    while(rid  = waitpid(-1, nullptr, WNOHANG) > 0)
    {
        lg(Info, "%d proccess catch a signal, signo: %d, %d child process quit succcess!", getpid(), signo, rid); 
    }
    
}

换一种说法,我们必须要进行进程等待吗?或者说必须调用 wait 接口吗?

也不是必须要这样做,我们可以使用 signal(17, SIG_IGN) 即可!

注:此方法对 Linux 有效,但是对类 Unix 不一定有效。

我们查看 signal 的手册

可以看到,OS 对17号信号默认执行的就是忽略操作,即17号信号的 SIG_DFL 的行为就是 SIG_IGN!

相关推荐
浪裡遊20 分钟前
Linux常用指令
linux·运维·服务器·chrome·功能测试
SugarPPig1 小时前
PowerShell 查询及刷新环境变量
服务器
段ヤシ.1 小时前
银河麒麟(内核CentOS8)安装rbenv、ruby2.6.5和rails5.2.6
linux·centos·银河麒麟·rbenv·ruby2.6.5·rails 5.2.6
深夜情感老师3 小时前
centos离线安装ssh
linux·centos·ssh
我的作业错错错3 小时前
搭建私人网站
服务器·阿里云·私人网站
王景程4 小时前
如何测试短信接口
java·服务器·前端
微网兔子4 小时前
伺服器用什么语言开发呢?做什么用什么?
服务器·c++·后端·游戏
夸克App5 小时前
实现营销投放全流程自动化 超级汇川推出信息流智能投放产品“AI智投“
运维·人工智能·自动化
Rainbond云原生5 小时前
83k Star!n8n 让 AI 驱动的工作流自动化触手可及
运维·人工智能·自动化
木觞清5 小时前
深度对比评测:n8n vs Coze(扣子) vs Dify - 自动化工作流工具全解析
运维·自动化