![cover](https://i-blog.csdnimg.cn/direct/f5e28b842a2a4d448bb34152e7049ee0.png)
【linux】信号的理论概述和实操
在Linux系统中,信号是一种软件中断,用于通知进程发生了某个事件信号是由操作系统发送给进程的,用于通知进程某个条件已经发生或者需要执行某个动作,进程处理信号的默认动作可用如下命令查看信号提供了一种进程间通信的机制,虽然它主要是用来通知异常或中断情况,但也可以被用来实现进程间的同步或控制CPU的内核态和用户态是操作系统中的两种重要状态,它们分别代表了CPU在执行不同类型程序时的权限和访问范围。内核
目录
个人主页东洛的克莱斯韦克
理论篇
信号概述
在Linux系统中,信号是一种软件中断,用于通知进程发生了某个事件
信号是由操作系统发送给进程的,用于通知进程某个条件已经发生或者需要执行某个动作,进程处理信号的默认动作可用如下命令查看
man 7 signal
信号提供了一种进程间通信的机制,虽然它主要是用来通知异常或中断情况,但也可以被用来实现进程间的同步或控制
信号的分类
信号分为普通信号 和 实时信号
用如下指令查看信号
kill -l
在Linux系统中1号信号到31号信号为普通信号,34号到64号信号为实时信号。每一种信号都有自己的宏定义。
实时信号一般用于特定的系统中,如嵌入式系统,能够在特定的硬件上运行并对外部事件做出迅速响应的系统。一个很贴近生活的例子是车载系统,如果用户层踩了刹车,车载系统即使再忙也要马上做出响应。实时信号会打破进程占用CPU资源的公平性。
而普通信号不会破坏进程占用CPU的资源的公平性,本文重点探讨普通信号。
信号机制
信号的机制属于一种软件中断,它模拟的是硬件中断
理解硬件中断
硬件中断是指当硬件设备(如网卡、硬盘、键盘等)有数据或事件需要处理时,会自动向CPU发送一个中断请求(IRQ),CPU在收到中断请求后,会暂停当前正在执行的任务,转而执行处理该中断请求的程序,这一过程称为硬件中断处理。
也就是说OS不会耗费资源去检查外设的数据情况,而是检测CPU是否收到中断请求。
以键盘为例,OS不可能知道用户什么时候敲键盘,OS也不会检测键盘是否有数据,只有用户敲键盘了,键盘会给CPU发送中断请求,OS检查到CPU的中断请求,才会把数据从键盘文件刷到内核缓冲区。
硬件中断是硬件行为,信号是在软件层模拟硬件中断。操作系统给进程发送相应的信号,进程收到信号完成某些任务。
异步
与硬件中断类似,进程不知道操作系统什么时候给自己发送信号。由于信号可以在进程执行的任何时间点到来,且进程通常不会预先知道何时会收到信号,因此信号的接收是异步的。
信号对应的三种动作
默认动作 忽略 自定义
默认动作:每一个信号对应一个动作。就比如红绿灯的 红灯 ,绿灯, 黄灯 分别对应停止, 前进, 等一等。
信号编号 | 信号名称 | 默认动作 | 备注 |
---|---|---|---|
1 | SIGHUP | 终止进程 | 当用户退出shell时,由该shell启动的所有进程将接收此信号 |
2 | SIGINT | 终止进程 | 相当于Ctrl+C,通常用于终止前台进程 |
3 | SIGQUIT | 终止进程并生成core文件 | 相当于Ctrl+\,除了终止进程外,还会生成core文件用于调试 |
4 | SIGILL | 终止进程并生成core文件 | 非法指令,例如执行了未知或不支持的指令 |
5 | SIGTRAP | 终止进程并生成core文件 | 跟踪/断点陷阱,通常与调试器相关 |
6 | SIGABRT | 终止进程并生成core文件 | 调用abort()函数产生的信号,用于异常终止进程 |
7 | SIGBUS | 终止进程并生成core文件 | 总线错误,非法访问内存地址(如对齐错误) |
8 | SIGFPE | 终止进程并生成core文件 | 浮点异常,如除以0、溢出等 |
9 | SIGKILL | 终止进程 | 不能被捕捉、阻塞或忽略,无条件终止进程 |
10 | SIGUSR1 | 终止进程 | 用户自定义信号1,程序员可以在程序中定义并使用该信号 |
11 | SIGSEGV | 终止进程并生成core文件 | 无效的内存引用,如解引用空指针 |
12 | SIGUSR2 | 终止进程 | 用户自定义信号2,程序员可以在程序中定义并使用该信号 |
13 | SIGPIPE | 终止进程 | 写入没有读端的管道,常见于socket编程中 |
14 | SIGALRM | 终止进程 | 由alarm()函数设置的定时器超时产生 |
15 | SIGTERM | 终止进程 | 请求程序终止,与SIGKILL不同,SIGTERM可以被捕捉、阻塞或忽略 |
16-17 | SIGSTKFLT | 终止进程 | 协处理器栈错误(已废弃,在某些系统上可能不存在) |
SIGCHLD | 忽略 | 子进程停止或终止时,通知父进程,但父进程通常选择忽略此信号 | |
18 | SIGCONT | 继续执行被停止的进程 | 使一个停止的进程继续执行(如果它被停止的话) |
19 | SIGSTOP | 停止进程 | 不能被捕捉、阻塞或忽略,无条件停止进程 |
20 | SIGTSTP | 停止进程 | 相当于Ctrl+Z,用于暂停前台进程 |
21-22 | SIGTTIN | 停止进程 | 后台进程尝试从控制终端读取时产生(如后台进程尝试读取输入) |
SIGTTOU | 停止进程 | 后台进程尝试向控制终端写入时产生(如后台进程尝试输出) | |
23-31 | SIGURG, ... | 依赖于具体实现 | 这些信号的默认动作可能因系统而异,且一些信号可能不在所有系统上都有定义 |
忽略:进程屏蔽了该信号
自定义:进程捕捉了该信号
9号信号和19号信号不能被捕捉也不能被屏蔽。
9号信号是用来杀进程的且一定能杀掉,用来处理有问题的进程,恶意攻击系统的进程等等
19号信号用来停掉进程而且一定能停掉,如果进程正在做一些很重要的事情但确实出了一些问题,可以用19号信号停掉该进程。
信号产生的条件
终端按键
常用的组合键如下
Ctrl+C信号:SIGINT(Interrupt,中断信号)
作用:当用户按下Ctrl+C时,终端驱动程序会接收到这个输入,并调用信号系统向当前的前台进程发送SIGINT信号。默认动作是终止进程。
补充:前台进程指的是你收到键盘输入的进程,linux中只允许有一个前台进程,可以有多个后台进程。
Ctrl+Z信号:SIGTSTP(Stop typed at terminal,终端停止信号)
作用:当用户按下Ctrl+Z时,终端驱动程序会向当前的前台进程发送SIGTSTP信号。这个信号会停止(挂起)进程的执行,但进程并没有被终止。用户可以使用fg
命令将进程恢复到前台继续执行,或者使用bg
命令将其放到后台执行。
Ctrl+\信号:SIGQUIT(Quit,退出信号)
作用:在某些系统中,当用户按下Ctrl+\时,会向当前的前台进程发送SIGQUIT信号。这个信号的默认处理方式是终止进程,并且会生成一个核心转储文件(core dump),该文件包含了进程终止时的内存、寄存器状态等信息,有助于开发者进行调试。
补充:服务器默认是关掉核心转储的,在线上生产环境中,会有大量收到SIGQUIT信号的进程,如果核心转储没有关掉,会有大量的核心转储文件挤压磁盘空间,使服务器无法正常运行。
系统调用
软件条件
对于管道而言,如果管道的读端全部被关闭,写端就没有意义了。操作系统会给写端进程发送SIGPIPE信号,其信号编号为13。
SIGPIPE信号是Linux中定义的一个标准信号,用于指示一个写操作尝试写入一个没有读端的管道。SIGPIPE信号的默认动作是终止进程,但可以通过设置信号处理函数来捕获或忽略该信号。大多数服务器程序为了避免因SIGPIPE信号而异常终止,会选择忽略该信号。
上述例子就属于进程触发了一些软件条件,从而让操作系统发送信号来通知进程。
硬件异常
硬件异常是指由硬件设备引起的程序执行过程中的错误或异常情况。下面分析除0错误和野指针问题的硬件异常
除0错误
除0错误是很常见的硬件异常,它会使CPU的运算溢出。当进程执行了除以0的指令时,CPU的运算单元会产生异常,并通知内核。内核会解析这个异常为SIGFPE信号,并发送给当前进程。
野指针
当进程访问了非法或未分配的内存地址时,MMU会产生异常,并通知内核。内核会解析这个异常为SIGSEGV信号,并发送给当前进程。
本质上讲,野指针问题都是虚拟地址在页表中转化物理地址失败。【Linux】进程地址空间
OS对于错误的态度
对于一些异常或错误系统往往不会发送9或19号信号,上文中4个条件触发的信号都是可以被捕捉的和屏蔽的。
也就是说即使触发了一些异常或错误,系统做的也只是通知,而不是强硬的杀掉或停掉进程。比如上述的除 0 错误,进程只要捕捉了SIGFPE信号,那么该进程只要被调度到CPU上就会触发除 0 异常,OS就会继续给该进程发送SIGFPE信号。
OS为什么要用信号的方式通知,而不是直接杀进程?
可以搭个场景来理解下,进程A正在向磁盘写入10万条转账记录,但进程A触发了某些异常,此时OS的做法是通知进程A,还是杀掉或停掉进程A呢。如果做的比较极端导致这10万条转账记录丢失了,是进程的问题,还是OS的问题呢。
那么我们也就不难理解,OS为什么要给进程足够多的宽容度。因为应用层可能正在做很重要的事情,OS太极端对应用层的体验会大打折扣。
所以OS会以信号的方式通知进程,而对异常的处理就看上层是怎么设计的,出了问题也和OS没关系。
信号在进程中的内核数据结构
屏蔽:进程不在接收次信号(block)
递达:实际执行信号的处理动作称为信号递达(Delivery)
未决 :信号从产生到递达之间的状态,称为信号未决(Pending)。
每个进程的内核数据都有三张表。
block是位图0表示该信号未被屏蔽,1表示该信号被屏蔽了。
Pending也是位图0表示没有收到该信号,1表示收到了该信号但没有执行。
bandler是一张函数指针数组表,如果该函数要被递达了就会执行对应的方法,可以这些默认方法,也可以执行自定义方法。
需要注意的是Pending位图中没有计数的概念。也就是说,在一段时间内一种信号如果被发送了多次,那么也只会被递达一次。
信号的处理
CPU的内核态和用户态概述
CPU的内核态和用户态是操作系统中的两种重要状态,它们分别代表了CPU在执行不同类型程序时的权限和访问范围。
内核态是CPU的一种特权状态,也称为系统态或管态。在此状态下,CPU可以执行任何机器指令,包括访问和修改所有硬件设备的寄存器,执行内存管理等特权操作。
用户态是CPU的一种非特权状态,也称为目态。在此状态下,CPU只能执行非特权指令,不能直接访问硬件设备或执行特权操作。
内核态(Kernel Mode) | 用户态(User Mode) | |
---|---|---|
定义 | CPU的特权状态,可执行任何机器指令 | CPU的非特权状态,只能执行非特权指令 |
权限 | 最高特权级别(通常为Ring 0) | 较低特权级别(通常为Ring 3) |
运行程序 | 操作系统内核代码 | 用户编写的应用程序和操作系统提供的用户接口程序 |
访问范围 | 可访问和修改系统的任何部分 | 只能访问和操作分配给它的资源 |
安全性 | 必须确保安全性和稳定性 | 权限较低,即使出现错误也不会对系统造成严重影响 |
切换方式 | 通过系统调用、异常和中断等方式实现 | 通过系统调用等方式请求操作系统服务时切换 |
cpu陷入内核后怎么找到内核的数据和代码呢?
在进程地址空间中0到3G是用户空间,3到4G是内核空间,所以进程的内核地址空间都会映射到同一块物理内存(这块物理内中是内核数据),只要CPU陷入内核就有权力访问地址空间中的内核空间。
地址空间示意图
需要注意的是,在执行用户代码是,CPU必须是用户态,因为用户代码中可能会有窃取数据,恶意攻击等非法操作。
进程处理信号的时机
红线以上是用户态,红线以下是内核态。蓝点是CPU切换用户态和内核态的时机,绿点是处理信号的时机。
在递达时该信号被屏蔽
一个信号在被处理时,该信号会被屏蔽。这样就保证了在处理某个信号时,如果这种信号再次产生,那么 它会被阻塞到当前处理结束为止。
实操篇
sigset_t
sigset_t 是一种数据类型,它涵括了上文的三张表。用于表示信号集,即一个或多个信号的集合。
sigset_t
是处理信号时的一个重要数据类型,它允许程序灵活地指定和操作信号集合。通过与相关的函数结合使用,sigset_t
提供了强大的信号处理能力,使得程序能够更精确地控制信号的行为。
信号集操作函数
-
int sigemptyset(sigset_t *set);
此函数用于初始化信号集,将其设置为空集,即不包含任何信号。如果操作成功,则返回0;如果发生错误,则返回-1。
-
int sigfillset(sigset_t *set);
此函数用于将信号集初始化为包含所有信号。这意味着,在调用此函数后,信号集将包含所有可能由系统发送的信号。如果操作成功,则返回0;如果发生错误,则返回-1。
-
int sigaddset(sigset_t *set, int signo);
此函数用于向信号集中添加一个信号。
signo
参数指定了要添加的信号编号。如果操作成功,则返回0;如果发生错误(例如,signo
不是有效信号),则返回-1。 -
int sigdelset(sigset_t *set, int signo);
与
sigaddset
相反,sigdelset
函数用于从信号集中删除一个信号。signo
参数指定了要删除的信号编号。如果操作成功,则返回0;如果发生错误(例如,signo
不是信号集中的成员),则返回-1。 -
int sigismember(const sigset_t *set, int signo);
此函数用于检查指定的信号编号(
signo
)是否属于信号集(set
)。如果signo
是信号集的成员,则返回1;如果不是,则返回0;如果发生错误(例如,set
不是有效的信号集指针),则返回-1。
这些函数在信号处理中非常有用,特别是当需要阻塞或捕捉特定信号时。例如,可以在进行关键操作之前使用sigprocmask
函数和sigemptyset
/sigaddset
来阻塞某些信号,以防止它们干扰这些操作。操作完成后,可以解除对这些信号的阻塞。
sigprocmask
参数
-
how
:指定如何更改信号屏蔽。这个参数可以是以下三个常量之一:SIG_BLOCK
:新的信号屏蔽是当前屏蔽与set
指向的集合的并集。即,set
中指定的信号将被添加到当前进程的屏蔽中。SIG_UNBLOCK
:新的信号屏蔽是当前屏蔽与set
指向的集合的差集。即,set
中指定的信号将从当前进程的屏蔽中移除。SIG_SETMASK
:新的信号屏蔽直接设置为set
指向的集合。即,忽略当前进程的屏蔽,并使用set
中指定的新屏蔽。
-
set
:指向sigset_t
类型的指针,表示要更改的信号集。如果how
是SIG_SETMASK
,则这个集合将直接成为新的信号屏蔽。如果how
是SIG_BLOCK
或SIG_UNBLOCK
,则这个集合中的信号将被相应地添加到或从当前屏蔽中移除。 -
oset
:如果此参数非空,则指向的sigset_t
变量将被设置为函数调用前的信号屏蔽。这允许调用者保存旧的屏蔽并在以后恢复它。
返回值
成功时,sigprocmask
返回0。失败时,返回-1,并设置errno
以指示错误。
#include <stdio.h>
#include <signal.h>
#include <string.h>
#include <unistd.h>
void handler(int sig) {
printf("Caught signal %d\n", sig);
}
int main() {
sigset_t set, oldset;
// 设置SIGINT的处理程序
signal(SIGINT, handler);
// 初始化信号集
sigemptyset(&set);
sigaddset(&set, SIGINT);
// 阻塞SIGINT
if (sigprocmask(SIG_BLOCK, &set, &oldset) == -1) {
perror("sigprocmask");
return 1;
}
// 现在SIGINT被阻塞了,发送SIGINT信号不会调用handler
printf("SIGINT is blocked. Trying to send SIGINT...\n");
raise(SIGINT); // 尝试发送SIGINT信号,但不会被处理
// 恢复旧的信号屏蔽
if (sigprocmask(SIG_SETMASK, &oldset, NULL) == -1) {
perror("sigprocmask");
return 1;
}
// 现在SIGINT不再被阻塞,发送SIGINT信号将调用handler
printf("SIGINT is unblocked. Trying to send SIGINT...\n");
raise(SIGINT); // 发送SIGINT信号,将调用handler
return 0;
}
sigpending
sigpending
函数是 POSIX 系统中用于查询当前进程阻塞(pending)的信号集合的接口。这些信号已经发送给进程,但由于某些原因(如信号被阻塞)而尚未被处理。通过调用 sigpending
函数,程序可以获取这些待处理的信号,并据此做出相应的处理。
#include <signal.h>
int sigpending(sigset_t *set);
参数
set
:指向sigset_t
类型的指针,用于存储查询到的待处理信号集合。
返回值
- 成功时,
sigpending
返回 0。 - 失败时,返回 -1,并设置
errno
以指示错误原因。
#include <stdio.h>
#include <signal.h>
#include <string.h>
void print_sigset(const sigset_t *set) {
printf("Pending signals: ");
for (int i = 1; i < _NSIG; ++i) {
if (sigismember(set, i)) {
printf("%d ", i);
}
}
printf("\n");
}
int main() {
sigset_t pending;
// 查询待处理信号集合
if (sigpending(&pending) == -1) {
perror("sigpending");
return 1;
}
// 打印待处理信号集合
print_sigset(&pending);
return 0;
}
sigaction
sigaction
函数是 POSIX 系统中用于检查和更改与指定信号相关联的处理动作的函数。这个函数比早期用于相同目的的 signal
函数提供了更多的功能和灵活性。通过 sigaction
,程序可以精确地指定信号的处理函数、设置信号处理的选项,并查询信号处理的当前状态。
#include <signal.h>
int sigaction(int signo, const struct sigaction *act, struct sigaction *oact);
参数
signo
:指定要操作的信号的编号。act
:指向struct sigaction
的指针,包含了新的信号处理动作。如果此参数为 NULL,则不会更改信号的处理动作,但可以用来查询当前的处理动作(如果oact
非空)。oact
:如果非空,指向一个struct sigaction
变量,该变量用于存储调用sigaction
之前信号的处理动作。这允许程序在更改信号处理动作之前保存它,以便将来恢复。
返回值
- 成功时,
sigaction
返回 0。 - 失败时,返回 -1,并设置
errno
以指示错误原因。
struct sigaction
结构
struct sigaction
结构体通常包含以下字段(但具体实现可能有所不同):
sa_handler
:指向信号处理函数的指针。如果此字段非 NULL,则它是一个普通的信号处理函数。如果为 SIG_IGN,则忽略该信号。如果为 SIG_DFL,则使用信号的默认处理动作。sa_sigaction
:一个指向函数的指针,该函数用于处理信号,并提供了对信号发生时的额外信息的访问(如siginfo_t
结构)。这通常与SA_SIGINFO
标志一起使用。sa_mask
:一个信号集,指定了在执行信号处理函数期间应该被阻塞的信号。这允许信号处理函数在执行时不受其他信号的干扰。sa_flags
:一个标志位集合,用于修改信号处理的行为。常见的标志包括SA_RESTART
(如果信号中断了一个系统调用,则自动重启它),SA_NODEFER
(在执行信号处理函数时不自动阻塞该信号),和SA_SIGINFO
(使用sa_sigaction
而不是sa_handler
)。sa_restorer
:这个字段在现代系统中很少使用,通常被忽略。它最初用于指定一个函数,该函数将恢复被信号处理函数中断的系统调用的执行环境。
#include <stdio.h>
#include <signal.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
void signal_handler(int signum) {
printf("Caught signal %d\n", signum);
// 清理资源、关闭文件等操作...
exit(signum);
}
int main() {
struct sigaction act;
// 初始化信号处理结构体
memset(&act, 0, sizeof(act));
act.sa_handler = signal_handler;
// 忽略 SIGPIPE 信号(可选)
signal(SIGPIPE, SIG_IGN);
// 设置 SIGINT 信号的处理函数
if (sigaction(SIGINT, &act, NULL) == -1) {
perror("sigaction");
exit(EXIT_FAILURE);
}
// 主循环,等待信号
while (1) {
pause(); // 暂停执行,等待信号
}
return 0; // 实际上永远不会执行到这里
}
闹钟
alarm
函数是 UNIX 和类 UNIX 系统(包括 Linux)中用于设置定时器的一个函数,它定义在 <unistd.h>
头文件中。当你调用 alarm
函数并传递给它一个参数 seconds
时,该函数会设置一个定时器,该定时器在指定的秒数后到期。当定时器到期时,如果进程没有捕获 SIGALRM 信号(通过调用 signal
或 sigaction
函数设置信号处理函数),则进程会收到 SIGALRM 信号。如果进程捕获了该信号,那么可以执行一些特定的操作,比如清理资源、记录日志等。
#include <unistd.h>
unsigned int alarm(unsigned int seconds);
参数
seconds
:定时器到期前的秒数。如果seconds
是 0,则任何当前设置的定时器都会被取消,但不会发送 SIGALRM 信号。
返回值
- 如果之前已经调用了
alarm
并且设置了定时器,alarm
函数会返回之前设置的剩余时间(秒),直到那个定时器到期。如果之前没有设置定时器,则返回 0。
注意事项
-
定时器重置:如果在一个已经设置了定时器的进程中再次调用
alarm
,那么之前的定时器会被新的定时器替换。返回值是之前定时器的剩余时间(如果有的话)。 -
精度和限制:
alarm
函数的精度和限制取决于系统的实现。在一些系统上,alarm
的精度可能较低,因为它基于系统的定时器中断。此外,一些系统可能对alarm
可以设置的最大时间有限制。 -
与 sleep/pause 的交互:如果进程在调用
alarm
后调用了sleep
或pause
,并且alarm
定时器在sleep
或pause
期间到期,那么sleep
或pause
会被中断,并且进程会收到 SIGALRM 信号(如果未捕获)。 -
信号处理:为了处理 SIGALRM 信号,你需要使用
signal
或sigaction
函数来设置信号处理函数。
#include <stdio.h>
#include <unistd.h>
#include <signal.h>
void sigalrm_handler(int sig_num) {
printf("Alarm signal received\n");
// 在这里执行清理或其他操作
}
int main() {
// 设置 SIGALRM 信号的处理函数
signal(SIGALRM, sigalrm_handler);
// 设置 5 秒后的定时器
alarm(5);
// 暂停执行,等待定时器到期或信号
pause();
return 0;
}
signal
在C语言中,signal
函数的原型可能因系统而异,但通常它遵循 POSIX 标准或类似的标准。不过,标准的 C 库(如 glibc)通常会在 <signal.h>
头文件中提供一个符合大多数系统需求的 signal
函数声明。
#include <signal.h>
void (*signal(int sig, void (*func)(int)))(int);
- 参数:
int sig
:要处理的信号编号。void (*func)(int)
:指向信号处理函数的指针,该函数接受一个整型参数(信号编号)并返回void
。
- 返回值:
- 返回一个指向之前为该信号设置的信号处理函数的指针(也接受一个整型参数并返回
void
),或者返回SIG_ERR
以表示错误,并设置errno
以指示错误原因。
- 返回一个指向之前为该信号设置的信号处理函数的指针(也接受一个整型参数并返回
#include <stdio.h>
#include <stdlib.h>
#include <signal.h>
#include <unistd.h>
// 信号处理函数
void handler(int sig_num) {
printf("Caught signal %d\n", sig_num);
// 可以在这里执行清理操作或退出程序
exit(0); // 或者使用其他逻辑来响应信号
}
int main() {
// 设置 SIGINT 信号的处理函数
if (signal(SIGINT, handler) == SIG_ERR) {
// 如果 signal 调用失败,则打印错误信息并退出
perror("signal");
exit(EXIT_FAILURE);
}
// 程序的主循环或其他逻辑
while (1) {
printf("Waiting for signal...\n");
sleep(1); // 暂停一秒,以便可以看到信号何时被捕获
}
// 注意:实际上,由于我们在信号处理函数中调用了 exit,
// 所以这行代码永远不会被执行。
return 0;
}
【linux】进程控制——进程创建,进程退出,进程等待-CSDN博客
【linux】进程间通信(IPC)——匿名管道,命名管道与System V内核方案的共享内存,以及消息队列和信号量的原理概述-CSDN博客
更多推荐
所有评论(0)