当前位置: 首页 > article >正文

Linux-进程与信号

目录

第一节:信号是什么

第二节:信号处理

        2-1.默认处理

        2-2.忽略

        2-3.捕捉

第三节:信号产生

        3-1.命令产生信号

        3-2.进程产生命令

                3-2-1.kill 给其他进程发送信号

                3-2-2.raise 给自己发送信号

                3-2-3.alarm 隔一段时间给自己发送SIGALRM(14)信号

        3-3.操作系统自动产生信号

第四节:进程中信号的保存

        4-1.信号集介绍

                4-1-1.pending 未决信号集

                4-1-2.handler 信号处理函数集

                4-1-3.block 阻塞信号集

        4-2.获得/设置block

        4-3.获得pending

        4-4.位图操作

第五节:使用信号集

第六节:信号屏蔽

总结:


第一节:信号是什么

        信号是由操作系统直接产生的,向某个进程发送的一种事件,进程收到信号后,会对信号进行相应的处理

        一个进程也可以通过系统调用让操作系统产生信号,并把信号发送出去。

        使用 kill -l 命令可以查看所有信号:

        

         前面的序号是信号代表的值,后面是信号的名字,类似于:

#define SIGHUP 1

        所以它们是可以混用的。

第二节:信号处理

        2-1.默认处理

        进程收到信号后,会对信号进行处理,每个信号都有自己的默认处理方式,如果不对信号做任何设置,进程就会使用信号的默认处理。

        2-2.忽略

        忽略就是进程收到信号后不做任何处理(不处理实际上也是一种处理方式),有些信号的默认处理就是忽略。

        2-3.捕捉

        捕捉就是设置信号的自定义处理,即进程如何处理信号由程序员决定,当然也可以将信号的处理方式设置成忽略。

        需要注意的是不能捕获SIGKILL(9)信号,因为它是操作系统强制终止进程的信号,防止一些进行逃避终止。

第三节:信号产生

        信号是由操作系统直接产生的,我们也可以使用系统调用命令使操作系统产生某个信号。

接下来我们以SIGINT(2)信号为例子,改信号的默认处理是终止进程,但是会给进程执行必要清理工作的时间。

        3-1.命令产生信号

        先编写一个无限循环的程序,并运行起来:

#include <thread>
#include <iostream>
int main()
{
    while(true)
    {
        // 休眠2s
        std::this_thread::sleep_for(std::chrono::seconds(2));
        std::cout << "我是进程,我正在运行" << std::endl;
    }
    return 0;
}

        此时进程就会一直向屏幕打印信息,说明它是运行起来的:

        然后新打开一个会话,先输入命令:

ps axj | head -1 && ps axj | grep signal_test | grep -v grep

         其中的 signal_test 就是程序名,它的作用是查看查看上述程序产生的进程的信息:

       

        找到pid:3767755,然后使用如下命令向进程3767755发送SIGINT(2):

kill -2 3767755
// 或者
kill -SIGINT 3767755

        此时该进程就会停止打印了,而且进程的信息也没有了:

        

        3-2.进程产生命令

                3-2-1.kill 给其他进程发送信号

        使用 kill 系统调用可以让操作系统产生信号,它的第一个参数是目标进程的pid,第二个参数是发送哪个信号,我们让父进程产生SIGINT(2)信号使子进程终止:

#include <thread>
#include <iostream>
#include <unistd.h>
// 信号相关系统调用头文件
#include <sys/types.h>
#include <signal.h>
int main()
{
    // 创建子进程
    pid_t pid = fork();
    if(pid == -1)
    {
        std::cout << "子进程创建失败!" << std::endl;
    }
    else if(pid == 0) // 子进程
    {
        while(true)
        {
            // 休眠2s
            std::this_thread::sleep_for(std::chrono::seconds(2));
            std::cout << "我是子进程,我正在执行,pid:" << getpid() <<std::endl;
        }
    }
    else // 父进程
    {
        // 休眠10s后,向子进程发送2号信号
        std::this_thread::sleep_for(std::chrono::seconds(10));
        kill(pid,SIGINT);
    }
    return 0;
}

         编译并执行。

        刚开始的时候,两个进程都存在:

        10s后,子进程因为收到2号信号而终止了,父进程发完信号后也退出了:

        此时子进程打印了4次内容,第五次打印的时候被终止了:

                3-2-2.raise 给自己发送信号

#include <thread>
#include <iostream>
#include <unistd.h>
// 信号相关系统调用头文件
#include <sys/types.h>
#include <signal.h>
int main()
{
    while(true)
    {
        std::cout << "我是进程,我正在执行,pid:" << getpid() <<std::endl;
        // 给自己发送信号
        raise(SIGINT);
    }
    return 0;
}

  

        进程打印了一遍之后就收到2号信号而终止了。

                3-2-3.alarm 隔一段时间给自己发送SIGALRM(14)信号

         这个信号的默认处理是终止进程,但是它可被忽略或者捕获,那么就可以自定义处理,让进程在特定时间间隔后执行特定的任务。

        使用signal捕获14号信号,并设置特定任务函数:

#include <thread>
#include <iostream>
#include <unistd.h>
// 信号相关系统调用头文件
#include <sys/types.h>
#include <signal.h>
// 自定义处理
void handler(int sig)
{
    std::cout << "要起床了!!!" <<std::endl;
}
int main()
{
    // 捕获信号
    signal(14,&handler); // 第二个参数传入SIG_ING就是忽略信号
    // 设置闹钟:10s后提醒我起床
    alarm(10);

    //...执行其他任务
    std::this_thread::sleep_for(std::chrono::seconds(30));
    return 0;
}

        执行结果:

                                

        alarm设置的时间是10s,进程的退出时间是30s,但是只打印了一次,说明alarm只触发一次。如果要多次处理,做成类似起床闹钟的程序,就需要在handler中再次调用alarm:

void handler(int sig)
{
    std::cout << "要起床了!!!" <<std::endl;
    // 再次定时10s
    alarm(10);
}

  

        只要进程不退出,这个闹钟就会一直运行下去了。

        一个进程只能有一个alarm,之后设置的alarm会覆盖前面的alarm。

        使用 alarm(0) 可以取消闹钟,返回上一个闹钟的剩余时间。

        3-3.操作系统自动产生信号

        操作系统自己也会产生信号,加强对进程的管理,维持整个系统的稳定。

        进程产生异常时,系统就会向进程发送终止信号,使进程终止,这就是所谓的程序崩溃了。

        程序崩溃的原因:

        (1)除0错误:当程序除以0时,因为是除不尽的,结果的位数就会越来越多,CUP 寄存器的溢出标志位就会被占用,此时操作系统就会向进程发送SIGFPE(8)信号使进程终止。

        (2)非法访问内存:如果程序对野指针解引用并赋值,CPU的MMU会先查看这个地址是不是被这个程序使用的地址,如果不是,就会将这个异常地址放到CR2,操作系统识别到CR2中的内容后就会向对应进程发送SIGSEGV(11)信号使进程终止。

        上述两中信号都可以被忽略或捕捉,这样程序崩溃时不会退出,但是不建议这么做。

        如果这样做,异常进程会不断重复剥离、载入CUP的操作,操作系统也会不断地向异常进程发送信号。

        

第四节:进程中信号的保存

        进程执行信号处理的动作叫做信号递达,信号从产生到递达之前的状态叫做信号未决。

        

         进程可以选择性的阻塞某些信号,这些被阻塞的信号可以被进程接收到,但是永远无法递达,直到阻塞解除。

        

        所以进程有3张表保存信号的上述信息:pending,handler,block。它们都以位图的形式保存信号的信息

        4-1.信号集介绍

                4-1-1.pending 未决信号集

        它保存进程收到的、还未递达的信号,无论是否阻塞。收到的信号的对应比特位会置1,没有收到的信号置0。

        当一个信号被递达了,相应比特位又置0。

        子进程不会继承父进程的pending 。

                4-1-2.handler 信号处理函数集

        它保存信号的处理函数指针,一个pending中的未决信号要进行递达时,先会去handler获取信号的处理函数进行信号递达。

        子进程会拷贝一份父进程的handler。

                4-1-3.block 阻塞信号集

        它记录哪些信号被阻塞了,被阻塞的信号的对应比特位置1,被阻塞的信号永不递达,如果一个被阻塞信号到来,那么它在pending的比特位永远是1。

        子进程会拷贝一份父进程的block。

        3个信号集在父进程、子进程是各自私有一份的。

        4-2.获得/设置block

int sigprocmask(int how,const sigset_t* set,sigset_t* oset)

        how:对block的具体操作

                (1)SIG_SETMASK:用set覆盖block的原始位图(MASK)

                (2)SIG_BLOCK:在原始位图中添加新的阻塞,即:MASK=MASK|set

                (3)SIG_UNBLOCK:在原始位图中删除阻塞,即:MASK=MASK&~set

        set:输入型参数,程序员传入的位图,具体操作由how决定

        oset:输出型参数,返回修改之前的位图

        4-3.获得pending

int sigpending(sigset_t* oset)

         oset:输出型参数,返回pending的位图

        4-4.位图操作

        清空位图:

int sigempty(sigset_t* set);

         添加信号到位图:

int sigaddset(sigset_* set,int signum);

        判断一个信号是否已经被添加到位图中了,存在返回1,不存在返回0:

int sigismember(const sigset_t* set,int signum);

       

第五节:使用block信号集

        我们还是让父进程对子进程发送SIGINT(2)信号,不同的是子进程要对2号信号进行阻塞处理:

#include <thread>
#include <iostream>
#include <unistd.h>
// 信号相关系统调用头文件
#include <sys/types.h>
#include <signal.h>
int main()
{
    pid_t pid = fork();
    if(pid == -1)
    {
        std::cout << "子进程创建失败" << std::endl;
    }
    else if(pid == 0)
    {
        // 设置位图
        sigset_t set;
        sigaddset(&set,2); // 添加2号信号
        sigprocmask(SIG_BLOCK,&set,nullptr); // 添加2号信号到block中
        while(true)
        {
            std::cout << "我是子进程,我正在运行" << std::endl;
            std::this_thread::sleep_for(std::chrono::seconds(1));
        }
    }
    else
    {
        std::this_thread::sleep_for(std::chrono::seconds(3));
        // 向子进程发送2号信号
        kill(pid,2);
        // 防止子进程变成孤儿进程
        while(true)
        {
            std::this_thread::sleep_for(std::chrono::seconds(2));
        }
    }
}

  

        如果子进程没有阻塞2号信号,那么打印两三次就不会打印了,子进程却一直在打印,说明子进程阻塞了2号信号。

        此时要退出子进程不能使用ctrl+c,因为ctrl+c的实质是向前台进程发送2号命令,此时要查找子进程的pid,使用如下命令强制终止子进程:

kill -9 pid

        父进程没有阻塞2号信号,正常ctrl+c终止即可。

        因为被阻塞的信号仍然会保存在pending中,所以如果被阻塞的2号信号在之后解了阻塞,仍然会进行递达:

#include <thread>
#include <iostream>
#include <unistd.h>
// 信号相关系统调用头文件
#include <sys/types.h>
#include <signal.h>
int main()
{
    pid_t pid = fork();
    if(pid == -1)
    {
        std::cout << "子进程创建失败" << std::endl;
    }
    else if(pid == 0)
    {
        // 设置位图
        sigset_t set;
        sigaddset(&set,2); // 添加2号信号
        sigprocmask(SIG_BLOCK,&set,nullptr); // 添加2号信号到block中
        std::this_thread::sleep_for(std::chrono::seconds(5));
        // 解除2号阻塞
        sigprocmask(SIG_UNBLOCK,&set,nullptr);
    }
    else
    {
        // 父进程等待子进程捕获2号信号后再发送信号
        std::this_thread::sleep_for(std::chrono::seconds(3));
        // 向子进程发送2号信号
        kill(pid,2);
        // 防止子进程变成孤儿进程
        while(true)
        {
            std::this_thread::sleep_for(std::chrono::seconds(2));
        }
    }
}

         这样看到子进程的STAT变成Z了,意味着它变成僵尸进程了,说明子进程已经退出了,它在等待父进程退出,然后回收它自己。

第六节:信号屏蔽

        进程在以下几种情况下不会接收信号:

        (1)信号在pending,已经存在了。即进程只能记录收到了哪些信号,不能记录收到的信号的次数。

        (2)信号正在递达,此时无法收到相同类型的信号。比如进程收到2号信号,进行进程终止的默认处理,此时进程就不能接收其他的2号信号了。

         父进程同时向子进程发送4次2号信号,子进程的处理方式是打印信息:

#include <thread>
#include <iostream>
#include <unistd.h>
// 信号相关系统调用头文件
#include <sys/types.h>
#include <signal.h>
void handler(int)
{
    std::cout << "信号递达\n"; 
    std::this_thread::sleep_for(std::chrono::seconds(5));
}
int main()
{
    pid_t pid = fork();
    if(pid == -1)
    {
        std::cout << "子进程创建失败" << std::endl;
    }
    else if(pid == 0)
    {
        signal(2,handler);
        std::this_thread::sleep_for(std::chrono::seconds(10));
    }
    else
    {
        // 父进程等待子进程捕获2号信号后再发送信号
        std::this_thread::sleep_for(std::chrono::seconds(1));
        // 多次向子进程发送2号信号
        kill(pid,2);
        kill(pid,2);
        kill(pid,2);
        kill(pid,2);
        // 防止子进程变成孤儿进程
        std::this_thread::sleep_for(std::chrono::seconds(20));
    }
}

  

        结果只打印了一次信息,说明在2号信号递达时屏蔽了其他2号信号。 

总结:

        信号给了操作系统和用户一种向进程发送特定事件的方式,而且是不受进程正在执行的代码所影响的,这极大地提升了管理进程的灵活性。 


http://www.kler.cn/a/568705.html

相关文章:

  • 大白话React第十一章React 相关的高级特性以及在实际项目中的应用优化
  • 第50天:Web开发-JavaEE应用SpringBoot栈ActuatorSwaggerHeapDump提取自动化
  • shell脚本编程实践第4天
  • 【网络安全 | 渗透测试】GraphQL精讲一:基础知识
  • 如何通过Python网络爬虫技术应对复杂的反爬机制?
  • Bash Shell 比较注入漏洞:分析与利用
  • 初识flutter1
  • Java Stream 流笔记
  • 电子电气架构 --- AI在整车产品领域的应用
  • 基于SpringBoot + Vue的商城购物系统实战
  • 【vue-echarts】——05.柱状图
  • Python面向对象编程入门:从类与对象到方法与属性
  • JavaFunction的使用
  • AVX2指令集
  • 目前主流 AI 大模型体系全解析:架构、特点与应用
  • 【Python · PyTorch】循环神经网络 RNN(基础应用)
  • HashMap与HashTable的区别
  • JDBC 完全指南:掌握 Java 数据库交互的核心技术
  • leetcode 76. 最小覆盖子串
  • 基于专利合作地址匹配的数据构建区域协同矩阵