news 2026/6/12 1:18:12

Linux 信号详解:从 Ctrl+C 到进程异常退出,真正理解信号机制

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Linux 信号详解:从 Ctrl+C 到进程异常退出,真正理解信号机制

引入信号概念

在 Linux 中,信号是一个非常重要的概念。我们平时写程序、调试程序、管理进程时,经常会在不知不觉中用到它。

比如按下:

Ctrl + C

正在运行的程序会退出。

再比如执行:

kill-9pid

某个进程会被强制杀死。

还有程序访问空指针时出现的:

Segmentation fault

这些现象背后,其实都和 Linux 的信号机制有关。

信号这个东西刚开始看起来有点抽象,但如果用一句话来理解,它其实就是:

信号是 Linux 提供的一种异步通知机制,用来告诉进程:某件事情发生了,你需要处理一下。

这篇文章就从最熟悉的Ctrl+C开始,一步一步讲清楚信号是什么、怎么产生、怎么保存、怎么处理,以及它在实际编程中的使用。


一、从 Ctrl+C 开始理解信号

先看一个最简单的程序:

#include<stdio.h>#include<unistd.h>intmain(){while(1){printf("I am running...\n");sleep(1);}return0;}

编译运行:

gcc test.c-otest./test

程序会一直打印:

I am running... I am running... I am running...

这时候如果我们按下Ctrl+C,程序就会退出。

表面上看,好像是键盘直接让程序停止了。实际上并不是这样。

当我们按下Ctrl+C时,终端驱动会向当前前台进程发送一个信号:

SIGINT

SIGINT的默认动作就是终止进程,所以程序退出了。

也就是说,Ctrl+C本身并不是“魔法按键”,它只是触发了一个信号。

信号可以来自很多地方,比如用户按键、命令发送、程序异常、定时器到期、子进程退出等。进程收到信号以后,可以执行默认动作,也可以选择忽略,或者执行自己写好的处理函数。


二、常见信号先有个印象

Linux 中的信号有很多,可以通过下面的命令查看:

kill-l

常见信号如下:

信号编号常见来源默认动作
SIGINT2按下Ctrl+C终止进程
SIGQUIT3按下Ctrl+\终止进程并产生 core dump
SIGKILL9kill -9 pid强制终止进程
SIGSEGV11访问非法内存终止进程并产生 core dump
SIGPIPE13管道读端关闭后继续写终止进程
SIGALRM14定时器到期终止进程
SIGTERM15默认kill pid终止进程
SIGCHLD17子进程退出默认忽略
SIGSTOP19暂停进程暂停进程
SIGCONT18继续运行暂停的进程继续进程

这里有几个信号需要重点记一下。

SIGINT是按下Ctrl+C时产生的信号,它的默认动作是终止进程。但是这个信号可以被捕捉,所以程序可以选择不退出,而是执行自己定义的处理函数。

SIGKILLkill -9发送的信号,它非常特殊。这个信号不能被捕捉、不能被忽略、也不能被阻塞。系统必须保留这样一种强制终止进程的手段,否则某些进程可能永远杀不掉。

SIGSEGV就是常见的段错误。比如:

int*p=NULL;*p=10;

这段代码访问了非法内存,程序一般就会收到SIGSEGV信号,然后异常退出。

SIGCHLD在多进程编程中非常重要。子进程退出时,操作系统会给父进程发送SIGCHLD,父进程可以借助它回收子进程,避免僵尸进程。

刚开始学习时,不需要把所有信号编号都背下来。先记住几个高频的:SIGINTSIGKILLSIGTERMSIGSEGVSIGCHLD就够了。


三、信号从产生到处理,中间发生了什么

信号不是一产生就一定马上被处理。

一个信号从产生到真正执行处理动作,中间大致会经历这样的流程:

这里面有几个关键概念需要连在一起理解。

首先是信号产生。信号可以由键盘产生,比如Ctrl+C;可以由命令产生,比如kill pid;也可以由程序异常产生,比如空指针访问;还可以由程序自己给自己发送,比如调用raise

信号产生之后,会被记录到进程中。如果这个信号已经产生,但是还没有被处理,就叫做未决信号,也就是 pending signal。

但是进程还可以选择暂时屏蔽某些信号。被屏蔽的信号即使产生了,也不会马上被处理,而是继续保持未决状态。这个过程叫做信号阻塞。

