LV020-信号简介
本文主要是进程通信——信号的相关笔记,若笔记中有错误或者不合适的地方,欢迎批评指正😃。
一、信号简介
1. 信号的概念
信号是事件发生时对进程的通知机制,也可以把它称为软件中断。信号与硬件中断的相似之处在于能够打断程序当前执行的正常流程,其实是在软件层次上对中断机制的一种模拟。大多数情况下,是无法预测信号达到的准确时间,所以,信号提供了一种处理异步事件的方法。
信号的目的都是用于通信的,当发生某种情况下,通过信号将情况告知相应的进程,从而达到同步、通信的目的。
另外信号是异步事件,而且是进程通信中唯一的异步通信机制,产生信号的事件对进程而言是随机出现的,进程无法预测该事件产生的准确时间,进程不能够通过简单地测试一个变量或使用系统调用来判断是否产生了一个信号,这就如同硬件中断事件,程序是无法得知中断事件产生的具体时间,只有当产生中断事件时,才会告知程序、然后打断当前程序的正常执行流程、跳转去执行中断服务函数,这就是异步处理方式。
2. 信号的产生
- 硬件发生异常
就是硬件检测到错误条件并通知内核,随即再由内核发送相应的信号给相关进程。硬件检测到异常的例子包括执行一条异常的机器语言指令,诸如,除数为0、数组访问越界导致引用了无法访问的内存区域等,这些异常情况都会被硬件检测到,并通知内核、然后内核为该异常情况发生时正在运行的进程发送适当的信号以通知进程。
- 在终端下输入了能够产生信号的特殊字符
之前我们结束一个进程都是使用Ctrl+C,其实这样一个组合按键是产生了一个中断信号(SIGINT),通过这个信号可以终止在前台运行的进程;还有其他的组合键,例如按下Ctrl + Z组合按键可以产生暂停信号(SIGCONT),通过这个信号可以暂停当前前台运行的进程。
- 进程调用
kill()系统调用可将任意信号发送给另一个进程或进程组
接收信号的进程和发送信号的进程的所有者必须相同,亦或者发送信号的进程的所有者是root超级用户。
- 通过
kill命令将信号发送给其它进程。
kill命令其实我们前边有使用过,通常我们会通过kill命令来杀死(终止)一个进程,例如在终端下执行kill -9 xxx来杀死PID为xxx的进程。kill命令其内部的实现原理便是通过kill()系统调用来完成的。
- 发生软件事件
也就是检测到某种软件条件已经发生。这里指的不是硬件产生的条件(如除数为0、引用无法访问的内存区域等),而是软件的触发条件、触发了某种软件条件(进程所设置的定时器已经超时、进程执行的CPU时间超限、进程的某个子进程退出等等情况)。
其实进程同样也可以向自身发送信号,然而发送给进程的诸多信号中,大多数都是来自于内核。
3. 信号的处理方式
信号通常是发送给对应的进程,当信号到达后,该进程需要做出相应的处理措施,通常进程会根据信号进行如下操作。
- 忽略信号
当信号到达进程后,该进程直接忽略,就好像是没有出现该信号,信号对该进程不会产生任何影响。事实上,大多数信号都可以使用这种方式进行处理,但有两种信号却决不能被忽略,它们是SIGKILL和SIGSTOP,这是因为它们向内核和超级用户提供了使进程终止或停止的可靠方法。另外,如果忽略某些由硬件异常产生的信号,则进程的运行行为是未定义的。
- 捕获信号
当信号到达进程后,执行预先绑定好的信号处理函数。为了做到这一点,要通知内核在某种信号发生时,执行用户自定义的处理函数,该处理函数中将会对该信号事件作出相应的处理,Linux系统提供了signal()系统调用可用于注册信号的处理函数。
- 执行系统默认操作
当信号到达进程后,进程不对该信号事件作出处理,而是交由系统进行处理,每一种信号都会有其对应的系统默认的处理方式。需要注意的是,对大多数信号来说,系统默认的处理方式就是终止该进程。
进程对信号的处理是可以通过函数来修改的,后边会详细学习。
4. 都有哪些信号
上边说到了几个信号,那在我们的Linux系统中,有多少种信号呢?信号在本质上其实是int类型的数字编号,这些数字从1开始,定义在.h,文件中,我们可以使用如下命令查找该文件的位置:
locate signum.h当然我们也可以通过终端直接打印出支持的信号,命令如下:
kill -l然后终端便会有如下信息显示:
1) SIGHUP 2) SIGINT 3) SIGQUIT 4) SIGILL 5) SIGTRAP
6) SIGABRT 7) SIGBUS 8) SIGFPE 9) SIGKILL 10) SIGUSR1
11) SIGSEGV 12) SIGUSR2 13) SIGPIPE 14) SIGALRM 15) SIGTERM
16) SIGSTKFLT 17) SIGCHLD 18) SIGCONT 19) SIGSTOP 20) SIGTSTP
21) SIGTTIN 22) SIGTTOU 23) SIGURG 24) SIGXCPU 25) SIGXFSZ
26) SIGVTALRM 27) SIGPROF 28) SIGWINCH 29) SIGIO 30) SIGPWR
31) SIGSYS 34) SIGRTMIN 35) SIGRTMIN+1 36) SIGRTMIN+2 37) SIGRTMIN+3
38) SIGRTMIN+4 39) SIGRTMIN+5 40) SIGRTMIN+6 41) SIGRTMIN+7 42) SIGRTMIN+8
43) SIGRTMIN+9 44) SIGRTMIN+10 45) SIGRTMIN+11 46) SIGRTMIN+12 47) SIGRTMIN+13
48) SIGRTMIN+14 49) SIGRTMIN+15 50) SIGRTMAX-14 51) SIGRTMAX-13 52) SIGRTMAX-12
53) SIGRTMAX-11 54) SIGRTMAX-10 55) SIGRTMAX-9 56) SIGRTMAX-8 57) SIGRTMAX-7
58) SIGRTMAX-6 59) SIGRTMAX-5 60) SIGRTMAX-4 61) SIGRTMAX-3 62) SIGRTMAX-2
63) SIGRTMAX-1 64) SIGRTMAX当我们需要t通过终端发送某个信号给进程时,可以使用如下命令,大但是我们需要先知道接收信号的进程的PID,
kill [-signal] <pid>signal即为信号对应的int类型数字,例如,我们要杀死一个进程,使用的命令如下:
kill -9 <pid>5. 常用信号及含义
上边那么多的信号,我们并不一定都用得上,常用的几个如下所示:
term表示终止进程core表示生成可用于调试的核心转储文件ignore表示忽略信号continue表示继续运行进程pause表示暂停进程
| 编号 | 信号名 | 默认操作 | 含义 |
| 1 | SIGHUP | term | 在用户终端关闭时产生,通常是发给和该终端关联的会话内的所有进程 |
| 2 | SIGINT | term | 该信号在用户键入INTR字符(Ctrl-C)时产生,内核发送此信号到当前终端的所有前台进程 |
| 3 | SIGQUIT | term+core | 该信号和SIGINT类似,但由QUIT字符(通常是Ctrl-\)来产生,进程如果陷入无限循环、或不再响应时,使用SIGQUIT信号就很合适 |
| 4 | SIGILL | term+core | 该信号在一个进程企图执行一条非法指令时产生 |
| 6 | SIGABRT | term+core | 当进程调用abort()系统调用时(进程异常终止),系统会向该进程发送SIGABRT信号 |
| 7 | SIGBUS | term+core | 总线错误(bus error)信号,表示发生了某种内存访问错误 |
| 8 | SIGFPE | term+core | 该信号因特定类型的算术错误而产生,例如除以0 |
| 9 | SIGKILL | term | 该信号用来结束进程,并且不能被捕捉和忽略 |
| 10 | SIGUSR1 | term | 该信号保留给用户程序使用,内核绝不会为进程产生这些信号,在我们的程序中,可以使用这些信号来互通通知事件的发生,或是进程彼此同步操作 |
| 11 | SIGSEGV | term | 该信号在非法访问内存时产生,如野指针、缓冲区溢出 |
| 12 | SIGUSR2 | term | 该信号保留给用户程序使用,内核绝不会为进程产生这些信号,在我们的程序中,可以使用这些信号来互通通知事件的发生,或是进程彼此同步操作 |
| 13 | SIGPIPE | term | 当进程向已经关闭的管道、FIFO或套接字写入信息时,那么系统将发送该信号给进程 |
| 14 | SIGALRM | term | 该信号用于通知进程定时器时间已到,与系统调用alarm()或setitimer()有关 |
| 15 | SIGTERM | term | 终止进程的标准信号,也是kill命令所发送的默认信号。有时我们会直接使用"kill -9 xxx"显式向进程发送SIGKILL信号来终止进程,然而这一做法通常是错误的,精心设计的应用程序应该会捕获SIGTERM信号、并为其绑定一个处理函数,当该进程收到SIGTERM信号时,会在处理函数中清除临时文件以及释放其它资源,再而退出程序。如果直接使用SIGKILL信号终止进程,从而跳过了SIGTERM信号的处理函数,通常SIGKILL终止进程是不友好且暴力的方式,这种方式应该作为最后手段,应首先尝试使用SIGTERM,而将SIGKILL作为最后手段 |
| 17 | SIGCHLD/SIGCLD | ignore | 当父进程的某一个子进程终止时,内核会向父进程发送该信号。当父进程的某一个子进程因收到信号而停止或恢复时,内核也可能向父进程发送该信号。注意这里说的停止并不是终止,我们可以理解为暂停 |
| 18 | SIGCONT | continue | 该信号让进程进入运行态 |
| 19 | SIGSTOP | pause | 该信号用于暂停进程,并且不能被捕捉和忽略 |
| 20 | SIGTSTP | pause | 该信号用于暂停进程,用户可键入SUSP字符(通常是Ctrl-Z)发出这个信号,按下组合键后系统会将SIGTSTP信号发送给前台进程组中的每一个进程,使其暂停运行 |
| 24 | SIGXCPU | term+core | 当进程的CPU时间超出对应的资源限制时,内核将发送此信号给该进程 |
| 26 | SIGVTALRM | term | 应用程序调用setitimer()函数设置一个虚拟定时器,当定时器定时时间到时,内核将会发送该信号给进程 |
| 28 | SIGWINCH | ignore | 在窗口环境中,当终端窗口尺寸发生变化时(例如用户手动调整了大小,应用程序调用ioctl()设置了大小等),系统会向前台进程组中的每一个进程发送该信号 |
| 29 | SIGPOLL/SIGIO | term/ignore | 用于提示一个异步IO事件的发生,例如应用程序打开的文件描述符发生了I/O事件时,内核会向应用程序发送SIGIO信号 |
| 31 | SIGSYS | term+core | 如果进程发起的系统调用有误,那么内核将发送该信号给对应的进程 |
6. 信号分类
Linux系统下可对信号从两个不同的角度进行分类,从可靠性方面将信号分为可靠信号与不可靠信号;而从实时性方面将信号分为实时信号与非实时信号。
6.1 可靠信号与不可靠信号
Linux信号机制基本上是从UNIX系统中继承过来的,早期UNIX系统中的信号机制比较简单和原始,后来在实践中暴露出一些问题,进程每次处理信号后,就将对信号的响应设置为系统默认操作。在某些情况下,将导致对信号的错误处理;因此,用户如果不希望这样的操作,那么就要在信号处理函数结尾再一次调用signal(),重新为该信号绑定相应的处理函数。
早期UNIX下的不可靠信号主要指的是进程可能对信号做出错误的反应以及信号可能丢失(处理信号时又来了新的信号,则导致信号丢失)。 Linux支持不可靠信号,但是对不可靠信号机制做了改进:在调用完信号处理函数后,不必重新调用signal()。因此,Linux下的不可靠信号问题主要指的是信号可能丢失。在Linux系统下,信号值小于SIGRTMIN(34)(编号为1~31)的信号都是不可靠信号,这就是不可靠信号的来源。
随着时间的发展,实践证明,有必要对信号的原始机制加以改进和扩充,所以,后来出现的各种UNIX版本分别在这方面进行了研究,力图实现可靠信号。由于原来定义的信号已有许多应用,不好再做改动,最终只好又新增加了一些信号SIGRTMIN~SIGRTMAX,编号为34~64,并在一开始就把它们定义为可靠信号。可靠信号并没有一个具体对应的名字,而是使用了SIGRTMIN+N或SIGRTMAX-N的方式来表示。
可靠信号支持排队,不会丢失,同时,信号的发送和绑定也出现了新版本,信号发送函数sigqueue()及信号绑定函数sigaction()。
6.2 实时信号与非实时信号
实时信号与非实时信号其实是从时间关系上进行的分类,与可靠信号与不可靠信号是相互对应的,非实时信号都不支持排队,都是不可靠信号,一般我们也把非实时信号(不可靠信号)称为标准信号;实时信号都支持排队,都是可靠信号。实时信号保证了发送的多个信号都能被接收,实时信号是POSIX标准的一部分,可用于应用进程。
7. 信号描述信息
在Linux下,每个信号都有一串与之相对应的字符串描述信息,用于对该信号进行相应的描述。这些字符串位于sys_siglist数组中,sys_siglist数组是一个char *类型的数组,数组中的每一个元素存放的是一个字符串指针,指向一个信号描述信息。
7.1 strsignal()
7.1.1 函数说明
在linux下可以使用man 3 strsignal命令查看该函数的帮助手册。
/* 需包含的头文件 */
#include <string.h>
/* 函数声明 */
char *strsignal(int sig);
extern const char * const sys_siglist[];【函数说明】该函数可以用于获取信号的描述信息。
【函数参数】
sig:int类型,需要显示详细信息的信号的宏(需要加上<signal.h>头文件)或者编号。
【返回值】char *类型,返回执行信号描述信息字符串的指针;函数会对参数sig进行检查,若传入的sig无效,则会返回Unknown signal信息。
【使用格式】一般情况下基本使用格式如下:
/* 需要包含的头文件 */
#include <string.h>
/* 至少应该有的语句 */
strsignal(sig);【注意事项】none
7.1.2 使用实例
/* 头文件 */
#include <stdio.h> /* perror */
#include <string.h>
#include <signal.h>
/* 主函数 */
int main(int argc, char *argv[])
{
printf("SIGINT Description: %s\n", strsignal(SIGINT));
printf("SIGQUIT Description: %s\n", strsignal(SIGQUIT));
printf("SIGBUS Description: %s\n", strsignal(SIGBUS));
return 0;
}在终端执行以下命令编译程序:
gcc test.c -Wall # 生成可执行文件 a.out
./a.out # 执行可执行程序然后,终端会有以下信息显示:
SIGINT Description: Interrupt
SIGQUIT Description: Quit
SIGBUS Description: Bus error7.2 psignal()
7.2.1 函数说明
在linux下可以使用man 3 psignal命令查看该函数的帮助手册。
/* 需包含的头文件 */
#include <signal.h>
/* 函数声明 */
void psignal(int sig, const char *s);【函数说明】该函数可以在标准错误(stderr)上输出信号描述信息,常用来输出信号的出错消息。
【函数参数】
sig:int类型,需要显示详细信息的信号的宏(需要加上<signal.h>头文件)或者编号。s:char *类型,调用该函数时添加的一些提示信息,由s指定,所以整个输出信息由字符串s、冒号、空格、描述信号编号sig的字符串和尾随的换行符组成。
【返回值】none
【使用格式】一般情况下基本使用格式如下:
/* 需要包含的头文件 */
#include <signal.h>
/* 至少应该有的语句 */
psignal(sig, "Description");【注意事项】none
7.2.2 使用实例
/* 头文件 */
#include <stdio.h> /* perror */
#include <signal.h>
/* 主函数 */
int main(int argc, char *argv[])
{
psignal(SIGINT, "SIGINT");
psignal(SIGQUIT, "SIGQUIT");
psignal(SIGBUS, "SIGBUS");
return 0;
}在终端执行以下命令编译程序:
gcc test.c -Wall # 生成可执行文件 a.out
./a.out # 执行可执行程序然后,终端会有以下信息显示:
SIGINT: Interrupt
SIGQUIT: Quit
SIGBUS: Bus error