信号的一生及其与线程的关系

版权声明:原创博客转载前请私信或评论,一天之内回复。 https://blog.csdn.net/qq_42381849/article/details/89818925

信号简述

信号是一种软件中断,用来处理异步事件。内核递送这些异步事件到某个进程,告诉进程某个特殊事件发生了。这些异步事件,可能来自硬件,比如访问了非法的内存地址,或者除以0了,可能来自用户的输入,比如shell终端上用户在键盘上敲击了Ctrl+C;还可能来自另一个进程,甚至来自进程自身。

信号的本质是一种进程间的通信,一个进程向另一个进程发送信号,内核至少传递了信号值这个字段。实际上,通信的内容不止是信号值。

那么如何将信号发送给进程呢?
对于发送的信号,内核会根据信号发送的目标进程尝试递送给该进程。内核会依据不同的情况采取不同的措施,如进程正在阻塞或者忙等不可中断等,这时内核就会把该信号挂起记录到进程task_struct的挂起信号量上;如进程屏蔽该信号或者该信号已经挂起那么内核就会忽略该信号;正常情况下,如果进程可接受信号那么进程就会在当前的执行流中断转而执行该信号处理函数(之前注册过的),执行完成后一般返回中断处继续执行。

信号的完整生命周期

信号的本质是一种进程间的通信。进程之间约定好:如果发生了某件事件就向目标进程发送某特定信号X,而目标进程看到就意识到某事件发生了,目标进程就会执行相应的动作。

接下来以配置文件改变为例,来描述整个过程。很多应用都有配置文件,如果配置文件发生改变,需要通知进程重新加载配置文件。一般而言,程序会默认采用SIGHUP信号来通知目标进程重新加载配置文件。
目标进程首先约定,只要收到SIGHUP,就执行重新加载配置文件的动作。这个行为称为信号的安装,或者信号处理函数的注册。安装好了之后,因为信号是异步事件,不知道何时会发生,所以目标进程依然正常地干自己的事情。收到SIGHUP信号,然后Linux内核就在目标进程的进程描述符里记录了一笔:收到SIGHUP一枚。Linux内核会在适当的时机,将信号递送给进程。在内核收到信号,但是还没有递送给目标进程的这一段时间。在内核收到信号,但是还没有递送给目标进程的这一段时间里,信号处于挂起状态,被称为挂起信号,也称为挂起信号,也称为未决信号。内核将信号递送给进程,进程就会暂停当前的控制流,转而去执行信号处理函数。这就是一个信号的完整生命周期。

一个典型的信号会按照上面所述的流程来处理,但是实际情况要复杂得多,还有很多场景响应考虑,比如:

  • 目标进程正在执行关键代码,不能被信号打断,需要阻塞某些信号,那么在这期间,信号就不允许被递送到进程,直到目标进程接触阻塞
  • 内核发生同一个信号已经存在,那么它该如何处理这种重复的信号?
  • 内核递送信号的时候,发现已经有多个不同的信号被挂起,那它应该优先递送那个信号?
  • 对于多线程的进程,如果向该进程发送信号,应该由哪个线程负责响应?

信号的产生

作为进程间通信的一种手段,进程之间可以相互发送信号,然而发送进程的信号,通常源于内核,包括:

  • 硬件异常
  • 终端相关的信号
  • 软件事件相关的信号
硬件异常

硬件检测到了错误并通知内核,由内核发送相应的信号给相关进程。和硬件异常相关的信号,如:

信号 说明
SIGBUS 7 总线错误,表示发生了内存访问错误
SIGFPE 8 表示发生了算数错误,尽管FPE是浮点异常的
SIGILL 9 进程尝试执行非法的机器语言指令
SIGSEGV 11 段错误,表示应用程序访问了无效地址

终端相关信号

对于Linux程序员而言,终端操作是免不了的。终端有很多的设置,可以通过执行如下指令来查看:
stty -a
很重要的是,终端定义了如下几种信号生成字符

  • Ctrl+C:产生SIGINT信号
  • Ctrl+\:产生SIGQUIT信号
  • Ctrl+Z:产生SIGTSTP信号

键入这些信号生成字符,相当于向前台进程组发送了对应的信号。
另一个和终端关系比较密切的信号是SIGHUP信号。很多程序员都遇到这种问题:使用ssh登入到远程的Linux服务器,执行比较耗时的操作,却因为网络不稳定,或者需要关机回家,ssh连接被断开,最终导致操作中途被放弃而失败。
之所以是因为一个控制进程在失去其终端之后,内核会负责向其发送一个SIGHUP信号。在登入会话中,shell通常是终端的控制进程,控制进程收到SIGHUP信号后,会引发如下的连锁反应。

shell收到SIGHUP后会终止,但是在终止之前,会向由shell创建的前台进程组合后台进程组发送SIGHUP信号,为了防止处于停止状态的任务接受不到SIGHUP信号,通常会在SIGHUP信号之后,发送SIGCONT信号,唤醒处于停止状态的任务。前台进程组合后台进程组的进程收到SIGHUP信号,默认的行为是终止进程,这也是前面提到的耗时任务会中途失败的原因。

