<摘要>
本文将围绕Linux中的SIGTERM信号展开深度解析。首先明确SIGTERM并非函数,而是一种用于请求进程正常终止的信号,如同生活中"温柔的下班提醒"。文中会介绍其基本概念与用途,说明它在POSIX标准中的定位及相关头文件;详解发送SIGTERM的函数(如kill())的参数、返回值;通过三个典型案例(基础信号捕获、服务管理工具应用、多进程资源清理)展示其实际应用,每个案例包含完整代码、流程图、编译及运行说明;最后用图示总结SIGTERM的工作流程,帮助读者全面理解这一重要信号的机制与实践价值。


<正文>
嘿,朋友!今天咱们要聊的不是一个普通的函数,而是Linux世界里一个特别重要的"信使"——SIGTERM。你可能会问,信号和函数有啥关系?别急,虽然SIGTERM本身不是函数,但它的发送、捕获和处理都离不开函数的帮忙。咱们就从这个"温柔的终止信号"说起,一点点揭开它的神秘面纱。

一、SIGTERM:进程世界里的"温馨提示"

想象一下,你在一家24小时便利店打工,到了凌晨5点,店长走过来说:"小王啊,今天差不多了,整理下货架,把收银机对账清楚,然后就可以下班啦。"你听到后,不会立刻扔下手里的活儿就走,而是会按部就班地完成收尾工作——这就是SIGTERM在进程世界里的作用。

在Linux系统中,进程就像一个个在工作的员工,有的负责处理网络请求,有的负责计算数据,有的负责管理文件。当我们需要让一个进程停止运行时,有两种常见方式:一种是"粗暴解雇"(比如SIGKILL信号),不管进程正在做什么,立刻强制终止;另一种就是SIGTERM这种"温馨提示",它会告诉进程:“你该结束工作了,请做好收尾,然后体面退出哦。”

SIGTERM的全称为"Signal Terminate"(终止信号),它的核心用途是请求进程正常终止。和那些强制终止的信号不同,SIGTERM是可以被进程"听到"并"回应"的——进程可以选择捕获这个信号,然后执行一系列清理操作(比如保存数据、释放网络连接、关闭打开的文件等),最后再自行退出。如果进程没有专门处理SIGTERM,系统会执行默认操作——终止进程,但这个过程依然比强制终止更"温和"。

常见的使用场景可太多了:

  • 系统关机时,init进程(或systemd)会向所有非关键进程发送SIGTERM,给它们几秒钟时间清理,之后再发送SIGKILL强制结束没反应的进程;
  • 用服务管理工具(如systemctl、service)停止服务时(比如systemctl stop nginx),工具会向服务进程发送SIGTERM;
  • 开发者在调试程序时,想让程序优雅退出以保存中间状态,会手动发送SIGTERM;
  • 容器管理工具(如Docker)停止容器时,默认也会先发送SIGTERM,等待一段时间后再强制终止。

二、SIGTERM的"出身"与相关工具

SIGTERM不是某个程序员拍脑袋发明的,它属于POSIX标准定义的信号之一(POSIX是一套操作系统接口标准,保证了不同类Unix系统的兼容性)。在Linux系统中,它的相关定义和处理函数都在signal.h头文件里,属于glibc库(GNU C库)的一部分。

要和SIGTERM打交道,咱们得认识几个关键的"工具人"函数:

  1. kill():用于向指定进程发送信号(包括SIGTERM);
  2. signal()sigaction():用于注册信号处理函数,让进程知道收到SIGTERM后该做什么;
  3. pthread_kill():向线程发送信号(但SIGTERM通常作用于整个进程)。

这些函数就像传递消息的快递员、接收消息的前台和处理消息的后台专员,共同构成了SIGTERM的工作流程。

三、与SIGTERM相关的函数:参数与返回值

既然SIGTERM的发送和处理都依赖函数,那咱们就得好好说说这些函数的参数和返回值。这里重点介绍最常用的kill()sigaction()

1. kill()函数:发送SIGTERM的"快递员"

kill()函数的作用是向进程(或进程组)发送指定信号,它的声明长这样:

#include <signal.h>
int kill(pid_t pid, int sig);

参数详解

  • pid_t pid:目标进程的ID(进程号),它的取值很灵活:
    • pid > 0时:向进程号为pid的单个进程发送信号;
    • pid = 0时:向当前进程所在进程组的所有进程发送信号;
    • pid = -1时:向系统中所有有权限发送的进程发送信号(除了init进程等特殊进程);
    • pid < -1时:向进程组ID为-pid的所有进程发送信号。
  • int sig:要发送的信号,发送SIGTERM时,这里就填SIGTERM(它在signal.h中被定义为15,所以也可以填15,但用宏更易读)。

返回值含义

  • 成功发送信号时,返回0
  • 失败时,返回-1,并设置errno来指示错误原因:
    • ESRCH:找不到pid对应的进程(比如进程已经退出了);
    • EPERM:没有权限向目标进程发送信号(比如普通用户不能向root进程发信号);
    • EINVALsig是无效的信号编号(比如填了个负数)。

举个例子,如果你想给进程号为1234的程序发送SIGTERM,用kill(1234, SIGTERM)就行;如果在命令行里,直接用kill -15 1234(15是SIGTERM的编号)或者kill -TERM 1234

2. sigaction()函数:处理SIGTERM的"前台专员"

sigaction()用于设置进程收到特定信号后的处理方式(比如注册一个函数,让进程收到SIGTERM时就执行它)。声明如下:

#include <signal.h>
int sigaction(int sig, const struct sigaction *restrict act, struct sigaction *restrict oact);

参数详解

  • int sig:要设置处理方式的信号,这里填SIGTERM
  • const struct sigaction *restrict act:指向struct sigaction结构体的指针,里面存放新的信号处理方式。这个结构体的关键成员是:
    • void (*sa_handler)(int):信号处理函数的指针,当收到信号时,系统会调用这个函数;
    • sigset_t sa_mask:在处理信号期间,需要阻塞的其他信号(比如处理SIGTERM时,不想被SIGINT打断);
    • int sa_flags:处理信号的标志,比如SA_RESTART表示被信号打断的系统调用会自动重启。
  • struct sigaction *restrict oact:如果不为NULL,会保存之前的信号处理方式(相当于"备份")。

返回值含义

  • 成功设置时,返回0
  • 失败时,返回-1,并设置errno(比如EINVAL表示sig是无效信号)。

sigaction()比旧的signal()函数更灵活、更可靠,所以推荐用它来处理SIGTERM。

四、实例与应用场景:让SIGTERM"动"起来

光说理论太枯燥,咱们来三个真实案例,看看SIGTERM在实际中是怎么用的。

案例一:基础版——捕获SIGTERM并优雅退出

应用场景:一个简单的后台程序,需要在退出前保存数据(比如记录程序运行了多久)。如果直接被强制终止,数据会丢失,所以用SIGTERM来触发保存操作。

完整代码(sigterm_demo1.c)

/**
 * @brief 演示SIGTERM信号的捕获与处理
 * 
 * 程序启动后会循环运行,每秒打印一次运行状态。
 * 当收到SIGTERM信号时,会执行处理函数:保存运行时间到文件,然后退出。
 * 
 * @in:
 *   - 无命令行参数
 * 
 * @out:
 *   - 控制台:每秒打印运行状态
 *   - 文件"run_time.log":保存程序运行的总秒数
 * 
 * 返回值说明:
 *   程序正常退出时返回0,处理信号失败时返回1
 */
#include <stdio.h>
#include <stdlib.h>
#include <signal.h>
#include <unistd.h>
#include <time.h>

// 全局变量:记录程序启动时间
time_t start_time;
// 标志:是否收到退出信号
volatile sig_atomic_t exit_flag = 0;

/**
 * @brief SIGTERM信号处理函数
 * 
 * 计算程序运行时间,保存到文件,然后设置退出标志。
 * 
 * @param sig:收到的信号编号(此处应为SIGTERM)
 */
void sigterm_handler(int sig) {
    // 计算运行时间(秒)
    time_t current_time = time(NULL);
    double run_seconds = difftime(current_time, start_time);

    // 保存到文件
    FILE *fp = fopen("run_time.log", "w");
    if (fp) {
        fprintf(fp, "程序运行了 %.0f 秒\n", run_seconds);
        fclose(fp);
        printf("\n收到SIGTERM信号,已保存运行时间到run_time.log\n");
    } else {
        printf("\n收到SIGTERM信号,但保存文件失败!\n");
    }

    // 设置退出标志(volatile sig_atomic_t确保多线程/信号安全)
    exit_flag = 1;
}

int main() {
    // 记录启动时间
    start_time = time(NULL);
    if (start_time == (time_t)-1) {
        perror("获取时间失败");
        return 1;
    }

    // 准备sigaction结构体,设置SIGTERM处理函数
    struct sigaction sa;
    // 清空结构体
    sigemptyset(&sa.sa_mask);
    // 设置处理函数
    sa.sa_handler = sigterm_handler;
    // 标志:不使用特殊处理
    sa.sa_flags = 0;

    // 注册SIGTERM信号处理函数
    if (sigaction(SIGTERM, &sa, NULL) == -1) {
        perror("注册SIGTERM处理函数失败");
        return 1;
    }

    printf("程序启动(PID:%d),等待SIGTERM信号...(按Ctrl+C退出或用kill发送SIGTERM)\n", getpid());

    // 主循环:每秒打印一次状态,直到收到退出信号
    int count = 0;
    while (!exit_flag) {
        printf("运行中... 第 %d 秒\n", count);
        count++;
        sleep(1); // 休眠1秒
    }

    printf("程序优雅退出\n");
    return 0;
}

程序流程图

程序启动
记录启动时间
初始化sigaction结构体
设置sa_handler为sigterm_handler
注册SIGTERM处理函数
成功?
打印启动信息(含PID)
打印错误信息,退出(返回1)
exit_flag为0?
(未收到信号)
打印运行状态,休眠1秒
打印优雅退出信息,退出(返回0)
外部发送SIGTERM信号
触发sigterm_handler函数
计算运行时间,保存到文件
设置exit_flag为1

Makefile

# 编译sigterm_demo1.c
sigterm_demo1: sigterm_demo1.c
	gcc -o sigterm_demo1 sigterm_demo1.c -Wall

# 清理编译产物
clean:
	rm -f sigterm_demo1 run_time.log

编译与运行步骤

  1. 保存代码为sigterm_demo1.c,Makefile为Makefile
  2. 在终端执行make,编译生成可执行文件sigterm_demo1
  3. 运行程序:./sigterm_demo1,此时程序会每秒打印运行状态,并显示自己的PID(比如12345);
  4. 打开另一个终端,发送SIGTERM信号:kill -TERM 12345(把12345换成实际的PID);
  5. 观察第一个终端的输出,程序会提示保存了运行时间,然后退出;
  6. 查看生成的run_time.log文件,确认运行时间被正确记录。

运行结果解读

  • 程序启动后,会持续输出"运行中… 第X秒",说明主循环在正常工作;
  • 当收到SIGTERM后,终端会显示"收到SIGTERM信号,已保存运行时间到run_time.log",然后主循环退出,输出"程序优雅退出";
  • 打开run_time.log,会看到类似"程序运行了5秒"的内容,说明清理操作(保存数据)成功执行;
  • 如果没收到SIGTERM,程序会一直运行,直到被其他信号(如Ctrl+C发送的SIGINT)终止。
案例二:进阶版——服务管理中的SIGTERM(模拟Nginx停止)

应用场景:Web服务器(如Nginx)在收到SIGTERM时,会停止接收新请求,处理完当前所有请求后再退出。咱们用一个简化程序模拟这个过程。

完整代码(sigterm_demo2.c)

/**
 * @brief 模拟Web服务器接收SIGTERM后的优雅关闭
 * 
 * 程序模拟Web服务器:启动后监听"请求"(实际是定时生成模拟请求),
 * 维护一个请求队列。收到SIGTERM后,停止接收新请求,处理完队列中
 * 剩余请求后退出。
 * 
 * @in:
 *   - 无命令行参数
 * 
 * @out:
 *   - 控制台:打印服务器状态、请求处理情况、关闭过程
 * 
 * 返回值说明:
 *   服务器正常关闭返回0,初始化失败返回1
 */
#include <stdio.h>
#include <stdlib.h>
#include <signal.h>
#include <unistd.h>
#include <pthread.h>
#include <time.h>

// 模拟请求结构体
typedef struct {
    int id; // 请求ID
    time_t create_time; // 创建时间
} Request;

// 服务器状态
typedef struct {
    pthread_mutex_t mutex; // 互斥锁,保护队列操作
    Request *queue; // 请求队列
    int queue_size; // 队列当前大小
    int max_queue; // 队列最大容量
    int running; // 服务器是否运行(1:运行,0:准备关闭)
    int next_req_id; // 下一个请求ID
} Server;

// 全局服务器实例
Server server;
// 标志:是否收到SIGTERM
volatile sig_atomic_t term_received = 0;

/**
 * @brief 初始化服务器
 * 
 * 初始化请求队列、互斥锁、服务器状态变量。
 * 
 * @return 成功返回0,失败返回-1
 */
int server_init() {
    server.max_queue = 10;
    server.queue = malloc(sizeof(Request) * server.max_queue);
    if (!server.queue) {
        perror("分配队列内存失败");
        return -1;
    }
    server.queue_size = 0;
    server.running = 1;
    server.next_req_id = 1;

    // 初始化互斥锁
    if (pthread_mutex_init(&server.mutex, NULL) != 0) {
        perror("初始化互斥锁失败");
        free(server.queue);
        return -1;
    }
    return 0;
}

/**
 * @brief 模拟接收新请求(添加到队列)
 * 
 * 只有服务器处于运行状态(running=1)时,才会接收新请求。
 */
void accept_request() {
    pthread_mutex_lock(&server.mutex);
    if (server.running && server.queue_size < server.max_queue) {
        Request req = {
            .id = server.next_req_id++,
            .create_time = time(NULL)
        };
        server.queue[server.queue_size++] = req;
        printf("[%ld] 接收新请求 #%d,当前队列大小:%d\n", 
               time(NULL), req.id, server.queue_size);
    }
    pthread_mutex_unlock(&server.mutex);
}

/**
 * @brief 处理队列中的请求
 * 
 * 从队列头部取出请求,模拟处理过程(耗时1秒),然后移除请求。
 */
void process_requests() {
    while (1) {
        pthread_mutex_lock(&server.mutex);
        if (server.queue_size == 0) {
            // 队列空时,检查是否需要退出
            if (!server.running) {
                pthread_mutex_unlock(&server.mutex);
                break;
            }
            pthread_mutex_unlock(&server.mutex);
            usleep(100000); // 休眠0.1秒,减少CPU占用
            continue;
        }

        // 取出队列头部的请求
        Request req = server.queue[0];
        // 队列元素前移
        for (int i = 0; i < server.queue_size - 1; i++) {
            server.queue[i] = server.queue[i + 1];
        }
        server.queue_size--;
        pthread_mutex_unlock(&server.mutex);

        // 模拟处理请求(耗时1秒)
        printf("[%ld] 开始处理请求 #%d...\n", time(NULL), req.id);
        sleep(1);
        printf("[%ld] 请求 #%d 处理完成\n", time(NULL), req.id);
    }
}

/**
 * @brief SIGTERM信号处理函数
 * 
 * 收到信号后,设置服务器状态为"停止接收新请求",并标记信号已收到。
 * 
 * @param sig:信号编号(SIGTERM)
 */
void sigterm_handler(int sig) {
    printf("\n[%ld] 收到SIGTERM信号,准备关闭服务器...\n", time(NULL));
    pthread_mutex_lock(&server.mutex);
    server.running = 0; // 停止接收新请求
    pthread_mutex_unlock(&server.mutex);
    term_received = 1;
}

/**
 * @brief 清理服务器资源
 * 
 * 释放队列内存,销毁互斥锁。
 */
void server_cleanup() {
    free(server.queue);
    pthread_mutex_destroy(&server.mutex);
    printf("[%ld] 服务器资源清理完成\n", time(NULL));
}

int main() {
    // 初始化服务器
    if (server_init() != 0) {
        return 1;
    }

    // 注册SIGTERM处理函数
    struct sigaction sa;
    sigemptyset(&sa.sa_mask);
    sa.sa_handler = sigterm_handler;
    sa.sa_flags = 0;
    if (sigaction(SIGTERM, &sa, NULL) == -1) {
        perror("注册SIGTERM处理函数失败");
        server_cleanup();
        return 1;
    }

    printf("模拟Web服务器启动(PID:%d)\n", getpid());
    printf("服务器开始接收请求...(发送SIGTERM停止服务器)\n");

    // 创建处理请求的线程
    pthread_t process_thread;
    if (pthread_create(&process_thread, NULL, (void *(*)(void *))process_requests, NULL) != 0) {
        perror("创建处理线程失败");
        server_cleanup();
        return 1;
    }

    // 主线程:模拟定时接收新请求(每2秒一个)
    while (!term_received) {
        accept_request();
        sleep(2);
    }

    // 等待处理线程处理完剩余请求
    printf("[%ld] 等待剩余请求处理完成...\n", time(NULL));
    pthread_join(process_thread, NULL);

    // 清理资源并退出
    server_cleanup();
    printf("[%ld] 服务器已优雅关闭\n", time(NULL));
    return 0;
}

程序流程图

term_received=0
term_received=1
队列非空
队列空且server.running=1
队列空且server.running=0
服务器启动
初始化服务器(队列、锁、状态)
注册SIGTERM处理函数(sigterm_handler)
创建处理请求线程(process_thread)
主线程:每2秒接收新请求(accept_request)
处理线程:循环处理队列中的请求(process_requests)
外部发送SIGTERM
触发sigterm_handler
设置server.running=0(停止接收新请求)
设置term_received=1
主线程等待处理线程结束(pthread_join)
取出请求,处理(1秒),移除请求
休眠0.1秒,继续循环
处理线程退出
清理服务器资源(释放队列、销毁锁)
服务器优雅关闭,退出

Makefile

# 编译sigterm_demo2.c(需要链接pthread库)
sigterm_demo2: sigterm_demo2.c
	gcc -o sigterm_demo2 sigterm_demo2.c -Wall -lpthread

# 清理编译产物
clean:
	rm -f sigterm_demo2

编译与运行步骤

  1. 保存代码为sigterm_demo2.c,Makefile为Makefile
  2. 终端执行make,生成sigterm_demo2(注意链接pthread库,否则编译失败);
  3. 运行程序:./sigterm_demo2,观察服务器启动并开始接收请求;
  4. 等待3-5秒(让队列积累几个请求),在另一个终端发送SIGTERM:kill -TERM 服务器PID
  5. 观察服务器输出,直到完全关闭。

运行结果解读

  • 程序启动后,每2秒会接收一个新请求(如"接收新请求 #1,当前队列大小:1"),处理线程会每秒处理一个请求(如"开始处理请求 #1…“→"请求 #1 处理完成”);
  • 收到SIGTERM后,服务器会打印"准备关闭服务器…",并停止接收新请求(此时主线程的accept_request不再添加新请求);
  • 处理线程会继续处理队列中剩余的请求(比如队列里还有#3、#4请求,会依次处理完);
  • 所有请求处理完毕后,处理线程退出,主线程执行清理操作(释放内存、销毁锁),最后打印"服务器已优雅关闭";
  • 这个过程完美模拟了Web服务器的优雅关闭:不丢弃已有请求,也不接收新请求,保证了服务的完整性。
案例三:高级版——多进程程序中的SIGTERM传递

应用场景:一个多进程程序(如父进程管理多个子进程工作),当父进程收到SIGTERM时,需要先向所有子进程发送SIGTERM,等待它们退出后,父进程再清理资源退出。

完整代码(sigterm_demo3.c)

/**
 * @brief 多进程程序中SIGTERM的传递与处理
 * 
 * 父进程创建3个子进程,每个子进程模拟执行任务。当父进程收到SIGTERM时,
 * 会向所有子进程发送SIGTERM,等待子进程退出后,父进程清理资源并退出。
 * 子进程收到SIGTERM后,会执行自己的清理操作(如保存任务进度)。
 * 
 * @in:
 *   - 无命令行参数
 * 
 * @out:
 *   - 控制台:打印父进程、子进程的状态、信号处理情况
 *   - 子进程会生成"child_X_progress.log"(X为子进程ID)
 * 
 * 返回值说明:
 *   父进程正常退出返回0,初始化失败返回1;子进程正常退出返回0
 */
#include <stdio.h>
#include <stdlib.h>
#include <signal.h>
#include <unistd.h>
#include <sys/wait.h>
#include <time.h>
#include <string.h>

#define NUM_CHILDREN 3 // 子进程数量

// 父进程中:子进程PID数组
pid_t child_pids[NUM_CHILDREN];
// 父进程中:已退出的子进程数量
int exited_children = 0;
// 父进程中:是否收到SIGTERM
volatile sig_atomic_t parent_term = 0;

/**
 * @brief 子进程的SIGTERM处理函数
 * 
 * 子进程收到SIGTERM后,保存当前任务进度到日志文件,然后退出。
 * 
 * @param sig:信号编号(SIGTERM)
 */
void child_sigterm_handler(int sig) {
    char log_file[32];
    sprintf(log_file, "child_%d_progress.log", getpid());
    FILE *fp = fopen(log_file, "w");
    if (fp) {
        time_t now = time(NULL);
        fprintf(fp, "子进程 %d 在 %s 收到终止信号,任务进度已保存\n",
                getpid(), ctime(&now));
        fclose(fp);
        printf("[子进程 %d] 收到SIGTERM,已保存进度到 %s\n", getpid(), log_file);
    } else {
        printf("[子进程 %d] 收到SIGTERM,但保存进度失败!\n", getpid());
    }
    exit(0); // 子进程退出
}

/**
 * @brief 子进程的任务函数
 * 
 * 模拟执行任务:每3秒打印一次进度,持续运行直到收到SIGTERM。
 * 
 * @param id:子进程编号(0~NUM_CHILDREN-1)
 */
void child_task(int id) {
    // 子进程注册自己的SIGTERM处理函数
    struct sigaction sa;
    sigemptyset(&sa.sa_mask);
    sa.sa_handler = child_sigterm_handler;
    sa.sa_flags = 0;
    if (sigaction(SIGTERM, &sa, NULL) == -1) {
        perror("[子进程] 注册SIGTERM处理函数失败");
        exit(1);
    }

    // 模拟任务执行
    int progress = 0;
    while (1) {
        printf("[子进程 %d(编号%d)] 任务进度:%d%%\n", getpid(), id, progress);
        progress += 10;
        if (progress > 100) progress = 0;
        sleep(3); // 每3秒更新一次进度
    }
}

/**
 * @brief 父进程的SIGCHLD处理函数
 * 
 * 当子进程退出时,父进程会收到SIGCHLD信号,此函数负责回收子进程资源,
 * 并记录已退出的子进程数量。
 * 
 * @param sig:信号编号(SIGCHLD)
 */
void parent_sigchld_handler(int sig) {
    pid_t pid;
    int status;
    // 循环回收所有已退出的子进程(避免信号丢失)
    while ((pid = waitpid(-1, &status, WNOHANG)) > 0) {
        exited_children++;
        printf("[父进程 %d] 子进程 %d 已退出,已退出数量:%d/%d\n",
               getpid(), pid, exited_children, NUM_CHILDREN);
    }
}

/**
 * @brief 父进程的SIGTERM处理函数
 * 
 * 父进程收到SIGTERM后,向所有子进程发送SIGTERM,然后等待它们退出。
 * 
 * @param sig:信号编号(SIGTERM)
 */
void parent_sigterm_handler(int sig) {
    printf("\n[父进程 %d] 收到SIGTERM,开始通知所有子进程...\n", getpid());
    // 向每个子进程发送SIGTERM
    for (int i = 0; i < NUM_CHILDREN; i++) {
        if (child_pids[i] > 0) { // 确保子进程存在
            if (kill(child_pids[i], SIGTERM) == 0) {
                printf("[父进程 %d] 已向子进程 %d 发送SIGTERM\n", getpid(), child_pids[i]);
            } else {
                perror("[父进程] 发送SIGTERM失败");
            }
        }
    }
    parent_term = 1;
}

int main() {
    pid_t parent_pid = getpid();
    printf("[父进程 %d] 启动,准备创建 %d 个子进程...\n", parent_pid, NUM_CHILDREN);

    // 父进程注册SIGTERM和SIGCHLD处理函数
    struct sigaction sa_term, sa_chld;

    // 处理SIGTERM
    sigemptyset(&sa_term.sa_mask);
    sa_term.sa_handler = parent_sigterm_handler;
    sa_term.sa_flags = 0;
    if (sigaction(SIGTERM, &sa_term, NULL) == -1) {
        perror("[父进程] 注册SIGTERM处理函数失败");
        return 1;
    }

    // 处理SIGCHLD(子进程退出时收到)
    sigemptyset(&sa_chld.sa_mask);
    sa_chld.sa_handler = parent_sigchld_handler;
    sa_chld.sa_flags = 0;
    if (sigaction(SIGCHLD, &sa_chld, NULL) == -1) {
        perror("[父进程] 注册SIGCHLD处理函数失败");
        return 1;
    }

    // 创建子进程
    for (int i = 0; i < NUM_CHILDREN; i++) {
        pid_t pid = fork();
        if (pid == -1) {
            perror("[父进程] 创建子进程失败");
            // 清理已创建的子进程
            for (int j = 0; j < i; j++) {
                kill(child_pids[j], SIGKILL);
                waitpid(child_pids[j], NULL, 0);
            }
            return 1;
        } else if (pid == 0) {
            // 子进程:执行任务
            child_task(i);
            // 子进程任务函数不会返回,这里只是保险
            exit(0);
        } else {
            // 父进程:记录子进程PID
            child_pids[i] = pid;
            printf("[父进程 %d] 已创建子进程 %d(编号%d)\n", parent_pid, pid, i);
        }
    }

    // 父进程等待:直到所有子进程退出
    printf("[父进程 %d] 等待子进程运行,发送SIGTERM给我来终止所有进程...\n", parent_pid);
    while (!parent_term || exited_children < NUM_CHILDREN) {
        sleep(1);
    }

    // 所有子进程退出后,父进程清理并退出
    printf("[父进程 %d] 所有子进程已退出,开始清理资源...\n", parent_pid);
    printf("[父进程 %d] 清理完成,退出\n", parent_pid);
    return 0;
}

程序流程图

parent_term=1且exited_children=3
父进程启动
注册父进程的SIGTERM处理函数(parent_sigterm_handler)
注册SIGCHLD处理函数(parent_sigchld_handler)
创建3个子进程(循环fork)
子进程:注册自己的SIGTERM处理函数(child_sigterm_handler)
执行任务(每3秒打印进度)
父进程:等待(循环sleep),直到收到SIGTERM且所有子进程退出
外部向父进程发送SIGTERM
触发parent_sigterm_handler
父进程向所有子进程发送SIGTERM
设置parent_term=1
子进程收到SIGTERM,触发child_sigterm_handler
子进程保存进度到日志文件,退出
子进程退出,内核向父进程发送SIGCHLD
触发parent_sigchld_handler,回收子进程资源
exited_children++
父进程清理资源,退出

Makefile

# 编译sigterm_demo3.c
sigterm_demo3: sigterm_demo3.c
	gcc -o sigterm_demo3 sigterm_demo3.c -Wall

# 清理编译产物和日志文件
clean:
	rm -f sigterm_demo3 child_*.log

编译与运行步骤

  1. 保存代码为sigterm_demo3.c,Makefile为Makefile
  2. 终端执行make,生成sigterm_demo3
  3. 运行程序:./sigterm_demo3,观察父进程创建3个子进程,子进程开始打印任务进度;
  4. 等待5-10秒,在另一个终端发送SIGTERM给父进程:kill -TERM 父进程PID
  5. 观察程序输出,直到父进程完全退出;
  6. 查看生成的child_X_progress.log文件(X为子进程PID)。

运行结果解读

  • 父进程启动后,会创建3个子进程,每个子进程有自己的编号,每3秒打印一次任务进度(0%→10%→…→100%→0%循环);
  • 当父进程收到SIGTERM后,会打印"开始通知所有子进程…",并向每个子进程发送SIGTERM;
  • 子进程收到SIGTERM后,会各自生成日志文件(如child_12345_progress.log),记录收到信号的时间和进度,然后退出;
  • 每个子进程退出时,父进程会收到SIGCHLD信号,触发parent_sigchld_handler,回收子进程资源并计数;
  • 当所有3个子进程都退出后,父进程会打印"所有子进程已退出,开始清理资源…",最后退出;
  • 这个案例展示了SIGTERM在多进程协作中的重要性:通过信号传递,实现了"自上而下"的优雅终止,确保每个进程都能完成自己的清理工作。

五、SIGTERM的"朋友圈":与其他信号的区别

聊了这么多SIGTERM,咱们再看看它的"亲戚们",理解它们的区别能更好地掌握SIGTERM的特性:

  1. SIGTERM vs SIGKILL(信号9)

    • SIGTERM是"请求终止",可以被捕获、忽略或处理;
    • SIGKILL是"强制终止",不能被捕获、忽略,进程会立即退出,没有清理机会;
    • 场景:如果进程收到SIGTERM后无响应(比如卡死),可以用SIGKILL强制杀死。
  2. SIGTERM vs SIGINT(信号2,Ctrl+C触发)

    • 两者默认行为都是终止进程,但语义不同;
    • SIGINT通常是"用户中断"(比如用户按Ctrl+C);
    • SIGTERM通常是"系统/程序请求终止"(比如服务管理工具);
    • 很多程序会对两者做相同处理,但也可以分开处理(比如SIGINT保存临时数据,SIGTERM做完整清理)。
  3. SIGTERM vs SIGQUIT(信号3,Ctrl+\触发)

    • SIGQUIT默认会终止进程并生成核心转储文件(core dump),用于调试;
    • SIGTERM不会生成核心转储,更侧重"正常退出"。

记住:SIGTERM的核心是"优雅",给进程一个体面退出的机会。

六、总结:SIGTERM的一生

让我们用一张图总结SIGTERM从发送到处理的完整流程:

使用kill()/pkill等工具
✅ 是
❌ 否
👤 信号发送者
(用户/系统/程序)
⚡ SIGTERM信号生成
🔄 内核将信号传递给目标进程
❓ 进程是否注册了
SIGTERM处理函数?
🛠️ 执行处理函数
(清理资源、保存数据等)
🚪 进程自行退出
⚰️ 执行默认操作(终止进程)
🏁 进程终止,生命周期结束

从图中可以看到,SIGTERM的旅程始于发送者,经过内核传递,最终由进程决定如何响应——是执行自定义清理,还是接受默认终止。这个过程体现了Unix/Linux系统设计的哲学:给程序足够的自主权,让它们能以最合理的方式结束自己的工作。

七、写在最后

SIGTERM虽然只是一个信号,但它背后反映的是系统设计中"优雅"与"秩序"的追求。理解并正确使用SIGTERM,能让你的程序更健壮、更友好——想象一下,如果每个程序都能在退出前认真清理自己的"烂摊子",系统会稳定得多。

希望通过今天的讲解,你不仅知道了SIGTERM是什么,更能在实际开发中用好它。下次当你需要停止一个程序时,不妨先试试发送SIGTERM,给它一个体面告别的机会——毕竟,谁不想优雅地结束呢?

Logo

魔乐社区(Modelers.cn) 是一个中立、公益的人工智能社区,提供人工智能工具、模型、数据的托管、展示与应用协同服务,为人工智能开发及爱好者搭建开放的学习交流平台。社区通过理事会方式运作,由全产业链共同建设、共同运营、共同享有,推动国产AI生态繁荣发展。

更多推荐