当前位置: 首页 > news >正文

【Linux】进程信号

本文主题内容

  • 理解 Linux 信号的概念与处理方式
  • 掌握信号产生的常见方式
  • 理解信号产生、保存和递达
  • 掌握信号集与信号屏蔽字的操作
  • 使用sigaction捕捉信号
  • 理解信号处理函数的执行流程
  • 理解中断、异常、系统调用与信号的关系
  • 认识可重入函数、volatile与异步信号安全
  • 使用SIGCHLD回收子进程

引言:按下Ctrl+C为什么能终止前台进程?除零和野指针为什么最终表现为进程收到信号?阻塞信号与忽略信号有什么区别?信号处理函数为什么不能随便调用printfmalloc?这些问题的背后,是操作系统对异步事件的统一管理。本文从信号的使用开始,逐步分析内核中的保存方式、捕捉流程和并发安全问题。

一、信号的基本概念

1.1 什么是信号

信号是进程之间进行异步事件通知的一种方式,也可以看作操作系统提供给进程的软件中断机制。

信号具有下面几个特点:

  • 每个信号都有编号和宏定义名称
  • 信号可能在进程执行到任意位置时产生
  • 信号的处理方式在信号到来前就已经确定
  • 信号产生后,可以暂时保存,未必立即处理
  • 常规信号主要表示某类事件是否发生,不适合传递大量数据

信号处理可以分为信号产生、信号保存和信号递达三个阶段。

1.2 查看系统信号

例:

kill-lman7signal

常见信号包括:

信号编号常见来源默认动作
SIGHUP1终端断开或会话变化终止
SIGINT2Ctrl+C终止
SIGQUIT3Ctrl+\终止并可能产生 Core
SIGABRT6abort终止并可能产生 Core
SIGFPE8算术异常终止并可能产生 Core
SIGKILL9强制终止终止
SIGSEGV11非法内存访问终止并可能产生 Core
SIGPIPE13向无读端管道写入终止
SIGALRM14闹钟到期终止
SIGTERM15请求进程正常终止终止
SIGCHLD17子进程状态变化忽略
SIGSTOP19强制暂停暂停
SIGTSTP20Ctrl+Z暂停

不同架构上的部分信号编号可能存在差异,程序应优先使用SIGINT这样的宏,而不是直接写数字。

1.3 三种处理方式

进程对信号可以选择:

  1. 执行默认动作
  2. 忽略信号
  3. 执行自定义处理函数

对应的特殊处理值是:

SIG_DFL SIG_IGN

SIGKILLSIGSTOP不能被捕捉、阻塞或忽略,内核必须保留最终控制进程的能力。

二、初步捕捉信号

2.1 signal 接口

#include<signal.h>typedefvoid(*sighandler_t)(int);sighandler_tsignal(intsignum,sighandler_thandler);

signal用于设置某个信号的处理方式。它只是注册处理函数,并不会直接调用函数。

2.2 捕捉 SIGINT

例:

#include<signal.h>#include<unistd.h>voidHandler(intsignal_number){constcharmessage[]="get SIGINT\n";write(STDOUT_FILENO,message,sizeof(message)-1);}intmain(){signal(SIGINT,Handler);while(1){pause();}return0;}

按下Ctrl+C后,终端驱动和内核向当前前台进程组发送SIGINT。由于程序把默认动作改成了自定义处理函数,所以进程不会直接终止。

2.3 忽略与恢复默认动作

例:

signal(SIGINT,SIG_IGN);

此时进程忽略SIGINT

恢复默认动作:

signal(SIGINT,SIG_DFL);

注意:忽略表示信号递达后不执行其他动作;阻塞表示信号暂时不能递达。二者不是同一个概念。

三、信号的产生

3.1 终端按键产生信号

Shell 中常见控制键包括:

  • Ctrl+C:向前台进程组发送SIGINT
  • Ctrl+\:向前台进程组发送SIGQUIT
  • Ctrl+Z:向前台进程组发送SIGTSTP

控制键只作用于前台进程组。后台进程不会因为当前终端按下Ctrl+C而直接收到同样的终端信号。

3.2 kill 命令

例:

kill-SIGTERM进程IDkill-15进程ID

kill命令的名字容易让人误解,它的本质是发送信号。发送的信号是否终止进程,取决于信号类型和目标进程的处理方式。

