Linux快速入门之 信号(13)

信号

文章目录

    • 信号
      • 1.1 信号概述
        • 1.1.1 信号编号
        • 1.1.2 查看信号信息
        • 1.1.3 信号的状态
      • 1.2 信号相关函数
        • 1.2.1 kill/raise/abort 函数
        • 1.2.2 定时器
      • 1.3 信号集
        • 1.3.1 阻塞 /未决信号集
        • 5.3.2 信号集函数
      • 1.4 信号捕捉
        • 1.4.1 signal 函数
        • 1.4.2 sigaction 函数
      • 1.5 SIGCHLD 信号

1.1 信号概述

Linux中的信号是一种信号机制,其本质是一个整数,不同的信号对应不同的值,由于信号的结构简单所以天生不能携带很多信息量;但是信号在系统中的优先级是非常高的;

在Linux系统中很多常规操作都会有相关信号产生,例如:

通过键盘操作产生信号:用户按下Ctril + C ,键盘输入产生了一个硬件中断,使用这个快捷键会产生信号,该信号会杀死对应进程;

通过shell命令产生信号 : 通过kill命名终止某一个进程 ,kill -9 进程PID;

通过函数调用产生信号: 如果当前CPU正在执行这个进程的代码调用,例如sleep()函数,进程收到相关的信号,被迫挂起;

通过对硬件的非法访问产生信号: 正在运行的程序访问了非法内存,发送段错误,进程退出;

信号也可以实现进程间通信,但是传递的信息量很少,同时由于信号的优先级很高,其对应的处理动作是回调完成的,它会打乱程序原有的处理流程影响到最终的处理结果。因此不建议使用信号进行进程间通信;

1.1.1 信号编号

通过kill -l 命令可以查看系统定义的信号列表

liu@liu-Ubuntu:~$ kill -l
 1) SIGHUP	 2) SIGINT	 3) SIGQUIT	 4) SIGILL	 5) SIGTRAP
 6) SIGABRT	 7) SIGBUS	 8) SIGFPE	 9) SIGKILL	10) SIGUSR1
11) SIGSEGV	12) SIGUSR2	13) SIGPIPE	14) SIGALRM	15) SIGTERM
16) SIGSTKFLT	17) SIGCHLD	18) SIGCONT	19) SIGSTOP	20) SIGTSTP
21) SIGTTIN	22) SIGTTOU	23) SIGURG	24) SIGXCPU	25) SIGXFSZ
26) SIGVTALRM	27) SIGPROF	28) SIGWINCH	29) SIGIO	30) SIGPWR
31) SIGSYS	34) SIGRTMIN	35) SIGRTMIN+1	36) SIGRTMIN+2	37) SIGRTMIN+3
38) SIGRTMIN+4	39) SIGRTMIN+5	40) SIGRTMIN+6	41) SIGRTMIN+7	42) SIGRTMIN+8
43) SIGRTMIN+9	44) SIGRTMIN+10	45) SIGRTMIN+11	46) SIGRTMIN+12	47) SIGRTMIN+13
48) SIGRTMIN+14	49) SIGRTMIN+15	50) SIGRTMAX-14	51) SIGRTMAX-13	52) SIGRTMAX-12
53) SIGRTMAX-11	54) SIGRTMAX-10	55) SIGRTMAX-9	56) SIGRTMAX-8	57) SIGRTMAX-7
58) SIGRTMAX-6	59) SIGRTMAX-5	60) SIGRTMAX-4	61) SIGRTMAX-3	62) SIGRTMAX-2
63) SIGRTMAX-1	64) SIGRTMAX	

信号产生的时机和对应的默认处理动作:

编码 信号 对应事件 默认动作
1 SIGHUP 用户退出 shell 时,由该 shell 启动的所有进程将收到这个信号 终止进程
2 SIGINT 当用户按下了 组合键时,用户终端向正在运行中的由该终端启动的程序发出此信号 终止进程
3 SIGQUIT 用户按下 组合键时产生该信号,用户终端向正在运行中的由该终端启动的程序发出些信号 终止进程
4 SIGILL CPU 检测到某进程执行了非法指令 终止进程并产生 core 文件
5 SIGTRAP 该信号由断点指令或其他 trap 指令产生 终止进程并产生 core 文件
6 SIGABRT 调用 abort 函数时产生该信号 终止进程并产生 core 文件
7 SIGBUS 非法访问内存地址,包括内存对齐出错 终止进程并产生 core 文件
8 SIGFPE 在发生致命的运算错误时发出。不仅包括浮点运算错误,还包括溢出及除数为 0 等所有的算法错误 终止进程并产生 core 文件
9 SIGKILL 无条件终止进程。本信号不能被忽略,处理和阻塞 终止进程,可以杀死任何进程
10 SIGUSE1 用户定义的信号。即程序员可以在程序中定义并使用该信号 终止进程
11 SIGSEGV 指示进程进行了无效内存访问 (段错误) 终止进程并产生 core 文件
12 SIGUSR2 另外一个用户自定义信号,程序员可以在程序中定义并使用该信号 终止进程
13 SIGPIPE Broken pipe 向一个没有读端的管道写数据 终止进程
14 SIGALRM 定时器超时,超时的时间 由系统调用 alarm 设置 终止进程
15 SIGTERM 程序结束信号,与 SIGKILL 不同的是,该信号可以被阻塞和终止。通常用来要示程序正常退出。执行 shell 命令 Kill 时,缺省产生这个信号 终止进程
16 SIGSTKFLT Linux 早期版本出现的信号,现仍保留向后兼容 终止进程
17 SIGCHLD 子进程结束时,父进程会收到这个信号 忽略这个信号
18 SIGCONT 如果进程已停止,则使其继续运行 继续 / 忽略
19 SIGSTOP 停止进程的执行。信号不能被忽略,处理和阻塞 为终止进程
20 SIGTSTP 停止终端交互进程的运行。按下 组合键时发出这个信号 暂停进程
21 SIGTTIN 后台进程读终端控制台 暂停进程
22 SIGTTOU 该信号类似于 SIGTTIN,在后台进程要向终端输出数据时发生 暂停进程
23 SIGURG 套接字上有紧急数据时,向当前正在运行的进程发出些信号,报告有紧急数据到达。如网络带外数据到达 忽略该信号
24 SIGXCPU 进程执行时间超过了分配给该进程的 CPU 时间 ,系统产生该信号并发送给该进程 终止进程
25 SIGXFSZ 超过文件的最大长度设置 终止进程
26 SIGVTALRM 虚拟时钟超时时产生该信号。类似于 SIGALRM,但是该信号只计算该进程占用 CPU 的使用时间 终止进程
27 SGIPROF 类似于 SIGVTALRM,它不公包括该进程占用 CPU 时间还包括执行系统调用时间 终止进程
28 SIGWINCH 窗口变化大小时发出 忽略该信号
29 SIGIO 此信号向进程指示发出了一个异步 IO 事件 忽略该信号
30 SIGPWR 关机 终止进程
31 SIGSYS 无效的系统调用 终止进程并产生 core 文件
34-64 SIGRTMIN ~ SIGRTMAX LINUX 的实时信号,它们没有固定的含义(可以由用户自定义) 终止进程

1.1.2 查看信号信息

通过Linux提供的man文档可以查看所有信号的详细信息

#查看 man文档的信号描述
$ man 7 signal

在信号描述中介绍了对产生的信号的五种默认处理动作,分别是:

  1. Term: 信号将进程终止
  2. Ign: 信号产生之后默认被忽略
  3. Core: 信号将进程终止,并且产生一个core文件(用于gdb调试)
  4. Stop: 信号会暂停进程的运行
  5. Cont: 信号会让暂停的进程继续运行

注意:

9号信号和19号信号不能被 捕捉、阻塞和忽略:

9号信号:无条件杀死进程;

19号信号: 无条件暂停进程;

1.1.3 信号的状态

