Linux 信号量

1.system v信号量

在使用 System V 信号量的任何函数前,必须包含以下头文件:

核心接口详解

System V 信号量主要由三个函数操作:semget(创建 / 获取)、semop(操作)、semctl(控制)。

semget - 创建或打开一个信号量集

用于创建一个新的信号量集,或获取一个已存在的信号量集的标识符。

函数原型:

cpp 复制代码
int semget(key_t key, int nsems, int semflg);

参数说明:

  • key : 键值,通常由 ftok() 生成,用于唯一标识一个 IPC 对象。也可以使用 IPC_PRIVATE 创建私有对象。
  • nsems: 该信号量集中包含的信号量数量。如果是创建新集合,必须指定此值;如果是获取已存在的集合,可以设为 0。
  • semflg : 标志位。
    • 权限位 :例如 0666(八进制),表示所有用户都可读写。
    • IPC_CREAT:如果 key 对应的信号量集不存在,则创建它。
    • IPC_EXCL :与 IPC_CREAT 一起使用,如果 key 已存在,则报错返回(而不是打开它)。

返回值:

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

semop - 执行信号量操作(P/V 操作)

这是最核心的函数,用于改变信号量的值(加、减或等待为 0)。

函数原型:

cpp 复制代码
int semop(int semid, struct sembuf *sops, size_t nsops);

参数说明:

  • semid : 由 semget 返回的标识符。
  • sops : 指向一个 struct sembuf 数组的指针。
  • nsops : 数组中 sembuf 结构体的数量(通常为 1)。

**关键数据结构 struct sembuf:**你需要填充这个结构体来告诉内核要做什么操作:

cpp 复制代码
struct sembuf {
    unsigned short sem_num;  /* 信号量在集合中的序号 (0, 1, ... nsems-1) */
    short          sem_op;   /* 操作:正数(V),负数(P),0(等待归零) */
    short          sem_flg;  /* 操作标志:0, IPC_NOWAIT, SEM_UNDO */
};
  • sem_num : 操作第几个信号量,单个信号量时填 0
  • sem_op :
    • -n (P 操作):申请资源。如果信号量值 >= n,则减去 n 并继续;否则阻塞,直到资源足够。
    • +n (V 操作):释放资源。信号量值加上 n。
    • 0:等待,直到信号量值变为 0。
  • sem_flg :
    • 0:阻塞模式。
    • IPC_NOWAIT:非阻塞,如果操作无法立即执行则报错返回。
    • SEM_UNDO强烈建议设置。进程退出时,内核会自动撤销该进程对信号量的操作(防止进程异常退出导致死锁)。

返回值:

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

semctl - 控制信号量(初始化、删除等)

用于获取或设置信号量集的属性,最常用于初始化 信号量的值或删除信号量集。

函数原型

cpp 复制代码
int semctl(int semid, int semnum, int cmd, ...);

注意:这是一个可变参数函数,第 4 个参数通常是一个联合体 union semun你必须自己定义这个联合体

自定义联合体:

cpp 复制代码
// 必须在代码中手动定义这个联合体
union semun {
    int              val;    /* 用于 SETVAL:设置单个信号量的值 */
    struct semid_ds *buf;    /* 用于 IPC_STAT / IPC_SET:获取/设置信号量集属性 */
    unsigned short  *array;  /* 用于 GETALL / SETALL:获取/设置所有信号量的值 */
    struct seminfo  *__buf;  /* 用于 IPC_INFO:获取系统范围内的信号量信息 (Linux特有) */
};

参数说明:

  • semid: 标识符。
  • semnum: 要操作的信号量序号(0 开始)。
  • cmd : 执行的命令。
    • SETVAL :初始化信号量的值为 arg.val
    • GETVAL:获取信号量的当前值(作为返回值)。
    • IPC_RMID :立即删除信号量集,唤醒所有等待的进程。此时 semnum 参数被忽略。

