【Linux】进程间通信——信号

进程间通信——信号

一, 信号的概念

信号是Linux进程间通信的最古老的方式之一,是事件发生时对进程的通知机制,有时也称为软件中断,是在软件层次上对中断机制的一种模拟,是一种异步通信的方式。

信号可以导致一个正在运行的进程被另一个正在运行的异步进程中断,转而处理某一个突发事件。

信号通常源于内核,引发内核为进程产生信号的各类事件如下:

  • 对于前台进程,用户可以通过输入特殊的中断字符来给它发送信号,比如输入Ctrl+C通常给进程发送一个中断信号。
  • 硬件发生异常,即硬件检测到一个错误条件并通知内核,随即再由内核发送相应信号给相关进程。比如执行一条异常的机器语言指令,比如被0除,或者引用了无法访问的内存区域。
  • 系统状态变化,比如alarm定时器到期将引起SIGALRM信号,进程执行的CPU时间超限,或者该进程的某个子进程退出等等,都会发送对应的信号。
  • 运行kill命令或调用kill()函数。

使用信号的两个主要目的:

  • 让进程知道已经发生了一个特定的事情;
  • 强迫进程执行它自己代码中的信号处理程序;

信号特点:

  • 简单
  • 不能携带大量信息
  • 满足某个特定条件才发送
  • 优先级比较高

1. 系统提供的信号

查看系统定义的信号列表: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

前31个信号常规信号,其余是实时信号。

下表是Linux信号表,其中红色标注的是常用的。

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

二, 信号默认的5种处理动作

查看信号的详细信息:man 7 signal,信号相关内容在第7章。

信号5种默认处理动作:

  • Term 终止进程
  • Ign 当前进程忽略掉这个信号
  • Core 终止进程,并生成一个Core文件(对错误进行调试)
  • Stop 暂停当前进程
  • Cont 继续执行当前被暂停的进程

信号的几种状态:产生、未决、递达;

SIGKILL和SIGSTOP信号不能被捕捉、阻塞或者忽略,只能执行默认动作

三, 信号相关的函数

1. 发送信号函数

信号发送函数有:kill()raise()abort()

kill()函数说明:

函数声明int kill(pid_t pid,int sig);
函数功能给任何进程pid或进程组发送任何信号sig
函数参数pidpid:需要发送给的进程的id
pid > 0:将信号发送给指定的进程
pid = 0:将信号发送给当前的进程组
pid = -1:将信号发送给每一个有权限接收这个信号的进程
pid < -1:绝对值表示某个进程组的ID
函数参数sigsig:发送的信号的宏或编号,=0表示不发送信号
返回值成功返回0,失败返回-1

raise()函数说明:

函数声明int raise(int sig);
函数功能给当前进程发送信号
函数参数sigsig:表示要发送的信号,类似于kill(getpid(),sig)
返回值成功返回0,失败返回-1

abort()函数说明:

函数声明void abort(void);
函数功能发送SIGABRT信号给当前进程,杀死当前进程
类似于 kill(getpid(),SIGABRT)

使用示例:

/**
 * @file sendsig.c
 * @author your name (you@domain.com)
 * @brief  使用kill 在父进程中向子进程发送SIGINT信号
 * @version 0.1
 * @date 2022-09-28
 *
 * kill     向任何进程发送任何信号
 * raise    向当前进程发送任何信号
 * abort    向当前进程发送SIGABRT信号
 *
 * @copyright Copyright (c) 2022
 *
 */

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

int main()
{
    /**
     * @brief 创建一个子进程,在父进程中向子进程发送SIGINT信号
     *
     */
    pid_t pid = fork();
    if (pid > 0)
    {
        // 父进程
        printf("parent process, pid : %d\n", getpid());
        sleep(2);
        printf("kill child process\n");
        int ret = kill(pid, SIGINT);
        if (ret == -1)
        {
            perror("kill");
            exit(-1);
        }
    }
    else if (pid == 0)
    {
        printf("child process, pid : %d\n", getpid());
    }

    return 0;
}

运行结果:

zoya@zoya-virtual-machine:~/Linux/chap2/lesson13/test$ ./a
parent process, pid : 3883
child process, pid : 3884
kill child process

2. 定时器函数

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

