Linux信号处理机制(一)——信号引入

2019-07-14 12:36发布

信号在最早的Unix系统中被引入,内核可用信号通知进程系统所发生的事件。在现实生活中,我们每天都在接触信号,下课铃声、红绿灯、闹钟等都是信号。

信号的本质

操作系统给进程发送信号,本质上是给进程的PCB中写入数据,修改相应的PCB字段,进程在合适的时间去处理所接受的信号。我们模拟一下这样的场景:(1)用户输入一个命令,在shell下启动一个前台进程;(2)用户按下Ctrl+c,通过键盘输入产生一个硬件中断;(3)如果CPU当前正在运行此进程的代码,则该进程的用户空间的代码将暂停执行,CPU从用户态切换至内核态处理中断;(4)终端驱动程序将Ctrl+c解释为一个SIGINT信号,记在该进程的PCB中;(5)当某个时刻从内核返回至该进程的用户空间代码继续执行之前,首先处理PCB中记录的信号;SIGINT信号的默认处理动作为终止信号,所以直接终止进程而不再返回到它的用户空间代码;:Ctrl+c所产生的信号只能发送给前台进程,如果想让该进程在后台运行,需要在启动该进程的时候,在后面加上&,这样shell就不必等待进程结束就可以接受新的命令,启动新的进程。
上图中,S为后台进程,S+为前台进程;shell可以同时运行一个前台进程和任意多个后台进程,只有前台进程才能接受诸如Ctrl+c这样的信号,前台进程在运行过程中用户随时按下Ctrl+c而产生一个信号,也就是说该进程的用户空间代码执行到任何地方都有可能接收到一个SIGINT信号而被终止,因此信号相对于进程的控制流程来说是异步的

普通信号与实时信号

我们使用 kill -l 命令可以查看系统定义的信号列表,每个编号都有一个宏与之对应,可以在 /usr/include/asm/signal.h 中查看,下图中 1~31号为普通信号,34~36号信号为实时信号。
那么使用上述信号的目的是什么呢?大致可以总结为两点。(1)让进程知道已经发生了一个特定的事件;(2)强迫进程执行它代码中的信号处理程序;上述的两个目的不是互斥的,因为进程经常通过执行一个特定的例程来对某一个事件做出反应。实时信号(real-time signal):编号为34~46,它们通常与普通信号有很大的不同,因为他们必须排序以便发送多个信号能被接收到。但是同种信号的普通信号并不排序,尽管在Linux内核并不使用实时信号,它还是通过几个特定的系统调用完全实现了POSIX标准。  信号有很多,常见的有:
  • SIGINT:在键盘按下组合键后产生,默认动作为终止进程;
  • SIGQUIT:在键盘按下组合键后产生,默认动作为终止进程;
  • SIGKILL:无条件终止进程。本信号不能被忽略、处理和阻塞。默认动作为终止进程。它向系统管理员提供了一种可以杀死任何进程的方法;
  • SIGALRM:定时器超时,超时的时间由系统调用alarm设置。默认动作为终止进程;
  • SIGCHLD:子进程结束时,父进程会收到这个信号。默认动作为忽略该信号;

信号的存储

内核给一个进程发送软中断信号的方法,是在进程所在的进程表项的信号域设置对应于该信号的位,而存储这32位信号的空间恰巧需要4个字节,因此采用位图存储是最好不过的。bit位的位置表示对应信号的编号,用0来表示未接受到信号,1表示接收到信号。

产生信号的主要条件

(1)用户在终端按下某些键时,终端驱动会发送信号给前台进程,例如Ctrl+c产生的SIGINT信号、Ctrl+产生SIGQUIT信号、Ctrl+z产生SIGSTOP信号;(2)硬件异常产生的信号,这些条件由硬件检测并通知内核,然后内核向当前进程发送合适的信号。比如当前进程访问了非法地址,MMU(内存管理单元)会产生异常,内核将这个异常解释为SIGSEGV信号发送给进程;(3)一个进程调用 kill 函数可以发送信号给另一个进程,也可以调用 kill 命令发送信号给某一个进程,kill 命令也是调用 kill 函数实现的,如果不明确指定信号,则发送SIGTERM信号,该信号的默认处理动作是终止进程,当内核检测到软件条件发生时可以通过信号通知进程。

如何处理信号

