linux中的信号
- 用户在Linux命令行输入命令,在Shell下启动一个前台进程。
- 用户按下Ctrl-C, 产生一个硬件中断,被OS获取,解释成信号,发送给目标前台进程
- 前台进程因为收到信号,进而引起进程退出
Ctrl-C特点
- Ctrl-C 产生的信号只能发给前台进程。一个命令后面加个& 可以放到后台运行,这样Shell不必等待进程结束就可以接受新的命令,启动新的进程。
- Shell可以同时运行一个前台进程和任意多个后台进程,只有前台进程才能接到像Ctrl-C 这种控制键产生的信号。
- 前台进程在运行过程中用户随时可能按下Ctrl-C而产生一个信号,也就是说该进程的用户空间代码执行到任何地方都有可能收到SIGINT信号而终止,所以信号相对于进程的控制流程来说是异步(Asynchronous)的。
信号概念
信号是进程之间事件异步通知的一种方式,属于软中断
信号本质
软中断信号(signal,又简称为信号)用来通知进程发生了异步事件。在软件层次上是对中断机制的一种模拟,在原理上,一个进程收到一个信号与处理器收到一个中断请求可以说是一样的。信号是进程间通信机制中唯一的异步通信机制,一个进程不必通过任何操作来等待信号的到达,事实上,进程也不知道信号到底什么时候到达。进程之间可以互相通过系统调用kill发送软中断信号。内核也可以因为内部事件而给进程发送信号,通知进程发生了某个事件。信号机制除了基本通知功能外,还可以传递附加信息。
查看系统定义的信号量
kill -l
- 每个信号都有一个编号和一个宏定义名称,这些宏定义可以在signal.h中找到,宏对应的值就是编号的值
- 编号34以上的是实时信号
查看信号各自产生条件,默认的处理动作
man 7 signal
信号处理常见方式
- 忽略此信号。
- 执行该信号的默认处理动作。
- 提供一个信号处理函数,要求内核在处理该信号时切换到用户态执行这个处理函数,这种方式称为捕捉(Catch)一个信号。
linux中的信号的两种分类方式
可靠信号与不可靠信号
不可靠信号
Linux信号机制基本上是从Unix系统中继承过来的。早期Unix系统中的信号机制比较简单和原始,信号值小于SIGRTMIN(34)的信号都是不可靠信号。这就是"不可靠信号"的来源。它的主要问题是信号可能丢失。
可靠信号
产生
有必要对信号的原始机制加以改进和扩充。由于原来定义的信号已有许多应用,不好再做改动,又新增加了一些信号,并在一开始就把它们定义为可靠信号,这些信号支持排队,不会丢失。
兼容性
信号值位于SIGRTMIN(34)和SIGRTMAX(64)之间的信号都是可靠信号
Linux在支持新版本的信号安装函数sigation()以及信号发送函数sigqueue()的同时,仍然支持早期的signal()信号安装函数,支持信号发送函数kill()。
信号的可靠与不可靠只与信号值有关,与信号的发送及安装函数无关。目前linux中的signal()是通过sigation()函数实现的,因此,即使通过signal()安装的信号,在信号处理函数的结尾也不必再调用一次信号安装函数。同时,由signal()安装的实时信号支持排队,同样不会丢失。
对于目前linux的两个信号安装函数:signal()及sigaction()来说,它们都不能把SIGRTMIN以前的信号变成可靠信号(都不支持排队,仍有可能丢失,仍然是不可靠信号),而且对SIGRTMIN以后的信号都支持排队。这两个函数的最大区别在于,经过sigaction安装的信号都能传递信息给信号处理函数,而经过signal安装的信号不能向信号处理函数传递信息。对于信号发送函数来说也是一样的。
实时信号与非实时信号
早期Unix系统只定义了32种信号,Ret hat7.2支持64种信号,编号0-63(SIGRTMIN=31,SIGRTMAX=63),将来可能进一步增加,这需要得到内核的支持。前32种信号已经有了预定义值,每个信号有了确定的用途及含义,并且每种信号都有各自的缺省动作。如按键盘的CTRL ^C时,会产生SIGINT信号,对该信号的默认反应就是进程终止。后32个信号表示实时信号,等同于前面阐述的可靠信号。这保证了发送的多个实时信号都被接收。
非实时信号都不支持排队,都是不可靠信号;实时信号都支持排队,都是可靠信号。
产生信号
1.通过终端按键产生信号(硬件)
SIGINT的默认处理动作是终止进程,SIGQUIT的默认处理动作是终止进程并且Core Dump
Ctrl + c --> 2)SIGINT(终止/中断) “INT”—Interrupt
Ctrl + z --> 20)SIGTSTP(暂停/停止) “T”—Terminal 终端
Ctrl + \ --> 3)SIGQUIT(退出)
2.硬件异常产生信号
硬件异常被硬件以某种方式检测到并通知内核,然后内核向当前进程发送适当的信号。例如当前进程执行了除以0的指令,CPU的运算单元会产生异常,内核将这个异常解释为SIGFPE信号发送给进程。再比如当前进程访问了非法内存地址,MMU会产生异常,内核将这个异常解释为SIGSEGV信号发送给进程。
3.调用系统函数kill向进程发信号
写一个死循环程序
#include <stdio.h>
int main(){
while(1){
sleep(1);
printf("immortal...\n");
}
return 0;
}
保存为endless.c
编译(make方式也可以)
gcc endless.c -o endless
复制SSH隧道在另一个窗口查看死循环进程的pid(在xshell中)
ps -ef|head -1;ps -ef | grep endless
记住Pid,然后使用11号信号SIGSEGV终止进程
kill -SIGSEGV pid //等效于kill -11 pid
屏蔽死循环的输出
#include <stdio.h>
int main(){
while(1){
sleep(1);
//printf("immortal...\n");
}
return 0;
}
./endless运行在后台,查看pid并使用11号信号SIGSEGV终止进程
- kill -宏或者宏编号 pid 命令都是等价的
-
kill -SIGSEGV pid之后再回车才显示Segmentation fault ,是因为在pid进程终止掉之前已经回到了Shell提示符等待用户输入下一条命令Shell不希望Segmentation fault信息和用 户的输入交错在一起,所以等用户输入命令之后才显示。
-
以往遇到的段错误都是由非法内存访问产生的,而这个程序本身没错,给它发SIGSEGV也能产生段错误
-
kill命令是调用kill函数实现的。kill函数可以给一个指定的进程发送指定的信号。raise函数可以给当前进程发送指定的信号(自己给自己发信号)。两个函数都是成功返回0,错误返回-1。
-
abort函数使当前进程接收到信号而异常终止。和exit函数一样,abort函数总是会成功的,所以没有返回值。
4.由软件条件产生信号
常见的产生信号的系统函数有:kill, raise, alarm和setitimer以及sigqueue函数
SIGPIPE是一种由软件条件产生的信号,客户端程序向服务器端程序发送了消息,然后关闭客户端,服务器端返回消息的时候就会收到内核给的SIGPIPE信号。
alarm函数与SIGALRM信号
alarm的返回值是0或者是以前设定的闹钟时间还余下的秒数
调用alarm函数可以设定一个闹钟,也就是告诉内核在seconds秒之后给当前进程发SIGALRM信号, 该信号的默认处理动作是终止当前进程。
#include <stdio.h>
#include <unistd.h>
int main(){
int count = 0;
alarm(1); //1秒钟之后,给当前进程发送SIGALRM信号
for(;1;count++){
printf("count = %d\n", count);
}
return 0;
}
这个程序的作用是1秒钟之内不停地数数,1秒钟到了就被SIGALRM信号终止
信号捕捉
siganl函数进行信号捕捉
#include <stdio.h>
#include <signal.h>
void handler(int sig)
{
printf("catch a sig : %d\n", sig);
}
int main()
{
signal(2, handler);
while (1);
return 0;
}
模拟一下野指针异常
#include <stdio.h>
#include <signal.h>
void handler(int sig)
{
printf("catch a sig : %d\n", sig);
}
int main()
{
signal(SIGSEGV, handler);
sleep(1);
int* p = NULL;
*p = 100;
while (1);
return 0;
}
上述代码会无限循环捕捉信号量: 11) SIGSEGV
在C/C++当中除零,内存越界等异常,在系统层面上,是被当成信号处理的
总结
-
OS是进程的管理者,因此产生的所有信号,最终都要有OS来进行执行
-
信号的处理不是立即处理,在合适的时候,因为信号是异步的
-
信号如果不是被立即处理,那么信号需要暂时被进程记录下来,记录在进程地址空间的一个位图中(无符号整形)
-
OS向进程发送信号,实际上就是修改信号发往的目标进程PCB位图的对应记录信号的比特位
信号处理的三种方式
第一种是类似中断的处理程序,对于需要处理的信号,进程可以指定处理函数,由该函数来处理。
第二种方法是,忽略某个信号,对该信号不做任何处理,就象未发生过一样。
第三种方法是,进程通过系统调用signal来指定进程对某个信号的处理行为。对该信号的处理保留系统的默认值,这种缺省操作,对大部分的信号的缺省操作是使得进程终止。
信号处理流程
1.信号产生(见上文4个产生信号的例子),总结一下:
硬件来源(比如我们按下了键盘或者其它硬件故障);
软件来源:最常用发送信号的系统函数是kill, raise, alarm和setitimer以及sigqueue函数,软件来源还包括一些非法运算等操作。
–>
2.信号在进程中注册
–>
3.信号的执行和注销
信号产生到处理的状态
- 信号从产生到递达之间的状态,称为信号未决(Pending)。
- 进程可以选择阻塞 (Block )某个信号,被阻塞的信号产生时将保持在未决状态,直到进程解除对此信号的阻塞,才执行递达的动作.
- 实际执行信号的处理动作称为信号递达(Delivery)
- 忽略是在递达之后可选的一种处理动作。
信号在内核中的表示
-
每个信号都有两个标志位分别表示阻塞(block)和未决(pending),还有一个函数指针表示处理动作。信号产生时,内核在进程控制块中设置该信号的未决标志,直到信号递达才清除该标志。在上图的例子中,SIGHUP信号未阻塞也未产生过,当它递达时执行默认处理动作。
-
SIGINT信号产生过,但正在被阻塞,所以暂时不能递达。虽然它的处理动作是忽略,但在没有解除阻塞之前不能忽略这个信号,因为进程仍有机会改变处理动作之后再解除阻塞。
-
SIGQUIT信号未产生过,一旦产生SIGQUIT信号将被阻塞,它的处理动作是用户自定义函数sighandler。如果在进程解除对某信号的阻塞之前这种信号产生过多次,将如何处理?POSIX.1允许系统递送该信号一次或多次。Linux是这样实现的:常规信号在递达之前产生多次只计一次,而实时信号在递达之前产生多次可以依次放在一个队列里。本章不讨论实时信号。
内核实现信号捕捉
如果信号的处理动作是用户自定义函数,在信号递达时就调用这个函数,这称为捕捉信号。由于信号处理函数的代码是在用户空间的,处理过程比较复杂,举例如下: 用户程序注册了SIGQUIT信号的处理函数sighandler。当前正在执行main函数,这时发生中断或异常切换到内核态。在中断处理完毕后要返回用户态的main函数之前检查到有信号SIGQUIT递达。内核决定返回用户态后不是恢复main函数的上下文继续执行,而是执行sighandler函数,sighandler和main函数使用不同的堆栈空间,它们之间不存在调用和被调用的关系,是两个独立的控制流程。sighandler函数返回后自动执行特殊的系统调用sigreturn再次进入内核态。如果没有新的信号要递达,这次再返回用户态就是恢复main函数的上下文继续执行了。