☰
Linux 进程信号入门——从 Ctrl+C 到 kill
2026/10/14 16:12:10 网站建设 项目流程

之前学习进程的时候,经常会在终端里按Ctrl+C结束程序。平时觉得这是一个很普通的操作,直到学习 Linux 信号,才发现这个按键并不是直接把程序关掉,而是通过操作系统给进程发送了一个信号。

这篇先从最常见的信号入手,把信号的基本概念、处理方式以及几种常见的产生方式整理一下。

一、什么是信号?

信号可以理解为操作系统向进程发送的一种异步通知,用来告诉进程某个事件已经发生了。它属于一种软件层面的中断机制。

这里有两个比较重要的特点。

首先,信号的产生和进程正在执行的代码没有必然关系。比如程序正在执行一个循环,用户随时都可能按下Ctrl+C,进程就有可能收到SIGINT信号。程序不需要提前执行到某个特定位置,信号也可以产生。

其次,信号产生后,不代表进程马上就会处理它。进程可能还在执行其他代码,信号需要经过操作系统的处理,在合适的时机递达进程。

可以把它理解成收到了一条通知:事情已经发生了,至于什么时候处理,要看进程当前的执行状态以及信号是否被阻塞。

二、信号的三种处理方式

当进程收到信号时,并不是所有信号都只能让程序退出。通常有三种处理方式:

  • 执行默认动作:由操作系统按照该信号规定的默认行为处理,例如终止进程。
  • 忽略信号:进程收到这个信号后不执行对应的处理动作。
  • 捕捉信号:进程注册自己的处理函数,在信号递达时执行这段代码。

在 Linux 中,可以使用signal()设置某个信号的处理方式:

#include<iostream>#include<unistd.h>#include<signal.h>voidhandler(intsigno){std::cout<<"caught signal: "<<signo<<std::endl;}intmain(){signal(SIGINT,handler);while(true){std::cout<<"process is running"<<std::endl;sleep(1);}return0;}

运行程序后按下Ctrl+C,可以看到程序执行了自己注册的处理函数,而不是按照SIGINT的默认动作退出。

这里有个细节容易忽略:signal(SIGINT, handler)只是设置处理方式,并不是主动产生一个信号。只有后续真的收到SIGINT,处理函数才有机会执行。

如果将注册语句改成:

signal(SIGINT,SIG_IGN);

进程就会忽略SIGINT。改成:

signal(SIGINT,SIG_DFL);

则恢复该信号的默认处理动作。

三、终端按键也能产生信号

平时经常用到的几个组合键,其实对应着不同的信号。

终端操作对应信号常见默认行为
Ctrl+CSIGINT,通常是 2 号信号终止进程
Ctrl+\SIGQUIT,通常是 3 号信号终止进程,并请求产生 Core Dump
Ctrl+ZSIGTSTP暂停前台进程

注意,Ctrl+Z通常是暂停进程,而不是直接结束进程。暂停后的任务可以通过 Shell 的作业控制命令继续处理。

另外,Ctrl+C这类终端控制字符通常会把信号发送给当前前台进程组。后台运行的程序并不会因为用户按下了Ctrl+C就自动收到同样的信号。

例如:

./sig

启动后按Ctrl+C,通常会对前台任务产生SIGINT。

而使用:

./sig&

把程序放到后台运行后,终端控制和前台任务就有所不同了。

四、使用 kill 和相关函数发送信号

除了终端按键,也可以主动向进程发送信号。

最熟悉的方式就是kill命令:

kill-2PID

这里的PID要替换成目标进程的进程号,-2表示发送 2 号信号,也就是SIGINT。

也可以直接使用信号名称:

kill-SIGINTPID

需要注意,kill并不意味着只能杀死进程。它的作用是发送信号,目标进程最终会怎样处理,要看信号对应的处理动作。比如目标进程捕捉了SIGINT,收到信号后也可以执行自己的处理函数。

程序内部同样可以发送信号。

1. kill():向指定进程发送信号

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

pid指定目标进程,sig指定发送的信号。函数成功返回0,出错返回-1。

2. raise():给当前进程发送信号

#include<signal.h>intraise(intsig);

例如:

raise(SIGINT);

这会向当前进程发送SIGINT。如果已经为它注册了处理函数,就可以观察到对应的处理过程。

3. abort():异常终止进程

#include<stdlib.h>abort();

abort()会使当前进程通过SIGABRT异常终止。即使程序为这个信号注册了处理函数,也不能简单地认为捕捉成功后就能让abort()正常返回。

五、由软件条件和硬件异常产生的信号

信号不一定是用户手动发送的,程序运行时也可能因为某些条件而产生信号。

1. alarm() 和 SIGALRM

alarm()可以设置一个定时器,让操作系统在指定秒数后向当前进程发送SIGALRM。

#include<unistd.h>#include<signal.h>intmain(){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()等方式产生信号,系统也可能因为定时器超时、管道异常或者硬件异常而产生信号。进程收到信号后,可以执行默认动作、忽略信号,或者执行自己注册的处理函数。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询