返回值:

  • 成功:根据 cmd 不同返回不同值(通常 0GETVAL 返回信号量值)。
  • 失败:返回 -1,并设置 errno
cpp 复制代码
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/sem.h>
#include <sys/wait.h>

// 必须手动定义 semun 联合体
union semun {
    int val;
    struct semid_ds *buf;
    unsigned short *array;
};

// P 操作 (Wait/Decrement):获取锁
void p(int semid) {
    struct sembuf sb = {0, -1, SEM_UNDO}; // 对0号信号量减1,设置SEM_UNDO
    if (semop(semid, &sb, 1) == -1) {
        perror("P operation failed");
        exit(1);
    }
}

// V 操作 (Signal/Increment):释放锁
void v(int semid) {
    struct sembuf sb = {0, 1, SEM_UNDO}; // 对0号信号量加1
    if (semop(semid, &sb, 1) == -1) {
        perror("V operation failed");
        exit(1);
    }
}

int main() {
    key_t key;
    int semid;
    union semun su;

    // 1. 生成 key (路径名+项目ID,必须唯一)
    // 这里用当前目录 "." 和 整数 66
    if ((key = ftok(".", 66)) == -1) {
        perror("ftok");
        exit(1);
    }

    // 2. 创建信号量集 (包含1个信号量,权限 0666)
    // IPC_CREAT | IPC_EXCL 确保如果已存在则报错,防止冲突
    if ((semid = semget(key, 1, 0666 | IPC_CREAT | IPC_EXCL)) == -1) {
        perror("semget (create)");
        // 如果是因为已存在报错,我们可以尝试直接获取并删除旧的,或者退出
        // 这里为了演示简单,我们尝试获取它 (实际项目中需谨慎处理)
        if ((semid = semget(key, 1, 0666)) == -1) {
             perror("semget (get)");
             exit(1);
        }
    } else {
        // 3. 初始化信号量 (只有创建者需要做这一步)
        // 我们将其初始化为 1,表示资源可用 (互斥锁模式)
        su.val = 1;
        if (semctl(semid, 0, SETVAL, su) == -1) {
            perror("semctl init");
            exit(1);
        }
        printf("Semaphore created and initialized to 1.\n");
    }

    // --- 临界区测试开始 ---

    printf("Forking process...\n");
    pid_t pid = fork();

    if (pid < 0) {
        perror("fork");
        exit(1);
    }

    if (pid == 0) {
        // --- 子进程 ---
        printf("Child (PID:%d) trying to enter critical section...\n", getpid());
        p(semid); // 加锁
        
        // 临界区:模拟一些耗时操作,故意让它容易被打断
        printf("Child (PID:%d) IN critical section.\n", getpid());
        for(int i=0; i<3; i++) {
            printf("Child working... %d\n", i);
            sleep(1);
        }
        printf("Child (PID:%d) LEAVING critical section.\n", getpid());
        
        v(semid); // 解锁
        exit(0);
    } else {
        // --- 父进程 ---
        printf("Parent (PID:%d) trying to enter critical section...\n", getpid());
        p(semid); // 加锁
        
        // 临界区
        printf("Parent (PID:%d) IN critical section.\n", getpid());
        for(int i=0; i<3; i++) {
            printf("Parent working... %d\n", i);
            sleep(1);
        }
        printf("Parent (PID:%d) LEAVING critical section.\n", getpid());
        
        v(semid); // 解锁

        // 等待子进程结束
        wait(NULL);

        // --- 清理工作 ---
        printf("\nCleaning up semaphore...\n");
        // IPC_RMID 命令忽略第二个参数 semnum
        if (semctl(semid, 0, IPC_RMID) == -1) {
            perror("semctl RMID");
        }
        printf("Done.\n");
    }

    return 0;
}

2.posix信号量

头文件

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

所有 POSIX 信号量接口的返回值规则完全一致,无需单独记忆:

返回值 含义 错误处理
0 接口调用成功(比如信号量初始化成功、P/V 操作执行成功) 无需处理
-1 接口调用失败(比如信号量已初始化、操作未初始化的信号量、权限不足等) 全局变量errno会被设置错误码,可通过perror("接口名")打印具体错误原因
  1. 接口 1:sem_init(初始化无名信号量)

函数原型

cpp 复制代码
int sem_init(sem_t *sem, int pshared, unsigned int value);

接口整体作用

创建并初始化一个无名信号量 (区别于有名信号量sem_open,新手先掌握无名),为后续的 P/V 操作做准备。

返回值

  • 成功:0
  • 失败:-1(如value为负数、sem指针无效、信号量已初始化等)

参数作用(逐个拆解)

参数名 类型 具体作用 新手注意点
sem sem_t * 指向要初始化的信号量对象(sem_t是信号量句柄,无需关心内部结构) 必须传入有效的sem_t变量地址(如&g_sem),不能传NULL
pshared int 信号量的作用域:✅ 0:仅用于线程间 同步(最常用,如示例)✅ 非 0:用于进程间同步(需配合共享内存) 新手优先用0,进程间同步需额外处理共享内存,复杂度更高
value unsigned int 信号量的初始值 (≥0):✅ 0:初始无资源,调用sem_wait会直接阻塞✅ 1:常用作互斥(临界区保护)✅ N:允许 N 个线程 / 进程同时访问资源 不能传负数,否则sem_init直接失败
  1. 接口 2:sem_wait(P 操作 / 等待信号量)

函数原型

cpp 复制代码
int sem_wait(sem_t *sem);

接口整体作用

对信号量执行减 1 操作(P 操作),是信号量的核心 "等待 / 获取资源" 逻辑:

  • 如果信号量当前值 > 0:值减 1,函数立即返回(获取资源成功);

  • 如果信号量当前值 = 0:调用线程 / 进程阻塞 ,直到其他线程 / 进程调用sem_post让信号量值 > 0。

返回值

  • 成功:0(信号量值已减 1)

  • 失败:-1(如sem未初始化、被信号中断等)

参数作用

参数名 类型 具体作用
sem sem_t * 指向已通过sem_init初始化的信号量对象
  1. 接口 3:sem_post(V 操作 / 释放信号量)

函数原型

cpp 复制代码
int sem_post(sem_t *sem);

接口整体作用

对信号量执行加 1 操作(V 操作),是信号量的核心 "释放 / 唤醒" 逻辑:

  • 信号量值加 1;

  • 如果有线程 / 进程因调用sem_wait阻塞在该信号量上,会唤醒其中一个 等待的线程 / 进程(使其完成sem_wait的减 1 操作)。

返回值

  • 成功:0(信号量值已加 1)

  • 失败:-1(如sem未初始化、信号量值超出系统限制等)

参数作用

参数名 类型 具体作用
sem sem_t * 指向已通过sem_init初始化的信号量对象
  1. 接口 4:sem_destroy(销毁信号量)

函数原型

cpp 复制代码
int sem_destroy(sem_t *sem);

接口整体作用

释放信号量对象占用的系统资源(内存、内核结构体等),是信号量的 "收尾操作":

  • 必须在信号量不再使用时调用;

  • 调用后,该信号量不能再被sem_wait/sem_post操作,除非重新sem_init

返回值

  • 成功:0(资源释放成功)

  • 失败:-1(如sem未初始化、还有线程阻塞在该信号量上)

参数作用

参数名 类型 具体作用
sem sem_t * 指向已通过sem_init初始化的信号量对象

4.sem_trywait ------ 非阻塞等待信号量

作用 :非阻塞版本的 sem_wait,尝试对信号量执行 P 操作(减 1)。如果信号量值 > 0,就减 1 并立即返回;如果信号量值 = 0,不会阻塞,而是直接返回错误。

  • 函数原型

    cpp 复制代码
    int sem_trywait(sem_t *sem);
  • 参数

    • sem:指向要操作的信号量对象(sem_t 类型指针)。
  • 返回值

    • 成功:返回 0,信号量值减 1。
    • 失败:返回 -1,并设置 errno
      • EAGAIN:信号量值为 0,无法执行 P 操作(非阻塞的核心体现)。
      • EINVALsem 指向的不是一个有效的已初始化信号量。
      • EINTR:操作被信号中断。

  1. sem_open ------ 创建 / 打开有名信号量

作用 :创建或打开一个有名信号量(用于进程间同步)。有名信号量在文件系统中有对应的标识,不同进程可以通过相同的名称访问同一个信号量。

  • 函数原型

    cpp 复制代码
    sem_t *sem_open(const char *name, int oflag, mode_t mode, unsigned int value);
  • 参数

    • name:信号量的名称,必须以 / 开头(如 /my_sem),这是 POSIX 有名信号量的命名规范。
    • oflag:打开标志,常用值:
      • O_CREAT:如果信号量不存在,则创建它;如果已存在,则打开它。
      • O_EXCL:与 O_CREAT 一起使用,如果信号量已存在,则返回错误(确保创建新的信号量)。
      • O_RDWR:以读写方式打开(默认)。
    • mode:权限位(如 0644),仅在创建新信号量时有效,指定新信号量的访问权限。
    • value:信号量的初始值(≥0),仅在创建新信号量时有效。
  • 返回值

    • 成功:返回指向信号量对象的指针(sem_t *)。
    • 失败:返回 SEM_FAILED(一个特殊的无效指针),并设置 errno
      • EEXISTO_CREAT | O_EXCL 被指定,但信号量已存在。
      • ENOENTO_CREAT 未被指定,且信号量不存在。
      • EINVALname 无效或 value 超过系统限制。

  1. sem_close ------ 关闭有名信号量

作用:关闭当前进程对有名信号量的引用,释放相关资源。注意:这不会删除文件系统中的信号量标识,只是当前进程不再使用它。

  • 函数原型

    cpp 复制代码
    int sem_close(sem_t *sem);
  • 参数

    • sem:由 sem_open 返回的有效信号量指针。
  • 返回值

    • 成功:返回 0
    • 失败:返回 -1,并设置 errno
      • EINVALsem 不是一个有效的信号量指针。

  1. sem_unlink ------ 删除有名信号量

作用:删除文件系统中的有名信号量标识。当所有进程都关闭了对该信号量的引用后,信号量对象才会被真正销毁。

  • 函数原型

    cpp 复制代码
    int sem_unlink(const char *name);
  • 参数

    • name:要删除的有名信号量的名称(与 sem_open 中的 name 一致)。
  • 返回值

    • 成功:返回 0
    • 失败:返回 -1,并设置 errno
      • ENOENT:指定名称的信号量不存在。
      • EINVALname 无效。
cpp 复制代码
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <semaphore.h>  // POSIX信号量核心头文件
#include <fcntl.h>      // 包含O_CREAT、O_EXCL等标志

// POSIX信号量的P操作 (Wait/Decrement):获取锁
void p(sem_t *sem) {
    // sem_wait:信号量值减1,若值为0则阻塞(核心P操作)
    if (sem_wait(sem) == -1) {
        perror("P operation (sem_wait) failed");
        exit(1);
    }
}

// POSIX信号量的V操作 (Signal/Increment):释放锁
void v(sem_t *sem) {
    // sem_post:信号量值加1,唤醒阻塞的进程/线程(核心V操作)
    if (sem_post(sem) == -1) {
        perror("V operation (sem_post) failed");
        exit(1);
    }
}

int main() {
    // POSIX有名信号量的名称(必须以/开头,是系统级唯一标识)
    const char *sem_name = "/my_sem_66";
    sem_t *sem = NULL;

    // 1. 创建/打开POSIX有名信号量(进程间共享需用有名信号量)
    // O_CREAT | O_EXCL:确保创建新信号量,若已存在则报错
    // 0666:信号量文件权限,最后一个参数1是信号量初始值(互斥锁模式)
    sem = sem_open(sem_name, O_CREAT | O_EXCL, 0666, 1);
    if (sem == SEM_FAILED) {
        perror("sem_open (create) failed");
        // 如果是因为信号量已存在报错,先删除旧信号量再重新创建
        if (sem_unlink(sem_name) == -1) {
            perror("sem_unlink old semaphore failed");
            exit(1);
        }
        // 重新尝试创建
        sem = sem_open(sem_name, O_CREAT | O_EXCL, 0666, 1);
        if (sem == SEM_FAILED) {
            perror("sem_open (retry) failed");
            exit(1);
        }
    }
    printf("POSIX semaphore created and initialized to 1.\n");

    // --- 临界区测试开始 ---
    printf("Forking process...\n");
    pid_t pid = fork();

    if (pid < 0) {
        perror("fork failed");
        sem_close(sem);    // 失败时关闭信号量
        sem_unlink(sem_name); // 失败时删除信号量
        exit(1);
    }

    if (pid == 0) {
        // --- 子进程 ---
        printf("Child (PID:%d) trying to enter critical section...\n", getpid());
        p(sem); // 加锁(P操作:信号量值减1)
        
        // 临界区:模拟耗时操作,确保互斥访问
        printf("Child (PID:%d) IN critical section.\n", getpid());
        for(int i=0; i<3; i++) {
            printf("Child working... %d\n", i);
            sleep(1);
        }
        printf("Child (PID:%d) LEAVING critical section.\n", getpid());
        
        v(sem); // 解锁(V操作:信号量值加1)
        sem_close(sem); // 子进程关闭信号量句柄
        exit(0);
    } else {
        // --- 父进程 ---
        printf("Parent (PID:%d) trying to enter critical section...\n", getpid());
        p(sem); // 加锁(P操作:信号量值减1)
        
        // 临界区
        printf("Parent (PID:%d) IN critical section.\n", getpid());
        for(int i=0; i<3; i++) {
            printf("Parent working... %d\n", i);
            sleep(1);
        }
        printf("Parent (PID:%d) LEAVING critical section.\n", getpid());
        
        v(sem); // 解锁(V操作:信号量值加1)

        // 等待子进程结束
        wait(NULL);

        // --- 清理工作 ---
        printf("\nCleaning up POSIX semaphore...\n");
        // 关闭信号量句柄(释放当前进程的引用)
        if (sem_close(sem) == -1) {
            perror("sem_close failed");
        }
        // 删除有名信号量(系统级清理,彻底移除信号量)
        if (sem_unlink(sem_name) == -1) {
            perror("sem_unlink failed");
        }
        printf("Done.\n");
    }

    return 0;
}
相关推荐
牛奶咖啡131 小时前
DevOps自动化运维实践_使用再生龙对Linux系统进行备份还原
运维·自动化·devops·linux系统的备份还原·linux系统克隆备份·再生龙
njsgcs1 小时前
最小化终端 到托盘 minimizeToNotificationArea
运维
2401_849339171 小时前
nginx
运维·nginx
再战300年1 小时前
Samba在ubuntu上安装部署
linux·运维·ubuntu
雨落花开3231 小时前
服务器集群,负载均衡,CDN简介
运维·服务器·负载均衡
晚秋大魔王1 小时前
ubutnu 服务器配置openclaw 使用阿里云百炼模型
运维·服务器·阿里云
勇闯逆流河2 小时前
【Linux】基础开发工具(软件包、vim)
linux·运维·服务器
岳清源2 小时前
【无标题】Keepalived
linux·服务器·网络
先做个垃圾出来………2 小时前
Python常见文件操作
linux·数据库·python