news 2026/4/26 23:13:11

Linux 信号处理与进程控制深度解析

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Linux 信号处理与进程控制深度解析

引言

在 Linux 系统编程中,信号是一种重要的进程间通信机制。它本质上是软件中断,用于通知进程某个事件已经发生。当进程收到信号时,可以采取默认处理、忽略信号或执行自定义处理函数。

信号通常与异常事件相关,例如:

  • 非法内存访问(段错误)

  • 除零操作(浮点异常)

  • 子进程终止(SIGCHLD)

  • 用户中断(Ctrl+C 发送 SIGINT)

理解信号的处理机制,是编写健壮系统程序的基础。今天,我将从信号的基本概念出发,全面讲解信号的发送、响应方式、SIGCHLD 信号的用途,以及如何通过信号解决僵尸进程问题。


第一部分:信号的基本概念

一、什么是信号?

信号是 Linux 系统中的软件中断机制,用于通知进程某个事件已经发生。它类似于硬件中断,但由软件产生。

二、信号的编号与名称

每个信号都有唯一的整数值和对应的宏名称。

信号名称编号默认行为触发场景
SIGINT2终止进程Ctrl+C 终端中断
SIGQUIT3终止进程+生成core文件Ctrl+\
SIGKILL9强制终止kill -9 PID(不可捕获)
SIGSEGV11终止进程+生成core文件非法内存访问
SIGPIPE13终止进程向关闭的管道写入
SIGTERM15终止进程kill PID(可捕获)
SIGCHLD17忽略子进程终止
SIGFPE8终止进程+生成core文件浮点异常(除零)

三、信号的三种响应方式

方式说明设置方法
默认处理按系统预定义方式处理(通常是终止进程)signal(sig, SIG_DFL)
忽略信号收到信号后不做任何响应signal(sig, SIG_IGN)
自定义处理执行用户编写的信号处理函数signal(sig, handler)

重要说明:

  • SIGKILL(9号)和SIGSTOP不能被捕获、忽略或自定义

  • 这是系统管理员强制终止进程的最后手段


第二部分:signal 函数详解

一、函数原型

#include <signal.h> void (*signal(int signum, void (*handler)(int)))(int);

这个函数原型较复杂,可以用typedef简化理解:

typedef void (*sighandler_t)(int); sighandler_t signal(int signum, sighandler_t handler);

二、参数说明