alarm()函数说明:

函数声明unsigned int alarm(unsigned int seconds);
函数功能设置定时器,函数调用时开始倒计时,当倒计时为0时,函数会给当前进程发送SIGALRM信号
函数参数seconds倒计时的时长
pid > =0表示定时器无效,不进行倒计时,不发送信号
取消一个定时器,可以使用alarm(0)
返回值之前没有定时器,返回0
之前有定时器,返回之前的定时器剩余的时间

需要注意的是:

  • SIGALRM信号默认终止当前进程,每一个进程都有且只有唯一的一个定时器;
  • 实际时间 = 内核时间 + 用户时间 + 消耗时间;
  • 定时器与进程的状态无关,是自然定时法,不管进程处于什么状态,alarm()都会计时;

alarm()函数使用示例:

/**
 * @file alarm.c
 * @author your name (you@domain.com)
 * @brief alarm()设置定时器,时间到达给当前进程发送SIGALRM信号,默认终止进程
 * @version 0.1
 * @date 2022-09-28
 *
 * @copyright Copyright (c) 2022
 *
 */

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

int main()
{
    int ret = alarm(5); // 设置一个5s的定时器
    printf("first alarm ret = %d\n", ret);

    sleep(2); // 延时2s,定时器还有3s时间

    // 设置新的定时器,定时5s
    ret = alarm(5);
    printf("second alarm, ret = %d\n", ret);

    // 每隔1s循环打印,观察定时器动作
    int i = 0;
    while (1)
    {
        printf(" %d \n", i++);
        sleep(1);
    }

    return 0;
}

运行结果:

first alarm ret = 0
second alarm, ret = 3
 0 
 1 
 2 
 3 
 4 
闹钟

setitimer()函数说明:

函数声明int setitimer(int which, const struct itimerval *new_value, struct itimerval *old_value);
函数功能设置定时器,可以替代alarm(),精度比alarm高,可以达到微妙us,实现周期性的定时
函数参数which指定定时器以什么时间计时
- ITIMER_REAL:真实时间,时间到达,会发送SIGALARM,比较常用
- ITIMER_VIRTUAL:用户时间,时间到达,发送SIGVTALRM
- ITIMER_PROF:以该进程在用户态和内核态下消耗的时间计算,时间到达,发送SIGPROF
函数参数new_value设置定时器的属性
函数参数old_value记录上一次的定时的时间参数;一般不使用,设置为NULL
返回值成功返回0,失败-1并设置errno

setitimer()函数中使用了结构体类型itimerval,该结构体声明如下:

// 定时器结构体
struct itimerval {
               struct timeval it_interval; /* 时间间隔*/
               struct timeval it_value;    /* 延迟时间执行定时器 */
           };

struct timeval {
               time_t      tv_sec;         /* seconds */
               suseconds_t tv_usec;        /* microseconds */
           };

示例,过3s后每隔2s定时一次,那么3s就是it_value,2s就是it_interval。

/**
 * @file setitimer.c
 * @author your name (you@domain.com)
 * @brief 使用setitimer()函数,过3s后每隔2s定时一次
 * @version 0.1
 * @date 2022-09-28
 *
 * @copyright Copyright (c) 2022
 *
 */

#include <sys/time.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>

int main()
{
    struct itimerval newvalue;

    newvalue.it_interval.tv_sec = 2; // 每隔2s定时一次
    newvalue.it_interval.tv_usec = 0;

    newvalue.it_value.tv_sec = 3; // 过3s后开始定时,3s后第一次定时,然后每隔2s定时一次
    newvalue.it_value.tv_usec = 0;

    setitimer(ITIMER_REAL, &newvalue, NULL);

    int i = 0;
    while (1)
    {
        printf(" %d \n", i++);
        sleep(1);
    }

    getc(stdin);

    return 0;
}

运行结果:

 0 
 1 
 2 
闹钟

3. 信号捕捉函数 signal()

信号捕捉函数有:signalsigaction,在头文件#include<signal.h>中声明。

需要注意:

  • 【SIGKILL】和【SIGSTOP】不能被捕捉、阻塞、忽略
  • aignal()的行为在不同的UNIX/Linux版本中也不同,所以建议使用另一个信号捕捉函数sigaction()代替signal()

