进程信号

· 浏览次数 : 7

小编点评

信号是操作系统用于进程间通信的一种机制。信号是一种软中断,用于通知进程某个事件已经发生,进程可以根据信号采取相应的行动。信号可以分为三类:普通信号、实时信号和特殊信号。 1. 普通信号(1-31号):进程可以根据信号编号(如1、2、3等)采取默认动作或自定义动作。默认动作通常是将进程终止,而自定义动作允许进程执行特定的函数。 2. 实时信号(34-64号):实时信号具有更高的优先级,用于处理紧急事件。与普通信号不同,实时信号不能被忽略。 3. 特殊信号(如SIGINT、SIGTERM等):这些信号用于控制进程的终止和其他操作。例如,Ctrl+C发送SIGINT信号以终止进程。 信号的产生可以通过以下几种方式: 1. 键盘发送:例如,使用Ctrl+c发送SIGINT信号。 2. 硬件产生:例如,除0操作和野指针异常会导致信号产生。 3. 软件产生:例如,管道关闭、闹钟和定时器等。 信号的处理可以通过以下几种方式: 1. 默认动作:进程收到信号后,执行默认动作,如终止进程。 2. 忽略动作:进程收到信号后,忽略信号,不执行任何操作。 3. 自定义动作:进程收到信号后,执行自定义函数,如捕获信号并执行特定操作。 信号的保存和处理可以通过以下几种方式: 1. 阻塞信号:信号可以被添加到信号集中,从而被阻塞。信号集是一个位图,用于表示进程收到的所有信号。 2. 信号的捕捉:在用户态和内核态之间切换,检查进程是否收到信号。根据信号编号,进程可以选择默认动作、忽略信号或执行自定义动作。 信号的传递和接收涉及到操作系统内核和进程控制块(PCB)。操作系统负责管理进程和信号,而进程控制块包含了进程的状态和资源信息。当进程收到信号时,操作系统会更新进程控制块的信号集,并根据信号编号采取相应的动作。

正文

1. 信号的产生

1.1 信号概念

在生活中有很多的信号在我们身边围绕,例如红绿灯,发令枪,上课铃等等

在接受到信号,我们可以做出三种动作 1.立马去做对应信号的事情 2.等一会再做,有自己的事情或者更重要的事情 3.直接忽视,不做

信号是给进程发送的 eg: kill -9 pid

进程本身是程序员编写的属性和逻辑的集合:所以进程有识别信号,认知到信号的功能

对于信号,进程必须要有保存信号的能力,就好比人记在脑子里一样

​ 由此可以引出对于信号,可以做出三种反应:

1.默认动作 2.忽略动作 3.自定义动作

  • 进程收到的信号保存在哪里呢 ?

如下图,可以发现信号有 1-31号(普通信号) ,34-64号(实时信号)

对于进程,收到一个信号时候,只有两种可能 : 是否收到信号,所以可以用0和1来表示是否收到信号

仔细观察则会发现信号有64个,那么我们可以用位图的形式用一个保存

进程收到的信号保存在pcb的一个变量里,用比特位来表示第几个信号

struct task_struct{
	......
	unsigned int singal;
	.......
}

给进程发送信号一定要经过系统调用,所以kill命令等都是OS提供的接口。

