进程通信之信号量

文章目录

信号量

  • 一个由内核维护的整数,其值被限定为大于或等于0,任何试图将信号量值降低到0之下的操作都会被阻塞

P/V 操作

  • P操作 (Proberen):测试/等待,减少信号量值,阻塞
  • V操作 (Verhogen):增加,释放资源,增加信号量值
  • 操作是原子的,保证线程安全
  • 内核会将所有试图将信号量值降低到0之下的操作阻塞

使用场景

  • 保护临界区(互斥)
  • 限制并发访问数量
  • 实现生产者-消费者模型
  • 同步多个进程/线程

System V 信号量

核心函数

创建/获取信号量集

c 复制代码
#include <sys/ipc.h>
#include <sys/sem.h>

int semget(key_t key, int nsems, int semflg);
  • key:用于标识系统级唯一信号量集的键值(本质是 int 类型),不同进程通过相同 key 可访问同一个信号量集(IPC_PRIVATE 或 ftok() 生成)
  • nsems:指定信号量集中包含的信号量个数(非负整数)
  • semflg:操作标志,可通过按位或组合多个标志,分为「控制标志」和「权限标志」两类
    • IPC_CREAT:若 key 对应的信号量集不存在则创建,已存在则直接获取
    • IPC_EXCL:与 IPC_CREAT 配合使用,若信号量集已存在则返回错误,保证创建的是全新资源
    • 权限标志(占低 9 位,与文件权限格式一致):例如 0644(所有者读写、同组读、其他读)、0666(所有用户读写),控制不同进程对信号量集的访问权限
  • 返回值:
    • 成功:返回一个有效的 信号量集 ID(semid,非负整数),后续 semctl()、semop() 操作均依赖该 ID
    • 失败:返回 -1,同时设置全局变量 errno 指示错误原因(例如 EEXIST:IPC_CREAT|IPC_EXCL 时信号量集已存在;ENOENT:无对应 key 的集且未指定 IPC_CREAT;ENOMEM:系统无足够资源创建新集)

信号量原子操作

c 复制代码
struct sembuf {
    unsigned short sem_num;  // 信号量编号
    short sem_op;            // 操作值(-1:P操作,+1:V操作)
    short sem_flg;           // 标志(SEM_UNDO, IPC_NOWAIT)
};