The behavior of signal() varies across UNIX versions, and has also varied historically acrossdifferent versions of Linux. Avoid its use: use sigaction(2) instead.

signal()函数说明:

函数声明sighandler_t signal(int signum, sighandler_t handler);
函数功能设置信号signum的捕捉行为handler
函数参数signum要捕捉的信号,一般使用宏值
函数参数handler捕捉的信号要如何处理,取值:
- SIG_IGN:忽略信号
- SIG_DFL:使用信号默认行为
- 回调函数,内核调用,需要实现捕捉的信号的处理
返回值成功返回上一次注册的信号处理函数的地址,第一次调用返回NULL;失败返回SIG_ERR并设置errno

参数handler使用回调函数时函数按如下声明:

void (*sighandler_t)(int);  // int参数表示捕捉到的信号的编号

示例:使用signal()捕捉setitimer()设置定时器产生的SIGALRM信号,捕捉到信号后打印相关信息。

/**
 * @file signal.c
 * @author your name (you@domain.com)
 * @brief signal()函数捕捉SIGALRM信号并进行处理
 * @version 0.1
 * @date 2022-09-28
 *
 * @copyright Copyright (c) 2022
 *
 */

#include <sys/time.h>
#include <signal.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>

/**
 * @brief 回调函数,SIGALRM信号处理函数
 *
 * @param signum 捕捉到的信号
 */
void my_alarmhandler(int signum)
{
    printf("捕捉到的信号编号为: %d,", signum);
    if (signum == SIGALRM)
    {
        printf("AIGALRM信号\n");
    }

    printf("==================\n");
}

int main()
{
    /**
     * @brief
     *  首先捕捉信号,然后设置定时器;
     * 这样做的目的是为了防止出现定时器设置后时间已经到达(发送了SIGALRM信号)但是信号还没有捕捉的情况
     */

    // 捕捉信号
    if (signal(SIGALRM, my_alarmhandler) == SIG_ERR)
    {
        perror("signal");
        exit(-1);
    }

    // 设置定时器
    struct itimerval newvalue;
    newvalue.it_interval.tv_sec = 2;
    newvalue.it_interval.tv_usec = 0;
    newvalue.it_value.tv_sec = 3;
    newvalue.it_value.tv_usec = 0;

    setitimer(ITIMER_REAL, &newvalue, NULL);

    int i = 0;
    while (1)
    {
        printf(" %d \n", i++);
        sleep(1);
    }

    getc(stdin);

    return 0;
}

运行显示结果:

 0 
 1 
 2 
捕捉到的信号编号为: 14,AIGALRM信号
==================
 3 
 4 
捕捉到的信号编号为: 14,AIGALRM信号
==================
 5 
 6 
^C

在介绍sigaction()函数前先了解下信号集。

4. 信号集

用户进程常常需要对多个信号进行处理,为了方便对多个信号进行处理,引入了信号集。信号集是用来表示多个信号的数据类型,sigset_t

PCB(进程控制块)中有两个比较重要的信号集:阻塞信号集和未决信号集。

  • “未决”是一种状态,指的是从信号产生到信号被处理的这一段时间;
  • “阻塞”是一个动作,表示阻止信号被处理,但不阻止信号的产生;
  • 阻塞是让系统暂时保留信号待以后发送,一般信号阻塞是暂时的,只是为了防止信号打断敏感的操作。

未决信号集和阻塞信号集使用位图机制实现,操作系统不允许直接对这两个信号集进行操作,需要自定义另一个信号集,借助信号集操作函数对PCB中的这两个信号集进行修改。
在这里插入图片描述
未决信号集和阻塞信号集之间的关系:

  1. 用户通过键盘 Ctrl+C 产生信号SIGINT,信号被创建;
  2. 信号产生但是没有被处理,处于未决状态;
    1. 在内核中将所有没有被处理的信号存储在一个集合中,即未决信号集;
    2. SIGINT信号状态被存储在第二个标志位上;
      1. 标志位的值为0,表示信号不是未决状态;
      2. 标志位的值是1,表示信号处于未决状态;
  3. 这个未决状态的信号,需要被处理,处理之前需要和另一个信号集(阻塞>信号集)对应的标志位比较,如果是0就会被处理,如果是1就会被阻塞;
    1. 阻塞信号集默认不阻塞任何信号;
    2. 如果需要阻塞某些信号,需要调用系统API;
  4. 处理时和阻塞信号集中的标志位查询,查看是否对该信号设置了阻塞;
    1. 如果没有阻塞,这个信号被处理;
    2. 如果阻塞了,这个信号就继续处于未决状态,直到阻塞解除,这个信号被处理
