进程间通信-信号量
资源竞争
- 资源竞争:当多个进程在同时访问共享资源时,会产生资源竞争,最终最导致数据混乱
- 临界资源:不允许同时有多个进程访问的资源,包括硬件资源(CPU、内存、存储器以及其他外围设备)与软件资源(共享代码段、共享数据结构)
- 临界区:访问临界资源代码
多进程对 stdout 资源的竞争
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/wait.h>
#include <sys/types.h>
int main(){
pid_t cpid;
cpid = fork();
if(cpid == -1){
perror("[Error] fork()");
exit(EXIT_FAILURE);
}
else if(cpid == 0){
while(1){
printf("----------------\n");
printf("C start.\n");
printf("C end.\n");
printf("---------------\n");
}
}else if(cpid > 0){
while(1){
printf("----------------\n");
printf("P start.\n");
printf("P end.\n");
printf("-----------------\n");
}
wait(NULL);
}
return 0;
}
二、同步与互斥
- 互斥 :同一时刻只有一个进程访问临界资源
- 同步: 在互斥的基础上增加了进程对临界资源的访问顺序
- 进程主要的同步与互斥手段是 信号量
信号量简介
- 信号量: 由内核维护的整数,其值被限制为大于或等于0
- 信号量可以执行如下操作:
- 将信号量设置成一个具体的值
- 在信号量当前值的基础上加上一个数值
- 在信号量当前值的基础上减上一个数值
- 等待信号量的值为 0
一般信号量分为 二值信号量 与 数信号量
- 二值信号量 :一般指的是信号量 的值为 1可以理解为只对应一个资源
- 计数信号量:一般指的是值大于等于2可以理解为对应多个资源
- 在 Linux 系统中查询信号量使用 ipcs -s
四、创建信号量集合
- 创建信号量集合调用 semget 函数
函数头文件
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/sem.h>
函数原型
int semget(key_t key, int nsems, int semflg):
函数功能
创建一个信号量集合
函数参数
- key : 由 ftok() 函数生成
- nsems : 信号量的数量
- semflg: 信号量集合的标志
- IPC CREAT:创建标志
- IPC EXCL: 与IPC CREAT 标志一起使用,如果信号量集合存在就报错
- 权限标志
五、初始化信号量
初始化信号量调用 semctl 函数
函数头文件
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/sem.h>
函数原型
int semctl(int semid, int semnum, int cmd, ...);
函数功能
信号集合控制函数,根据 cmd 决定当前函数的功能
函数参数
- semid : 信号量集合的id
- semnum:信号量的编号,信号量的编号从 0开始
- cmd : 命令控制字
- SETVAL: 设置信号量的值
- GETVAL: 获取信号量的值
- ... : 后面是属于可变参参数列表,根据不同的命令有不同的参数
: 后面是属于可变参参数列表,根据不同的命令有不同的参数
函数返回值
成功: 根据不同的命令有不同的返回值,可以查看帮助文档关于 RETURN 的说明
- GETNCNT the value of semncnt
- GETPID the value of sempid
- GETVAL the value of semval
- GETZCNT the value of semzcnt
- All other cmd values return 0 on success
- 失败: 返回 -1,并设置 errno
在使用命令时需要使用 union semun 共用体,具体定义如下
创建一个信号量集合,集合中包含一个信号量,并设置信号量的值为 1
#include <stdio.h>
#include <stdlib.h>
#include <sys/ipc.h>
#include <sys/sem.h>
#include <string.h>
#include <errno.h>
#include <unistd.h>
#define SEM_PATHNAME "."
#define SEM_PRO_ID 100
union semun{
int val;
};
int main(){
int semid,ret;
union semun s;
key_t key = ftok(SEM_PATHNAME,SEM_PRO_ID);
if(key == -1){
perror("[ERROR] ftok():");
exit(EXIT_FAILURE);
}
semid = semget(key,2,IPC_CREAT | 0666);
if(semid == -1){
perror("[ERROR] semget");
fprintf(stdout,"%s\n",strerror(errno));
exit(EXIT_FAILURE);
}
s.val = 1;
ret = semctl(semid,0,SETVAL,s);
if(ret == -1){
perror("[ERROR] semctl()");
exit(EXIT_FAILURE);
}
ret = semctl(semid,1,SETVAL,s);
if(ret == -1){
perror("[ERROR] setctl()");
exit(EXIT_FAILURE);
}
sleep(10);
ret = semctl(semid,0,IPC_RMID,NULL);
if(ret == -1){
perror("[ERROR] semctl()");
exit(EXIT_FAILURE);
}
return 0;
}
信号量操作
- 信号量可以进行以下操作:
- 对信号量的值加1
- 对信号量的值减1
- 等待信号量的值为0
- 操作信号量调用 semop 函数
函数头文件
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/sem.h>
函数原型
int semop(int semid, struct sembuf *sops, size t nsops);
函数功能
信号量操作函数,用于占用信号量、释放信号量、设置信号量等待
函数参数
- semid :信号量集合id
- sops : 信号量操作结构体指针,见后面关于 struct sembuf 解释
- nsops: 操作的信号量的数量
函数返回值
- 成功 : 返回 0
- 失败: 返回-1,并设置 errno
struct sembuf 结构体
- unsigned short sem_num;
- 信号量编号,从0 开始,在 sem_op 的帮助文档中0
- short sem_op;
- 信号量操作
- -1 : 占用资源
- +l : 释放资源
- 0 : 等待资源
- 信号量操作
- short sem_flg;
- 信号量操作标志
- IPC_NOWAIT:非阻塞,在信号量的值为0时,会立即返回
- SEM_UNDO: 在进程终止时,会自动释放信号量
在 semop 函数中关于信号量集合编号的说明
- 信号量操作标志
信号量集合删除
- 信号量集合调用 semctl 函数,设置命令为 IPC_RMID
- 注意:在使用 IPC_RMID 时,第 三个参数会被忽略,下面是帮助文档中的说明
- 注意:在使用 IPC_RMID 时,第 三个参数会被忽略,下面是帮助文档中的说明
ret = semctl(semid,IPC_RMID,NULL);
信号量互斥应用
- 使用信号量解决父子进程对终端的竞争
- 信号量操作封装
- sem.h
#ifndef _SEM_H
#define _SEM_H
#endif
#include <stdio.h>
#include <stdlib.h>
#include <sys/ipc.h>
#include <sys/sem.h>
// 创建信号量合集
extern int sem_create(int nsems,unsigned short values[]);
extern int sem_p(int semid,int semnum);
extern int sem_v(int semid,int semnum);
extern int sem_del(int semid);
extern int sem_get(int semid,int semnum);
- sem.c
#include<stdio.h>
#include "sem.h"
#define PATHNAME "."
#define SEM_PRO_ID 100
union semun{
int val;
unsigned short *array;
};
int sem_create(int nsems,unsigned short values[]){
int semid, ret;
key_t key;
union semun s;
key = ftok(PATHNAME,SEM_PRO_ID);
if(key == -1){
perror("[ERROR] ftok()");
exit(EXIT_FAILURE);
}
semid = semget(key,nsems,IPC_CREAT|0600);
if(semid == -1){
perror("[ERRIR] semget()");
exit(EXIT_FAILURE);
}
s.array = values;
ret = semctl(semid,0,SETALL,s);
if(ret == -1){
perror("[ERROR] setctl()");
exit(EXIT_FAILURE);
}
return semid;
}
int sem_p(int semid, int sem_num){
struct sembuf sops;
sops.sem_num = sem_num;
sops.sem_op = -1;
sops.sem_flg = SEM_UNDO;
return semop(semid,&sops,1);
}
int sem_v(int semid,int sem_num){
struct sembuf sops;
sops.sem_num = sem_num;
sops.sem_op = 1;
sops.sem_flg = SEM_UNDO;
return semop(semid,&sops,1);
}
int sem_del(int semid){
return semctl(semid,0,IPC_RMID,NULL);
}
int sem_get(int semid,int sem_num){
union semun2 s;
int val = semctl(semid,sem_num,GETVAL);
if(val == -1){
perror("[ERROR] semctl error");
exit(EXIT_FAILURE);
}
return val;
}
- main.c
#include<stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <unistd.h>
#include "sem.h"
int main(){
pid_t cpid;
int semid;
unsigned short values[] = {1};
semid = sem_create(1,values);
if(semid == -1){
perror("[ERROR] sem_create()");
exit(EXIT_FAILURE);
}
cpid = fork();
if(cpid == -1){
perror("[ERROR] fork()");
exit(EXIT_FAILURE);
}
else if(cpid == 0){
while(1){
sem_p(semid,0);
printf("-----------\n");
printf("C start \n");
sleep(1);
printf("C end \n");
printf("-----------\n");
sem_v(semid,0);
}
}else if(cpid > 0){
while(1){
sem_p(semid,0);
printf("-----------\n");
printf("P start \n");
sleep(1);
printf("P end \n");
printf("------------\n");
sem_v(semid,0);
}
}
return 0;
}
信号量同步
需求
- 创建父子进程,输出“ABA”字符串,具体需求如下
- 父进程 输出A
- 子进程 输出 B
- 父进程 输出 A,输出换行
- 能够循环输出“ABA”字符
实现思路
通过创建一个信号量集合,包含 2个信号量,一个信号量 编号为 0 (SEM_CONTROL_P) 控制父进程的运行与暂停,一个信号量 编号为 1 (SEM_CONTROL_C) 控制子进程的运行与暂停
- 信号初始化
- SEM_CONTROL_P :初始化为 1
- SEM_CONTROL_C :初始化为 0
- 子进程:
- 占用 SEM_CONTROL_C,此时子进程阻塞
- 当父进程释放 SEM_CONTROL_C 时,子进程输出 B,释放 SEM_CONTROL_P
- 循环占用 SEM_CONTROL_C,由于之前已经占用,此时进入子进程阻塞,等待父进程释放SEM_CONTROL_C
- 父进程:
- 占用 SEM_CONTROL_P此时父进程正常运行,输出 A
- 释放 SEM_CONTROL_C,占用 SME_CONTROL_P,此时父进程阻塞,子进程继续执行
- 当子进程输出 B 之后,释放 SEM_CONTROL_P父进程继续执行,输出 A
- 父进程释放 SEM_CONTROL_P 循环结束
#include<stdio.h>
#include "sem.h"
#include <stdlib.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <unistd.h>
#define SEM_CONTROL_P 0
#define SEM_CONTROL_C 1
int main(){
pid_t cpid;
int semid;
unsigned short values[] = {1,0};
semid = sem_create(2,values);
if(semid == -1){
perror("[ERROR] sem_create");
exit(EXIT_FAILURE);
}
cpid = fork();
if(cpid == -1){
perror("[ERROR] fork()");
exit(EXIT_FAILURE);
}
else if(cpid == 0){
while(1){
if(sem_p(semid,SEM_CONTROL_C) == -1){
perror("[ERROR] sem_p");
exit(EXIT_FAILURE);
}
printf("B");
fflush(stdout);
sem_v(semid,SEM_CONTROL_P);
}
}
else if(cpid > 0){
while(1){
sem_p(semid,SEM_CONTROL_P);
printf("A");
fflush(stdout);
sem_v(semid,SEM_CONTROL_C);
sem_p(semid,SEM_CONTROL_P);
printf("A");
fflush(stdout);
sem_v(semid,SEM_CONTROL_P);
putchar('\n');
sleep(0.1);
}
wait(NULL);
}
return 0;
}
对于SEM_UNDO来说,内核记录的信息是跟进程相关的。一个进程在P操作的时候对应该进程的UNDO计数就多一个,V操作的时候那么计数就减一。在设置SEM_UNDO的时候一定要注意使用的场景,否则就会导致Numerical result out of range错误。
1)如果P\V操作都是在一个进程中完成,就可以设置该标志, 但要注意P\V操作时要同时设置,否则也会导致计数值一直增加而溢出;
2)如果一个进程做P操作,另外一个进程做V操作,就不能设置该标志,因为对单一的进程来说UNDO计数会一直增加而溢出,计数的上限是32767(和信号量的最大值相同)