信号

1, 信号基础

1.1 中断概念: 了解

中断是计算机硬件和操作系统之间交互的一种机制,用于处理紧急事件或立即需要注意的情况。我们一般把中断分为硬件中断和软件中断,它允许一个硬件设备/软件程序通知CPU它需要处理的事件。(下面是一些常见的例子)

Eg: 一个键盘输入操作触发中断处理的大致流程 (硬件中断)

Eg: 一个网卡接收网络数据包触发中断的大致流程 (硬件中断)

Eg: 一个读取文件触发中断的大致流程 (软件中断)

ps: 软件中断,也常称为异常或系统调用,是由程序执行的指令引发的中断,通常用于请求操作系统的服务或处理异常情况。

1.2 信号概念

在操作系统中, 信号用于通知进程----发生了某些事情

  • 信号是一种进程间通信机制

  • 每个信号用一个整型常量宏表示,以 SIG 开头。

  • POSIX.1-1990 标准信号 (了解)

(ps: Term/Terminate表示终止进程; Core表示进程会导出内存映像到文件(一般默认不产生);Ign表示信号被忽略;Stop表示暂停进程)

SignalValueActionComment
SIGHUP1Term链接断开
SIGINT2Term键盘中断(Ctrl+C触发) (默认行为:终止进程)
SIGQUIT3Core键盘退出(Ctrl+\触发) (默认行为:终止进程)
SIGILL4CoreCPU指令译码阶段无法识别
SIGABRT6Core异常终止
SIGFPE8Core浮点异常
SIGKILL9Term终止进程
SIGSEGV11Core异常内存访问
SIGPIPE13Term写入无读端的管道
SIGALRM14Term定时器超时
SIGTERM15Term终止
SIGUSR130, 10, 16Term自定义信号1
SIGUSR231, 12, 17Term自定义信号2
SIGCHLD20, 17, 18Ign子进程终止或者暂停
SIGCONT19, 18, 25Cont暂停后恢复
SIGSTOP17, 19, 23Stop暂停进程(可通过Ctrl+Z触发)(SIGCONT或者fg恢复)
SIGTSTP18, 20, 24Stop终端输入的暂停
SIGTTIN21, 22, 26Stop后台进程控制终端读
SIGTTOU22, 22, 27Stop后台进程控制终端写
  • 当进程接收到信号以后,常见有3种选择处理机制(参考信号的Action)。

  • 信号和中断是完全相互独立的: 中断涉及到的是, CPU对紧急事件的处理和响应, 以及进程的调度和上下文切换; 而信号则是被某些行为产生, 产生的信号会修改对应进程的状态, 对应的进程, 又根据被改变的进程状态, 做出对应反应。

  • 信号和中断, 很多时候会在操作系统中协作 (并不一定是必须),使得系统和应用程序可以响应各种事件。

1.3 信号递送的流程

1.3.1 进程的执行流程

信号的递送流程在 Linux 系统中是一个复杂的过程,涉及到信号的生成、传递、处理等多个步骤。

  • 信号生成: 信号可以有多种事件触发,比如外部中断(如按下 Ctrl+C),系统调用(如kill进程函数),或者软件条件(如除零操作)。当这些事件发生时,内核生成一个相应的信号。

  • 信号的传递:一旦信号被生成,内核将分辨和处理信号发送给对应的进程。(这一过程涉及到修改目标进程的task_struct结构体中相关的字段) (某种程度上, 信号从产生,到传递给目标进程, 到执行信号的决策过程, 这个步骤的整体也可以称为信号的递送)

  • 信号的排队:内核为信号创建一个信号队列项,并将其加入到目标进程的位图OR信号队列中。(信号也可能会覆盖旧的信号或者被丢弃)

  • 信号的阻塞与解除阻塞:进程通过修改其信号掩码来阻塞或解除阻塞特定的信号。如果一个信号被阻塞,它仍然可以被递送到目标进程,但不会被立即处理。被阻塞的信号会留在待处理信号集合中,直到它们被解除阻塞。

  • 信号的接收与处理:当正在运行的进程某一时刻从内核态执行返回用户态时, 在返回之前会先检查待处理信号集合。如果存在未被阻塞的待处理信号,内核会在进程继续执行前(返回用户态之前), 先安排信号处理程序的执行(默认或者自定义程序)。

  • 信号处理程序的执行:如果进程为信号指定了自定义的处理函数,则该函数会被执行。执行完成后,进程通常会继续其正常执行流程(继续返回用户态)。如果进程使用默认的信号处理行为,或者忽略信号(对于可以被忽略的信号),则相应的默认行动会被采取,比如终止进程或暂停进程。

1.3.2 一些状态