4.1 信号集相关函数

信号集相关函数都在头文件 #include <signal.h>中声明。

以下是用于自定义信号集操作的函数:

函数名说明返回值函数声明参数介绍
sigemptyset清空信号集中的数据,将信号集中所有的标志位置为0成功返回0
失败返回-1
int sigemptyset(sigset_t *set); set–需要清空的信号集
sigfillset将信号集中所有标志位置为1成功返回0
失败返回-1
int sigfillset(sigset_t *set);set–需要置为1的信号集
sigaddset在信号集中添加某个信号,即把该信号对应的位置为1成功返回0
失败返回-1
int sigaddset(sigset *set, int signum);set–需要操作的信号集;
signum–需要添加的信号
sigdelset在信号集中删除某个信号,即把该信号对应的位置为0成功返回0
失败返回-1
int sigdelset(sigset_t *set, int signum);set–需要操作的信号集
signum–需要删除的信号
sigismember在某个集合中是否存在某个信号返回1表示信号在信号集中
0表示信号不在信号集中
-1表示出错
int sigismember(const sigset_t *set, int signum);判断信号signum是否在信号集set

以下函数是用于内核信号集相关函数:

int sigprocmask(int how,const sigset_t *set,sigset_t *oldset);  
int sigpending(sigset_t *set);

sigprocmask()函数说明:

函数声明int sigprocmask(int how,const sigset_t *set,sigset_t *oldset);
函数功能将自定义信号集中的数据设置到内核中(可以设置阻塞、解除阻塞、替换)
函数参数how对内核中的信号集如何处理
- SIG_BLOCK:将用户设置的信号集添加到内核中,设置阻塞;假设内核中的阻塞信号集是 mask,那么最终的信号集为 mask | set
- SIG_UNBLOCK:将用户设置的数据添加到内核中,设置为非阻塞;假设内核中的阻塞信号集是 mask,那么最终的信号集为 mask &= ~set
- SIG_SETMASK:覆盖内核中原来的信号集
函数参数set已经初始化的用户自定义信号集
函数参数oldset保存设置之前的内核中的信号集的状态
返回值成功返回0,失败返回-1并设置errno

需要注意:

  • 如果oldset参数非空,则进程当前信号屏蔽字通过oldset返回;
  • 如果set非空,则参数how只是如何修改信号屏蔽字;
  • 如果set为空,则不改变该进程的信号屏蔽字,how值无意义;

sigpending()函数说明:

函数声明int sigpending(sigset_t *set);
函数功能获取内核中的未决信号集
函数参数set保存内核中的未决信号集中的信号
返回值成功返回0,失败返回-1并设置errno

示例:把前31个信号的未决状态打印到屏幕上;设置信号SIGINTSIGQUIT为阻塞,通过键盘产生这两个信号,查看信号的未决状态。


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

void my_sighandler(int signum)
{
    printf("捕捉到信号:%d,", signum);
    if (SIGINT == signum)
    {
        printf(" SIGINT\n");
    }
    else if (SIGQUIT == signum)
    {
        printf(" SIGQUIT\n");
    }
    printf("=========================\n");
}

