信号捕捉的细节理解以及代码

目录

函数接口

signal()函数 信号捕捉

sigaction()函数 信号捕捉

实时信号和普通信号

不可重入函数

编译器优化--volatile保持内存可见性

SIGCHLD17号信号


函数接口

signal()函数 信号捕捉

#include <signal.h>
功能 信号处理器,即可以收到特定信号时,执行自定义动作
​
原型
    typedef void (*sighandler_t)(int); // 函数指针,传入的int表示信号的编号
    sighandler_t signal(int signum, sighandler_t handler);//其实就是拿着signum去操作方法表里修改对应信号的操作方法
// 设置了收到2号信号的自定义动作
void handler(int signo)
{
    std::cout << "捕捉到2号信号" << signo << std::endl;
}
signal(signo, SIG_IGN);//显示忽略某信号
​
// 这里是对signal函数的调用,而不是对handler的调用
// handler对应的方法一般不会执行,除非收到对应的信号
signal(2, handler);// 只是注册了该信号对应的动作
//要想触发该动作,需要按ctrl+c组合键 SIGINT的默认处理动作是终止进程
//2号的默认动作是退出进程,当我们给这个进程的2号信号设置了自定义动作,就会执行自定义动作

sigaction()函数 信号捕捉

#include <signal.h>
功能 信号处理器,即可以收到特定信号时,执行自定义动作
​
原型
    int sigaction(int signo, const struct sigaction *act, struct sigaction *oact);
参数
    signo:信号编号
    act输入型参数:
        非nullptr:根据act修改对应信号的处理动作
    oact输出型参数:
        nullptr:不保存原来的操作方法
        非nullptr:保存原来的操作方法
返回值
    调用成功则返回0,出错则返回-1
struct sigaction {
               void     (*sa_handler)(int);//sa_handler==SIG_IGN,表示忽略;sa_handler==SIG_DFL,表示执行系统默认动作;sa_handler==用户自定义,表示捕捉
               void     (*sa_sigaction)(int, siginfo_t *, void *);//实时信号的处理函数
               sigset_t   sa_mask;//表示当前进程正在处理某一种信号时,额外需要屏蔽的信号集【正在递达的信号自动放进当前进程的信号屏蔽字里】。当信号处理函数返回时,自动恢复原来的信号屏蔽字【不会再屏蔽额外添加的这个信号集】 sigaddset(&act.sa_mask, signo);
               int        sa_flags;//设为0即可
               void     (*sa_restorer)(void);//已不再使用
           };
sa_handler和sa_sigaction两个参数不共存!

实时信号和普通信号

实时信号是可靠信号【SIGRTMIN32以及SIGRTMAX=63之间的信号】,普通信号是不可靠信号【信号值小于SIGRTMIN】。

void Count(int cnt)
{
    while(cnt)
    {
        printf("cnt: %2d\r", cnt);
        fflush(stdout);
        cnt--;
        sleep(1);
    }
}
void handler(int signo)
{
    printf("pid: %d, %d号信号正在被捕捉\n", getpid(), signo);
    Count(20);
}
int main()
{
    struct sigaction act, oact;
    // 方法1 初始化sa_mask
    // sigset_t mask;
    // sigemptyset(&mask);
    // act.sa_mask = mask;
    // 方法2 初始化sa_mask
    sigemptyset(&act.sa_mask);
​
    act.sa_handler = handler;
    act.sa_flags = 0;
    sigaction(2, &act, &oact);
​
    while(1);
    return 0;
}

实验结果:当我们给进程发了5次2号信号时,进程就处理了2次该信号。分析:第一次处理该信号是第一次接收到该信号,在pending位图中2号信号置1,进程就捕捉了该信号,随即进行处理进入到自定义动作的20s休眠期期间,这个处理是先把pending位图上的2号信号置0,再去调用自定义处理函数,OS还会自动把2号信号加入到当前进程的信号屏蔽字里。而一个进程只有一个位图,多出来的信号也无法记录。待20s休眠期过了,OS会解除对2号信号的屏蔽,还看到pending位图上2号信号是1,就会自动递达2号信号。再去处理第2次收到的2号信号。所以2号信号被处理了2次。

