目录
认识信号
信号的概念
说到信号我们并不陌生,生活中充斥着各种信号,如路边的红绿灯、早晨的闹钟、教室的上课铃等。信号的产生意味着我们一般都需要对这个信号做出反应(忽略这个信号也算是一种反应),而进程间的信号也是类似,当一个信号发送给某一个进程,或者说一个进程接收到某种信号,这意味着这个进程需要对这个信号做出某种特定的反应。
信号是一种异步的进程间通信机制,它可以在任何时候发送信号给某个进程。通过发送指定信号来通知进程某个异步事件的发送,以迫使进程执行信号处理程序。信号处理完毕后,被中断进程将恢复执行。用户、内核和进程都能生成和发送信号。
前台进程与后台进程
在Linux中,在终端运行的进程叫做前台进程,而其它默默在后端执行或暂停等的进程叫做后台进程。在Linux中,每个终端下只允许存在一个前台进程,但可以存在多个后台进程。
后台进程在默认情况下通常不会获取到标准输入流,即键盘输入的内容。这是因为标准输入输出流(stdin、stdout和stderr)是与进程与终端(TTY)之间直接绑定的。当进程在前台运行时,可以与标准IO流进行交互。而当一个进程被放入后台时,这个后台进程就会脱离与终端的直接绑定,进而就不能再与标准IO流之间进行交互了。
其中,我们可以通过在终端指令后加“&”符号,以指定当前指令是在后台启动,例如:
./a.out &
sleep 10 &
此外,还可以通过nohup、screen、systemctl等命令达到后台启动进程的效果。
一般我们在运行一个可执行程序时,默认就是在前台运行的,此时的bash进程就会退到后台,变成一个后台进程。直到这个可执行程序结束退出了,bash进程才会重新被唤醒。所以这也就解释了,为什么在执行其它的可执行程序时 bash 是无法进行响应的。
可以通过 jobs 指令来查看当前终端的后台进程。需要注意的是,jobs 指令只能查看当前终端的后台进程,而不是查看整个操作系统的后台进程。而如果 bash 进程退到后台时,我们就无法通过 jobs 指令来查看当前终端的后台进程了。
每一个进程被移入后台后都会有一个任务编号(即下图的1、2、3等数字),任务编号后面有+、-两个标志。+ 代表我fg或bg不指定任务编号时,将该进程调至前台执行。当把带有 + 的进程调至前台执行后,- 标志的进程就自动变成 +,没有标志的自动变成 - 。类似于一个栈,先入后出。
可以通过 fg 指令将后台进程提到前台,使用格式为 fg+任务编号。若后台任务中只有一个,则使用该命令时可以省略任务号。而 fg 指令的本质其实就是向进程发送信号,使得后台任务提到前台。
信号的种类
在Linux中,可以通过 kill -l 指令查看Linux下的信号种类,如下图。
可以看到,是没有0、32、33号信号的,也就是一共有62个信号。其中我们把 1~31 号信号称为普通信号。其它的称为实时信号,当信号产生必须立即处理就是实时信号。普通信号是有系统定义的默认方法的,而实时信号一般需要自定义方法。所以这里只研究普通信号,不研究实时信号。
这些信号的含义是由POSIX标准定义的,是标准的UNIX信号。每个信号都有一个编号和一个宏定义名称,即 SIGHUP 的本质就是一个为1的宏。
31个普通信号的大致含义如下(参考:Linux中的31个普通信号_CSDN博客)
信号原理
信号的产生
Linux中,信号产生的4种方式可以概括为4种:终端快捷键、硬件信号、系统指令、软件条件。
所以归根结底,不管是哪种形式发送的信号,最终映射到到操作系统中一定是那31普通信号。
信号集
而信号其实是有三种状态的,分别是:
而Linux中普通信号只有31种,所以我们可以只用一个四字节的位图来表示每一种信号的二元状态。这个位图结构就是信号集,即 sigset_t 类型。sigset_t 类型对于每种信号用一个比特位表示有效或无效,至于这个类型内部如何存储这些比特位则依赖具体的系统实现,从使用者的角度是不必关心的,使用者只能调用相关函数来操作 sigset_t 类型的变量,比如用 printf 直接打印 sigset_t 变量是没有意义的。
所以内核中其实是有两张信号集位图的,一个是阻塞信号集(blcok),又叫信号屏蔽字,一个未决信号集(pending)。相应的,对于每一种信号都会有对应的处理方法,所以在内核中,信号体系中还是需要维护一个信号处理方法的指针数组,存放着与下标一致的信号的处理方法。图例如下。
信号的执行
在信号递达之后,通常会经过如下几个步骤(如图)
信号集操作方法
信号集操作
#include <signal.h>
// 清空信号集合。
int sigemptyset(sigset_t *set);
// 将指定的信号添加到信号集合中。即将位图中的对应位置置1。
int sigaddset (sigset_t *set, int signo);
// 将信号集合设置为包含所有可能的信号。
int sigfillset(sigset_t *set);
// 从信号集合中删除指定的信号。
int sigdelset(sigset_t *set, int signo);
// 检查指定的信号是否包含在信号集合中。
int sigismember(const sigset_t *set, int signo);
/*函数说明:
set是一个指向sigset_t类型的指针,用于表示信号集合。
signo是要添加的信号编号。
函数都是成功返回0,失败返回-1。
*/
sigprocmask - 阻塞信号集操作
#include <signal.h>
int sigprocmask(int how, const sigset_t *set, sigset_t *oset);
接口说明:
sigpending - 读取当前进程的未决信号集
#include <signal.h>
int sigpending(sigset_t *set);
接口说明:
signal - 设置信号的处理方法
#include <signal.h>
typedef void (*sighandler_t)(int);
sighandler_t signal(int signum, sighandler_t handler);
接口说明:
sigaction - 设置和检查信号处理函数
#include <signal.h>
int sigaction(int signum, const struct sigaction *act,
struct sigaction *oact);
接口说明:
特别的,act和oact都是struct sigaction类型的指针,struct sigaction结构体的大致内容如下:
struct sigaction
{
void (*sa_handler)(int);
void (*sa_sigaction)(int, siginfo_t *, void *);
sigset_t sa_mask;
int sa_flags;
void (*sa_restorer)(void);
};