Linux 信号

1. 信号

1.1 前言

在 Linux 中,信号是一种用于进程之间的通信机制,通常是异步的,也就是进程随时都可以收到信号,可以通过信号来通知进程发生了什么事,并且进程可以马上对这个信号做出反应和处理

在 Linux 中输入 kill -l可以得到信号的编号以及这些信号大概代表的是什么,常见的比如 9 号信号就是常见的强制杀死进程信号,以及 11 号就是段错误,具体点就是空指针,指针越界这些异常了,还有Ctrl + C 终止信号是 2 号信号。
(注意:信号是从 1 开始的,没有 0 号信号)

在这里插入图片描述虽然总共有 64 个信号,但是前 32 个信号被称为核心信号,现在更关注的也是这前 32 个信号,用的也很频繁,通常用于程序的各种异常问题

1.2 信号的位置

操作系统随时都可能会给进程发送信号,而且还可能连续发送,进程收到信号之后,可能也不会马上处理,手头可能有优先级更高的事,甚至有可能直接忽略这个信号

先来了解一下信号的三种状态

① 递达(Delivery

  • 当进程收到信号之后,就直接执行该信号的处理方法,称为 信号递达,也就是 处理信号

② 未决(Pending

  • 当信号到达的时候,进程不会马上处理这个信号,先放着,称为 未决,即暂时不处理

③ 阻塞 (Block

  • 如果某个信号被设为阻塞,那么进程收到这个信号的时候,会将这个信号置为 未决 状态,并且阻塞如果不取消,这个信号会一直得不到处理

task_struct 中,可以理解成存储了关于 32 个信号对应的 3 个数据结构,这三结构就是 blockpendingdelivery。并且 pendingblock 可以理解为位图

① 比方说,1 号信号在 pending 位图中对应的标志位为 1,那么说明这个信号暂时不被处理,但是当操作系统识别到这个 1 的时候,就会执行相应的处理函数(如果没被屏蔽的话)
delivery 结构存储了关于这些信号的处理方法
block 中如果对应的 1 号信号为 1,那么说明接下来进程如果收到 1 号信号,那么会将 pending 中关于 1 号信号对应的标志位设为 1,表示 未决,暂时不处理

在这里插入图片描述

其中,delivery存储了某个信号对应的处理方法,⭐「处理方法」包括三种:默认,忽略(不处理),用户自定义。并且 Linux 为默认和忽略提供了宏:默认 —— SIG_DFL忽略 —— SIG_IGN,而每个信号都有各自默认的处理方法

1.3 接口

1.3.1 sigset_t

首先认识一个数据结构 sigset_t,这个数据结构可以用来表示一个信号集,可以表示某个信号的状态。

  • 可以理解成一个位图,比方说,1 号信号在里面的比特位为1,那么表示有效;反之,0 表示无效,或者说没有这个信号

并且这个结构可以用来表示 blockpending,意义也是一样的,比如 sigset_t pendings,如果 1 号信号在里面的标志位为 1,那么表示 1 号信号在这个进程中的状态是未决的

1.3.2 信号集操作接口

1. sigemptyset(sigset_t* set) 					清空信号集,全部置0就对了
2. sigfillset(sigset_t* set) 					将信号集全置 1
3. sigaddset(sigset_t* set, int signo)  		将某个信号在这个信号集中标记为 有效
4. sigdelset(sigset_t* set, int signo) 			将某个信号在这个信号集中标记为 无效
5. sigismember(const sigset_t set, int signo) 	这个信号在信号集中是否 有效

这里面就是各种对 sigset_t 的操作了,后面举例子

1.3.3 signal

在这里插入图片描述上面不是说信号的处理方法有三种吗,而自定义处理方法就可以通过这个函数来指定

  • signum 表示要处理的信号,handler表示函数指针,就是自己定的函数了

(sigaction接口也可以修改信号对应的处理方案,signal 相对于它来说是简化版的)

举个例子:在进程中设置对 2 号信号的捕捉,捕捉之后打印一段话。之后对这个进程发送 2 号信号,这时候就会执行我们绑定的自定义函数了

在这里插入图片描述

1.3.4 sigprocmask

在这里插入图片描述作用:修改进程的阻塞信号集(block

how 参数常见的可以有 3 个值,假设当前进程的 信号屏蔽集为 block

  • SIG_BLOCK:往阻塞信号集中添加set中有效的信号,相当于让当前进程的 block | set
  • SIG_UNBLCOK:往阻塞信号集中去掉 set 中有效的信号,相当于让当前进程的 block | ~set
  • SIG_SETMARK,让当前进程的 block = set

oset 是输出型参数,如果不为空,那么会返回变化之前的 block ,便于后续恢复;如果不关心之前的状态,设为 NULL 就好了

举个例子
在这里插入图片描述屏蔽之后 2 号信号怎么处理?当然可以在程序中定期扫描没有处理的信号,也可以将 2 号信号的屏蔽字恢复为 0,比如:在后面将程序对 2 号信号的屏蔽取消之后,这个进程就会被终止了

在这里插入图片描述
可以看出虽然 pending2 号信号有效,但是由于屏蔽字,2 号信号的处理被拖延了。但是我们一旦取消屏蔽,而pending2号依然有效,2 号信号的处理函数就会被执行

1.3.5 sigpending

在这里插入图片描述
这个很简单了

  • set:输出型参数,会得到进程的 pending 集合,返回 0 表示一切顺利

2. 信号的处理

当进程收到信号之后,pending 中对应的标志位会被置为 1,然后⭐进程每次陷入内核态,再从内核态切换回用户态之前,内核大多会检查进程的信号状态,以处理这些未处理的信号,处理完后,对应标志位置为 0

先了解一下内核态和用户态

2.1 内核态和用户态

还记得虚拟地址空间吗,在 32 位下,虚拟地址空间中有 1G 内核空间,但是这部分空间不使用普通的页表,这 1G内核空间有自己独有的页表 —— 内核级页表,不同于用户空间的页表中 —— 每个进程都有一个自己的用户页表,而内核级页表所有进程共享,操作系统自己的数据和代码就是通过内核页表映射到物理内存上的

操作系统的代码数据必然不允许被随便访问,所以内核态页表带有权限验证。

  • 而进程的内核态和用户态其实就相当于两个身份,如果进程是内核态,那么也就可以访问内核的页表,也就可以访问所有内核代码数据,用户级的代码和数据当然也可以访问。内核态是特权模式,可以访问操作系统的数据和代码,也可以访问硬件设备,执行速度较快。

在这里插入图片描述

当进程需要从用户态切换成内核态的时候,会修改处理器的特权等级,从 3(用户态)改成 0(内核态),这里的处理器包括(CR0,CR3,CR4寄存器),当从用户态转化成内核态的时候,会保存用户态的上下文信息,并加载内核态的上下文信息,然后就可以访问内核态的页表,执行操作系统的内置代码,比如进程调度,异常处理等操作,当工作完成后,就会拿着计算结果,恢复用户态的上下文数据,再返回给用户,然后回到刚刚中断的代码后继续执行

总之,在程序的运行过程中,操作系统无形中会大量地访问系统硬软件资源,在这些访问过程中,操作系统都会切换成内核态,使用内核页表,然后调用内核部分的代码,最终再拿着计算的结果切换成用户态返回给用户

2.2 信号的监测和处理

所以当一个进程收到操作系统发送的信号之后,首先做的就是将 pending 中将对应的信号有效位置为 1,而该信号的处理一般是等到进程下一次嵌入内核态,再从内核态切换回用户态之前,因为在这之前,操作系统大都会检查该进程的信号状态。

  • 假设当前有个进程收到了信号 2,那么pending 上的相应位置也会被置为 1
  • 然后该进程突然执行了 write 等系统调用,就需要切换成内核态来执行操作系统的代码,以及访问硬件资源。然后按理来说,执行完 write 的时候,就应该拿着执行结果切回用户态返回给程序了
  • 但是在这之前,操作系统会以内核态的身份检查该进程的 pendingblock,如果 block中某信号标志位为 1,那么说明被屏蔽了,不用管它,而如果 block = 0, pending = 1 ,说明这个信号现在需要被处理
  • 如果信号的处理方法是 SIG_IGN,那么就忽略,将结果返回给用户,并从中断处继续往下执行
  • 如果信号的处理方法是 SIG_DEL,那么操作系统还是在内核态执行处理方法,比如终止进程…
  • 如果该信号的处理方法是用户自定义的函数,那么要执行这个函数,就又需要切换成用户态来执行这个处理函数(如果代码是恶意代码,那么以内核态的身份执行就存在风险)
  • 处理完这个函数之后,也不能直接将结果从用户层返回,因为还需要继续回到内核态,执行内核代码,比如更新进程的状态数据,在内核态中执行信号处理程序的收尾工作等
  • 最后再拿着执行结果返回给用户,从系统调用 write 处继续往后执行
  • 就完成了一次信号的处理

在这里插入图片描述