int main()
{
    /**
     * @brief
     * 打印前31个信号的未决状态;设置信号 SIGINT和SIGQUIT为阻塞,查看未决状态
     * 1. 捕捉信号
     * 2. 把信号 SIGINT和SIGQUIT添加到内核中这是阻塞
     * 3. 打印状态
     */

    // 捕捉信号
    if (SIG_ERR == signal(SIGINT, my_sighandler))
    {
        perror("signal");
        exit(-1);
    }

    // 添加信号 SIGINT和SIGQUIT到内核中
    sigset_t set;
    sigemptyset(&set);
    sigaddset(&set, SIGINT);
    sigaddset(&set, SIGQUIT);

    // 修改内核中的信号集
    if (-1 == sigprocmask(SIG_BLOCK, &set, NULL))
    {
        perror("sigprocmask");
        exit(-1);
    }

    // 获取当前未决信号集
    int num = 0;
    while (1)
    {
        num++;
        sigset_t pending;
        sigemptyset(&pending);
        sigpending(&pending);

        // 遍历获取前31个信号集的状态
        for (int i = 1; i <= 32; i++)
        {
            if (sigismember(&pending, i) == 1)
            {
                printf("1");
            }
            else if (sigismember(&pending, i) == 0)
            {
                printf("0");
            }
            else
            {
                perror("sigismember");
                exit(-1);
            }
        }
        printf("\n");
        sleep(1);

        // 循环10次后解除阻塞
        if (5 == num)
        {
            sigprocmask(SIG_UNBLOCK, &pending, NULL);
        }
    }

    return 0;
}

显示结果:

00000000000000000000000000000000
00000000000000000000000000000000
00000000000000000000000000000000
^C01000000000000000000000000000000
^\01100000000000000000000000000000
退出 (核心已转储)

5. 信号捕捉函数sigaction()

sigaction()函数说明:

函数声明int sigaction(int signum,const struct sigaction *act, struct sigaction *oldact);
函数功能信号捕捉,检查或改变信号处理
函数参数signum需要捕捉的信号的编号,建议使用宏
函数参数act捕捉到信号后对应的处理动作
函数参数oldact上一次对捕捉到的信号的相关的设置,不使用设置为NULL
返回值成功返回0,失败返回-1并设置errno

该函数用得到了结构体sigaction

struct sigaction {
               void     (*sa_handler)(int);  // 函数指针,指向的函数是信号捕捉到之后的处理函数
               void     (*sa_sigaction)(int, siginfo_t *, void *);  // 不常用
               sigset_t   sa_mask;  // 临时阻塞信号集,信号捕捉函数执行中,临时阻塞某些信号
               int        sa_flags;  // 使用哪一个信号处理函数对捕捉到的信号进行处理,0表示使用sa_handler,SA_SIGINFO表示使用sa_sigaction
               void     (*sa_restorer)(void);  // 被废弃掉,设置为NULL
           };

使用示例:

/**
 * @file sigaction.c
 * @author your name (you@domain.com)
 * @brief 使用sigaction()捕捉定时器信号,定时器在3s后每隔2s定时一次
 * @version 0.1
 * @date 2022-09-28
 *
 * @copyright Copyright (c) 2022
 *
 */

#include <sys/time.h>
#include <unistd.h>
#include <signal.h>
#include <stdio.h>
#include <stdlib.h>

void my_alarm(int signum)
{
    printf("捕捉到信号:%d, SIGALRM\n", signum);
    printf("================\n");
}

int main()
{
    // 捕捉信号SIGALRM
    struct sigaction act;
    act.sa_flags = 0;
    act.sa_handler = my_alarm;
    sigemptyset(&act.sa_mask);
    if (-1 == sigaction(SIGALRM, &act, NULL))
    {
        perror("sigaction");
        exit(-1);
    }

    // 设置定时器
    struct itimerval newvalue;
    newvalue.it_interval.tv_sec = 2;
    newvalue.it_interval.tv_usec = 0;
    newvalue.it_value.tv_sec = 3;
    newvalue.it_value.tv_usec = 0;
    setitimer(ITIMER_REAL, &newvalue, NULL);

    printf("定时器开始...\n");

    int num = 0;
    while (1)
    {
        num++;
        printf(" %d \n", num);
        sleep(1);
    }

    return 0;
}

运行结果:

定时器开始...
 1 
 2 
 3 
捕捉到信号:14, SIGALRM
================
 4 
 5 
捕捉到信号:14, SIGALRM
================
 6 
 7 
捕捉到信号:14, SIGALRM
================
 8 
^C

6. 内核实现信号捕捉

内核实现信号捕捉的过程:
在这里插入图片描述
注意:

  • 内核中有阻塞信号集,在捕捉信号时使用的是临时阻塞信号及,执行完处理函数后,恢复到内核阻塞信号集;
  • 执行某个回调函数期间,捕捉到的信号默认会被屏蔽直到回调函数执行结果;
  • 阻塞的常规信号是不支持排队的;

四, SIGCHLD信号介绍

SIGCHLD信号产生的条件:

  • 子进程终止时;
  • 子进程接收到SIGSTOP信号停止时;
  • 子进程处于停止状态,接收到SIGCONT后被唤醒时;

父进程接收到SIGCHLD信号,默认忽略该信号;
使用SIGCHLD信号可以解决僵尸进程的问题。

应在创建任何子进程之前就设置信号捕捉函数

示例:创建20个子进程,利用子进程结束时产生SIGCHLD信号让父进程回收子进程的资源。

/**
 * @file sigchild.c
 * @author your name (you@domain.com)
 * @brief 利用SIGCHLD信号解决僵尸进程的问题
 * @version 0.1
 * @date 2022-09-28
 *
 * @copyright Copyright (c) 2022
 *
 */

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

void my_childaction(int signum)
{
    // 父进程对SIGCHLD信号的处理函数, 回收子进程
    printf("捕捉到的信号:%d\n", signum);
    while (1)
    {
        int ret = waitpid(-1, NULL, WNOHANG); // -1表示回收任意进程组的进程,WNOHANG表示设置为非阻塞
        if (ret == 0)
        {
            // =0表示在非阻塞状态下还有子进程没有退出
            break;
        }
        else if (ret > 0)
        {
            // >0 表示被回收的子进程
            printf("child process , pid : %d 被回收\n", ret);
        }
        else if (ret == -1)
        {
            break;
        }
    }
}

int main()
{
    // 设置SIGCHLD阻塞
    // 1. 自定义信号集
    sigset_t set;
    // 2. 清空自定义信号集
    sigemptyset(&set);
    // 3. 把SIGCHLD信号加入到自定义信号集
    sigaddset(&set, SIGCHLD);
    // 4. 自定义信号集加入到内核信号集并设置为阻塞
    sigprocmask(SIG_BLOCK, &set, NULL);

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

    if (pid > 0)
    {
        // 父进程 捕捉SIGCHLD信号,在处理函数中回收子进程
        struct sigaction act;
        act.sa_flags = 0;
        act.sa_handler = my_childaction;
        sigemptyset(&act.sa_mask);
        sigaction(SIGCHLD, &act, NULL);
        // 5. 捕捉信号后设置SIGCHLD信号为非阻塞
        sigprocmask(SIG_UNBLOCK, &set, NULL);

        while (1)
        {
            printf("parent process, pid : %d\n", getpid());
            sleep(2);
        }
    }
    else if (pid == 0)
    {
        printf("child process, pid : %d\n", getpid());
    }
}

显示结果:

child process, pid : 7736
child process, pid : 7737
child process, pid : 7739
child process, pid : 7740
child process, pid : 7738
child process, pid : 7742
child process, pid : 7749
child process, pid : 7752
child process, pid : 7746
child process, pid : 7744
捕捉到的信号:17
child process , pid : 7736 被回收
child process , pid : 7737 被回收
child process , pid : 7738 被回收
child process , pid : 7739 被回收
child process , pid : 7740 被回收
child process , pid : 7742 被回收
child process , pid : 7744 被回收
child process, pid : 7747
child process, pid : 7754
child process, pid : 7745
child process, pid : 7748
child process , pid : 7749 被回收
child process, pid : 7755
child process, pid : 7750
child process, pid : 7743
child process, pid : 7741
child process, pid : 7751
child process, pid : 7753
child process , pid : 7745 被回收
child process , pid : 7741 被回收
child process , pid : 7746 被回收
child process , pid : 7747 被回收
child process , pid : 7748 被回收
child process , pid : 7750 被回收
child process , pid : 7751 被回收
child process , pid : 7752 被回收
child process , pid : 7753 被回收
child process , pid : 7754 被回收
child process , pid : 7755 被回收
捕捉到的信号:17
parent process, pid : 7735
捕捉到的信号:17
child process , pid : 7743 被回收
parent process, pid : 7735
parent process, pid : 7735
parent process, pid : 7735
parent process, pid : 7735
parent process, pid : 7735
parent process, pid : 7735
parent process, pid : 7735
parent process, pid : 7735
parent process, pid : 7735
parent process, pid : 7735
^C

