您的位置:首页 > 运维架构 > Linux

学习Linux(37)信号

2020-07-25 18:39 204 查看

概述
信号(signal),又称为软中断信号,用于通知进程发生了异步事件,它是Linux系统响应某些条件而产生的一个事件,它是在软件层次上对中断机制的一种模拟,是一种异步通信方式,在原理上,一个进程收到一个信号与处理器收到一个中断请求可以说是一样的。
信号是进程间通信机制中唯一的异步通信机制,一个进程不必通过任何操作来等待信号的到达,事实上,进程也不知道信号到底什么时候到达。正如我们所了解的中断服务函数一样,在中断发生的时候,就会进入中断服务函数中去处理,同样的,当进程接收到一个信号的时候,也会相应地采取一些行动。我们可以使用术语“生成(raise)”表示一个信号的产生,使用术语“捕获(catch)”表示进程接收到一个信号。
在Linux系统中,信号可能是由于系统中某些错误而产生,也可以是某个进程主动生成的一个信号。由于某些错误条件而生成的信号:如内存段冲突、浮点处理器错误或非法指令等,它们由shell和终端处理器生成并且引起中断。由进程主动生成的信号可以作为在进程间传递通知或修改行为的一种方式,它可以明确地由一个进程发送给另一个进程,当进程捕获了这个信号就会按照程序进行相应的处理。无论何种情况,它们的编程接口都是相同的,信号可以被生成、捕获、响应或忽略。进程之间可以互相发送信号,内核也可以因为内部事件而给进程发送信号,通知进程发生了某个事件。
ctrl + c:中断信号
ctrl + |:退出信号
ctrl + z:停止信号

keill 杀死进程

pkeill 杀死进程

系统支持的信号
我们可以使用kill 命令来查看系统中支持的信号种类

