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!

相关推荐
致奋斗的我们8 分钟前
RHCE的学习(7)
linux·服务器·网络·学习·redhat·rhce·rhcsa
petaexpress11 分钟前
云存储的费用是多少?2024年最新价格表
运维·服务器·云存储·云存储的费用是多少·云存储的费用是多少最新·云存储费用
昨天今天明天好多天1 小时前
【Linux】ClickHouse 部署
linux·服务器·clickhouse
taolichao301 小时前
架设一台NFS服务器,按照要求配置
linux·运维·服务器
程序员yt1 小时前
2025秋招八股文--服务器篇
linux·运维·服务器·c++·后端·面试
2301_810730101 小时前
RHCSA基础命令整理1
linux·运维·服务器
稳重的大王1 小时前
如何使用 NetBak PC Agent 备份和恢复 Windows 计算机或服务器?
运维·服务器
纪伊路上盛名在1 小时前
服务器与服务器之间文件上传下载
运维·服务器
小何学计算机1 小时前
Nginx 配置基于主机名的 Web 服务器
服务器·前端·nginx
Chris-zz1 小时前
Linux:磁盘深潜:探索文件系统、连接之道与库的奥秘
linux·网络·c++·1024程序员节