进程信号

24 篇文章 0 订阅
7 篇文章 0 订阅

1.进程信号概念

信号是一个软件中断,通知进程某个事件发生了异步事件,打断进程当前的操作,去处理这个事件,信号是多种多样的,并且一个信号对应一个事件,这样才能做到进程收到一个信号后,知道是什么事件,应该如何去处理

1.1 信号的查看

使用 kill -l 命令查看信号种类
在这里插入图片描述
信号种类62种,其中,
1-31号信号都是非可靠信号(从unix借鉴而来,每个信号都有具体对应的系统事件,有可能会信号丢失
34-64号信号 都是可靠信号(没有具体对应的事件,不会丢失信号
这些信号各自在什么条件下产生, 默认的处理动作是什么, 可通过命令
man 7 signal 查看详细说明
在这里插入图片描述

1.2 信号的生命周期

信号产生->在进程中注册->在进程中注销->捕捉处理(从信号发送到信号处理函数执行完毕)

2. 信号的产生

我们平常在Linux系统下所写的程序遇到死循环等问题时,退不出来, 常用 ctrl+c / ctrl+z /ctrl + | / kill + pid 解决, 这里的这些组合键盘输入就是产生了一个信号,以死循环程序为例说明:

#include<iostream>
#include<unistd.h>
int main(){
	pid_t pid = getpid(); // 获取当前进程pid
	while(1){
		std::cout << "死循环-" << pid << std::endl;
		sleep(1);
	}
	return 0;
}

2.1 通过终端按键产生信号

  • ctrl + c 产生的是 2号(SIGINT)终止信号,终止正在运行的前台进程,对后台进程无效。
    前台进程在运行过程中用户随时可能按下 ctrl+c产生一个信号,该进程的用户空间代码执行到任何地方都有可能收到 SIGINT信号而终止, 所以信号对于进程来说是异步的
    在这里插入图片描述
  • ctrl + z 产生的是 20号(SIGTSTP)暂停信号,暂停正在运行的前台进程。 状态 T 表示是停止状态,可以通过发送 SIGCONT 信号让进程继续运行。(进程信息的查看可以参考这里
    在这里插入图片描述

2.2 通过调用系统函数产生信号

  • kill函数可以给一个指定的进程发送指定的信号
int kill(pid_t pid, int sig);

kill + pid 默认发送15号信号。 本质上就是向一个进程发送了终止信号,进程收到这个信号并且处理才会退出。如果对于处在停止状态的进程, 进程没有在运行,意味着不会去处理这个信号, 所以 kill + pid 不能终止处于停止状态的进程(kiil -9 + pid 不在讨论范围)
在这里插入图片描述

  • raise函数可以给当前进程发送指定信号
int raise(int sig);
  • abort 函数给当前进程发送 SIGABRT(6号) 信号,使当前进程收到信号后异常终止, 通常用于异常通知
void abort();

2.3 通过软件条件产生信号

  • alarm 函数,等待设置的时钟周期后, 向进程发送SIGALRM 信号, 该信号的默认处理是终止当前进程。
int alarm(int seconds)

2.4 硬件异常产生信号

硬件异常被硬件以某种方式被硬件检测到并通知内核,然后内核向当前进程发送适当的信号。例如当前进程执行了除以0的指令,CPU的运算单元会产生异常,内核将这个异常解释 为SIGFPE信号发送给进程。再比如当前进程访问了非法内存地址,MMU会产生异常,内核将这个异常解释为SIGSEGV信号发送给进程。

3. 信号的相关概念

  • 实际执行信号的处理动作称为信号递达(Delivery)
  • 信号从产生到递达之间的状态,称为信号未决(Pending)。
  • 进程可以选择阻塞(Block )某个信号。 被阻塞的信号产生时将保持在未决状态,直到进程解除对此信号的阻塞,才执行递达的动作.
    (阻塞和忽略是不同的,只要信号被阻塞就不会递达,而忽略是在递达之后可选的一种处理动作。)

3.1 信号在内核中的表示

在这里插入图片描述

  • 每个信号都有两个标志位分别表示阻塞(block)和未决(pending),还有一个函数指针表示处理动作。信号产生时,内核在进程控制块中设置该信号的未决标志,直到信号递达才清除该标志。在上图的例子中,SIGHUP信号未阻塞也未产生过,当它递达时执行默认处理动作。
  • SIGINT信号产生过,但正在被阻塞,所以暂时不能递达。虽然它的处理动作是忽略,但在没有解除阻塞之前不能忽略这个信号,因为进程仍有机会改变处理动作之后再解除阻塞。
  • SIGQUIT信号未产生过,一旦产生SIGQUIT信号将被阻塞,它的处理动作是用户自定义函数处理。如果在进程解除对某信号的阻塞之前这种信号产生过多次,将如何处理?POSIX.1允许系统递送该信号一次或多次。Linux是这样实现的:常规信号在递达之前产生多次只计一次,而实时信号在递达之前产生多次可以依次放在一个队列里。

3.2 信号在进程中的注册

为了让进程知道自己收到了某个信号,

  • 在PCB中有一个未决信号 pending 集合,信号的注册就是指在这个 pending 集合中标记对应信号数值的二进制位为1
  • 信号的注册不仅会修改位图, 还会为信号组织一个sigqueue 节点添加到PCB中的sigqueue链表中

PCB—> struct sigpending —> struct sigset_t
sigset_t 这个结构体中只有一个数组成员;这个数组成员用于实现一个位图------表示未决信号集合------表示收到了但是还没有被处理的信号集合
给进程一个信号,就会将这个位图对应位置置1,表示当前进程收到了这个信号;(由于位图只有0和1, 所以只能表示收到了这个信号, 但是无法表示收到了多少个这样的信号)

  • 非可靠信号 注册一个信号最多只有一个节点
  • 可靠信号的注册,同一个信号允许存在多个相同节点
  • 位图只是表示一个信号有没有, 而节点可以反映一个信号有多少个

3.3 信号的销毁

为了保证一个信号只会被处理一次, 因此事先注销在处理;在PCB中删除当前信号信息
将 pending 位图置0, 删除要处理信号的 sigqueue 节点

  • 若信号是非可靠信号, 则直接将位图置0(非可靠信号在没有被处理前只会注册一次)
  • 若信号是可靠信号,则删除后,需要判断是否还有相同节点, 没有的话才会重置位图为0

4 信号的处理方式

信号的处理:信号表示一个事件的到来,处理事件就像是完成某个功能, 在C语言中完成一个功能的最小模块就是函数,也就是说每一个信号都有对应的处理函数,信号到来,去处理这个事件,也就是去执行对应的处理函数。
信号的处理方式:

  1. 忽略该信号----(什么也不做)
  2. 执行该信号的默认处理动作----(操作系统中针对该信号默认的处理方式)
  3. 提供一个信号处理函数, 内核在处理该信号时切换到用户态执行这个处理函数,也称为捕捉一个信号。----(信号的捕捉处理)

4.1 信号的捕捉处理

以具体代码来加深对信号的捕捉处理的理解
首先,学习相应是函数接口:

// 定义函数指针
typedef void (*sighandler_t)(int);
// 修改信号的回调函数
sighandler_t signal(int signum, sighandler_t handler) 
	handler:----用户自己定义的一个处理函数 没有返回值, 有一个 int 型参数的函数地址
    SIG_DFL----默认处理方式
    SIG_IGN----忽略处理方式
// 修改信号的整个处理动作
sigaction(int signum, struct sigaction *new, struct sigaction *old);
	调用成功则返回0,出错返回-1;
	signum是指定信号的编号;
	new和old指向sigaction的结构体。
	若new指针非空,则根据new修改该信号的处理动作;若old指针非空,则通过old传出该信号原来的处理动作;

示例代码:

/*                       
  * 学习修改信号的处理方式
  * sighandler_t signal(int signum, sighandler_t handler) 接口使用*/
#include<stdio.h>        
#include<stdlib.h>       
#include<signal.h>       
#include<unistd.h>       
                          
void sigcb(int signum)   
{                        
  printf("recv a signal num : %d\n", signum);
}                        
int main()               
{                         
   // 1.将 SIGINT 处理方式修改为 SIG_IGN 忽略处理方式 
   //signal(SIGINT, SIG_IGN);
   // 2.将 SIGINT 处理方式修改为 自定义 处理方式 
   // 只是修改了内核中信号的回调函数指针 ---当信号到来的时候才会调用这个回调函数,并且通过参数传入当前触发回调函数的信号值                                                                
   signal(SIGINT, sigcb);   
   while(1){                
     printf("我好冷啊!!!\n");
     sleep(10);
   }

请添加图片描述
可以看出,信号会打断进程当前的操作

4.2 信号的捕捉处理流程

信号的处理是在程序运行从内核态切换回用户态之前, 默认/忽略处理 直接在内核中完成处理, 而用户自定义信号处理方式, 则需要返回用户态执行回调函数, 完成后返回内核态, 最终没有信号处理了, 再返回程序主控流程
1.当程序在用户态主控流程运行的时候, 会由于 系统调用 \ 异常 \ 中断 切换到内核态运行
2. 默认处理方式调用的函数与 忽略处理方式调用的函数都是系统中已经实现的-----内核中直接处理
在这里插入图片描述
信号的处理,是程序运行从内核态返回用户态之前处理的
用户态运行: 用户编写的程序或者库函数,运行在用户态.
内核态运行: 运行内核中的代码来完成某个内核功能

5 信号的阻塞

组织信号的递达, 信号依然可以注册,只是暂时不处理, 在PCB中有一个 blocked位图(阻塞信号集合),凡是添加到这个集合中的信号,都表示需要阻塞,暂时不处理

步骤:

  1. 将一些信号的处理函数自定义
  2. 将所有的信号都给阻塞
  3. 在解除阻塞之前, 向进程发送信号
  4. 解除阻塞,查看信号的处理情况
int sigprocmask(int how, sigset_t *set, sigset *old);----
how : 当前要对 block 集合进行的操作
    SIG_BLOCK   将set集合中的信号添加到block进程阻塞信号集合中  block = block | set
             表示阻塞set集合中的信号以及原有的阻塞信号, 并且将原有阻塞的信号返回到old集合中(便于还原)
    SIG_UNBLOCK 将set集合中的信号从block集合中移除, 将set集合中的信号解除阻塞
             block = block & (~set) 10110100 & ~00100000  11011111
    SIG_SETMASK  直接将内核中的 block阻塞集合中的信号修改为set集合中的信号, block = set
set    
// 清空set信号集合---使用一个变量的时候的初始化过程
int sigemptyset(sigset_t *set); 
// 向set集合中添加指定信号
int sigaddset(sigset_t *set, int signum); 
// 将所有信号添加到set集合中
int sigfillset(sigset_t *set); 
// 从set集合中移除指定的信号
int sigdelset(sigset_t *set, int signum); 

代码示例:

#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
#include<signal.h>

void sigcb(int signum){
  printf("recv a signal: %d\n", signum);
}

int main(){
  signal(SIGINT, sigcb);
  signal(SIGRTMIN+4, sigcb);
  sigset_t set;
  sigemptyset(&set); // 清空集合, 防止未知数据造成影响
  sigfillset(&set); // 向集合中添加所有信号
  sigprocmask(SIG_BLOCK, &set, NULL); // 阻塞set集合中的所有信号

  printf("press enter continue\n");
  getchar(); // 等待一个回车, 如果不按回车就一直卡在这里

  sigprocmask(SIG_UNBLOCK, &set, NULL); // 解除阻塞

  while(1){
    sleep(1);
    printf("等待一秒\n");
  }

  return 0;
}

需要注意的是:
SIGKILL -9
SIGSTOP -19
在所有信号中, 这两个信号不可被阻塞, 不可被自定义修改处理方式, 也不可被忽略

联系之前总结的僵尸进程
SIGCHLD 信号(17号信号—非可靠信号):
僵尸进程: 子进程退出后, 操作系统发送 SIGCHLD 信号给父进程, 但是因为 SIGCHLD 信号的默认处理方式就是忽略, 因此在之前的程序中并没有感受到操作系统的通知, 因此只能固定的使用进程等待来避免产生僵尸进程, 但是在这个过程中父进程是一直阻塞的, 只能一直等待子进程退出

如何让程序感知到操作系统的通知?

在程序初始化阶段, 将SIGCHLD 信号的处理方式自定义, 并且在自定义函数重掉 waitpid ,这样的话就当子进程退出的时候, 则自动回调处理了, 父进程就不需要一直等待了

多个子进程同时退出, 都会向父进程发送 SIGCHLD 信号, 但是 SIDCHLD信号是非可靠信号, 有可能会丢失事件。

例如: 三个子进程同时退出, 但是信号只注册一次, 意味着 只会执行一次回调函数, 调用一次 waitpid  , 只能处理一个僵尸进程

非可靠信号的丢失是无法避免的 因此只能在一次信号回调中处理完所有的僵尸进程

  • 2
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值