在学习了进程间通信的前几种方式管道,消息队列、共享内存之后,进程间通信的另一种方式——信号量。
在学习信号量之前,需要先理解几个名词。
互斥:由于各进程要求共享资源,而有些资源需要互斥使用,因此各进程间需要竞争使用这些资源,这种关系为进程的互斥。
同步:多个进程需要相互配合共同完成一项任务。
临界资源:系统中某些资源一次只允许一个进程使用,称这样的资源为临界资源或互斥资源。
临界区:在进程中涉及到互斥资源的程序段叫临界区。
1、信号量
信号量本质上就是一个计数器,用来衡量临界资源的个数。但与此同时,信号量也是一个临界资源。
信号量的结构体伪代码:
struct semaphore
{
int value;
pointer_PCB queue;
}
信号量值得含义:
S>0: S表示可用资源的个数;
S=0: 表示无可用资源,无进程等待;
S<0: |S|表示等待队列中等待进程的个数;
2、 P、V原语
信号量中最重要的就是P、V操作。
P操作:减1操作 就是向操作系统申请资源,申请成功,进行减1操作;
V操作:加1操作 就是向操作系统归还资源,归还成功,进行加1操作;
P、V操作均为原子操作,在使用时,需要保证P、V操作的原子性。这也从侧面说明了为什么不定义一个全局变量int count,而需要信号量集的出现。
3、 信号量集结构
4、信号量集函数
(1)创建信号量集的函数
int semget(key_t key, int nsems, int semflg);
参数:key:(同消息队列和共享内存)信号量集的名字;
nems:信号量集中信号量的个数;
semflg:(同消息队列和共享内存)和创建文件时使用的mode标志一样的。
IPC_CREAT|IPC_EXCL:不存在创建,存在出错返回;IPC_CREAT:不存在创建,存在返回;
返回值:创建成功返回信号量集的标识符,创建失败返回-1
(2)控制信号量集的函数
int semctl(int semid, int semnum, int cmd, ...);
参数:semid:semget返回的信号量集的标识符;
semnum:要对信号量集中的第几个信号量进行操作;
cmd:将要采取的动作,不同的可取值有不同的含义。如下图
在使用信号量集之前必须对信号量集进行初始化,相当于要把cmd设置为SETVAL,这时候就要在加一个参数,这个参数是一个联合体。
不过,这个联合体中我们只使用第一个变量val。
返回值:成功返回0,失败返回-1
(3)访问信号量集的函数
int semop(int semid, struct sembuf *sops, unsigned nsops);
参数:semid:senget返回的信号量集的标识符;
sops:是一个指向结构数组的指针;
nsops:信号量的个数;
说明一下,这个结构体struct sembuf
5、信号量的实现
comm.h
#ifndef _COMM_H_
#define _COMM_H_
#include<stdio.h>
#include<sys/types.h>
#include<sys/ipc.h>
#include<sys/sem.h>
#include<unistd.h>
#define PATHNAME "."
#define PROJ_ID 0X6666
int creatsem(int nums);
int initsem(int semid,int nums,int initval);
int getsem(int nums);
int p(int semid,int who);
int v(int semid,int who);
int destroysem(int semid);
#endif
test_sem.c
#include "comm.h"
union semun
{
int val;
};
int commsem(int nums, int flags)
{
key_t key = ftok(PATHNAME, PROJ_ID);
if (key<0)
{
perror("ftok");
return -1;
}
int semid = semget(key, nums, flags);
if (semid<0)
{
perror("semget");
return -2;
}
return semid;
}
int creatsem(int nums)
{
return commsem(nums, IPC_CREAT | IPC_EXCL | 0666);
}
int initsem(int semid, int nums, int value)
{
union semun u;
u.val = value;
if (semctl(semid, nums, SETVAL, u)<0)
{
perror("semctl");
return -1;
}
return 0;
}
int getsem(int nums)
{
return commsem(nums, IPC_CREAT);
}
int commpv(int semid, int who, int op)
{
struct sembuf sf;
sf.sem_num = who;
sf.sem_op = op;
sf.sem_flg = 0;
if (semop(semid, &sf, 1)<0)
{
perror("semop");
return -1;
}
return 0;
}
int p(int semid, int who)
{
return commpv(semid, who, -1);
}
int v(int semid, int who)
{
return commpv(semid, who, 1);
}
int destroyaem(int semid)
{
if (semctl(semid, 0, IPC_RMID)<0)
{
perror("semctl");
return -1;
}
}
int main()
{
int semid = creatsem(1);
initsem(semid, 0, 1);
pid_t id = fork();
int _semid = getsem(0);
if (id == 0)
{// child
while (1)
{
// p(_semid, 0);
printf("A");
fflush(stdout);
usleep(100000);
printf("A ");
fflush(stdout);
usleep(300000);
// v(_semid, 0);
}
}
else
{//father
while (1)
{
// p(_semid, 0);
printf("B");
fflush(stdout);
usleep(200000);
printf("B ");
fflush(stdout);
usleep(100000);
// v(_semid, 0);
}
wait(NULL);
}
destroysem(semid);
return 0;
}
测试结果:
通过结果截图我们可以看出,在不加P、V操作之前,输出的并不是我们想要的两个相同的字符成对出现的情况。
此时,显示器只有一个,两个进程同时打印,此时显示器成为临界资源,这就需要我们加互斥锁来进行保护。互斥锁就是二元信号量,
当使用互斥锁进行保护后(即加上P、V操作之后),屏幕上就会成对成对的打印我们所想要的字符,而不会出现交叉打印的现象。
和其他进程间通信的方式一样,信号量依然可以使用ipcs -s命令来查看,使用ipcrm -s+标号进行删除。