Linux系统支持信号62种信号,每种信号名称都以SIG三个字符开头,注意,没有信号值为32和33的信号。
可以将这62中信号分为2大类:信号值为1~31的信号属性非实时信号(也称为不可靠信号),它们是从UNIX 系统中继承下来的信号,具体的作用见表格,信号值为34~64的信号为实时信号(也被称为可靠信号)。
信号值 名称 描述 默认处理
1 SIGHUP 控制终端被关闭时产生。 终止
2 SIGINT 程序终止(interrupt)信号,在用户键入INTR字符(通常是Ctrl + C)时发出,用于通知前台进程组终止进程。 终止
3 SIGQUIT SIGQUIT 和SIGINT类似,但由QUIT字符(通常是Ctrl + )来控制,进程在因收到SIGQUIT退出时会产生core文件,在这个意义上类似于一个程序错误信号。 终止并产生转储文件(core文件)
4 SIGILL CPU检测到某进程执行了非法指令时产生,通常是因为可执行文件本身出现错误, 或者试图执行数据段、堆栈溢出时也有可能产生这个信号。 终止并产生转储文件(core文件)
5 SIGTRAP 由断点指令或其它trap指令产生,由debugger使用。 终止并产生转储文件(core文件)
6 SIGABRT 调用系统函数 abort()时产生。 终止并产生转储文件(core文件)
7 SIGBUS 总线错误时产生。一般是非法地址,包括内存地址对齐(alignment)出错。比如访问一个四个字长的整数,但其地址不是4的倍数。它与SIGSEGV的区别在于后者是由于对合法存储地址的非法访问触发的(如访问不属于自己存储空间或只读存储空间)。 终止并产生转储文件(core文件)
8 SIGFPE 处理器出现致命的算术运算错误时产生,不仅包括浮点运算错误,还包括溢出及除数为0等其它所有的算术的错误。 终止并产生转储文件(core文件)
9 SIGKILL 系统杀戮信号。用来立即结束程序的运行,本信号不能被阻塞、处理和忽略。如果管理员发现某个进程终止不了,可尝试发送这个信号将进程杀死。 终止
10 SIGUSR1 用户自定义信号。 终止
11 SIGSEGV 访问非法内存时产生,进程试图访问未分配给自己的内存,或试图往没有写权限的内存地址写数据。 终止
12 SIGUSR2 用户自定义信号。 终止
13 SIGPIPE 这个信号通常在进程间通信产生,比如采用FIFO(管道)通信的两个进程,读管道没打开或者意外终止就往管道写,写进程会收到SIGPIPE信号。此外用Socket通信的两个进程,写进程在写Socket的时候,读进程已经终止,也会产生这个信号。 终止
14 SIGALRM 定时器到期信号,计算的是实际的时间或时钟时间,alarm函数使用该信号。 终止
15 SIGTERM 程序结束(terminate)信号,与SIGKILL不同的是该信号可以被阻塞和处理。通常用来要求程序自己正常退出,shell命令kill缺省产生这个信号,如果进程终止不了,才会尝试SIGKILL。 终止
16 SIGSTKFLT 已废弃。 终止
17 SIGCHLD 子进程暂停或终止时产生,父进程将收到这个信号,如果父进程没有处理这个信号,也没有等待(wait)子进程,子进程虽然终止,但是还会在内核进程表中占有表项,这时的子进程称为僵尸进程,这种情况我们应该避免。父进程默认是忽略SIGCHILD信号的,我们可以捕捉它,做成异步等待它派生的子进程终止,或者父进程先终止,这时子进程的终止自动由init进程来接管。 忽略
18 SIGCONT 系统恢复运行信号,让一个停止(stopped)的进程继续执行,本信号不能被阻塞,可以用一个handler来让程序在由stopped状态变为继续执行时完成特定的工作 恢复运行
19 SIGSTOP 系统暂停信号,停止进程的执行。注意它和terminate以及interrupt的区别:该进程还未结束,只是暂停执行,本信号不能被阻塞,处理或忽略。 暂停
20 SIGTSTP 由控制终端发起的暂停信号,停止进程的运行,但该信号可以被处理和忽略,比如用户键入SUSP字符时(通常是Ctrl+Z)发出这个信号。 暂停
21 SIGTTIN 后台进程发起输入请求时控制终端产生该信号。 暂停
22 SIGTTOU 后台进程发起输出请求时控制终端产生该信号。 暂停
23 SIGURG 套接字上出现紧急数据时产生。 忽略
24 SIGXCPU 处理器占用时间超出限制值时产生。 终止并产生转储文件(core文件)
25 SIGXFSZ 文件尺寸超出限制值时产生。 终止并产生转储文件(core文件)
26 SIGVTALRM 由虚拟定时器产生的虚拟时钟信号,类似于SIGALRM,但是计算的是该进程占用的CPU时间。 终止
27 SIGPROF 类似于SIGALRM / SIGVTALRM,但包括该进程用的CPU时间以及系统调用的时间。 终止
28 SIGWINCH 窗口大小改变时发出。 忽略
29 SIGIO 文件描述符准备就绪, 可以开始进行输入/输出操作。 终止
30 SIGPWR 启动失败时产生。 终止
31 SIGUNUSED 非法的系统调用。 终止并产生转储文件(core文件)

1.信号的”值”在 x86、PowerPC 和 ARM平台下是有效的,但是别的平台的信号值也许跟这个表的不一致。
2.“描述”中注明的一些情况发生时会产生相应的信号,但并不是说该信号的产生就一定发生了这个事件。事实上,任何进程都可以使用kill()函数来产生任何信号。
3.信号 SIGKILL 和 SIGSTOP 是两个特殊的信号,他们不能被忽略、阻塞或捕捉,只能按缺省动作来响应。
4.一般而言,信号的响应处理过程如下:如果该信号被阻塞,那么将该信号挂起,不对其做任何处理,等到解除对其阻塞为止。如果该信号被捕获,那么进一步判断捕获的类型,如果设置了响应函数,那么执行该响应函数;如果设置为忽略,那么直接丢弃该信号。最后才执行信号的默认处理。