这里要特别注意:

阻塞不是忽略。

阻塞的意思是:

信号来了,但是我现在先不处理,等解除阻塞之后再说。

忽略的意思是:

信号来了,但是我直接丢掉,不处理。

这两个概念一定不要混淆。

从内核角度看,每个进程都有自己的进程控制块,也就是 PCB。信号相关的信息也会保存在 PCB 中。

可以简单理解成:进程内部有几张“表”。

pending信号集记录哪些信号已经来了;block信号集记录哪些信号现在被屏蔽;handler表记录每个信号应该怎么处理。

比如SIGINT已经产生了,并且它又被阻塞了,那么它就会同时出现在 pending 和 block 中。

这时候它的状态就是:

SIGINT 已经来了,但是暂时不能被处理。

等以后解除阻塞时,这个信号才有机会被递达。


四、发送信号与捕捉信号

理解完信号的基本流程之后,再来看相关命令和函数就清晰很多了。

很多人看到kill命令,第一反应是“杀进程”。其实kill的本质不是杀进程,而是向指定进程发送信号。

语法如下:

kill-信号 pid

比如:

kill-2pid

表示向指定进程发送SIGINT

也可以写成:

kill-SIGINTpid

如果不指定信号,默认发送的是SIGTERM

killpid

SIGTERM可以理解成一种比较礼貌的退出请求。它是在告诉进程:“你该退出了。”

而:

kill-9pid

发送的是SIGKILL,属于强制终止。进程没有机会执行清理逻辑。

所以实际使用时,一般建议先用:

killpid

如果进程确实无法正常退出,再考虑:

kill-9pid

程序中也可以捕捉信号,最简单的方式是使用signal函数:

#include<stdio.h>#include<signal.h>#include<unistd.h>voidhandler(intsigno){printf("捕捉到信号:%d\n",signo);}intmain(){signal(SIGINT,handler);while(1){printf("程序正在运行,按 Ctrl+C 试试...\n");sleep(1);}return0;}

运行程序后,按下Ctrl+C,程序不会直接退出,而是执行handler函数。

原本SIGINT的默认动作是终止进程,现在我们把它改成了执行自己的处理函数。

信号递达后,大体有三种处理方式:

不过需要注意,signal虽然简单,但是在实际工程中更推荐使用sigaction,因为它的行为更加明确,也能设置更多选项。

#include<stdio.h>#include<signal.h>#include<unistd.h>voidhandler(intsigno){printf("catch signal: %d\n",signo);}intmain(){structsigactionact;act.sa_handler=handler;sigemptyset(&act.sa_mask);act.sa_flags=0;sigaction(SIGINT,&act,NULL);while(1){printf("running...\n");sleep(1);}return0;}

sigaction中常见的几个成员如下:

structsigaction{void(*sa_handler)(int);sigset_tsa_mask;intsa_flags;};

其中,sa_handler表示信号处理函数;sa_mask表示执行处理函数期间临时屏蔽哪些信号;sa_flags用来设置额外选项。

初学阶段不需要把所有选项都背下来,先掌握这种基本写法就够了。

还要注意一点,信号处理函数不是普通函数。

普通函数是我们主动调用的,而信号处理函数是信号到来后,由操作系统安排执行的。它可能在程序执行的任意位置被调用,所以处理函数里面不要写太复杂的逻辑。

教学代码里经常用printf,但严格来说,printf并不是异步信号安全函数。更稳妥的做法是只修改一个标志位,然后让主流程去处理真正的逻辑。

例如:

#include<stdio.h>#include<signal.h>#include<unistd.h>volatilesig_atomic_tquit=0;voidhandler(intsigno){quit=1;}intmain(){signal(SIGINT,handler);while(!quit){printf("running...\n");sleep(1);}printf("收到退出信号,开始清理资源...\n");return0;}

这种写法更接近实际开发习惯。


五、信号阻塞:让信号先等一等

信号可以被阻塞。所谓阻塞,就是信号已经来了,但是进程暂时不处理它。

阻塞信号需要用到信号集,常见函数有:

sigemptyset();sigaddset();sigdelset();sigismember();sigprocmask();sigpending();

这些函数名字看起来比较多,但思路很简单:

先定义一个信号集 把要阻塞的信号加入信号集 调用 sigprocmask 修改进程的阻塞信号集 用 sigpending 查看哪些信号处于未决状态

下面这个程序会阻塞SIGINT,也就是Ctrl+C对应的信号:

#include<stdio.h>#include<signal.h>#include<unistd.h>voidprint_pending(){sigset_tpending;sigpending(&pending);for(inti=1;i<=31;i++){if(sigismember(&pending,i))printf("1");elseprintf("0");}printf("\n");}intmain(){sigset_tset;sigemptyset(&set);sigaddset(&set,SIGINT);sigprocmask(SIG_BLOCK,&set,NULL);while(1){print_pending();sleep(1);}return0;}

运行程序后,按下Ctrl+C,你会发现程序没有退出。

但是 pending 信号集中对应的位置会变成 1,大概类似这样:

0000000000000000000000000000000 0000000000000000000000000000000 ^C0100000000000000000000000000000 0100000000000000000000000000000

这说明SIGINT已经来了,但是因为它被阻塞了,所以暂时没有被处理。

再看一个解除阻塞的例子:

#include<stdio.h>#include<signal.h>#include<unistd.h>voidhandler(intsigno){printf("处理信号:%d\n",signo);}intmain(){signal(SIGINT,handler);sigset_tset;sigemptyset(&set);sigaddset(&set,SIGINT);sigprocmask(SIG_BLOCK,&set,NULL);printf("SIGINT 已被阻塞,10 秒内按 Ctrl+C 不会立即处理\n");sleep(10);printf("解除 SIGINT 阻塞\n");sigprocmask(SIG_UNBLOCK,&set,NULL);while(1){sleep(1);}return0;}

程序运行后,在前 10 秒内按下Ctrl+C,不会马上执行处理函数。等到 10 秒之后解除阻塞,之前处于 pending 状态的SIGINT就会被递达。

这就能看出阻塞和忽略的区别:

阻塞:信号来了,先放着,之后可能处理。 忽略:信号来了,直接丢掉,不再处理。

还有一个细节也很重要:

普通信号在未决状态下不会重复排队。

也就是说,如果SIGINT被阻塞了,你连续按很多次Ctrl+C,内核一般只记录“SIGINT 来过”,而不会记录“SIGINT 来了 10 次”。

普通信号更像是一个位图:

0 表示没来 1 表示来了

至于来了几次,普通信号并不关心。


六、信号在实际编程中的几个典型场景

信号不是只存在于课本里的概念,在实际 Linux 编程中经常会遇到。

一个很常见的场景是定时器。

alarm函数可以设置一个闹钟:

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

它的意思是:seconds秒后,系统向当前进程发送SIGALRM信号。

#include<stdio.h>#include<unistd.h>intmain(){alarm(3);while(1){printf("running...\n");sleep(1);}return0;}

这个程序运行 3 秒后会被终止。原因是alarm(3)到期后,系统发送SIGALRM,而SIGALRM的默认动作就是终止进程。

如果我们捕捉SIGALRM,就可以实现一个简单的周期性任务:

#include<stdio.h>#include<signal.h>#include<unistd.h>voidhandler(intsigno){printf("闹钟响了\n");alarm(1);}intmain(){signal(SIGALRM,handler);alarm(1);while(1){pause();}return0;}

这里的pause函数表示让进程暂停,直到有信号到来。每次SIGALRM到来后,执行处理函数,然后在处理函数里重新设置下一次闹钟。

另一个很常见的场景是回收子进程。

当子进程退出时,操作系统会给父进程发送SIGCHLD。如果父进程不回收子进程,子进程就可能变成僵尸进程。

#include<stdio.h>#include<stdlib.h>#include<signal.h>#include<unistd.h>#include<sys/wait.h>voidhandler(intsigno){while(waitpid(-1,NULL,WNOHANG)>0){printf("回收一个子进程\n");}}intmain(){structsigactionact;act.sa_handler=handler;sigemptyset(&act.sa_mask);act.sa_flags=0;sigaction(SIGCHLD,&act,NULL);for(inti=0;i<3;i++){pid_tpid=fork();if(pid==0){printf("child %d exit\n",getpid());exit(0);}}while(1){sleep(1);}return0;}

这里处理函数中用了:

while(waitpid(-1,NULL,WNOHANG)>0)

为什么是while

