【Linux】---------信号知识点合集

0.什么是信号?

信号在我们的生活中随处可见, 如:古代战争中摔杯为号;现代战争中的信号弹;体育比赛中使用的信号枪…

共性

1) 简单
2)不能携带大量信息
3)满足某个特设条件才发送。

信号是信息的载体,Linux/UNIX 环境下,古老、经典的通信方式, 现下依然是主要的通信手段。

信号的机制

A 给 B 发送信号,B收到信号之前执行自己的代码,收到信号后,不管执行到程序的什么位置,都要暂停运行,去处理信号,处理完毕再继续执行。与硬件中断类似——异步模式。但信号是软件层面上实现的中断,早期常被称为 软中断。

信号的特质:由于信号是通过软件方法实现,其实现手段导致信号有很强的延时性。但对于用户来说,这个延迟时间非常短,不易察觉。

每个进程收到的所有信号,都是由 内核 负责发送的,在内核态 中处理。

1.Linux信号

信号概念

信号是进程之间事件异步通知的一种方式,属于软中断。

信号列表

kill -l命令可以察看系统定义的信号列表
在这里插入图片描述

  • 每个信号都有一个编号和一个宏定义名称,这些宏定义可以在signal.h中找到,例如其中有定 义 #define SIGINT 2(即使不同平台,也能方便使用)
  • 编号34以上的是实时信号,我们只讨论编号34以下的信号,不讨论实时信号。这些信号各自在什么条件下产生,默认的处理动作是什么,在signal(7)中都有详细说明: man 7 signal。就不再陈列。

2.Linux信号产生

1)按键产生

如:Ctrl+c、Ctrl+z、Ctrl+\

注意

  1. Ctrl-C 产生的信号只能发给前台进程。一个命令后面加个&可以放到后台运行,这样Shell不必等待进程结束就可以接受新的命令,启动新的进程。

  2. Shell可以同时运行一个前台进程和任意多个后台进程,只有前台进程才能接到像 Ctrl-C 这种控制键产生的信号。

  3. 前台进程在运行过程中用户随时可能按下 Ctrl-C 而产生一个信号,也就是说该进程的用户空间代码执行到任何地方都有可能收到 SIGINT 信号而终止,所以信号相对于进程的控制流程来说是异步(Asynchronous)的

2)系统调用产生

如:kill、raise、abort

#include <signal.h>
int kill(pid_t pid, int signo);
int raise(int signo);
这两个函数都是成功返回0,错误返回-1#include <stdlib.h>
void abort(void);
就像exit函数一样,abort函数总是会成功的,所以没有返回值

3)软件条件产生

SIGPIPE是一种由软件条件产生的信号,在“管道”中已经介绍过了

如:定时器alarm
对应的信号为14号信号。

#include <unistd.h>
unsigned int alarm(unsigned int seconds);
调用alarm函数可以设定一个闹钟,也就是告诉内核在seconds秒之后给当前进程发SIGALRM信号, 该信号的默认处理动作是终止当前进程

4)硬件异常产生

如:非法访问内存(段错误)、除0(浮点数例外)、内存对齐出错(总线错误)

5)命令产生

如:kill命令

kill -信号 进程ID

3.Linux信号阻塞

在这里插入图片描述
注意:

  • 实际执行信号的处理动作称为信号递达(Delivery)
  • 信号从产生到递达之间的状态,称为信号未决(Pending)。
  • 进程可以选择阻塞 (Block )某个信号。
  • 被阻塞的信号产生时将保持在未决状态,直到进程解除对此信号的阻塞,才执行递达的动作.
  • 注意,阻塞和忽略是不同的,只要信号被阻塞就不会递达,而忽略是在递达之后可选的一种处理动作
  • 每个信号都有两个标志位分别表示阻塞(block)和未决(pending),还有一个函数指针表示处理动作。信号产生时,内核在进程控制块中设置该信号的未决标志,直到信号递达才清除该标志。在上图的例子中,SIGHUP信号未阻塞也未产生过,当它递达时执行默认处理动作。
  • SIGINT信号产生过,但正在被阻塞,所以暂时不能递达。虽然它的处理动作是忽略,但在没有解除阻塞之前不能忽略这个信号,因为进程仍有机会改变处理动作之后再解除阻塞。SIGQUIT信号未产生过,一旦产生SIGQUIT信号将被阻塞,它的处理动作是用户自定义函数sighandler。
  • 如果在进程解除对某信号的阻塞之前这种信号产生过多次,将如何处理?POSIX.1允许系统递送该信号一次或多次。Linux是这样实现的:常规(不可靠)信号在递达之前产生多次只计一次,而实时(可靠)信号在递达之前产生多次可以依次放在一个队列里。下面在信号排队里讨论。

