Linux信号机制详解
目录
- Linux 信号机制
- 一、信号基础概念
- 1.1 信号引言与特性
- 1.2 信号处理方式
- 二、信号的产生方式
- 2.1 命令与键盘产生
- 2.2 系统调用产生
- 2.3 软件条件产生
- 2.4 硬件异常产生
- 三、信号的保存与管理
- 3.1 关键概念
- 3.2保存方式
- 四、信号的处理
- 4.1 内核态与用户态
- 4.2 信号处理流程
- 4.2 自定义信号处理
- 五、深入理解地址空间、中断与系统调用
- 5.1地址空间再探讨
- 5.2键盘输入与中断机制
- 5.3操作系统运行与系统调用
- 5.4可重入函数概念
- 六、子进程退出信号
Linux 信号机制
一、信号基础概念
1.1 信号引言与特性
- 异步性:信号产生与进程执行异步,可随时产生
- 识别能力:进程能够识别并理解信号含义
- 处理能力:进程可以捕获并处理信号
- 延迟处理:进程可选择暂时不处理信号,但会记住并在适当时机处理
使用
kill -l
查看所有信号,其中 1-31 号为普通信号,其余为实时信号
1.2 信号处理方式
a. 默认动作(Default Action)
默认动作通常:终止自己,暂停,忽略等
常见的信号:
SIGINT(2):终止进程(Ctrl+C)
SIGQUIT(3):退出进程(Ctrl+\)
SIGABRT(6):允许自定义捕捉,但最终仍终止
上图中Term与Core的区别
类型 | 行为 | 特点 |
---|---|---|
Term | 异常终止 | 直接结束进程 |
Core | 异常终止+核心转储 | 生成core文件辅助调试 |
**核心转储:**帮我们形成一个协助我们进行debug的文件,文件名称为core,该文件内存储了一些异常的时候程序的核心数据,该行为叫做核心转储
在云服务器环境中,核心转储功能通常默认处于关闭状态,这主要是由于云上运行的程序多为用户提供的服务,具备完善的运行维护机制。一旦服务发生崩溃,运维系统会自动将其重启,并通过预设的脚本或其他控制程序进行恢复。在较早的 Linux 系统中,核心转储文件会以 core.pid
的方式命名(其中 pid 为崩溃进程的进程号),如果某个服务因未修复的错误反复崩溃和重启,就会持续生成大量 core 文件,若未能及时清理,可能引发存储空间耗尽等更严重的问题。而在较新的系统中,core 文件不再附带进程号,直接统一命名为 core
,这样即使服务多次崩溃重启,也仅会覆盖同一份 core 文件,从而避免了大量冗余文件的产生。
进程退出时,退出状态status中,低8位的最高位(core dump标志)用来记录进程退出时有没有核心转储,为1表示进行了核心转储
核心转储配置:
ulimit -c 10240 # 开启核心转储,限制文件大小为10240
Core文件调试:
gcc -g program.c -o program # 编译时加入调试信息
./program # 运行产生core文件
gdb program core # 使用gdb调试
b. 忽略动作(Ignore)
进程可选择忽略某些信号
示例:signal(SIGCHLD, SIG_IGN)
可避免僵尸进程
c. 自定义处理(Custom Handler)-- 信号捕捉
用户可注册自定义信号处理函数,对信号的自定义捕捉,我们只要捕捉一次,后续一直有效
二、信号的产生方式
2.1 命令与键盘产生
kill -<信号编号> <PID> # 向指定进程发送信号
键盘组合:
Ctrl+C → SIGINT(2)
Ctrl+\ → SIGQUIT(3)
2.2 系统调用产生
kill() 系统调用
#include <sys/types.h>
#include <signal.h>
int kill(pid_t pid, int sig);
//发送给指定进程sig信号
raise() 函数
#include <signal.h>
int raise(int sig); //向当前进程发送信号
alarm() 函数
#include <unistd.h>
unsigned int alarm(unsigned int seconds);
设置定时器,seconds秒后发送SIGALRM信号,也会终止进程,该函数返回值表示上一个闹钟的剩余时间,参数为0表示取消闹钟。
在系统中,可能存在多份闹钟,操作系统对于闹钟的管理也是有相应的数据结构和数据类型,用最小堆的形式进行管理组织,
“描述”闹钟:
struct alarm
{time_t expired; //未来超时时间 = seconds + Now()pid_t pid;func_t f;......
}
2.3 软件条件产生
管道读端关闭,写端继续写入 → SIGPIPE(13),定时器、间隔计时器等
2.4 硬件异常产生
除0,野指针访问,非法访问,操作等行为让程序异常,也会收到来自操作系统的终止信号,
浮点错误→ SIGFPE(8)
非法内存访问 → SIGSEGV(11)
注意:异常本质是硬件问题,被操作系统识别后发送信号
三、信号的保存与管理
3.1 关键概念
- 实际执行信号的处理的动作称为信号递达(Delivery)
- 信号从产生到递达之间的状态,称为信号未决(Pending)
- 进程可以选择阻塞(Block)某个信号,
- 阻塞一个信号,那么对应的信号一旦产生用不递达,一直未决,直到主动解除阻塞,阻塞和未决没有关系
- 阻塞和忽略是不同的,只要信号被阻塞就不会递达,而忽略是在递达之后可选的一种处理动作
3.2保存方式
底层结构:
在进程pcb中维护这2张位图和1张函数指针表(信号处理的方法),分别是block,pending,hander。
block(信号屏蔽字),该表中bit位的位置代表信号的编号,bit位的内容表示信号是否被阻塞
pending(未决信号集),该表中bit位的位置代表信号的编号,bit位的内容表示信号是否收到
hander表中存放着对应信号的。处理方法,当我们进行信号捕捉的时候就是将该表中的方法替换成我们自己定义的方法。不自定义的时候一般有默认和忽略
sigset_t类型:
Linux操作系统给我们提供了一种位图结构,sigset_t。
block和pending就是sigset_t类型的位图,对于每种信号用一个bit表示"有效"或者“无效”,至于这个类型内部如何存储bit则由系统内部实现,因此系统也给我们提供了许多接口用来操作该类型
以信号sigset_t类型的信号举例:
#include <signal.h>
int sigemptyset(sigset_t *set); // 清空信号集
int sigfillset(sigset_t *set); // 填充所有信号
int sigaddset(sigset_t *set, int signum); // 添加信号
int sigdelset(sigset_t *set, int signum); // 删除信号
int sigismember(const sigset_t *set, int signum); // 判断信号是否在集中
信号屏蔽字管理
调用该函数 可以读取或者更改进程的信号屏蔽字
int sigprocmask(int how, const sigset_t *set, sigset_t *oldset);
how
参数:
SIG_BLOCK
:添加阻塞,相当于mask = mask | setSIG_UNBLOCK
:解除阻塞, 相当于 mask = mask & ~setSIG_SETMASK
:直接设置屏蔽字, 相当于 mask = set
获取未决信号集
int sigpending(sigset_t *set);
四、信号的处理
信号的处理就是递达
信号可能不会立即处理,而是在合适的时间处理
合适的时间是从进程的内核态返回到用户态的时候进行处理
4.1 内核态与用户态
cpu在执行内核代码的时候就是内核态(系统调用执行的时候),这时候执行代码会有更高的权限,因为是操作系统的代码,本质是cpu内的段寄存器(ecs)的低两位为00,此时可以访问进程地址空间的内核空间
cpu在执行 用户写的代码就是用户态(除系统调用外),本质是cpu内的段寄存器(ecs)的低两位为11,此时只能访问进程地址空间的用户空间
4.2 信号处理流程
当进程在执行主程序的某条指令时,如果遇到中断、异常或系统调用而进入操作系统内核处理任务,此时进程处于内核态;操作系统在处理完自身任务并准备返回到用户代码之前,会检查当前进程的信号状态。若发现有信号待处理且动作为忽略,则内核会清除该信号的未决标志位后直接返回用户态;若信号的默认处理方式是终止进程,则大部分情况下内核会直接终止当前进程;若信号设置了自定义处理方法,则内核会先切换回用户态执行用户注册的信号处理函数,待信号处理函数执行完毕后,再次通过系统调用进入内核态,并由内核通过特殊的返回流程恢复至主程序之前被中断的指令位置继续执行,此时进程状态也切换回用户态。
信号捕捉过程进行了4次状态切换,其中在内核态切换回用户态的时候,进行信号的检测和处理
4.2 自定义信号处理
信号捕捉
#include <signal.h>
typedef void (*sighandler_t)(int);
sighandler_t signal(int signum, sighandler_t handler);
handler可选的系统方法:
SIG_IGN
:忽略信号SIG_DFL
:恢复默认行为
sigaction系统调用
sigaction系统调用提供了更强大的信号处理能力:
#include <signal.h>
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;
};
其中sa_mask是信号屏蔽字,用于指定在处理当前信号时需要阻塞的其他信号。当一个信号正在被处理时,系统会自动阻塞该信号本身以防止递归调用,但不会自动阻塞其他信号。通过设置sa_mask,可以在处理某个信号时同时阻塞其他指定信号。需要注意的是,某些关键信号(如SIGKILL、SIGSTOP等)是不允许被阻塞的。
五、深入理解地址空间、中断与系统调用
5.1地址空间再探讨
进程的地址空间由用户空间(通常为3GB)和内核空间(通常为1GB)组成。操作系统本身也是一个程序,在系统启动时会被加载到内存中,并通过内核级页表映射到每个进程的内核空间。关键的是,整个系统只维护一份内核级页表,所有进程共享这一份页表,因此所有进程的内核地址空间内容完全一致。
由此得出三个重要结论:
- 无论进程如何切换,操作系统始终存在于进程的地址空间中,也就是说操作系统是在进程的地址空间内运行的;
- 我们通过系统调用访问操作系统时,实际上是在自己的进程地址空间内进行操作,这与调用库函数没有本质区别;
- 操作系统不信任任何用户程序,因此只提供系统调用接口来约束用户行为,保障系统安全。
5.2键盘输入与中断机制
键盘输入数据的完整过程如下:操作系统启动时会在内存中建立中断向量表(一个函数指针数组),其中每个元素对应一个硬件操作的处理函数,数组下标对应硬件设备的中断号。当键盘有数据输入时,会向CPU发送包含其中断号的中断信号,CPU接收到中断后暂停当前任务,根据中断号在中断向量表中找到对应的处理函数并执行,从而读取键盘数据。
信号机制实际上是模拟中断实现的,但两者有本质区别:中断需要硬件参与(硬件触发+软件处理),而信号是纯粹的软件实现。
5.3操作系统运行与系统调用
操作系统依靠硬件时钟定期产生中断来维持正常运行:时钟硬件会固定间隔向CPU发送中断,触发进程调度程序执行,检查时间片是否用完并在需要时执行进程切换。
系统调用的执行过程则更加复杂:操作系统维护一张系统调用表(函数指针表),当用户程序发起系统调用时,首先将系统调用号(在表中的索引)存入寄存器,然后通过指令(如int 0x80)触发中断切换到内核态,中断处理程序根据寄存器中的系统调用号找到并执行对应的系统调用函数。
值得注意的是,CPU内部产生的中断称为陷阱或异常,而由外部硬件产生的中断才称为中断。
5.4可重入函数概念
可重入函数是指:当一个函数正在执行时,由于信号捕捉或其他执行流导致该函数被再次调用,不会造成数据错误或丢失。相反,不可重入函数在这种情况下可能出现数据不一致的问题。
判断函数是否可重入的关键标准是:如果函数内部只使用局部变量,那么它通常是可重入的;如果函数使用了全局变量、静态数据或动态内存分配,那么它很可能是不可重入的。在编写信号处理函数时,必须确保使用可重入函数,以避免难以调试的并发问题。
六、子进程退出信号
使用SIGCHLD异步回收子进程
子进程退出时 ,会向父进程发送SIGCHLD信号,父进程只要捕捉一下该信号,就可以实现与子进程的解耦,各自执行各自的代码,等到子进程退出发出信号,父进程再去回收
如果有多个子进程同时退出,这样就会导致父进程只能收到一个退出信号,因此在自定义的处理方法中应考虑到多个子进程同时退出的情况,则需要适用非阻塞等待多次检测和回收
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <signal.h>
#include <sys/wait.h>void sigchld_handler(int sig) {while (waitpid(-1, NULL, WNOHANG) > 0) {// 循环回收所有已退出的子进程}
}int main() {signal(SIGCHLD, sigchld_handler);// 创建子进程pid_t pid = fork();if (pid == 0) {// 子进程代码exit(0);}// 父进程继续执行其他任务while (1) {pause(); // 等待信号}return 0;
}
忽略SIGCHLD避免僵尸进程
由于UNIX的历史原因,要想不产生僵尸进程,还可以让父进程调用sigaction将SIGCHLD信号置为SIG_IGN,这样创建的子进程 在终止时会自动清理掉,不会产生僵尸进程,也不会通知父进程,此方法在Linux系统上适用,在其他UNIX系统可能不适用
signal(SIGCHLD, SIG_IGN);