Linux信号有三种状态,分别为:产生、未决、递达

  1. 产生: 键盘输入,函数调用、执行shell命令,对硬盘进行非法访问都会产生信号;
  2. 未决: 信号产生了但是还没有被处理掉,这个期间信号的状态称为未决状态;
  3. 递达: 信号被处理了(某个进程处理掉)

Linux快速入门之 信号(13)_第1张图片

1.2 信号相关函数

Linux系统有很多函数能够产生信号,例如下面几个函数;

1.2.1 kill/raise/abort 函数

三个函数功能类似,可以发送相关信号给指定对应进程

  • kill 发送指定的信号给指定的进程
#include 
# 给某个进程发送某个信号
int kill(pid_t pid ,int sig);

参数:

pid: 进程ID;

sig: 要发送的信号;

//自己杀死自己
kill(getpid(),9);
//子进程杀死父进程
kill(getppid(),10);
  • raise 给当前进程发送指定的信号
//给自己发送某个信号
#include 
// 参数为给当前进程发送的信号
int raise(int sig);
  • abort: 给当前进程发送一个固定信号(SIGABRT);
//中端函数 ,调用这个函数,发送一个固定信号杀死当前进程
#include 
void abort(void);

1.2.2 定时器

  • alarm 函数

alarm() 函数只能进行单次定时,定时完成发射出一个信号。

#include 
unsigned int alarm(unsigned int seconds);

参数: 倒计时 second 秒,倒计时完成发送一个信号 SIGALRM(中端当前进程),当前进程收到这个信号;

返回值: 大于 0 为倒计时还剩多少秒 ; 返回值为0 表示倒计时完成信号被发出;

//使用定时器函数,检测当前计算机1s 内能数多少数
#include 
#include 
#include 
#include 
#include 

int main()
{
    //设置一个1s的定时器 
    alarm(1);   //1s 后进程中断
    int i =0;
    while(1)
    {
        printf("%d \n",i++);
    }
    return 0;
}
  • setitimer 函数

setitimer() 函数可以进行周期性定时,每触发一次定时器就会发射出一个信号。

// 这个函数可以实现周期性定时, 每个一段固定的时间, 发出一个特定的定时器信号
#include 

struct itimerval {
	struct timeval it_interval; /* 时间间隔 */
	struct timeval it_value;    /* 第一次触发定时器的时长 */
};
// 举例: luffy有一个闹钟, 并且使用这个闹钟定时:
// 早晨7点中起床, 第一次闹钟响起时可能起不来, 之后每隔5分钟再响一次
//  - it_value: 当前设置闹钟的时间点 到 明天早晨7点 对应的总秒数
//  - it_interval: 闹钟第一次响过之后, 每隔5分钟响一次

// 这个结构体表示的是一个时间段: tv_sec + tv_usec
struct timeval {
	time_t      tv_sec;         /* 秒 */
	suseconds_t tv_usec;        /* 微妙 */
};

int setitimer(int which, const struct itimerval *new_value, 
              struct itimerval *old_value);

参数:

  • ==which:==定时器使用什么样的计时法则,不同的计时法则发出的信号不同;
  • ITIMER_REAL: 自然计时法 ,最常用,发出的信号为 SIGALRM, 一般使用这个宏值,自然计时法时间 = 用户区 + 内核 + 消耗的时间 (从进程的用户区到内核区切换使用的总时间);
  • ITIMER_VIRTUAL: 只计算程序在用户区运行使用的时间 ,发射的信号为 SIGVTALRM;
  • ITIMER_PROF: 只计算内核运行使用的时间 ,发出的信号为 SIGPROF;
  • new_value: 给定时器设置的定时信息,传入参数;
  • old_value: 上一次给定时器设置的定时信息,传出参数,如果不需要这个信息,指定为 NULL;

1.3 信号集

1.3.1 阻塞 /未决信号集

阻塞/未决信号集是PCB中非常重要的信号集。 但是操作系统不允许我们直接对这两个信号集直接操作,而是需要自定义另外一个集合,借助信号集操作函数对PCB中这两个集合进行修改;

  • 信号的 “未决” 是一种状态,指的从信号产生到信号被处理前这段时间;

  • 信号的 "阻塞" 是一个开关动作,指的是阻止信号被处理,但不阻止信号产生;