具体验证看最后的代码实例

4.Linux信号处理

信号处理

完整三步骤

1)信号产生,未决信号集中描述该信号的位立刻翻转为1,表示信号处于未决状态。当信号被处理对应位翻转回为0(这一时刻往往非常短暂)。

2)信号产生后,由于某些原因(主要是阻塞)不能抵达。这类信号的集合称之为未决信号集(penging) 在屏蔽解除前,信号一直处于未决状态。

3)处理信号时,按照下面三个处理方式

  1. 执行默认动作
  2. 忽略(丢弃)
  3. 捕捉(调用户处理函数)
    捕捉特性:
    • 当注册了某个信号捕捉函数,捕捉到该信号以后,要调用该函数。而该函数有可能执行很长时间,在这期间所屏蔽的信号不由☆来指定。而是用sa_mask来指定。调用完信号处理函数,再恢复为☆。
    • XXX信号捕捉函数执行期间,XXX信号自动被屏蔽。
    • 阻塞的常规信号不支持排队,产生多次只记录一次。(后32个实时信号支持排队)

信号捕捉函数:

  1. signal
  2. sigaction
    下面介绍

系统默认处理

默认动作:

Term:终止进程
Ign: 忽略信号 (默认即时对该种信号忽略操作)
Core down:终止进程,生成Core文件。(查验进程死亡原因, 用于gdb调试)
Stop:停止(暂停)进程
Cont:继续运行进程

注意从man 7 signal帮助文档中可看到 : The signals SIGKILL and SIGSTOP cannot be caught, blocked, or ignored。特别强调了 9) SIGKILL 和 19) SIGSTOP信号,不允许忽略和捕捉,只能执行默认动作,甚至不能将其设置为阻塞。

另外需清楚,只有每个信号所对应的事件发生了,该信号才会被递送(但不一定递达),不应乱发信号!!

信号捕捉

在这里插入图片描述

如果信号的处理动作是用户自定义函数,在信号递达时就调用这个函数,这称为捕捉信号。由于信号处理函数的代码是在用户空间的,处理过程比较复杂,举例如下: 用户程序注册了SIGQUIT信号的处理函数sighandler。 当前正在执行
main函数,这时发生中断或异常切换到内核态。 在中断处理完毕后要返回用户态的main函数之前检查到有信号SIGQUIT递达。 内核决定返回用户态后不是恢复main函数的上下文继续执行,而是执行sighandler函 数,sighandler
和main函数使用不同的堆栈空间,它们之间不存在调用和被调用的关系,是 两个独立的控制流程。 sighandler函数返回后自动执行特殊的系统调用sigreturn再次进入内核态。 如果没有新的信号要递达,这次再返回用户态就是恢复main函数的上下文继续执行了。

信号排队

对于每一个目标进程,内核会用一个位图(bitmap)来记录信号的处理状态。如果一个信号还未被目标进程处理,那么它就是挂起/未决(pending)的状态。内核在向目标进程递送信号时,会查看进程对应的bitmap中,该信号对应的bit是否有挂起的信号。

如果当前有挂起的信号,按照Unix传统的做法,内核将直接丢弃这一信号,其结果当然就是该信号不能被目标进程正确接收。这样的信号存在丢失的风险,因而被称为“不可靠信号”。

而后出现的各种Unix衍生版本对此做出了一些改进,以Linux的实现为例,内核会为每个进程维护一组队列(queue),有挂起信号时,就将新来的信号排队(enqueue)。只要挂起的信号个数没有超过内核设定的上限,理论上就不会丢失,这样的信号被称为“可靠信号”。

在代码实现中,bitmap用sigset_t表示,而队列就是一个双向链表,链表头结点包含在进程对应的task_struct中。