4.1 上述代码中出现的问题

本文内容是基于牛客网相关课程撰写。这里贴一下其中一位网友关于上述代码中出现的问题的讨论。
上面SIGCHLD信号的代码中原来是没有将SIGCHLD信号阻塞的,运行时会出现段错误;设置SIGCHLD信号阻塞后就可以解决这个问题。

研究僧018#
首先感谢评论区的各位大佬提的有质量的问题,让我对SIGCHLLD信号传递背后发生的故事产生了探索的欲望。借助权威书籍《Linux/UNIX系统编程手册》,我尝试对评论区小伙伴的问题写下自己的理解,疏漏之处还望理解。
1.为什么加了while可以回收之前被忽略掉SIGCHLD的僵尸进程。

小伙伴们不要有这样的误解,A子进程产生信号,调用了myfun函数,waitpid(wait函数同理)就只会去回收A进程。waitpid函数是个劳模,它只要见到僵尸进程就忍不住要回收,但能力有限,一次只能回收一次。只要给它机会,它可以把所有的僵尸进程一网打尽。所以只要有while循环,就可以不断执行waitpid函数,直到break。

2.如果信号阻塞以后不能被捕获,那么是如何做到 “先阻塞SIGCHLD信号,当注册完信号捕捉以后,再解除阻塞,这样就会继续执行回调函数回收资源”?

要弄懂这个问题,我们需要理清内核是如何处理信号的。信号的产生是异步的,A子进程产生SIGCHLD信号,不意味着父进程要立刻捕捉然后去做一些反应。当信号产生时,内核中未决信号集第17位会置1,它会等待父进程拥有cpu权限再去执行捕获信号处理函数,在去处理的瞬间17号位就会由1变为0,代表该信号有去处理了。

当我们提前设置了堵塞SIGCHLD信号,那未决集中就会一直保持1,不会调用捕获信号处理函数(也可以说信号不能被捕获),等待堵塞解除。所以并不是说,我们把信号堵塞了,然后解除堵塞,这个信号就消失了,它还是在未决集中的,值为1。捕捉函数捕获的其实就是这个1。信号捕捉不是钓鱼,钓鱼的话如果不及时处理,鱼就会跑掉。更像是网鱼,只要信号入网了,就跑不掉了。等我们准备好工具去捕获,会看到网上的鱼还是在的。

高老师最后为什么要提前堵塞SIGCHLD信号?加了阻塞之后是什么情况?假设极端情况,20个子进程老早就终止了,内核收到SIGCHLD信号,会将未决信号集中的17号位置为1,就算他们是接连终止,该信号位也不会计数,只有保持1 。但同时该信号被提前阻塞,所以该17号位置保持1(阻塞是保持1,不是变回0),等待处理。当注册完信号捕捉函数以后,再解除阻塞。内核发现此时第17号位居然是1,那就去执行对应的捕获处理函数。在处理函数中,waitpid函数发现:“哎呦,这怎么躺着20具僵尸呀”,然后它就先回收一具僵尸,返回子进程id,循环第二次,继续回收第2具僵尸,直到所以僵尸被回收,此时已经没有子进程了,waitpid函数返回-1,break跳出循环。

while循环中,返回值0对应的是没有僵尸但有正常的儿子,返回值-1代表压根没有儿子。所以只要子进程中存在僵尸,这个while就不会break,waitpid就可以悠哉悠哉地一次回收一具。

《Linux/UNIX系统编程手册》指出为了保障可移植性,应用应在创建任何子进程之前就设置信号捕捉函数。【牛客789400243号】提出了这个观点,应该在fork之前就注册信号捕捉的。其实就是对应了书上这句话。

3. 【去冰加芝士】小伙伴的问题:为什么捕捉到了信号后没有进行处理就直接继续执行父进程后面的程序了呢?