参数说明
signum信号编号(如SIGINTSIGTERM
handler处理方式:SIG_DFL(默认)、SIG_IGN(忽略)、函数指针
返回值之前设置的信号处理函数指针

三、信号处理函数的要求

// 信号处理函数必须符合这个签名 void handler(int sig) { // 信号处理代码 // 注意:只能调用异步信号安全(async-signal-safe)的函数 }

信号处理函数的限制:

  • 只能调用可重入函数(如write,不能调用printf

  • 不能调用非异步信号安全的函数(如mallocprintf

  • 应尽量简短,避免复杂逻辑


第三部分:信号应用示例

一、信号响应方式演示

#include <stdio.h> #include <unistd.h> #include <signal.h> int main() { // 设置 SIGINT 为忽略 signal(SIGINT, SIG_IGN); while (1) { printf("hello\n"); sleep(1); } return 0; }

效果:Ctrl+C 无法终止程序,因为 SIGINT 被忽略了。

二、自定义信号处理函数

#include <stdio.h> #include <unistd.h> #include <signal.h> void sig_handler(int sig) { printf("收到信号: %d\n", sig); } int main() { // 注册信号处理函数 signal(SIGINT, sig_handler); while (1) { printf("程序运行中... PID=%d\n", getpid()); sleep(1); } return 0; }

运行效果:

三、动态修改信号响应方式

#include <stdio.h> #include <unistd.h> #include <signal.h> void sig_handler(int sig) { printf("收到 SIGINT,下次将恢复默认行为\n"); signal(SIGINT, SIG_DFL); // 恢复默认处理 } int main() { signal(SIGINT, sig_handler); while (1) { printf("程序运行中... PID=%d\n", getpid()); sleep(1); } return 0; }

运行效果:

四、发送信号:kill 函数

#include <stdio.h> #include <stdlib.h> #include <signal.h> #include <sys/types.h> int main(int argc, char* argv[]) { if (argc != 3) { printf("用法: %s <PID> <信号编号>\n", argv[0]); exit(1); } pid_t pid = atoi(argv[1]); int sig = atoi(argv[2]); if (kill(pid, sig) == -1) { perror("kill 失败"); exit(1); } printf("已向进程 %d 发送信号 %d\n", pid, sig); return 0; }

在另一个终端测试:

# 编译并运行发送信号的程序
./send_signal 1234 2 # 向 PID 1234 发送 SIGIN

第四部分:SIGCHLD 信号与僵尸进程

一、SIGCHLD 信号介绍

SIGCHLD(17号信号)是内核在子进程终止时自动发送给父进程的信号。它的默认处理方式是忽略。

二、使用 SIGCHLD 解决僵尸进程

问题代码(父进程不回收子进程)
#include <stdio.h> #include <unistd.h> #include <stdlib.h> int main() { pid_t pid = fork(); if (pid == 0) { // 子进程:运行3秒后退出 for (int i = 0; i < 3; i++) { printf("子进程: %d\n", i); sleep(1); } exit(0); } else { // 父进程:运行7秒,但不调用 wait for (int i = 0; i < 7; i++) { printf("父进程: %d\n", i); sleep(1); } } return 0; }

问题:子进程结束后成为僵尸进程,直到父进程结束才被回收。

解决方案1:在信号处理函数中调用 wait
#include <stdio.h> #include <unistd.h> #include <stdlib.h> #include <signal.h> #include <sys/wait.h> void sigchld_handler(int sig) { printf("收到 SIGCHLD,回收子进程\n"); wait(NULL); // 回收子进程资源 } int main() { // 注册 SIGCHLD 信号处理函数 signal(SIGCHLD, sigchld_handler); pid_t pid = fork(); if (pid == 0) { // 子进程 for (int i = 0; i < 3; i++) { printf("子进程: %d\n", i); sleep(1); } exit(0); } else { // 父进程 for (int i = 0; i < 7; i++) { printf("父进程: %d\n", i); sleep(1); } } return 0; }
解决方案2:忽略 SIGCHLD 信号
#include <stdio.h> #include <unistd.h> #include <stdlib.h> #include <signal.h> int main() { // 忽略 SIGCHLD 信号,内核自动回收子进程资源 signal(SIGCHLD, SIG_IGN); pid_t pid = fork(); if (pid == 0) { for (int i = 0; i < 3; i++) { printf("子进程: %d\n", i); sleep(1); } exit(0); } else { for (int i = 0; i < 7; i++) { printf("父进程: %d\n", i); sleep(1); } } return 0; }

注意:忽略SIGCHLD后,父进程无法获取子进程的退出状态。

三、解决僵尸进程的两种方法对比

方法原理优点缺点
信号处理 + wait子进程结束时父进程收到信号,调用 wait 回收父进程不阻塞,可获取退出状态代码稍复杂
忽略 SIGCHLD内核自动回收子进程资源代码简单无法获取子进程退出状态

第五部分:系统调用与库函数的区别

一、核心区别

特性系统调用库函数
执行空间内核态用户态
切换开销需要陷入内核(开销大)无切换(开销小)
调用方式通过软中断(int 0x80 或 syscall)直接函数调用
举例openreadwriteforkexecvefopenfreadprintfsystem

二、exec 函数族

exec函数族用于替换当前进程的代码段,执行新程序。

函数说明是否为系统调用
execl参数列表形式库函数
execv参数数组形式库函数
execlp搜索 PATH库函数
execvp搜索 PATH + 参数数组库函数
execve完整参数 + 环境变量系统调用

关系图:

第六部分:综合示例——自定义 Shell(mybash)

一、功能需求

实现一个简单的 Shell,支持:

  • 执行系统命令(如lsps

  • 支持带参数的命令(如ls -lcp a.c b.c

  • 内置命令exit退出

二、代码实现

#include <stdio.h> #include <stdlib.h> #include <string.h> #include <unistd.h> #include <sys/wait.h> #define MAX_CMD_LEN 1024 #define MAX_ARG_COUNT 128 // 分割命令字符串,提取命令和参数 char* get_cmd(char* buffer, char* argv[]) { if (buffer == NULL || argv == NULL) { return NULL; } int idx = 0; char* token = strtok(buffer, " "); while (token != NULL && idx < MAX_ARG_COUNT - 1) { argv[idx++] = token; token = strtok(NULL, " "); } argv[idx] = NULL; // execvp 需要以 NULL 结尾 return argv[0]; // 返回命令名称 } int main() { char buffer[MAX_CMD_LEN]; char* argv[MAX_ARG_COUNT]; while (1) { // 显示提示符 printf("mybash$ "); fflush(stdout); // 读取用户输入 if (fgets(buffer, sizeof(buffer), stdin) == NULL) { break; } // 去除末尾换行符 buffer[strlen(buffer) - 1] = '\0'; // 处理 exit 命令 if (strcmp(buffer, "exit") == 0) { printf("退出 mybash\n"); break; } // 分割命令 char* cmd = get_cmd(buffer, argv); if (cmd == NULL) { continue; } // 创建子进程执行命令 pid_t pid = fork(); if (pid == -1) { perror("fork 失败"); continue; } if (pid == 0) { // 子进程:执行命令 execvp(cmd, argv); // 如果执行到这里说明 execvp 失败 perror("execvp 失败"); exit(1); } else { // 父进程:等待子进程结束 int status; wait(&status); } } return 0; }

三、编译与运行

# 编译
gcc -o mybash mybash.c

# 运行
./mybash

# 测试命令
mybash$ ls -l
mybash$ ps -f
mybash$ cp test.c main.c
mybash$ exit

总结

一、信号核心要点

概念说明
信号本质软件中断,用于进程间通信
SIGKILL(9)不可捕获、不可忽略,必须终止进程
SIGCHLD(17)子进程终止时发送给父进程
三种响应默认、忽略、自定义
信号处理函数必须使用异步信号安全函数

二、解决僵尸进程的两种方法

方法实现能否获取退出状态
信号处理 + waitsignal(SIGCHLD, handler)中调用wait()✅ 可以
忽略 SIGCHLDsignal(SIGCHLD, SIG_IGN)❌ 不能

三、exec 函数族总结

函数PATH 搜索参数形式是否为系统调用
execl列表库函数
execv数组库函数
execlp列表库函数
execvp数组库函数
execve数组 + 环境变量系统调用

信号是 Linux 系统编程中重要的异步通信机制。理解信号的响应方式、信号处理函数的限制,以及如何利用 SIGCHLD 解决僵尸进程问题,是编写健壮系统程序的基础。

学习建议:

  1. 记住几种常用信号的编号和含义(SIGINT=2、SIGKILL=9、SIGTERM=15、SIGCHLD=17)

  2. 理解 SIGKILL 和 SIGSTOP 不可被捕获的特殊性

  3. 掌握 signal 函数的使用和信号处理函数的限制

  4. 区分系统调用与库函数,理解 exec 函数族的层次关系

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

CompressO视频压缩工具:3分钟掌握免费开源的多媒体压缩神器

CompressO视频压缩工具&#xff1a;3分钟掌握免费开源的多媒体压缩神器 【免费下载链接】compressO Convert any video/image into a tiny size. 100% free & open-source. Available for Mac, Windows & Linux. 项目地址: https://gitcode.com/gh_mirrors/co/compre…

作者头像 李华
网站建设 2026/4/26 23:09:00

LocalClaw + DeepSeek V4:本地部署百万 token 上下文实战

LocalClaw DeepSeek V4&#xff1a;本地部署百万 token 上下文实战 2026年4月24日&#xff0c;DeepSeek V4 系列正式发布&#xff0c;其中 V4-Flash 拥有 285B 参数、128K tokens 上下文窗口&#xff0c;V4-Pro 则达到 1.6T 参数规模。更重要的是——LocalClaw 已完成 DeepSee…

作者头像 李华
网站建设 2026/4/26 23:07:04

决策树模型中的有序编码优化技巧

1. 决策树与有序编码实战指南在机器学习项目中&#xff0c;我们经常遇到包含有序分类特征的数据集。上周处理信用卡风控数据时&#xff0c;我发现直接将"用户收入等级"&#xff08;低/中/高&#xff09;这样的有序变量简单Label Encoding会导致决策树模型效果下降15%…

作者头像 李华
网站建设 2026/4/26 23:02:31

VS Code 远程容器开发效率跃迁指南(2024企业级调优白皮书)

更多请点击&#xff1a; https://intelliparadigm.com 第一章&#xff1a;VS Code 远程容器开发效率跃迁的核心价值与演进脉络 VS Code 的 Remote-Containers 扩展彻底重构了现代云原生开发的工作流范式&#xff0c;将开发环境从本地机器解耦至标准化的 Docker 容器中&#xf…

作者头像 李华