之前学习进程的时候经常会在终端里按CtrlC结束程序。平时觉得这是一个很普通的操作直到学习 Linux 信号才发现这个按键并不是直接把程序关掉而是通过操作系统给进程发送了一个信号。这篇先从最常见的信号入手把信号的基本概念、处理方式以及几种常见的产生方式整理一下。一、什么是信号信号可以理解为操作系统向进程发送的一种异步通知用来告诉进程某个事件已经发生了。它属于一种软件层面的中断机制。这里有两个比较重要的特点。首先信号的产生和进程正在执行的代码没有必然关系。比如程序正在执行一个循环用户随时都可能按下CtrlC进程就有可能收到SIGINT信号。程序不需要提前执行到某个特定位置信号也可以产生。其次信号产生后不代表进程马上就会处理它。进程可能还在执行其他代码信号需要经过操作系统的处理在合适的时机递达进程。可以把它理解成收到了一条通知事情已经发生了至于什么时候处理要看进程当前的执行状态以及信号是否被阻塞。二、信号的三种处理方式当进程收到信号时并不是所有信号都只能让程序退出。通常有三种处理方式执行默认动作由操作系统按照该信号规定的默认行为处理例如终止进程。忽略信号进程收到这个信号后不执行对应的处理动作。捕捉信号进程注册自己的处理函数在信号递达时执行这段代码。在 Linux 中可以使用signal()设置某个信号的处理方式#includeiostream#includeunistd.h#includesignal.hvoidhandler(intsigno){std::coutcaught signal: signostd::endl;}intmain(){signal(SIGINT,handler);while(true){std::coutprocess is runningstd::endl;sleep(1);}return0;}运行程序后按下CtrlC可以看到程序执行了自己注册的处理函数而不是按照SIGINT的默认动作退出。这里有个细节容易忽略signal(SIGINT, handler)只是设置处理方式并不是主动产生一个信号。只有后续真的收到SIGINT处理函数才有机会执行。如果将注册语句改成signal(SIGINT,SIG_IGN);进程就会忽略SIGINT。改成signal(SIGINT,SIG_DFL);则恢复该信号的默认处理动作。三、终端按键也能产生信号平时经常用到的几个组合键其实对应着不同的信号。终端操作对应信号常见默认行为CtrlCSIGINT通常是 2 号信号终止进程Ctrl\SIGQUIT通常是 3 号信号终止进程并请求产生 Core DumpCtrlZSIGTSTP暂停前台进程注意CtrlZ通常是暂停进程而不是直接结束进程。暂停后的任务可以通过 Shell 的作业控制命令继续处理。另外CtrlC这类终端控制字符通常会把信号发送给当前前台进程组。后台运行的程序并不会因为用户按下了CtrlC就自动收到同样的信号。例如./sig启动后按CtrlC通常会对前台任务产生SIGINT。而使用./sig把程序放到后台运行后终端控制和前台任务就有所不同了。四、使用 kill 和相关函数发送信号除了终端按键也可以主动向进程发送信号。最熟悉的方式就是kill命令kill-2PID这里的PID要替换成目标进程的进程号-2表示发送 2 号信号也就是SIGINT。也可以直接使用信号名称kill-SIGINTPID需要注意kill并不意味着只能杀死进程。它的作用是发送信号目标进程最终会怎样处理要看信号对应的处理动作。比如目标进程捕捉了SIGINT收到信号后也可以执行自己的处理函数。程序内部同样可以发送信号。1. kill()向指定进程发送信号#includesys/types.h#includesignal.hintkill(pid_t pid,intsig);pid指定目标进程sig指定发送的信号。函数成功返回0出错返回-1。2. raise()给当前进程发送信号#includesignal.hintraise(intsig);例如raise(SIGINT);这会向当前进程发送SIGINT。如果已经为它注册了处理函数就可以观察到对应的处理过程。3. abort()异常终止进程#includestdlib.habort();abort()会使当前进程通过SIGABRT异常终止。即使程序为这个信号注册了处理函数也不能简单地认为捕捉成功后就能让abort()正常返回。五、由软件条件和硬件异常产生的信号信号不一定是用户手动发送的程序运行时也可能因为某些条件而产生信号。1. alarm() 和 SIGALRMalarm()可以设置一个定时器让操作系统在指定秒数后向当前进程发送SIGALRM。#includeunistd.h#includesignal.hintmain(){alarm(3);while(true){// 等待信号}return0;}上面的程序在设置定时器后持续运行通常在三秒后收到SIGALRM。如果没有修改这个信号的默认处理方式进程会终止。alarm()设置的是一次性闹钟超时后不会自动重复。将参数设置为0可以取消之前设置的闹钟。如果希望进程等待信号而不是一直空转可以了解一下pause()。它会让调用进程休眠直到有相应的信号递达并满足唤醒条件。2. 硬件异常产生的信号程序中的某些错误也可能通过信号表现出来。例如除零等运算异常可能对应SIGFPE。访问非法内存地址可能对应SIGSEGV。也就是说程序出现段错误并不只是 C 层面看到了一条错误提示。底层通常是处理器或内存管理单元检测到异常由内核处理后向进程发送相应信号。这也是为什么使用空指针访问内存时经常会看到Segmentation fault。六、Core Dump 是什么有些信号除了终止进程还会请求生成 Core Dump。Core Dump 可以理解为程序异常终止时保存下来的部分进程内存信息后续可以配合调试器分析程序崩溃前的状态排查段错误等问题。不过信号的默认动作包含 Core Dump并不代表磁盘上一定会生成 core 文件。是否真的生成文件还会受到进程资源限制等因素影响。可以在 Shell 中使用ulimit-a查看当前 Shell 的各项资源限制其中包括core file size。如果 Core Dump 大小限制为0就可能出现进程异常终止了但当前目录没有生成 core 文件的情况。开发调试时可以根据实际需要调整限制例如ulimit-c1024该设置只影响当前 Shell 及其后续创建的进程所继承的相关限制不会自动修改已经运行的其他进程。七、总结这一篇最重要的是弄清楚信号不是简单的“杀进程命令”而是 Linux 中用于异步通知进程的一种机制。用户可以通过终端按键、kill()、raise()等方式产生信号系统也可能因为定时器超时、管道异常或者硬件异常而产生信号。进程收到信号后可以执行默认动作、忽略信号或者执行自己注册的处理函数。