尽管信号有着多种产生来源,但是对于被动接收信号的进程而言,信号的产生只不过是被修改了task_struct结构体的一些表示信号的参数,就是说,信号产生于内核。

  • 当一个进程处于一个可以接受信号状态的时候(这种状态被称为响应时机),它会取得信号信息,并执行默认行为、或者执行忽略、或者执行自定义信号处理函数。

  • 信号产生表示内核已知信号发生。

  • 信号递送表示内核将生成的信号添加到目标进程的待处理信号集(修改task_struct)(有些时候也表示信号从产生一直到执行的完整过程)。

  • 信号递送侧重表达将信号传递给进程的过程(修改task_struct),而信号处理是侧重表示进程对接收到的信号采取的具体动作。

  • 已经递送但是还没有执行的信号被称为挂起信号(pending signal) 或者是未决信号 (可能是因为: 信号被阻塞, 或者进程暂时不能处理信号, ps: 信号未决 != 信号阻塞, 信号未决的意思是信号暂时未被执行, 而信号阻塞需要对应解除阻塞的操作)。

  • 信号阻塞是由进程的信号掩码控制的。

  • 由进程的某个操作产生的信号称为同步信号(synchronous signals)(例如在代码中除0)。

  • 像用户击键这样的进程外部事件产生的信号叫做异步信号(asynchronous signals)。

2, 信号和函数

2.1 注册信号: signal

再前面我们提到, 进程对信号有三种处理方式。

  • 如果我们未进行任何设置和操作,信号具有预设的处理机制, 将按照预设的处理机制执行。 (操作系统控制/预设的)

  • 我们也可以选择设置忽略一个信号。( signal(信号, SIG_IGN) ) (并不是每一个信号都是可以被忽略的)

  • 设置信号为其默认行为SIG_DFL (signal(信号, SIG_DFL))

  • 我们改变/修改信号的预设机制,给信号设置一个新的处理函数。当信号触发时,让其处理机制,按照我们设置的函数执行。(我们可以通过signal函数完成)

2.1.1 signal函数

signal函数可以用来捕获信号并且指定对应的信号处理行为。 (man 2 signal)

EgCode:

2.1.2 注册多个信号

使用signal函数是可以同时注册多个信号。甚至可以把不同的信号的处理函数设置为同一个。

EgCode:

ps1: 信号的处理函数, 可以在进程执行过程中重新指定.

ps2: sleep()可能会被信号到达而提前终止 (返回剩余睡眠时间)。

2.1.3 多个信号触发: 重要

在使用函数signal时,如果进程收到一个信号,自然地就会进入信号处理的流程,如果在信号处理的过程中:

  • 接受到了另一个不同类型信号。那么当前的信号处理流程是会被中断的, CPU会先转移执行新到来的信号处理流程,执行完毕以后再恢复原来信号的处理流程。

  • 接受到了另一个相同类型信号。那么当前的信号处理流程是会不会被中断的, CPU会继续原来的信号处理流程,执行完毕以后再响应新来到的信号。

  • 如果接受到了连续重复的相同类型的信号,后面重复的信号会被忽略,从而该信号处理流程只能再多执行一次。

EgCode:

2.1.4 几个问题

在signal处理机制下,在一些特殊的场景下:

  • 通过signal注册一个信号处理函数,并且处理完毕一个信号之后, 还需要重新注册吗?

  • 如果信号处理函数正在处理信号,并且还没有处理完毕时,又产生了一个同类型的信号,会怎么样?

  • 如果信号处理函数正在处理信号,并且还没有处理完毕时,又产生了一个不同类型的信号,会怎么样?

  • ...

2.2 注册信号: sigaction

虽然signal函数广泛用于设置信号处理函数,但它存在诸多局限性,当需要更精确控制信号处理行为,特别是在编写依赖于特定行为的代码时,更推荐使用sigaction函数

2.2.1 sigaction函数

使用函数sigaction可以自定义某些场景下的行为。(man 2 sigaction)

sa_sigaction: 指向一个接受三个参数的信号处理函数

  • 参数一:信号的编号

  • 参数二:siginfo_t 结构体,提供关于信号的更多信息,如发送信号的进程ID等等等(参考man sigaction对siginfo_t类型的说明)(因系统不同结果可能不同, 未必符合预期)

  • 参数三:进程当前上下文的指针(与硬件和操作系统实现相关)

  • 比sa_handler可接受更多的信号的上下文信息

  • 和sa_handler选一个即可

  • 通常仅在参数sa_flags包含SA_SIGINFO标志时使用

sa_flags:信号处理方式掩码, 可以用来设置信号的处理模式。

  • SA_SIGINFO:使用 sa_sigaction 成员而不是 sa_handler 作为信号处理函数。

  • SA_RESETHAND: 处理完捕获的信号以后,信号处理回归到默认( 一次注册只生效一次)

  • SA_NODEFER: 在信号处理函数执行期间,同一个信号设置可以再次被触发

  • SA_RESTART:使被信号打断的系统调用自动重新调用。

EgCode: sa_handler函数作为处理函数

EgCode: sa_sigaction函数作为处理函数

EgCode: 使用sa_flags演示只注册一次

EgCode: 使用sa_flags演示信号打断的系统调用(eg:read), 信号执行函数结束后自动重新调用

EgCode: 使用sa_flags演示同一信号可以被再次触发

2.2.2 sa_mask

