引言在 Linux 系统编程中信号是一种重要的进程间通信机制。它本质上是软件中断用于通知进程某个事件已经发生。当进程收到信号时可以采取默认处理、忽略信号或执行自定义处理函数。信号通常与异常事件相关例如非法内存访问段错误除零操作浮点异常子进程终止SIGCHLD用户中断CtrlC 发送 SIGINT理解信号的处理机制是编写健壮系统程序的基础。今天我将从信号的基本概念出发全面讲解信号的发送、响应方式、SIGCHLD 信号的用途以及如何通过信号解决僵尸进程问题。第一部分信号的基本概念一、什么是信号信号是 Linux 系统中的软件中断机制用于通知进程某个事件已经发生。它类似于硬件中断但由软件产生。二、信号的编号与名称每个信号都有唯一的整数值和对应的宏名称。信号名称编号默认行为触发场景SIGINT2终止进程CtrlC 终端中断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)重要说明SIGKILL9号和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信号编号如SIGINT、SIGTERMhandler处理方式SIG_DFL默认、SIG_IGN忽略、函数指针返回值之前设置的信号处理函数指针三、信号处理函数的要求// 信号处理函数必须符合这个签名 void handler(int sig) { // 信号处理代码 // 注意只能调用异步信号安全async-signal-safe的函数 }信号处理函数的限制只能调用可重入函数如write不能调用printf不能调用非异步信号安全的函数如malloc、printf应尽量简短避免复杂逻辑第三部分信号应用示例一、信号响应方式演示#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; }效果CtrlC 无法终止程序因为 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 信号介绍SIGCHLD17号信号是内核在子进程终止时自动发送给父进程的信号。它的默认处理方式是忽略。二、使用 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直接函数调用举例open、read、write、fork、execvefopen、fread、printf、system二、exec 函数族exec函数族用于替换当前进程的代码段执行新程序。函数说明是否为系统调用execl参数列表形式库函数execv参数数组形式库函数execlp搜索 PATH库函数execvp搜索 PATH 参数数组库函数execve完整参数 环境变量系统调用关系图第六部分综合示例——自定义 Shellmybash一、功能需求实现一个简单的 Shell支持执行系统命令如ls、ps支持带参数的命令如ls -l、cp 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 -lmybash$ ps -fmybash$ cp test.c main.cmybash$ exit总结一、信号核心要点概念说明信号本质软件中断用于进程间通信SIGKILL9不可捕获、不可忽略必须终止进程SIGCHLD17子进程终止时发送给父进程三种响应默认、忽略、自定义信号处理函数必须使用异步信号安全函数二、解决僵尸进程的两种方法方法实现能否获取退出状态信号处理 waitsignal(SIGCHLD, handler)中调用wait()✅ 可以忽略 SIGCHLDsignal(SIGCHLD, SIG_IGN)❌ 不能三、exec 函数族总结函数PATH 搜索参数形式是否为系统调用execl❌列表库函数execv❌数组库函数execlp✅列表库函数execvp✅数组库函数execve❌数组 环境变量系统调用信号是 Linux 系统编程中重要的异步通信机制。理解信号的响应方式、信号处理函数的限制以及如何利用 SIGCHLD 解决僵尸进程问题是编写健壮系统程序的基础。学习建议记住几种常用信号的编号和含义SIGINT2、SIGKILL9、SIGTERM15、SIGCHLD17理解 SIGKILL 和 SIGSTOP 不可被捕获的特殊性掌握 signal 函数的使用和信号处理函数的限制区分系统调用与库函数理解 exec 函数族的层次关系