进程以三种方式对一个信号做出应答:
(1)显示的忽略信号;(2)执行与信号相关的默认操作;由内核预定义的默认操作取决于信号的类型,可以是以下类型之一:Treminate:进程被终止(杀死)Dump:进程被终止(杀死),如果可能,创建包含进程执行上下文的核心转储文件Ignore:信号被忽略Stop:进程被停止,即把进程置为TASK_STOPPED状态Continue:如果进程被停止,就把它设置为TASK_RUNNING状态
(3)通过调用相应的信号处理函数捕捉信号(自定义类型)信号捕捉函数:可以修改信号的默认处理操作,但某些信号是不能够被捕捉的,比如9号信号,它存在的目的是防止恶意进程入侵而无法被终止,在一定程度上保护了操作系统。
#include typedef void (*sighandler_t)(int); sighandler_t signal(int signum, sighandler_t handler); 参数signum:信号的编号参数handler:是一个函数指针,表示接受此信号要执行的函数的地址返回值:若成功则为指向前次处理程序的指针,若出错则为SIG_ERR我们做一个测试,对2号信号进行捕捉:#include #include void handler() { printf("handler "); } int main() { signal(2,handler); while(1); return 0; } 我们可以看到Ctrl+c是不能终止该程序的,无奈我们只能用9号信号来杀死该进程。修改上面的程序:#include #include #include typedef void(*sighandler_t) (int);//函数指针 sighandler_t _handler = NULL; void handler() { printf("handler "); signal(2,_handler);//恢复默认处理 } int main() { _handler = signal(2,handler);//捕捉2号信号 while(1); return 0; } 对上述程序的解释:首先我们用Ctrl+c捕捉2号信号,并用_handler函数指针对象接受,在handler函数内,再次用 Ctrl+c 捕捉2号信号,并指向_handler捕捉成功,返回之前的信号处理函数,即恢复了默认处理,程序得以终止。

信号产生的方法

(1)通过终端按键产生信号(Core dump)
(2)调用系统函数向进程发送信号首先在后台执行死循环程序,然后用 kill 命令给它发一个 SIGSEGV 信号。我们将signal_a程序在后台运行,之所以要按一次回车键才显示段错误的原因在于,该进程终止之前已经回到了shell提示符等待用户输入下一条命令,shell不希望段错误的信息和用户输入的交错在一起,所以等用户输入命令之后才会显示。
kill命令是调用 kill 函数实现的, kill 函数可以给一个特定的进程发送指定的信号;raise函数可以给当前进程发送指定的信号(自己也可以给自己发送信号),原型如下:#include int kill(pid_t pid, int signum); //给任意进程发送任意信号 int raise(int signo); //给自己发送任意信号参数pid:进程号参数signum:信号的编号返回值:两者都是成功返回0,失败返回-1我们模拟一下raise函数,给自己发送2号信号:#include #include int count = 0; void myhandler(int sig) { printf("count:%d, sig:%d ",count++,sig); } int main() { signal(2,myhandler); while(1) { raise(2); sleep(1); } return 0; } 运行结果如下:

abort可以使当前进程接收到信号而异常终止,但是abort会认为进程不安全。#include void abort(void);类似于exit函数一样,abort函数总是成功的,因此没有返回值。(3)由软件条件产生信号进程可以通过调用alarm向它自己发送SIGALRM信号,函数原型如下:#include unsigned int alarm(unsigned int seconds); 参数seconds:alarm函数安排内核在seconds秒内发送一个SIGALRM信号给调用进程,如果soconds等于0,那么不会调度新的闹钟(alarm)返回值:前一次闹钟剩余的秒数,若以前没有设定闹钟,则为0下面这个程序,我们让SIGALRM信号在5秒内数数,当传送第6个SIGALRM信号的时候程序会终止。#include #include #include #include void handler(int sig) { static int count = 0; printf("count=%d ",count); if(count++ < 5) { alarm(1); } else { printf("end... "); exit(0); } } int main() { signal(SIGALRM,handler); alarm(1); while(1); return 0; } 运行结果如下:
我们使用signal函数设置了一个信号处理函数,只有进程收到一个SIGALRM信号,就异步调用该函数,中断main的while循环,当handler返回时,控制传递回main函数,它就从最初被信号到达时中断了的地方继续执行。