3.3 kill 函数

#include<signal.h>#include<sys/types.h>intkill(pid_tpid,intsig);

例:实现一个简化版kill命令。

#include<signal.h>#include<stdio.h>#include<stdlib.h>#include<sys/types.h>intmain(intargc,char*argv[]){if(argc!=3){fprintf(stderr,"Usage: %s signal pid\n",argv[0]);return1;}intsignal_number=atoi(argv[1]);pid_tid=(pid_t)atoi(argv[2]);if(kill(id,signal_number)<0){perror("kill");return2;}return0;}

3.4 raise 与 abort

raise用于给当前进程发送信号。

intraise(intsig);

例:

raise(SIGINT);

abort会使当前进程异常终止,通常表现为收到SIGABRT

#include<stdlib.h>voidabort(void);

3.5 alarm 产生信号

#include<unistd.h>unsignedintalarm(unsignedintseconds);

调用alarm(5)后,内核会在大约 5 秒后向当前进程发送SIGALRM。每个进程只有一个传统 alarm,重新设置会覆盖之前的倒计时。

例:

#include<signal.h>#include<unistd.h>volatilesig_atomic_ttimeout=0;voidAlarmHandler(intsignal_number){timeout=1;}intmain(){signal(SIGALRM,AlarmHandler);alarm(1);while(!timeout){//执行其他计算}return0;}

alarm默认只触发一次。需要周期定时,可以在处理函数外重新设置,或使用setitimer、POSIX Timer、timerfd等接口。

3.6 软件条件产生信号

某些信号由内核检测到的软件条件产生。例如:

  • 向所有读端已经关闭的管道写数据,产生SIGPIPE
  • 子进程退出或状态变化,父进程收到SIGCHLD
  • 闹钟到期,进程收到SIGALRM

3.7 硬件异常转化为信号

程序执行除零、非法地址访问等指令时,CPU 先产生异常并进入内核。内核判断异常与当前用户进程有关,随后把它转化为信号,在返回用户态前进行处理。

因此,信号本身仍然由操作系统发送。硬件异常只是触发内核处理的一种来源。

四、Core Dump

4.1 什么是 Core Dump

某些信号的默认动作不仅终止进程,还会尝试生成 Core 文件。Core 文件保存进程异常时的部分内存、寄存器和线程状态,可用于事后调试。

4.2 开启与查看限制

例:

ulimit-culimit-cunlimited

是否真正生成 Core,还受到系统配置、资源限制、目录权限、容器环境等因素影响。

4.3 使用 GDB 分析

例:

gcc test.c-g-otest./test gdb ./test core

进入 GDB 后可以使用:

bt info registers frame 0

注意:Core 文件可能包含口令、密钥、请求数据等敏感内容,生产环境中应当控制保存位置和访问权限。

五、信号产生、未决与递达

5.1 三个状态

  • 信号产生:事件发生,内核决定向进程发送信号
  • 信号未决:信号已经产生,但还没有执行处理动作
  • 信号递达:进程实际执行默认、忽略或自定义动作

如果信号被阻塞,它会保持未决状态,直到解除阻塞后才有机会递达。

5.2 内核中的表示

内核需要为每个进程保存:

  • 哪些信号正在被阻塞
  • 哪些信号处于未决状态
  • 每个信号对应什么处理动作

常规信号通常使用位图表示。某个常规信号在递达前产生多次,未决位仍然只是1,因此一般不会累计次数。实时信号则支持排队,但本文不展开。

5.3 阻塞与忽略

假设SIGINT已经被阻塞,此时用户按下Ctrl+C

  1. 内核把SIGINT记为未决
  2. 因为阻塞位有效,暂时不递达
  3. 进程以后可以修改信号处理动作
  4. 解除阻塞后,内核按照当时的处理动作递达

所以,即使处理动作暂时设置为忽略,只要信号仍被阻塞,内核也不能简单地把未决状态当作已经完成处理。

六、信号集

6.1 sigset_t

sigset_t用于表示一组信号。其内部实现由系统决定,不应该直接把它当整数操作。

常用函数:

intsigemptyset(sigset_t*set);intsigfillset(sigset_t*set);intsigaddset(sigset_t*set,intsignal_number);intsigdelset(sigset_t*set,intsignal_number);intsigismember(constsigset_t*set,intsignal_number);

