进程的信号

目录

信号(signal)入门

技术应用角度的信号

注意

用kill -l命令可以察看系统定义的信号列表

信号处理常见方式概览

产生信号

1.通过终端(键盘)按键产生信号

signal函数

2. 调用系统函数向进程发信号

kill 函数

raise 函数

3.由软件条件产生的信号

alarm 函数

4.硬件异常产生信号

核心转储文件(core dump)

ulimit指令

总结:

阻塞(保存)信号

1.信号其他相关常见概念

2. 在内核中的表示

3.sigset_t类型

4.信号集操作函数

1. sigemptyset:

2. sigfillset:

3. sigaddset:

4. sigdelset:

5. sigismember:

sigprocmask

sigpending

捕捉信号(信号的处理)

1.内核如何实现信号的捕捉

2.sigaction

可重入函数

volatile关键字

gcc/g++在进行编译时,是有一些优化级别的选项:

信号(signal)入门

在计算机系统中,信号是一种用于通知进程发生了某种事件的软件中断。它是一种进程间通信的方式,通常用于在异步事件发生时通知进程,例如用户输入、硬件错误、或者其他进程的状态变化。

信号的特点包括:

1.异步性: 信号的产生和处理是异步的,即信号可以在任何时间点发生,而进程必须随时准备好处理信号。

2.瞬时性: 信号是一种瞬时事件,通常是由硬件或其他进程生成,被发送到目标进程后立即执行相应的处理函数。

3.中断性: 信号是一种中断处理流程的机制,进程在接收到信号时会中断当前的执行,执行与该信号相关联的处理函数,然后继续执行原来的流程。

信号的产生:

信号可以由多种事件触发,其中包括:

4.硬件事件: 例如,除零错误、段错误等硬件异常可以触发相应的信号。

5.软件事件: 进程可以使用系统调用 kill 主动发送信号给其他进程,或者使用 raise 或 kill 自己产生信号。

6.用户操作: 例如,按下 Ctrl+C 键盘组合会发送一个 SIGINT 信号给前台进程。

7.其他进程的状态变化: 当子进程终止时,父进程会收到 SIGCHLD 信号。

在Linux系统中,可以使用 kill -l命令显示所有的信号或系统调用 kill 来向进程发送信号。每个信号都有一个唯一的编号,例如,SIGINT 的编号是2,SIGTERM 的编号是15。除了标准的信号,还有一些特殊的信号,如 SIGKILL 用于强制终止进程。

进程可以注册信号处理函数,用于在接收到信号时执行特定的操作。这可以通过 signal 函数或 sigaction 函数来完成。处理函数可以是系统提供的默认处理函数,也可以是用户自定义的函数。

总的来说,信号是一种重要的进程间通信机制,用于处理各种事件和异常情况,使得进程能够响应外部环境的变化。

技术应用角度的信号

比如用户输入命令,在Shell下启动一个前台进程。用户按下Ctrl-C ,这个键盘输入产生一个硬件中断,被OS获取,解释成信号,发送给目标前台进程前台进程因为收到信号,进而引起进程退出,如下:

[root@erciyuan Day11]# ll
总用量 20
-rw-r--r-- 1 root root   82 11月 28 01:15 Makefile
-rwxr-xr-x 1 root root 9184 11月 28 01:20 mysignal
-rw-r--r-- 1 root root  221 11月 28 01:20 mysignal.cc
[root@erciyuan Day11]#
[root@erciyuan Day11]# cat mysignal.cc 
#include <iostream>
#include<unistd.h>

using namespace std;

int main()
{
    while(true)
    {
        cout << "我是一个进程,正在运行...,pid: " << getpid() << endl;
        sleep(1);
    }

    return 0;
}
[root@erciyuan Day11]# ./mysignal 
我是一个进程,正在运行...,pid: 19927
我是一个进程,正在运行...,pid: 19927
我是一个进程,正在运行...,pid: 19927
我是一个进程,正在运行...,pid: 19927
^C
[root@erciyuan Day11]# 

硬件中断:

硬件中断是计算机体系结构中的一种机制,用于处理和响应外部设备发出的信号或事件。当外部设备需要与计算机进行通信或发出某种请求时,它会通过硬件中断发送一个信号给计算机的中央处理器(CPU)。

硬件中断可以是由各种外部设备触发的,例如键盘、鼠标、网络适配器、磁盘控制器等。当外部设备发生相关事件或需要处理时,它会发出一个硬件中断信号,这个信号会被CPU的中断控制器接收。

硬件中断的处理过程如下:

  1. 外部设备发出中断请求信号。
  2. CPU的中断控制器接收到中断请求信号,并将其转发给中央处理器。
  3. 中央处理器暂停当前正在执行的任务,保存当前的执行状态,并跳转到预定义的中断处理程序。
  4. 中断处理程序会执行特定的操作来响应中断请求,根据中断源的不同进行相应的处理。处理完后,中断处理程序会恢复之前保存的执行状态,并返回到中断发生的地方继续执行。

硬件中断的主要作用是允许外部设备与计算机进行异步通信,而不需要不断地轮询设备的状态。它使得计算机能够响应外部设备的事件,并及时进行处理,提高了系统的效率和响应性能。

在操作系统中,中断处理程序通常由设备驱动程序编写,用于处理特定设备发出的中断请求。操作系统负责管理和分发中断请求,将其分派给合适的中断处理程序进行处理。

进程是如何记录保存对应的信号:

进程该如何记录对应产生的信号?记录在哪里?先描述,在组织,怎么描述一个信号?用0和1来描述一个信号。用什么数据结构管理这个信号?通过位图来管理产生的信号。

task _struct内部必定要存在一个位图结构,用int表示:

uint32_t signals;

0000 0000 0000 0000 0000 0001 0000 0000 (比特位的位置,信号的编号,比特位的内容,是否收到该信号)

所谓的发送信号,本质其实写入信号,直接修改特定进程的信号位图中的特定的比特位,0->1

task_struct数据内核结构,只能由OS进行修改--无论后面我们有多少种信号产生的方式,最终都必须让OS来完成最后的发送过程!

信号产生之后,不是立即处理的。是在合适的时候进行处理。

注意

1. Ctrl-C 产生的信号只能发给前台进程。一个命令后面加个&可以放到后台运行,这样Shell不必等待进程结束就可以接受新的命令,启动新的进程。
2. Shell可以同时运行一个前台进程和任意多个后台进程,只有前台进程才能接到像 Ctrl-C 这种控制键产生的信号
3. 前台进程在运行过程中用户随时可能按下 Ctrl-C 而产生一个信号,也就是说该进程的用户空间代码执行到任何地方都有可能收到 SIGINT 信号而终止,所以信号相对于进程的控制流程来说是异步(Asynchronous)的。

