一.概述:

软中断信号(signal, 简称信号)是用来通知进程发生了异步事件。在软件层次上是对中断的一种模拟,在原理上,一个进程收到一个信号与处理器收到一个中断请求可以说是一样的。信号是进程间通信机制中唯一的异步通信机制,一个进程不必通过任何操作来等待信号的到达,事实上,进程也不知道信号到底什么时候到达。进程之间可以互相通过系统调用kill发送软中断信号。内核也可以因为内部事件而给进程发送信号,通知进程发生了某个事件。信号机制除了基本通知功能外,还可以传递附加信息。


收到信号的进程对各种信号的处理方法:

(1).忽略此信号。

(2).执行信号的默认动作。

(3).提供一个信号处理函数,要求内核在处理该信号时切换到用户态执行这个处理函数,这种方式称为捕捉(Catch)一个信号。

PS:查看信号列表:kill -l      查看各个信号的默认处理动作:man 7 signal


信号产生的一般方式:

(1).通过键盘输入产生信号。如:ctrl+c (SIGINT)   ctrl+\(SIGQUIT) ctrl+z(SIGTSTP)等

(2).硬件产生信号。如:被除数为0,访问非法地址等

(3).软件(系统接口)产生信号。


信号在内核中的表示:

wKiom1csGv7h-vv8AACKduE04-s987.png

以上我们讨论了信号产生(Generation )的各种原因,而实际执行信号的处理动作称为信号递(Delivery),信号从产生到递达之间的状态,称为信号未决(Pending)。进程可以选择阻塞(Block )某个信号。被阻塞的信号产生时将保持在未决状态,直到进程解除对此信号的阻塞,才 执行递达的动作。注意,阻塞和忽略是不同的,只要信号被阻塞就不会递达,而忽略是在递达之后 可选的一种处理动作。


每个信号都有两个标志位分别表示阻塞(block)和未决(pending),还有一个函数指针表示处理动作。信号产生时,内核在进程控制块中设置该信号的未决标志,直到信号递达才清除该标志。


(1). SIGHUP信号未阻塞也未产生过,当它递达时执行默认处理动作。
(2). SIGINT信号产生过,但正在被阻塞,所以暂时不能递达。虽然它的处理动作是忽略,但在没 有解除阻塞之前不能忽略这个信号,因为进程仍有机会改变处理动作之后再解除阻塞。
(3). SIGQUIT信号未产生过,一旦产生SIGQUIT信号将被阻塞,它的处理动作是用户自定义函数sighandler。


PS:block和pending状态是通过bit位的0或1来表示的,这是操作系统做的事,我们并不能直接打印出各个bit位,但可以用操作系统提供的API来模拟查看各bit位的状态。


如果在进程解除对某信号的阻塞之前这种信号产生过多次,将如何处理?POSIX.1允许系统递送该信号一次或多次。Linux是这样实现的:常规信号在递达之前产生多次只计一次,而实时信号在递达之前产生多次可以依次放在一个队列里里。在此不讨论实时信号。从上图来看,每个信号只有一 个bit的未决标志,非0即1,不记录该信号产生了多少次,阻塞标志也是这样表示的。因此,未决和阻塞标志可以用相同的数据类型sigset_t来存储,sigset_t称为信号集,这个类型可以表示每个信号的“有效”或“无无效”状态,在阻塞信号集中“有效”和“无无效”的含义是该信号是否被阻塞,而在未决信号集中“有效”和“无无效”的含义是该信号是否处于未决状态。 阻塞信号集也叫做当前进程的信号屏蔽字(Signal Mask),这里里的“屏蔽”应该理解为阻塞而而不是忽略。



信号在内核中的执行过程:

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

如下图:

wKiom1csIHfBzfQjAAK3JjmEaMI550.png

此图很重要,一定要记住!!!



Linux支持的信号列表如下。(很多信号是与机器的体系结构相关的)

信号        类似信号值       默认处理动作               发出信号的原因

SIGHUP              1                     A                            终端挂起或者控制进程终止

SIGINT                2                     A                            键盘中断(如break键被按下)

SIGQUIT             3                     C                             键盘的退出键被按下

SIGILL                4                      C                           非法指令

SIGABRT            6                      C                           由abort(3)发出的退出指令

SIGFPE               8                      C                           浮点异常

SIGKILL              9                     AEF                        Kill信号

SIGSEGV           11                    C                            无效的内存引用

SIGPIPE            13                    A                             管道破裂: 写一个没有读端口的管道

SIGALRM         14                     A                             由 alarm(2)发出的信号

SIGTERM         15                     A                              终止信号

SIGUSR1          30,10,16         A                             用户自定义信号1

SIGUSR2          31,12,17         A                              用户自定义信号2

SIGCHLD         20,17,18         B                              子进程结束信号

SIGCONT        19,18,25        进程继续                (曾被停止的进程)

SIGSTOP         17,19,23         DEF                          终止进程

SIGTSTP          18,20,24         D                              控制终端(tty)上按下停止键

SIGTTIN          21,21,26         D                              后台进程企图从控制终端读

