首页 > 系统相关 >进程信号

进程信号

时间:2023-10-18 20:01:55浏览次数:26  
标签:函数 sigset int 发送 信号 进程


信号


什么是信号

用户或者操作系统通过发送一定的信号,通知进程,让进程做出相应的处理,这就是信号

进程要处理信号,必须要具有识别他的能力

信号产生之后,进程可以找个时间进行处理,不需要立即进行处理——那么此时我们就要记录下来这个信号——记录这个信号我们可以用位图结构

常见的信号:

进程信号_#include


1到31为普通信号

34到64为实时信号

每个信号其实就是一个宏,它有自己对应的值

进程信号_用户态_02


这里的Core 为核心转储

信号如何产生


键盘产生

进程信号_自定义_03


核心转储

我们在学习进程等待的时候,当一个进程被杀死的时候,第8位为core dump标志,为是发生核心转储。

进程信号_#include_04


一般而言,云服务器(生产环境)的核心转储功能是被关闭的

用命令ulimit -a进行查看,可以用ulimit -c 数字进行修改

它会产生一个文件,文件名为core.进程pid主要是为了调试

进程信号_#include_05


核心转储演示:

int main()
{
    pid_t pid=fork();
    if(pid==0)
    {
        int i=0;

        while(true)
        {
            cout<<"我是子进程:"<<getpid()<<endl;
            sleep(1);

            if(i==10)
            {
                int a=1;
                a/=0;
            }
            ++i;
        }
    }
    int stat;
    waitpid(pid,&stat,0);
    //提取code dump
    cout<<"我是父进程:"<<getpid()<<"是否发生核心转储:"<<((stat>>7)&1)<<endl;
    return 0;
}

就会发现有这个,除0,发送的8号信号,浮点错误,行为会发送核心转储。

进程信号_#include_06


调用系统函数向进程发信号

当我们在命令行上输入kill命令的时候,其实是调用的kill函数实现的,kill函数可以向指定进程发送信号。

进程信号_用户态_07


成功返回0,失败返回-1。

模拟一个像命令一样的程序

int main(int argc, char *argv[])
{
    if(argc!=3)
    {
        cout<<"命令输入有误"<<endl;
        exit(1);
    }
    kill(atoi(argv[2]),atoi(argv[1]));
    return 0;
}

还有raise函数

进程信号_用户态_08


它的作用就是给当前进程发送信号

还有一个abort函数

进程信号_#include_09


就像exit函数一样,abort函数其实发送的是6号信号。

如何理解系统调用接口向进程发送信号:


由软件条件产生信号

在管道中,如果读端不读而且关闭,写端一直写,那么就会被os会自动终止写端进程,发送的是SIGPIPE信号
下面就进行验证:

int main()
{
    int fd[2];
    int ret=pipe(fd);
    if(ret!=0)
    exit(-1);
    pid_t pid=fork();

    //child
    if (pid == 0)
    {
        close(fd[0]);
        for (int i = 0; i < 100000; i++)
            write(fd[1], "h", 1);
        exit(0);
    }

    close(fd[0]);
    close(fd[1]);
    int stat;
    waitpid(pid,&stat,0);
    cout<<"退出信号:"<< (stat&0x0000007f)<<endl;
    return 0;
}

看运行的结果:

[lighthouse@VM-4-8-centos 信号]$ ./signal退出信号:13

为什么是父进程读取呢?如果是父进程写,子进程读。因为子进程把读端关闭,父进程写没有意义,就会把父进程终止,那么我们无法拿到信号,而反过来就可以的。

还有一种由软件异常产生的信号,就是时钟信号,当时间到了,系统发送时钟信号SIGALRM14号信号。

该函数

进程信号_自定义_10


这个是设置的是秒级别的定时器。

如何理解软件条件给进程发送信号?

  1. os先识别某种软件条件触发或者不满足
  2. os构建信号,发送给指定的进程

硬件异常产生信号

除0发生的异常以及野指针、越界问题导致的硬件异常。
除0发送的是8号信号,越界、野指针发送的是11号信号

在cpu中有一个标志寄存器,当发生除0错误的时候,在寄存器中会进行标记,os会自动识别检测。当识别出有问题的时候,os会向当前进程发送信号,进程在合适的时候,进行处理

如何理解野指针、越界我问题导致的硬件异常?

  1. 都必须通过地址,找出目标位置
  2. 语言上面的地址,全部都是虚拟地址
  3. 将虚拟地址转换成物理地址,需要用到页表+MMU(这是一个硬件,分页内存管理单元)
  4. 转换的时候,MMU会发送异常,导致报错。

所有的信号,有他的来源,但最终全部被OS识别,解释,并发送的!

信号的处理

信号处理的常见方式:

  1. 默认的,进程自带的,也就是程序员写好的逻辑
  2. 忽略
  3. 自定义动作(捕捉信号)

