Linux信号的有关概念及使用
程序员文章站
2022-07-12 10:31:04
...
一、信号的基本概念
为了理解信号,先从我们最熟悉的场景说起,比如我们过马路时的交通信号灯,当我们看到红灯的时候,就会停下来等待,当我们看到绿灯的时候,会选择通行。这个红灯就是传给我们的信号。
对于Linux中的信号,我们需要先了解:
1、Ctrl-C产生的信号只能发给前台进程。一个命令后面加个&可以放到后台运行,这样Shell不必等待进程结束就可以接受新的命令,启动新的进程。
2. Shell可以同时运行一个前台进程和任意多个后台进程,只有前台进程才能接到像Ctrl-C这种控制键产生的信号。
3. 前台进程在运行过程中用户随时可能按下Ctrl-C而产生一个信号,也就是说该进程的用户空间代码执行到任何地方都有可能收到SIGINT信号而终止,所以信号相对于进程的控制流程来说是异步 (Asynchronous)的
我们先利用kill -l命令察看系统定义的信号列表:
我们可以观察到:每个信号都有一个编号和⼀一个宏定义名称,这些宏定义可以在signal.h中找到,例如其中有定义#define SIGINT 2 。
注:1至31号信号,是非实时信号,发送的信号可能会丢失,不支持信号排队。编号34以上的是实时信号,支持信号排队,发送的多个实时信号都会被接收,本章只讨论编号34以下的信号,不讨论实时信号。
二、信号的产生方式
1、通过终端按键产生的信号
我们知道,SIGINT(2号)的默认处理动作是终止进程,SIGQUIT(3号)的默认处理动作是终止进程并且Core Dump,现在我们来验证一下。
我们首先了解一下Core Dump是什么?
当一个进程要异常终止时,可以选择把进程的用户空间内存数据全部保存到磁盘上,文件名通常是core,这叫做Core Dump(核心转储)。
进程异常终止通常是因为有Bug,比如非法内存访问导致段错误,事后可以用调试器检查core文件以查清错误原因,这叫做Post-mortem Debug(事后调试)。
注意:系统默认是不允许产生core文件的,因为core文件中可能包含用户密码等敏感信息,不安全。在开发调试阶段可以用ulimit命令改变这个限制,允许产生core文件。首先用ulimit命令改变Shell进程的Resource Limit,允许core文件最大为1024K。
来看一下:
改变后:
我们写一个死循环,在前台运行这个程序:
1 #include<stdio.h>
2
3 int main(){
4 printf("pid is : %d\n",getpid());
5 while(1);
6 return 0;
7 }
运行,,然后在终端键入Ctrl-\:
由此可见,ulimit命令改变了Shell进程的Resource Limit,test进程的PCB由Shell进程复制而来,所以也具有和Shell进程相同的Resource Limit值,这样就可以产⽣生Core Dump了。 我们用调试一下,使用core文件,看哪里出错了:
2. 调用系统函数向进程发信号
我们首先在后台执行死循环程序:
1 #include<stdio.h>
2
3 int main(){
4 while(1);
5 return 0;
6 }
然后用kill命令给它发SIGSEGV信号,如图:注:6070是a.out进程的id。之所以要再次回车才显示 Segmentation fault ,是因为在6070进程终止掉之前已经回到了Shell提示符等待用户输入下一条命令,Shell不希望Segmentation fault信息和用户的输入交错在一起,所以等用户输入命令之后才显示。
我们需要了解几个函数:
1、kill函数、raise函数
kill命令是调用kill函数实现的。kill函数可以给一个指定的进程发送指定的信号。raise函数可以给当前进程发送指定的信号(自己给自己发信号)
这两个函数都是成功返回0,错误返回-1
2、abort函数
abort函数使当前进程接收信号而异常终止,所以总是成功,无返回值。
3. 由软件条件产生信号
SIGPIPE是一种由软件条件产⽣的信号,我们首先认识一个函数alarm。
顾名思义,就是一个闹钟。
设定一个闹钟,告诉内核在senconds秒后给当前进程发14号SIGALRM信号,该信号的默认动作是终止当前进程。
参数设为0,表示取消闹钟
参数设为n,表示n秒后发送信号
函数返回值:是0或以前设定闹钟时间还余下的秒数
参数设为0,表示取消闹钟
参数设为n,表示n秒后发送信号
函数返回值:是0或以前设定闹钟时间还余下的秒数
下面有一个例子,作用是2秒钟之内不停地数数,1秒钟到了就被SIGALRM信号终止,如下:
1 #include<stdio.h>
2 #include<stdlib.h>
3
4 int main(){
5 int count=10;
6 alarm(1);
7 for(;1;count++){
8 printf("count %d\n",count);
9 }
10 return 0;
11 }
~
4、硬件产生信号
硬件异常产生信号,这些条件由硬件检测到并通知内核,然后内核向当前进程发送适当的信号。
例如当前进程执行了除以0的指令,CPU的运算单元会产生异常,内核将这个异常解释为SIGFPE信号发送给进程。
再比如当前进程访问了非法内存地址,,MMU会产生异常,内核将这个异常解释为 SIGSEGV信号发送给进程。
三、信号的处理方式
1.忽略。
2.执行默认动作。
3.提供一个信号处理函数,要求内核在处理该信号时切换到用户态执行这个处理函数,这种方式称为捕捉一个信号。
2.执行默认动作。
3.提供一个信号处理函数,要求内核在处理该信号时切换到用户态执行这个处理函数,这种方式称为捕捉一个信号。
四、阻塞信号
1、信号的实质
我们先了解几个概念:
信号递达(Delivery):实际执行信号的处理动作(第三点的三种方式)
信号未决(Pending):信号从产生到递达之间的状态
阻塞 (Block ):进程可以选择阻塞某个信号,被阻塞的信号产生时将保持在未决状态,直到进程解除对此信号的阻塞,才执行递达的动作.
注意,阻塞和忽略是不同的,只要信号被阻塞就不会递达,而忽略是在递达之后可选的一种处理动作。
产生信号的实质就是操作系统将信号位图的对应位置的比特位由0改为1,其实每个信号都有两个标志位,就是上文介绍的阻塞 (Block)、信号未决(Pending),还有一个函数指针表示处理动作(handler)。结构如下图:
在上图的例子中, SIGHUP信号未阻塞也未产生过,当它递达时执行默认处理动作。SIGINT信号产生过,但正在被阻塞,所以暂时不能递达。虽然它的处理动作是忽略,但在没有解除阻塞之前不能忽略这个信号,因为进程仍有机会改变处理动作之后再解除阻塞。 SIGQUIT信号未产生过,一旦产生SIGQUIT信号将被阻塞,它的处理动作是用户自定义函数sighandler。
从上图来看,每个信号只有一个bit的未决标志,非0即1,不记录该信号产生了多少次,阻塞标志也是这样表示的。
因此,未决和阻塞标志可以用相同的数据类型sigsett来存储 ,sigsett称为信号集。
阻塞信号集也叫做当前进程的信号屏蔽字(Signal Mask),这里的“屏蔽”应该理解为阻塞而不是忽略。
2、信号集操作函数
1).sigemptyset函数:初始化set所指向的信号集,使其所有信号对于的bit清零
2).sigfillset函数:初始化set所指向的信号集,使其所有信号对于的bit置1
3).sigaddset函数:添加signum信号
4).sigdelset函数:删除signum信号
5).sigismember函数:判断signum信号是否在信号集中
2).sigfillset函数:初始化set所指向的信号集,使其所有信号对于的bit置1
3).sigaddset函数:添加signum信号
4).sigdelset函数:删除signum信号
5).sigismember函数:判断signum信号是否在信号集中
前四个函数都是成功返回0,出错返回-1。
sigismember是一个布尔函数,用于判断一个信号集的有效信号中是否包含某种信号,若包含则返回1,不包含则返回0,出错返回-1。
3、sigprocmask函数----读取或更改进程的信号屏蔽字(阻塞信号集)
参数:
how的可选值:
SIG_BLOCK(添加到当前信号屏蔽字的信号)
SIG_UNBLOCK(解除当前信号屏蔽字中解除阻塞的信号)
SIG_SETMASK(设置当前信号屏蔽字为set所指向的值)
set:更改进程当前信号屏蔽字
oldset:保存原有的信号屏蔽字
set:更改进程当前信号屏蔽字
oldset:保存原有的信号屏蔽字
4、sigpending函数----读取当前进程的未决信号集
参数:
set:读取当前进程的未决信号集,通过set参数传出返回值:调用成功则返回0,出错则返回-1。
我们举一个例子:
1 #include<stdio.h>
2 #include<signal.h>
3 #include<unistd.h>
4
5 void printsigset(sigset_t *set){
6 int i=0;
7 for(;i<32;i++){
8 if(sigismember(set,i)){
9 putchar('1');
10 }else{
11 putchar('0');
12 }
13 }
14 puts("");
15 }
16
17 int main(){
18 sigset_t s,p;
19 sigemptyset(&s);
20 sigaddset(&s,SIGINT);
21 sigprocmask(SIG_BLOCK,&s,NULL);
22 while(1){
23 sigpending(&p);
24 printsigset(&p);
25 sleep(1);
26 }
27 return 0;
28 }
解释:
结果,我们阻塞了SIGINT信号,当键盘输如组合键Ctrl-C时,使得SIGINT信号处于未决状态,不被处理。
五、捕捉信号
1、捕捉信号的过程
捕捉信号,即信号处理的handler自定义方法
如下图,为信号捕捉的过程:
2、sigaction函数 ----读取和修改与指定信号相关联的处理动作
参数:act和oldact指向sigaction结构体
sigaction结构体如下:
2、pause函数--使调用进程挂起直到有信号递达
如果信号的处理动作为忽略,则进程处于挂起状态;如果处理动作为终止进程,则进程终止;如果处理动作为捕捉,则调用了信号处理函数后返回-1。
六、用alarm和pause实现sleep()函数
1 #include<stdio.h>
2 #include<signal.h>
3 #include<unistd.h>
4 void sig_alrm(int signo){
5
6 }
7 unsigned int mysleep(unsigned int nsecs){
8 struct sigaction new,old;
9 unsigned int unslept=0;
10 new.sa_handler=sig_alrm;
11 sigemptyset(&new.sa_mask);
12 alarm(nsecs);
13 pause();
14 unslept=alarm(0);
15 sigaction(SIGALRM,&old,NULL);
16 return unslept;
17 }
18 int main(){
19 while(1){
20 mysleep(5);
21 printf("5 seconds passed\n");
22 }
23 return 0;
24 }
解释:
结果:
上一篇: 信号的产生
下一篇: Linux进程信号之阻塞信号