前台进程(Foreground Process)是指在终端(或控制台)中正在直接运行的进程,前台进程在运行时我们无法输入指令。前台进程通常是用户当前正在交互的进程,接收用户的输入并将输出显示在终端上。与之相对的是后台进程(Background Process),后台进程在终端不接受用户输入,但仍然可以在系统中运行。

在Linux或类Unix系统中,可以使用一些命令和操作符来控制前台和后台进程:

  1. 启动前台进程:
    • 在终端中运行一个程序,该程序将成为前台进程。例如:
bash./my_program
  1. 启动后台进程:
    • 在命令末尾加上 & 符号可以将一个进程放到后台运行,使终端立即返回可输入状态,例如:
bash./my_program &
  1. 查看前台和后台进程:
    • 使用 jobs 命令可以列出当前终端中运行的所有作业(包括前台和后台),以及它们的状态。
  1. 将后台进程切换到前台:
    • 使用 fg 命令可以将一个后台进程切换到前台运行。例如,fg %1 将编号为1的后台进程切换到前台。
  1. 将前台进程放到后台:
    • 使用 Ctrl+Z 可以将当前正在前台运行的进程暂停,并将其放到后台。然后,可以使用 bg 命令将其继续在后台运行。
  1. 终止进程:
    • 使用 Ctrl+C 可以发送 SIGINT 信号,终止当前前台进程。使用 kill 命令可以发送其他信号,例如 kill -9 <PID> 可以强制终止一个进程。

前台进程的交互性使得它们适合用户直接操作,而后台进程则可以在不阻塞终端的情况下在后台执行任务。控制前台和后台进程的方法可以提供更灵活的进程管理。

用kill -l命令可以察看系统定义的信号列表

普通信号和实时信号是两种不同类型的信号,它们在处理机制和特性上有一些区别。下面是它们的主要区别:

  1. 实时信号的引入:
    • 普通信号: 普通信号是早期UNIX系统中引入的,其处理机制并没有特别强调对实时性的支持。
    • 实时信号: 实时信号是为了满足对实时性和精确性要求更高的应用而引入的。它们在POSIX标准中定义,并且相对于普通信号,提供了更可靠的信号传递机制。
  1. 排队特性:
    • 普通信号: 普通信号在接收端排队的能力有限,同一种类型的信号在排队时可能会被合并成一个。
    • 实时信号: 实时信号具有排队特性,即同一种类型的信号可以被排队,不会丢失。
  1. 信号编号范围:
    • 普通信号: 普通信号的编号范围通常比较有限,取值在1到31之间,且不包括0。
    • 实时信号: 实时信号的编号范围相对较大,可以是任意正整数,不受限于1到31的范围。
  1. 实时信号的优先级:
    • 普通信号: 普通信号没有定义优先级的概念,它们在信号队列中按照到达的顺序被处理。
    • 实时信号: 实时信号可以具有优先级,低编号的实时信号比高编号的实时信号具有更高的优先级。
  1. 实时信号的可靠性:
    • 普通信号: 普通信号在传递和处理过程中可能会出现一些不可靠的情况,例如丢失信号。
    • 实时信号: 实时信号提供了更可靠的信号传递机制,确保信号在传递和处理时的可靠性。

在使用信号时,选择使用普通信号还是实时信号通常取决于应用程序的实际需求。如果应用程序对信号的实时性和可靠性有较高的要求,那么使用实时信号可能更为适合。否则,普通信号可能足够满足一般的信号通知需求。

信号处理常见方式概览

可选的处理动作有以下三种:
1. 忽略此信号。
2. 执行该信号的默认处理动作。
3. 提供一个信号处理函数(signal、sigaction函数),要求内核在处理该信号时切换到用户态执行这个处理函数,这种方式称为捕捉(Catch)一个信号

产生信号

1.通过终端(键盘)按键产生信号

signal函数

在Linux中,signal 函数用于注册信号处理函数,以便在程序接收到指定信号时执行相应的操作(简单来说signal的作用就是捕捉发送的信号,并执行相应的自定义函数)。signal 函数的原型如下:

#include <signal.h>

typedef void (*sighandler_t) (int)//函数指针
sighandler_t signal(int signum, sighandler_t handler);

typedef void (*sighandler_t)(int); 这行代码定义了一个类型别名 sighandler_t,它是一个函数指针类型,指向一个函数,该函数接受一个整数参数(代表信号编号),返回 void。

然后,signal 函数的原型是 sighandler_t signal(int signum, sighandler_t handler);,这表示 signal 函数接受两个参数:

  1. signum:表示要处理的信号的编号。可以是预定义的信号常量(如 SIGINT 表示中断信号)或自定义的信号编号。
  2. handler:是一个函数指针,指向用户定义的信号处理函数。当程序接收到指定信号时,系统将调用这个函数执行相应的操作。如果 handler 的值是 SIG_IGN,表示忽略该信号;如果是 SIG_DFL,表示使用系统默认的处理方式。

函数返回之前与指定信号相关联的信号处理函数的值。如果发生错误,返回 SIG_ERR。

所以,typedef void (*sighandler_t)(int); 定义了一个函数指针类型,用于表示信号处理函数的类型,而 signal 函数则用于注册信号处理函数。

下面是一个简单的例子,演示了如何使用 signal 函数注册一个信号处理函数:

#include <stdio.h>
#include <signal.h>
#include <unistd.h>

void handler(int signo)
{
    cout << "get a singal: " << signo << endl;
}


int main() 
{
    // 注册 SIGINT 信号的处理函数为 sigint_handler
    if (signal(SIGINT, sigint_handler) == SIG_ERR) 
    {
        perror("Unable to register SIGINT handler");
        return 1;
    }

    // 进入一个无限循环
    while(true)
    {
        cout << "我是一个进程,正在运行...,pid: " << getpid() << endl;
        sleep(1);
    }

    return 0;
}

在这个例子中,程序在 main 函数中使用 signal 函数注册了 SIGINT 信号的处理函数为 sigint_handler。当用户按下Ctrl+C时,程序将收到 SIGINT 信号,然后调用 sigint_handler 函数来处理这个信号。

需要注意的是,signal 函数在一些平台上被认为是不可靠的,因为它对信号处理的具体实现可能有所不同。在现代的程序中,更推荐使用 sigaction 函数,因为它提供了更多的控制选项和可移植性。

#include <iostream>
#include <unistd.h>
#include <signal.h>


using namespace std;


//自定义方法
//signal作用:特定信号被发送给当前进程的时候,执行handler方法的时候,要自动填充对应的信号给handler方法
//我们甚至可以给所以信号设置同一个处理函数
void handler(int signo)
{
    cout << "get a singal: " << signo << endl;
    exit(2);
}

int main()
{
    signal(2, handler);//ctrl + 'c'
    signal(3, handler);//ctrl + '\'
    //signal(9, handler);// 9号 信号不可被捕捉,因为9号只会执行默认动作。
    
    while(true)
    {
        cout << "我是一个进程,正在运行...,pid: " << getpid() << endl;
        sleep(1);
    }

    return 0;
}

捕捉键盘发送的2号和3号 信号

这里只介绍1 - 31号 信号(普通信号),这些信号各自在什么条件下产生,默认的处理动作是什么,在signal(7)手册中都有详细说明: man 7 signal。

2. 调用系统函数向进程发信号

kill 函数

在Linux中,kill函数用于向指定的进程发送信号。具体语法如下:

#include <sys/types.h>
#include <signal.h>

int kill(pid_t pid, int sig);

该函数的参数解释如下:

  • pid:要发送信号的进程ID。可以使用进程ID(pid)或进程组ID(-pid)发送信号。特殊值0表示发送给当前进程所属的进程组,特殊值-1表示发送给所有具有权限的进程。其他特殊值如-2、-3和-4有特殊的含义,用于特定情况下的信号发送。
  • sig:要发送的信号编号,可以使用<signal.h>中定义的宏常量,例如SIGINT表示中断信号(也可以改为使用这些宏常量的编号)。

该函数的返回值为成功发送信号的数量,如果出错则返回-1,并设置errno变量来指示错误类型。

以下是kill函数的一些常见用法:

  1. 给指定进程发送信号:
kill(pid, SIGTERM);  // 发送SIGTERM信号给pid进程
  1. 发送终止信号给进程组:
kill(-pid, SIGKILL);  // 向进程组ID为pid的进程组发送SIGKILL信号
  1. 发送信号给当前进程所属的进程组:
kill(0, SIGINT);  // 向当前进程所属的进程组发送SIGINT信号

需要注意的是,只有具有足够权限的进程才能向其他进程发送信号。进程接收到信号后,可以通过注册信号处理函数来处理信号。

使用kill函数封装实现一个kill指令:

//mykill.cc
#include <iostream>
#include <cstdlib>
#include <cstring>
#include <unistd.h>
#include <cerrno>
#include <cassert>
#include <string>
#include <signal.h>
#include <sys/types.h>

using namespace std;

int count = 0;

void Usage(std::string proc)
{
    //指令用法提示
    cout << "\tUsage: \n\t";
    cout << proc << " 信号编号 目标进程\n"<< endl;
}

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

    int signo = atoi(argv[1]);
    int target_id = atoi(argv[2]);
    int n = kill(target_id, signo);
    if(n != 0)
    {
        cerr << errno << " : " << strerror(errno) << endl;
    }
    
    return 0;
}

raise 函数

在Linux中,raise 函数通常用于向当前进程发送信号(意思是谁调用我,我就给谁发送信号)。这个函数的声明如下:

#include <signal.h>

int raise(int sig);

这个函数的目的是向当前进程发送信号 sig。如果成功,返回0;否则,返回非零值。

使用 raise 函数,你可以在程序中发送信号,触发信号处理函数或默认的信号处理行为。例如,如果你想向当前进程发送信号,你可以这样做:

#include <stdio.h>
#include <stdlib.h>
#include <signal.h>

void myhandler(int signo)
{
    cout << "get a signal: " << signo << endl;
}

int main(int argc, char *argv[])
{
    signal(SIGINT, myhandler);
    
    while(true)
    {
        sleep(1);
        raise(2);//自动发送信号
    }

    return 0;
}

需要注意的是,使用信号处理函数时要谨慎,因为它们在异步环境中执行,可能会导致一些不可预测的行为。

在C和C++中也有一个类似raise系统函数的函数,abort 函数用于终止程序的运行,并生成一个程序终止信号。其声明如下:

#include <stdlib.h>

void abort(void);

调用 abort 函数会导致程序异常终止,同时产生一个 SIGABRT 信号(6号信号)。默认情况下,如果程序收到 SIGABRT 信号,会产生一个核心转储文件(core dump),该文件包含程序在崩溃时的内存映像,有助于调试。但是,你可以通过设置环境变量 COREDUMP_DISABLE 来禁用核心转储文件的生成。

注:abort发送的信号可以被捕捉,就算是被捕捉了当前进程也会退出。

3.由软件条件产生的信号

SIGPIPE是一种由软件条件产生的信号,在“管道”中已经介绍过了。在操作系统中,信号是用于在进程之间或由操作系统向进程发送通知的一种机制。信号可以由不同的条件产生,包括硬件条件和软件条件。

软件条件产生的信号是由软件或操作系统内部的事件或条件引发的。这些信号用于与进程通信,传递某些特定的事件或请求。

alarm 函数

在Linux系统中,alarm 函数用于设置一个定时器,以在指定的时间间隔后发送 SIGALRM 信号给正在运行的进程。这个函数的声明如下:

#include <unistd.h>

unsigned int alarm(unsigned int seconds);

alarm 函数接受一个正整数参数 seconds,也就是指定了定时器的时间间隔(单位为秒)。函数返回的是上一次设置的定时器剩余的时间,如果之前没有设置定时器,则返回0。

使用 alarm 函数可以在程序中创建一个简单的定时器。当指定的时间间隔过去后,进程将收到 SIGALRM 信号。可以通过注册 SIGALRM 的信号处理函数来处理该信号,并执行相应的操作。

注:把alarm的参数设置为0就是取消闹钟。

下面是一个简单的示例,演示了如何使用 alarm 函数创建一个定时器:

#include <stdio.h>
#include <unistd.h>
#include <signal.h>

void alarm_handler(int signum) {
    printf("Alarm received\n");
    // 执行需要在定时器触发时执行的操作
}

int main() 
{
    signal(SIGALRM, alarm_handler); // 注册 SIGALRM 的信号处理函数

    unsigned int seconds = 5;
    printf("Setting alarm for %u seconds\n", seconds);
    alarm(seconds); // 设置定时器

    // 其他的程序逻辑
    // ...

    while (1) 
    {
        // 进程的其他工作
        // ...
    }

    return 0;
}

在上面的例子中,alarm_handler 函数是注册给 SIGALRM 信号的处理函数。当定时器触发时,进程将收到 SIGALRM 信号,并调用该处理函数,在该函数中执行需要在定时器触发时执行的操作。

请注意,alarm 函数只能设置一个全局定时器,并且在调用 alarm 函数时,之前设置的定时器将被新的定时器替换。如果你需要多个定时器,可以考虑使用 timer_create 和 timer_settime 函数,它们提供了更灵活和精确的定时器功能。

实验例子1:

实验例子2:

4.硬件异常产生信号

硬件异常被硬件以某种方式被硬件检测到并通知内核,然后内核向当前进程发送适当的信号。例如当前进程执行了除以0的指令,CPU的运算单元会产生异常,内核将这个异常解释 (8号)为SIGFPE信号发送给进程。再比如当前进程访问了非法内存地址,,MMU会产生异常,内核将这个异常解释为(11号)SIGSEGV信号发送给进程。

以下是一些常见的硬件异常及其相关的软件信号:

  1. SIGSEGV(段错误):由于试图访问未分配的内存或对只读内存执行写操作等引起的内存访问错误。硬件检测到这种错误时,会发送SIGSEGV信号。
  2. SIGILL(非法指令):当进程执行了不合法或未定义的指令时产生。这可能是由于程序错误、二进制文件损坏等原因引起的。
  3. SIGFPE(浮点异常):由于进行了不合法的浮点运算(如除以零)而产生的信号。
  4. SIGBUS(总线错误):由于对计算机硬件总线上的地址执行了不合法的内存访问而产生。

这些信号是在进程运行时由硬件检测到的,表明了发生了某些严重的错误。当进程收到这些信号时,通常会执行相应的信号处理函数,以进行清理操作、记录错误信息或终止进程。

要注意的是,硬件异常通常表示程序中存在错误,因此在正常情况下应该避免它们的发生。合理的错误处理和调试手段是确保程序健壮性的关键。

MMU:

MMU 是内存管理单元(Memory Management Unit)的缩写。它是计算机系统中的一个硬件组件,负责执行虚拟内存到物理内存的地址映射,以及访问内存时的权限控制。

主要功能包括:

  1. 地址映射: 将程序中使用的虚拟地址映射到物理内存中的实际地址。这样,程序可以使用虚拟地址,而不需要知道实际物理地址。
  2. 内存保护: 控制对内存的访问权限,包括读、写、执行等。通过在页表中设置相应的权限位,MMU 可以确保程序只能访问它被授权的内存区域。
  3. 地址转换: 将虚拟地址转换为物理地址。当程序访问某个虚拟地址时,MMU 负责将其转换为实际的物理地址。
  4. 缓存控制: 管理虚拟内存与物理内存之间的数据缓存,以提高访问速度。
  5. TLB(Translation Lookaside Buffer): 一种用于加速地址转换的高速缓存,存储了最近使用的虚拟地址到物理地址的映射。这有助于避免每次地址访问都要完全查询页表的开销。

MMU 的引入使得操作系统能够实现虚拟内存的概念,从而提供了一种抽象层,使得程序可以使用比实际物理内存更大的虚拟地址空间。这对于多任务处理、内存保护和地址空间隔离等方面都有很大的好处。

总的来说,MMU 在计算机体系结构中发挥着至关重要的作用,为操作系统提供了有效管理内存的手段,同时提高了系统的可靠性和安全性。

核心转储文件(core dump)

ulimit指令

ulimit 是一个用于设置或显示用户级资源限制的命令。这个命令通常在命令行终端中使用,它允许用户限制特定的资源,以防止单个用户或进程占用过多的系统资源。

以下是一些常见的用法和选项:

  1. ulimit -a 或 ulimit -all:显示所有的资源限制。这将列出当前 shell 的所有资源限制,包括软限制和硬限制。
bashulimit -a
  1. ulimit -c [限制]:设置或显示核心转储文件的大小限制。如果没有给定限制,它将显示当前限制。
bashulimit -c unlimited
  1. ulimit -n [限制]:设置或显示文件描述符的数量限制。
bashulimit -n 1024
  1. ulimit -u [限制]:设置或显示用户进程数限制。
bashulimit -u 500
  1. ulimit -q [限制]:设置或显示队列的大小限制。
bashulimit -q 1000
  1. ulimit -f [限制]:设置或显示文件的大小限制。
bashulimit -f unlimited
  1. ulimit -l [限制]:设置或显示锁定内存的大小限制。
bashulimit -l 64
  1. ulimit -s [限制]:设置或显示堆栈的大小限制。
bashulimit -s 8192
  1. ulimit -v [限制]:设置或显示虚拟内存的大小限制。
bashulimit -v 1048576

这些是 ulimit 命令的一些常见用法。请注意,ulimit 命令设置的资源限制通常只对当前的 shell 会话有效,并且这些限制可能会被子进程继承。如果你希望更改全局系统范围内的资源限制,通常需要在系统启动时或者使用特定配置文件中进行设置。

总结:

1.上面所说的所有信号产生,最终都要有OS来进行执行,为什么?

因为OS是进程的管理者,只有OS有权利修改进程PCB当中的数据。
2.信号的处理是否是立即处理的?

在合适的时候进行处理。
3.信号如果不是被立即处理,那么信号是否需要暂时被进程记录下来?记录在哪里最合适呢?

是的,记录在PCB当中。

4.一个进程在没有收到信号的时候,能否能知道,自己应该对合法信号作何处理呢?

知道,因为他已经被默认设置进了编码进程的处理逻辑当中。
5.如何理解OS向进程发送信号?能否描述一下完整的发送处理过程?

不管是用户通过键盘,系统调用、还是软件条件,或者硬件异常,无论什么方式操作系统都一定能识别到,识别到之后,向目标进程写信号,这就是发信号的过程。

阻塞(保存)信号

1.信号其他相关常见概念

实际执行信号的处理动作称为信号递达(Delivery)。
信号从产生到递达之间的状态,称为信号未决(Pending)。
进程可以选择阻塞 (Block )某个信号。
被阻塞的信号产生时将保持在未决状态,直到进程解除对此信号的阻塞,才执行递达的动作
注意,阻塞和忽略是不同的,只要信号被阻塞就不会递达,而忽略是在递达之后可选的一种处理动作。

2. 在内核中的表示

信号在内核中的表示示意图:

1、每个信号都有两个标志位分别表示阻塞(block:也称为,信号屏蔽集)和未决(pending:也称为pending信号集),还有一个函数指针表示处理动作。信号产生时,内核在进程控制块中设置该信号的未决标志,直到信号递达才清除该标志。在上图的例子中,SIGHUP信号未阻塞也未产生过,当它递达时执行默认处理动作。
2、SIGINT信号产生过,但正在被阻塞,所以暂时不能递达。虽然它的处理动作是忽略,但在没有解除阻塞之前不能忽略这个信号,因为进程仍有机会改变处理动作之后再解除阻塞。
3、SIGQUIT信号未产生过,一旦产生SIGQUIT信号将被阻塞,它的处理动作是用户自定义函数sighandler。如果在进程解除对某信号的阻塞之前这种信号产生过多次,将如何处理?POSIX.1允许系统递送该信号一次或多次。Linux是这样实现的:常规信号在递达之前产生多次只计一次,而实时信号在递达之前产生多次可以依次放在一个队列里。本章不讨论实时信号

3.sigset_t类型

从上图来看,每个信号只有一个bit的未决标志,非0即1,不记录该信号产生了多少次,阻塞标志也是这样表示的。因此,未决和阻塞标志可以用相同的数据类型sigset_t(sigset_t类型是一个位图结构)来存储,sigset_t称为信号集,这个类型可以表示每个信号的“有效”或“无效”状态,在阻塞信号集中“有效”和“无效”的含义是该信号是否被阻塞而在未决信号集中“有效”和“无效”的含义是该信号是否处于未决状态。我们将详细介绍信号集的各种操作。 阻塞信号集也叫做当前进程的信号屏蔽字(Signal Mask),这里的“屏蔽”应该理解为阻塞而不是忽略。(直白点就是sigset_t类型里面包含了两张表,分别是block表和pending表)

4.信号集操作函数

sigset_t类型对于每种信号用一个bit表示“有效”或“无效”状态,至于这个类型内部如何存储这些bit则依赖于系统实现,从使用者的角度是不必关心的,使用者只能调用以下函数来操作sigset_ t类型变量,而不应该对它的内部数据做任何解释,比如用printf直接打印sigset_t变量是没有意义的。

以下是与 sigset_t 相关的一些常见函数:

1. sigemptyset:
    • 函数原型:int sigemptyset(sigset_t *set);
    • 功能:清空信号集合,将所有信号从集合中移除。
    • 示例:
sigset_t my_set;
sigemptyset(&my_set);
2. sigfillset:
    • 函数原型:int sigfillset(sigset_t *set);
    • 功能:将所有信号添加到信号集合中,即将信号集合设置为包含所有信号。
    • 示例:
sigset_t my_set;
sigfillset(&my_set);
3. sigaddset:
    • 函数原型:int sigaddset(sigset_t *set, int signum);
    • 功能:将指定的信号添加到信号集合中。
    • 示例:
sigset_t my_set;
sigemptyset(&my_set);
sigaddset(&my_set, SIGINT);
4. sigdelset:
    • 函数原型:int sigdelset(sigset_t *set, int signum);
    • 功能:从信号集合中删除指定的信号。
    • 示例:
sigset_t my_set;
sigfillset(&my_set);
sigdelset(&my_set, SIGTERM);
5. sigismember:
    • 函数原型:int sigismember(const sigset_t *set, int signum);
    • 功能:检查指定的信号是否包含在信号集合中。
    • 示例:
sigset_t my_set;
sigfillset(&my_set);
if (sigismember(&my_set, SIGUSR1)) {
    // SIGUSR1 在信号集合中
}

这些函数通常用于在信号处理中设置和管理信号集合。例如,在使用 sigprocmask 函数时,你可能会使用 sigset_t 来指定哪些信号需要被阻塞。这些函数提供了对信号集合进行操作的便利方式。

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

sigprocmask

在Linux中,sigprocmask 函数用于检查或修改进程的信号屏蔽集(signal mask)。信号屏蔽集(信号屏蔽集指的是block表)是一个集合,用于指定哪些信号在调用时应该被阻塞,即不被传递给进程。

以下是 sigprocmask 函数的基本信息:

#include <signal.h>

int sigprocmask(int how, const sigset_t *set, sigset_t *oldset);
//返回值:若成功则为0,若出错则为-1
  • how:表示要执行的操作,可以是以下值之一:
    • SIG_BLOCK(添加):将指定的信号集合添加到当前信号屏蔽集中,阻塞这些信号。
    • SIG_UNBLOCK(删除):从当前信号屏蔽集中移除指定的信号集合,解除对这些信号的阻塞。
    • SIG_SETMASK(覆盖):更改当前进程信号屏蔽集,将当前参数信号屏蔽集(set)设置为指定的信号集合(how)。
  • set:对应于 how 操作的信号集合。
  • oldset:如果不为 NULL,则在函数调用结束时,存储之前的信号屏蔽集。

如果oset是非空指针,则读取进程的当前信号屏蔽字通过oset参数传出。如果set是非空指针,则 更改进程的信号屏蔽字,参数how指示如何更改。如果oset和set都是非空指针,则先将原来进程的信号 屏蔽字备份到oset里,然后根据set和how参数更改信号屏蔽字。假设当前的信号屏蔽字为mask,下表说明了how参数的可选值

下面是一些示例用法:

using namespace std;

void showBlock(sigset_t *oset)
{
    int signo = 1;//从1开始,因为没有0号信号
    for(; signo <= 31; signo++)//遍历所有的比特位
    {
        if(sigismember(oset, signo))
            cout << "1";
        else
            cout << "0";

    }
    cout << endl;
}

int main()
{
    //1.只是在用户层面上进行设置。设置的什么?设置的信号!
    //说直白点sigaddset(&set, 2);只是把信号设置到了set变量里
    //通过第2步,调用sigprocmask(SIG_SETMASK, &set, &oset);才是设置到进程里
    sigset_t set, oset;
    sigemptyset(&set);//清空(初始化)
    sigemptyset(&oset);
    sigaddset(&set, 2);//SIGIN

    //2.设置进入进程,谁调用,设置给谁
    int cnt = 0;
    sigprocmask(SIG_SETMASK, &set, &oset);
    while(true)
    {
        showBlock(&oset);//输出旧的信号集的所有信号
        sleep(1);
        cnt++;

        if(cnt == 10)
        {
            cout << "recover block" << endl;
            sigprocmask(SIG_SETMASK, &oset, &set);//恢复旧的信号集
            showBlock(&set);//输出旧的信号集的所有信号
            showBlock(&oset);//输出旧的信号集的所有信号
            sleep(10);
        }
    }
    
	return 0;
}

这个例子演示了如何使用 sigprocmask 函数来设置和修改信号屏蔽集。