1.2 产生信号的方式

    1. 通过键盘发送

      在运行进程的时候,终止进程一般用Ctrl+c来终止当前进程,其实这个热键是OS进行对这个操作进行识别,然后再对对应的进程发送2号信号(SIGINT),从而终止当前进程

    1. 系统调用向目标进程发送信号

    int kill(pid_t pid, int sig) ;

    头文件#include <sys/types.h> #include <signal.h>

    功能: 对目标进程pid,发送sig号信号

    为此,我们可以实现一个自己的kill命令,eg:

    #include<iostream>
    #include<cstdio>
    #include<string>
    #include<unistd.h>
    #include<sys/types.h>
    #include<signal.h>
    void UsPage(const std::string& pro){
        //用户手册
        std::cout<<"\nplease input: "<<pro<<" pid signal"<<std::endl;
    }
    
    // 输入格式为  ./mysignal  pid  signal
    int main(int argc,char* argv[]){
    
        if(argc!=3){
            UsPage(argv[0]);
            exit(0);
        }
        int id=atoi(argv[1]);       //id为要杀死的进程的pid即为传的命令行的第二个参数
        int sign=atoi(argv[2]);     //对进程发送的信号sign
        int res=kill(id,sign);
        if(res==-1){
           perror("kill: ");//打印错误信息
           exit(-1);
        }
        return 0;
    }
    
    1. 硬件产生信号

(1) 除0操作

当在代码中有除0操作的时侯,底层实际上是硬件计算溢出,CPU会异常,此时会给进程发送异常的信号,发送

发生除0操作系统会抛以下异常:Floating point exception

man 7 signal 查看信号时候可以发现为 : SIGFPE 8 Core Floating point exception,为8号信号

(2) 野指针异常

如果发生对于一个无效指针的访问,实际是进程地址空间发生对野指针的访问异常,进程地址空间是由页表映射到物理内存的,其中包括了一个MMU(内存管理单元),如果异常,那么会给进程发送信号

野指针等越界错误会发生 Segmentation fault

man 7 signal 查看:SIGSEGV 11 Core Invalid memory reference

..........例子不止这两个,所以硬件也可以导致信号的产生

    1. 软件产生信号

**(1)管道 **

在之前的管道学习中,假如有一个管道 : 关闭管道的读端,但是写端没有关,那么此时也会产生信号 ,因为操作系统不允许任何的资源浪费,所以这种关闭读,写不关就会造成信号的产生,从而终止程序(大部分信号都会终止)

(2) 闹钟

int alarm(unsigned int num) ;//num秒后关闭进程

其实这个闹钟底层就是在num秒后给进程发送一个14号信号,然后num秒后终止

SIGALRM 14 Term Timer signal from alarm(2)

..........例子不止这两个

1.3 signal ()函数

​ signal()函数:

sighandler_t signal(int signum, sighandler_t handler);

​ 其中第二个参数是一个函数指针类型,类型为 (*func)(int)

typedef void (*func)(int);  //其中这个函数的参数为int型

​ 设置处理信号的功能

​ 指定使用sig指定的信号编号处理信号的方法。 参数func指定程序可以处理信号的三种方式之一:

​ 这个就是自定义动作,我们自己定义改进程在遇到signnum的信号的时候做什么操作

eg:

void catchSign(int signal){
	std::cout<<"catach signal:"<<signal<<std::endl;
	sleep(1);
}
int main(){
    signal(11,catchSign);  
    int arr[100];
    arr[10000000]=666;
    return 0;
}

signal只是声明,本来遇到第num号信号应该是按系统指定的操作

signal函数之后就把遇到该信号的时候,做出自己定义的操作了,即上边的catchSign函数

所以当遇到改变操作的函数的信号的时候,才会触发自定义操作,并不是声明signal就会触发,如果没有遇到该信号甚至不会触发自己的自定义操作。

1.4 核心转储问题

以上 31个普通信号,CoreTerm类型的都会造成进程终止,但是不同的是,Core会产生核心转储

在云服务器上如果进程是Core退出的,没有明显的现象可以观察到,这是因为云服务器关闭了Core file的文件大小

如果要更改有效文件大小可以使用命令:ulimit -c 1024,1024是设置的文件大小,可以设置为其他,-c是corefile 其他的也可以用后边那个-字母设置

将core file设置大小后,再次遇到造成core退出的进程,会在进程目录下产生一个名为core.xxx的文件

用gdb下输入: core-file core.xxx会出现以下内容 要生成debug的可执行程序才可以

