【Linux】信号

一、信号概念

1.1 信号的定义

  信号(signal)是一种软中断,它本质上是在软件层次上对硬件中断机制的一种模拟。信号可用于进程间通信、处理异常,它通过操作系统向一个进程或者线程发送一种异步通知,用于通知该进程或线程某种事件已经发生,需要做出相应的处理。同时,进程收到信号到处理这个信号之前,必须具备保存这个信号的能力。

1.2 信号的编号及其描述

  要处理信号,我们必需要先知道每种信号的类型及其对应的作用。在 Linux 中,信号被分类为标准信号和实时信号,每个信号都有一个唯一的编号(就是一个宏定义)。标准信号是最基本的信号类型,由整数编号表示,编号范围是 131。实时信号是 Linux 中的扩展信号类型,由整数编号表示,编号范围是 3464,这里我们主要学习标准信号。

我们可以通过 kill -l 命名来查看信号的编号:

下面是常见的信号编号及其对应的描述:

信号编号信号名称描述
1SIGHUP控制终端挂起或者断开连接
2SIGINT中断信号,通常由 Ctrl+C 发送
3SIGQUIT退出信号,通常由 Ctrl+\ 发送
4SIGILL非法指令信号
5SIGTRAP跟踪异常信号
6SIGABRT中止信号
7SIGBUS总线错误信号
8SIGFPE浮点错误信号
9SIGKILL强制退出信号
10SIGUSR1用户定义信号1
11SIGSEGV段错误信号
12SIGUSR2用户定义信号2
13SIGPIPE管道破裂信号
14SIGALRM闹钟信号
15SIGTERM终止信号
16SIGSTKFLT协处理器栈错误信号
17SIGCHLD子进程状态改变信号
18SIGCONT继续执行信号
19SIGSTOP暂停进程信号
20SIGTSTP终端停止信号
21SIGTTIN后台进程尝试读取终端输入信号
22SIGTTOU后台进程尝试写入终端输出信号
23SIGURG套接字上的紧急数据可读信号
24SIGXCPU超时信号
25SIGXFSZ文件大小限制超出信号
26SIGVTALRM虚拟定时器信号
27SIGPROF分析器定时器信号
28SIGWINCH窗口大小变化信号
29SIGIO文件描述符上就绪信号
30SIGPWR电源失效信号
31SIGSYS非法系统调用信号
34SIGRTMIN实时信号最小编号
.........
64SIGRTMAX实时信号最大编号

二、信号的产生

2.1 按键产生

  我们在终端按下 Ctrl+C 组合键时,操作系统会识别这个动作并生成一个中断信号。键盘硬件通过中断控制器向 CPU 发送这个信号,然后操作系统的中断处理程序会捕获这个信号,并将其转换为软件信号 SIGINT。随后,操作系统的信号分发机制随后将 SIGINT 信号发送给当前终端的前台进程,执行该信号的处理函数。类似的还有其它键盘组合键 Ctrl+\Ctrl+Z

2.2 系统调用

void abort(void):发送 SIGABRT 信号终止调用进程
int raise(int sig):向调用进程发送一个信号
int kill(pid_t pid, int sig):向指定的进程ID发送一个信号

2.3 硬件异常

  信号产生,不一定非得用户显示的发送,有些情况下信号会在 OS 内部自动产生。比如程序中存在 除0和野指针错误,除0会使结果无穷大,引起状态寄存器溢出,标记位由 0 变成 1OS 得知 CPU 发生运算异常,就会找到使状态寄存器的标记位变为 1 的进程,并发送信号终止这个进程了。同样的,野指针,CPU 在拿着这个虚拟地址进行页表映射时,就会发现相关的权限问题或者越界访问,然后发送信号终止这个进程。
  程序出现问题时,发送信号是一种有效的方式,让进程了解到发生了错误或异常。信号机制允许进程以受控的方式响应异常情况,避免错误扩散到操作系统的其他部分。通过信号,操作系统能够将异常隔离在单个进程中,同时允许进程采取适当的措施来处理错误,如记录日志、释放资源或优雅地退出。这样,即使某个进程遇到问题,也不会波及到整个操作系统的稳定性

2.4 软件条件

  比如当管道的读端被关闭,而写端进程仍然尝试写入数据时,操作系统会采取措施阻止这种无效的写操作。由于没有进程读取数据,继续写入变得没有意义。为了避免资源浪费和潜在的错误,操作系统会向写入进程发送一个 SIGPIPE (即 13 号信号)。这个信号的发送是由读端关闭这一软件条件触发的,目的是通知写进程其写入操作不再被需要,从而可以安全地终止写入操作,避免产生无效的输出。

定时器设定闹钟 : unsigned int alarm(unsigned int seconds)
  alarm 函数用于设置一个定时器,告诉操作系统在指定的 seconds 秒后向当前进程发送 SIGALRM 信号,其默认行为是终止该进程。该函数的返回值是上一次闹钟设置的剩余时间。如果 seconds 设置为 0alarm 函数将取消之前设置的闹钟

三、信号的保存和处理