为了避免正在执行的信号处理函数被新的信号中断, 我们可以设置sa_mask参数(指定当前信号处理函数执行时需要阻塞的额外信号)来增加一些信号的阻塞操作。

  • sa_mask的sigset_t类型是一个位图

  • 使用sa_mask: POSIX 定义了一系列的函数来操作 sigset_t 类型的变量 (man sigemptyset)

EgCode: 使用sa_mask阻塞信号(Eg: 在SIGINT/Ctrl+c信号处理函数执行的时候, 阻塞SIGQUIT/Ctrl+\ )

ps: 重提一下, 阻塞屏蔽和忽略信号有着截然不同的含义,内核会维护一个所有未决信号的位图,阻塞表示信号因为某种原因限制暂未执行,如果信号已经被阻塞,被阻塞的信号可能会在后续合适的时机被执行,而被忽略的信号则是直接被丢弃了。

2.3 信号mask

2.3.1 sigpending

sigpending函数用于检查当前进程的未决信号集,即那些已经发送给进程但由于某种原因(通常是因为被阻塞)尚未被处理的信号。这个函数可以用来确定哪些信号已经被产生并等待处理,但尚未被当前进程捕获或忽略。

EgCode:

2.3.2 sigprocmask

sigprocmask函数用于在系统中检查和更改进程的信号屏蔽字(即信号掩码mask)。信号掩码确定了哪些信号可以递送给该进程,哪些信号被阻塞。和前面通过sigaction函数设置的sa_mask阻塞机制不同的是, sigaction函数设置的sa_mask阻塞是临时屏蔽。而sigprocmask函数修改信号掩码mask是全程屏蔽. (man sigprocmask)

EgCode:

ps:闲聊:

假设存在这样一种场景,我们需要在进程中写入共享资源,自然就会采用加锁/解锁操作,如果这种写入过程十分重要,那么我们往往需要在加解锁之间屏蔽某些信号的递送。

我们之前的sigaction函数设置的sa_mask阻塞, 只能在某个信号处理过程中去阻塞另一个信号,另一种解决方案的实现思路是,在加锁的时候,将信号注册为忽略,在解锁的时候将信号注册为默认, 也就是使用上面的sigprocmask的方式阻塞信号。

2.3 其它

2.3.1 kill

系统调用kill函数可以用来给另一个进程发送信号。 (man 2 kill)

EgCode:

2.3.2 pause

pause函数作用是使调用进程挂起(即暂停执行),直到该进程捕获到一个信号。换句话说,pause函数让进程休眠,等待任何类型的信号到来;一旦接收到信号,如果有为该信号定义的处理函数,则执行该函数。如果没有为信号定义处理函数(或者信号的行为是默认的),进程会根据信号的默认行为来响应。

EgCode:

ps: 闲聊

如果使用sigprocmask,可以实现所谓的信号保护临界区,在临界区当中执行代码的时候,此时产生的信号将会被阻塞,临界区结束的位置只需要再使用sigprocmask即可。

如果希望在临界区之后再次捕获信号,可以使用系统调用pause进行捕获, 即先使用sigprocmask解开信号阻塞,再调用pause函数, 使进程被阻塞, 等待信号到来, 进而捕捉信号。

但是需要注意的是, 如果是在临界区间产生了信号, 当使用sigprocmask解开信号阻塞时, 却会直接执行信号处理流程,无法使 sigprocmask之后的pause函数就绪。因为在解除阻塞后, 处于sigprocmask函数之后的pause函数,此时进程还未进入到pause的阻塞状态中。

所以为了捕获期间产生的信号, 一种策略就是将解除阻塞和等待信号合并成一个原子操作,这就是sigsuspend。

2.3.3 sigsuspend: 了解

sigsuspend函数用于原子地更改进程的信号屏蔽字(block mask)并挂起进程执行,直到捕获到一个信号。 (man sigsuspend )

  • 更改信号掩码和挂起进程之间提供原子操作

  • 在sigsuspend返回后,进程的信号掩码会自动恢复到调用sigsuspend之前的状态

EgCode:

2.3.4 alarm

alarm函数用于设置一个计时器(定时器),该计时器在指定的秒数后到期。当计时器到期时,内核会向该进程发送 SIGALRM 信号。如果程序没有捕获或忽略该信号,则其默认行为是终止进程。

EgCode:

使用alarm和sigsuspend实现一个类似sleep的功能

EgCode:

2.3.5 setitimer

setitimer函数是一个高级定时器接口,相较于alarm函数,它提供了更多的灵活性和精度。 (man 2 getitimer)

which参数:指定定时器的类型。常用的类型包括:

  • ITIMER_REAL:按照真实时间, 当时间到达, 发出一个 SIGALRM 信号。

  • ITIMER_VIRTUAL:按照用户态代码执行时间计算, 当时间到达, 发出一个 SIGVTALRM信号。

  • ITIMER_PROF:按照用户态用户态和内核态代码执行时间计算, 当时间到达, 发出一个 SIGPROF 信号。

EgCode: