进程间通信之信号量
资源竞争
多个进程竞争同一资源时,会发生资源竞争。
资源竞争会导致进程的执行出现不可预测的结果。
临界资源
不允许同时有多个进程访问的资源, 包括硬件资源 (CPU、内存、存储器以及其他外
围设备) 与软件资源(共享代码段、共享数据结构)
临界区
多个进程共享的资源被称为临界资源,
这些资源被保护在一个临界区中,
只有进入临界区的进程才能访问临界资源。
信号量
信号量是一种进程间通信机制,用于协调对共享资源的访问。
多进程对stdout资源的竞争
//多进程对stdout资源的竞争
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <unistd.h>
#include <sys/wait.h>
int main(){
pid_t cpid;
cpid = fork();//创建子进程
if(cpid < 0){
printf("fork error\n");//fork失败
exit(EXIT_FAILURE);//EXIT_FAILURE表示程序运行失败
} else if(cpid == 0){
//子进程
while(1){
printf("------------------------\n");
printf("C Start.\n");
sleep(1);
printf("C End.\n");
printf("------------------------\n");
}
} else{
//父进程
while(1){
printf("------------------------\n");
printf("P Start.\n");
sleep(1);
printf("P End.\n");
printf("------------------------\n");
}
wait(NULL); //等待子进程结束
}
return 0;
}
代码的输出混乱:
------------------------
P Start.
------------------------
C Start.
P End.
------------------------
C End.
------------------------
------------------------
P Start.
------------------------
C Start.
P End.
C End.
------------------------
------------------------
同步和互斥
互斥
互斥是指进程独占资源,使得其他进程无法访问该资源。
同步
同步是指进程间通信,用于协调进程的执行。
同步在互斥的基础上增加了进程对临界资源的访问顺序
进程主要的同步与互斥手段是信号量
信号量
信号量,由内核维护的整数,其值被限制为大于或等于0;
信号可以执行一下操作:
- 将信号量设置成一个具体的值;
- 在信号量当前的基础上加上一个数值;
- 在信号量当前值的基础上减上一个数值;
- 等待信号量的值为0;
一般信号量分为
- 二值信号量:一般指的是信号量值为1,可以理解为只对应一个资源
- 计数信号量:一般指的是值大于等于2,可以理解为对应多个资源
在linux系统中使用ipcs -s 查询系统中信号量
无名信号量
sem_init()初始化无名信号量
函数头文件:
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/sem.h>
#include <semaphore.h>
int sem_init(sem_t *sem, int pshared, unsigned int value);
函数功能:初始化一个无名信号量;
函数参数:
- sem: 指向信号量的指针;
- pshared: 共享标志位,
- 0表示不共享, 两个线程间同步
- 非0表示共享,两个进程间同步;
- value: 信号量的初始值;
- 如果是1表示可以获取信号量
- 如果是0表示不可以获取信号量
函数返回值:
- 成功: 返回0;
- 失败: 返回-1, 并设置errno;
### sem_wait()等待信号量
函数头文件:
```c
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/sem.h>
#include <semaphore.h>
int sem_wait(sem_t *sem);
函数功能:等待信号量;
函数参数:
- sem: 指向信号量的指针;(将信号量的值-1)
- 如果信号量的值已经是0了,则sem_wait会阻塞,等到能执行减1操作为止
函数返回值:
- 成功: 返回0;
- 失败: 返回-1, 并设置errno;
sem_post()释放信号量
函数头文件:
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/sem.h>
#include <semaphore.h>
int sem_post(sem_t *sem);
函数功能:
释放信号量;(将信号量的值+1)
函数参数:
- sem: 指向信号量的指针;
函数返回值:
- 成功: 返回0;
- 失败: 返回-1, 并设置errno;
sem_destroy()销毁信号量
函数头文件:
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/sem.h>
#include <semaphore.h>
int sem_destroy(sem_t *sem);
函数功能:销毁信号量;
函数参数:
- sem: 指向信号量的指针;
函数返回值:
- 成功: 返回0;
- 失败: 返回-1, 并设置errno;
- 如果信号量已经被销毁,则再次调用 sem_destroy() 不会有任何作用, 不会报错;
创建信号量集合
调用 semget() 函数
函数头文件:
#include <sys/ipc.h>
#include <sys/sem.h>
#include <sys/types.h>
int semget(key_t key, int nsems, int semflg);
函数功能:创建一个信号量集合;
函数参数:
- key: 信号量集合的键值, 用于标识信号量集合;由ftok()函数生成;
- nsems: 信号量集合中信号量的个数;
- semflg: 信号量集合的标志位, 用于设置信号量集合的属性;
-
- IPC_CREAT: 如果key对应的信号量集合不存在, 则创建新的信号量集合;
-
- IPC_EXCL: 如果key对应的信号量集合已经存在, 则返回-1;
-
- 权限标志
函数返回值:
- 成功: 返回信号量集合的ID;
- 失败: 返回-1, 并设置errno;
//多进程对stdout资源的竞争
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <unistd.h>
#include <sys/wait.h>
#include <sys/ipc.h>
#include <sys/sem.h>
#define MSG_PATH "/home/gopher"
#define MSG_ID 88
int main(){
key_t key;
//通过文件路径和ID生成key,
key= ftok(MSG_PATH,MSG_ID);
if(key==-1){
printf("ftok()");
exit(EXIT_FAILURE);
}
//创建信号量集合,包含了一个信号量,编号为0
int semid=semget(key,1,IPC_CREAT|0666);
if(semid==-1){
printf("semget()");
exit(EXIT_FAILURE);
}
return 0;
}
创建出一个信号量集合,包含了一个信号量,编号为0
初始化信号量
调用 semctl() 函数
函数头文件:
#include <sys/ipc.h>
#include <sys/sem.h>
#include <sys/types.h>
int semctl(int semid, int semnum, int cmd, ... /* arg */ );
函数功能:对信号量集合中的信号量进行操作;根据cmd 决定当前函数的功能;
函数参数:
- semid: 信号量集合的ID;
- semnum: 信号量的编号;编号从0开始;
- cmd: 信号量操作命令;
- SETVAL:设置信号量的值。
- GETPID:返回最后一个执行 semop 操作的进程的PID。
- GETVAL:返回指定信号量的值。不需要第四个参数
- GETALL:返回信号量集中所有信号量的值。
- GETNCNT:返回正在等待信号量增加的进程数。
- GETZCNT:返回正在等待信号量变为零的进程数。
- SETALL:设置信号量集中所有信号量的值。
- IPC_STAT:获取信号量集的状态信息。 第二参数被忽略,第四个参数buf选项
- IPC_SET:设置信号量集的状态信息。第二参数被忽略,第四个参数buf选项
- IPC_RMID:删除信号量集。
- ... :是属于可变参参数列表,根据不同的命令有不同的参数;
函数返回值:
- 成功: 根据不同的cmd, 返回不同的结果;
-
GETPID:返回等待最后一个 semop 操作的进程的 PID。
GETVAL:返回指定信号量的值。
ls
GETALL:如果成功,返回 0。GETNCNT:返回正在等待增加信号量值的进程数量。
GETZCNT:返回正在等待信号量值为零的进程数量。
IPC_STAT:如果成功,返回 0。
IPC_SET:如果成功,返回 0。
IPC_RMID:如果成功,返回 0。
SETVAL:如果成功,返回 0。
SETALL:如果成功,返回 0。
失败: 返回-1, 并设置errno;
//多进程对stdout资源的竞争
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <unistd.h>
#include <sys/wait.h>
#include <sys/ipc.h>
#include <sys/sem.h>
#define MSG_PATH "/home/gopher"
#define MSG_ID 88
union semun{
int val;
};
int main(){
key_t key;
//通过文件路径和ID生成key,
key= ftok(MSG_PATH,MSG_ID);
if(key==-1){
printf("ftok()");
exit(EXIT_FAILURE);
}
//创建信号量集合,包含了一个信号量,编号为0
int semid=semget(key,1,IPC_CREAT|0666);
if(semid==-1){
printf("semget()");
exit(EXIT_FAILURE);
}
union semun s;//定义一个联合体,用于设置信号量的值
s.val=1;//设置信号量的值为1
int ret=semctl(semid,0,SETVAL,s);//设置semid信号集中的第编号为0的信号量的值为1
if(ret==-1){
printf("semctl()");
exit(EXIT_FAILURE);
}
return 0;
}
信号量操作
- 信号量可以进⾏以下操作:
- 对信号量的值加 1
- 对信号量的值减 1
- 等待信号量的值为 0
调用 semop() 函数
函数头文件:
#include <sys/ipc.h>
#include <sys/sem.h>
#include <sys/types.h>
int semop(int semid, struct sembuf *sops, size_t nsops);
函数功能:对信号量集合中的信号量进行操作;
函数参数:
- semid: 信号量集合的ID;
- sops: 信号量操作结构体指针
- nsops: 信号量操作结构体的个数;
函数返回值:
- 成功: 返回 0;
- 失败: 返回-1, 并设置errno;
struct sembuf *sops: 信号量操作结构体指针
```c
struct sembuf
{
unsigned short int sem_num;//信号量编号,从0开始
short int sem_op; //信号量操作
//-1:占用资源
// +1:释放资源
// 0:等待资源
short int sem_flg; //信号量操作标志位
//IPC_NOWAIT:非阻塞,在信号量的值为0时,立即返回
// SEM_UNDO:在进程终止时,会自动释放信号量
};
## 信号量集合删除
调用 semctl() 函数 ,设置命令为 IPC_RMID
在使用 semctl() 函数删除信号量集合时,需要注意第三个参数会被忽略
## 信号量互斥应用
> 使用信号量实现进程间互斥,同一时间只有一个进程访问临界资源
>
>
1.创建sem.h
```c
#ifndef _mySEM_H_
#define _mySEM_H_
#include <stdio.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/sem.h>
//创建信号量集
int sem_create(int names,unsigned short value[]);
//占用信号量
int sem_p(int semid,int semnum);
//释放信号量
int sem_v(int semid,int semnum);
//删除信号量集
int sem_delete(int semid);
#endif /* _SEM_H_ */
2.创建sem.c
#include "sem.h"
union semun {
int val; val 是一个整数类型,用于 SETVAL 命令,该命令用于设置信号量的值。
struct semid_ds *buf; 一个指向 struct semid_ds 结构的指针。semid_ds 结构用于存储信号量集合的状态信息。
buf 用于 IPC_STAT 和 IPC_SET 命令,分别用于获取和设置信号量集合的状态信息。
unsigned short *array; 一个指向无符号短整数数组的指针。
该数组用于 GETALL 和 SETALL 命令,分别用于获取和设置信号量集合中所有信号量的值
struct seminfo *__buf; 一个指向 struct seminfo 结构的指针。seminfo 结构用于存储信号量集合的详细信息。
__buf 用于 IPC_INFO 命令,该命令用于获取信号量集合的详细信息。这个成员是 Linux 特定的。
};
//创建信号量集
//@param names 信号量集的个数
//@param value 信号量集的初始值
//@return 成功返回信号量集的id,失败返回-1
int sem_create(int names,unsigned short value[]){
key_t key;
//创建key
key= ftok(".",88);
if (key == -1){
perror("ftok");
return -1;
}
//创建信号量集
int semid;
semid = semget(key,names,IPC_CREAT|0666);//参数:key,信号量集的个数,权限
if (semid == -1)
{
perror("semget");
return -1;
}
union semun s; //定义union semun
s.array = value;//将value数组赋值给union semun的array成员
//初始化信号量集
int ret=semctl(semid,0,SETALL,s);//这个操作将value数组中的值设置到信号量集中
if (ret == -1){
perror("semctl");
return -1;
}
return semid;
}
//占用信号量
//@param semid 信号量集的id
//@param semnum 信号量的编号
int sem_p(int semid,int semnum){
struct sembuf sem_b;//定义一个信号量操作结构体
sem_b.sem_num=semnum;//信号量编号
sem_b.sem_op= -1;//占用资源
sem_b.sem_flg=SEM_UNDO;//在进程终止时,会自动释放信号量
//操作1个信号量,如果操作多个信号量,需要创建sembuf结构体的数组
int r= semop(semid,&sem_b,1); //失败返回-1,并设置errno
return r;
}
//释放信号量
int sem_v(int semid,int semnum){
struct sembuf sem_b;//定义一个信号量操作结构体
sem_b.sem_num=semnum;//信号量编号
sem_b.sem_op= 1;//释放资源
sem_b.sem_flg=SEM_UNDO;//在进程终止时,会自动释放信号量,
int r= semop(semid,&sem_b,1); //操作1个信号量,如果操作多个信号量,需要创建sembuf结构体的数组
//失败返回-1,并设置errno
return r;
}
//删除信号量集
int sem_delete(int semid){
int r= semctl(semid,0,IPC_RMID); //删除信号量集
return r;
}
3.创建main.c
// 多进程对stdout资源的竞争
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <unistd.h>
#include <sys/wait.h>
#include "sem.h"
int main()
{
int semid;// 信号量ID
unsigned short values[] = {
1};// 信号量初始值
semid = sem_create(1, values);
if(semid == -1 ){
printf("sem_create error\n");
exit(EXIT_FAILURE);
}
pid_t cpid;// 子进程ID
cpid = fork(); // 创建子进程
if (cpid < 0)
{
printf("fork error\n"); // fork失败
exit(EXIT_FAILURE); // 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
{
// 父进程
while (1)
{
sem_p(semid,0);
printf("------------------------\n");
printf("P Start.\n");
sleep(1);
printf("P End.\n");
printf("------------------------\n");
sem_v(semid,0);
}
wait(NULL); // 等待子进程结束
}
return 0;
}
4.编译运行
------------------------
P Start.
P End.
------------------------
------------------------
C Start.
C End.
------------------------
------------------------
P Start.
P End.
------------------------
------------------------
C Start.
C End.
----------
信号量同步应用
同步在互斥的基础上增加了进程对临界资源的访问顺序
进程主要的同步与互斥手段是信号量
示例:
创建⽗⼦进程,输出 “ABA” 字符串,具体需求如下:
⽗进程 输出 A
⼦进程 输出 B
⽗进程 输出 A ,输出换⾏
能够循环输出 “ABA” 字符
基本思路:
通过创建⼀个信号量集合,包含 2 个信号量,⼀个信号量 编号为 0
(SEM_CONTROL_P)控制⽗进程的运⾏与暂停,⼀个信号量 编号为 1
(SEM_CONTROL_C) 控制⼦进程的运⾏与暂停
// 多进程对stdout资源的竞争
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <unistd.h>
#include <sys/wait.h>
#include "sem.h"
#define SEM_C = 1
#define SEM_P = 0
// todo 创建一个信号量集合,集合中两个信号量,信号量0的值是1,信号量1的值是0;
int main()
{
int semid; // 信号量ID
unsigned short values[2] = {
1, 0}; // 信号量初始值
// todo 创建一个信号量集合,集合中两个信号量,信号量编号0的值是1,信号量编号1的值是0;
semid = sem_create(2, values);
if (semid == -1)
{
printf("sem_create error\n");
exit(EXIT_FAILURE);
}
pid_t cpid; // 子进程ID
cpid = fork(); // 创建子进程
if (cpid < 0)
{
printf("fork error\n"); // fork失败
exit(EXIT_FAILURE); // EXIT_FAILURE表示程序运行失败
}
else if (cpid == 0)
{
// 子进程
while (1)
{
sem_p(semid, 1); //?占用信号量编号1,信号量编号1的值初始是0 ,在这里阻塞,等待父进程操作
printf("B");
fflush(stdout); // 刷新缓冲
sem_v(semid, 0); //!释放信号量编号0,信号量编号0的值 0=>1,此时父进程不再阻塞,第二次占用0
}
}
else
{
// 父进程
while (1)
{
//@param semid 信号量集的id
//@param semnum 信号量的编号
sem_p(semid, 0); //?占用信号量编号0,信号量编号0的值 1=>0
printf("A");
fflush(stdout); // 刷新缓冲
sem_v(semid, 1); //?释放信号量编号1,信号量编号1的值 0=>1,此时子进程不再阻塞
sem_p(semid, 0); //!第二次占用信号量编号0,信号量编号0的值是0,在这里阻塞,等待子进程的操作
printf("A\n");
fflush(stdout); // 刷新缓冲
sem_v(semid, 0);
sleep(1);
}
wait(NULL); // 等待子进程结束
}
return 0;
}
+
sem.h:
#ifndef __SEM_H__
#define __SEM_H__
#include <sys/ipc.h>
#include <sys/shm.h>
#include <sys/ipc.h>
#include <string.h>
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/sem.h>
#include <errno.h>
#define SHM_PATH "/home/gopher"
#define SHM_ID 1234
//初始化信号量
int sem_init(int num);
//信号量P操作
int sem_p(int semid, int num);
//信号量V操作
int sem_v(int semid, int num);
//销毁信号量
int sem_destroy(int semid);
#endif
sem.c:
#include "sem.h"
//初始化信号量
//@param num 信号量的数量
int sem_init(int num){
key_t key;//消息队列的key
//通过文件路径和ID生成key,
key= ftok(SHM_PATH,SHM_ID);
if(key==-1){
printf("ftok() error\n");
exit(EXIT_FAILURE);
}
printf("key: %d\n",key);
//创建共享内存
//@param key 共享内存的key
//@param nsems 共享内存的数量
//@param semflg 共享内存的标志
int semid = semget(key,2,IPC_CREAT|0666);
if(semid==-1){
if (errno== EEXIST ){
//EEXIST文件存在
printf("信号量集合已存在\n");//提示已存在
if(-1==(semid = semget(key,2,IPC_CREAT|0666))){
return semid;
}
}
printf("semget()");
exit(EXIT_FAILURE);
}
//初始化信号量
for(int i = 0; i < num; i++){
//设置信号量的值
if(-1 == semctl(semid, i, SETVAL, !i)){
printf("semctl() error\n");
exit(EXIT_FAILURE);
}
}
return semid;
}
//信号量P操作
int sem_p(int semid, int num){
struct sembuf s={
.sem_num=num,//信号量的编号
.sem_op=-1,//P操作
.sem_flg=0 //操作标志
};
//P操作
//@param semid 信号量集合ID
//@param s 信号量操作结构体
//@param nsops 信号量操作结构体的数量
semop(semid,&s,1);
return 0;
}
//信号量V操作
int sem_v(int semid, int num){
struct sembuf s={
.sem_num=num,//信号量的编号
.sem_op=1,//V操作
.sem_flg=0//操作标志
};
//V操作
//@param semid 信号量集合ID
//@param s 信号量操作结构体
//@param nsops 信号量操作结构体的数量
semop(semid,&s,1);
return 0;
}
//销毁信号量
int sem_destroy(int semid){
//销毁信号量集合
//@param semid 信号量集合ID
//@param semnum 信号量的数量
//@param semflg 信号量的操作标志//删除信号灯集 第二参数被忽略,第4个参数不用填写
semctl(semid,0,IPC_RMID);
return 0;
}