信号的阻塞是为了让系统暂时保留信号等待以后发送。 由于另外有办法让系统忽略信号,所以通常信号的阻塞只是暂时的,是为了防止信号打断某些敏感的操作;

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-8oIrPH7D-1648192941035)(E:/富贵的编程日记/Linux系统/阻塞-未决信号.png)]

阻塞信号集未决信号集在内核中的结构是相同的。都是一个被封装好的整形数组 ,一共128字节(int [32] ==1024bit),1024个标志位,其中前31个标志位每个对应Linux中的标准信号 ,通过标志位的值来标记当前信号在信号集中的状态。

# 上图对信号集在内核中存储的状态的描述
# 前31个信号: 1-31 , 对应 1024个标志位的前31个标志位
			信号		标志位(从低地址位 到 高地址位)
		 	  1      ->  	0
			  2             1
			  3             2
			  4             3
			 31            30	
  • 在阻塞信号集中,描述这个信号有没有被阻塞:
  1. 默认情况下没有信号被阻塞的,因此信号对应的标志位的值为 0;
  2. 如果某个信号被设置为了阻塞 , 这个信号对应的标志位 被设置为 1;
  • 在未决信号集中,描述信号是否处于未决状态:
  1. 如果这个信号被阻塞了,不能处理,这个信号对应的标志位被设置为 1;
  2. 如果这个信号的阻塞被解除了,未决信号集中的这个信号马上就被处理,这个信号对应的标志位值变成 0;
  3. 如果信号没有被阻塞,信号产生之后直接被处理,因此不会在未决信号集中做任何记录;

5.3.2 信号集函数

用户不能直接操作内核中的阻塞信号集和未决信号集,必要调用系统函数,关于阻塞信号集可以通过系统函数进行读写操作,未决信号集只能对其进行读操作;

读写信号集函数:

#include 
//使用此函数修改内核中的阻塞信号集
//signal_t 被封装之后得到的数据类型,原型:int [32] 有1024个标志位 ,每一个信号对应一个标志位
int sigprocmask(int how , const sigset_t *set , sigset_t *oldset);

参数:

how:

  1. SIG_BLOCK: 将参数set集合中的数据追加到阻塞信号集中;
  2. SIG_UNBLOCK: 将参数set集合中的信号在阻塞信号集中解除阻塞;
  3. SIG_SETMASK: 使用参数 set 集合中的数据覆盖内核的阻塞信号集数据;
  4. oldset:通过此参数将设置之前的阻塞信号集传出,如果不需要可以指定为NULL;

返回值: 函数调用成功返回 0; 调用失败返回 -1;

sigprocmask () 函数有一个 sigset_t 类型的参数,对这种类型的数据进行初始化需要调用一些相关的操作函数:

#include 
// 如果在程序中读写 sigset_t 类型的变量
// 阻塞信号集和未决信号集都存储在 sigset_t 类型的变量中, 这个变量对应一块内存
// 阻塞信号集和未决信号集, 对应的内存中有1024bit = 128字节

// 将set集合中所有的标志位设置为0
int sigemptyset(sigset_t *set);
// 将set集合中所有的标志位设置为1
int sigfillset(sigset_t *set);
// 将set集合中某一个信号(signum)对应的标志位设置为1
int sigaddset(sigset_t *set, int signum);
// 将set集合中某一个信号(signum)对应的标志位设置为0
int sigdelset(sigset_t *set, int signum);
// 判断某个信号在集合中对应的标志位到底是0还是1, 如果是0返回0, 如果是1返回1
int sigismember(const sigset_t *set, int signum);

Linux快速入门之 信号(13)_第2张图片
未决信号集不需要程序猿修改,如果设置了某个信号阻塞,当这个信号产生之后,内核会将这个信号的未决状态记录到未决信号集中,当阻塞的信号被解除阻塞,未决信号集中的信号随之被处理,内核再次修改未决信号集将该信号的状态修改为递达状态(标志位置 0)。因此,写未决信号集的动作都是内核做的,这是一个读未决信号集的操作函数:

#include
//此函数的参数是传出参数 ,传出的内核未决信号集的拷贝;
//读以下这个集合就指定那盒信号是未决状态;
int sigpending(sigset_t *set);

信号集操作函数的使用:

需求:
在阻塞信号集中设置某些信号,通过一些操作产生这些信号,然后读取未决信号集,最后解除这些信号的阻塞;
假设阻塞这些信号:
     --- 2号信号: SIGINT : ctrl + c
     --- 3号信号: SIGQUIT: ctrl + \
     --- 9号信号: SIGKILL:通过shell命令给进程发送这个信号 kill -9 PID
#include 
#include 
#include
#include 
#include 

int main()
{
    // 1. 初始化信号集
    sigset_t myset;
    sigemptyset(&myset);
    //设置阻塞信号
    sigaddset(&myset,SIGINT); // 2
    sigaddset(&myset,SIGQUIT); //3
    sigaddset(&myset,SIGKILL); //9

    //2 . 将初始化的信号集中的数据设置给内核
    sigset_t old;
    sigprocmask(SIG_BLOCK,&myset,&old);

    //让进程一直运行,在当前进程中产生对于的信号
    int i=0;
    while (1)
    {
        //4. 读内核的未决信号集
        sigset_t curset;
        sigpending(&curset);
        //遍历这个信号集
        for (int i=1;i<32;++i)
        {
            int ret = sigismember(&curset,i);
            printf("%d",ret);
        }
        printf("\n");
        sleep(1);
        i++;
        if (i ==10)
        {
            //解除阻塞 ,重新设置阻塞信号集
            sigprocmask(SIG_SETMASK,&old,NULL);
        }
    }
    return 0;
}

//程序对9号信号的阻塞时无效的 ,9号信号无法被阻塞

信号集操作函数关系:

Linux快速入门之 信号(13)_第3张图片

1.4 信号捕捉

Linux系统中的每个信号都会有对应的默认处理行为,如果忽略这个信号或者修改某些信号的默认行为就需要在程序中捕捉该信号。程序中进行信号的捕捉可以看作是一个注册动作,提前告知应用程序信号产生之后做什么处理当信号中对应的信号产生了,该处理动作就会被调用;

1.4.1 signal 函数

使用signal()函数可以捕捉进程中产生的信号,并修改捕捉到的信号行为 ,该信号的自定义处理动作是一个回调函数,内核通过signal()得到这个回调函数的地址,在信号产生后之后该函数会被内核调用;

#include 
//在信号产生之前,提供一个注册函数,用来捕捉信号
//       --- 假设将来这个信号产生了,委托内核进行捕捉,这个信号的默认动作就不能被执行
//	   --- 执行什么样的处理动作 -> 在signal 函数中指定的处理动作
//       --- 如果这个信号不产生 , 回调函数不被执行
sighandler_t signal (int signum , sighandler_t hander);

参数:

  • signum: 需要捕捉的信号;

  • hander: 信号捕捉到之后的处理动作,是一个指针函数,其原型为:

    typedef void (*sighandler_t)(int);
    

    这个回调函数是需要程序猿写的,但是程序猿不调用,由内核调用,内核调用回调函数的时候,会给它传递一个实参,这个实参的值就是捕捉的那个信号值。

使用signal()函数捕捉定时器产生的信号SIGALRM:

#include 
#include 
#include
#include
#include
#include 

// 定时器信号的处理动作
void doing(int arg)
{
    printf("当前捕捉到的信号是: %d\n", arg);
    // 打印当前的时间
}

int main()
{
    // 注册要捕捉哪一个信号, 执行什么样的处理动作
    signal(SIGALRM, doing);
    // 1. 调用定时器函数设置定时器函数
    struct itimerval newact;
    // 3s之后发出第一个定时器信号, 之后每隔1s发出一个定时器信号
    newact.it_value.tv_sec = 3;
    newact.it_value.tv_usec = 0;
    newact.it_interval.tv_sec = 1;
    newact.it_interval.tv_usec = 0;
    // 这个函数也不是阻塞函数, 函数调用成功, 倒计时开始
    // 倒计时过程中程序是继续运行的
    setitimer(ITIMER_REAL, &newact, NULL);

    // 编写一个业务处理, 阻止当前进程自己结束, 让当前进程被发出的信号杀死
    while(1)
    {
        sleep(1000000);
    }

    return 0;
}

liu@liu-Ubuntu:~/vscode$ ./signal 
当前捕捉到的信号是: 14
当前捕捉到的信号是: 14
当前捕捉到的信号是: 14
当前捕捉到的信号是: 14
当前捕捉到的信号是: 14
当前捕捉到的信号是: 14
当前捕捉到的信号是: 14
当前捕捉到的信号是: 14
^C

1.4.2 sigaction 函数

sigaction() 函数和 signal() 函数功能一样,捕捉进程中的信号,将用户自定义的信号行为函数(回调函数)注册给内核,内核在内核信号产生之后调用这个处理动作。 sigaction() 可认为是 signal() 的加强版,参数复杂一点功能加强点;

#Include <signal.h>
int sigaction(int signum ,const struct sigaction *act , struct sigaction *olddact);

参数:

sigunm: 要捕捉的信号;

act: 捕捉到信号之后的处理动作;

oldact: 上一次调用该函数进行信号捕捉设置的信号处理动作,默认指定 NULL

返回值: 函数调用成功返回 0 ,失败返回 -1;

函数的参数是一个结构体,其原型:

struct sigaction {
	void     (*sa_handler)(int);    // 指向一个函数(回调函数)
	void     (*sa_sigaction)(int, siginfo_t *, void *);
	sigset_t   sa_mask;             // 初始化为空即可, 处理函数执行期间不屏蔽任何信号
	int        sa_flags;	        // 0
	void     (*sa_restorer)(void);  //不用
};

结构体成员:

  • sa_handler : 函数指针 ,指定的函数为捕捉到的信号的处理动作;
  • sa_sigaction : 函数指针 , 指定的函数为捕捉到的信号的处理动作
  • sa_mask:在信号处理函数执行期间,临时屏蔽某些信号,将要屏蔽的信号设置在集合中即可
  1. 当前处理函数执行完毕,临时屏蔽自动解除
  2. 假设这个集合中不屏蔽任何信号,默认也会屏蔽一个(捕捉信号谁,临时屏蔽谁)
  • sa_flags: 使用那个函数指针指向的函数处理捕捉到的信号;
  1. 0: 使用 sa_handler() 默认使用;
  2. SA_SIGINFO: 使用 sa_sigaction (使用信号传递数据 == 进程间通信)
  • sa_restorer: 被废弃的成员;

通过sigaction()函数捕捉阻塞信号集中的解除阻塞的信号,如果捕捉多个信号,给不同的信号添加不同的处理动作,代码中的处理动作只有一个;

#include 
#include 
#include 
#include 
#include 

// 信号的处理动作
void callback(int num)
{
    printf("当前捕捉的信号: %d\n", num);
}

int main()
{
    // 1. 初始化信号集
    sigset_t myset;
    sigemptyset(&myset);
    // 设置阻塞的信号
    sigaddset(&myset, SIGINT);  // 2
    sigaddset(&myset, SIGQUIT); // 3
    sigaddset(&myset, SIGKILL); // 9 测试不能被阻塞

    // 当阻塞的信号被解除阻塞, 该信号就可以被捕捉到了
    // 如果信号被捕捉到之后, 马上就被处理掉了 --> 递达状态
    struct sigaction act;
    act.sa_handler = callback;
    act.sa_flags = 0;
    sigemptyset(&act.sa_mask);
    sigaction(SIGINT, &act, NULL);
    // 和sigint的处理动作相同
    sigaction(SIGQUIT, &act, NULL);
    sigaction(SIGKILL, &act, NULL);

    // 2. 将初始化的信号集中的数据设置给内核
    sigset_t old;
    sigprocmask(SIG_BLOCK, &myset, &old);

    // 3. 让进程一直运行, 在当前进程中产生对应的信号
    int i = 0;
    while(1)
    {
        // 4. 读内核的未决信号集
        sigset_t curset;
        sigpending(&curset);
        // 遍历这个信号集
        for(int i=1; i<32; ++i)
        {
            int ret = sigismember(&curset, i);
            printf("%d", ret);
        }
        printf("\n");
        sleep(1);
        i++;
        if(i==10)
        {
            // 解除阻塞, 重新设置阻塞信号集
            //sigprocmask(SIG_UNBLOCK, &myset, NULL);
            sigprocmask(SIG_SETMASK, &old, NULL);
        }
    }
    return 0;
}
//程序中对 9 号信号的捕捉是无效的,因为它无法被捕捉。