非实时信号与实时信号
Linux 系统中有许多信号,其中前面 31 个信号都有一个特殊的名字,对应一个特殊的事件,比如 信号值为1的信号 SIGHUP(Signal Hang UP),这个信号就是通知系统关闭中断的,当系统中的一个控制终端被关闭(即挂断, hang up)时,都会产生这个信号。
信号值为1- 31的信号属性非实时信号,它主要是因为这类信号不支持排队,因此信号可能会丢失。比如发送多次相同的信号,进程只能收到一次,也只会处理一次,因此剩下的信号将被丢弃。而实时信号(信号值为34- 64的信号)则不同,它是支持排队的,发送了多少个信号给进程,进程就会处理多少次。
为什么说信号还有可靠与不可靠呢,这得从信号的处理过程来介绍了:一般来说,一个进程收到一个信号后不会被立即处理,而是在恰当时机进行处理!一般是在中断返回的时候,或者内核态返回用户态的时候 (这种情况是比较常见的处理方式)。
也就是说,即使这些信号到来了,进程也不一定会立即去处理它,因为系统不会为了处理一个信号而把当前正在运行的进程挂起,这样的话系统的资源消耗太大了,如果不是紧急信号,是不会立即处理的,所以系统一般都会选择在内核态切换回用户态的时候处理信号。比如有时候进程处于休眠状态,但是又收到了一个信号,于是系统就得把信号储存在进程唯一的 PCB(进程控制块)当中,而非实时信号则是不支持排队的,假如此时又有一个信号到来,那么它将被丢弃,这样进程就无法处理这个信号,所以它是不可靠的。对于实时信号则没有这种顾虑,因为它支持排队,信号是不会被丢弃的,这样子每个到来的信号都能得到有效处理。

信号的处理
无论是同步还是异步信号,当信号发生时,我们可以告诉Linux内核采取如下3种动作中的任意一种:
忽略信号。大部分信号都可以被忽略,但有两个除外:SIGSTOP和SIGKILL绝不会被忽略。不能忽略这两个信号的原因是为了给超级用户提供杀掉或停止任何进程的一种手段。此外,尽管其他信号都可以被忽略,但其中有一些却不宜忽略。例如,若忽略硬件例外(非法指令)信号,则会导致进程的行为不确定。
捕获信号。这种处理是要告诉Linux内核,当信号出现时调用专门提供的一个函数。这个函数称为信号处理函数,它专门对产生信号的事件作出处理。
让信号默认动作起作用。系统为每种信号规定了一个默认动作,这个动作由Linux内核来完成,有以下几种可能的默认动作:
1.终止进程并且生成内存转储文件,即写出进程的地址空间内容和寄存器上下文至进程当前目录下名为cone的文件中;
2.终止终止进程但不生成core文件。
3.忽略信号。
4.暂停进程。
5.若进程是暂停暂停,恢复进程,否则将忽略信号。

Linux 常用信号分析

信号名 信号值 产生原因 处理方式
SIGHUP 1 关闭终端 终止
SIGINT 2 ctrl + c 终止
SIGQUIT 3 ctrl + \ 终止 + 转储
SIGABRT 6 abort() 终止 + 转储
SIGPE 8 算术错误 终止
SIGKILL 9 kill -9 pid 终止,不可捕获/忽略
SIGUSR1 10 自定义 忽略
SIGSEGV 11 段错误 终止 + 转储
SIGUSR2 12 自定义 忽略
SIGALRM 14 alarm() 终止
SIGTERM 15 kill pid 终止
SIGCHLD 17 (子)状态变化 忽略
SIGTOP 19 ctrl + z 暂停,不可捕获/忽略

pkill命令

发送信号相关API函数
发送信号的函数主要有 kill()、 raise()、alarm()
kill()函数
头文件
#include <sys/types.h>
#include <signal.h>
函数原型
int kill(pid_t pid, int sig);

kill()函数的参数有两个,分别是pid与sig,还返回一个int类型的错误码。
pid的取值如下:
pid > 1:将信号sig发送到进程ID值为pid指定的进程。
pid = 0:信号被发送到所有和当前进程在同一个进程组的进程。
pid = -1:将sig发送到系统中所有的进程,但进程1(init)除外。
pid < -1:将信号sig发送给进程组号为-pid (pid绝对值)的每一个进程。
sig:信号值。
函数返回值:
0:发送成功。
-1:发送失败。
进程可以通过调用kill()函数向包括它本身在内的其他进程发送一个信号。如果程序没有发送该信号的权限,对kill函数的调用就将失败,失败的常见原因是目标进程由另一个用户所拥有。因此要想发送一个信号,发送进程必须拥有相应的权限,这通常意味着两个进程必须拥有相同的用户ID(即你只能发送信号给属于自己的进程,但超级用户可以发送信号给任何进程)。
Kill()函数会在失败时返回-1并设置errno变量。失败的原因可能是:给定的信号无效(errno设置为INVAL)、发送进程权限不够(errno设置为EPERM)、目标进程不存在(errno设置为ESRCH)等情况。

