Linux进程信号(五)之捕捉信号

捕捉信号

信号是什么时候被处理的?

内核态:允许进程访问操作系统的代码和数据!
用户态:只能访问(进程)自己的代码和数据!

当进程从内核态返回到用户态的时候,进行信号的检测和处理!

返回之前,还处于内核态,而且这时候更重要的事已经做完了!

CPU在跑代码时,不仅仅跑用户自己写的代码,还跑库函数的代码和操作系统的代码!

调用系统调用,除了调用相应的函数,还需要切换身份:用户身份变成内核身份(或者反过来)。

内核态-用户态

image-20250417145521767

不允许以内核态的身份来访问用户代码,

防止用户用这种方式用操作系统的权限做非法操作。

身份切换就是将ECS寄存器的低两位比特位改变0->3/3->0

image-20250423233150572

内核如何实现信号的捕捉

如果信号的处理动作是用户自定义函数,在信号递达时就调用这个函数,这称为捕捉信号。

由于信号处理函数的代码是在用户空间的,处理过程比较复杂,

举例如下:

用户程序注册了SIGQUIT信号的处理函数sighandler

当前正在执行main函数,这时发生中断或异常切换到内核态。

在中断处理完毕后要返回用户态的main函数之前检查到有信号SIGQUIT递达。

内核决定返回用户态后不是恢复main函数的上下文继续执行,

而是执行sighandler函数,sighandlermain函数使用不同的堆栈空间,

它们之间不存在调用和被调用的关系,是 两个独立的控制流程。

sighandler函数返回后自动执行特殊的系统调用sigreturn再次进入内核态。

如果没有新的信号要递达,这次再返回用户态就是恢复main函数的上下文继续执行了。

sigaction

image-20250424230021961

#include <signal.h>
int sigaction(int signo, const struct sigaction *act, struct sigaction *oact); 
sigaction函数可以读取和修改与指定信号相关联的处理动作。
调用成功则返回0,出错则返回-1。
signo是指定信号的编号。
若act指针非空,则根据act修改该信号的处理动作。(输入型参数)
若oact指针非空,则通过oact传出该信号原来的处理动作。(输出型参数)

actoact指向sigaction结构体:

sa_handler赋值为常数SIG_IGN传给sigaction表示忽略信号,

赋值为常数SIG_DFL表示执行系统默认动作,

赋值为一个函数指针表示用自定义函数捕捉信号,

或者说向内核注册了一个信号处理函数,

该函数返回值为void,可以带一个int参数,通过参数可以得知当前信号的编号,

这样就可以用同一个函数处理多种信号。

显然,这也是一个回调函数,不是被main函数调用,而是被系统所调用。

image-20250424230120772

代码应用

使用sa_handler

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

using namespace std;

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

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(1)
    {
        cout<<"I am a process: "<<getpid()<<endl;
        sleep(1);
    }
    return 0;
}

image-20250424231454700


问题1:什么时候将捕捉的信号的pending位图由1->0的呢?

思路:在handler信号捕捉执行完之前打印pending位图。

如果pending位图是 0000 0000 0000 0000 0000 0000 0000 0000

那么说明位图的改变是在捕捉信号之前

否则位图的改变就是在捕捉信号之后

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

using namespace std;

//问题1:pending位图,什么时候由1->0
//思路:在handler信号捕捉执行完之前打印pending位图。
//如果pending位图是 0000 0000 0000 0000 0000 0000 0000 0000
//那么说明位图的改变是在捕捉信号之前
//否则位图的改变就是在捕捉信号之后
void PrintPending()
{
    sigset_t set;
    sigemptyset(&set);
    sigpending(&set);
    for(int i=31;i>=1;i--)
    {
        if(sigismember(&set,i))
        {
            cout<<"1";
        }
        else
        {
            cout<<"0";
        }
    }
    cout<<endl;
}

void handler(int signo)
{
    PrintPending();
    cout<<"catch a signo: "<<signo<<endl;
}

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(1)
    {
        cout<<"I am a process: "<<getpid()<<endl;
        sleep(1);
    }

    return 0;
}