当我们正在递达2号信号期间【即自定义动作的20s休眠期期间】,同类型信号无法被抵达!因为此时2号信号正在被捕捉,系统会自动将当前信号加入到进程的信号屏蔽字block表里。当信号完成捕捉动作后,系统会解除对该信号的屏蔽。一般一个信号被解除屏蔽的时候,OS会检测进程的pending位图,该信号为1的话就自动进行递达当前屏蔽信号,为0就不做任何动作。

进程处理信号的原则是串行处理同类型信号正在递达某个信号时,同类型信号无法被抵达。不允许递归处理,即不会记录多次信号。

以下代码为比较2号信号和40号信号,哪个信号不会丢失的代码--比较可靠信号和不可靠信号。

#include <iostream>
#include <vector>
#include <signal.h>
#include <sys/types.h>
#include <unistd.h>
​
static std::vector<int> sigArr = {2, 40};
#define MAX_SIGNUM 31
​
static void showpending(const sigset_t& pending)
{
    for(int signo = MAX_SIGNUM; signo >= 1; signo--)
    {
        if(sigismember(&pending, signo))
        {
            std::cout << "1";
        }
        else
        {
            std::cout << "0";
        }
    }
    std::cout << std::endl;
}
​
void handler(int signo)
{
    std::cout << "捕捉到一个信号,编号是" << signo << std::endl;
}
​
int main()
{
    signal(2, handler);
    signal(40, handler);
    // 1.1创建屏蔽字信号集 输出屏蔽字信号集 pengding信号集
    sigset_t block, oblock, pending;
    // 1.2初始化信号集
    sigemptyset(&block);
    sigemptyset(&oblock);
    sigemptyset(&pending);
    // 1.3信号屏蔽字添加2号和40号信号
    for(const auto& signo : sigArr) sigaddset(&block, signo);
    // 1.4使当前进程屏蔽2号和40号信号
    sigprocmask(SIG_SETMASK, &block, &oblock);
    std::cout << getpid() << std::endl;
    
    int cnt = 15;
    while(true)
    {
        sigemptyset(&pending);
        sigpending(&pending);
        showpending(pending);
        sleep(1);
​
        if(cnt-- == 0)
        {
            std::cout << "不屏蔽任何信号,信号已递达" << std::endl;
            // 解除对2号和40号信号的屏蔽
            sigprocmask(SIG_SETMASK, &oblock, &block);
        }
    }
    return 0;
}

不可重入函数

理论上讲一个程序就只有1个执行流,但是在分析的时候,我们将main执行流和信号捕捉执行流分开看待。重入:重复进入

在main()中和在handler()中,有个函数func()被两个执行流重复进入和调用,导致程序运行出问题,则该func()函数为不可重入函数;反之,函数被重复进入,但程序没有出问题,则为可重入函数。

符合不可重入函数的条件

  1. 调用了malloc或free,因为malloc也是用全局链表来管理堆的;

  2. 调用了标准I/O库函数。标准I/O库的很多实现都以不可重入的方式使用全局数据结构。

目前我们用的大部分接口都是不可重入函数,故不可重入是特性,不是问题。

编译器优化--volatile保持内存可见性

#include <stdio.h>
#include <signal.h>
​
int quit = 0;
​
void handler(int signo)
{
    printf("%d号信号正在被捕捉\n", signo);
    printf("quit: %d", quit);
    quit = 1;
    printf(" -> %d\n", quit);
}
​
int main()
{
    signal(2, handler);
    while(!quit);
    printf("我是正常退出的\n");
    return 0;
}
​
//同一份代码用不同的编译选项, man gcc查看编译器优化选项 -O -O0 -O1 -O2 -O3
//gcc mysignal.c -o mysignal
//gcc mysignal.c -o mysignal -O3