对于自定义动作的演示

#include <iostream>
#include <signal.h>
#include <unistd.h>
using namespace std;

void handler(int signum)
{
    cout<<"获得一个信号:"<<signum<<endl;
}

int main()
{
    for(int i=1;i<32;i++)
    {
        signal(i,handler); 
    }
    

    while(true)
    {
        cout<<"hello world "<<getpid()<<endl;
        sleep(1);
    }
    return 0;
}

看似是把所有的信号都自定义捕捉了,但是当我们发生9号信号的,依然可以终止进程,os是不可能让一个进程无法终止的。


阻塞信号


一些常见的概念:

信号递达:执行信号的处理动作
信号未决(Pending):信号产生到递达之间的状态
阻塞:阻塞就是进销存阻塞该信号——阻塞信号集也叫做信号屏蔽字

当一个信号被阻塞的时候,当产生这个信号的时候,那么这个信号一直处于未决状态,直到进程解除对此信号的阻塞,才能执行递达的动作。阻塞和忽略是不同的,阻塞之后信号就不会被递达,而忽略是递达之后的一种处理动作


信号在内核中的样子:

进程信号_#include_11


在block中,0表示没有阻塞,1表示阻塞

在pending中,0表示没有接收到信号,1表示接受到信号

hander表示处理动作——忽略,默认,自定义


sigset_t类型

该类型不允许用户自己进行位的操作,os会给我们提供对应的操作位图的方法sigset_t一定需要对应的系统接口,来完成对应发功能。

像block,pending这样的信号我们用sigset_t来存储,sigset_t称为信号集,他的本质也就是一个位图结构。
对于这个类型的结构我们要学会怎么使用它,我们不需要关注它内部是怎么实现的。
下面是操作它的几个函数:

#include <signal.h>
int sigemptyset(sigset_t *set);//初始化,都清0
int sigfillset(sigset_t *set);
int sigaddset (sigset_t *set, int signo);//把信号添加到信号集中
int sigdelset(sigset_t *set, int signo);//从信号集中删除该信号
int sigismember(const sigset_t *set, int signo);//检测信号集中是否包含该信号

上面这些函数虽然操作信号了,但是并没有在系统的角度设计信号,下面介绍几个系统接口来设置信号。
sigprocmask

#include <signal.h>
int sigpromask(int how,const sigset_t* set,sigset_t* oset);

how的参数选择:SIG_BLOCK,把set里面的信号写入到系统中进行屏蔽SIG_UNBLOCK,把set里面的信号从系统中解除屏蔽SIG_SETMASK,把系统的屏蔽字设置成set指向的set为我们设置的信号屏蔽字,oset为我们旧的信号屏蔽字。对于返回值,成功为0,失败为-1。

sigpending

#include <signal.h>
int sigpending(sigset_t *set);

函数 sigpending 用于获取当前被阻塞且未决的信号集。这个函数可以用来查询那些在当前进程中被阻塞但尚未处理的信号。

进程信号_#include_12

返回值:成功返回0,失败返回-1。


信号的保存

进程信号_用户态_13


脚本发送信号

信号发送的本质就是:OS向目标进程写信号,OS直接修改pcb中的指定位图结构,完成信号发送的过程

进程信号_#include_14


进程信号_自定义_15


进程信号_#include_16


捕捉信号

信号产生之后,信号可能无法被立即处理,在合适的时候会被处理。

  1. 在合适的时候(是什么时候?)
  2. 信号处理的整个流程

下面对这两个问题进行回答:

  1. 与信号相关的数据字段都是在进程的PCB内部的,当接收到一个信号的时候,会从用户态切换到内核态,进行修改内核中有关信号的数据结构,之后会从内核态再次切换到用户态,在切换的时候(内核->用户),就会对信号进行检测和处理!因为此时已经处理好系统的各种逻辑了。
  2. 在CPU中也有2套,1套是可见的,一套是不可见的,用来自用的,这个自用的里面,其中有有关CR3的寄存器表示当前cpu的执行权限——1为内核,3为用户

进程信号_用户态_17


内核是如何实现信号捕捉的?

  • 捕捉信号

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

信号捕捉的一个流程:

  1. 进程在执行的时候,由于中断、异常或者系统调用等原因进入内核开始执行代码
  2. 内核开始执行自己的代码,当处理完成的时候,准备返回用户态的时候
  3. 在返回用户态的时候,就会进行信号的处理,检查pending位图中,是否存在信号,如果不存在信号,之间返回用户态;如果存在,在去看它的block位图是否存在被阻塞,如果阻塞,那么也直接返回用户态;如果不阻塞,就进行信号的处理,信号处理有3种——忽略,默认,自定义捕捉。如果为忽略,把pending位图置成0,然后直接进行返回到用户层;如果为默认,把pending位图置成0,执行它的默认动作,如果有核心转储,就进行核心转储,然后杀掉进程;如果是自定义①,

