进程间通信方式(四):System V 信号量

本文深入解析信号量的概念、特点及数据结构,详细介绍信号量相关函数semget、semctl和semop的功能、参数与使用示例,帮助读者掌握信号量在进程间通信中的应用。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

一、概念

  信号量是一种用于控制多进程访问共享资源的同步互斥机制,它是以集合的形式存在,一个信号量集可以包含多个信号量。此外,每个信号量都有一个整数值,表示可用资源的数量,进程就是通过对信号量值进行减少和增加来获取和释放资源的。

二、特点

  1. 功能强大,支持信号量集合

  2. 操作是原子性的,适合复杂同步场景

  3. 信号量集存在于内核中,不依赖于单个进程的生命周期,需要显式删除或系统重启才会消失

  4. 在 Linux 中,信号量有如下限制:

    (1)SEMMNI :系统范围内信号量集的最大数量
    (2)SEMMSL:单个信号量集中的信号量数量上限
    (3)SEMMNS:所有信号量集中的信号量总数上限
    (4)SEMOPM:单个 semop 调用能执行的最大操作数

    查看方法如下:
    方法一:通过 ipcs -ls 命令查看

    在这里插入图片描述
    方法二:查看系统内核信息(依次是:SEMMSLSEMMNSSEMOPMSEMMNI

    在这里插入图片描述

三、信号量数据结构

  信号量集具有自身特有的数据结构 semid_ds,该结构描述了信号量集的一些属性和状态信息等,详细信息可参阅文件 /usr/include/linux/sem.h

struct semid_ds {    
    struct ipc_perm 	sem_perm;       		/* permissions .. see ipc.h */    
    __kernel_time_t 	sem_otime;      		/* last semop time */    
    __kernel_time_t 	sem_ctime;      		/* last change time */    
    struct sem			*sem_base;      		/* ptr to first semaphore in array */    
    struct sem_queue 	*sem_pending;      		/* pending operations to be processed */    
    struct sem_queue 	**sem_pending_last;		/* last pending operation */    
    struct sem_undo 	*undo;					/* undo requests on this array */    
    unsigned short  	sem_nsems;				/* no. of semaphores in array */    
};

四、信号量相关函数

1. semget

  • 【头文件】:#include <sys/types.h>#include <sys/ipc.h>#include <sys/sem.h>
  • 【函数原型】:int semget(key_t key, int nsems, int semflg);
  • 【功能】:创建或访问一个信号量集
  • 【参数】:

(1)key:信号量集的键值
(2)nsems:信号量集中信号量的数量
(3)semflg:权限标志位,由两部分组成,一部分为IPC对象存取权限(含义同 ipc_perm 中的 mode),另一部分为IPC对象创建模式标志(IPC_CREAT、IPC_EXCL),一般会将这两部分进行 | 运算,从而完成对IPC对象创建的管理

  • 【返回值】:成功返回一个信号量集标识符(非负整数);失败则返回 -1,并将 errno 设置为错误标识符

在这里插入图片描述

创建或打开一个 IPC 对象的逻辑流程图

【示例】:演示 IPC_CREAT|IPC_EXCL 的使用效果
【代码】

#include <stdio.h>    
#include <sys/types.h>    
#include <sys/ipc.h>    
#include <sys/sem.h>    
#include <errno.h>    
    
int main() {    
    extern int errno;    
    
    int semid = semget(0x123, 1, IPC_CREAT|IPC_EXCL|0644);                                       
    if (semid == -1) {    
        perror("semget");    
        printf("errno: %d\n", errno);    
        return -1;    
    }    
    
    printf("semid: %d\n", semid);    
    
    return 0;    
}

【执行结果】

在这里插入图片描述
【分析】
  从执行结果来看,当我们第一次运行程序时,成功创建了信号量集。但是,当第二次运行程序后,发现创建失败,errno 被置为17,也就是 EEXIST,错误信息为 File exists,出现这样的结果是因为我们在第一次程序运行后,该信号量集就已经在系统中存在了,而我们在创建信号量集时又使用了 IPC_EXCL,这才导致 semget 出错返回。

2. semctl

  • 【头文件】:#include <sys/types.h>#include <sys/ipc.h>#include <sys/sem.h>
  • 【函数原型】:int semctl(int semid, int semnum, int cmd, ...);
  • 【功能】:获取或设置信号量集的有关信息
  • 【参数】:

(1)semid:信号量集标识符
(2)semnum:信号量集中的哪一个信号量(编号从0开始)
(3)cmd:要执行的操作

取值说明
GETVAL获取指定信号量的值(通过函数返回)
SETVAL设置指定信号量的值(取值来自于 val)
IPC_STAT获取信号量集的 semid_ds 结构信息(保存在 buf 中)
IPC_SET设置信号量集的 semid_ds 结构信息(取值来自于 buf)
GETALL获取信号量集中所有信号量的值(保存在 array 中)
SETALL设置信号量集中所有信号量的值(取值来自于 array)
IPC_INFO获取系统范围内信号量的限制信息(保存在 __buf 中)
IPC_RMID立即删除信号量集

(4) :这个参数取决于cmd,如果存在,则定义如下:

union semun {
	int              val;    /* Value for SETVAL */
	struct semid_ds *buf;    /* Buffer for IPC_STAT, IPC_SET */
	unsigned short  *array;  /* Array for GETALL, SETALL */
	struct seminfo  *__buf;  /* Buffer for IPC_INFO (Linux-specific) */
}
  • 【返回值】:成功时具体的返回值依赖于cmd;失败则返回 -1,并将errno设置为错误标识符

【示例】:演示 IPC_STATIPC_INFO 的使用效果
【代码】

#define _GNU_SOURCE

#include <stdio.h>  
#include <sys/types.h>  
#include <sys/ipc.h>        
#include <sys/sem.h>      
                          
int main() {  
    int semid = semget(0x123, 5, IPC_CREAT|0644);  
    if (semid == -1) {                                 
        perror("semget");   
        return -1;             
    }                   
    printf("semid: %d\n", semid);  
                                       
    struct semid_ds buf;  
    if (semctl(semid, 0, IPC_STAT, &buf) == -1) {  
        perror("semctl");                              
        return -1;             
    }                   
    printf("key: %#x\n", buf.sem_perm.__key);  
    printf("mode: %#o\n", buf.sem_perm.mode);      
    printf("nsems: %lu\n", buf.sem_nsems);         
                                                
   struct seminfo __buf;  
   if (semctl(semid, 0, IPC_INFO, &__buf) == -1) {  
       perror("semctl");                            
       return -1;             
   }                   
   printf("semmni: %d\n", __buf.semmni);  
   printf("semmsl: %d\n", __buf.semmsl);      
   printf("semmns: %d\n", __buf.semmns);      
   printf("semopm: %d\n", __buf.semopm);      
                                              
    return 0;  
}

【执行结果】

在这里插入图片描述

3. semop

  • 【头文件】:#include <sys/types.h>#include <sys/ipc.h>#include <sys/sem.h>
  • 【函数原型】:int semop(int semid, struct sembuf *sops, unsigned nsops);
  • 【功能】:对信号量集中的信号量进行操作
  • 【参数】:

(1)semid:信号量集标识符
(2)sops:指向 sembuf 结构体数组的指针,每个结构体描述一个要执行的操作
(3)nsops:sops 数组中操作的数量

  • 【返回值】:成功时返回 0;失败则返回 -1,并将errno设置为错误标识符

五、信号量操作

1. P 操作

  • 属于原子操作,源于荷兰语 Passeren
  • 行为:
    • 获取资源:减少信号量的值
    • 等待:如果信号量值小于要减去的值,进程会阻塞

2. V 操作

  • 属于原子操作,源于荷兰语 Vrijgeven
  • 行为:
    • 释放资源:增加信号量的值
    • 发信号:如果有进程因该信号量被阻塞,会唤醒其中一个

3. 操作数据结构

struct sembuf {
	unsigned short sem_num;		/* semaphore index in array */
	short sem_op;				/* semaphore operation */
	short sem_flg;				/* operation flags */
};
  • sem_num:操作信号量集中的哪一个信号量(编号从0开始)
  • sem_op:操作值
    • 负数:将信号量的值减少,通常用于获取资源,也就是P操作
    • 0:等待信号量的值变为零
    • 正数:将信号量的值增加,通常用于释放资源,也就是V操作
  • sem_flg:操作标志
    • 0:阻塞模式(默认行为)
    • IPC_NOWAIT:非阻塞模式(如果信号量操作不能立即执行,不阻塞进程,而是返回错误)
    • SEM_UNDO:进程退出时撤销(内核会记录该进程对信号量的修改,当进程正常或异常退出时,自动撤销这些修改,确保进程崩溃不会导致死锁)

【示例代码】

struct sembuf p_op = {0, -1, 0};	//P操作结构
struct sembuf v_op = {0,  1, 0};	//V操作结构

六、综合应用

示例一

【功能】:模拟多个进程竞争资源,利用信号量实现互斥
【代码】

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

#define PATHNAME "."
#define PROJ_ID 123
#define CHILD_NUM 3

int main() {
    //生成IPC键值
    key_t key = ftok(PATHNAME, PROJ_ID);
    if (key < 0) {
        perror("ftok");
        return -1;
    }

    //创建含有1个信号量的信号量集
    int semid = semget(key, 1, IPC_CREAT|IPC_EXCL|0644);
    if (semid < 0) {
        perror("semget");
        return -1;
    }

    //设置信号量值为1
    if (semctl(semid, 0, SETVAL, 1) < 0) {
        perror("semctl(SETVAL)");
        return -1;
    }

    //定义信号量操作结构
    struct sembuf p_op = {0, -1, SEM_UNDO}; //P操作结构
    struct sembuf v_op = {0,  1, SEM_UNDO}; //V操作结构

    //创建多个子进程竞争资源
    int i = 0;
    for (i = 0; i < CHILD_NUM; ++i) {
        pid_t pid = fork();
        if (pid < 0) {
            perror("fork");
            return -1;
        }
        else if (pid == 0) {
            char child = 'A' + i;
            printf("进程(%c)等待获取资源中...\n", child);
            if (semop(semid, &p_op, 1) < 0) {
                perror("semop(P)");
                exit(EXIT_FAILURE);
            }
            
            printf("进程(%c)获取到资源,执行任务中...\n", child);
            sleep(3);
            printf("进程(%c)任务完成,释放资源中...\n", child);
            
            if (semop(semid, &v_op, 1) < 0) {
                perror("semop(V)");
                exit(EXIT_FAILURE);
            }
            printf("进程(%c)资源释放完成!\n", child);
            exit(EXIT_SUCCESS);
        }
    }

    //等待所有子进程退出
    for (i = 0; i < CHILD_NUM; ++i) {
        wait(NULL);
    }

    //删除信号量集
    if (semctl(semid, 0, IPC_RMID) < 0) {
        perror("semctl(IPC_RMID)");
        return -1;
    }

    return 0;
}

【执行结果】

在这里插入图片描述

示例二

【功能】:模拟多个进程按照顺序输出,利用信号量实现同步与互斥
【代码】

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

#define PATHNAME "."
#define PROJ_ID 123
#define COUNT 3
#define PRINT_COUNT 3

int main() {
    //生成IPC键值
    key_t key = ftok(PATHNAME, PROJ_ID);
    if (key < 0) {
        perror("ftok");
        return -1;
    }

    //创建含有COUNT个信号量的信号量集
    int semid = semget(key, COUNT, IPC_CREAT|IPC_EXCL|0644);
    if (semid < 0) {
        perror("semget");
        return -1;
    }

    //设置各个信号量值
    int semval[COUNT] = {1, 0, 0};
    if (semctl(semid, 0, SETALL, semval) < 0) {
        perror("semctl(SETALL)");
        return -1;
    }

    //定义每个进程的P操作和V操作对应的信号量编号(控制进程执行顺序)
    unsigned short p_semnum[COUNT] = {0, 1, 2};
    unsigned short v_semnum[COUNT] = {1, 2, 0};

    //创建多个子进程按照顺序输出
    int i = 0;
    for (i = 0; i < COUNT; ++i) {
        pid_t pid = fork();
        if (pid < 0) {
            perror("fork");
            return -1;
        }
        else if (pid == 0) {
            char text = 'A' + i;
            int count = 0;
            for (count = 0; count < PRINT_COUNT; ++count) {
                //P操作
                struct sembuf p_op = {p_semnum[i], -1, SEM_UNDO};
                if (semop(semid, &p_op, 1) < 0) {
                    perror("semop(P)");
                    exit(EXIT_FAILURE);
                }

                printf("%c", text);
                fflush(stdout);
                sleep(1);
                printf("%c", text);
                fflush(stdout);
                sleep(1);

                //V操作
                struct sembuf v_op = {v_semnum[i], 1, SEM_UNDO};
                if (semop(semid, &v_op, 1) < 0) {
                    perror("semop(V)");
                    exit(EXIT_FAILURE);
                }
            }
            exit(EXIT_SUCCESS);
        }
    }

    //等待所有子进程退出
    for (i = 0; i < COUNT; ++i) {
        wait(NULL);
    }
    printf("\n");

    //删除信号量集
    if (semctl(semid, 0, IPC_RMID) < 0) {
        perror("semctl(IPC_RMID)");
        return -1;
    }

    return 0;
}

【执行结果】

在这里插入图片描述
【分析】
  从执行结果来看,在没有信号量的情况下,三个进程执行顺序不确定,而且进程在执行中还会被其他进程打断;在加入了信号量后,三个进程既能按照指定的顺序执行(实现了同步),也不会被打断(实现了互斥)。

再次理解同步与互斥

  • 同步
    • 定义:协调进程的执行顺序,确保它们按照预期的逻辑顺序执行
    • 核心问题:解决进程间的依赖关系(如生产者-消费者问题)
  • 互斥
    • 定义:确保同一时间只有一个进程能访问共享资源(如变量、文件、内存等),避免竞争条件
    • 核心问题:解决多进程对资源的写冲突(例如同时修改同一数据)
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值