linux-进程通信(5)信号量(实现读者写者问题)

本文深入介绍了信号量的概念,作为进程间同步和互斥的工具,它不是一个数据传递机制,而是用于管理临界资源。通过PV操作,信号量能确保对共享资源的有序访问。文中提供了Linux下创建、操作和控制信号量的API示例,包括semget、semop和semctl函数,并展示了如何解决读者写者问题和在生产者消费者模型中的应用。此外,还给出了消息队列、信号量和共享内存结合使用的例子。

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

信号量

信号量(semaphore)与已经介绍过的 IPC 结构不同(前面的管道,消息队列,共享内存,信号都可以携带数据),它是一个计数器。信号量用于实现进程间的互斥与同步,而不是用于存储进程间通信数据。
原理
临界资源(各进程采取互斥的方式,实现共享的资源称作临界资源。)
多道程序系统中存在许多进程,它们共享各种资源,然而有很多资源一次只能供一个进程使用。一次仅允许一个进程使用的资源称为临界资源。许多物理设备都属于临界资源,如输入机、打印机、磁带机等。

可以将信号量比喻成一个盒子,初始化时在盒子里放入N把钥匙,钥匙先到先得,当N把钥匙都被拿走完后,再来拿钥匙的人就需要等待了,只有等到有人将钥匙归还了,等待的人才能拿到钥匙;
临界资源就像一间房间,用锁锁起来,只有持有钥匙的人才能进去访问,当盒子没有钥匙的时候,外面的人需要等里面的人出来之后归还钥匙,才可以进去访问。

特点
(1)信号量用于进程间同步,若要在进程间传递数据需要结合共享内存。

(2)信号量基于操作系统的PV 操作,程序对信号量的操作都是原子操作

(3)每次对信号量的 PV 操作不仅限于对信号量值加 1 或减 1,而且可以加减任意正整数。

(4)支持信号量组。

注意:
PV操作是一种实现进程互斥与同步的有效方法。PV操作与信号量的处理相关,P(拿钥匙)表示通过的意思,V(放回钥匙)表示释放的意思。
原子操作是指不会被线程调度机制打断的操作;这种操作一旦开始,就一直运行到结束,中间不会有任何 context switch(切换到另一个线程)。

创建
最简单的信号量是只能取 0 和 1 的变量,这也是信号量最常见的一种形式,叫做二值信号量(Binary Semaphore)。而可以取多个正整数的信号量被称为通用信号量。

Linux 下的信号量函数都是在通用的信号量数组上进行操作,而不是在一个单一的二值信号量上进行操作。
常用API

 // 创建或获取一个信号量组:若成功返回信号量集ID,失败返回-1
int semget(key_t key, int nsems, int semflg);

// 对信号量组进行操作,改变信号量的值:成功返回0,失败返回-1
int semop(int semid,  struct sembuf *sops, unsigned nsops);  

// 控制信号量的相关信息
int semctl(int semid, int sem_num, int cmd, ...);

semget函数

创建或获取一个信号量组:
函数原型

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

参数
key:所创建或打开信号量集的键值。
nsems:信号量的个数,通常为1;如果是引用一个现有的集合,则将nsems指定为 0,该参数只在创建信号量集时有效。
(也可以设置多个,比如设置成2,理解成一个管理a事件,另一个管理b事件)
semflg:调用函数的操作类型,也可用于设置信号量集的访问权限,两者通过 | 表示。
返回值
若成功返回信号量集ID,失败返回-1

semop函数

对信号量组进行操作,改变信号量的值:
函数原型

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

参数
semid:semget()返回的信号量标识符。
*sops:结构的定义如下:

struct sembuf{
   
    short sem_num; // 除非使用一组信号量,否则它为0
    short sem_op;  // 信号量在一次操作中需要改变的数据,
    //通常是两个数,一个是-1,即P(拿钥匙)操作,一个是+1,即V(放回)操作。
    short sem_flg; // 通常为SEM_UNDO,使操作系统跟踪信号,
                   // 并在进程没有释放该信号量而终止时,操作系统释放信号量
};

