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 函数族的层次关系

相关推荐
皆圥忈10 分钟前
Linux文件系统与缓冲区深度解析
linux
壹号用户25 分钟前
初识linux
linux·运维·服务器
衫水31 分钟前
Windows Server Nginx 代理企业内网 API 偶发超时处理与保活 SOP(20260608))
运维·windows·nginx
Java 码思客35 分钟前
【Redis分布式缓存实战】第20章 Redis监控运维与自动化体系
运维·redis·缓存
梦想的颜色37 分钟前
硬核|Docker从入门到精通:镜像构建、仓库推送、Compose编排、生产部署全攻略
运维·服务器·docker·容器·部署·环境·镜像
团象科技37 分钟前
中小出海企业站点运维实践 关于WP建站海外主机的行业观察
运维·人工智能
凡人叶枫1 小时前
Effective C++ 条款02:宁可以编译器替换预处理器
java·linux·c语言·开发语言·c++
爱看老照片1 小时前
linux上查看磁盘空间占用情况,清理大文件
linux·清理·大文件·磁盘空间
你是个什么橙1 小时前
Linux 远程桌面访问和管理——VNC服务器
linux·运维·服务器
nhfc991 小时前
whisper.cpp编译
linux·运维·服务器