1.5 SIGCHLD 信号

当子进程退出、暂停、从暂停回复运行的时候,在子进程中会产生一个 SIGCHLD 信号,并将其发送给父进程 ,但是父进程收到这个信号之后默认就忽略了。我们可以在父进程中对这个信号加以利用,基于这个信号来回收子进程的资源,因此需要在父进程中捕捉子进程发送过来的这个信号

#include 
#include 
#include 
#include 
#include 
#include 

// 回收子进程处理函数
void recycle(int num)
{
    printf("捕捉到的信号是: %d\n", num);
    // 子进程的资源回收, 非阻塞
    // SIGCHLD信号17号信号, 1-31号信号不支持排队
    // 如果这些信号同时产生多个, 最终处理的时候只处理一次
    // 假设多个子进程同时退出, 父进程同时收到了多个sigchld信号
    // 父进程只会处理一次这个信号, 因此当前函数被调用了一次, waitpid被调用一次
    // 相当于只回收了一个子进程, 但是是同时死了多个子进程, 因此就出现了僵尸进程
    // 解决方案: 循环回收即可
    while(1)
    {
        // 如果是阻塞回收, 就回不到另外一个处理逻辑上去了
        pid_t pid = waitpid(-1, NULL, WNOHANG);
        if(pid > 0)
        {
            printf("child died, pid = %d\n", pid);
        }
        else if(pid == 0)
        {
            // 没有死亡的子进程, 直接退出当前循环
            break;
        }
        else if(pid == -1)
        {
            printf("所有子进程都回收完毕了, 拜拜...\n");
            break;
        }
    }
}


int main()
{
    // 设置sigchld信号阻塞
    sigset_t myset;
    sigemptyset(&myset);
    sigaddset(&myset, SIGCHLD);
    sigprocmask(SIG_BLOCK, &myset, NULL);

    // 循环创建多个子进程 - 20
    pid_t pid;
    for(int i=0; i<20; ++i)
    {
        pid = fork();
        if(pid == 0)
        {
            break;
        }
    }

    if(pid == 0)
    {
        printf("我是子进程, pid = %d\n", getpid());
    }
    else if(pid > 0)
    {
        printf("我是父进程, pid = %d\n", getpid());
        // 注册信号捕捉, 捕捉sigchld
        struct sigaction act;
        act.sa_flags  =0;
        act.sa_handler = recycle;
        sigemptyset(&act.sa_mask);
        // 注册信号捕捉, 委托内核处理将来产生的信号
        // 当信号产生之后, 当前进程优先处理信号, 之前的处理动作会暂停
        // 信号处理完毕之后, 回到原来的暂停的位置继续运行
        sigaction(SIGCHLD, &act, NULL);

        // 解除sigcld信号的阻塞
        // 信号被阻塞之后,就捕捉不到了, 解除阻塞之后才能捕捉到这个信号
        sigprocmask(SIG_UNBLOCK, &myset, NULL);

        // 父进程执行其他业务逻辑就可以了
        // 默认父进程执行这个while循环, 但是信号产生了, 这个执行逻辑或强迫暂停
        // 	父进程去处理信号的处理函数
        while(1)
        {
            sleep(100);
        }
    }
    return 0;
}

你可能感兴趣的:(Linux,linux,unix,服务器)