使用信号集前必须初始化。

例:

sigset_tset;sigemptyset(&set);sigaddset(&set,SIGINT);sigaddset(&set,SIGQUIT);

6.2 sigprocmask

intsigprocmask(inthow,constsigset_t*set,sigset_t*oldset);

how常见取值:

取值作用
SIG_BLOCKset中的信号加入当前屏蔽字
SIG_UNBLOCK从当前屏蔽字中移除set中的信号
SIG_SETMASK直接用set替换当前屏蔽字

例:阻塞SIGINT

sigset_tset;sigset_toldset;sigemptyset(&set);sigaddset(&set,SIGINT);sigprocmask(SIG_BLOCK,&set,&oldset);

恢复原来的屏蔽字:

sigprocmask(SIG_SETMASK,&oldset,NULL);

6.3 sigpending

intsigpending(sigset_t*set);

该函数读取当前进程的未决信号集。

例:打印 1 到 31 号信号的未决状态。

voidPrintPending(void){sigset_tpending;sigpending(&pending);for(intsignal_number=1;signal_number<=31;signal_number++){intvalue=sigismember(&pending,signal_number);printf("%d",value==1?1:0);}printf("\n");}

6.4 阻塞实验

例:

#include<signal.h>#include<stdio.h>#include<unistd.h>intmain(){sigset_tset;sigset_toldset;sigemptyset(&set);sigaddset(&set,SIGINT);sigprocmask(SIG_BLOCK,&set,&oldset);for(intcount=10;count>0;count--){sigset_tpending;sigpending(&pending);printf("SIGINT pending: %d\n",sigismember(&pending,SIGINT));sleep(1);}printf("unblock SIGINT\n");sigprocmask(SIG_SETMASK,&oldset,NULL);return0;}

运行期间按下Ctrl+CSIGINT会进入未决状态。恢复屏蔽字时,如果仍然采用默认动作,信号递达后进程会终止。

七、使用 sigaction 捕捉信号

7.1 sigaction 接口

sigactionsignal提供更明确、更完整的控制。

#include<signal.h>intsigaction(intsignal_number,conststructsigaction*action,structsigaction*old_action);

结构中的常用字段:

  • sa_handler:普通信号处理函数
  • sa_mask:执行处理函数期间额外阻塞的信号
  • sa_flags:行为选项

7.2 注册处理函数

例:

#include<signal.h>#include<string.h>#include<unistd.h>voidHandler(intsignal_number){constcharmessage[]="signal delivered\n";write(STDOUT_FILENO,message,sizeof(message)-1);}intmain(){structsigactionaction;memset(&action,0,sizeof(action));action.sa_handler=Handler;sigemptyset(&action.sa_mask);action.sa_flags=0;if(sigaction(SIGINT,&action,NULL)<0){return1;}while(1){pause();}return0;}

7.3 sa_mask 的作用

处理某个信号时,内核会自动暂时阻塞当前信号,避免同类信号立即重入。如果还希望处理期间阻塞其他信号,可以加入sa_mask

例:处理SIGINT时同时阻塞SIGQUIT

sigemptyset(&action.sa_mask);sigaddset(&action.sa_mask,SIGQUIT);

处理函数返回后,原来的信号屏蔽字会自动恢复。

八、信号处理函数的执行流程

8.1 用户态与内核态切换

信号处理函数位于用户空间,但信号状态由内核维护。一个典型流程是:

  1. 用户程序正在执行
  2. 因中断、异常或系统调用进入内核
  3. 内核完成当前工作,准备返回用户态
  4. 内核检查到有可递达信号
  5. 内核构造新的用户态上下文,先执行信号处理函数
  6. 处理函数结束后通过sigreturn再次进入内核
  7. 内核恢复被打断的用户态上下文,原程序继续运行

信号处理函数不是由main普通调用的,二者是两个独立的控制流程。处理完成后,程序通常从被打断的位置附近继续执行。

8.2 信号为什么是异步的

用户程序无法预知信号具体在何时递达。它可能发生在一次表达式计算、内存分配或链表修改的中间。因此信号处理函数与主流程之间存在类似并发的重入问题。

注意:信号可能在主流程执行到几乎任意位置时打断它,编写处理函数时必须按照异步环境考虑安全性。

