Linux进程信号(一):从Ctrl+C到kill、raise、alarm,深入理解信号的概念与产生原理

🔥 星恒随风: 个人主页 ❄️ 个人专栏: 《指针合集》 | 《C语言基础》 | 《数据结构》 | 《机器学习导论》 | 《前端基础》 | 《python基础》 | 《C++从入门到入土》 | 《Linux的学习之旅》 ✨ 数据即知识,压缩即智能
文章目录
- Linux进程信号(一):从Ctrl+C到kill、raise、alarm,深入理解信号的概念与产生原理
- 一、从生活中的例子理解信号
-
- [1.1 用收快递理解信号](#1.1 用收快递理解信号)
- [1.2 信号到来,不代表立即处理](#1.2 信号到来,不代表立即处理)
- [1.3 收到信号以后可以怎么处理?](#1.3 收到信号以后可以怎么处理?)
- [1.4 信号具有异步性](#1.4 信号具有异步性)
- 二、从技术角度认识Linux信号
-
- [2.1 第一个信号实验:Ctrl+C](#2.1 第一个信号实验:Ctrl+C)
- [2.2 信号是什么?](#2.2 信号是什么?)
- 三、signal():改变信号的处理方式
-
- [3.1 signal函数原型](#3.1 signal函数原型)
- [3.2 自定义SIGINT处理函数](#3.2 自定义SIGINT处理函数)
- [3.3 信号处理由谁调用?](#3.3 信号处理由谁调用?)
- [3.4 为什么使用write而不是cout?](#3.4 为什么使用write而不是cout?)
- 四、Linux信号的三种处理动作
-
- [4.1 默认动作:SIG_DFL](#4.1 默认动作:SIG_DFL)
- [4.2 忽略信号:SIG_IGN](#4.2 忽略信号:SIG_IGN)
- [4.3 自定义捕捉](#4.3 自定义捕捉)
- [4.4 所有信号都能捕捉吗?](#4.4 所有信号都能捕捉吗?)
- 五、查看Linux支持的信号
-
- [5.1 kill -l](#5.1 kill -l)
- [5.2 使用man查看默认动作](#5.2 使用man查看默认动作)
- 六、信号产生的第一种方式:终端按键
-
- [6.1 Ctrl+C:SIGINT](#6.1 Ctrl+C:SIGINT)
- [6.2 Ctrl+\:SIGQUIT](#6.2 Ctrl+\:SIGQUIT)
- [6.3 Ctrl+Z:SIGTSTP](#6.3 Ctrl+Z:SIGTSTP)
- [6.4 前台进程与后台进程](#6.4 前台进程与后台进程)
- 七、为什么按下键盘能够产生信号?
-
- [7.1 从硬件中断开始理解](#7.1 从硬件中断开始理解)
- [7.2 硬件中断和信号有什么区别?](#7.2 硬件中断和信号有什么区别?)
- 八、信号产生的第二种方式:系统命令
-
- [8.1 kill命令](#8.1 kill命令)
- [8.2 实验:向后台进程发送信号](#8.2 实验:向后台进程发送信号)
- [8.3 为什么kill -SIGSEGV也能造成段错误式终止?](#8.3 为什么kill -SIGSEGV也能造成段错误式终止?)
- 九、信号产生的第三种方式:kill()、raise()和abort()
-
- [9.1 kill()函数](#9.1 kill()函数)
- [9.2 自己实现一个简单的mykill](#9.2 自己实现一个简单的mykill)
- [9.3 raise()函数](#9.3 raise()函数)
- [9.4 abort()函数](#9.4 abort()函数)
- 十、信号产生的第四种方式:软件条件
-
- [10.1 什么是软件条件?](#10.1 什么是软件条件?)
- [10.2 alarm()函数](#10.2 alarm()函数)
- [10.3 alarm()的返回值](#10.3 alarm()的返回值)
- [10.4 alarm(0)是什么意思?](#10.4 alarm(0)是什么意思?)
- 十一、alarm实验:体会IO对程序效率的影响
-
- [11.1 第一组:循环中不断输出](#11.1 第一组:循环中不断输出)
- [11.2 第二组:循环中不进行IO](#11.2 第二组:循环中不进行IO)
- [11.3 为什么有IO时效率较低?](#11.3 为什么有IO时效率较低?)
- 十二、实现重复闹钟与pause()
-
- [12.1 alarm默认只安排一次到期通知](#12.1 alarm默认只安排一次到期通知)
- [12.2 在处理函数中重新设置闹钟](#12.2 在处理函数中重新设置闹钟)
- [12.3 pause()是什么?](#12.3 pause()是什么?)
- [12.4 为什么alarm体现了操作系统的定时能力?](#12.4 为什么alarm体现了操作系统的定时能力?)
- 十三、信号产生的第五种方式:硬件异常
-
- [13.1 除零异常与SIGFPE](#13.1 除零异常与SIGFPE)
- [13.2 非法内存访问与SIGSEGV](#13.2 非法内存访问与SIGSEGV)
- [13.3 为什么是内核发送信号?](#13.3 为什么是内核发送信号?)
- [13.4 为什么捕捉SIGSEGV后可能反复收到信号?](#13.4 为什么捕捉SIGSEGV后可能反复收到信号?)
- [十四、Core Dump:进程为什么会生成core文件?](#十四、Core Dump:进程为什么会生成core文件?)
-
- [14.1 什么是Core Dump?](#14.1 什么是Core Dump?)
- [14.2 哪些信号可能产生Core Dump?](#14.2 哪些信号可能产生Core Dump?)
- [14.3 查看Core文件大小限制](#14.3 查看Core文件大小限制)
- [14.4 修改Core Dump限制](#14.4 修改Core Dump限制)
- [14.5 安全的实验思路](#14.5 安全的实验思路)
- [14.6 父进程怎样知道子进程是否发生Core Dump?](#14.6 父进程怎样知道子进程是否发生Core Dump?)
- 十五、总结:Linux信号究竟如何产生?
- 十六、课后思考
- 总结
前言:Linux为什么需要信号?
在前面的 Linux 学习中,我们已经掌握了:
- 进程创建:
fork() - 进程等待:
wait()、waitpid() - 程序替换:
execvp() - 进程间通信:匿名管道、命名管道、共享内存等
通过这些知识,我们已经可以创建进程、控制程序执行,并让多个进程进行数据交换。
但是,还有一个问题:
如果一个进程正在执行某项工作,操作系统或者其他进程希望通知它发生了某个事件,该怎么办?
例如,我们经常在终端执行:
bash
./myprocess
当程序陷入死循环时,可以按下:
text
Ctrl+C
结束程序。
但是:
- 为什么按下
Ctrl+C就能够终止进程? Ctrl+C与操作系统内核之间是什么关系?- 如果进程正在执行其他代码,是否必须立即处理这个事件?
- 能不能让进程收到
Ctrl+C后不退出? - 除了键盘,操作系统还有哪些产生信号的方式?
这些问题都属于 Linux 进程信号(Signal) 的范畴。
本篇按照课程的讲解顺序,从生活中的例子开始,依次介绍信号概念、信号处理动作、信号产生方式以及 Core Dump。
一、从生活中的例子理解信号
1.1 用收快递理解信号
假设我们在网上购买了几件商品,正在等待快递到达。
虽然快递还没有到,但我们已经知道:
text
快递到来以后,需要去领取
也就是说:
在事件真正发生之前,我们就已经知道应该如何处理这个事件。
这与 Linux 进程识别信号非常相似。
进程不需要等到某个信号真正产生后,才知道它是什么。
操作系统早已定义了合法的信号类型及其含义。
1.2 信号到来,不代表立即处理
继续假设:
快递员打电话通知我们:
text
快递已经送到楼下
但是此时我们正在完成某项重要工作,决定五分钟以后再去领取。
于是存在三个不同的阶段:
- 快递已经到达。
- 我们记住了有快递需要领取。
- 在合适的时间去领取并处理快递。
对应到 Linux 信号:
text
信号产生
|
v
信号保存
|
v
信号处理
这也是学习 Linux 信号时最重要的一条主线。
信号产生后不一定立即得到处理,但操作系统需要能够记录相关信号状态。
1.3 收到信号以后可以怎么处理?
还是快递的例子。
收到快递后,可以采用三种方式:
- 默认处理: 按照通常方式打开并使用商品。
- 自定义处理: 按照自己的特殊需求处理快递。
- 忽略: 不采取进一步操作。
Linux 信号也有类似的三种处理动作:
text
默认动作
忽略信号
自定义捕捉
1.4 信号具有异步性
快递什么时候到来,并不由我们正在执行的工作决定。
同样:
text
进程正在执行正常代码
与:
text
某个信号什么时候产生
通常也不存在固定的同步关系。
因此,信号是一种典型的异步事件通知机制。

二、从技术角度认识Linux信号
2.1 第一个信号实验:Ctrl+C
先编写一个一直运行的程序。
signal_test.cpp:
cpp
#include <iostream>
#include <unistd.h>
using namespace std;
int main()
{
while (true)
{
cout << "I am waiting for signal!" << endl;
sleep(1);
}
return 0;
}
编译:
bash
g++ signal_test.cpp -o signal_test
运行:
bash
./signal_test
程序会不断输出:
text
I am waiting for signal!
I am waiting for signal!
I am waiting for signal!
此时按下:
text
Ctrl+C
程序通常就会终止。
问题是:
我们的代码根本没有判断用户是否按下Ctrl+C,为什么程序却能退出?
因为终端驱动会将这一控制键组合解释为相应的信号事件,向前台进程组发送:
c
SIGINT
信号。
在常见的 Linux 平台上:
text
SIGINT = 2
该信号的默认处理动作是终止进程。
因此,程序即使没有主动检查键盘输入,也可能因为收到 SIGINT 而结束。
2.2 信号是什么?
可以给出一个初步定义:
信号是操作系统用于向进程或线程通知某些事件的一种机制,在经典 Unix/Linux 进程模型中,常被称为软件中断。
这里的"软件中断"主要用于说明它具有异步通知、打断正常执行流程的特点。
但它并不等同于 CPU 的硬件中断。
两者存在联系,也存在不同。
后面我们还会从硬件异常和操作系统中断机制的角度进一步分析。
三、signal():改变信号的处理方式
3.1 signal函数原型
c
#include <signal.h>
void (*signal(int signum, void (*handler)(int)))(int);
原型看起来比较复杂,因为这里涉及函数指针。
如果先定义一种函数指针类型,可以更容易理解:
c
typedef void (*sighandler_t)(int);
那么在常见的 GNU/Linux 接口中,可以将其理解为:
c
sighandler_t signal(
int signum,
sighandler_t handler
);
其中:
c
signum
表示要设置处理方式的信号编号。
c
handler
表示该信号对应的处理动作。
它可以是:
- 一个自定义处理函数。
SIG_DFL,使用默认处理动作。SIG_IGN,忽略信号。
需要特别注意:
调用signal()只是设置处理方式,不代表立即产生或者处理一个信号。
3.2 自定义SIGINT处理函数
下面修改之前的程序。
为了避免在异步信号处理函数中使用不适合的标准 IO 接口,我们采用 write() 输出固定消息。
cpp
#include <signal.h>
#include <unistd.h>
void Handler(int signo)
{
(void)signo;
const char msg[] = "catch SIGINT\n";
write(STDOUT_FILENO, msg, sizeof(msg) - 1);
}
int main()
{
signal(SIGINT, Handler);
while (true)
{
sleep(1);
}
return 0;
}
编译:
bash
g++ signal_test.cpp -o signal_test
运行后按下:
text
Ctrl+C
可以看到:
text
^Ccatch SIGINT
但是程序不再因为这一次信号而退出。
为什么?
因为我们执行了:
cpp
signal(SIGINT, Handler);
将 SIGINT 的处理动作从默认终止,改为了执行自定义函数。
处理函数执行结束以后,如果没有其他原因终止进程,程序就可以继续运行。
3.3 信号处理由谁调用?
这里非常值得注意。
我们并没有主动调用:
cpp
Handler(SIGINT);
但是当进程收到相应信号并进行递达时,操作系统可以安排执行该处理函数。
因此:
信号处理函数是一种回调函数。
用户负责注册处理函数,操作系统负责在满足相应条件时安排它执行。
3.4 为什么使用write而不是cout?
课堂演示中经常写:
cpp
cout << "catch signal" << endl;
但在异步信号处理函数中,普通 C++ IO 接口不保证具有异步信号安全性。
如果正常执行流程恰好也在操作同一个 IO 对象,信号处理函数再次进入相关库代码,就可能出现不可重入问题。
因此规范的信号处理函数应尽可能简单。
例如:
c
write(STDOUT_FILENO, "SIGINT\n", 7);
属于适合此类简单演示的方式。
关于可重入函数的问题,第二篇会详细讨论。

四、Linux信号的三种处理动作
4.1 默认动作:SIG_DFL
可以这样设置:
c
signal(SIGINT, SIG_DFL);
表示恢复 SIGINT 的默认处理动作。
对于 SIGINT,默认行为是终止进程。
因此按下:
text
Ctrl+C
程序就会退出。
但需要注意,不同信号的默认行为可能不同。
常见默认行为包括:
- 终止进程。
- 终止进程并可能产生 Core Dump。
- 忽略。
- 停止进程。
- 恢复已停止的进程。
4.2 忽略信号:SIG_IGN
例如:
c
signal(SIGINT, SIG_IGN);
表示忽略 SIGINT。
之后按下:
text
Ctrl+C
程序通常不会因此终止。
4.3 自定义捕捉
c
signal(SIGINT, Handler);
表示注册自定义处理函数。
当信号递达时执行该函数。
三种处理方式可以总结如下:
| 处理动作 | 写法 | 含义 |
|---|---|---|
| 默认处理 | SIG_DFL |
交给系统定义的默认动作 |
| 忽略 | SIG_IGN |
忽略对应信号 |
| 自定义捕捉 | 函数指针 | 运行用户注册的处理函数 |
4.4 所有信号都能捕捉吗?
不能。
例如:
c
SIGKILL
SIGSTOP
这两个信号不能被进程捕捉、忽略或者阻塞。
它们为系统提供了强制终止或停止进程的重要控制手段。
因此不能认为:
c
signal(SIGKILL, Handler);
可以成功改变 SIGKILL 的处理方式。
五、查看Linux支持的信号
5.1 kill -l
在终端执行:
bash
kill -l
可以看到系统支持的信号名称。
典型信号包括:
| 信号 | Linux常见编号 | 作用 |
|---|---|---|
SIGHUP |
1 | 终端挂起等事件 |
SIGINT |
2 | 终端中断 |
SIGQUIT |
3 | 终端退出请求 |
SIGILL |
4 | 非法指令 |
SIGTRAP |
5 | 跟踪或断点事件 |
SIGABRT |
6 | 异常中止 |
SIGFPE |
8 | 算术异常 |
SIGKILL |
9 | 强制终止 |
SIGSEGV |
11 | 非法内存访问 |
SIGPIPE |
13 | 向没有读端的管道写入 |
SIGALRM |
14 | 定时器到期 |
SIGTERM |
15 | 请求终止 |
SIGCHLD |
17 | 子进程状态发生变化 |
SIGCONT |
18 | 恢复停止的进程 |
SIGSTOP |
19 | 强制停止 |
SIGTSTP |
20 | 终端请求停止进程 |
以上编号主要针对常见 Linux 架构,不应将所有编号当作跨平台常量。
编写代码时优先使用:
c
SIGINT
这样的宏,而不是直接写:
c
2
5.2 使用man查看默认动作
执行:
bash
man 7 signal
可以看到信号的标准名称、默认处理动作以及相关说明。
课程中将信号主要分为:
text
普通信号
实时信号
本篇重点讨论普通信号。
实时信号在排队语义等方面存在不同,后续可以进一步研究。

六、信号产生的第一种方式:终端按键
6.1 Ctrl+C:SIGINT
已经验证过:
text
Ctrl+C -> SIGINT
默认终止前台进程组中的相关进程。
6.2 Ctrl+\:SIGQUIT
在终端中按下:
text
Ctrl+\
通常产生:
c
SIGQUIT
常见 Linux 编号:
text
3
默认动作通常是:
text
终止进程,并尝试产生Core Dump
可以编写循环程序后直接测试。
如果注册了自定义 SIGQUIT 处理函数,则可能不再执行默认终止动作。
6.3 Ctrl+Z:SIGTSTP
在终端中按:
text
Ctrl+Z
通常产生:
c
SIGTSTP
它的默认动作是停止对应的前台进程组。
注意:
停止进程不等于进程已经退出。
例如:
bash
./signal_test
按下 Ctrl+Z 后,Shell 可能显示:
text
[1]+ Stopped ./signal_test
执行:
bash
jobs
可以查看被停止的作业。
可以通过:
bash
fg
让对应作业恢复到前台。
也可以使用:
bash
bg
尝试让已停止的作业在后台恢复运行。
6.4 前台进程与后台进程
例如:
bash
./signal_test &
表示让程序在后台运行。
此时终端输入的 Ctrl+C 通常针对当前终端的前台进程组,而不是简单地发送给所有后台进程。
所以学习终端产生信号时,需要建立:
text
终端
|
v
前台进程组
|
v
相关进程
的概念。
这里涉及更完整的进程组、会话和作业控制机制,后续可以继续展开。
七、为什么按下键盘能够产生信号?
7.1 从硬件中断开始理解
课件专门分析了操作系统如何知道键盘有数据到来。
可以先建立一个简化流程:
- 用户按下键盘。
- 键盘硬件与相关控制器产生输入事件。
- 操作系统通过硬件中断等机制获知输入。
- 内核中的输入及终端子系统处理该事件。
- 如果事件符合终端控制字符规则,就可以产生相应信号。
例如:
text
Ctrl+C
可以被终端驱动解释为中断控制字符,从而向前台进程组产生 SIGINT。
7.2 硬件中断和信号有什么区别?
可以先区分它们面对的对象。
硬件中断
主要面向处理器和操作系统中断处理机制,用于通知硬件事件。
进程信号
主要用于向进程或线程表达某种需要处理的事件。
所以不能直接写成:
text
键盘硬件把SIGINT直接发送给用户进程
更合理的理解是:
text
硬件输入事件
|
v
内核输入处理
|
v
终端控制字符识别
|
v
产生对应进程信号
两者具有事件通知的相似性,但所处的软件与硬件层次不同。

八、信号产生的第二种方式:系统命令
8.1 kill命令
除了按键以外,我们还可以使用:
bash
kill
命令向指定进程发送信号。
例如:
bash
kill -SIGTERM 12345
表示向 PID 为 12345 的进程发送 SIGTERM。
这里的 PID 仅为示例,实际操作时必须先确认目标进程。
也可以使用:
bash
kill -15 12345
但推荐使用信号名称,便于阅读。
8.2 实验:向后台进程发送信号
首先编译一个循环程序:
cpp
#include <iostream>
#include <unistd.h>
int main()
{
std::cout << "pid = " << getpid() << std::endl;
while (true)
{
sleep(1);
}
}
编译:
bash
g++ signal_test.cpp -o signal_test
后台运行:
bash
./signal_test &
可以使用:
bash
jobs -l
查看作业及相关进程编号。
假设其 PID 为:
text
12345
就可以执行:
bash
kill -SIGTERM 12345
结束该实验进程。
注意:
kill命令的本质是发送信号,不一定代表强制杀死进程。
例如:
bash
kill -SIGSTOP 12345
用于停止进程。
bash
kill -SIGCONT 12345
用于使停止的进程继续执行。
8.3 为什么kill -SIGSEGV也能造成段错误式终止?
课件有一个实验:
向一个正常运行的进程发送:
bash
kill -SIGSEGV 12345
如果使用默认处理动作,程序会因 SIGSEGV 终止,并可能产生 Core Dump。
但这里必须区分:
- 真实内存访问异常: 进程执行了错误内存访问,引发硬件异常并最终形成相应信号。
- 手工发送SIGSEGV: 通过信号接口产生相同编号的信号。
两者都可能导致进程收到 SIGSEGV。
但是手工发送信号并不意味着程序真的执行了非法内存访问指令。
九、信号产生的第三种方式:kill()、raise()和abort()
9.1 kill()函数
函数原型:
c
#include <signal.h>
#include <sys/types.h>
int kill(pid_t pid, int sig);
其中:
c
pid
指定信号目标。
c
sig
指定信号类型。
对于:
c
pid > 0
通常表示向指定 PID 的进程发送信号。
其他 PID 参数形式还涉及进程组及权限判断,暂时不展开。
9.2 自己实现一个简单的mykill
mykill.cpp:
cpp
#include <cerrno>
#include <csignal>
#include <cstdlib>
#include <iostream>
using namespace std;
int main(int argc, char* argv[])
{
if (argc != 3)
{
cerr << "Usage: " << argv[0]
<< " signal pid" << endl;
return 1;
}
int signo = atoi(argv[1]);
pid_t pid = static_cast<pid_t>(atoi(argv[2]));
// 教学版仅允许指定正PID和有效信号编号
if (pid <= 0 || signo <= 0 || signo >= NSIG)
{
cerr << "invalid signal or pid" << endl;
return 1;
}
if (kill(pid, signo) < 0)
{
perror("kill");
return 1;
}
return 0;
}
编译:
bash
g++ mykill.cpp -o mykill
使用:
bash
./mykill 15 12345
即向对应 PID 发送 SIGTERM。
这里使用 atoi() 只是为了教学简化,严格的命令行工具还应使用 strtol() 检查非法字符及整数溢出等问题。
此外,NSIG 为常见 GNU/Linux 扩展宏,不能将其视为全部 POSIX 平台都有的通用接口。
9.3 raise()函数
函数原型:
c
#include <signal.h>
int raise(int sig);
作用是向调用者自身发送指定信号。
例如:
c
raise(SIGINT);
可以理解为:
text
当前执行流向自身产生SIGINT
对于简单单线程实验,可以把它理解成"自己给自己发信号"。
完整代码
c
#include <signal.h>
#include <unistd.h>
void Handler(int signo)
{
(void)signo;
const char msg[] = "received SIGUSR1\n";
write(STDOUT_FILENO, msg, sizeof(msg) - 1);
}
int main(void)
{
signal(SIGUSR1, Handler);
raise(SIGUSR1);
return 0;
}
运行后能够看到:
text
received SIGUSR1
9.4 abort()函数
函数原型:
c
#include <stdlib.h>
void abort(void);
它用于让当前进程异常终止,通常通过产生:
c
SIGABRT
实现。
在常见 Linux 平台上:
text
SIGABRT = 6
例如:
c
#include <stdlib.h>
int main(void)
{
abort();
return 0;
}
程序会异常终止,并可能生成 Core Dump。
一个特别值得注意的问题是:
即使用户注册了SIGABRT处理函数,只要处理函数正常返回,abort()仍会确保异常终止,而不会像普通函数一样正常返回。

十、信号产生的第四种方式:软件条件
10.1 什么是软件条件?
有些信号并不是直接来自键盘,也不是其他进程主动使用 kill() 发送的。
而是由于某个软件状态满足特定条件,由系统产生相应信号。
例如:
- 管道没有读端时继续写入,可能产生
SIGPIPE。 - 设定的定时器到期,产生
SIGALRM。
其中 SIGPIPE 已经在进程间通信的管道部分介绍过。
本节重点学习:
c
alarm()
10.2 alarm()函数
函数原型:
c
#include <unistd.h>
unsigned int alarm(unsigned int seconds);
它的作用是为当前进程设置一个定时器。
当指定的时间到达后,系统会产生:
c
SIGALRM
其默认处理动作通常是终止进程。
例如:
c
alarm(5);
表示设置一个大约 5 秒后到期的定时器。
注意,定时器到期与进程最终处理信号的时间,可能存在调度延迟。
10.3 alarm()的返回值
alarm() 的返回值表示之前尚未到期的闹钟大致还剩余多少秒。
如果此前没有设置闹钟:
text
返回0
如果已经设置了闹钟,再次调用 alarm() 会替换先前的设置。
例如:
c
alarm(30);
经过一段时间后:
c
unsigned int left = alarm(15);
这里新的闹钟被设置为 15 秒后到期。
left 则表示旧闹钟剩余的秒数。
10.4 alarm(0)是什么意思?
c
alarm(0);
用于取消当前尚未到期的闹钟。
其返回值仍然表示原先闹钟剩余的秒数。
十一、alarm实验:体会IO对程序效率的影响
课件设计了一个非常有代表性的实验:
在约一秒钟内进行大量计数,比较频繁输出与不频繁输出的差异。
11.1 第一组:循环中不断输出
cpp
#include <iostream>
#include <unistd.h>
using namespace std;
int main()
{
alarm(1);
unsigned long long count = 0;
while (true)
{
cout << count++ << endl;
}
return 0;
}
编译:
bash
g++ -O0 count_io.cpp -o count_io
运行:
bash
./count_io
大约一秒后,程序会被 SIGALRM 终止。
可以观察在终止前打印了多少次。
11.2 第二组:循环中不进行IO
为了避免在信号处理函数中打印,下面让处理函数只设置一个标志。
c
#include <signal.h>
#include <stdio.h>
#include <unistd.h>
volatile sig_atomic_t expired = 0;
void Handler(int signo)
{
(void)signo;
expired = 1;
}
int main(void)
{
signal(SIGALRM, Handler);
volatile unsigned long long count = 0;
alarm(1);
while (!expired)
{
++count;
}
printf("count = %llu\n", count);
return 0;
}
编译:
bash
gcc -O0 count_no_io.c -o count_no_io
运行:
bash
./count_no_io
观察计数结果。
通常情况下,第二组计数次数远多于第一组的逐次终端输出数量。
11.3 为什么有IO时效率较低?
因为:
cpp
cout << count << endl;
需要进行字符串转换、流输出等操作。
尤其是:
cpp
endl
还会刷新输出缓冲。
终端输出也涉及操作系统和终端设备的处理。
相比之下:
c
++count;
只进行简单计算,开销通常更低。
因此这个实验的主要结论是:
在高频循环中进行大量IO,可能显著限制程序的执行效率。
但两组代码的具体计数不能被当成严格的 CPU 性能基准,因为循环内容、编译优化、调度、终端环境等条件都不完全相同。

十二、实现重复闹钟与pause()
12.1 alarm默认只安排一次到期通知
例如:
c
alarm(1);
表示大约一秒后产生一次 SIGALRM。
如果希望每隔一秒都获得通知,就需要重新安排定时器。
12.2 在处理函数中重新设置闹钟
c
#include <signal.h>
#include <unistd.h>
void Handler(int signo)
{
(void)signo;
const char msg[] = "one second passed\n";
write(STDOUT_FILENO, msg, sizeof(msg) - 1);
// 再次设置下一次闹钟
alarm(1);
}
int main(void)
{
struct sigaction action = {0};
action.sa_handler = Handler;
sigemptyset(&action.sa_mask);
sigaction(SIGALRM, &action, NULL);
alarm(1);
while (1)
{
pause();
}
return 0;
}
运行后大约每秒输出一次:
text
one second passed
可以通过:
text
Ctrl+C
终止程序。
这里使用 sigaction() 注册处理函数,它会在下一篇中详细讲解。
12.3 pause()是什么?
函数原型:
c
#include <unistd.h>
int pause(void);
作用:
使调用线程暂停执行,直到某个信号触发了终止动作,或者执行了捕捉处理函数并返回。
如果捕捉到的信号处理函数返回,pause() 会返回:
text
-1
并设置:
c
errno = EINTR;
因此:
c
while (1)
{
pause();
}
可以让进程等待信号,而不是一直执行空循环消耗 CPU 时间。
但需要注意,简单地先检查某个标志,再调用 pause(),可能出现竞态条件。后面会在信号同步部分讨论。
12.4 为什么alarm体现了操作系统的定时能力?
因为操作系统需要管理大量定时事件。
例如:
text
进程A:3秒后产生通知
进程B:10秒后产生通知
进程C:1秒后产生通知
内核必须维护这些定时器的到期时间,并在适当时机执行对应的处理工作。
课件通过旧版 Linux 内核的 timer_list 等结构说明:
text
定时器也需要先描述,再组织
这一操作系统设计思想。
需要注意,现代 Linux 内核的定时器实现比早期版本复杂,不能直接把旧版结构体字段当成当前内核的固定实现。
十三、信号产生的第五种方式:硬件异常
前面介绍的信号,主要来自:
text
键盘
系统命令
函数调用
软件条件
但信号还可能来自程序执行过程中出现的硬件异常。
这也是课件中非常重要的部分。
13.1 除零异常与SIGFPE
例如:
c
int a = 10;
int b = 0;
int result = a / b;
在某些 Linux/x86 环境下,如果实际执行了整数除零指令,CPU 会产生相应的算术异常。
内核可以将其转换为:
c
SIGFPE
发送给相关执行流。
但必须特别说明:
在C/C++语言层面,整数除零属于未定义行为。
因此不应该假设所有编译器、所有优化级别都必然执行除零指令并产生同样的信号。
13.2 非法内存访问与SIGSEGV
例如:
c
int* p = NULL;
*p = 100;
这也是未定义行为。
在典型 Linux 环境中,如果程序实际执行了对无效地址的写访问,硬件内存管理机制可能产生访问异常。
内核随后可能向进程发送:
c
SIGSEGV
默认结果通常是终止进程并可能生成 Core Dump。
13.3 为什么是内核发送信号?
硬件异常首先进入 CPU 和内核的异常处理流程。
例如:
text
程序执行非法内存访问
|
v
CPU检测到异常
|
v
进入内核异常处理
|
v
内核判断异常原因
|
v
向相关进程产生SIGSEGV
因此:
硬件并不是直接执行用户编写的信号处理函数,而是先由内核处理异常,再通过进程信号机制安排相应动作。
13.4 为什么捕捉SIGSEGV后可能反复收到信号?
课件通过实验指出:
如果在 SIGSEGV 处理函数中只是打印一条消息,然后返回,却没有修复导致异常的程序状态,进程可能继续执行同一条错误指令。
于是再次发生异常。
例如:
text
执行非法访问指令
|
v
产生SIGSEGV
|
v
执行处理函数
|
v
处理函数返回
|
v
重新执行原错误指令
|
v
再次发生异常
这个现象很适合理解异常与信号的关系。
但它不代表我们应该在真实程序中反复捕捉 SIGSEGV 后继续运行。
对于由未定义行为造成的严重程序错误,普通信号处理函数通常不能安全地恢复程序执行。

十四、Core Dump:进程为什么会生成core文件?
14.1 什么是Core Dump?
当程序发生某些异常并终止时,操作系统可能按照当前配置生成包含进程相关内存状态、寄存器状态等信息的转储文件。
这通常称为:
text
Core Dump
它能够帮助开发者在程序退出以后分析错误原因。
例如:
text
程序因SIGSEGV异常终止
|
v
生成Core Dump
|
v
使用调试器检查异常现场
14.2 哪些信号可能产生Core Dump?
常见包括:
text
SIGQUIT
SIGILL
SIGABRT
SIGFPE
SIGSEGV
但要注意:
默认动作为Core并不意味着系统一定在当前目录生成一个名为core的文件。
还受到资源限制、系统配置、安全策略与 Core Dump 收集服务等因素影响。
14.3 查看Core文件大小限制
执行:
bash
ulimit -c
可以查看当前 Shell 环境中的 Core Dump 文件大小限制。
例如:
text
0
通常表示禁止生成传统 Core Dump 文件。
14.4 修改Core Dump限制
在实验终端中:
bash
ulimit -c unlimited
允许生成的 Core Dump 大小不再受这个资源限制约束。
但文件是否实际出现,还取决于系统其他配置。
可以查看:
bash
cat /proc/sys/kernel/core_pattern
了解内核配置的 Core Dump 输出模式。
14.5 安全的实验思路
可以写一个只用于测试异常终止的程序:
c
#include <signal.h>
int main(void)
{
raise(SIGABRT);
return 0;
}
编译时保留调试信息:
bash
gcc -g core_test.c -o core_test
运行:
bash
./core_test
程序将因 SIGABRT 异常终止。
如果系统配置允许并生成了 Core Dump,可以使用调试工具继续分析。
例如:
bash
gdb ./core_test core
这里的 core 只是示例文件名,实际文件名和保存位置应以系统配置为准。
注意:Core Dump可能包含程序运行时的敏感数据,不应随意公开或上传。
14.6 父进程怎样知道子进程是否发生Core Dump?
这可以结合我们之前学习的 waitpid()。
例如:
c
#include <stdio.h>
#include <signal.h>
#include <unistd.h>
#include <sys/wait.h>
int main(void)
{
pid_t pid = fork();
if (pid == 0)
{
raise(SIGABRT);
_exit(0);
}
int status = 0;
if (waitpid(pid, &status, 0) < 0)
{
perror("waitpid");
return 1;
}
if (WIFSIGNALED(status))
{
printf(
"signal = %d\n",
WTERMSIG(status)
);
#ifdef WCOREDUMP
printf(
"core dump flag = %d\n",
!!WCOREDUMP(status)
);
#endif
}
return 0;
}
这里:
c
WIFSIGNALED(status)
判断子进程是否由信号终止。
c
WTERMSIG(status)
提取终止信号编号。
部分平台还提供:
c
WCOREDUMP(status)
用于检查等待状态中的 Core Dump 标志。
注意它并不是所有 POSIX 平台都具备的标准宏,而且相关标志不能单独保证某个指定路径存在 Core 文件。

十五、总结:Linux信号究竟如何产生?
按照本篇的学习顺序,可以将信号产生方式归纳如下。
| 产生方式 | 典型来源 | 示例 |
|---|---|---|
| 终端控制字符 | 终端驱动 | Ctrl+C、Ctrl+\、Ctrl+Z |
| 系统命令 | Shell调用相关接口 | kill -SIGTERM PID |
| 函数调用 | 应用程序主动产生信号 | kill()、raise()、abort() |
| 软件条件 | 定时器、管道状态等 | SIGALRM、SIGPIPE |
| 硬件异常 | CPU异常及内核异常处理 | SIGFPE、SIGSEGV |
尽管信号来源不同,但最终都需要通过操作系统管理相关的进程信号状态。
因此,本篇最重要的理解是:
信号是操作系统管理进程的一种事件通知机制;信号产生与信号处理是两个不同阶段。
十六、课后思考
结合之前的进程控制与本篇知识,可以思考以下问题。
signal()是否会直接执行处理函数?Ctrl+C为什么通常影响终端前台进程组?- 忽略
SIGINT和阻塞SIGINT是同一个概念吗? - 如果信号到来时,进程暂时不能处理,操作系统怎样记录它?
SIGKILL为什么不能被普通进程忽略?alarm(1)为什么不会天然变成每秒一次的周期定时器?- 为什么捕捉
SIGSEGV后简单返回,不能保证程序恢复正常?
总结
本文根据课程的学习顺序,从生活中的收快递例子出发,逐步认识 Linux 信号。
首先,我们建立了:
text
信号产生
信号保存
信号处理
这一整体认识。
随后通过:
c
signal()
理解了信号的三种处理方式:
text
默认处理
忽略
自定义捕捉
接下来,我们依次学习了:
text
终端按键
kill命令
kill()、raise()、abort()
alarm()与软件条件
CPU硬件异常
等信号产生方式。
最后,通过 Core Dump 与 waitpid() 的结合,理解了进程因异常信号退出时,父进程如何获得相关终止状态。
不过,我们现在还没有回答一个最关键的问题:
如果信号已经产生,却暂时没有被处理,那么它究竟保存在哪里?
这就需要深入理解:
text
Pending
Block
sigset_t
sigprocmask
sigpending
以及信号真正递达和被捕捉的过程。