则gdb会显示该进程异常的原因,该原因是野指针问题

2. 信号的保存

2.1 阻塞信号

首先要理解信号的保存要引入以下概念:

  1. 实际执行信号的处理过程称为递达
  2. 信号从产生到递达之间的状态称为未决
  3. 信号可以被阻塞
  4. 被阻塞的信号将处于未决状态,直到解除阻塞,才会执行递达的动作

所以信号是可以被阻塞的 , 也可以说某个信号可以被屏蔽

其实在pcb中信号采用位图的思想保存,这些所有的信号被称为信号集

pcb中信号的保存分为三个部分:

  1. 未决信号集
  2. 信号屏蔽集
  3. 信号集操作函数表

  • pending : 表示到达pcb中的未决信号
  • bolck : 表示当前进程屏蔽的信号
  • handler :其实就是一个函数指针数组,该数组是信号的处理方式,所以假设用户自定义了信号的处理方式,那么这个函数表就会把自定义函数的指针替换到函数表的特定位置

当屏蔽信号存在时,这个信号将不会被递达

pending : 00000000000000000000000

block : 00000000000000000000010 不会被递达

当屏蔽信号存在时,未决信号也存在,这个信号也不会被递达

pending : 00000000000000000000010

block : 00000000000000000000010 不会被递达

只有当未决信号存在,并且没有被屏蔽,才能被递达

pending : 00000000000000000000010

block : 00000000000000000000000 递达

2.2 信号的捕捉

2.2.1 用户态和内核态

首先要理解信号的捕捉流程需要理解以下两个内容:

用户态和内核态

在操作系统中,内存以及硬件等资源是由操作系统所维护的,正所谓操作系统不相信任何人,当我们进行申请系统资源的时候,其实进程会切换到内核态去进行一系列操作,从而申请资源

实际执行系统调用的人是”进程“,但是身份是内核 , 系统调用往往比较费时间所以尽量少的进行系统调用

那么切换状态的过程是什么呢?

  1. 在之前的进程地址空间中,了解到了用户有0-3G的地址空间,进程地址空间通过页表映射到物理内存中,从而实现cpu与进程交互

  2. 操作系统在开机时候也会加载到内存中,管理着整个电脑,整个问题毋庸置疑,所以操作系统也在内存中

  3. 对于每个进程,剩余的3-4G地址空间,其实就是内核态的进程地址空间,称为内核空间,整个进程地址空间映射到物理内存中的操作系统的物理内存区。

  4. 由于每个进程固定的3-4G都是内核空间,而且操作系统只有一个,所以对于每个进程都会通过内核级页表映射到一个操作系统的物理内存处,内核级页表一个就够了。

  5. 当进程通过系统调用时候,系统调用会发出一个陷入指令,让cpu的CR3寄存器(该寄存器记录当前进程的运行级别0代表内核态,3代表用户态)标记为内核态,从而变为内核态,所以在用户态进行系统调用的时候,切换身份并且跳转在内核空间中执行有关操作,执行完后会发送指令再将CR3寄存器标记为用户态

2.2.2 信号的捕捉流程

信号的捕捉处理是在内核态返回用户态的时候进行的

首先我一定是进入了内核态,进入内核态的原因有很多 : 中断,系统调用,进程切换等。

当要返回到用户态时,既然进入内核态那么不容易,所以索性去检查一下进程是否收到了信号,因为是在内核态,所以pcb访问轻轻松松,然后检查是否有需要递达的信号

对于信号的处理有三种方式 : 1.默认动作 2.忽略动作 3.自定义动作

对于不同的处理方式也会有不同的流程

  • 默认动作和忽略动作

  • 自定义动作,假设这个自定义动作没有使用进程退出,假设是一个自动修补漏洞并且继续执行的处理

3. 信号集操作函数

