【Linux】信号的处理

目录

信号的处理

sigaction

volatile

SIGCHLD


我们之前说信号产生后OS向进程的PCB中写入信号,那么什么时候对信号进行处理呢?

就是进程从内核态切换回用户态的时候,信号会被检测并处理

信号的处理

这里就不得不提什么是内核态,什么是用户态,其实我们平常在调用系统调用的时候就会去执行OS的代码,这时我们就说进程陷入了内核,目前处于内核态。那有可能我们的代码不调用系统调用,那它怎么会进入内核态呢?不要忘记,进程是基于时间片进行调度的,当时间到了进程会从CPU中剥离下来,这时就是OS在执行操作,就是处于内核态。

所以基于上面的一些原理,我们就可以模拟出进程从内核态和用户态之间转换的图,就像下面一样:

这个图是信号处理方法被自定义时需要走的路线,如果只是默认处理或忽略信号时,只需要在内核态处理完直接杀死进程或修改pending位图返回用户态就够了,这里是进程需要切换回用户态执行用户写的方法,那为什么不能直接在内核态执行用户写的方法呢?肯定也是为了安全考虑,内核态的权限较高,用户有可能在自定义方法中越界访问一些信息

并且有些信号的默认处理动作是杀死进程(core、term),为什么OS不直接杀死进程,而要把信号写道pending位图中,等到合适的时候进程自己终止?因为进程可能在做一些很重要的工作,比如它申请了资源,直接杀死可能会导致资源泄露或数据不一致,所以OS必须允许进程进行必要的清理工作。

那为什么CPU在运行进程的时候永远都能找到操作系统呢?这个就得从进程地址空间说了,我们知道一个进程的地址空间有4G,其中0-3G都存的是关于进程有关的信息,剩下3-4G就是映射的OS,所以,一个进程要调用系统调用只需要在它自己的地址空间中跳转即可

这也就解释了为什么会有用户态和内核态,当执行用户级别的代码时,此时就是用户态,权限较低,当执行系统级别的代码时,此时就是内核态,权限较高。并且CPU中有个寄存器叫CS,这里边存的就是权限标识,为0就是内核态,为3就是用户态

操作系统是如何运行的呢?其实操作系统本身就是一个死循环,它需要不断接受来自外部的硬件中断来执行相应的操作。比如有周期性的时钟中断,信号技术本身就是通过软件的方式来模拟的硬件中断。

sigaction

我们之前说信号捕捉可以用signal,今天再介绍一种方法:

man sigaction

它还有一个同名结构体

第一个参数就是要捕捉哪个信号,第二、三个参数是传入一个结构体对象的指针,我们可以创建这个结构体对象,并修改对象的内容,那么我们具体来用一下

对于这个结构体创建的对象,我们只需要管第一个和第三个成员变量即可,其他的都给成0或nullptr。

第三个成员变量是当我们处理比如二号信号时,进程会默认阻塞二号信号,如果你还想阻塞其他的信号,你可以通过这个成员变量去告诉OS。那么处理信号的时候,内核就会自动阻塞这些信号,处理完,这些信号也会从信号屏蔽字中去除。

那为什么进程会默认阻塞当前正在处理的信号呢?因为如果我们目前正在处理二号信号,但是又来了一个二号信号,那我们就可能会嵌套处理二号信号,这是没有意义的,所以为了避免这种情况,操作系统就会默认阻塞。

我们也可以来证明一下进程在处理一个信号时会默认阻塞这个信号,我们的证明方法就是在处理比如2号信号时再发2号信号,然后打印pending位图

void printpending(sigset_t &pending)
{
    for (int i = 31; i > 0; i--)
    {
        if (sigismember(&pending, i))
            cout << "1";
        else
            cout << "0";
    }
    cout << endl;
}
void handler(int sig)
{
    cout << "signal: " << sig << endl;
    sigset_t pending;
    sigemptyset(&pending);

    while (1)
    {
        sigpending(&pending);
        printpending(pending);
    }
}
int main()
{
    struct sigaction act, oact;
    act.sa_handler = handler;
    act.sa_flags = 0;
    sigemptyset(&act.sa_mask);
    sigaction(2, &act, &oact);
    while (1)
        sleep(1);

    return 0;
}

我们可以看到二号信号确实被阻塞了

volatile

用这个关键字修饰变量就是告诉编译器不要把变量放到寄存器中,要一直从内存中读取。

因为如果我们编译时让编译器进行优化,那么它是有可能把变量放到寄存中的,比如

int g_flag=0;
void handler(int sig)
{
    cout<<"g_flag 0 -> 1"<<endl;
    g_flag=1;
}
int main()
{
    signal(2,handler);
    while(!g_flag);
    cout<<"process quit success"<<endl;
    return 0;
}

我们可以用下面的指令,让编译器进行优化

g++ test.cc -O1

这里必须是大写的O,小写的编译器会认为是形成哪个文件,后面的1可以是2可以是3,表示优化等级。

这个代码我们如果不优化的话,当进程收到2号信号就会改掉g_flag的值,从而程序退出。但是优化的话g_flag这个变量因为编译器检查到,下面没有对g_flag进行修改的地方,所以它就会被放到寄存器中,从而我们修改内存中的值也不会影响到寄存器,所以程序不会停止。

于是,在这种情况下,在初始化变量的时候我们就可以这样写:volatile int g_flag=0;这样就算优化也不会把g_flag放到寄存器中。

SIGCHLD

SIGCHLD是17号信号,子进程退出时会向父进程发送此信号。并且这个信号父进程的默认动作是忽略,所以之前我们没有发觉。

我们之前父进程等待子进程退出是主动等待,是阻塞式等待或非阻塞轮询等待,父进程就无法专心忙于自己的工作,但是在我们学习了这个信号后,父进程可以收到子进程信号后在处理信号时等待。

于是在面对同时有多个子进程且只有部分子进程退出时,我们可以选择循环非阻塞式等待,这样就可以保证父进程等待完所有要退出的子进程后就退出等待了。

void handler(int sig)
{
    if(sig==SIGCHLD)
    {
        while(1)
        {//-1表示等待任意子进程
            pid_t rid=waitpid(-1,nullptr,WNOHANG);
            if(rid>0)
            {
                cout<<"wait child success"<<endl;
            }
            else if(rid<=0)
            {
                break;
            }
            cout<<"wait sub process done"<<endl;
        }
    }
}

有时如果我们不想管子进程,但是不等待就会产生僵尸进程,所以这时Linux下有一种特殊处理,就是将SIGCHLD的处理动作改为SIG_IGN

这样子进程在退出后会自动清理掉,不会产生僵尸,也不会通知父进程。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值