软件事件相关的信号

软件事件触发信号产生的情况也比较多:

  • 子进程退出,内核可能会向父进程发送SIGCHLD信号。
  • 父进程退出时,内核可能会给子进程发送信号
  • 定时器到期,给进程发送信号

我们已经熟知子进程退出时会向父进程发送SIGCHLD信号。

与子进程退出向父进程发送信号相反,有时候,进程希望父进程退出时想自己发送信号,从而可以得知父进程的退出事件

信号的默认处理函数

信号产生的源头有很多。那么内核将信号递送给进程,进程会执行什么操作呢?

很多信号尤其是传统的信号,都会有默认的信号处理方式。如果我们不改变信号的处理函数,那么收到信号之后,就会执行默认的操作。

信号的默认操作有以下几种:

  • 显示的忽略信号:即内核会丢弃该信号,信号不会对目标进程产生任何影响

  • 终止进程:很多信号的默认处理是终止进程,即将进程杀死

  • 生成核心转储文件并终止进程:进程被杀死,并且产生核心转储文件。核心转储文件记录了该进程死亡现场的信息。用户可以使用核心转储文件来调试,分析进程死亡的原因。

  • 停止进程:停止进程不同于终止进程,终止进程是进程已经死亡,但是停止进程仅仅是四进程暂停,将进程的状态设置成TASK_STOPPED,一旦收到恢复执行的信号,进程还可以继续执行。

  • 恢复进程的执行:和停止进程相对应,某些进程可以使进程恢复执行。
    可以简单的标记这5种行为

  • ignore

  • terminate

  • core

  • stop

  • continue

      很多信号产生核心转储文件也是非常有意义的。一般而言,程
      序出错才会导致SIGSEGV、SIGBUS、SIGFPE、SIGILL及SIGABRT
      等信号的产生。生成的核心转储文件保留了进程死亡的现场,提供
      了大量的信息供程序员调试、分析错误产生的原因。核心转储文件
      的作用有点类似于航空中的黑盒子,可以帮助程序员还原事故现
      场,找到程序漏洞。
      很多情况下,默认的信号处理函数,可能并不能满足实际的需
      要,这时需要修改信号的信号处理函数。信号发生时,不执行默认
      的信号处理函数,改而执行用户自定义的信号处理函数。为信号指
      定新的信号处理函数的动作,被称为信号的安装。glibc提供了signal
      函数和sigaction函数来完成信号的安装。signal出现得比较早,接口
      也比较简单,sigaction则提供了精确的控制。
    

信号的分类

在Linux的shell终端,执行kill-l,可以看到所有的信号:
这些信号分为两类:

  • 可靠信号
  • 不可靠信号

可靠信号与不可靠信号的根本差异在于,收到信号后,内核有不同的处理方式。

对于不可靠信号,内核用位图来记录该信号是否处于挂起状态。如果收到不可靠信号,内核发现已经存在该信号处于为处决态,就会简单地丢弃该信号。因此发送不可靠信号,信号可能会丢失,即内核递送给目标进程的次数,可能小于信号发送的次数。

对于可靠信号,内核内部有队列来维护,如果收到可靠信号,内核会将信号挂到相应的队列中,因此不会丢失。严格来说,内核也设有上限,挂起的信号的个数也不可能无限地增大,因此只能说,在一定范围之内,可靠信号不会被丢弃。

信号与线程的关系

提到线程与信号的关系,必须先介绍下POSIX标准,POSIX标
准对多线程情况下的信号机制提出了一些要求:
·信号处理函数必须在多线程进程的所有线程之间共享,但是
每个线程要有自己的挂起信号集合和阻塞信号掩码。
·POSIX函数kill/sigqueue必须面向进程,而不是进程下的某个
特定的线程。
·每个发给多线程应用的信号仅递送给一个线程,这个线程是
由内核从不会阻塞该信号的线程中随意选出来的。

之间共享信号处理函数

内核中k_sigaction结构体的定义和glibc中sigaction函数中用到
的struct sigaction结构体的定义几乎是一样的。通过sigaction函数安
装信号处理函数,最终会影响到进程描述符中的sighand指针指向
的sighand_struct结构体对应位置上的action成员变量。

在创建线程时,最终会执行内核的do_fork函数,由do_fork函数
走进copy_sighand来实现线程组内信号处理函数的共享。创建线程
时,CLONE_SIGHAND标志位是置位的。创建线程组的主线程时,
内核会分配sighand_struct结构体;创建线程组内的其他线程时,并
不会另起炉灶,而是共享主线程的sighand_struct结构体,只须增加
引用计数而已。

统一进程的多个线程共享信号处理函数

异步信号安全

设计信号处理函数是一件很头疼的事情。当内核递送给信号给进程时,进程正在执行的指令序列就会被中断,转而执行信号处理函数。待信号处理函数执行完毕返回(如果可以返回的话),则继续执行中断的正常指令序列。问题就来了,同一个进程中出现了两条执行流,而两条执行流正是信号机制众多问题的根源。

猜你喜欢

转载自blog.csdn.net/qq_42381849/article/details/89818925
今日推荐