SIGTTOU        22,22,27         D                               后台进程企图从控制终端写


处理动作一项中的字母含义如下

A 缺省的动作是终止进程

B 缺省的动作是忽略此信号,将该信号丢弃,不做处理

C 缺省的动作是终止进程并进行内核映像转储(dump core),内核映像转储是指将进程数据在内存的映像和进程在内核结构中的部分内容以一定格式转储到文件系统,并且进程退出执行,这样做的好处是为程序员 提供了方便,使得他们可以得到进程当时执行时的数据值,允许他们确定转储的原因,并且可以调试他们的程序。

D 缺省的动作是停止进程,进入停止状况以后还能重新进行下去,一般是在调试的过程中(例如ptrace系统调用)

E 信号不能被捕获

F 信号不能被忽略


参考资料:

http://www.jb51.net/LINUXjishu/173601.html



二.相关API:

(1).kill函数:  int kill(pid_t pid, int signo)

函数功能:向进程号为pid的进程发送一个signo信号。

返回值:成功返回0,失败返回-1。

PS:kill -信号 进程号     这个命令的内部实现是调用了kill函数,如果此命令不明确指定信号则发送
SIGTERM信号,该信号的默认处理动作是终止进程。


(2).raise函数:int raise(int signo)

函数功能:给当前进程发送一个signo信号。

返回值:成功返回0,失败返回-1。


(3).abort函数:void abort(void)

函数功能:使当前进程接收到SIGABRT信号而异常终止。

PS:就像exit函数一样,abort函数总是会成功的,所以没有返回值。


(4).alarm函数:unsigned int alarm(unsigned int seconds)

函数功能:设定一个闹钟,也就是告诉内核在seconds秒之后给当前进程发SIGALRM信号, 该信号的默认处理动作是终止当前进程。

返回值:是0或者是以前设定的闹钟时间还余下 的秒数。

PS:seconds值为0,则表示取消以前设定的闹钟,函数的返回值仍然是以前设定的闹钟时间还余下的秒数。


(5).信号集操作函数:

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

函数sigemptyset初始化set所指向的信号集,使其中所有信号的对应bit清零,表示该信号集不包含 任何有效信号。

函数sigfillset初始化set所指向的信号集,使其中所有信号的对应bit置1,表示 该信号集的有效信号包括系统支持的所有信号。

PS:在使用sigset_t类型的变量之前,一定要调 用sigemptyset或sigfillset做初始化,使信号集处于确定的状态。

初始化sigset_t变量之后就可以 在调用sigaddset和sigdelset在该信号集中添加或删除某种有效信号。

PS:这四个函数都是成功返回0,出错返回-1。

sigismember是一个布尔函数,用于判断一个信号集的有效信号中是否包含某种 信号,若包含则返回1,不包含则返回0,出错返回-1。


(6).sigprocmask函数:int sigprocmask(int how, const sigset_t* set, sigset_t* oldset)

函数功能:读取或更改进程的信号屏蔽字。

set参数:非空则表示更改进程的信号屏蔽字,how表示如何更改。

oldset参数:非空则读取当前的信号屏蔽字,并通过oldset传出。(相当于保持原来的信号屏蔽字于oldset中)

PS:如果oldset和set都是非空指针,则先将原来的信号屏蔽字备份到oldset里,然后根据set和how参数更改信号屏蔽字。

how参数:

  how

  说明

  SIG_BLOCK  该进程新的信号屏蔽字是其当前信号屏蔽字和set指向信号集的并集。set包含了我们希望阻塞的附加信号。相当于mask=mask|set
  SIG_UNBLOCK  该进程新的信号屏蔽字是其当前信号屏蔽字和set所指向信号集补集的交集。set包含了我希望解除阻塞的信号。相当于mask=mask&~set
  SIG_SETMASK  该进程新的信号屏蔽字将被set指向的信号集的值代替。相当于mask=set

PS:只要关注set中的信号屏蔽字即可。添加,删除或者直接等于set中的信号屏蔽子中的有效信号。


(7).sigpending函数:int sigpending(sigset_t* set)

函数功能:读取当前进程的未决信号集,并通过set传出。

返回值:成功返回0,失败返回-1。


(8).signal函数:

typedef void (*sighandler_t )(int)

sighandlet_t signal(int signum, sighandler_t handler)

函数功能:为信号signum定制handler动作。

handler参数:

  1. SIG_IGN 忽略信号

  2. SIG_DFL 执行默认动作

  3. 捕捉信号后调用的函数地址

PS:SIG_IGN和SIG_DEL在系统中就是一个函数指针,还有sighandlet_t的回调函数中有一个int参数,一般传入的是signum。

返回值:成功时返回上一次调用signal时的handler的值,失败返回SIG_ERR(signal()  returns  the previous value of the signal handler, or SIG_ERR on error.)


(9).sigaction函数:

int sigaction(int signo, const struct sigaction* act, struct sigaction* oact)

函数功能:sigaction函数可以读取和修改与指定信号相关联的处理动作。

act参数:如果非空,则根据act修改该信号的处理动作。