现象:正常编译并执行,能看到程序是正常退出的;当加上编译选项-O3时,程序并没有退出!说明quit肯定还是0,那自定义函数改的是内存上的数据。

共识:数据的保存不是在内存,就是在寄存器里。cpu:到内存(映射到物理内存)取指令-->分析指令-->执行指令-->将结果写回对应的内存(映射到物理内存)。

编译器优化后:在main执行流里,OS发现quit只被做检测,没有做修改,就建议编译器把quit变量放到寄存器中,cpu就不做取指令这个行为了(不再从内存中取数据),而handler()函数改的是对应内存里的quit的值,与预加载优化到寄存器的quit变量无关。所以程序不会退出。

这样的行为就导致了寄存器遮盖了内存,while眼里只有寄存器,这是不对的!所以要加上关键字volatile,可以让cpu每次读取该变量时都要到内存里去读取,而不是从寄存器中读取。

SIGCHLD17号信号

当讲到僵尸进程(子进程退出了父进程还在运行,但父进程没有读取子进程状态,子进程进入Z状态)时,提过父进程可以阻塞等待子进程结束,也可以非阻塞地查询是否有子进程结束等待清理(也就是轮询的方式)。采用第一种方式,父进程阻塞了就不能处理自己的工作了;采用第二种方式,父进程在处理自己的工作的同时还要记得时不时地轮询一下,程序实现复杂。

实际上,子进程在终止时会给父进程发SIGCHLD信号,该信号的默认处理动作是内核级别的忽略,父进程可以自定义SIGCHLD信号的处理函数,这样父进程只需专心处理自己的工作,子进程终止时父进程会收到信号,父进程在自定义的信号处理函数中调用wait清理子进程即可。

具体代码如下(此代码仅限linux系统有效)

#include <stdio.h>
#include <stdlib.h>
#include <signal.h>
#include <unistd.h>
​
//SIGCHLD测试
void Count(int cnt)
{
    while(cnt)
    {
        printf("cnt: %2d\r", cnt);
        fflush(stdout);
        cnt--;
        sleep(1);
    }
}
void handler(int signo)
{
    // 这里肯定是子进程退出后才执行自定义捕捉函数
    // 情况1:多个子进程同一时刻退出==>pid写-1,还要while(1)等待
​
    // 情况2:多个子进程,只有一部分子进程退出==>不等
    // WNOHANG: 若pid指定的子进程没有结束,则waitpid()函数返回0,不予以等待。若正常结束,则返回该子进程的ID
    while(1)
    {
        printf("pid: %d, %d号信号正在被捕捉\n", getpid(), signo);
        pid_t ret = waitpid(-1, NULL, WNOHANG);
        if(ret == 0) break;
    }    
}
int main()
{
    // 实验1:验证父进程收到了17号信号
    //signal(SIGCHLD, handler);
    
    // 实验2:手动设置忽略17号信号 与默认的Ign表现不一样
    //显示忽略17号信号,父进程就可以不用再等待子进程了
    //子进程退出后,自动变成僵尸状态,自动被系统回收
    signal(SIGCHLD, SIG_IGN);
    
    printf("父进程, pid:%d, ppid:%d\n", getpid(), getppid());
​
    pid_t id = fork();
    if(id == 0)
    {
        printf("子进程, pid:%d, ppid:%d\n", getpid(), getppid());
        Count(5);
        exit(0);
    }
    while(1)
    {        
        sleep(1);
    }
    return 0;
}
[yyq@VM-8-13-centos 2023_03_11_ProcessSignal]$ make
gcc -o mysignal mysignal.c
[yyq@VM-8-13-centos 2023_03_11_ProcessSignal]$ ./mysignal 
父进程, pid:4719, ppid:13529
子进程, pid:4720, ppid:4719
Cnt:  1
[yyq@VM-8-13-centos 2023_03_11_ProcessSignal]$ while :; do ps ajx | head -1 && ps ajx | grep mysignal | grep -v grep; sleep 1; echo "-----------"; done
 PPID   PID  PGID   SID TTY      TPGID STAT   UID   TIME COMMAND