因为多个子进程可能几乎同时退出,而普通信号不会排队。父进程可能只收到一次SIGCHLD,但实际上有多个子进程需要回收。所以需要循环调用waitpid,把已经退出的子进程都回收干净。

还有一个常见场景是SIGPIPE

比如管道中,读端已经关闭,写端还继续写,就可能触发SIGPIPE

例如:

catbigfile|head-n1

head读完一行后就退出了,管道读端关闭。如果cat继续向管道写数据,就可能收到SIGPIPE

网络编程中也会遇到类似情况。如果对端连接已经关闭,本端还继续写数据,也可能因为相关问题导致程序异常退出。因此一些网络程序会选择忽略SIGPIPE

signal(SIGPIPE,SIG_IGN);

这样程序不会因为一次写操作直接被信号终止,而是可以通过返回值和错误码自己处理异常。


七、程序崩溃和 core dump 也和信号有关

平时调试程序时,经常会看到:

Segmentation fault

它背后对应的信号就是SIGSEGV

有些信号的默认动作不只是终止进程,还会产生 core dump。core dump 可以理解成程序崩溃时留下的一份“现场快照”。

常见会产生 core dump 的信号有:

SIGSEGV SIGQUIT SIGABRT

可以通过下面的命令开启 core dump:

ulimit-cunlimited

然后写一个故意崩溃的程序:

#include<stdio.h>intmain(){int*p=NULL;*p=10;return0;}

运行后,如果生成了 core 文件,就可以使用 gdb 调试:

gdb ./a.out core

这样就能看到程序崩溃的位置。

所以信号不仅能控制进程退出,也能帮助我们定位程序异常。


八、最后把信号机制串起来

学信号时,不建议一开始死记函数。更好的方式是抓住这条主线:

信号为什么产生 | v 信号产生后保存在哪里 | v 信号是否被阻塞 | v 信号什么时候递达 | v 递达后执行默认动作、忽略,还是自定义处理函数

再用一句话总结:

信号是 Linux 中的一种异步事件通知机制。
它不是普通函数调用,而是操作系统在某些事件发生后通知进程的一种方式。信号产生后可能暂时处于未决状态,如果没有被阻塞,就会在合适的时机递达给进程。进程可以选择默认处理、忽略信号,或者执行自定义处理函数。

刚开始学信号时,最重要的不是记住所有信号编号,而是理解这几个核心问题:

信号从哪里来? 信号来了之后保存在哪里? 为什么信号来了不一定马上处理? 阻塞和忽略有什么区别? 自定义信号处理函数什么时候执行?

把这几个问题想明白之后,再去看signalsigactionsigprocmasksigpendingwaitpid这些函数,就不会觉得零散了。

Linux 信号机制看起来复杂,但本质上就是一句话:

操作系统通过信号告诉进程:有事情发生了,接下来怎么处理,由系统默认规则和进程自身设置共同决定。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/6/12 1:15:21

软件学院项目实训-智面(职导助手)个人工作(七)

一、目标概览 前期已完成固化题库&#xff08;Java 后端 / 算法各 30 题&#xff09;、八类追问模板&#xff08;83&#xff09;、面试状态机、RAG 向量检索等基础能力。本周工作重心从「配置文件里有了」转向「页面上能感到」&#xff1a;八类追问真正驱动多轮对话&#xff1…

作者头像 李华
网站建设 2026/6/12 1:11:25

不止于存储:用N32G0系列内部FLASH实现断电数据保存与配置管理

超越基础存储&#xff1a;N32G0系列内部FLASH的高阶应用实践在嵌入式系统开发中&#xff0c;数据持久化存储是一个永恒的话题。当我们需要保存用户配置、设备参数或运行日志时&#xff0c;传统做法是外接EEPROM或FRAM芯片。但你知道吗&#xff1f;现代MCU内置的FLASH存储器&…

作者头像 李华
网站建设 2026/6/12 1:11:02

一篇讲透 Agent:Token、Skill、RAG、MCP、SDD、Harness

上周有个朋友拿着一个 Agent 项目来问我。 他做的是代码变更助手&#xff1a;用户提一句“给订单模块加一个优惠券核销能力”&#xff0c;Agent 自动读代码、查接口文档、改代码、跑测试&#xff0c;最后生成 PR。 Demo 很顺。 第一轮它能找到 OrderService&#xff0c;第二轮…

作者头像 李华