①:因为自定义的代码在用户态,那么内核态可以执行用户态的代码吗?——是可以的,但是我们不能去执行,因为用户写的代码可以存在非法的情况,必须要切换到用户态去执行自定义捕捉的代码。在执行自定义代码的时候,在返回的时候会执行特殊的系统调用,然后再次进入内核。最后再从内核返回用户,执行用户的代码,在返回的时候还要在做检查,如果有其他信号,就按照上面的方式继续进行信号的处理;如果没有可以返回用户态了。

上面的信号处理的逻辑可以用下面的简图来解释:

进程信号_自定义_18


sigaction函数

#include <signal.h>
int sigaction(int signo, const struct sigaction *act, struct sigaction *oact);

sigaction函数可以读取和修改与指定信号相关的处理动作。调用成功返回0,失败返回-1。
signo是指定的信号编号。 struct sigaction是一个结构体类型,用于定义信号处理程序的属性和行为 下面是这个结构体的内容

struct sigaction {
   void     (*sa_handler)(int);//信号处理动作
   void     (*sa_sigaction)(int, siginfo_t *, void *);
   sigset_t   sa_mask;//信号屏蔽字
   int        sa_flags;
   void     (*sa_restorer)(void);
};

在这里,我们只需要了解void (*sa_handler)(int);sigset_t sa_mas即可,act为需要修改的信号动作,oact为旧的,可以联想sigpromask

处理信号的时候,执行自定义动作,如果在处理信号期间,又来了同样的信号,os将怎么办呢?

当某个信号的处理函数被调用时,内核自动将当前信号加入到进程的信号屏蔽字,处理完成后解除。这样就可以保证在处理该信号的时候,再次来相同的信号,就会被阻塞到当前处理结束为止。

验证一下:

#include <iostream>
#include <signal.h>
#include <sys/types.h>
#include <unistd.h>
using namespace std;

void printsig(sigset_t &set)
{
    for(int i=1;i<=31;i++)
    {
        if(sigismember(&set,i))
        cout<<1;
        else
        cout<<0;
    }
    cout<<endl;
}
void handler(int sig)
{
    cout<<"这是"<<sig<<"号信号"<<endl;
    sigset_t pending;
    int c=0;
    while(true)
    {
        sigpending(&pending);
        printsig(pending);
        c++;
        if(c==10)
        break;
        sleep(1);
    }


}
int main()
{
    struct sigaction act,oact;
    act.sa_handler=handler;
    sigemptyset(&act.sa_mask);

    sigaddset(&act.sa_mask,2);
    sigaddset(&act.sa_mask,3);
    sigaddset(&act.sa_mask,4);
    sigaddset(&act.sa_mask,5);
    //把2号设置到当前进程的pcb中,其中2号进程为自定义捕捉动作;3,4,5为默认的动作
    sigaction(2,&act,&oact);

    cout<<"进程pid:"<<getpid()<<endl;
    while(true)
    {
        ;
    }
    return 0;
}


可重入函数

什么叫做重入函数:同一个函数被多个执行流进入,那么这个函数就是重入函数
可以让多个执行流进入的重入函数叫做可重入函数;不可以让多个执行流进入的,叫做不可重入函数。
为什么不可以让多个执行流进入,就是因为不出现不好的结果。
可重入、不可重入是函数的一种特征。
比如一个函数实现链表的头插,那么这个函数就是不可重入函数


volatile

c语言中的关键字volatile,它的作用就是保持内存的可见性,告知编译器,被该关键字修饰的变量,不允许被优化,对该变量的任何操作,都必须在内存级别进行操作。
下面进行验证一下:

int flag=0;

void hendler(int sig)
{
    cout<<"flag的值进行进行修改"<<flag;
    flag=1;
    cout<<"->"<<flag<<endl;
}


int main()
{
    signal(2,hendler);
    while(!flag);

    cout<<"flag的最终值:"<<flag<<endl;
    return 0;
}

当我们对上面的代码进行普通的编译g++ -o $@ $^ -std=c++11它的运行结果如下:

[ml@VM-4-8-centos 信号]$ ./mysignal 
^Cflag的值进行进行修改0->1
flag的最终值:1

当我们加入优化选项的时候g++ -o $@ $^ -std=c++11 -O3它的运行结果如下:

[ml@VM-4-8-centos 信号]$ ./mysignal 
^Cflag的值进行进行修改0->1
^Cflag的值进行进行修改1->1
^Cflag的值进行进行修改1->1

接收2号信号的时候,会陷入死循环
为什么会出现这种情况呢?

