semctl semget semop 函数系列构成的 信号量
semctl semget semop 函数系列构成的 信号量
信号量原语
简介
信号量是由 Dijkstra 提出的解决多进程沟通和并发编程问题的特殊变量,这种变量只能取自然数,而且只支持两种操作: wait & signal 。它主要处理多个进程访问资源的问题,通过生成并使用令牌来授权,在任一时刻只能有一个执行线程访问代码的临界区域,而临界区域是指执行数据更新的代码需要独占式地执行。
实现方式
首先创建一个信号量 SV,并且使用一个整数唯一的标识信号量(这个标识量是跨越进程存在的,即在不同的进程中同一个信号量也能唯一的标识该事件)。只有两种 一种是 wait 操作,一种是 signal 操作,不过为了避免和linux的概念重复,又被称为 P 操作 和 V 操作
P(SV) :如果 SV 大于 0,那么 SV 的值 减一,这里的值指的是信号量所带的值,而不是标识信号量的整数,就和结构体名和结构体的内的一个变量一样,减的是变量。如果是SV==0,那么就挂起该进程的执行
V(SV):如果有其他进程由 因等待SV 挂起,那么 激活给他,如果没有 那么就 SV 加 一
上面很难理解对吧,举个例子吧。
我有两个进程 A & B,他们分想访问资源 C (访问过程的代码为关键代码),利用 信号量 来保证同时只有唯一的一个。比如
开始时首先设置 SV 的值 为 1
假设 A 优先 走到 关键代码区域,它会执行 P(SV) ,根据上面的操作,他会减 1 ,同时执行关键的代码。
在 A 执行关键代码的时候,B运行到了关键代码出,执行 P(SV
),它会则会挂起该进程,直到 A 执行完关键代码,并且 执行了 V(SV)
但必须强调的是 P(SV),V(SV ),都必须是原子操作。是由
sys/sem.h 的定义的函数实现的。
有些分类方式业在这提一下啊
信号量按其用途可分为两种:
公用信号量:联系一组并发进程,相关的进程均可在此信号量上执行P 和V操作。初值常常为1,用于实现进程互斥。
私有信号量:联系一组并发进程,仅允许此信号量拥有的进程执行P 操作,而其他相关进程可在其上施行V 操作。初值常常为0 或正整数,多用于并发进程同步。
信号量按其取值可分为两种:
二元信号量:仅允许取值为0 和1,主要用于解决进程互斥问题。
一般信号量:允许取值为非负整数,主要用于解决进程同步问题。
函数集
创建or 获得
int semget(key_t key, int num_sems, int sem_flags);
第一个参数key是整数值(唯一非零),不相关的进程可以通过它访问一个信号量,它代表程序可能要使用的某个资源,程序对所有信号量的访问都是间接的,程序先通过调用semget函数并提供一个键,再由系统生成一个相应的信号标识符(semget函数的返回值),只有semget函数才直接使用信号量键,所有其他的信号量函数使用由semget函数返回的信号量标识符。如果多个程序使用相同的key值,key将负责协调工作。
第二个参数num_sems指定信号集中信号的个数
具体的值是根据你使用信号量的目的,如果你想要实现进程之间的互斥操作,设置为1 ,而如果想要实现多进程的同步问题,那么至少得大于一。在为零的情况下,是获取该信号量。
第三个参数sem_flags是一组标志,当想要当信号量不存在时创建一个新的信号量,可以和值IPC_CREAT做按位或操作。设置了IPC_CREAT标志后,即使给出的键是一个已有信号量的键,也不会产生错误。而IPC_CREAT | IPC_EXCL则可以创建一个新的,唯一的信号量,如果信号量已存在,返回一个错误。
semget函数成功返回一个相应信号标识符(非零),失败返回-1.
信号量操作
int semop(int sem_id, struct sembuf *sem_opa, size_t num_sem_ops);
它用来实现信号量的 P,V 操作,利用是的结stuct sembuf
来指示操作内容
struct sembuf{
short sem_num;//除非使用一组信号量,否则它为0
short sem_op;//信号量在一次操作中需要改变的数据,通常是两个数,一个是-1,即P(等待)操作,
//一个是+1,即V(发送信号)操作。
short sem_flg;//通常为SEM_UNDO,使操作系统跟踪信号,
//并在进程没有释放该信号量而终止时,操作系统释放信号量
};
结构体中的变量含义为
sem_num : 指定操作信号量集中的第 sem_num+1 个信号量,一般设置为 0,表示操作第一个
sem_flag:
IPC_NOWAIT :设置信号量操作不等待
SEM_UNDO : 选项会让内核记录一个与调用进程相关的UNDO记录,如果该进程崩溃,则根据这个进程的UNDO记录自动恢复相应信号量的计数值
sem_op : 表示 操作信号量的方式,实现 P | V 的操作
sem_op | 意义 |
---|---|
大于0 | 将 sem_val + opt ,但要求有写权限 |
等于 0 | 在没有设置 IPC_NOWAIT 的情况下,会挂起 等待 sem_val==0的情况发生,发生则立即返。但设置的话,就会立即返回错误信息EAGAIN |
小于 0 | 在没有设置 IPC_NOWAIT的情况下,会等待 sem_op的绝对值小于 等于sem_val的值,小于的话,就减去它,并且放回,设置的话,与 等于0 的情况基本一致 |
等于 0 一般被称为 等待 0 ,而小于 0 则可以称为 等待可减
信号量设置
int semctl(int sem_id, int sem_num, int command, ...);
该函数用于对信号量直接设置
sem_id 为信号量级标识
sem_um 指定被操作信号量在信号量集中的编号
command 为操作命令
而... 代表可扩展参数,推荐使用一下啊的结构体填充
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_ds 和 seminfo 的结构体,seminfo 表示信号量系统资源配置信息,semid_ds则是一个内核结构体,在 semget
函数创建信号量时,被初始化和关联
的具体信息可以去man 一下 该函数,
针对 特殊键值的 IPC_PRIVATE
IPC_PRIVATE 是一个可以放在 semget 函数 中 key 值参数 的特殊键值,其值为0,它会忽略 key 参数的本身作用(标识一个信号量集),创建一个新的信号量,标识由返回值指示。但代表与该进程并无关系的进程无法访问该信号量,必须是有血缘关系的进程才能访问该信号量,
实例:
#include<sys/sem.h>
#include<stdio.h>
#include<iostream>
#include<stdlib.h>
#include<unistd.h>
#include<sys/wait.h>
union semun{
int val;
struct semid_ds * buf;
unsigned short int * array;
struct seminfo *__buf;
};
void pv(int semd_id,int op )
{
struct sembuf sem_b;
sem_b.sem_num = 0;
sem_b.sem_op = op;
sem_b.sem_flg = SEM_UNDO;
semop(semd_id,&sem_b,1);
}
int main()
{
int sem_id = semget(IPC_PRIVATE,1,0666);
std::cout<<"sem_id="<<sem_id<<std::endl;
union semun sem_um;
sem_um.val =1;
semctl(sem_id,0,SETVAL,sem_um );
pid_t id = fork();
if(id<0)
{
return 1;
}
else if(id==0)
{
printf("child try to get binary sem\n");
pv (sem_id,-1);
printf("child get the sem de would release it after 5 second\n");
sleep(10);
pv(sem_id,1);
exit(0);
}
else {
printf("parent try to get binary sem \n");
pv(sem_id,-1);
printf("parent get the sem and would release it after 5 second\n");
sleep(5);
pv(sem_id,1);
}
waitpid(id,NULL,0);
semctl(sem_id,0,IPC_RMID,sem_um);
return 0;
}
实例二:
无血缘之间进程的信号量通信的操作
转自:这里
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include <sys/sem.h>
union semun
{
int val;
struct semid_ds *buf;
unsigned short *arry;
};
static int sem_id = 0;
static int set_semvalue();
static void del_semvalue();
static int semaphore_p();
static int semaphore_v();
int main(int argc, char *argv[])
{
char message = 'X';
int i = 0;
//创建信号量
sem_id = semget((key_t)1234, 1, 0666 | IPC_CREAT);
if(argc > 1)
{
//程序第一次被调用,初始化信号量
if(!set_semvalue())
{
fprintf(stderr, "Failed to initialize semaphore\n");
exit(EXIT_FAILURE);
}
//设置要输出到屏幕中的信息,即其参数的第一个字符
message = argv[1][0];
sleep(2);
}
for(i = 0; i < 10; ++i)
{
//进入临界区
if(!semaphore_p())
exit(EXIT_FAILURE);
//向屏幕中输出数据
printf("%c", message);
//清理缓冲区,然后休眠随机时间
fflush(stdout);
sleep(rand() % 3);
//离开临界区前再一次向屏幕输出数据
printf("%c", message);
fflush(stdout);
//离开临界区,休眠随机时间后继续循环
if(!semaphore_v())
exit(EXIT_FAILURE);
sleep(rand() % 2);
}
sleep(10);
printf("\n%d - finished\n", getpid());
if(argc > 1)
{
//如果程序是第一次被调用,则在退出前删除信号量
sleep(3);
del_semvalue();
}
exit(EXIT_SUCCESS);
}
static int set_semvalue()
{
//用于初始化信号量,在使用信号量前必须这样做
union semun sem_union;
sem_union.val = 1;
if(semctl(sem_id, 0, SETVAL, sem_union) == -1)
return 0;
return 1;
}
static void del_semvalue()
{
//删除信号量
union semun sem_union;
if(semctl(sem_id, 0, IPC_RMID, sem_union) == -1)
fprintf(stderr, "Failed to delete semaphore\n");
}
static int semaphore_p()
{
//对信号量做减1操作,即等待P(sv)
struct sembuf sem_b;
sem_b.sem_num = 0;
sem_b.sem_op = -1;//P()
sem_b.sem_flg = SEM_UNDO;
if(semop(sem_id, &sem_b, 1) == -1)
{
fprintf(stderr, "semaphore_p failed\n");
return 0;
}
return 1;
}
static int semaphore_v()
{
//这是一个释放操作,它使信号量变为可用,即发送信号V(sv)
struct sembuf sem_b;
sem_b.sem_num = 0;
sem_b.sem_op = 1;//V()
sem_b.sem_flg = SEM_UNDO;
if(semop(sem_id, &sem_b, 1) == -1)
{
fprintf(stderr, "semaphore_v failed\n");
return 0;
}
return 1;
}
运行结果:
分析:
例子分析 :同时运行一个程序的两个实例,注意第一次运行时,要加上一个字符作为参数,例如本例中的字符‘O’,它用于区分是否为第一次调用,同时这个字符输出到屏幕中。因为每个程序都在其进入临界区后和离开临界区前打印一个字符,所以每个字符都应该成对出现,正如你看到的上图的输出那样。在main函数中循环中我们可以看到,每次进程要访问stdout(标准输出),即要输出字符时,每次都要检查信号量是否可用(即stdout有没有正在被其他进程使用)。所以,当一个进程A在调用函数semaphore_p进入了临界区,输出字符后,调用sleep时,另一个进程B可能想访问stdout,但是信号量的P请求操作失败,只能挂起自己的执行,当进程A调用函数semaphore_v离开了临界区,进程B马上被恢复执行。然后进程A和进程B就这样一直循环了10次。