信号产生,内核中未决信号集SIGCHLD信号置1,内核调用信号捕捉函数myfun的同时把该信号置0,也就是说进入myfun函数后,内核依然是可以接收到SIGCHLD信号的。但是Linux为了防止某一个信号重复产生,在myfun函数进行多次递归导致堆栈空间爆了,它在调用myfhun函数会自动(内核自己完成)堵塞同类型信号。当然也可以用参数,自己指定要堵塞其他类型的信号。要注意的是,这里堵塞不是不接收信号,而是接收了不处理。当myfun函数结束,堵塞就会自动解除,该信号会传递给父进程。想象一个场景,20个子进程,先瞬间终止10个,父进程捕获到信号,进入myfun函数wait回收。这里有个点就是,父进程在执行myfun函数的时候,其他子进程不是挂起的,也是会运行的,至于怎么调度,那就看神秘莫测的调度算法了。在回收过程中,其余10个子进程也终止了,发出呼喊:“爹,快来回收我!”。父进程:“我没空,我还在myfun函数中干活”。于是内核将未决集中SIGCHLD信号置1等待处理,父进程在myfun函数中使用waitpid函数回收僵尸,”怎么越回收越多呀”,在while函数的加持下,他成功回收了20个僵尸。当它回到主函数打算休息下,内核叮的一声,有你的SIGCHLD信号,父进程以为有僵尸再次进入myfun函数,执行waipid函数,发现压根没有僵尸(上一次都回收完了),甚至儿子都没了(返回-1,break),骂骂咧咧返回了主函数。这就是为什么父进程捕获到了信号,进入了myfun函数,一个僵尸都没回收的真相。

4.段错误究竟是怎么发生的?段错误的复现为什么这么难?

段错误是个迷,有的人碰到过几次,有的人怎么也碰不到,这是由于神秘莫测的调度算法导致的。【潇潇_暮雨】小伙伴提出了,这是调用了不可重入的函数。《Linux/UNIX系统编程手册》第21.1.2节 对可重入函数进行了详细的解释,有兴趣的可以去翻一下。

可重入函数的意思是:函数由两条或多条线程调用时,即便是交叉执行,其效果也与各线程以未定义顺序依次调用时一致。通俗点讲,就是存在一个函数,A线程执行一半,B线程抢过CPU又来调用该函数,执行到1/4倍A线程抢回执行权。在这样不断来回执行中,不出问题的,就是可重入函数。多线程中每个线程都有自己的堆栈,所以如果函数中只用到局部变量肯定是可重入的,没问题的。但是更新了全局变量或静态数据结构的函数可能是不可重入的。假设某线程正在为一个链表结构添加一个新的链表项,而另外一个线程也视图更新同一链表。由于中间涉及多个指针,一旦另一线程中断这些步骤并修改了相同指针,结果就会产生混乱。但是并不是一定会出现,一定是A线程刚好在修改指针,另外一线程又去修改才会出现。这就是为什么该问题复现难度较高的原因。

作者在文中指出,将静态数据结构用于内部记账的函数也是不可重入的。其中最明显的例子就是stdio函数库成员(printf()、scanf()等),它们会为缓冲区I/O更新内部数据结构。所以,如果在捕捉信号处理函数中调用了printf(),而主程序又在调用printf()或其他stdio函数期间遭到了捕捉信号处理函数的中断,那么有时就会看到奇怪的输出,设置导致程序崩溃。虽然printf()不是异步信号安全函数,但却频频出现在各种示例中,是因为在展示对捕捉信号处理函数的调用,以及显示函数中相关变量的内容时,printf()都不失为一种简单而又便捷的方式。真正的应用程序应当避免使用该类函数。

printf函数会使用到一块缓冲区,这块缓冲区是使用malloc或类似函数分配的一块静态内存。所以它是不可重入函数。

lyyyrx : 我自己有一个小问题,可能是我什么地方理解的不对,还望大佬能帮忙解答一下: 假如内核收到了一个SIGCHILD信号,将未决信号集中该信号对应的第17位设置为1以后,如果捕获信号函数没有注册完,那么第17位是不是也不会被置为0,直到捕获信号函数注册完成进行捕获了以后才会再被置为0呢?如果是这样的话,那阻塞和不阻塞的意义应该是一样的吧?
09-02 03:16 北京回复(0)赞(0)

研究僧0 回复 lyyyrx : 因为对SIGCHILD信号的默认处理是将其忽略(此时捕捉信号函数还没有注册),如果没有阻塞,直接忽略掉,那父进程就不知道之前发生了什么事情。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值