Linux——进程信号(1)(signal与sigaction)
进程信号初识
- 信号
- 信号概念类比理解
- 一个简单的信号案例
- 信号系统调用
- (1) signal() 系统函数
- (2) 示例代码解析
- (3) 信号处理的核心问题
- (4) 生活类比
- (5) 补充知识点
- (6) 总结
- 信号处理(默认与忽略)
- signal 函数
- 示例代码
- 默认与忽略宏定义解析
- 信号处理机制
- sigaction 函数(高级信号处理)
- 示例代码
- 总结
信号
信号概念类比理解
在Linux中,信号——Signal是系统或用户向进程发送的一种异步通知机制,用于通知进程发生了某种事件(如错误、中断或状态变化)。类比生活中的现象可以更直观地理解这一抽象概念:
(1)信号 ≈ 快递员的敲门(中断通知)
场景:你正在家专心工作,突然快递员敲门(信号到达)。
你需要决定:立即开门(处理信号)、忽略敲门(忽略信号),或者等忙完手头的事再开门(延迟处理)。
对应信号:SIGINT(Ctrl+C 终止进程)或 SIGUSR1(用户自定义信号)。(宏定义)
关键点:信号是异步的——你无法预知敲门何时发生。
(2)默认行为 ≈ 条件反射
场景:有人突然从背后拍你肩膀,你会本能地转身(默认行为)。但你可以训练自己保持不动(覆盖默认行为)。
对应信号:
-
SIGTERM:默认终止进程(像礼貌的“请离开”)。
-
SIGKILL:强制终止(像直接拔掉电源,无法忽略或捕获)。
关键点:进程可以捕获信号并自定义行为(如保存数据再退出)。
(3) 信号优先级 ≈ 紧急电话 vs 短信
场景:你同时接到电话(高优先级)和短信(低优先级)。电话会打断当前任务,而短信可以稍后处理。
对应信号:
-
SIGSEGV(段错误)会立即终止进程(像火警警报)。
-
SIGCHLD(子进程退出)可能被延迟处理(像垃圾邮件提醒)。
关键点:某些信号会抢占进程的当前执行。
(4)信号阻塞 ≈ 开启勿扰模式
场景:开会时开启手机勿扰模式,期间所有来电被静音(信号被阻塞),但结束后会收到未接通知(信号递送)。
对应操作:sigprocmask() 可阻塞信号,解除阻塞后信号才会生效。
关键点:阻塞≠忽略,信号只是被暂缓处理。
(5)信号丢失 ≈ 重复按电梯按钮
场景:电梯按钮被多次按下,但电梯只会响应一次(同类信号可能合并)。
对应问题:标准信号(如 SIGINT)可能丢失,实时信号(如 SIGRTMIN)会排队。
关键点:非实时信号是不可靠的(多次发送≈一次)。
(6)父子进程信号 ≈ 家长与孩子的约定
场景:家长(父进程)和孩子(子进程)约定:孩子崩溃时打电话通知(发送 SIGCHLD)。
对应机制:父进程通过 wait() 回收子进程资源,避免僵尸进程。
关键点:信号可用于进程间通信(简单场景)。
一个简单的信号案例
// sig.cc
#include <iostream>
#include <unistd.h>
int main()
{
while(true)
{
std::cout << "I am a process, I am waiting signal!" << std::endl;
sleep(1);
}
}
运行后发送信号终止:
$ g++ sig.cc -o sig
$ ./sig
I am a process, I am waiting signal!
I am a process, I am waiting signal!
^C
为什么?
-
在终端运行程序时,按下 Ctrl+C 会向进程发送 SIGINT(中断信号)。
-
SIGINT 的默认行为是终止进程(Terminate)。
-
因此,进程收到 SIGINT 后立即终止,程序退出。
信号系统调用
(1) signal() 系统函数
功能:设置特定信号的处理方式(捕获、忽略或恢复默认行为)。
头文件:#include <signal.h>
函数原型:
typedef void (*sighandler_t)(int); // 函数指针类型
sighandler_t signal(int signum, sighandler_t handler);
参数:
-
signum:信号编号(如 SIGINT 的编号是 2)。
-
handler:信号处理函数指针,收到信号时回调该函数。
示例:
void handler(int signum) {
std::cout << "Received signal: " << signum << std::endl;
}
signal(SIGINT, handler); // 捕获 SIGINT(Ctrl+C)
(2) 示例代码解析
代码:
复制
#include <iostream>
#include <unistd.h>
#include <signal.h>
void handler(int signum) {
std::cout << "PID: " << getpid() << ", Signal: " << signum << std::endl;
}
int main() {
std::cout << "PID: " << getpid() << std::endl;
signal(SIGINT, handler); // 捕获 SIGINT
while (true) {
std::cout << "Waiting for signal..." << std::endl;
sleep(1);
}
}
运行结果:
$ ./sig
PID: 12345
Waiting for signal...
Waiting for signal...
^CPID: 12345, Signal: 2 # 按下 Ctrl+C
Waiting for signal...
^CPID: 12345, Signal: 2 # 再次按下 Ctrl+C
关键点:
进程不退出:因为自定义的 handler 仅打印信息,未调用 exit(),进程继续执行。
信号异步性:Ctrl+C 可打断 sleep(1),说明信号是异步的。
(3) 信号处理的核心问题
a.为什么进程不退出?
signal() 仅修改信号的处理方式,不自动终止进程。
若需退出,应在 handler 中调用 exit()。
b. 信号处理是“自己处理”
信号处理函数(handler)是用户自定义的,由进程自身执行。
类比:快递员(操作系统)敲门(发送信号),你(进程)决定是否开门或忽略。
c. 前台进程与后台进程
前台进程:直接接收终端信号(如 Ctrl+C 的 SIGINT)。
后台进程(命令后加 &):不接收终端信号,需通过 kill 发送信号。
$ ./sig & # 后台运行
$ kill -SIGINT 12345 # 手动发送 SIGINT
d.信号与进程关系
-
异步性:信号可能在任何时间点打断进程。
-
可靠性:标准信号(如 SIGINT)可能丢失,实时信号(SIGRTMIN~SIGRTMAX)可排队。
(4) 生活类比
场景 Linux 信号机制
快递员敲门(电话通知)| 操作系统发送信号(如 SIGINT)
你决定是否开门 | 进程捕获信号并执行 handler
直接拒收快递 | 忽略信号(signal(signum, SIG_IGN))
强制拆除房门(消防员)| SIGKILL(不可捕获/忽略)
(5) 补充知识点
信号默认行为:
-
SIGINT(Ctrl+C):终止进程。
-
SIGQUIT(Ctrl+\):终止并生成核心转储。
-
SIGTERM:优雅终止(可捕获)。
-
SIGKILL:强制终止(不可捕获)。
信号阻塞与未决信号:
使用 sigprocmask() 阻塞信号,信号会被标记为未决(pending),直到解除阻塞。
类比:开启勿扰模式,未接来电稍后处理。
nohup 命令:
使进程忽略 SIGHUP(终端关闭时发送的信号),常用于后台长期运行:
$ nohup ./sig &
信号安全函数:
在 handler 中只能调用异步信号安全函数(如 write()),避免使用 cout/printf(非线程安全)。
(6) 总结
信号本质:异步事件通知机制,由操作系统发送,进程决定处理方式。
核心函数:signal() 或更健壮的 sigaction()。
关键特性:
-
异步性(可能打断任何代码)。
-
用户自定义处理(捕获/忽略)。
-
前台/后台进程差异。
-
应用场景:进程优雅退出、定时任务(SIGALRM)、父子进程通信(SIGCHLD)。
附:常用信号列表
信号 编号 默认行为 触发方式
SIGINT 2 终止 Ctrl+C
SIGQUIT 3 终止+核心 Ctrl+\
SIGTERM 15 终止 kill
SIGKILL 9 强制终止 kill -9
SIGSTOP 19 暂停进程 Ctrl+Z
信号宏定义:
信号处理(默认与忽略)
signal 函数
signal 是用于设置信号处理方式的标准库函数,其原型如下:
#include <signal.h>
typedef void (*__sighandler_t)(int);
__sighandler_t signal(int signum, __sighandler_t handler);
参数:
- SIG_IGN:忽略信号。
- SIG_DFL:执行默认动作。
- 自定义函数:用户定义的信号处理函数。
返回值:
- 成功时返回之前的信号处理函数。
- 失败时返回 SIG_ERR。
示例代码
忽略信号
#include <iostream>
#include <unistd.h>
#include <signal.h>
int main() {
std::cout << "我是进程: " << getpid() << std::endl;
signal(SIGINT, SIG_IGN); // 忽略 SIGINT 信号
while (true) {
std::cout << "I am a process, I am waiting signal!" << std::endl;
sleep(1);
}
}
运行效果:
输入 Ctrl+C(发送 SIGINT 信号)时,程序无反应,继续运行。
执行默认动作
#include <iostream>
#include <unistd.h>
#include <signal.h>
int main() {
std::cout << "我是进程: " << getpid() << std::endl;
signal(SIGINT, SIG_DFL); // 执行默认动作
while (true) {
std::cout << "I am a process, I am waiting signal!" << std::endl;
sleep(1);
}
}
运行效果:
输入 Ctrl+C 时,程序退出(默认动作是终止进程)。
默认与忽略宏定义解析
在信号处理中,SIG_DFL 和 SIG_IGN 是两个重要的宏:
#define SIG_DFL ((__sighandler_t) 0) /* 默认动作 */
#define SIG_IGN ((__sighandler_t) 1) /* 忽略信号 */
SIG_DFL:将信号处理函数设置为 0,表示执行默认动作。
SIG_IGN:将信号处理函数设置为 1,表示忽略信号。
这两个宏实际上是将整数 0 和 1 强制转换为函数指针类型 __sighandler_t。
信号处理机制
(1) 信号处理流程
a.内核检测到信号(如用户输入 Ctrl+C)。
b.内核根据进程的信号处理方式执行相应动作:
- 忽略信号:直接丢弃。
- 默认动作:执行系统预定义的行为(如终止进程)。
- 自定义捕捉:切换到用户态,调用用户定义的信号处理函数。
(2) 注意事项
a.信号处理函数应尽可能简单,避免调用不可重入函数(如 printf、malloc 等)。
b.信号处理函数执行期间,系统会阻塞其他同类型信号(防止重入)。
sigaction 函数(高级信号处理)
signal 函数功能有限,推荐使用 sigaction 函数进行更精细的信号处理。
sigaction 函数原型
#include <signal.h>
int sigaction(int signum, const struct sigaction *act, struct sigaction *oldact);
参数:
- signum:信号编号。
- act:指向 sigaction 结构体的指针,定义新处理方式。
- oldact:指向 sigaction 结构体的指针,保存旧处理方式(可为 NULL)。
sigaction 结构体:
struct sigaction {
void (*sa_handler)(int); // 信号处理函数或 SIG_IGN/SIG_DFL
void (*sa_sigaction)(int, siginfo_t *, void *); // 替代信号处理函数(可选)
sigset_t sa_mask; // 信号处理期间阻塞的信号集
int sa_flags; // 选项标志(如 SA_RESTART)
};
示例代码
#include <iostream>
#include <unistd.h>
#include <signal.h>
void handler(int signumber) {
std::cout << "捕获信号: " << signumber << std::endl;
}
int main() {
struct sigaction act;
act.sa_handler = handler;
sigemptyset(&act.sa_mask);
act.sa_flags = 0;
sigaction(SIGINT, &act, nullptr);
while (true) {
std::cout << "等待信号..." << std::endl;
sleep(1);
}
}
补充注释:
struct sigaction act:
- 定义一个 sigaction 结构体变量,用于描述信号处理方式。
act.sa_handler = handler:
- 将 sa_handler 成员设置为自定义的信号处理函数 handler。
- 当 SIGINT 信号被捕获时,会调用 handler 函数。
sigemptyset(&act.sa_mask):
- 初始化 sa_mask 成员为一个空集。
- sa_mask 指定在信号处理函数执行期间需要阻塞的信号集。
- 空集表示不阻塞任何信号。
act.sa_flags = 0:
- 设置 sa_flags 成员为 0,表示不使用任何特殊选项。
- 常见的选项包括 SA_RESTART(自动重启被信号中断的系统调用)。
总结
信号处理方式:
- 忽略信号(SIG_IGN)。
- 执行默认动作(SIG_DFL)。
- 自定义捕捉信号(信号处理函数)。
- signal 函数:简单易用,但功能有限。
- sigaction 函数:功能强大,推荐用于复杂场景。
注意事项:
- 信号处理函数应简单、快速。
- 避免调用不可重入函数。(
不可重入函数是指那些在执行过程中如果再次被调用,可能会导致数据不一致、程序崩溃或未定义行为的函数。这些函数通常依赖于全局状态、静态变量或不可中断的资源。
)