pending和bolck都是由一个信号集组成的,也就是位图的思想类型为sigset_t

  typedef struct {
  	unsigned long sig[_NSIG_WORDS];
  } sigset_t;

所以定义一个信号集,可以 sigset block;

下边介绍对信号集的有关函数:

#include <signal.h>
 int sigemptyset(sigset_t *set);
//初始化信号集,全置为0
 int sigfillset(sigset_t *set);
//初始化信号集,使所有的信号都有,即为全1
 int sigaddset (sigset_t *set, int signo);
//将信号集中添加signo号信号,即将set信号集中signo信号的位置置为1
 int sigdelset(sigset_t *set, int signo);
//将set信号集中的signo号信号由1置为0,即去掉
 int sigismember(const sigset_t *set, int signo);
//判断signo信号是否在set中出现,如果没有返回0,有的话返回1

sigprocmask函数 :

调用sigpromask函数可以读取或者更改当前进程中屏蔽信号集

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

参数:

  • how : 当前想进行的操作类型,1. SIG_BLOCK 2. SIG_UNBLOCK 3. SIG_SETMASK

​ SIG_BLOCK : 希望添加的屏蔽字的信号,即 mask = mask|set

​ SIG_UNBLOCK : 希望解除屏蔽字的信号,即可mask = mask | ~set

​ SIG_SETMASK : 将当前进程的屏蔽字设置为set所指向的值

  • set: 输入型参数,根据how传给函数的信号集
  • oset : 输出型参数 ,将没有被更改之前的信号集保存到oset所指向的信号集

sigpending函数:读取当前进程的未决信号集,通过set参数传出

#include <signal.h>
int sigpending(sigset_t *set);
//读取当前进程的未决信号集,通过set参数传出。调用成功则返回0,出错则返回-1。 

下面用刚学的几个函数做个实验。程序如下:

#include<iostream>
#include<vector>
#include<string>
#include<unistd.h>
#include<signal.h>

using namespace std;

vector<int> Blocks={2};//想屏蔽多少,假如数组中即可

string show_pending(sigset_t& pending){
    string s;
    int cnt=1;
    for(int i=31;i>=1;i--){
        //打印31个信号集
        if(sigismember(&pending,i)){
            //sigismember比较传参的信号集是否有第i个信号,有返回1,无返回0
            s+='1';
        }
        else s+='0';
        if(cnt++%4==0) s+=' ';
    }
    
    return s;
}


void hander(int sign){
    //自定义捕捉函数
    cout<<"捕捉到信号"<<sign<<endl;
}

int main(){
    //1.初始化信号集
    sigset_t block,oblock,pending;
    sigemptyset(&block);
    sigemptyset(&oblock);
    sigemptyset(&pending);
    //2.阻塞信号
    for(const auto& e:Blocks) sigaddset(&block,e);
    //3.设置屏蔽的信号
    sigprocmask(SIG_SETMASK,&block,&oblock);
    int num=1;

    for(const auto&e:Blocks) signal(e,hander);

    while(true){
        //获取当前进程的pending表
        sigpending(&pending);
        //打印当前进程表
        cout<<show_pending(pending)<<endl;
        sleep(1);

        if(num++==5){
            //假设5秒后取消阻塞
            sigprocmask(SIG_SETMASK,&oblock,&block);
            //oblock就是保存的原来的,代表取消屏蔽,为了安全,将原来的block备份一下
        }
    }
    return 0;
}

与进程信号相似的内容:

进程信号

1. 信号的产生 1.1 信号概念 在生活中有很多的信号在我们身边围绕,例如红绿灯,发令枪,上课铃等等 在接受到信号,我们可以做出三种动作 1.立马去做对应信号的事情 2.等一会再做,有自己的事情或者更重要的事情 3.直接忽视,不做 信号是给进程发送的 eg: kill -9 pid 进程本身是程序

kill 进程时遇到的一件有意思的事情

