一,信号量简介
信号量⼜称为信号灯,主要是⽤来保护共享资源,使得资源在⼀个时刻只有⼀个进程(线程)所拥有。
它的本质就是⼀个整数计数器,⽤来为多个进程共享的数据结构提供受控访问。信号量⽤于实现进程间或指定线程的不同线程间的互斥与同步,而不是⽤于存储进程间通信数据,它是为配合其他通信⼿段⽽存在的。
Linux提供两种信号量:
1.内核信号量,由内核控制路径使⽤
2.⽤户态进程使⽤的信号量,这种信号量⼜分为POSIX信号量和SYSTEMV信号量。
libc库实现了POSIX和System V两种接⼝标准的信号量API。这⾥主要讨论SystemV接⼝的信号量API。
相关概念:
临界资源:是⼀次仅允许⼀个进程使⽤的共享资源。
原⼦操作:原⼦操作是不可分割的,在执⾏完毕之前不会被任何其它任务或事件中断。
信号量是⼀个特殊的变量,程序对其访问都是原⼦操作。
二,信号量操作
信号量初始化的值的⼤⼩⼀般⽤于表示可⽤资源的数;如果初始化为1,则称之⼆值信号量(⼆元信号量),⼆值信号量的功能就有点像互斥锁,使⽤⽐较常⻅。
当它的值⼤于0时,表示当前可⽤资源的数量;当它的值⼩于0时,其绝对值表示等待使⽤该资源的进程个数。内核会负责维护信号量的值,并确保其值不⼩于0。
信号量只能进⾏两种操作即P操作和V操作
P操作是:
(1)S减1;
(2)若S减1后仍⼤于或等于零,则进程继续执⾏;
(3)若S减1后⼩于零,则该进程被阻塞
V操作是:
(1)S加1;
(2)若相加结果⼤于等于零,则进程继续执⾏;
(3)若相加结果⼩于零,则该进程被阻塞。
使⽤PV操作实现进程互斥时应该注意:
1.每个程序中⽤户实现互斥的P、V操作必须成对出现,先做P操作,进临界区,后做V操作,出临界区。若有多个分⽀,要认真检查其成对性。
2.P、V操作应分别紧靠临界区的头尾部,临界区的代码应尽可能短,不能有死循环。
3.互斥信号量的初值⼀般为1
三,信号量API介绍
信号量创建函数:

说明:
当semget创建新的信号量集合时,必须指定集合中信号量的个数(即num_sems),通常为1; 如果是引⽤⼀个现有的集合,则将num_sems指定为 0。
操作信号量:

信号量操作相关的数据结构:
struct sembuf //定义好的,可以直接拿来使⽤
{
short sem_num; // 信号量集中信号的编号,0代表第⼀个信号
short sem_op; //// 信号量在⼀次操作中需要改变的数据,通常是两个数,⼀个
是-1,即P(等待)操作,
// ⼀个是+1,即V(发送信号)操作。
short sem_flg;
};
sem_op: 若sem_op >0,表示V操作,使得信号量+sem_op,
若sem_op <0,表示p操作,使得信号量-sem_op
若sem_op =0,调⽤进程进⼊阻塞,直到信号量为0,
若 sem_flg 值为 IPC_NOWAIT,不会阻塞,返回EAGAIN错误
sem_flg: 若设为IPC_NOWAIT,信号量会以⾮阻塞⽅式进⾏
通常设为SEM_UNDO,使操作系统跟踪信号,并在进程没有释放该信号量⽽终⽌时,操作 系统释放信号量
控制信号量函数:

信号量常⽤控制指令:
IPC_SET: 设置信号量struct semid_ds信息
IPC_STAT: 获取信号量struct semid_ds信息
IPC_INFO: 获取信号量struct seminfo信息
IPC_RMID: 删除信号量集
SETVAL: 通过公⽤体中val设置信号量集中单个信号的值
GETPID: 获取信号量拥有者的pid的值;
GETNCNT: 获取等待信号量的值递增的进程数;
GETZCNT: 获取等待信号量的值递减的进程数;
GETVAL: 获取信号量集中单个信号的值,放⼊共⽤体val中
SETVAL: 通过公⽤体中val设置信号量集中单个信号的值
GETALL: 获取信号量集中所有信号的值,放⼊公⽤体数组中
SETALL: 通过公⽤体数组设置信号量集中所有信号的值
常用的主要有:
SETVAL:⽤来把信号量初始化为⼀个已知的值。这个值通过union semun中的val成员设置。 其作⽤是在信号量第⼀次使⽤之前对它进⾏设置。
IPC_RMID:⽤于删除⼀个⽆需继续使⽤的信号量标志符。
信号量控制相关的结构体:
对于semctl函数,只有当command取某些特定的值的时候,才会使⽤到第4个参数,第四个参数⼀般传的是个union semun sem_un 联合体, 主要⽤来对信号量集中特定的信号量进⾏初始,因为参数信号量参数多,所以采⽤联合体的形式减少内存。
union semun
{
int val; /* Value for SETVAL */
struct semid_ds *buf; /*⽤于获取IPC_STAT, IPC_SET */
unsigned short *array; /* Array for GETALL, SETALL */ 是对信号量集的操作
struct seminfo *__buf; /* Buffer for IPC_INFO */获取信号集的信息
};
array指向⼀个数组,当cmd==SETALL时,就根据arg.array来将信号量集的所有值都赋值;当cmd ==GETALL时,就将信号量集的所有值返回到arg.array指定的数组中。
//⽤SETVAL⼀次初始化⼀个信号量
int semid=semget(125,2,IPC_CREAT|0666); //创建2个信号量。
semctl(semid,0,SETVAL,5); //设置第1个信号量的值为 5
semctl(semid,1,SETVAL,3); //设置第1个信号量的值为 3
int val1 = semctl(semid, 0, GETVAL); //获取第 0 个信号量的值
int val2 = semctl(semid, 1, GETVAL); //获取第 1 个信号量的值
printf("val1=%d,val2=%d\n",val1,val2); //5,3
//⽤SETALL初始化多个信号量
int semid=semget(127,3,IPC_CREAT|0666);
unsigned short sem_arr[3] = {2, 5, 8};
semctl(semid,0,SETALL,sem_arr);// 注意:此时第⼆个参数被忽略
//读取多个信号量
unsigned short temp[3]={0};
semctl(semid, 0, GETALL, temp);// 注意:这时候第⼆个参数被忽略
for(int i=0;i<3;i++)
printf("val=%d\n",temp[i]);
注意:信号量⽤完要删除。
四,信号量的使用步骤
1>>创建⼀个新信号量或取得⼀个已有信号量
int semget(key_t key, int num_sems, int sem_flags) ;
2>>初始化信号量(可以⽤SETVAL命令)
int semctl(int sem_id, int sem_num, int command, ...) ;
3>>操作信号量(PV操作)
int semop(int sem_id, struct sembuf *sem_opa, size_t num_sem_ops) ;
4>>删除信号量(可以⽤IPC_RMID命令)
int semctl(int sem_id, int sem_num, int command, ...) ;
样例程序:
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/ipc.h>
#include <sys/sem.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <fcntl.h>
#define SEM_KEY 1234 // 信号量 key
#define LOOP_NUM 5
// 封装 P 操作
void P(int semid) {
struct sembuf sb;
sb.sem_num = 0; // 信号量编号(信号量集里第0个)
sb.sem_op = -1; // P操作:减1
sb.sem_flg = 0;
semop(semid, &sb, 1);
}
// 封装 V 操作
void V(int semid) {
struct sembuf sb;
sb.sem_num = 0; // 信号量编号
sb.sem_op = 1; // V操作:加1
sb.sem_flg = 0;
semop(semid, &sb, 1);
}
int main() {
int semid;
pid_t pid;
// 1. 创建一个信号量集合,包含1个信号量
semid = semget(SEM_KEY, 1, IPC_CREAT | 0666);
if (semid == -1) {
perror("semget");
exit(1);
}
// 2. 初始化信号量的值为1(互斥锁)
if (semctl(semid, 0, SETVAL, 1) == -1) {
perror("semctl");
exit(1);
}
// 初始化文件
FILE *fp = fopen("count.txt", "w+");
if (!fp) {
perror("fopen");
exit(1);
}
fprintf(fp, "0\n");
fclose(fp);
// 3. 创建子进程
pid = fork();
if (pid < 0) {
perror("fork");
exit(1);
}
for (int i = 0; i < LOOP_NUM; i++) {
// P操作:进入临界区
P(semid);
// ---- 临界区 ----
fp = fopen("count.txt", "r+");
int count;
fscanf(fp, "%d", &count);
rewind(fp);
fprintf(fp, "%d\n", count + 1);
fclose(fp);
printf("进程 %d 修改 count: %d -> %d\n", getpid(), count, count + 1);
// ---- 临界区 ----
// V操作:退出临界区
V(semid);
sleep(1);
}
// 父进程等待子进程
if (pid > 0) {
wait(NULL);
// 删除信号量
semctl(semid, 0, IPC_RMID);
printf("最终结果写在 count.txt 里\n");
}
return 0;
}

8550

被折叠的 条评论
为什么被折叠?