raise()函数
头文件
#include <signal.h>
函数原型
int raise(int sig);
raise()函数只有一个参数sig,它代表着发送的信号值,如果发送成功则返回0,发送失败则返回-1,发送失败的原因主要是信号无效,因为它只往自身发送信号,不存在权限问题,也不存在目标进程不存在的情况。

alarm()函数
头文件
#include <unistd.h>
函数原型
unsigned int alarm(unsigned int seconds);

alarm()也称为闹钟函数,它可以在进程中设置一个定时器,当定时器指定的时间到时,它就向进程发送SIGALARM信号。如果在seconds秒内再次调用了alarm()函数设置了新的闹钟,则新的设置将覆盖前面的设置,即之前设置的秒数被新的闹钟时间取代,如果新的seconds为0,则之前设置的闹钟会被取消,并将剩下的时间返回。因此它的返回值是之前闹钟的剩余秒数,如果之前未设闹钟则返回0。

捕获信号相关API函数
在Linux中,捕获信号的函数有很多,比如signal()、sigaction()等函数。
signal()函数
头文件
#include <signal.h>
函数原型
typedef void (*sighandler_t)(int);
sighandler_t signal(int signum, sighandler_t handler);

signal()主要是用于捕获信号,可以改变进程中对信号的默认行为,我们在捕获这个信号后,也可以自定义对信号的处理行为,当收到这个信号后,应该如何去处理它,这也是我们在开发Linux最常用的方式。
signal()这个函数一般是跟 kill()函数配套使用的,目标进程必须先使用 signal()来为某个信号设置一个响应函数,或者设置忽略某个信号,才能改变信号的默认行为,这个过程称为”信号的捕获”。对一个信号的”捕获”可以重复进行, 不过signal()函数将会返回前一次设置的信号响应函数指针。

这个相当复杂的函数定义说明,signal是一个带有signum和handler两个参数的函数。准备捕获或忽略的信号由参数signum指出,接收到指定的信号后将要调用的函数由参数handler指出。
signum是指定捕获的信号,如果指定的是一个无效的信号,或者尝试处理的信号是不可捕获或不可忽略的信号(如SIGKILL),errno将被设置为EINVAL。
handler是一个函数指针,它的类型是 void(*sighandler_t)(int) 类型,拥有一个int类型的参数,这个参数的作用就是传递收到的信号值,返回类型为void。
signal()函数会返回一个sighandler_t类型的函数指针,这是因为调用signal()函数修改了信号的行为,需要返回之前的信号处理行为是哪个,以便让应用层知悉,如果修改信号的默认行为识别则返回对应的错误代码SIG_ERR。
handler需要用户自定义处理信号的方式,当然还可以使用以下宏定义:
SIG_IGN:忽略该信号。
SIG_DFL:采用系统默认方式处理信号。
虽然这个函数是比较简单的,但是还是要注意一下:如果调用处理程序导致信号被阻塞,则从处理程序返回后,信号将被解除阻塞。无法捕获或忽略信号SIGKILL和SIGSTOP。

sigaction()函数
头文件
#include <signal.h>
函数原型
int sigaction(int signum, const struct sigaction *act, struct sigaction *oldact);

signum:指定捕获的信号值。
act:是一个结构体,该结构体的内容如下:
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_handler是一个函数指针,是捕获信号后的处理函数,它也有一个int类型的参数,传入信号的值,这个函数是标准的信号处理函数。
sa_sigaction则是扩展信号处理函数,它也是一个函数指针,但它比标准信号处理函数复杂的多,事实上如果选择扩展接口的话,信号的接收进程不仅可以接收到int 型的信号值,还会接收到一个 siginfo_t类 型的结构体指针,还有一个void类型的指针,还有需要注意的就是,不要同时使用sa_handler和sa_sigaction,因为这两个处理函数是有联合的部分(联合体)。关于siginfo_t类型的结构体我们在后续讲解。
sa_mask是信号掩码,它指定了在执行信号处理函数期间阻塞的信号的掩码,被设置在该掩码中的信号,在进程响应信号期间被临时阻塞。除非使用SA_NODEFER标志,否则即使是当前正在处理的响应的信号再次到来的时候也会被阻塞。
re_restorer则是一个已经废弃的成员变量,不要使用。
oldact:返回原有的信号处理参数,一般设置为NULL即可。
sa_flags是指定一系列用于修改信号处理过程行为的标志,由下面的0个或多个标志组合而成:
SA_NOCLDSTOP:如果signum是SIGCHLD,则在子进程停止或恢复时,不会传信号给调用sigaction()函数的进程。即当它们接收到SIGSTOP、SIGTSTP、SIGTTIN或SIGTTOU(停止)中的一种时或接收到SIGCONT(恢复)时,父进程不会收到通知。仅当为SIGCHLD建立处理程序时,此标志才有意义
SA_NOCLDWAIT:从Linux 2.6开始就存在这个标志了,它表示父进程在它的子进程终止时不会收到 SIGCHLD 信号,这时子进程终止则不会成为僵尸进程。
SA_NODEFER:不要阻止从其自身的信号处理程序中接收信号,使进程对信号的屏蔽无效,即在信号处理函数执行期间仍能接收这个信号,仅当建立信号处理程序时,此标志才有意义。
SA_RESETHAND:信号处理之后重新设置为默认的处理方式。
SA_SIGINFO:从Linux 2.2开始就存在这个标志了,使用 sa_sigaction成员而不是使用sa_handler 成员作为信号处理函数。