image-20250424232724787

结论:在处理信号之前,pending位图就已经由1->0了。


问题二:信号被处理的时候,对应的信号也会被添加到block表中,防止信号被嵌套捕捉!

思路:

在信号捕捉(处理)方法里面写一个死循环,

那么就一直是在捕捉信号的过程,

再来一个2号信号,因为2号信号的block位图是1,所以只能阻塞(不能递达),

所以2号信号的pending0->1

image-20250424234244352

结论:

当某个信号的处理函数被调用时,

内核自动将当前信号加入进程的信号屏蔽字,

防止该信号被嵌套捕捉,

当信号处理函数返回时自动恢复原来的信号屏蔽字,

这样就保证了在处理某个信号时,

如果这种信号再次产生,

那么它会被阻塞到当前处理结束为止。

在阻塞期间,该信号发送了n次,但是最终也只会被记录一次。


使用sa_mask

如果在调用信号处理函数时,

除了当前信号被自动屏蔽之外,

还希望自动屏蔽另外一些信号,

则用sa_mask字段说明这些需要额外屏蔽的信号,

当信号处理函数返回时自动恢复原来的信号屏蔽字。

对比实验:

没有手动屏蔽任何信号

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

using namespace std;

void PrintPending()
{
    sigset_t set;
    sigemptyset(&set);
    sigpending(&set);
    for (int i = 31; i >= 1; i--)
    {
        if (sigismember(&set, i))
        {
            cout << "1";
        }
        else
        {
            cout << "0";
        }
    }
    cout << endl;
}

void handler(int signo)
{
    cout << "catch a signo: " << signo << endl;
    while (1)
    {
        PrintPending();
        sleep(1);
    }
    //该循环就是在处理信号
}

int main()
{
    struct sigaction act, oact;
    memset(&act, 0, sizeof(act));
    memset(&oact, 0, sizeof(oact));
    
    // sigemptyset(&act.sa_mask);
    // sigaddset(&act.sa_mask,1);
    // sigaddset(&act.sa_mask,3);
    // sigaddset(&act.sa_mask,4);

    act.sa_handler = handler;//SIG_DFL SIG_IGN
    sigaction(2, &act, &oact);

    while (1)
    {
        cout << "I am a process: " << getpid() << endl;
        sleep(1);
    }
    return 0;
}

image-20250424235334636

手动屏蔽1,3,4号信号

sigemptyset(&act.sa_mask);
sigaddset(&act.sa_mask,1);
sigaddset(&act.sa_mask,3);
sigaddset(&act.sa_mask,4);

image-20250424235546122

sa_flags字段包含一些选项,本章的代码都把sa_flags设为0,

sa_sigaction是实时信号的处理函数。

可重入函数

可重入函数

main函数调用insert函数向一个链表head中插入节点node1,

插入操作分为两步,刚做完第一步的 时候,因为硬件中断使进程切换到内核,

再次回用户态之前检查到有信号待处理,于是切换到sighandler函数,

sighandler也调用insert函数向同一个链表head中插入节点node2,

插入操作的两步都做完之后从sighandler返回内核态,

再次回到用户态就从main函数调用的insert函数中继续往下执行,

先前做第一步之后被打断,现在继续做完第二步。

结果是,main函数和sighandler先后向链表中插入两个节点,

而最后只有一个节点真正插入链表中了。

image-20250417145534585

像上例这样,insert函数被不同的控制流程调用,、

有可能在第一次调用还没返回时就再次进入该函数,这称为:重入,

insert函数访问一个全局链表,有可能因为重入而造成错乱,像这样的函数称为:不可重入函数,

反之,如果一个函数只访问自己的局部变量或参数,则称为可重入(Reentrant) 函数。

如果一个函数符合以下条件之一则是不可重入的:

调用了mallocfree,因为malloc也是用全局链表来管理堆的。

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

volatile

保存内存可见性!!

该关键字在C当中我们已经有所涉猎,今天我们站在信号的角度重新理解一下

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

using namespace std;
int flag=0;

void handler(int signo)
{
    cout<<"catch a sig: "<<signo<<endl;
    flag=1;
}

int main()
{
    signal(2,handler);
    while(!flag);
    cout<<"process normal quit!"<<endl;

    return 0;
}

image-20250425200844784

标准情况下,键入 CTRL-C ,2号信号被捕捉,执行自定义动作,

修改 flag=1while 条件不满足,退出循环,进程退出。

因为main函数和handler函数是两个不同的执行流,

flag的值在main函数中没有修改flagmain函数只对flag进行了逻辑判断(检测),

算数运算/逻辑运算都是在cpu内部进行的,

所以在优化条件下,flag 变量可能直接被优化到寄存器中。

优化

image-20250425201818311

mysignal:mysignal.cc
	g++ -o $@ $^ -O3 -std=c++11
.PHONY:clean
clean:
	rm -rf mysignal

image-20250425201949053

优化情况下,键入 CTRL-C ,2号信号被捕捉,执行自定义动作,

修改 flag=1 ,但是 while 条件依旧满足,进程继续运行!

但是很明显flag肯定已经被修改了,但是为何循环依旧执行?

很明显, while 循环检查的flag,

并不是内存中最新的flag,这就存在了数据二异性的问题。

while 检测的flag其实已经因为优化,被放在了CPU寄存器当中。

image-20250425203237625

如何解决呢?很明显需要 volatile

volatile int flag=0;

image-20250425203332549

volatile 作用:防止过度优化,保持内存的可见性,

告知编译器,被该关键字修饰的变量,不允许被优化,

对该变量的任何操作,都必须在真实的内存中进行操作。

SIGCHLD信号

之前讲过用waitwaitpid函数清理僵尸进程,

父进程可以阻塞等待子进程结束,

也可以非阻塞地查询是否有子进程结束等待清理(也就是轮询的方式)。

采用第一种方式,父进程阻塞了就不能处理自己的工作了;

采用第二种方式,父进程在处理自己的工作的同时还要记得时不时地轮询一 下,程序实现复杂。

image-20250425203751546

其实,子进程在终止时会给父进程发SIGCHLD信号,

该信号的默认处理动作是忽略,父进程可以自定义SIGCHLD信号的处理函数,

这样父进程只需专心处理自己的工作,不必关心子进程了,

子进程终止时会通知父进程,父进程在信号处理函数中调用wait清理子进程即可。

验证:在子进程退出时,父进程会收到17号信号

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

using namespace std;

void handler(int signo)
{
    cout<<"I am "<<getpid()<<" catch a signal: "<<signo<<endl;
}

int main()
{
    signal(17,handler);
    pid_t id=fork();
    if(id==0)
    {
        while(1)
        {
            cout<<"I am child , pid: "<<getpid()<<" , ppid: "<<getppid()<<endl;
            sleep(1);
            break;
        }
        cout<<"child quit!!!!!!!!!!"<<endl;
        exit(1);
    }
    while(1)
    {
        cout<<"I am father , pid: "<<getpid()<<endl;
        sleep(1);
    }

    return 0;
}

image-20250425204758043

父进程在进行等待的时候,我们可以采用基于信号的方式异步等待。

等待的好处:

1.获取子进程的退出状态,释放子进程的僵尸。

虽然我们不知道父子进程谁先运行,但是我们知道父进程一定最后退出!

还是得调用 wait/waitpid 这样的接口!

并且要保证父进程是一直在运行的。(防止子进程变孤儿)

请编写一个程序完成以下功能:父进程fork出子进程,

子进程调用exit终止,父进程自定义SIGCHLD信号的处理函数,

在其中调用wait/waitpid等待子进程退出。

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

using namespace std;

void handler(int signo)
{
    sleep(3);
    pid_t rid=waitpid(-1,nullptr,0);
    cout<<"I am "<<getpid()<<" catch a signal: "<<signo<<" ,child quit: "<<rid<<endl;
}

int main()
{
    signal(17,handler);
    pid_t id=fork();
    if(id==0)
    {
        while(1)
        {
            cout<<"I am child , pid: "<<getpid()<<" , ppid: "<<getppid()<<endl;
            sleep(5);
            break;
        }
        cout<<"child quit!!!!!!!!!!"<<endl;
        exit(1);
    }
    while(1)
    {
        cout<<"I am father , pid: "<<getpid()<<endl;
        sleep(1);
    }

    return 0;
}

image-20250425205944924

如果我们有5个子进程,

同时退出?

那么在捕捉方法里设置一个循环,不断等待子进程退出即可。

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

using namespace std;

void handler(int signo)
{
    sleep(3);
    pid_t rid;
    while ((rid = waitpid(-1, nullptr, WNOHANG)) > 0)
    {
        cout << "I am " << getpid() << " catch a signal: " << signo << " ,child quit: " << rid << endl;
    }
}

int main()
{
    signal(17, handler);
    for (int i = 0; i < 5; i++)
    {
        pid_t id = fork();
        if (id == 0)
        {
            while (1)
            {
                cout << "I am child , pid: " << getpid() << " , ppid: " << getppid() << endl;
                sleep(5);
                break;
            }
            cout << "child quit!!!!!!!!!!" << endl;
            exit(1);
        }
    }
    while (1)
    {
        cout << "I am father , pid: " << getpid() << endl;
        sleep(1);
    }

    return 0;
}

image-20250425211328656

退出一半?

在等待的时候,使用非阻塞等待,就不会一直卡在没有退出的子进程上了。

#include <iostream>
#include <unistd.h>
#include <cstdlib>
#include <signal.h>
#include <cstring>
#include<ctime>
#include <sys/wait.h>
#include <sys/types.h>

using namespace std;

void handler(int signo)
{
    sleep(3);
    pid_t rid;
    while ((rid = waitpid(-1, nullptr, WNOHANG)) > 0)
    {
        cout << "I am " << getpid() << " catch a signal: " << signo << " ,child quit: " << rid << endl;
    }
}

int main()
{
    srand(time(nullptr));
    signal(17, handler);
    for (int i = 0; i < 5; i++)
    {
        pid_t id = fork();
        if (id == 0)
        {
            while (1)
            {
                cout << "I am child , pid: " << getpid() << " , ppid: " << getppid() << endl;
                sleep(5);
                break;
            }
            cout << "child quit!!!!!!!!!!" << endl;
            exit(1);
        }
        sleep(rand()%5+3);
    }
    while (1)
    {
        cout << "I am father , pid: " << getpid() << endl;
        sleep(1);
    }

    return 0;
}

image-20250425212208515

事实上,由于UNIX 的历史原因,要想不产生僵尸进程还有另外一种办法:

父进程调 用sigaction将SIGCHLD的处理动作置为SIG_IGN,

这样fork出来的子进程在终止时会自动清理掉,

不会产生僵尸进程,也不会通知父进程。

系统默认的忽略动作和用户用sigaction函数自定义的忽略

通常是没有区别的,但这是一个特例。

此方法对于Linux可用,但不保证在其它UNIX系统上都可用。

请编写程序验证这样做不会产生僵尸进程。

测试代码

#include <iostream>
#include <unistd.h>
#include <cstdlib>
#include <signal.h>
#include <cstring>
#include <ctime>
#include <sys/wait.h>
#include <sys/types.h>

using namespace std;

int main()
{
    signal(17, SIG_IGN);// SIG_DFL -> IGN
    for (int i = 0; i < 5; i++)
    {
        pid_t id = fork();
        if (id == 0)
        {
            while (1)
            {
                cout << "I am child , pid: " << getpid() << " , ppid: " << getppid() << endl;
                sleep(3);
                break;
            }
            cout << "child quit!!!!!!!!!!" << endl;
            exit(1);
        }
        sleep(1);
    }
    while (1)
    {
        cout << "I am father , pid: " << getpid() << endl;
        sleep(1);
    }
    return 0;
}

image-20250425213317158

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值