进程信号的本质与处理

Posted 白龙码~

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了进程信号的本质与处理相关的知识,希望对你有一定的参考价值。

文章目录

进程信号

一、信号的概念

信号是一种软中断,是进程之间进行异步通知的一种方式。

在Linux下,信号拥有对应的编号。其中:1号~31号是普通信号,34~64是实时信号。

1、kill命令

kill命令用来向进程发送信号。

  • kill -l:查看当前系统内定义了哪些信号(编号+名称)。
  • kill -编号/名称 + 进程id:向对应发送信号。

常见信号如:

  1. 2号SIGINT:即interrupt,中断信号,相当于进程执行时键入ctrl c,该信号强制前台进程退出,即直接杀死前台进程
  2. 19号SIGSTOP:暂停信号,相当于进程执行时键入ctrl z,该信号会将进程暂时挂起并放入后台,可以通过18号SIGCONT重新唤醒并运行于后台
  3. 9号SIGKILL:立即结束正在执行的进程(前台、后台都适用)。

注:对于相当一部分信号而言,进程收到该信号的默认处理动作都是终止当前进程。

2、信号的产生方式

  • 键盘,如ctrl c强制当前进程退出。
  • kill命令。
  • 硬件异常产生信号:遇到除零、野指针等错误时,操作系统会识别,然后向目标进程发送信号以达到终止进程的目的。
  • 软件产生信号:如alarm函数可以设定一定时间后让内核发送SIGALRM信号使得该进程退出。

3、为什么操作系统可以识别错误并发送信号?

因为这些错误一般都和硬件联系在一起,比如除零错误对应CPU的运算器,系统可以通过当前CPU执行的进程来决定向谁发送对应的出错信号。

二、信号的本质

普通信号不是立即处理的,因此需要先被保存在进程控制块task_struct中的信号位图中,以标志收到的信号编号。

1、普通信号的描述与组织形式

  1. block位图(信号屏蔽字):第几个比特位为1则说明几号信号被阻塞,因此block位图可以理解为一种状态掩码。如果进程收到被阻塞的信号,那么该信号就会处于未决状态暂时不会被处理,直到它被解除阻塞,即从block位图中移除。
  2. pending位图(未决信号集):第几个比特位为1则说明收到几号信号,该信号是未决状态。信号产生时,内核将pending对应位置修改为1,当信号递达后该位置恢复为0。
  3. handler数组:用信号编号作为索引,对应位置存储不同信号的处理方法。

信号抵达和信号未决

  • 信号递达:信号正在被处理的过程。
  • 信号未决:信号从产生到递达的过程。

2、信号集和操作信号集的方法

blockpending都是位图,它们在Linux中被封装为sigset_t类型,即信号集

信号集的操作方法

  • int sigemptyset(sigset_t *set)

将信号集全部置0


  • int sigfillset(sigset_t *set)

将信号集全部置1


  • int sigaddset (sigset_t *set, int signum)

将指定信号所在位置为1


  • int sigdelset(sigset_t *set, int signum)

将指定信号所在位置为0


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

判断指定信号所在位是否为1


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

如果set不为空,则将当前进程的block位图设置为set,如果oset不为空,则将更改前的阻塞信号集作为输出型参数返回为用户。

其中,how有三种参数:

  1. SIG_BLOCK:将set指定的信号添加到当前进程的block中

  2. SIG_SETMASK:将当前进程的block替换为set

  3. SIG_UNBLOCK:将set指定的信号从当前进程的block中删除,即解除阻塞


  • int sigpending(sigset_t* set)

set作为输出型参数,获取当前进程的pending信号集

三、信号的处理方式

  1. 忽略

  2. 执行该信号的默认处理动作

  3. 提供一个信号处理函数,要求内核在处理该信号时切换到用户态执行这个处理函数,这种方式称为捕捉(Catch)一个信号。

1、信号的捕捉过程