15837 16031 16031 15837 pts/14   16031 S+    1001   6:38 ./mysignal
-----------
 PPID   PID  PGID   SID TTY      TPGID STAT   UID   TIME COMMAND
15837 16031 16031 15837 pts/14   16031 S+    1001   6:38 ./mysignal
-----------
 PPID   PID  PGID   SID TTY      TPGID STAT   UID   TIME COMMAND
13529  4719  4719 13529 pts/4     4719 S+    1001   0:00 ./mysignal//父进程 可以看到这个状态保持5s
 4719  4720  4719 13529 pts/4     4719 S+    1001   0:00 ./mysignal//子进程
15837 16031 16031 15837 pts/14   16031 S+    1001   6:38 ./mysignal
-----------
 PPID   PID  PGID   SID TTY      TPGID STAT   UID   TIME COMMAND
13529  4719  4719 13529 pts/4     4719 S+    1001   0:00 ./mysignal//父进程
 4719  4720  4719 13529 pts/4     4719 S+    1001   0:00 ./mysignal//子进程
15837 16031 16031 15837 pts/14   16031 S+    1001   6:38 ./mysignal
-----------
 PPID   PID  PGID   SID TTY      TPGID STAT   UID   TIME COMMAND
13529  4719  4719 13529 pts/4     4719 S+    1001   0:00 ./mysignal//父进程
 4719  4720  4719 13529 pts/4     4719 S+    1001   0:00 ./mysignal//子进程
15837 16031 16031 15837 pts/14   16031 S+    1001   6:38 ./mysignal
-----------
 PPID   PID  PGID   SID TTY      TPGID STAT   UID   TIME COMMAND
13529  4719  4719 13529 pts/4     4719 S+    1001   0:00 ./mysignal//父进程
 4719  4720  4719 13529 pts/4     4719 S+    1001   0:00 ./mysignal//子进程
15837 16031 16031 15837 pts/14   16031 S+    1001   6:38 ./mysignal
-----------
 PPID   PID  PGID   SID TTY      TPGID STAT   UID   TIME COMMAND
13529  4719  4719 13529 pts/4     4719 S+    1001   0:00 ./mysignal//父进程
 4719  4720  4719 13529 pts/4     4719 S+    1001   0:00 ./mysignal//子进程
15837 16031 16031 15837 pts/14   16031 S+    1001   6:38 ./mysignal
-----------
 PPID   PID  PGID   SID TTY      TPGID STAT   UID   TIME COMMAND
13529  4719  4719 13529 pts/4     4719 S+    1001   0:00 ./mysignal//父进程 //子进程已被迅速回收,我们都看不到Z
15837 16031 16031 15837 pts/14   16031 S+    1001   6:38 ./mysignal
-----------
 PPID   PID  PGID   SID TTY      TPGID STAT   UID   TIME COMMAND
13529  4719  4719 13529 pts/4     4719 S+    1001   0:00 ./mysignal//父进程
15837 16031 16031 15837 pts/14   16031 S+    1001   6:38 ./mysignal
-----------
 PPID   PID  PGID   SID TTY      TPGID STAT   UID   TIME COMMAND
15837 16031 16031 15837 pts/14   16031 S+    1001   6:38 ./mysignal

注意:前面讲过SIGCHLD信号的默认处理动作是Ign,是内核级别的忽略,还需要用户调用waitpid来回收子进程;而signal(SIGCHLD, SIG_IGN);是手动设置的,子进程退出时系统会自动回收该子进程。

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值