C高级:与关僵尸进程、信号与 System V 信号量的学习记录

Linux 系统编程详解:僵尸进程、信号与 System V 信号量

前言

在 Linux 系统编程中,进程管理、信号处理和进程间通信(IPC)是三个非常核心的主题。本文将从实际开发的角度出发,详细讲解僵尸进程的产生与避免、signal() 信号处理函数,以及 System V 信号量相关的 ftok()semget()semctl()semop() 四个函数。每个函数都会逐一分析其参数含义和使用方法,并配有示例代码。以下是我在职坐标的学习记录:

一、僵尸进程(Zombie Process)

1.1 什么是僵尸进程

在 Linux 中,当一个子进程终止(调用 exit() 或被信号杀死)后,它并不会立即从进程表中消失。内核会为它保留一部分信息(进程 ID、退出状态、运行时间等),等待父进程通过 wait()waitpid() 来"收尸"。

如果父进程没有调用 wait()/waitpid() 去获取子进程的退出状态,那么这个已经终止但尚未被回收的子进程就会变成 僵尸进程(Zombie Process)

top 命令中,僵尸进程的状态标记为 Z;在 ps 命令输出中,它的 STATUS 列也显示为 Z<defunct>

1.2 僵尸进程的危害

僵尸进程虽然已经不占用内存和 CPU 资源,但它仍然占据着一个进程表项,主要危害包括:

  • 浪费进程 ID:Linux 系统的进程号是有限的(默认最大 32768,可通过 /proc/sys/kernel/pid_max 查看)。大量僵尸进程会耗尽可用的 PID,导致无法创建新进程。
  • 占用进程表资源:每个僵尸进程都会在内核进程表中保留一个条目,消耗系统管理资源。

1.3 如何避免僵尸进程

方法一:父进程主动调用 wait()/waitpid()

最直接的方式是在子进程结束后,父进程调用 wait()waitpid() 回收子进程资源:

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/wait.h>

int main() {
    pid_t pid = fork();
    if (pid == 0) {
        // 子进程
        printf("子进程运行中,PID = %d\n", getpid());
        exit(0);
    } else if (pid > 0) {
        // 父进程等待子进程结束
        int status;
        waitpid(pid, &status, 0);
        printf("子进程已被回收,退出状态 = %d\n", WEXITSTATUS(status));
    }
    return 0;
}

方法二:捕获 SIGCHLD 信号

子进程终止时,内核会向父进程发送 SIGCHLD 信号。父进程可以注册一个信号处理函数,在其中调用 waitpid() 来回收子进程:

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <signal.h>
#include <sys/wait.h>

void sigchld_handler(int sig) {
    // 使用循环 + WNOHANG 回收所有已终止的子进程
    while (waitpid(-1, NULL, WNOHANG) > 0) {
        printf("已回收一个僵尸子进程\n");
    }
}

int main() {
    signal(SIGCHLD, sigchld_handler);

    pid_t pid = fork();
    if (pid == 0) {
        printf("子进程运行中,PID = %d\n", getpid());
        sleep(2);
        exit(0);
    }

    // 父进程做其他事情
    while (1) {
        sleep(1);
    }
    return 0;
}

二、signal() 函数

2.1 函数原型

#include <signal.h>

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

2.2 函数作用

signal() 用于为指定的信号注册一个处理函数。当进程收到该信号时,会中断当前的执行流,跳转到注册的处理函数中去执行。

2.3 参数详解

参数 类型 说明
signum int 要捕获的信号编号,例如 SIGINT(2,Ctrl+C)、SIGCHLD(17)等。可以是 <signal.h> 中定义的任何信号常量。
handler sighandler_t(即 void (*)(int) 信号处理函数指针。也可以传入特殊值:SIG_DFL(恢复默认处理行为)或 SIG_IGN(忽略该信号)。

2.4 返回值

  • 成功:返回之前的信号处理函数指针。
  • 失败:返回 SIG_ERR,并设置 errno

2.5 示例代码

#include <stdio.h>
#include <signal.h>
#include <unistd.h>

void handle_sigint(int sig) {
    printf("收到信号 %d (SIGINT),但我不退出!\n", sig);
}

int main() {
    // 注册 SIGINT 的处理函数
    signal(SIGINT, handle_sigint);

    printf("进程运行中,PID = %d\n", getpid());
    while (1) {
        sleep(1);
    }
    return 0;
}

注意signal() 在不同 UNIX 系统上的行为可能不一致。在可移植性要求较高的场景下,推荐使用 sigaction() 替代。

三、ftok() 函数

3.1 函数原型

#include <sys/types.h>
#include <sys/ipc.h>

key_t ftok(const char *pathname, int proj_id);

3.2 函数作用

ftok() 将一个已存在的文件路径名和一个项目标识符转换为一个 System V IPC 键值(key_t 类型)。这个键值随后可以传递给 semget()msgget()shmget() 等函数,用于创建或访问对应的 IPC 对象。

它的核心作用是让多个不相关的进程能够通过约定同一个文件路径和项目 ID 来获得相同的 IPC 键值,从而找到同一个共享资源。

3.3 参数详解

参数 类型 说明
pathname const char * 一个已存在的文件路径名。该文件必须存在且可访问,否则 ftok() 会失败。通常使用一个约定的文件路径(如 /tmp/my_ipc_key 或项目中的某个文件)。
proj_id int 项目标识符,通常是一个 1~255 之间的整数(虽然标准只要求非零)。不同的项目可以使用同一个文件路径但不同的 proj_id 来生成不同的键值。

3.4 返回值

  • 成功:返回一个非零的 key_t 键值。
  • 失败:返回 -1,并设置 errno(如文件不存在、无权限等)。

3.5 生成原理

ftok() 内部通常取文件的 inode 编号和 st_dev(设备号)的低字节,结合 proj_id 组合生成键值。因此,如果文件被删除后重建(inode 变化),即使路径相同,生成的键值也可能不同。

3.6 示例代码

#include <stdio.h>
#include <sys/ipc.h>

int main() {
    // 确保 /tmp/my_ipc_key 文件存在
    // 可以用 touch /tmp/my_ipc_key 提前创建
    key_t key = ftok("/tmp/my_ipc_key", 65);
    if (key == -1) {
        perror("ftok 失败");
        return 1;
    }
    printf("生成的 IPC key = %d (0x%x)\n", key, key);
    return 0;
}

四、semget() 函数

4.1 函数原型

#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/sem.h>

int semget(key_t key, int nsems, int semflg);

4.2 函数作用

semget() 用于创建一个新的 System V 信号量集,或者获取一个已存在的信号量集的标识符(semid)。它是使用 System V 信号量的第一步——先通过 semget() 拿到信号量集 ID,再通过 semctl() 初始化,最后通过 semop() 进行操作。

4.3 参数详解

参数 类型 说明
key key_t IPC 键值,通常由 ftok() 生成。也可以使用特殊值 IPC_PRIVATE(值为 0),表示创建一个只能被当前进程及其子进程使用的私有信号量集。
nsems int 信号量集中信号量的个数。如果创建新信号量集,该值必须大于 0;如果只是获取已存在的信号量集,可以传 0。
semflg int 标志位,控制权限和行为,常用值包括:IPC_CREAT(如果不存在则创建)、IPC_EXCL(配合 IPC_CREAT 使用,如果已存在则失败返回)、0666(权限掩码,类似文件权限 rwxrwxrwx)。多个标志可以用 `

4.4 semflg 常用组合

// 获取已存在的信号量集,不存在则报错
semget(key, 1, 0666);

// 不存在则创建,已存在则获取
semget(key, 1, IPC_CREAT | 0666);

// 不存在则创建,已存在则报错(确保是全新创建的)
semget(key, 1, IPC_CREAT | IPC_EXCL | 0666);

4.5 返回值

  • 成功:返回信号量集的标识符 semid(一个非负整数)。
  • 失败:返回 -1,并设置 errno

4.6 示例代码

#include <stdio.h>
#include <sys/ipc.h>
#include <sys/sem.h>

int main() {
    key_t key = ftok("/tmp/my_ipc_key", 65);
    if (key == -1) {
        perror("ftok 失败");
        return 1;
    }

    // 创建一个包含 1 个信号量的信号量集
    int semid = semget(key, 1, IPC_CREAT | 0666);
    if (semid == -1) {
        perror("semget 失败");
        return 1;
    }
    printf("信号量集 ID = %d\n", semid);
    return 0;
}

五、semctl() 函数

5.1 函数原型

#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/sem.h>

union semun {
    int              val;    // SETVAL 时使用:设置信号量的值
    struct semid_ds *buf;    // IPC_STAT / IPC_SET 时使用
    unsigned short  *array;  // GETALL / SETALL 时使用
    struct seminfo  *__buf;  // IPC_INFO 时使用(Linux 特有)
};

int semctl(int semid, int sem_num, int cmd, ...);

注意union semun 在某些系统上需要自己定义(POSIX 标准要求用户自定义),Linux 的 <sys/sem.h> 在定义了 _GNU_SOURCE 时会提供它。

5.2 函数作用

semctl() 是对信号量集执行各种控制操作的函数,包括:设置/获取信号量的值、删除信号量集、获取信号量集的元信息等。它是信号量的"管理工具"。

5.3 参数详解

参数 类型 说明
semid int 信号量集的标识符,由 semget() 返回。
sem_num int 信号量集中具体某个信号量的编号(从 0 开始)。如果信号量集中只有 1 个信号量,则传 0。某些命令(如 IPC_RMIDIPC_STAT)会忽略此参数。
cmd int 要执行的操作命令,常用命令见下表。
...(第四个参数) union semun 可选参数,根据 cmd 的不同,可能需要传入一个 union semun 类型的联合体。

5.4 常用 cmd 命令

命令 说明 是否需要第四个参数
SETVAL 设置指定信号量的初始值 需要(使用 val 成员)
GETVAL 获取指定信号量的当前值 不需要
IPC_RMID 删除整个信号量集 不需要
IPC_STAT 获取信号量集的 semid_ds 结构体信息 需要(使用 buf 成员)
IPC_SET 设置信号量集的权限等信息 需要(使用 buf 成员

5.5 返回值

  • 成功:根据 cmd 不同,返回不同的非负值(如 GETVAL 返回信号量值)。
  • 失败:返回 -1,并设置 errno

5.6 示例代码

#include <stdio.h>
#include <sys/ipc.h>
#include <sys/sem.h>

// 自定义 semun 联合体(某些系统需要)
union semun {
    int              val;
    struct semid_ds *buf;
    unsigned short  *array;
};

int main() {
    key_t key = ftok("/tmp/my_ipc_key", 65);
    int semid = semget(key, 1, IPC_CREAT | 0666);
    if (semid == -1) {
        perror("semget 失败");
        return 1;
    }

    // 使用 SETVAL 将信号量初始值设为 1(相当于二值信号量/互斥锁)
    union semun arg;
    arg.val = 1;
    if (semctl(semid, 0, SETVAL, arg) == -1) {
        perror("semctl SETVAL 失败");
        return 1;
    }

    // 使用 GETVAL 获取信号量当前值
    int val = semctl(semid, 0, GETVAL);
    printf("信号量当前值 = %d\n", val);

    // 删除信号量集(程序结束时清理资源)
    if (semctl(semid, 0, IPC_RMID) == -1) {
        perror("semctl IPC_RMID 失败");
        return 1;
    }
    printf("信号量集已删除\n");
    return 0;
}

六、semop() 函数

6.1 函数原型

#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/sem.h>

int semop(int semid, struct sembuf *sops, size_t nsops);

其中 struct sembuf 的定义如下:

struct sembuf {
    unsigned short sem_num;  // 信号量在集合中的编号(从 0 开始)
    short          sem_op;   // 操作值(正数、负数或 0)
    short          sem_flg;  // 操作标志
};

6.2 函数作用

semop() 对信号量集中的信号量执行原子操作(P 操作和 V 操作)。它是实际使用信号量进行进程同步/互斥时最核心的函数。

6.3 参数详解

参数 类型 说明
semid int 信号量集的标识符,由 semget() 返回。
sops struct sembuf * 指向 sembuf 结构体数组的指针,每个元素描述一个操作。
nsops size_t sops 数组中操作的个数(即要执行几个操作)。

6.4 sembuf 结构体成员详解

sem_num(信号量编号)

指定要操作信号量集中第几个信号量,从 0 开始编号。如果信号量集中只有 1 个信号量,则填 0。

sem_op(操作值)

这是最关键的参数,决定了操作的语义:

sem_op 值 含义 说明
正数(如 +1) V 操作(释放资源) 将信号量的值加上 sem_op。表示进程释放了若干个资源。此操作不会阻塞。
负数(如 -1) P 操作(申请资源) 将信号量的值减去 `
0 等待信号量变为 0 进程会阻塞,直到信号量的值变为 0。如果信号量已经是 0,则立即返回。

sem_flg(操作标志)

标志 说明
0 默认行为,操作会阻塞直到可以执行。
IPC_NOWAIT 非阻塞模式。如果操作不能立即执行,不阻塞,直接返回 -1 并设置 errnoEAGAIN
SEM_UNDO 自动撤销。当进程异常终止时,内核会自动撤销该进程对这个信号量所做的操作,防止死锁。强烈建议使用

6.5 返回值

  • 成功:返回 0
  • 失败:返回 -1,并设置 errno

6.6 经典的 P/V 操作封装

#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/sem.h>

// P 操作:申请资源(信号量减 1)
int sem_p(int semid) {
    struct sembuf sb;
    sb.sem_num = 0;          // 操作第 0 个信号量
    sb.sem_op  = -1;         // P 操作:减 1
    sb.sem_flg = SEM_UNDO;   // 进程退出时自动撤销
    return semop(semid, &sb, 1);
}

// V 操作:释放资源(信号量加 1)
int sem_v(int semid) {
    struct sembuf sb;
    sb.sem_num = 0;          // 操作第 0 个信号量
    sb.sem_op  = +1;         // V 操作:加 1
    sb.sem_flg = SEM_UNDO;   // 进程退出时自动撤销
    return semop(semid, &sb, 1);
}

七、总结:System V 信号量的使用流程

使用 System V 信号量的标准流程可以概括为以下四个步骤:

  1. 生成键值:调用 ftok(pathname, proj_id) 根据文件路径和项目 ID 生成 IPC 键值。
  2. 创建/获取信号量集:调用 semget(key, nsems, semflg) 创建或获取信号量集,得到 semid
  3. 初始化信号量:调用 semctl(semid, sem_num, SETVAL, arg) 设置信号量的初始值。
  4. 执行 P/V 操作:调用 semop(semid, &sops, nsops) 进行资源的申请(P)和释放(V)。
  5. 清理资源:使用完毕后,调用 semctl(semid, 0, IPC_RMID) 删除信号量集,避免资源泄漏。

提醒:System V 信号量是随内核持续的,即使程序退出,信号量也不会自动销毁(除非使用了 SEM_UNDO 标志且进程异常终止)。因此,务必在程序结束时手动调用 IPC_RMID 清理信号量。

如果本文对你有帮助,欢迎点赞、收藏、关注!如有错误,欢迎在评论区指正。

posted @ 2026-07-15 14:38  Solitude0420  阅读(9)  评论(4)    收藏  举报