nsops:信号操作结构的数量,恒大于或等于1。
返回值
成功返回0,失败返回-1

semctl函数

控制信号量的相关信息
函数原型

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

参数
semid:semget()返回的信号量标识符。
semnum:是操作信号在信号集中的编号,第一个信号的编号是0。
cmd:cmd通常是下面的两个值:
SETVAL:
用来把信号量初始化为一个已知的值。这个值通过union semun中的val成员设置,其作用是在信号量第一次使用前对它进行设置。

IPC_RMID:
用于删除一个已经无需继续使用的信号量标识符。
cmd中其他可以使用的命令如下:

IPC_STAT//读取一个信号量集的数据结构semid_ds,并将其存储在semun中的buf参数中。
IPC_SET//设置信号量集的数据结构semid_ds中的元素ipc_perm,其值取自semun中的buf参数。
IPC_RMID//将信号量集从内存中删除。
GETALL//用于读取信号量集中的所有信号量的值。
GETNCNT//返回正在等待资源的进程数目。
GETPID//返回最后一个执行semop操作的进程的PID。
GETVAL//返回信号量集中的一个单个的信号量的值。
GETZCNT//返回正在等待完全空闲的资源的进程数目。
SETALL//设置信号量集中的所有的信号量的值。
SETVAL//设置信号量集中的一个单独的信号量的值。

如果有第四个参数(需要用户自行定义),它通常是一个union semun结构,定义如下:

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) */
};

作用是在信号量第一次使用前对它进行设置。

对多个信号量初始化
semid =semget(key,3,IPC_CREAT|0666)
union semun arg;
unsigned short vals[3]={0};//假设初始化信号量集的值都为0
arg.array = vlas
semctl(sem_id, 0, SETALL, arg) //使用SETALL

参考生产者和消费者
示例1(父子进程)

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

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) */
};
//p操作
//将信号量值-1 
void pGet(int id)
{
   
        struct sembuf set;
        set.sem_num=0;
        set.sem_op=-1;
        set.sem_flg =SEM_UNDO;
        semop(id,&set,1);
        printf("get\n");
}
//v操作
//将信号量值+1 
void vPut(int id)
{
   
        struct sembuf set;
        set.sem_num=0;
        set.sem_op=1;
        set.sem_flg =SEM_UNDO;
        semop(id,&set,1);
        printf("put\n");
}
//父进程获取信号量将被阻塞,直到子进程将信号量+1
int main(int argc,char **argv)
{
   
        key_t key;
        int semid;
        key = ftok(".",8);// 获取key值
        semid =semget(key,1,IPC_CREAT|0666);// 创建信号量集,其中只有一个信号量
        union semun initsem;
        initsem.val = 0;//设置SETVAL的值(一开始没有钥匙)
        semctl(semid,0,SETVAL,initsem);//SETVAL:用来把信号量初始化为一个已知的值。
        int pid = fork();//建立进程
        if(pid>0){
   
                pGet(semid);//父进程将信号量-1,因为一开始为0,会堵塞到子进程将信号量+1才会继续执行
                printf("this is father\n");
                vPut(semid);
                semctl(semid,0,IPC_RMID);//删除信号量
        }
        else if(pid == 0){
   
        		sleep(1);
                printf("this is child\n");
                vPut(semid);//子进程将信号量+1
               	putchar('\n');
        }else{
   
                printf("fork error\n");
        }
}

在这里插入图片描述
如果没有pv操作,父进程肯定时先执行完的,因为子进程sleep了2秒

示例2 (不同进程访问共享内存—读者写者问题–写者优先)
在这里插入图片描述

  1. 先实现一个共享资源只能有一个进程(一个读者或者一个写者)访问
semaphore rw=1;//用于实现对共享数据的互斥访问

	写进程:			
	writer(){
   			
		while(1){
   	
			P(rw);	
			"写操作"	
			V(rw);	
		
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值