九、中断、异常与系统调用

9.1 硬件中断

键盘、网卡、磁盘等外设可以通过中断通知 CPU。CPU 根据中断向量进入内核预先注册的处理程序,操作系统不需要持续轮询每个设备。

9.2 时钟中断

时钟设备周期产生中断,为操作系统提供时间基准。内核可以在时钟中断中更新时间、统计时间片并触发调度,使多个进程轮流使用 CPU。

9.3 异常

除零、缺页、非法指令、访问越界等事件由 CPU 在执行当前指令时检测,属于同步异常。内核接管后决定怎样处理:

  • 合法缺页:分配或加载页面,更新页表后继续执行
  • 非法内存访问:向当前进程产生SIGSEGV
  • 除零:向当前进程产生SIGFPE

9.4 系统调用

用户程序不能直接执行所有特权操作,需要通过syscall等机制主动进入内核。系统调用号用于选择具体的内核服务,参数通过寄存器或用户缓冲区传递。

9.5 统一理解

可以把三者区分为:

  • 中断:由外部设备异步触发
  • 异常:由当前指令执行结果同步触发
  • 系统调用:用户程序主动请求进入内核

它们都会导致 CPU 从用户态进入内核态,但触发来源和处理语义不同。信号经常是内核处理这些事件之后,通知用户进程的一种结果。

十、可重入函数与异步信号安全

10.1 什么是重入

假设主流程正在修改一个全局链表,只完成了一半就被信号打断;处理函数又调用同一个链表插入函数。第二次调用结束后,主流程继续执行,链表可能已经被破坏。

同一个函数在前一次调用尚未结束时再次进入,叫作重入。如果函数依赖共享可变状态,重入就可能导致错误。

10.2 常见的不安全操作

信号处理函数中不应随意调用:

  • printfstd::cout等缓冲 IO
  • mallocfreenewdelete
  • 大多数使用内部锁或全局状态的库函数

因为主流程可能正好在这些函数内部被打断,处理函数再次调用会破坏数据或产生死锁。

POSIX 定义了一组异步信号安全函数,write是常用成员之一。

10.3 安全的设计方式

信号处理函数应当尽量短,只做下面这类操作:

  • 设置volatile sig_atomic_t类型的标志
  • 向预先创建的管道写入一个简单通知
  • 调用明确属于异步信号安全集合的函数

把真正的业务处理留给主循环完成。

十一、volatile 与 sig_atomic_t

11.1 volatile 的作用

编译器可能把循环中反复读取的普通变量缓存到寄存器。如果信号处理函数异步修改该变量,主循环未必会按预期重新读取内存。

volatile告诉编译器,每次访问都应当按照可观察内存操作处理,不要把值长期假定为不变。

11.2 信号标志的写法

例:

#include<signal.h>#include<unistd.h>volatilesig_atomic_tquit=0;voidHandler(intsignal_number){quit=1;}intmain(){signal(SIGINT,Handler);while(!quit){//执行主循环}return0;}

sig_atomic_t保证该类型的简单读写适合用于信号处理场景。

注意volatile不是线程同步工具,不提供复合操作原子性、内存顺序或互斥。多线程并发应使用 C/C++ 原子类型、互斥锁等机制。

十二、SIGCHLD 与子进程回收

12.1 SIGCHLD

子进程退出、停止或继续时,父进程可能收到SIGCHLD。其默认动作通常是忽略,但这不等于子进程一定会自动被回收。

父进程可以注册处理函数,在收到通知后调用waitpid回收已经退出的子进程。

12.2 为什么要循环 waitpid

常规信号不保证按产生次数排队。多个子进程几乎同时退出时,父进程可能只观察到一次SIGCHLD递达,因此处理函数中要循环回收。

例:

#include<errno.h>#include<signal.h>#include<sys/wait.h>#include<unistd.h>voidChildHandler(intsignal_number){intold_errno=errno;while(waitpid(-1,NULL,WNOHANG)>0){}errno=old_errno;}intmain(){structsigactionaction;action.sa_handler=ChildHandler;sigemptyset(&action.sa_mask);action.sa_flags=SA_RESTART|SA_NOCLDSTOP;sigaction(SIGCHLD,&action,NULL);for(inti=0;i<3;i++){pid_tid=fork();if(id==0){_exit(0);}}while(1){pause();}return0;}

保存和恢复errno,可以避免处理函数改变主流程正在观察的错误码。

十三、常见问题

13.1 pause 的竞态条件

下面的思路存在竞态:

while(!flag){pause();}

如果信号恰好在判断flag之后、调用pause之前递达,程序可能错过唤醒并永久阻塞。需要原子地更换屏蔽字并等待时,应使用sigsuspend

13.2 被信号中断的系统调用

阻塞系统调用可能因为信号递达而返回-1,同时把errno设为EINTR。程序要根据接口语义选择重试,或者为sigaction设置合适的SA_RESTART

13.3 不要在处理函数中做复杂工作

处理函数执行期间,主流程处于被打断状态。复杂日志、内存分配、锁操作和业务回调都会扩大重入风险。最可靠的策略是只记录事件,再由正常执行流处理。

十四、总结

信号是操作系统向进程异步通知事件的机制。信号可以由终端按键、命令与系统调用、软件条件或硬件异常产生。产生后的信号先进入未决状态,内核根据进程的阻塞集合与处理动作,在合适的返回用户态时机完成递达。

信号处理有默认、忽略和自定义三种方式。sigset_tsigprocmasksigpending用于观察并控制信号集合,sigaction用于可靠地注册处理函数和临时屏蔽其他信号。信号处理函数属于异步控制流,必须考虑可重入性和异步信号安全,通常只设置volatile sig_atomic_t标志或发送简单通知。

真正理解信号,需要把用户态程序、内核中的 pending/block/handler 信息以及中断返回流程连在一起,而不是只记住几个信号编号。

http://www.jsqmd.com/news/1385039/

相关文章:

  • 2026工业网络安全技术演进、防护体系搭建与产品选型实战指南
  • DeepSeek+Pi-Agent吊打Claude-Code
  • 微信聊天记录导出终极指南:用 WeChatMsg 永久保存并生成年度报告
  • 游戏字幕看不懂?Translumo实时屏幕翻译工具零基础上手全指南
  • 高校实验室试剂采购平台推荐:从选型到报销的全流程指南
  • 阿里云-cdn的证书到期-续期
  • Bash命令补全神器:从原理到实战,提升终端效率必备
  • Win11Debloat 快速上手:这款免费开源系统优化工具,几分钟让 Windows 11 提速又清净
  • 英雄联盟玩家的4个深夜破防瞬间:免费开源的 League Akari 辅助工具是怎么救场的
  • Git历史重写工具filter-repo详解与应用实践
  • Switchyard:专为大语言模型流量打造的 Rust 代理与库,功能强大!
  • Dify分隔符功能说明
  • 免费网页版暗黑2存档编辑器d2s-editor上手教程:零基础三分钟改出心仪角色
  • 网站建设需要考虑因素有哪些?新手必看避坑指南及全流程解析
  • Windows硬件监控终极指南:FanControl与AIDA64联动,告别风扇噪音与温度焦虑
  • 松滋市白蚁防治服务商选择标准问答客观解读 - 米諾
  • 加密音乐打不开?5分钟上手Unlock-Music,免费解锁12种主流加密格式
  • faster-whisper-GUI 终极实战:把音视频批量变成字幕的本地语音转文字指南
  • 别再只会查库了:复杂数据链路测试,我现在基本都按这套方法做
  • 鸿蒙测试2
  • 终极免费开源跨平台自动化工具:3步掌握鼠标键盘录制神器
  • 百度文库三步免费提取:去广告、加载全文、一键保存PDF
  • Windows Defender移除终极指南:3种模式全面提升系统性能与游戏体验
  • 10分钟搞定Mem Reduct内存清理:中文界面与自动清理一次配好
  • 万方AIGC检测报告怎么看?如何按疑似片段完成修改和复检?
  • SQL Server图片存储实战:VARBINARY(MAX)方案设计与性能优化
  • 厦门招聘平台哪个好:【帅聘网】成就自我 - 晴光转树
  • 31个功能!NSC_BUILDER:Switch游戏文件管理的终极解决方案
  • ERPNext一键安装保姆级指南:4步搞定全版本自动化部署
  • DABM-D223数据采集卡架构深度解析:STM32H743+FPGA双核如何各司其职