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

引言

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

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

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

  • 除零操作(浮点异常)

  • 子进程终止(SIGCHLD)

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

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


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

一、什么是信号?

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

二、信号的编号与名称

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

信号名称 编号 默认行为 触发场景
SIGINT 2 终止进程 Ctrl+C 终端中断
SIGQUIT 3 终止进程+生成core文件 Ctrl+\
SIGKILL 9 强制终止 kill -9 PID(不可捕获)
SIGSEGV 11 终止进程+生成core文件 非法内存访问
SIGPIPE 13 终止进程 向关闭的管道写入
SIGTERM 15 终止进程 kill PID(可捕获)
SIGCHLD 17 忽略 子进程终止
SIGFPE 8 终止进程+生成core文件 浮点异常(除零)

三、信号的三种响应方式

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

重要说明:

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

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


第二部分:signal 函数详解

一、函数原型

cpp 复制代码
#include <signal.h>

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

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

cpp 复制代码
typedef void (*sighandler_t)(int);
sighandler_t signal(int signum, sighandler_t handler);

二、参数说明

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

三、信号处理函数的要求

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

信号处理函数的限制:

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

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

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


第三部分:信号应用示例

一、信号响应方式演示

cpp 复制代码
#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 被忽略了。

二、自定义信号处理函数

cpp 复制代码
#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;
}

运行效果:

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

cpp 复制代码
#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 函数

cpp 复制代码
#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 解决僵尸进程

问题代码(父进程不回收子进程)
cpp 复制代码
#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
cpp 复制代码
#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 信号
cpp 复制代码
#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) 直接函数调用
举例 openreadwriteforkexecve fopenfreadprintfsystem

二、exec 函数族

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

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

关系图:

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

一、功能需求

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

  • 执行系统命令(如 lsps

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

  • 内置命令 exit 退出

二、代码实现

cpp 复制代码
#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) 子进程终止时发送给父进程
三种响应 默认、忽略、自定义
信号处理函数 必须使用异步信号安全函数

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

方法 实现 能否获取退出状态
信号处理 + wait signal(SIGCHLD, handler) 中调用 wait() ✅ 可以
忽略 SIGCHLD signal(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 函数族的层次关系

相关推荐
姚青&2 小时前
Linux 文件处理命令
linux·运维·服务器
tryqaaa_2 小时前
学习日志(二)【linux全部命令,http请求头{有例题},Php语法学习】
linux·学习·http·php·web
云达闲人2 小时前
搭建DevOps企业级仿真实验环境:003Proxmox 系统优化与国内源配置
运维·devops·服务器搭建·实验环境搭建·apt源配置·虚拟化运维·实验指南
LSL666_2 小时前
3 安装docker
运维·docker·容器
云达闲人2 小时前
搭建DevOps企业级仿真实验环境:002Proxmox 系统安装流程详解
运维·虚拟化·devops·kvm·proxmox·实验环境搭建·web管理
万法若空3 小时前
ANSI转义码详解
linux·c++
精益数智工坊3 小时前
红牌作战是什么?红牌作战的实施步骤与核心要点
大数据·运维·前端·人工智能·精益工程
计算机安禾3 小时前
【Linux从入门到精通】第21篇:Shell脚本开篇——什么是Shell?写第一个Hello World
linux·运维·服务器
Lumos_7773 小时前
Linux -- 系统调用
linux·运维·算法