进程收到信号时,其实并不是立即处理的,而是在从内核态切换到用户态的之前检查task_structpending信号集是否标记接收到信号,如果接收到并且没有被阻塞,则执行该信号对应的处理操作。

处理分为两种:

  1. 忽略或执行默认处理函数。此时直接在内核态完成对应的系统函数调用,然后返回用户态即可。

  2. 用户自定义处理函数。此时需要先从内核态返回用户态(因为OS不信任用户提供的代码,所以不在最高权限的内核态执行,以避免系统安全问题),执行完用户代码提供的处理函数后,再次返回内核态,调用sys_sigreturn()函数,之后再回到用户态,继续向下执行。

信号的产生可以在进程的任何时候,而进程处理信号不一定是实时的,因此信号和进程是"异步"的

2、自定义信号处理函数

1、sighandler_t signal(int signum, sighandler_t handler)

定义进程接收到signum号信号的处理函数handler,同时返回该信号原先的处理函数。

注:当handler为SIG_IGN时,表示忽略signum信号。


2、int sigaction(int signum, const struct sigaction *act, struct sigaction *oact)

该函数也可用于自定义信号处理的方式,其主要参数包括要自定义的信号signum,以及处理方式结构体act

我们一般将act.sa_flags初始化为0,act.sa_handler初始化为自定义处理函数,act.sa_mask是一个信号集,表示在signum信号被处理时,act.sa_mask中标注的信号要被屏蔽,其余指针成员可以初始化为NULL。

oact是输出型参数,可以将修改前的默认处理方式保存下来,方便以后恢复。

避免僵尸进程的另一方式:SIGCHLD信号

子进程退出时,会向父进程发送SIGCHLD信号。

父进程可以选择等待子进程,也可以选择不等待。如果父进程并不关心子进程的退出状态,那么可以选择将SIGCHLD的处理函数设置为SIG_IGN,即忽略。此时子进程在退出后就会立刻被系统回收,不会造成内存泄漏(Linux下有效,其余类Unix平台不一定)。

3、无法被捕捉和阻塞的信号

Linux规定:对于9号信号SIGKILL19号信号SIGSTOP,我们无法捕捉或阻塞它们,因而也就无法自定义它的行为或忽略它。

4、SIGKILLSIGTERM的区别

SIGKILL(9号)无法被捕捉,因此收到该信号的进程会直接退出。

SIGTERM(15号)可以被捕捉,因此进程可以通过自定义处理函数来让进程在退出前完成资源的释放等善后操作

正因SIGTERM的处理更加"人性化",因此它也是kill命令默认使用的信号

5、普通信号与实时信号的区别

普通信号通过pending未决信号集来标识是否接收,因此当多份相同的信号同时发送时,未决信号集对应位置只能被标注一次,从而导致信号丢失。

task_struct中维护了实时信号的执行队列,因此每一个实时信号都会加入到队列中,它们都会被执行。

补充概念:函数的重入

一个函数被多个执行流进入的情况叫做函数的重入。

例如:在主函数执行func函数时收到信号signum,而signum的自定义处理函数也要调用func函数,此时func就是被重入了。

  • 可重入函数必须满足没有操控临界资源,因此它们在多线程情况下也是线程安全的。
  • 只有少部分函数是允许重入的,因为大部分函数都会操作某一全局的数据结构,操作之间都会互相影响。

如果一个函数符合以下条件之一则是不可重入的:

  1. 调用了mallocfree,因为malloc也是用全局链表来管理堆的。
  2. 调用了标准I/O库函数。标准I/O库的很多实现都以不可重入的方式使用全局数据结构。

注:函数的重入是针对单线程提出的,它并不依赖与多线程的概念。

以上是关于进程信号的本质与处理的主要内容,如果未能解决你的问题,请参考以下文章

进程间通信之信号量

信号量的基本概念与使用semget,semop

Operating System: Semaphore

22信号和信号量

有N个进程共享同一临界资源,如用信号量机制,实现对一临界资源的互斥访问,则信号量的变化范围是?

简述利用信号量实现两个进程互斥的实现