struct sigpending {
struct list_head list;
sigset_t signal;
};
看起来不可靠信号和可靠信号只是Unix/Linux中处理信号的两种机制,跟信号本身无关,为什么把信号划分为不可靠信号和可靠信号呢?

因为早期的信号都是按不排队的方式实现的,它们的信号值在1到31之间,为了保持前向兼容性,Linux对[1, 31]之间的信号还是保持了原来的处理方式,只有对[SIGRTMIN, SIGRTMAX]之间的信号才采用排队的处理方式,因此[1, 31]之间的就是不可靠信号,[SIGRTMIN, SIGRTMAX]之间的就是实时(可靠)信号。

这里出现了一个"RT",它代表Real Time,这是来源于POSIX对可靠信号机制的标准化定义。在POSIX标准中,可靠信号被称为realtime signal,与之相对的不可靠信号则被称为regular signal。作为一种标准,POSIX只对可靠信号机制应具有的功能和对外接口做了定义,并没有对实现的具体方式作出要求。

你就算用sigqueue()函数去发送不可靠信号,它还是不会排队,还是不可靠,生来注定,不可逆转。你也不能怪sigqueue这个名字取的有歧义,人家本来就是为了配合会排队的可靠信号而生的,只不过为了兼容性,也可以用来发送不可靠信号罢了。

5.信号集操作函数

由于不同平台下,实现信号比特位的数据类型不同,我们不必关心如何实现的,只要能正确的使用下面的信号集操作函数完成我们的需求即可。

5.1 模拟信号函数集

下面这五个函数都是用来模拟出你想要信号,并不是直接设置信号。

#include <signal.h>
int sigemptyset(sigset_t *set);
int sigfillset(sigset_t *set);
int sigaddset (sigset_t *set, int signo);
int sigdelset(sigset_t *set, int signo);
int sigismember(const sigset_t *set, int signo);

第一个和第二个分别用来清空,和全部置1。
第三个和第四个用来增加和删除信号。
第五个用来判断是否存在这个信号。

5.2 sigprocmask函数

用来 屏蔽信号、解除屏蔽 也使用该函数。其本质,读取或修改进程的信号屏蔽字(PCB中)

严格注意,屏蔽信号:只是将信号处理延后执行(延至解除屏蔽);不是忽略。

int sigprocmask(int how, const sigset_t *set, sigset_t *oldset); 
成功:0;失败:-1,设置errno

参数:

  • set:传入参数,是一个位图,set中哪位置1,就表示当前进程屏蔽哪个信号。

  • oldset:传出参数,保存旧的信号屏蔽集。

  • how参数取值: 假设当前的信号屏蔽字为mask

      SIG_BLOCK: 当how设置为此值,set表示需要屏蔽的信号。相当于 mask = mask|set
      SIG_UNBLOCK: 当how设置为此,set表示需要解除屏蔽的信号。相当于 mask = mask & ~set
      SIG_SETMASK: 当how设置为此,set表示用于替代原始屏蔽及的新屏蔽集。相当于 mask = set,若调用sigprocmask解除了对当前若干个信号的阻塞,则在sigprocmask返回前,至少将其中一个信号递达。
    

5.3 sigpending函数

读取当前进程的未决信号集

int sigpending(sigset_t *set); 
set传出参数。   返回值:成功:0;失败:-1,设置errno

5.4 信号捕捉函数

常用两个捕捉函数

1)signal函数

注册一个信号捕捉函数:

当捕捉到signum信号时,执行这个handler函数

typedef void (*sighandler_t)(int);
sighandler_t signal(int signum, sighandler_t handler);

该函数由ANSI定义,由于历史原因在不同版本的Unix和不同版本的Linux中可能有不同的行为。因此应该尽量避免使用它,取而代之使用sigaction函数。

2)sigaction函数

修改信号处理动作(通常在Linux用其来注册一个信号的捕捉函数)

int sigaction(int signum, const struct sigaction *act, 
					struct sigaction *oldact);
成功:0;失败:-1,设置errno

参数:

act:传入参数,新的处理方式。
oldact:传出参数,旧的处理方式。

	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);
	};