oact参数:如果非空,则读取原来的处理动作,并通过oact传出。

PS:如果act和oact都非空,则先将原来的处理动作备份到oseact中,然后根据act更改此信号的处理动作。前者为非空、后者为空则为修改处理动作,前者为空、后者为非空则为读取处理动作。

返回值:成功返回0,出差返回-1。

sigaction结构体:

struct sigaction{
  void (*sa_handler)(int);      
   sigset_t sa_mask;
  int sa_flag;
  void (*sa_sigaction)(int,siginfo_t *,void *);
};

sa_handler字段包含一个信号捕捉函数的地址

sa_mask字段说明了一个信号集,在调用该信号捕捉函数之前,这一信号集要加进进程的信号屏蔽字中。仅当从信号捕捉函数返回时再将进程的信号屏蔽字复位为原先值。

sa_flag是一个选项:(一般可以为0)

SA_INTERRUPT 由此信号中断的系统调用不会自动重启
SA_RESTART 由此信号中断的系统调用会自动重启

SA_SIGINFO 提供附加信息,一个指向siginfo结构的指针以及一个指向进程上下文标识符的指针

最后一个参数是一个替代的信号处理程序,为实时信号处理函数,当设置SA_SIGINFO时才会用它。

PS:

(1).sa_handler指向的函数也有一个int参数,一般传入的是signo。

(2).当某个信号的处理函数被调用时,内核自动将当前信号加入进程的信号屏蔽字,当信号处理函数返回时自动恢复原来的信号屏蔽字,这样就保证了在处理某个信号时,如果这种信号再次产生,那么 它会被阻塞到当前处理结束为止。而sa_mask是将其它信号也加入信号屏蔽字中。        


(10).pause函数:int pause(void )             (pause:暂停)

函数功能:pause函数使当前进程挂起,直到有信号递达。(也就是说,只有执行自定义函数是才有机会返回,因为如果默认动作是终止进程,就不会执行pause函数,如果是忽略,则没有信号递达,也就没返回)

返回值:如果是处理动作终止进程或是忽略,则没有机会返回,如果信号的处理动作是捕捉,则调用了信号处理函数之后pause返回-1,errno设置为EINTR(被信号中断), 所以pause只有出错的返回值。




三.相关代码:

(1).用库函数实现打印出pending表中各个bit位的状态:(注意,我们是不能直接打印pending表的)

  1 #include<stdio.h>                                                           
  2 #include<signal.h>
  3 #include<unistd.h>
  4 
  5 void printsig(sigset_t *pend)
  6 {
  7     int i = 1;
  8     for(;i <= 32;i++)
  9     {   
 10         if(sigismember(pend, i))
 11         {   
 12             printf("1");
 13         }
 14         else
 15         {   
 16             printf("0");
 17         }
 18     }
 19     printf("\n");
 20 }
 21 
 22 
 23 
 24 int main()
 25 {
 26     sigset_t block,pending;
 27     sigemptyset(&block);
 28     sigemptyset(&pending);
 29     sigaddset(&block, SIGINT);   //阻塞ctrl+c产生的信号
 30     sigprocmask(SIG_SETMASK, &block, NULL);
 31     while(1)
 32     {
 33         sigpending(&pending);
 34         printsig(&pending);
 35         sleep(1);
 36     }
 37 }                                                                           
                                                              37,1         底端

执行结果:

wKioL1csPyKyUH49AAA2F3q5QoQ419.png

PS:执行ctrl+c是不会终止该进程的   要执行ctrl+\才能终止该进程。



(2).模拟实现一个sleep函数:

  1 #include<stdio.h>                                                                                                                                            
  2 #include<signal.h>
  3 #include<unistd.h>
  4 
  5 void DoNothing(int signal)
  6 {
  7     ;
  8 }
  9 
 10 
 11 int MySleep(int seconds)
 12 {
 13     struct sigaction handler, old;
 14     int ret = 0;
 15     handler.sa_handler = DoNothing;
 16     sigemptyset(&handler.sa_mask);
 17     handler.sa_flags = 0;
 18     sigaction(SIGALRM, &handler, &old);
 19     alarm(seconds);
 20     pause();
 21     ret = alarm(0);                          //清空闹钟 防止在seconds秒之前系统可能发送了一个SIGALRM,seconds秒之后再发送此信号 
 22     sigaction(SIGALRM, &old, NULL);   //恢复SIGALRM信号的默认处理动作 防止以后这个信号再出现时动作被修改
 23     return ret;
 24 }
 25 
 26 
 27 int main()
 28 {
 29     while(1)
 30     {
 31         printf("after 5 seconds wakeup!\n");
 32         MySleep(5);
 33     }
 34     return 0;
 35 }

执行结果:

wKioL1csQSWyStNcAAApPyWCKyU982.png



总结:信号又称为软中断信号,其在软件层次上是对中断的一种模拟,就像线程是轻量级进程一样,都是对另一种技术的模拟。在信号知识点中,要掌握信号的三种常见的产生情况、三种处理动作,尤其要掌握信号在内核中的表示和信号在内核中的执行过程。