3.1 信号的保存与阻塞

  上面我们提到,进程收到信号到处理这个信号之前,必须具备保存这个信号的能力。而保存这个信号就是对描述信号结构体的特定字段进行修改,它是通过内核设定的位图结构来实现的。对此我们还要引入以下一组概念,因为信号还可以被阻塞的,阻塞后不会执行信号对应的处理方法

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

  所以我们学习信号主要看这三张表,收到一个信号时,先看信号有没有被阻塞,如果被阻塞了,进入信号未决状态,即在 pending 表中将该信号保存下来。没有被阻塞,就递达执行对应的方法。
  block 表和 pending 表都是使用内核中的位图结构 sigset_t 来实现,比特位的位置代表信号的编号。对此要对内核中的位图进行修改,我们必须要使用系统调用,即调用信号集操作函数

将信号集清空: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)
获取未决信号集:sigpending(sigset_t *set)

函数详解:int sigprocmask(int how, const sigset_t *set, sigset_t *oldset)
成功返回0,失败返回-1,并设置错误码
参数解释:
how:表示信号屏蔽字的操作方式,以下三选一
SIG_BLOCK:将set中的信号集合添加到当前的信号屏蔽字中
SIG_UNBLOCK:将set中的信号集合从当前的信号屏蔽字中移除
SIG_SETMASK:将set中的信号集合替换为当前的信号屏蔽字。
set:要设置的信号集合
oldset:保存之前的信号集合,便于恢复工作的展开

3.2 信号的处理方式(捕捉)

进程收到信号有以下三种处理方式:
1、忽略此信号
2、执行该信号的默认处理动作
3、自定义处理动作(即捕捉信号),提供一个信号处理函数,要求内核在处理该信号时切换到用户态执行这个处理函数

typedef void (*sighandler_t)(int)
sighandler_t signal(int signum, sighandler_t handler)
  要执行自定义处理动作,必须通过捕捉信号来实现。这需要为特定信号注册一个 signal 信号处理函数。如果在进程中成功注册了信号处理函数,当指定的信号被触发时,操作系统会中断进程的当前执行流程,不再执行默认的信号处理动作,而是调用这个注册的函数,即回调函数。这个回调函数接收一个参数,即触发的信号编号,并且没有返回值。

int sigaction(int signum, const struct sigaction *act, struct sigaction *oldact)

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

   sigaction 既是函数名又是一个结构体对象,在这里我们关注的是结构体中的两个关键部分,第一个和第三个成员。当某个信号的处理函数被调用时 ,内核自动将当前信号加入进程的信号屏蔽字 ,当信号处理函数返回时,会自动恢复原来的信号屏蔽字。这样就可以防止同一信号的重复触发,如果在调用信号处理函数时, 还希望自动屏蔽另外一些信号 , 则在 sa_mask 字段中添加即可。同时需注意,如果信号未决时,解除了该信号的屏蔽,那么执行 handler 方法之前,会把 pending 表中的该信号由 1 置 0
  并不是所有信号都能被捕捉,一些信号的设计具有强制性和即时性,以确保操作系统在遇到严重错误时,能够立即采取行动。比如 SIGKILL 9号信号和 SIGSTOP 19信号,它们用于立即终止或暂停进程,不能被捕捉或忽略,以此保持了操作系统对进程的最终控制权。不然所有信号被捕捉之后,进程将不再受操作系统控制

3.3 用户态和内核态

  我们编写的用户态代码虽独立于操作系统内核,但常需通过系统调用来访问系统资源或硬件设备。系统调用是用户态程序与内核态之间的桥梁,它们允许进程在用户态和内核态之间切换。由于身份转换和执行内核代码需要时间,频繁的系统调用可能导致性能开销。
  进程在执行时,其上下文信息,包括寄存器状态,必须载入 CPU。寄存器中有些是可见的,如 eaxebx 等,负责存储临时数据;还有些不可见寄存器,如状态寄存器,控制着 CPU 的状态。特别地,CR3 寄存器存储用户级页表的起始地址,其值的变化标志着进程运行模式的切换,0 代表内核态,3 代表用户态,确保了系统能够有效区分当前执行的是用户态还是内核态的代码。
下面我们来进一步理解进程地址空间:
  操作系统中,每个进程地址空间顶部的 3-4GB 空间是专门保留给内核使用的,这片空间对于所有进程都是一样的。它们共享同一份内核级页表,映射到操作系统的物理内存。在这 3-4GB 的内核空间内,进程通过执行系统调用来请求操作系统服务,这些服务包括文件操作、进程控制、网络通信等。当系统调用被触发时,CPU 的控制权从用户态转移到内核态,操作系统执行相应的服务,然后再将控制权安全地交还给用户态程序。这个过程虽然涉及到从用户态到内核态的切换,但在逻辑上,每个进程都可以通过自己的地址空间独立访问内核资源。这种设计使得内核资源可以高效地被所有进程利用,同时保持了内存使用的优化。

3.4 信号处理流程

  在用户空间中,信号处理函数的执行涉及复杂的上下文切换。通过系统调用,陷入内核,如果当用户程序注册了信号的处理方法 handler,就会转回用户态,执行自定义的 handler 函数。值得注意的是,sighandlermain 函数使用不同的堆栈空间,它们之间不存在直接的调用关系,而是作为两个独立的控制流程并行存在,所以我们可以在程序的任意位置注册某个信号的方法。当 handler 函数执行完毕,它将通过一个特殊的系统调用 sigreturn 重新陷入内核态。在内核态即将返回用户态的时候,会对信号进行检测,如果没有新的信号要递达,就会返回 main 函数,执行程序剩下的代码

  • 16
    点赞
  • 18
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

杰瑞的猫^_^

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值