int semop(int semid, struct sembuf *sops, size_t nsops);
  • semid:由 semget() 成功返回的信号量集 ID,标识要操作的目标信号量集
  • sops:指向 struct sembuf 结构体数组的指针,每个结构体描述一个单独的信号量操作
    • sem_num:信号量在集中的编号(从 0 开始,例如 0 表示第一个信号量)
    • sem_op:
      • 正数:将 sem_op 的值加到信号量的当前值上(释放资源,例如 sem_op=1 对应 V 操作)
      • 0:等待信号量的当前值变为 0(直到满足条件,或被信号中断)
      • 负数:尝试将信号量当前值减去 sem_op 的绝对值(获取资源,例如 sem_op=-1 对应 P 操作),若信号量当前值小于该绝对值,进程会阻塞(直到信号量值满足条件)
    • sem_flg:
      • 0(默认):阻塞模式,操作无法立即完成时进程挂起等待
      • IPC_NOWAIT:非阻塞模式,操作无法立即完成时直接返回错误(errno=EAGAIN)
      • SEM_UNDO:进程退出时,内核自动撤销该进程对信号量的所有操作(避免进程异常退出导致信号量处于无效状态,防止死锁
  • nsops:指定 sops 数组中操作结构体的个数(即要执行的原子操作数量,必须 ≥ 1),semop() 对 sops 数组中的所有操作执行「原子操作」------ 要么所有操作全部执行完成,要么一个都不执行,不会出现部分执行的中间状态,这是实现进程同步 / 互斥的关键
  • 返回值:
    • 返回 0,表示 sops 数组中的所有信号量操作已原子性执行完成
    • 失败:返回 -1,同时设置全局变量 errno 指示错误原因(例如 EINVAL:无效的 semid 或 sem_num;EAGAIN:IPC_NOWAIT 模式下操作无法立即完成;EIDRM:信号量集已被删除;EINTR:阻塞等待时被信号中断)

一个 semop() 调用可同时操作多个信号量(通过 nsops 指定),原子性保证了复杂同步场景的安全性

控制操作

  • 信号量集控制操作,含初始化、删除、查询
c 复制代码
union semun {
    int val;                    // SETVAL 时使用
    struct semid_ds *buf;       // IPC_STAT/IPC_SET 时使用
    unsigned short *array;      // GETALL/SETALL 时使用
};

int semctl(int semid, int semnum, int cmd, ...);
  • semid:由 semget() 成功返回的信号量集 ID,标识要进行控制操作的目标信号量集
  • semnum:指定信号量集中要操作的单个信号量编号(从 0 开始,对应 semget() 创建时的信号量顺序)
  • cmd:要执行的控制命令(系统定义宏),分为「单个信号量命令」和「整个信号量集命令」两类:
    • 单个信号量命令(常用):
      • SETVAL:设置单个信号量的初始值(或当前值),需配合第 4 个可变参数(union semun 类型)传递要设置的值
      • GETVAL:获取单个信号量的当前值,返回值即为该信号量的值(无需第 4 个参数)
    • 整个信号量集命令(常用):
      • IPC_RMID:删除整个信号量集(最核心的删除命令),此时无需第 4 个参数(可设为 NULL)
      • IPC_STAT:查询信号量集的属性信息,将属性写入第 4 个参数(union semun 中的 buf 成员
      • IPC_SET:修改信号量集的属性(仅能修改权限相关字段),需通过第 4 个参数传递新属性
    • 其他少见命令:GETPID(获取最后操作该信号量的进程 ID)、GETNCNT(获取等待信号量值增加的进程数)、SEMA(设置整个信号量集的所有值)

示例:进程间互斥

c 复制代码
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/sem.h>

#define SEM_KEY 1234  // 信号量的关键字

// 信号量操作结构体
struct sembuf sb;

// 创建一个信号量
int create_semaphore() {
    int semid = semget(SEM_KEY, 1, IPC_CREAT | 0666);  // 创建信号量集
    if (semid == -1) {
        perror("semget");
        exit(1);
    }
    
    // 初始化信号量的值为 1(表示资源可用)
    union semun {
        int val;
    } arg;
    arg.val = 1;
    
    if (semctl(semid, 0, SETVAL, arg) == -1) {
        perror("semctl");
        exit(1);
    }
    
    return semid;
}

// P 操作(等待信号量,减小信号量的值)
void P(int semid) {
    sb.sem_num = 0;
    sb.sem_op = -1;  // 对信号量做减操作
    sb.sem_flg = SEM_UNDO;
    
    if (semop(semid, &sb, 1) == -1) {
        perror("semop - P");
        exit(1);
    }
}

// V 操作(释放信号量,增加信号量的值)
void V(int semid) {
    sb.sem_num = 0;
    sb.sem_op = 1;   // 对信号量做加操作
    sb.sem_flg = SEM_UNDO;
    
    if (semop(semid, &sb, 1) == -1) {
        perror("semop - V");
        exit(1);
    }
}

// 进程对共享资源的操作
void critical_section(int semid) {
    P(semid);  // 等待信号量,进入临界区
    
    printf("Process %d is in critical section.\n", getpid());
    
    printf("Process %d is leaving critical section.\n", getpid());
    V(semid);  // 释放信号量,离开临界区
}

int main() {
    int semid = create_semaphore();  // 创建信号量
    
    if (fork() == 0) {
        // 子进程
        critical_section(semid);
        exit(0);
    } else {
        // 父进程
        critical_section(semid);
        wait(NULL);  // 等待子进程完成
    }
    
    // 删除信号量
    if (semctl(semid, 0, IPC_RMID, 0) == -1) {
        perror("semctl - IPC_RMID");
        exit(1);
    }
    
    return 0;
}

POSIX 信号量

命名信号量

  • 命名信号量通过文件系统路径标识,可用于无关进程间同步
  • 命名信号量由以下形式的名称标识 /somename
  • 具有内核持久性,虚拟文件系统/dev/shm

核心函数

创建/打开
c 复制代码
#include <semaphore.h>
#include <fcntl.h>

// 仅打开已存在的 POSIX 命名信号量
sem_t *sem_open(const char *name, int oflag);
// 打开或创建新的 POSIX 命名信号量,带权限和初始值配置
sem_t *sem_open(const char *name, int oflag, 
                mode_t mode, unsigned int value);
  • name:POSIX 命名信号量的系统级唯一标识名,命名规则严格且固定,必须以 / 开头,且后续字符串中不能再包含其他
  • oflag: 打开 / 创建标志, 可通过 按位或组合多个标志,核心分为「访问标志」和「创建辅助标志」两类
    • 核心基础标志(无额外扩展,仅用于打开已存在信号量)传入 0 即可(表示以默认模式打开已存在的信号量)
    • 可选创建 / 辅助标志(核心):
      • O_CREAT:若 name 对应的信号量不存在,则创建新的信号量;若已存在,则直接打开该信号量(此时忽略 mode 和 value 参数)
      • O_EXCL:必须与 O_CREAT 配合使用,若 name对应的信号量已存在,则返回错误(避免重复创建,保证创建的是全新的信号量,防止覆盖已有资源)

注意:若使用O_CREAT标志,必须使用第二个重载原型,传入mode和value参数

  • mode:仅当 oflag 包含 O_CREAT 时有效,用于指定新创建信号量的访问权限,取值格式与 open()、shm_open()、mq_open() 等函数的权限一致
    • 0644:所有者读写、同组用户读、其他用户读
    • 0666:所有用户读写(最常用,适用于进程间共享访问)
    • 最终实际权限会被进程的 umask 掩码修正(实际权限 = mode & ~umask)
  • value:仅当 oflag 包含 O_CREAT 且信号量不存在(需要创建新信号量)时有效,用于指定新信号量的初始值。表示当前可用的「资源数量」,用于同步 / 互斥场景(例如初始值为 1 用于实现互斥锁,初始值为 n 用于实现 n 个共享资源的分配)

信号量创建后,初始值仅通过 value 设定一次,后续需通过 sem_post()(增加值)、sem_wait()(减少值)修改,无法再通过 sem_open() 重置

操作函数
c 复制代码
// 等待(P操作)
int sem_wait(sem_t *sem);        // 阻塞
int sem_trywait(sem_t *sem);     // 非阻塞
// 带绝对超时的信号量等待操作
int sem_timedwait(sem_t *sem, const struct timespec *abs_timeout);

// 释放(V操作)
int sem_post(sem_t *sem);

// 获取当前值
int sem_getvalue(sem_t *sem, int *sval);

// 关闭和删除
int sem_close(sem_t *sem);
int sem_unlink(const char *name);
  • abs_timeout:指向 struct timespec 结构体的指针,指定绝对超时时间点
  • sval:指向 int 类型变量的有效指针,用于存储查询到的信号量当前值
  • name:与 sem_open() 中一致的POSIX 命名信号量唯一标识名,必须符合合法命名规则

命名信号量示例:

c 复制代码
// 进程1:创建并初始化
sem_t *sem = sem_open("/mysem", O_CREAT, 0666, 1);
sem_wait(sem);
// 临界区操作
sem_post(sem);
sem_close(sem);

// 进程2:打开并使用
sem_t *sem = sem_open("/mysem", 0);
sem_wait(sem);
// 临界区操作
sem_post(sem);
sem_close(sem);
sem_unlink("/mysem");  // 最后一个进程删除

匿名信号量

  • 匿名信号量存在于内存中,用于相关进程或线程间同步

核心函数

初始化与销毁
c 复制代码
int sem_init(sem_t *sem, int pshared, unsigned int value);
int sem_destroy(sem_t *sem);
  • sem:指向 sem_t 类型信号量对象的有效指针,用于存储初始化后的信号量数据,该指针指向的内存空间必须提前分配(可声明为局部变量、全局变量或通过 malloc() 动态分配),禁止传入 NULL
  • pshared:指定信号量的共享范围,用于区分「线程间共享」和「进程间共享」,仅支持两个核心取值(非 0 或 0)
    • 0(默认且最常用):信号量为线程内共享(私有),仅适用于同一进程内的多个线程间同步 / 互斥;此时 sem 通常指向进程内的全局变量或线程间可访问的共享内存(如堆内存)
    • 非 0(通常为 1):信号量为进程间共享,适用于不同进程间的同步 / 互斥;此时 sem 必须指向「共享内存区域」,否则不同进程无法访问到同一个 sem_t 对象
  • pshared:0表示线程间,非0表示进程间
  • value:指定无名信号量的初始值,表示当前可用的资源数量,常用取值:1(用于实现互斥锁,保证同一时间只有一个线程 / 进程访问临界资源)、n(用于实现 n 个共享资源的分配),不能超过系统定义的 SEM_VALUE_MAX(通常为 32767,可在 <semaphore.h> 中查看宏定义

匿名信号示例:

c 复制代码
#include <sys/mman.h>
#include <semaphore.h>

struct shared_data {
    sem_t sem;
    int value;
} *shm_data;

// 在共享内存中初始化
sem_init(&shm_data->sem, 1, 1);  // 1表示进程间共享

// 使用
sem_wait(&shm_data->sem);
shm_data->value++;
sem_post(&shm_data->sem);

// 清理
sem_destroy(&shm_data->sem);

信号量解决共享内存同步问题

问题分析

  • 多个进程同时访问共享内存会导致:
    • 数据竞争(Data Race)
    • 数据不一致
    • 不可预测的行为

解决方案

使用命名信号量

c 复制代码
#include <sys/ipc.h>
#include <sys/shm.h>
#include <semaphore.h>

int main() {
    // 1. 创建共享内存
    int shmid = shmget(ftok(".", 65), 1024, IPC_CREAT | 0666);
    int *data = shmat(shmid, NULL, 0);
    
    // 2. 创建/打开命名信号量
    sem_t *sem = sem_open("/shm_sem", O_CREAT, 0666, 1);
    
    // 3. 同步访问
    sem_wait(sem);
    (*data)++;  // 安全操作
    sem_post(sem);
    
    // 4. 清理
    shmdt(data);
    sem_close(sem);
    sem_unlink("/shm_sem");
    
    return 0;
}

使用匿名信号量

c 复制代码
#include <sys/ipc.h>
#include <sys/shm.h>
#include <semaphore.h>

struct shared_memory {
    sem_t lock;      // 信号量
    int counter;     // 共享数据
};

int main() {
    // 1. 创建包含信号量的共享内存
    int shmid = shmget(ftok(".", 66), sizeof(struct shared_memory), 
                       IPC_CREAT | 0666);
    struct shared_memory *shm = shmat(shmid, NULL, 0);
    
    // 2. 初始化信号量(在第一个进程中)
    sem_init(&shm->lock, 1, 1);  // 进程间共享,初始值1
    
    // 3. 使用
    sem_wait(&shm->lock);
    shm->counter++;
    sem_post(&shm->lock);
    
    // 4. 清理
    sem_destroy(&shm->lock);
    shmdt(shm);
    shmctl(shmid, IPC_RMID, NULL);
    
    return 0;
}

线程同步工具在进程间应用

基本原理

  • 默认情况下,互斥量用于同一进程内的线程同步
  • 在初始化时设置其属性为PTHREAD_PROCESS_SHARED。可以实现多个进程通过共享内存来访问同一个互斥量,从而实现进程间的同步

互斥锁(Mutex)进程间使用

c 复制代码
#include <pthread.h>
#include <sys/mman.h>

typedef struct {
    pthread_mutex_t mutex;
    int data;
} shared_data_t;

int main() {
    // 创建共享内存
    shared_data_t *shm = mmap(NULL, sizeof(shared_data_t),
                               PROT_READ | PROT_WRITE,
                               MAP_SHARED | MAP_ANONYMOUS, -1, 0);
    
    // 设置互斥锁属性为进程间共享
    pthread_mutexattr_t attr;
    pthread_mutexattr_init(&attr);
    pthread_mutexattr_setpshared(&attr, PTHREAD_PROCESS_SHARED);
    
    // 初始化互斥锁
    pthread_mutex_init(&shm->mutex, &attr);
    
    // 使用
    pthread_mutex_lock(&shm->mutex);
    shm->data++;
    pthread_mutex_unlock(&shm->mutex);
    
    // 清理
    pthread_mutex_destroy(&shm->mutex);
    munmap(shm, sizeof(shared_data_t));
    
    return 0;
}

条件变量(Condition Variable)进程间使用

  • 条件变量通常与互斥量配合使用,用于线程间的同步
  • 与共享内存结合以后可以变成进程间的同步工具
  • 具体而言,可以在共享内存中创建条件变量,并确保相关的互斥量也设置为进程间共享。多个进程可以通过共享内存中的条件变量来进行同步
c 复制代码
#include <pthread.h>
#include <sys/mman.h>

typedef struct {
    pthread_mutex_t mutex;
    pthread_cond_t cond;
    int ready;
} shared_data_t;

int main() {
    shared_data_t *shm = mmap(NULL, sizeof(shared_data_t),
                               PROT_READ | PROT_WRITE,
                               MAP_SHARED | MAP_ANONYMOUS, -1, 0);
    
    // 设置互斥锁和条件变量为进程间共享
    pthread_mutexattr_t m_attr;
    pthread_mutexattr_init(&m_attr);
    pthread_mutexattr_setpshared(&m_attr, PTHREAD_PROCESS_SHARED);
    pthread_mutex_init(&shm->mutex, &m_attr);
    
    pthread_condattr_t c_attr;
    pthread_condattr_init(&c_attr);
    pthread_condattr_setpshared(&c_attr, PTHREAD_PROCESS_SHARED);
    pthread_cond_init(&shm->cond, &c_attr);
    
    shm->ready = 0;
    
    if (fork() == 0) {
        // 子进程:发送信号
        pthread_mutex_lock(&shm->mutex);
        shm->ready = 1;
        pthread_cond_signal(&shm->cond);
        pthread_mutex_unlock(&shm->mutex);
        exit(0);
    } else {
        // 父进程:等待
        pthread_mutex_lock(&shm->mutex);
        while (shm->ready == 0) {
            pthread_cond_wait(&shm->cond, &shm->mutex);
        }
        pthread_mutex_unlock(&shm->mutex);
    }
    
    // 清理
    pthread_cond_destroy(&shm->cond);
    pthread_mutex_destroy(&shm->mutex);
    munmap(shm, sizeof(shared_data_t));
    
    return 0;
}

读写锁(RW Lock)进程间使用

  • 读写锁在Linux中主要用于线程间的同步
  • 要在进程间使用读写锁,需要将其与共享内存结合使用,并确保相关的互斥量设置为进程间共享
  • 通过这种方式,多个进程可以通过共享内存中的读写锁来进行同步
c 复制代码
#include <pthread.h>
#include <sys/mman.h>

typedef struct {
    pthread_rwlock_t rwlock;
    int data;
} shared_data_t;

int main() {
    shared_data_t *shm = mmap(NULL, sizeof(shared_data_t),
                               PROT_READ | PROT_WRITE,
                               MAP_SHARED | MAP_ANONYMOUS, -1, 0);
    
    pthread_rwlockattr_t attr;
    pthread_rwlockattr_init(&attr);
    pthread_rwlockattr_setpshared(&attr, PTHREAD_PROCESS_SHARED);
    pthread_rwlock_init(&shm->rwlock, &attr);
    
    // 读锁
    pthread_rwlock_rdlock(&shm->rwlock);
    // 读操作
    pthread_rwlock_unlock(&shm->rwlock);
    
    // 写锁
    pthread_rwlock_wrlock(&shm->rwlock);
    shm->data = 100;
    pthread_rwlock_unlock(&shm->rwlock);
    
    pthread_rwlock_destroy(&shm->rwlock);
    munmap(shm, sizeof(shared_data_t));
    
    return 0;
}

进程通信与同步工具总结

通信工具对比

工具 通信方式 使用场景 特点
管道 字节流 相关进程 简单,单向
命名管道 字节流 任意进程 文件系统可见
消息队列 消息 任意进程 结构化,支持类型
共享内存 内存 任意进程 最高性能,需同步
信号 信号值 任意进程 异步,轻量级

同步工具对比

工具 类型 适用范围 特点
System V信号量 内核 进程间 功能强大,复杂
POSIX命名信号量 文件系统 进程间 简单,易用
POSIX匿名信号量 内存 线程/进程间 灵活,需共享内存
互斥锁(进程间) 内存 进程间 互斥访问
条件变量(进程间) 内存 进程间 条件等待
读写锁(进程间) 内存 进程间 读写分离
相关推荐
我在人间贩卖青春4 天前
进程通信之管道
进程通信·管道·命名管道·无名管道
努力的小帅19 天前
Linux_进程间通信(Linux入门到精通)
linux·c++·centos·共享内存·进程通信·命名管道·管道的学习
为什么要做囚徒1 个月前
并发系列(一):深入理解信号量(含 Redis 分布式信号量)
redis·分布式·多线程·并发编程·信号量
添砖java‘’1 个月前
常见的进程间通信方式详解
linux·c++·操作系统·信息与通信·进程通信
赖small强2 个月前
Linux 内核 8 类同步机制详解(原理、场景与示例)
linux·信号量·原子操作·自旋锁·内核同步方法·读-写自旋锁·读-写信号量
阿巴~阿巴~2 个月前
Linux同步机制:POSIX 信号量 与 SystemV信号量 的 对比
linux·服务器·线程·信号量·线程同步·posix·system v
NiKo_W3 个月前
Linux 进程通信——基于责任链模式的消息队列
linux·服务器·消息队列·责任链模式·进程通信
cccyi73 个月前
Linux 进程间通信机制详解
linux·进程通信
egoist20233 个月前
[linux仓库]System V 进程通信详解:System V消息队列、信号量
linux·c语言·消息队列·pv·信号量