sa_restorer:该元素是过时的,不应该使用,POSIX.1标准将不指定该元素。(弃用)
sa_sigaction:当sa_flags被指定为SA_SIGINFO标志时,使用该信号处理程序。(很少使用)
重点:

sa_handler:指定信号捕捉后的处理函数名(即注册函数)。也可赋值为SIG_IGN表忽略 或 SIG_DFL表执行默认动作
sa_mask: 调用信号处理函数时,所要屏蔽的信号集合(信号屏蔽字)。注意:仅在处理函数被调用期间屏蔽生效,是临时性设置。
sa_flags:通常设置为0,表使用默认属性。

5.5 验证与练习

下面这份代码和操作将证明上面的论述

  1 #include<stdio.h>                                                                                                                                                                                            
  2 #include<signal.h>
  3 #include<unistd.h>
  4  
  5 void sigcb(int sig)
  6 {                   
  7   printf("begin catch a sig:%d !\n",sig);
  8   sleep(5);           
  9   printf("end catch a sig:%d !\n",sig);
 10 }                     
 11                         
 12 void sigbitprintf(sigset_t *sig)
 13 {                                              
 14   int i=1;           
 15   for(i=1;i<32;i++) 
 16   {                                                                     
 17     if(sigismember(sig,i))
 18     {
 19       printf("1");
 20     }                                        
 21     else
 22     {                      
 23       printf("0");
 24     }
 25   }       
 26   printf("\n");
 27 }  
 28 int main()            
 29 {                       
 30              
 31   //signal(2,sigcb);
 32                                   
 33   struct sigaction zt;                   
 34   zt.sa_flags=0;
 35   zt.sa_handler=sigcb;
 36   sigaction(2,&zt,NULL);
 37    
 38   //sig用来设置block集,pend用来查看penging集。
 39   sigset_t sig, pend;
   sigemptyset(&sig);
 41   //sigfillset(&sig);         //这个函数是将传入的信号集:sig设置为全1。
 42   sigaddset(&sig,2);
 43   
 44   sigset_t osig;
 45   if(sigprocmask(SIG_SETMASK,&sig,&osig)==-1)
 46   {
 47     perror("sigprocmask!");
 48   }
 49   
 50   int i=0;
 51   while(1)
 52   {
 53     sigpending(&pend);
 54     sigbitprintf(&pend);
 55     if(i==10)
 56     {
 57       printf("取消屏蔽信号2!\n");
 58       sigprocmask(SIG_UNBLOCK,&sig,NULL);
 59     }
 60     i++;
 61     sleep(1);
 62   }
 63   return 0;
 64 }         

现象:由于是不可靠信号,没有对应的信号队列,在处理信号时,不论递达多少次信号,都只会处理一次。而对于可靠实时信号(34-64)则由信号队列完成多次信号处理。
在这里插入图片描述