案例现象 一般来讲,我们在 kill 掉一个进程的时候通常有两个选择: 找到进程的 pid 号,然后执行 kill 命令 找到进程的名字,然后执行 pkill 命令 pkill 和 kill 命令都是向指定的进程发送信号,从而完成终结进程的操作,主要区别在于 pkill 命令与 pgrep 配套使用

[转帖]kill 参数表

https://www.cnblogs.com/alix-1988/p/14331898.html kill命令格式:kill -Signal pidpid是进程号,可以用 ps 命令查出 signal是发送给进程的信号, 默认参数下,kill 发送SIGTERM(15)信号给进程,告诉进程,你需要

深入探讨进程间通信的重要性:理解不同的通信机制(上)

本文旨在探讨进程间通信的重要性,并介绍了不同的通信机制,如管道、消息队列、共享内存、信号量、信号和套接字。通过理解这些通信机制的特点和应用场景,可以更好地实现进程间的高效数据共享。同时,本文还强调了同步和互斥机制的重要性,以确保数据的一致性和正确性。最后,还介绍了套接字作为一种跨网络和同一主机上进程间通信的通信机制,为读者提供了更全面的了解。通过阅读本文,读者将能够深入理解进程间通信的概念和不同机制,为实现有效的数据共享提供指导。

深入探讨进程间通信的重要性:理解不同的通信机制(下)

本文旨在探讨进程间通信的重要性,并介绍了不同的通信机制,如管道、消息队列、共享内存、信号量、信号和套接字。通过理解这些通信机制的特点和应用场景,可以更好地实现进程间的高效数据共享。同时,本文还强调了同步和互斥机制的重要性,以确保数据的一致性和正确性。最后,还介绍了套接字作为一种跨网络和同一主机上进程间通信的通信机制,为读者提供了更全面的了解。通过阅读本文,读者将能够深入理解进程间通信的概念和不同机制,为实现有效的数据共享提供指导。

如何优雅地退出程序

在Linux系统中执行操作时,进程可以通过发送和接收信号与其他进程进行通信。信号是用于通知进程发生特定事件或请求进程采取特定动作的软件中断。 以下是Linux系统中常见的一些信号及其含义: 1. **SIGINT (2)**:中断信号,通常由终端用户按下`Ctrl + C`发送给前台进程。该信号用于

[转帖] Strace的介绍与使用

https://www.cnblogs.com/skandbug/p/16264609.html Strace简介 strace命令是一个集诊断、调试、统计于一体的工具,常用来跟踪进程执行时的系统调用和所接收的信号,我们可以用它来监控用户空间进程和内核的交互。如对应用程序的系统调用、信号传递与进程状

[转帖]Linux下strace调试系统应用参数总结(附实例操作讲解)

文章目录 一、简介二、常用参数详解三、实例详解3.1 跟踪具体进程3.2 监控具体程序执行过程 四、其他相关知识链接 一、简介 strace命令是一个集诊断、调试、统计与一体的Linux 用户空间跟踪器,我们可以使用strace对应用的系统调用、信号传递和进程状态变更的监控结果来对应用进行分析,以达

[转帖]Linux下strace调试系统应用参数总结(附实例操作讲解)

文章目录 一、简介二、常用参数详解三、实例详解3.1 跟踪具体进程3.2 监控具体程序执行过程 四、其他相关知识链接 一、简介 strace命令是一个集诊断、调试、统计与一体的Linux 用户空间跟踪器,我们可以使用strace对应用的系统调用、信号传递和进程状态变更的监控结果来对应用进行分析,以达

[转帖]016 Linux 卧槽,看懂进程信息也不难嘛?top、ps

016 Linux 卧槽,看懂进程信息也不难嘛?top、pshttps://my.oschina.net/u/3113381/blog/5455267 1 扒开看看 top 命令参数详情 Linux top 命令用于实时显示 process 的动态详情。 第一行,[top -] 任务队列信息 系统时