当加入优化选项之后,在循环中flag的值会存放在寄存器中,而改变flag值的时候,直接改的是内存中flag的值,寄存器中的值是木有改变的。所以会一直进入循环。下面我们对flag加上关键字volatile的时候,对变量使用会从内存中进行找。下面是代码改变的部分volatile int flag=0;,还是按照刚才的优化进行编译。结果如下:

[ml@VM-4-8-centos 信号]$ ./mysignal 
^Cflag的值进行进行修改0->1
flag的最终值:1


SIGCHLD信号

当子进程退出或者被终止的时候,会给父进程发送SIGCHLD信号,该信号的动作是忽略——让子进程陷入僵尸状态。父进程可以自己定义SIGCHLD信号的处理函数,如果自己设置忽略,就会让子进程退出,和系统的忽略还是不一样的。

标签:函数,sigset,int,发送,信号,进程
From: https://blog.51cto.com/u_15869810/7922500

相关文章

  • Linux进程间通信
    因为进程间具有独立性,你们想用进行进程间通信,难度还是比较大的。进程间通信的本质就是让不同的进程看到同一份资源。为什么要进行进程间通信——交互数据、控制、通知等目标进程间通信的技术背景进程是具有独立性的。虚拟地址空间+页表保证进程运行的独立性(进程内核数据结构+进程......
  • 服务器如何终止进程
    先在命令行中输入psaux显示服务器全部的进程,找到自己想要终止的进程 PID为进程的ID比如终止ID为143508这个进程kill143508就可以终止这个进程了!!!......
  • 信号隔离的利器:光耦合器的应用与重要性| 百能云芯
    在当今数字化和电子技术的时代,各种电子设备和电路在我们的日常生活和工作中扮演着至关重要的角色。为了使这些设备正常运行并确保它们之间的相互作用,一种叫做光耦合器(CTR)的元件扮演着重要的连接桥梁角色。接下来云芯将带您深入探讨光耦合器的工作原理、应用领域以及其在电子世......
  • 深度学习驱动的交通信号灯检测与识别:实现智能化道路交通管理【人工智能实战】
    随着人工智能的快速发展,基于深度学习的视觉算法在道路交通领域中起到了重要作用。本文将探讨如何利用深度学习技术实现道路交通信号灯的检测与识别,通过多处代码实例展示技术深度。道路交通信号灯是指示交通参与者行驶和停止的重要信号。准确地检测和识别交通信号灯对于智能交通系统......
  • delphi判断进程和杀进程函数
    functionTMainForm.CheckTask(ExeFileName:string):Boolean;//检测XX进程是否存在函数const PROCESS_TERMINATE=$0001;var ContinueLoop:BOOL; FSnapshotHandle:THandle; FProcessEntry32:TProcessEntry32;begin result:=False; FSnapshotHandle:=Create......
  • Cef笔记:进程间通信
    原文出处:https://bitbucket.org/chromiumembedded/cef/wiki/GeneralUsage#markdown-header-threadsInter-ProcessCommunication(IPC)SinceCEF3runsinmultipleprocessesitisnecessarytoprovidemechanismsforcommunicatingbetweenthoseprocesses.CefBrowserand......
  • 工程监测仪器振弦传感器信号转换器在桥梁安全监测中的重要性
    工程监测仪器振弦传感器信号转换器在桥梁安全监测中的重要性桥梁是人类社会建设过程中最重要的交通基础设施之一,对于保障人民出行、促进经济发展具有极其重要的作用。由于桥梁结构在长期使用过程中受到环境因素和负荷的影响,会逐渐发生变形和损伤,因此对桥梁进行安全监测和评估显得......
  • 进程
    进程的基本概念同一个程序3个进程同时进行(比如同时打开3个QQ),它们的PID也是不一样的。同一个程序执行一次进程,然后结束掉这个进程,然后再执行一次进程,则两次进程的PID是不一样的进程的状态转换运行态到就绪态:比如CPU收到一个时钟中断的信号,CPU会让当前进程下CPU,该进......
  • 实现多任务之进程与线程
    进程与线程一、多任务概念1、举个栗子比如在网盘下载资料的时候,为什么要多个资料同时下载?答:多个任务同时下载可以大大提高程序执行的效率。多任务的最大好处就是充分利用好CPU资源,提高程序的执行效率。2、什么是多任务多任务是指同一时间内执行多个任务。例如:现在安装的......
  • 信号波形测出来有问题?竟是示波器接地探头接错了,不能这么瞎搞
    原创:卧龙会关羽兄弟||前言从事硬件测试好多年,但是纯硬件测试的文章相对于来说讲的和写得很少。这也是为人诟病的一个地方,经常搞些看起来高大上的东西,搞搞热,弄弄噪声,调调电源,改改电磁等等与硬件测试几乎不沾边的事情。但是,没办法啊!人在屋檐下,岂能不低头。硬件测试是硬件部下边......