sigpending

sigpending 函数用于获取当前进程被阻塞但是已经产生的待处理信号集。这个函数允许程序查询在信号阻塞状态下已经产生但尚未处理的信号。以下是 sigpending 函数的基本信息:

#include <signal.h>

int sigpending(sigset_t *set);
  • set:用于存储待处理信号的信号集。函数成功调用后,set 将被设置为包含了当前被阻塞的、但已经产生的信号。

函数返回值:

  • 如果成功,返回0。
  • 如果失败,返回-1,并设置 errno 表示错误的原因。

下面是一个简单的示例,演示如何使用 sigpending 函数:

#include <iostream>
#include <signal.h>
#include <assert.h>
#include <unistd.h>


using namespace std;


void handler(int signo)
{
    cout << "对特定信号:" << signo << "执行捕捉" << endl;
}

void PrintPemding(const sigset_t &pending)
{
    cout << "当前进程的pending位图:";
    for(int signo = 1; signo <= 31; signo++)
    {
        if(sigismember(&pending, signo))
            cout << "1";
        else
            cout << "0";
    }
    cout << endl;
}

int main()
{
    //2.0 设置对2号信号的自定义捕捉,以防止解除对2号信号的屏蔽之后退出进程
    signal(2, handler);
    
    sigset_t set, oset;
    //1.1 初始化
    sigemptyset(&set);
    sigemptyset(&oset);
    //1.2 将2号信号添加到set中
    sigaddset(&set, 2);
    //1.3 将新的信号屏蔽字设置到进程
    sigprocmask(SIG_BLOCK, &set, &oset);
    
    
    int cnt = 0;
    while(true)
    {
        //2.1 先获取pending信号集
        sigset_t pending;//用来存储被阻塞的信号
        sigemptyset(&pending);
        int n = sigpending(&pending);//获取被阻塞的信号
        assert(n == 0);
        (void)n;

        //2.2 打印,方便进行查看
        PrintPemding(pending);
        //2.3 休眠时间
        sleep(1);

        //2.4 10s之后,恢复所以信号的屏蔽(block)动作
        if(cnt++ == 10)
        {
            cout << "解除对2号信号的屏蔽" << endl;
            sigprocmask(SIG_SETMASK, &oset, nullptr);
        }
    }
    
    return 0;
}

捕捉信号(信号的处理)

我们之前说过,信号的处理(信号的递达),可以不是立即执行,而是"合适"的时候,那么这个"合适"指的又是什么时候?

信号可以立即被处理吗?如果一个信号之前被block了,当他解除block的时候,对应的信号会被立即递达!因为信号的产生是异步的,当前进程可能正在做更重要的事情!

什么时候是"合适"的时候?当进程从内核态 切换回 用户态的时候,进程会在OS的指导下,进行信号的检测与处理!

用户态:执行你写的代码的时候,进程所处的状态。

内核态:执行OS的代码的时候,进程所处的状态

所以什么时候从用户态进入内核态呢?

1.进程时间片到了,需要切换,就要执行进程切换逻辑。2.系统调用

1.内核如何实现信号的捕捉

如果信号的处理动作是用户自定义函数,在信号递达时就调用这个函数,这称为捕捉信号。由于信号处理函数的代码是在用户空间的,处理过程比较复杂,举例如下: 用户程序注册(设置捕捉)了SIGQUIT信号的处理函数sighandler。 当前正在执行main函数,这时发生中断或异常切换到内核态。 在中断处理完毕后要返回用户态的main函数之前检查到有信号SIGQUIT递达。 内核决定返回用户态后不是恢复main函数的上下文继续执行,而是执行sighandler函 数,sighandler和main函数使用不同的堆栈空间,它们之间不存在调用和被调用的关系,是 两个独立的控制流程。 sighandler函数返回后自动执行特殊的系统调用sigreturn再次进入内核态。 如果没有新的信号要递达,这次再返回用户态就是恢复main函数的上下文继续执行了。

2.sigaction

在Linux中,sigaction 函数用于设置对信号的处理方式,调用成功则返回0,出错则返回- 1 。以下是 sigaction 函数的一般形式:

#include <signal.h>

int sigaction(int signum, const struct sigaction *act, struct sigaction *oldact);
//sigaction函数和signal函数类似,都是用来捕捉当前进程发送的信号
  • signum:表示要处理(捕捉)的信号的编号,比如 SIGINT 代表中断信号。
  • act:是一个指向 struct sigaction 结构体类型的指针,该结构包含了对信号的新的处理方式(处理函数、标志等)。
  • oldact:是一个指向 struct sigaction 结构的指针,如果不为 NULL,则 oldact 将用于存储之前对信号的处理方式。

struct sigaction 这个结构类型定义在 signal.h 头文件中。以下是 struct sigaction 结构的简化原型:

cstruct sigaction {
    void (*sa_handler)(int); // 处理函数的地址,或者是 SIG_IGN、SIG_DFL
    void (*sa_sigaction)(int, siginfo_t *, void *); // 用于三参数信号的处理函数的地址
    sigset_t sa_mask; // 指定在信号处理期间需要被屏蔽的信号集
    int sa_flags; // 指定信号处理的一些标志
    void (*sa_restorer)(void); // 恢复函数的地址
};
  • sa_handler:用于设置信号处理函数的地址,或者可以指定为 SIG_IGN(忽略信号)或 SIG_DFL(使用默认处理方式)。
  • sa_sigaction:用于设置三参数信号的处理函数的地址。如果 sa_handler 被使用,这个字段将被忽略。
  • sa_mask:指定在信号处理期间需要被屏蔽的信号集。
  • sa_flags:指定信号处理的一些标志,例如 SA_RESTART 表示在系统调用中自动重启被信号中断的系统调用。
  • sa_restorer:用于设置恢复函数的地址。在一些旧的系统中可能使用,一般置为 NULL。

在使用 sigaction 函数时,你可以通过设置 act 参数为指向一个 struct sigaction 结构的指针,从而定义对特定信号的处理方式。

注:

当某个信号的处理函数被调用时,内核自动将当前信号加入进程的信号屏蔽字(说白了就是操作系统正在执行某一个信号的处理函数时,哪怕是这个信号曾经没有被设置为block状态(信号屏蔽字),即block表对应的比特位由0置为1,操作系统也会自动将这个信号设置为block状态。简单来说就是,后来的信号要排队,直到当前信号的处理函数被执行完,才会轮到下一个信号),当信号处理完函数返回时也会自动恢复原来的信号屏蔽字(即block表对应的比特位由1置为0),这样就保证了在处理某个信号时,如果这种信号再次产生,那么它会被阻塞到当前处理结束为止。 如果在调用信号处理函数时,除了当前信号被自动屏蔽之外,还希望自动屏蔽另外一些信号(比如说再屏蔽3号和4号信号),则用sa_mask字段说明这些需要额外屏蔽的信号(也就是说把3号和4号添加到sa_mask里),当信号处理函数返回时自动恢复原来的信号屏蔽字。 sa_flags字段包含一些选项,本章的代码都把sa_flags设为0,sa_sigaction是实时信号的处理函数,本章不详细解释这两个字段,有兴趣的可以自己在了解一下。

下面是一个示例,演示如何使用 sigaction 函数:

#include <iostream>
#include <signal.h>
#include <assert.h>
#include <unistd.h>
#include <string.h>

using namespace std;


void PrintPemding(const 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)
{
    cout << "对特定信号:" << signo << "执行捕捉" << endl;
    int cnt = 10;
    while(cnt)
    {
        cnt--;

        sigset_t pending;
        sigemptyset(&pending);
        sigpending(&pending);//获取被阻塞的信号
        PrintPemding(pending);//输出pending表的位图
        sleep(1);
    }
}

int main()
{
    struct sigaction act, oldact;
    memset(&act, 0, sizeof(act));
    memset(&act, 0, sizeof(oldact));
    act.sa_handler = handler;
    act.sa_flags = 0;
    sigemptyset(&act.sa_mask);

    sigaddset(&act.sa_mask, 3);

    sigaction(2, &act, &oldact);

    while(true)
    {
        cout << getpid() << endl;
        sleep(1);
    }

    return 0;
}

可重入函数

可重入函数(reentrant function)是指一个函数在被多个任务(线程)同时调用时,能够正确地处理多个调用而不会出现冲突或错误。这通常需要确保函数内部使用的数据是线程安全的,不依赖于全局状态,而是依赖于函数的参数和局部变量。

main函数调用insert函数向一个链表head中插入节点node1,插入操作分为两步,刚做完第一步的 时候,因为硬件中断(时间片到了)使进程切换到内核,再次回用户态之前检查到有信号待处理,于是切换 到sighandler函
数,sighandler也调用insert函数向同一个链表head中插入节点node2,插入操作的 两步都做完之后从
sighandler返回内核态,再次回到用户态就从main函数调用的insert函数中继续 往下执行,先前做第一步
之后被打断,现在继续做完第二步。结果是,main函数和sighandler先后 向链表中插入两个节点,而最后只有一个节点真正插入链表中了。
像上例这样,insert函数被不同的控制流程调用,有可能在第一次调用还没返回时就再次进入该函数,这称
为重入,insert函数访问一个全局链表,有可能因为重入而造成错乱,像这样的函数称为 不可重入函数,反之,
如果一个函数只访问自己的局部变量或参数,则称为可重入(Reentrant) 函数。想一下,为什么两个不同的
控制流程调用同一个函数,访问它的同一个局部变量或参数就不会造成错乱?

以下是一些确保函数可重入性的常见做法:

  1. 使用本地变量: 避免使用全局变量,因为全局变量是共享的,可能导致不同线程之间的竞态条件。使用函数的参数和局部变量,这样每个线程都有自己的副本。
  2. 避免使用静态变量: 静态变量在多线程环境中可能引发问题。如果确实需要使用静态变量,要确保它们在函数内部是线程私有的,可以通过关键字 static 和函数作用域来实现。
  3. 避免使用不可重入的库函数: 有些库函数是不可重入的,因为它们使用了全局变量或其他共享资源。在多线程环境中,应该选择可重入的库函数或者使用线程安全的版本。
  4. 使用互斥锁: 在必要的情况下,可以使用互斥锁来保护共享资源,确保同一时刻只有一个线程能够访问这些资源。
  5. 注意信号处理: 在信号处理函数中,要谨慎使用那些不是异步信号安全(async-signal-safe)的函数,因为信号处理是在中断上下文中执行的,可能会中断正在执行的函数。
  6. 避免递归调用: 在一些情况下,递归调用可能导致函数不可重入。确保函数能够正确处理递归调用,或者避免使用递归。

可重入函数的设计考虑到了并发执行的需求,因此在多线程环境中更为安全。在使用现代编程语言和库时,通常会提供一些线程安全的工具和函数,但程序员仍然需要注意函数的可重入性。

volatile关键字

volatile 是一个在C和C++中使用的关键字,它主要用于告诉编译器不要对被声明为 volatile 的变量进行优化,因为这些变量的值可以在程序的执行流中被意外地改变。

主要作用:

  1. 禁止编译器优化: 当一个变量被声明为 volatile 时,编译器会避免对该变量的操作进行优化。这是因为该变量的值可以被意外地改变,例如在中断服务例程中。
  2. 告知编译器不要缓存: 对于一些对硬件寄存器进行读写的情况,使用 volatile 可以告诉编译器不要将这些寄存器的值缓存在寄存器中,而是要每次都从内存中读取。这是因为这些寄存器的值可能会被硬件或者其他并发的代码改变。

示例:

cvolatile int flag = 0;  // 定义一个 volatile 变量

void interruptServiceRoutine() {
    // 在中断服务例程中改变 flag 的值
    flag = 1;
}

int main() {
    while (flag == 0) {
        // 在循环中检查 flag 的值
        // 由于 flag 是 volatile,编译器不会进行优化,确保每次都从内存中读取 flag 的值
    }

    // 执行其他操作...

    return 0;
}

注意事项:

  1. 不解决并发问题:volatile 并不能解决并发访问的问题。它仅仅告诉编译器不要对这个变量进行某些优化,但并不提供同步机制。在多线程环境下,仍需要使用互斥锁等机制来确保对变量的原子操作。
  2. 适用于特定场景:volatile 通常用于与硬件相关的编程,比如在嵌入式系统中对寄存器的访问。
  3. 不同编译器的实现可能有差异: 标准中对 volatile 的语义定义相对宽泛,因此不同编译器可能有不同的实现方式,特别是在多线程环境下。在需要跨平台的代码中,需要注意这一点。

总的来说,volatile 是一种告知编译器的工具,用于处理一些特定的、容易被优化掉的场景,以确保程序的行为符合预期。

gcc/g++在进行编译时,是有一些优化级别的选项:

这些选项是用来设置编译器的优化级别的,通常用于控制生成可执行程序时的优化程度。这些选项的含义可能略有不同,具体取决于所使用的编译器,但一般来说,它们包含以下几个级别:

  1. -O0: 不进行优化。编译器将生成易于调试的代码,包括完整的调试信息,以便于在调试器中进行源代码级别的调试。这会导致生成的可执行文件较大,执行速度较慢,但对于调试目的非常有用。
  2. -O1: 低级别的优化。编译器会执行一些基本的优化,如删除不可达代码和一些局部优化,但不会进行过多的优化,以确保编译速度较快。
  3. -O2: 中级别的优化。在-O1的基础上,编译器会执行更多的优化,包括一些可能会增加编译时间的优化。这通常会产生更高效的代码,但也可能增加生成可执行文件的时间。
  4. -O3: 高级别的优化。这一级别会进行更多、更激进的优化,包括一些可能会导致编译时间显著增加的优化。生成的代码可能更加紧凑和高效,但这也可能导致一些编译器可能无法处理的问题,或者增加代码的复杂性。
  5. -Os: 以尽可能减小目标文件的大小为目标进行优化。这个选项更注重代码大小而非执行速度,适用于一些嵌入式系统或者需要优化可执行文件大小的场景。
  6. -Ofast: 启用除了标准不允许的一些优化,例如允许忽略 IEEE 浮点数规范,可能会导致数学计算结果的不确定性。这个选项通常用于对执行速度要求非常高、而对精确性要求相对较低的场景。
  7. -Og: 优化以保留调试信息的方式。这个选项在-O1级别的基础上进行优化,但同时保留了对调试的支持,用于在开发阶段进行调试。
  8. -On: 一些编译器可能提供其他的优化级别,如 -O4、-O5 等,具体含义取决于编译器的实现。

选择优化级别通常是一个权衡,需要考虑编译时间和生成代码的效率。在开发和调试阶段,通常会选择较低的优化级别以获得更好的调试支持和更快的编译速度。在最终发布版本时,可以选择较高的优化级别以获得更好的执行性能。

请注意,具体的优化选项和级别可能因编译器而异,建议查阅编译器的文档以获取详细信息。在实际应用中,选择适当的优化级别需要根据具体情况进行权衡,考虑编译时间、可执行文件大小和执行性能。

SIGCHLD(17号信号)

进程一章讲过用wait和waitpid函数清理僵尸进程,父进程可以阻塞等待子进程结束,也可以非阻 塞地查询是否有子进程结束等待清理(也就是轮询的方式)。采用第一种方式,父进程阻塞了就不 能处理自己的工作了;采用第二种方式,父进程在处理自己的工作的同时还要记得时不时地轮询一 下,程序实现复杂。

其实,子进程在终止时会给父进程发SIGCHLD信号,该信号的默认处理动作是忽略,父进程可以自 定义SIGCHLD信号的处理函数,这样父进程只需专心处理自己的工作,不必关心子进程了,子进程 终止时会通知父进程,父进程在信号处理函数中调用wait清理子进程即可。

编写一个程序完成以下功能:父进程fork出子进程,子进程调用exit(2)终止,父进程自定义SIGCHLD信号的处理函数,在其中调用wait获得子进程的退出状态并打印。

方法一:

#include <iostream>
#include <signal.h>
#include <sys/types.h>
#include <unistd.h>
#include <stdlib.h>
#include<sys/wait.h>


using namespace std;

pid_t id;//因为信号和当前的main是两个执行流,所以定义为全局的。

void handler(int signo)
{
    cout << "捕捉到一个信号:" << signo << ", who:" << getpid() << endl;
    sleep(3);//等待3秒,期间可以查看子进程是否处于僵尸状态
    while(1)
    {
        //0:阻塞式等待,但是我们这里绝对不会阻塞!为什么呢?因为我已经收到了信号,所以当前子进程
        //肯定要退出了,所以wait只要调用就会立马回收子进程并返回
        //-1:等待任意一个子进程退出,只要有死掉的子进程就会一直回收
        pid_t res = waitpid(-1, NULL, WNOHANG);//返回成功res就是等待的子进程的pid
        if(res > 0)
        {
            printf("wait success, res: %d, id: %d\n", res, id);
        }
        else
            break;
    }
    
    cout << "handler done..." << endl;
}

int main()
{
    signal(SIGCHLD, handler);
    
    //如果一次性创建多个子进程呢?
    int i = 1;
    for(; i <= 10; i++)
    {
        id = fork();
        if(id == 0)
        {
            //child
            int cnt = 5;
            while(cnt)
            {
                cout << "我是子进程,我的pid:" << getpid() << ", ppid:" << getppid() << endl;
                sleep(1);
                cnt--;
            }

            exit(1);
        }
    }

    //如果你的父进程没有事干,你还是用以前的方法
    //如果你的父进程很忙,而且不退出,可以选择信号的方法
    while(1)
    {
        sleep(1);
    }

    return 0;
}

事实上,由于UNIX 的历史原因,要想不产生僵尸进程还有另外一种办法父进程调 用 signal/sigaction 将SIGCHLD的处理动作置为SIG_IGN (SIG_IGN,表示忽略该信号),这样fork出来的子进程在终止时会自动清理掉,不 会产生僵尸进程,也不会通知父进程。系统默认的忽略动作和用户用sigaction函数自定义的忽略 通常是没有区别的,但这是一个特例。此方法对于Linux可用,但不保证在其它UNIX系统上都可 用。请编写程序验证这样做不会产生僵尸进程

方法二:

nt main()
{
    signal(SIGCHLD, SIG_IGN);//将SIGCHLD的参数设置为SIG_IGN即可自动回收子进程
    
    //如果一次性创建多个子进程呢?
    int i = 1;
    for(; i <= 10; i++)
    {
        id = fork();
        if(id == 0)
        {
            //child
            int cnt = 5;
            while(cnt)
            {
                cout << "我是子进程,我的pid:" << getpid() << ", ppid:" << getppid() << endl;
                sleep(1);
                cnt--;
            }

            exit(1);
        }
    }

    //如果你的父进程没有事干,你还是用以前的方法
    //如果你的父进程很忙,而且不退出,可以选择信号的方法
    while(1)
    {
        sleep(1);
    }

    return 0;
}

因为子进程在终止时会给父进程发SIGCHLD信号,该信号的默认处理动作是忽略,所以父进程调 用 signal/sigaction 将它们参数的SIGCHLD的处理动作置为SIG_IGN (SIG_IGN,表示忽略该信号),这样fork出来的子进程在终止时会自动清理掉,不会产生僵尸进程,也不会通知父进程。此方法对于Linux可用,但不保证在其它UNIX系统上都可 用。

  • 19
    点赞
  • 30
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值