3. system V-IPC
3.1 知识点
ipcs -a查看所有的ipc对象
在系统中他们都使用一种叫做 key 的键值来唯一标识,而且他们都是“持续性”资源——即他 们被创建之后,不会因为进程的退出而消失,而会持续地存在,除非调用特殊的函数或者命 令删除他们。
跟文件类型,进程每次“打开”一个 IPC 对象,就会获得一个表征这个对象的 ID,进而再使用这个 ID 来操作这个对象。IPC 对象的 key 是唯一的,但是 ID 是可变的。key 类似于文件的路径名,ID 类似于文件的描述符
系统中的多个进程,如果他们需要使用IPC 对象来通信,那么他们必须持有这个对象的 键值 key:
3.1.1 创建key
path:这是一个字符串,通常是指向一个存在的文件的路径。ftok() 将使用这个路径名来生成键。这个路径名应该是一个可访问的文件,因为它的存在与访问权限会影响键的生成。通常情况下,不同的路径名会生成不同的键。
proj:这是一个整数,通常取值在0到255之间。它用于在指定路径名的情况下生成唯一的键。如果不同的进程使用相同的路径名,可以通过不同的 proj 值生成不同的键。通常,每个IPC对象(消息队列、信号量、共享内存)都会有一个唯一的 proj 值。
这个函数需要注意的几点:
1,如果两个参数相同,那么产生的 key 值也相同。
2 ,第一个参数一般取进程所在的目录,因为在一个项目中需要通信的几个进程通常会 出现在同一个目录当中。
3 ,如果同一个目录中的进程需要超过 1 个 IPC 对象,可以通过第二个参数来标识。
4 ,系统中只有一套 key 标识,也就是说,不同类型的 IPC 对象也不能重复。
3.1.2 查看和删除ipc
可以使用以下命令来查看或删除当前系统中的IPC 对象:
查看消息队列:ipcs -q
查看共享内存:ipcs -m
查看信号量:ipcs -s
查看所有的 IPC 对象:ipcs -a
删除指定的消息队列:ipcrm -q MSG_ID 或者 ipcrm -Q msg_key
删除指定的共享内存:ipcrm -m SHM_ID 或者 ipcrm -M shm_key
删除指定的信号量:ipcrm -s SEM_ID 或者 ipcrm -S sem_key
3.2 消息队列
3.2.1 创建消息队列,获取操作id
权限只有读和写,执行权限是无效的,例如 0777 跟 0666 是等价的。
删除消息队列
可以用
ipcrm -q 消息队列id
或者
ipcrm -Q 消息队列的key
3.2.2 发送和接收消息
使用这两个收、发消息函数需要注意以下几点:
1,发送消息时,消息必须被组织成以下形式:
struct msgbuf
{
long mtype; // 消息的标识
char mtext[1]; // 消息的正文
};
也就是说:发送出去的消息必须以一个 long 型数据打头,作为该消息的标识,后 面的数据则没有要求。
2,消息的标识可以是任意长整型数值,但不能是 0L。
3,参数 msgsz 是消息中正文的大小,不包含消息的标识。
3.2.3 代码
ipc_msg_que_send.c
#include <sys/types.h>
#include <sys/stat.h>
#include <stdio.h>
#include <fcntl.h>
#include <string.h>
#include <unistd.h>
#include <sys/ipc.h>
#include <sys/msg.h>
//消息队列的使用
struct msgbuf
{
long mtype;//消息的标识(整数)
char mtext[128];//消息的正文
};
int main()
{
//创建key
key_t key = ftok("/",0);
//创建消息队列,获取操作消息队列的id
int msg_id = msgget(key,IPC_CREAT | 0777);
if(msg_id == -1)
{
perror("msg_id error");
}
struct msgbuf buf;
buf.mtype = 1;
while(1){
memset(buf.mtext,0,sizeof(buf.mtext));//清零
fgets(buf.mtext,sizeof(buf.mtext),stdin);//获取输入
//发送
msgsnd(msg_id,&buf,128,0);
}
return 0;
}
ipc_msg_que_receive.c
#include <sys/types.h>
#include <sys/stat.h>
#include <stdio.h>
#include <fcntl.h>
#include <string.h>
#include <unistd.h>
#include <sys/ipc.h>
#include <sys/msg.h>
//消息队列的使用
struct msgbuf
{
long mtype;//消息的标识(整数)
char mtext[128];//消息的正文
};
int main()
{
//创建key
key_t key = ftok("/",0);
//创建消息队列,获取操作消息队列的id
int msg_id = msgget(key,IPC_CREAT | 0777);
if(msg_id == -1)
{
perror("msg_id error");
}
struct msgbuf msg;
while(1){
memset(msg.mtext,0,sizeof(msg.mtext));//清零
//接收
msgrcv(msg_id,&msg,128,1,0);
printf("receive msg: %s\n",msg.mtext);
}
return 0;
}
3.2.4 练习
1.设计一个消息队列程序,发送时,通过键盘来指定消息标识接收时,通过键盘来指定要接收的消息
ipc_que_test_w.c
#include <sys/types.h>
#include <sys/stat.h>
#include <stdio.h>
#include <fcntl.h>
#include <string.h>
#include <unistd.h>
#include <sys/ipc.h>
#include <sys/msg.h>
//设计一个消息队列程序,发送时,通过键盘来指定消息标识接收时,通过键盘来指定要接收的消息
struct msgbuf{
long mtype;//消息的标识(整数)
char mtext[128];//消息的正文
};
int main()
{
//创建key
key_t key = ftok("/",4);
//创建消息队列
int msg_id = msgget(key,IPC_CREAT | 0777);
if(msg_id==-1){
perror("msg_id error");
}
struct msgbuf buf;
int type;
while(1){
memset(buf.mtext,0,sizeof(buf.mtext));
//键盘输入消息标识符
printf("请输入发送消息标识符/(99退出):");
scanf("%d",&type);
if(type==99){
break;
}
buf.mtype = type;
getchar();//清空缓冲区
//键盘接收消息正文
printf("请输入消息正文:");
fgets(buf.mtext,sizeof(buf.mtext),stdin);
//发送
msgsnd(msg_id,&buf,128,0);
}
return 0;
}
ipc_que_test_r.c
#include <sys/types.h>
#include <sys/stat.h>
#include <stdio.h>
#include <fcntl.h>
#include <string.h>
#include <unistd.h>
#include <sys/ipc.h>
#include <sys/msg.h>
//设计一个消息队列程序,发送时,通过键盘来指定消息标识接收时,通过键盘来指定要接收的消息
struct msgbuf{
long mtype;//消息的标识(整数)
char mtext[128];//消息的正文
};
int main()
{
//创建key
key_t key = ftok("/",4);
//创建消息队列
int msg_id = msgget(key,IPC_CREAT | 0777);
if(msg_id==-1){
perror("msg_id error");
}
struct msgbuf buf;
int type;
while(1){
memset(buf.mtext,0,sizeof(buf.mtext));
//键盘输入消息标识符
printf("请输入接收消息标识符/(99退出):");
scanf("%d",&type);
if(type==99){
break;
}
buf.mtype = type;
printf("\n");
//发送
int ret = msgrcv(msg_id,&buf,128,type,0);
printf("接收到标识符尾%d的消息为: %s\n",type,buf.mtext);
}
return 0;
}
3.3 共享内存
3.3.1 使用步骤
使用共享内存的一般步骤是:
1,获取共享内存对象的 ID
2,将共享内存映射至本进程虚拟内存空间的某个区域
3,当不再使用时,解除映射关系
4,当没有进程再需要这块共享内存时,删除它。
3.3.2 获取共享内存的 ID
共享内存的大小必须是偶数
3.3.3 共享内存映射
3.3.4 代码
ipc_share_w.c
#include<sys/types.h>
#include<sys/stat.h>
#include<stdio.h>
#include<fcntl.h>
#include<string.h>
#include<unistd.h>
#include<sys/ipc.h>
#include<sys/shm.h>
//共享内存
int main()
{
//创建key 和 获取共享内存id
int share_id = shmget(ftok("/",1),1024,IPC_CREAT | 0777);
//对共享内存进行映射 将这个共享内存映射至本进程的虚拟空间
char* p = shmat(share_id,NULL,0);//p就是共享内存的首地址
fgets(p,1024,stdin);//获取键盘输入
shmdt(p);//解除映射
return 0;
}
ipc_share_r.c
#include<sys/types.h>
#include<sys/stat.h>
#include<stdio.h>
#include<fcntl.h>
#include<string.h>
#include<unistd.h>
#include<sys/ipc.h>
#include<sys/shm.h>
//共享内存
int main()
{
//创建key 和 获取共享内存id
int share_id = shmget(ftok("/",1),1024,IPC_CREAT | 0777);
//对共享内存进行映射 将这个共享内存映射至本进程的虚拟空间
char* p = shmat(share_id,NULL,0);//p就是共享内存的首地址
printf("%s\n",p);//打印
shmdt(p);//解除映射
return 0;
}
3.4 信号量
一些基本概念如下:
1,多个进程或线程有可能同时访问的资源 (变量、链表、文件等等) 称为共享资源, 也叫临界资源 (critical resources) 。
2,访问这些资源的代码称为临界代码,这些代码区域称为临界区 (critical zone) 。
3,程序进入临界区之前必须要对资源进行申请,这个动作被称为 P 操作,这就像你要 把车开进停车场之前,先要向保安申请一张停车卡一样,P 操作就是申请资源,如果申请成 功,资源数将会减少。如果申请失败,要不在门口等,要不走人。
4,程序离开临界区之后必须要释放相应的资源,这个动作被称为 V 操作,这就像你把 车开出停车场之后,要将停车卡归还给保安一样,V 操作就是释放资源,释放资源就是让资 源数增加。
system-V 的信号量并不是单个的值,而是一组 (事实上是一个数 组) 信号量元素构成的
信号量跟前面的 MSG 和 SHM 有极大的不同,SEM 不是用来传输数据的,而 是作为“旗语”,用来协调各进程或者线程工作的。
信号量的 P、V 操作最核心的特征是:他们是原子性的,也就是说对信号量元素的值的 增加和减少,系统保证在 CPU 的电气特性级别上不可分割,这跟整型数据的加减法有本质 的区别。
3.4.1 创建信号量获取操作id
创建信号量时,还受到以下系统信息的影响:
1,SEMMNI:系统中信号量的总数最大值。
2,SEMMSL:每个信号量中信号量元素的个数最大值。
3,SEMMNS:系统中所有信号量中的信号量元素的总数最大值。
Linux 中,以上信息在/proc/sys/kernel/sem 中可查看。
3.4.2 获取或者设置信号量的相关属性
使用以上函数接口,需要注意以下几点:
1,这是一个变参函数,根据cmd 的不同,可能需要第四个参数,第四个参数是一个如 下所示的联合体,用户必须自己定义:
union semun
{
int val; /* 当 cmd 为 SETVAL 时使用 */
struct semid_ds *buf; /* 当 cmd 为 IPC_STAT 或 IPC_SET 时使用 */
unsigned short *array; /* 当 cmd 为 GETALL 或 SETALL 时使用 */
struct seminfo *__buf; /* 当 cmd 为 IPC_INFO 时使用 */
};
3.4.3 对信号量进行 P/V 操作,或者等零操作
使用以上函数接口需要注意以下几点:
1,信号量操作结构体的定义如下:
struct sembuf
{
unsigned short short sem_num; /* 信号量元素序号 (数组下标) */
short sem_op; /* 操作参数 */
short sem_flg; /* 操作选项 */
};
请注意:信号量元素的序号从 0 开始,实际上就是数组下标。
1. 当 sem_op 大于 0 时:进行 V 操作,即信号量元素的值 (semval) 将会被加上 sem_op 的值。
2. 当 sem_op 等于 0 时:进行等零操作
3. 当 sem_op 小于 0 时:进行 P 操作,即信号量元素的值 (semval) 将会被减去 sem_op 的绝对值。
重点
一个信号量(Semaphore)通常对应一个 struct sembuf 数组,但这个 struct sembuf 数组可以包含多个操作,而不仅限于一个操作。让我更详细地解释:
一个信号量对应一个 struct sembuf 数组:每个信号量在信号量集合中都有一个唯一的标识符,你可以使用这个标识符来指定要操作的信号量。通常情况下,你会创建一个 struct sembuf 数组,其中的每个元素描述了对某个特定信号量的操作。
一个 struct sembuf 数组可以包含多个操作:在 struct sembuf 数组中,你可以定义多个操作,这些操作都是针对同一个信号量的。每个 struct sembuf 结构体包括三主要字段:sem_num(信号量集合中的索引,用于指定操作的目标信号量)、sem_op(要执行的操作,通常是 -1 表示 P 操作或 1 表示 V 操作),以及 sem_flg(标志位,通常设置为 0)。
所以,你可以创建一个 struct sembuf 数组,其中的每个元素描述了对同一个信号量的不同操作。这种方式允许你在一个数组中组织多个操作,以便在一次函数调用中执行多个操作,这些操作都是基于同一个信号量的。
总结,一个信号量对应一个 struct sembuf 数组,而这个数组可以包含多个操作,用于对该信号量执行不同的操作。这是一种有效的方式来管理并发访问共享资源或进行同步操作。
3.4.5 代码
sys_sem_w.c
#include <stdio.h>
#include <fcntl.h>
#include <unistd.h>
#include <string.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <sys/stat.h>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <sys/msg.h>
#include <sys/sem.h>
#define DATA 0
#define SPACE 1
//共享内存的结合信号量,循环写入0-9
union semun
{
int val; /* 当 cmd 为 SETVAL 时使用 */
struct semid_ds *buf; /* 当 cmd 为 IPC_STAT 或 IPC_SET 时使用 */
unsigned short *array; /* 当 cmd 为 GETALL 或 SETALL 时使用 */
struct seminfo *__buf; /* 当 cmd 为 IPC_INFO 时使用 */
};
void sem_init(int id, int semnum, int val)
{
union semun a;
a.val = val;
semctl(id, semnum,SETVAL, a);
}
void sem_p(int id,int semnum)//资源-1
{
struct sembuf sops[1];
sops[0].sem_num = semnum;
sops[0].sem_op = -1;
sops[0].sem_flg = 0;
semop(id, sops, 1);
}
void sem_v(int id, int semnum)//资源+1
{
struct sembuf sops[1];
sops[0].sem_num = semnum;
sops[0].sem_op = 1;
sops[0].sem_flg = 0;
semop(id, sops, 1);
}
int main()
{
//创建共享内存的IPC对象
int shmid = shmget(ftok("/",2), 2,IPC_CREAT | 0666);//共享内存的大小必须是偶数,实际上在本代码中只需要1个
char *p = shmat(shmid, NULL, 0);//映射
//创建信号量的IPC对象
int sem_id = semget(ftok("/",3),2,IPC_CREAT | 0666);
//数据当0
//数据初始化为0个
sem_init(sem_id, DATA, 0);
//空间当1
//空间初始化为1个
sem_init(sem_id, SPACE, 1);
char *msg = "0123456789";
int i = 0;
while(1)
{
//1个空间
//0个数据
//写入数据
sem_p(sem_id,SPACE);//p -1 s 0
memcpy(p, msg+i, 1);//0,1,2,3,4,5,6,7,8,9
sem_v(sem_id,DATA);//v +1 d 1
i = (i+1)%10;//10%10=0
}
}
sys_sem_r.c
#include <stdio.h>
#include <fcntl.h>
#include <unistd.h>
#include <string.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <sys/stat.h>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <sys/msg.h>
#include <sys/sem.h>
//共享内存的结合信号量,循环写入0-9
//首先搞清楚一个概念 信号量集合中可以有多个信号量,一个信号量对应一个struct sembuf sops[]结构体数组
//, 一个信号量可以有多个资源也就是说一个struct sembuf sops[]结构体数组可以有多个元素
//这里两个宏定义 的0 1 可以理解为信号量的下标(因为有的情况会有很多个信号量
//我们的例子中只有两个信号量,如果有多个的话 就是 0 1 2 3 4.....这样排下去)
#define DATA 0
#define SPACE 1
//获取或设置信号量的相关属性涉及到的结构体
union semun
{
int val; /* 当 cmd 为 SETVAL 时使用 */
struct semid_ds *buf; /* 当 cmd 为 IPC_STAT 或 IPC_SET 时使用 */
unsigned short *array; /* 当 cmd 为 GETALL 或 SETALL 时使用 */
struct seminfo *__buf; /* 当 cmd 为 IPC_INFO 时使用 */
};
//信号量初始化
void sem_init(int id, int semnum, int val)
{
union semun a;//联合体
a.val = val;
//获取或设置信号量的相关属性
//第二个参数是指定哪个信号量
//这是一个变参函数 第三个参数SETVAL(设置该信号量元素的值) 会影响第四个参数应该传递啥(参数是个联合体)
//这里带入解释一下我们假如传来的的是我们定义的SPACE信号量,val参数为1
//参数semnum是SPACE 所以semctl函数的意思是,给SPACE信号量 设置该信号量元素的值为 a联合体
//因为我们第三个参数使用的是SETVAL 所以a联合体中主要用到属性val被赋值为该信号量元素的值为1
semctl(id, semnum,SETVAL, a);
}
//p操作
void sem_p(int id,int semnum)//资源-1
{
//这个结构体数组可以有多个元素,代表同一个信号量可以携带多个资源并在一个函数内对多个资源操作
//这里我们的一个信号量只设置了一个资源,所以结构体数组的大小为1,千万别被迷惑了
struct sembuf sops[1];
sops[0].sem_num = semnum; //semnum指定信号量
sops[0].sem_op = -1;
sops[0].sem_flg = 0;
//对信号量进行pv操作的函数,这个函数的第二参数需要一个struct sembuf类型的结构体数组
//所以我们上面才会定义一个结构体数组
//第三个参数 就是你的结构体数组中有几个元素
semop(id, sops, 1);
}
//v操作
void sem_v(int id, int semnum)//资源+1
{
struct sembuf sops[1];
sops[0].sem_num = semnum;
sops[0].sem_op = 1;
sops[0].sem_flg = 0;
semop(id, sops, 1);
}
int main()
{
//创建共享内存的IPC对象,获取操作id
int shmid = shmget(ftok("/",2), 2,IPC_CREAT | 0666);//共享内存的大小必须是偶数,实际上在本代码中只需要1个
//将共享内存 映射 到虚拟内存
char *p = shmat(shmid, NULL, 0);
//创建信号量的IPC对象 ,,2,代表这里我们创建2个信号量
int sem_id = semget(ftok("/",3),2,IPC_CREAT | 0666);
//初始化信号量
//数据信号量 的下标为0
//数据初始化为0个
sem_init(sem_id, DATA, 0);//这里的DATA代表它在信号量集合中的下标(就是我们上面的宏定义),0代表这个信号量的大小
//空间信号量我们规定下标为1
//空间初始化为1个
sem_init(sem_id, SPACE, 1);
char *msg = "0123456789";
int i = 0;
while(1)
{
//1个空间
//0个数据
//写入数据
//p操作
sem_p(sem_id,DATA);//p -1 s 0
fprintf(stderr,p);
//v操作
sem_v(sem_id,SPACE);//v +1 d 1
i = (i+1)%10;//10%10=0
}
}
编译运行