signal函数演示

第一次 ctrl + c 的时候,捕获到信号值2,然后修改为默认模式。
第二次 ctrl + c 的时候,执行默认模式,直接退出。

kill函数演示

子进程使用 raise() 函数给自己发送 SIGSTOP 停止信号,然后父进程给子进程发送 SIGKILL信号杀死子进程。最后父进程退出。

信号集
在Linux系统中有一个能表示多个信号集合的数据类型sigset_t——信号集(signalset),它在头文件signal.h中被定义,此外还定义了用来处理信号集的函数。这些函数中使用信号集这个数据类型,以告诉内核不允许发生该信号集里面的信号。正如上一节所述的信号掩码就是这种数据类型,不过由于信号种类数目可能超过一个整型变量所包含的位数,所以一般而言,不能用整型量中的一位代表一种信号,所以POSIX.1定义数据类型sigset_t以包含一个信号集,并且定义了下列五个处理信号集的函数,以修改进程在接收到信号时的行为

头文件
#include <signal.h>
函数原型
int sigemptyset(sigset_t *set); //将信号集合初始化为0
int sigfillset(sigset_t *set); //将信号集合初始化为1
int sigaddset(sigset_t *set, int signum); //将信号集合某一位设置为1
int sigdelset(sigset_t *set, int signum); //将信号集合某一位设置为0
int sigismember(const sigset_t *set, int signum); //判断信号是否属于信号集
int sigprocmask(int how, const sigset_t set, sigset_t oldset); //使用设置好的信号集合去修改信号屏蔽集

参数how:
SIG_BLOCK:屏蔽某个信号(屏蔽集 | set)
SIG_UNBLOCK:打开某个信号(屏蔽集 & (~set))
SIG_SETMASK:屏蔽集 = set
参数oldset:保存旧的屏蔽集的值,NULL表示不保存

见名知义,这些函数执行的操作如它们的名字一样,sigemptyset()函数是将信号集初始化为空,使进程不会屏蔽任何信号;sigfillset()函数将信号集初始化为包含所有已定义的信号,刚好与igemptyset()函数是相对立的。
所有应用程序在使用信号集前,要对该信号集调用sigemptyset()函数或sigfillset()函数将信号初始化一次,这是因为C编译器会将不赋初值的外部和静态度量都初始化为0,而这是否与给定系统上信号集的实现相对应并不清楚。
一旦已经初始化了一个信号集,以后就可在该信号集中增、删特定的信号。sigaddset()函数会将一个信号添加到现有的信号集中,sigdelset()函数则是从现有的信号集中删除一个信号。在两个函数在操作信号集成功时返回0,失败时返回-1并设置errno。不过只有一个错误代码被定义了,即当给定的信号无效时, errno将设置为EINVAL。
函数sigismember()用于判断一个给定的信号是否是一个信号集的成员。如果是就返回1;如果不是,它就返回0;如果给定的信号无效,它就返回-1并设置errno为EINVAL。

未处理信号集
信号如果被屏蔽,则记录在未处理信号集中
非实时信号(1~31),不排队,只保留一个

实时信号(34-64),排队,保留全部

手动打开信号集,保证每次信号都能正常接收并处理。

Hankin
2020.07.23

内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: