概念
信号量
- 信号量和P、V原语由Dijkstra(迪杰斯特拉)提出
- 互斥:P、V在同一个进程中
- 同步:P、V在不同进程中
- 信号量值含义
- S>0:S表示可用资源的个数
- S=0:表示无可用资源,无等待进程
- S<0:|S|表示等待队列中进程个数
- 信号量的数据结构–注意不是信号量集合
struct semaphore
{
int value;
pointer_PCB queue;
}
P原语
P(s)
{
s.value = s.value--;
if (s.value < 0)
{
该进程状态置为等待状状态
将该进程的PCB插入相应的等待队列s.queue末尾
}
}
V原语
V(s)
{
s.value = s.value++;
if (s.value < =0)
{
唤醒相应等待队列s.queue中等待的一个进程
改变其状态为就绪态
并将其插入就绪队列
}
}
相关API
描述信号量集合的数据结构
struct semid_ds {
struct ipc_perm sem_perm; /* Ownership and permissions */
time_t sem_otime; /* Last semop time */
time_t sem_ctime; /* Last change time */
unsigned short sem_nsems; /* No. of semaphores in set */
};
semget函数
- 功能:用来创建和访问一个信号量集
- 原型
int semget(key_t key, int nsems, int semflg);
- 参数
- key: 信号集的名字
- nsems:信号集中信号量的个数
- semflg: 由九个权限标志构成,它们的用法和创建文件时使用的mode模式标志是一样的
- 返回值:成功返回一个非负整数,即该信号集的标识码;失败返回-1
shmctl函数
- 功能:用于控制信号量集
- 原型
int semctl(int semid, int semnum, int cmd, ...);
- 参数
- semid:由semget返回的信号集标识码
- semnum:信号集中信号量的序号
- cmd:将要采取的动作(有三个可取值)
- 最后一个参数根据命令不同而不同
- 返回值:成功返回0;失败返回-1
semop函数
- 功能:用来创建和访问一个信号量集
- 原型
int semop(int semid, struct sembuf *sops, unsigned nsops);
- 参数
- semid:是该信号量的标识码,也就是semget函数的返回值
- sops:是个指向一个结构数值的指针
- nsops:信号量的个数
- 返回值:成功返回0;失败返回-1
- sembuf结构体:
struct sembuf {
short sem_num;
short sem_op;
short sem_flg;
};
- sem_num是信号量的编号。
- sem_op是信号量一次PV操作时加减的数值,一般只会用到两个值,一个是“-1”,也就是P操作,等待信号量变得可用;另一个是“+1”,也就是我们的V操作,发出信号量已经变得可用
- sem_flag的两个取值是IPC_NOWAIT或SEM_UNDO
- 还要注意struct sembuf 里面的sem_flg如果为SEM_UNDO,则会自动撤销之前的p或v操作,是信号量的计数值恢复pv操作之前的值!
示例代码
封装了很多必要的操作,简化了API的调用过程,可以直接在以后的项目中使用!
#include <stdlib.h>
#include <stdio.h>
#include <errno.h>
#include <string.h>
#include <unistd.h>
#include <fcntl.h>
#include <signal.h>
#include <sys/time.h>
#include <sys/mman.h>
#include <sys/ipc.h>
#include <sys/sem.h>
#include <sys/stat.h>
#include <sys/wait.h>
#include <sys/types.h>
#define SEM_NUM 1
typedef 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) */
}semun_t;
#if 0
typedef struct sembuf{
unsigned short sem_num; /* semaphore number */
short sem_op; /* semaphore operation */
short sem_flg; /* operation flags */
}sembuf_t;
#endif
typedef struct sembuf sembuf_t;
//封装创建信号量集合的代码
int sem_create(key_t key)
{
int sem_id = 0;
sem_id = semget(key,SEM_NUM,0666|IPC_CREAT|IPC_EXCL);
if(sem_id == -1)
{
perror("semget");
if(errno == EEXIST)
{
printf("Judge by self..exist\n");
}
return -1;
}
return sem_id;
}
//封装打开已存在的信号量集合
int sem_open(key_t key)
{
int sem_id = 0;
sem_id = semget(key,SEM_NUM,0666);
if(sem_id == -1)
{
//perror("semget");
return -1;
}
return sem_id;
}
//设置信号量集合里面的信号量的计数值
int sem_set(int semid,int val)
{
int ret = 0;
if(semid < 0)
{
return -1;
}
semun_t su;
su.val = val;
ret = semctl(semid,0,SETVAL,su);
return ret;
}
//获取信号量集合里面的信号量的计数值
int sem_get(int semid,int* val)
{
int ret = 0;
if(semid < 0)
{
return -1;
}
semun_t su;
ret = semctl(semid,0,GETVAL,su);
*val = su.val;
return ret;
}
//原子p操作
int sem_p(int semid)
{
int ret = 0;
sembuf_t buf = {0,-1,0};//一般默认是从第0个信号量开始,第三个参数默认是0表示阻塞模式
ret = semop(semid,&buf,1 );//只操作一个信号量
return ret;
}
//原子v操作
int sem_v(int semid)
{
int ret = 0;
sembuf_t buf = {0,+1,0};//一般默认是从第0个信号量开始,第三个参数默认是0表示阻塞模式
ret = semop(semid,&buf,1);//只操作一个信号量
return ret;
}
int main()
{
int sem_id = 0;//信号量集ID
int key;//用来创建信号量集的key
int tmp_val = 1024;//临时变量--用来测试设置和获取信号量的计数值
pid_t pid ;
int i= 0;
key = ftok("./",'c');//获取key
//利用获取的key打开/创建信号量集合
sem_id = sem_open(key);
if(sem_id == -1)
sem_id = sem_create(key);
//设置信号量集合中的信号量(元素)的计数值
//设置为1表示一个进程使用临界区--执行p操作以后,还为0--表示没有进程等待--当前进程可以执行临界区代码
//设置为0表示当前进程也要被阻塞,--因为在执行P操作以后,计数值小于0,会将当前进程放到等待队列
//实际上这里设置为多少,表示有多少个进程可以并发执行临界区代码
sem_set(sem_id,1);
//获取信号量集合中的信号量(元素)的计数值,存储到临时变量tmp_val并打印
sem_get(sem_id,&tmp_val);
printf("tmp_val:%d\n",tmp_val);
pid = fork();
if(pid == -1)
{
perror("fork");
exit(-1);
}
//p操作--类似上锁--保证一个进程结束另一个进程才开始执行
sem_p(sem_id);
printf("my pid:%d\t i = %d\n",getpid(),i++);
printf("my pid:%d\t i = %d\n",getpid(),i++);
//v操作--类似解锁
sem_v(sem_id);
return 0;
}
同时说明,fork以后父子进程的代码段是有猫腻的,有时间在总结,如果按照普通的说法,子进程复制了父进程的代码段、数据段、堆栈段,则子进程和父进程各自拥有自己的代码段,也就不会有临界区一说了!不会发生上述程序的现象!不会发生互斥!