6. Linux常规信号一览表

  1. SIGHUP: 当用户退出shell时,由该shell启动的所有进程将收到这个信号,默认动作为终止进程
  2. SIGINT:当用户按下了<Ctrl+C>组合键时,用户终端向正在运行中的由该终端启动的程序发出此信号。默认动作为终止进程。
  3. SIGQUIT:当用户按下<ctrl+>组合键时产生该信号,用户终端向正在运行中的由该终端启动的程序发出些信号。默认动作为终止进程。
  4. SIGILL:CPU检测到某进程执行了非法指令。默认动作为终止进程并产生core文件
  5. SIGTRAP:该信号由断点指令或其他 trap指令产生。默认动作为终止里程 并产生core文件。
  6. SIGABRT: 调用abort函数时产生该信号。默认动作为终止进程并产生core文件。
  7. SIGBUS:非法访问内存地址,包括内存对齐出错,默认动作为终止进程并产生core文件。
  8. SIGFPE:在发生致命的运算错误时发出。不仅包括浮点运算错误,还包括溢出及除数为0等所有的算法错误。默认动作为终止进程并产生core文件。
  9. SIGKILL:无条件终止进程。本信号不能被忽略,处理和阻塞。默认动作为终止进程。它向系统管理员提供了可以杀死任何进程的方法。
  10. SIGUSE1:用户定义 的信号。即程序员可以在程序中定义并使用该信号。默认动作为终止进程。
  11. SIGSEGV:指示进程进行了无效内存访问。默认动作为终止进程并产生core文件。
  12. SIGUSR2:另外一个用户自定义信号,程序员可以在程序中定义并使用该信号。默认动作为终止进程。
  13. SIGPIPE:Broken pipe向一个没有读端的管道写数据。默认动作为终止进程。
  14. SIGALRM: 定时器超时,超时的时间 由系统调用alarm设置。默认动作为终止进程。
  15. SIGTERM:程序结束信号,与SIGKILL不同的是,该信号可以被阻塞和终止。通常用来要示程序正常退出。执行shell命令Kill时,缺省产生这个信号。默认动作为终止进程。
  16. SIGSTKFLT:Linux早期版本出现的信号,现仍保留向后兼容。默认动作为终止进程。
  17. SIGCHLD:子进程结束时,父进程会收到这个信号。默认动作为忽略这个信号。
  18. SIGCONT:如果进程已停止,则使其继续运行。默认动作为继续/忽略。
  19. SIGSTOP:停止进程的执行。信号不能被忽略,处理和阻塞。默认动作为暂停进程。
  20. SIGTSTP:停止终端交互进程的运行。按下<ctrl+z>组合键时发出这个信号。默认动作为暂停进程。
  21. SIGTTIN:后台进程读终端控制台。默认动作为暂停进程。
  22. SIGTTOU: 该信号类似于SIGTTIN,在后台进程要向终端输出数据时发生。默认动作为暂停进程。
  23. SIGURG:套接字上有紧急数据时,向当前正在运行的进程发出些信号,报告有紧急数据到达。如网络带外数据到达,默认动作为忽略该信号。
  24. SIGXCPU:进程执行时间超过了分配给该进程的CPU时间 ,系统产生该信号并发送给该进程。默认动作为终止进程。
  25. SIGXFSZ:超过文件的最大长度设置。默认动作为终止进程。
  26. SIGVTALRM:虚拟时钟超时时产生该信号。类似于SIGALRM,但是该信号只计算该进程占用CPU的使用时间。默认动作为终止进程。
  27. SGIPROF:类似于SIGVTALRM,它不公包括该进程占用CPU时间还包括执行系统调用时间。默认动作为终止进程。
  28. SIGWINCH:窗口变化大小时发出。默认动作为忽略该信号。
  29. SIGIO:此信号向进程指示发出了一个异步IO事件。默认动作为忽略。
  30. SIGPWR:关机。默认动作为终止进程。
  31. SIGSYS:无效的系统调用。默认动作为终止进程并产生core文件。
  32. SIGRTMIN ~ (64) SIGRTMAX:LINUX的实时信号,它们没有固定的含义(可以由用户自定义)。所有的实时信号的默认动作都为终止进程。

7. SIGCHLD

我们知道用wait和waitpid函数清理僵尸进程,父进程可以阻塞等待子进程结束,也可以非阻 塞地查询是否有子进程结束等待清理(也就是轮询的方式)。采用第一种方式,父进程阻塞了就不 能处理自己的工作了;采用第二种方式,父进程在处理自己的工作的同时还要记得时不时地轮询一 下,程序实现复杂。

其实,子进程在终止时会给父进程发SIGCHLD信号,该信号的默认处理动作是忽略,父进程可以自 定义SIGCHLD信号的处理函数,这样父进程只需专心处理自己的工作,不必关心子进程了,子进程 终止时会通知父进程,父进程在信号处理函数中调用wait清理子进程即可。

事实上,由于UNIX 的历史原因,要想不产生僵尸进程还有另外一种办法:父进程调 用sigaction将SIGCHLD的处理动作置为SIG_IGN,这样fork出来的子进程在终止时会自动清理掉,不 会产生僵尸进程,也不会通知父进程。系统默认的忽
略动作和用户用sigaction函数自定义的忽略 通常是没有区别的,但这是一个特例。此方法对于Linux可用,但不保证在其它UNIX系统上都可 用。
在这里插入图片描述

在这里插入图片描述

总结

下面这张图以及本文局部内容摘录自该作者:我是管小亮

原文章链接: 信号产生和处理.
在这里插入图片描述

  • 6
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值