目录
前言
一、什么是共享内存
二、创建一个共享内存
三、信号灯/信号集
1、临界资源
2、同步互斥机制
1、互斥机制:
2、同步:
3、信号灯的机制
4、信号灯的函数
四、信号灯控制进程对共享内存的访问
进程1:
进程2:
总结
前言
本节将介绍,进程间通信的最后一个内容,也就是共享内存和信号灯机制
一、什么是共享内存
共享内存就是创建在内核内存中,将同一个内存空间地址分别映射到不同的进程中,进程只需要操作对应用户空间的虚拟地址空间们就可以操作共享内存。
创建的内存空间有起始地址,和结束地址
共享内存通信方式:当进程1想要和进程2进行通信时,由于内核空间是公用的,我们在内核空间中创建一个共享内存,然后得到共享内存的地址,在进程1中将数据传输到共享内存当中,进程2在通过地址找到共享内存的地址,把进程1传入的数据拿到进程2中,就实现了进程间通信的过程。但是在这个过程中会有一个问题,我们对在进程操作共享内存空间时没有安排访问共享内存的顺序,会导致达不到我们想要传输一次,读取一次的目的,这时候就引入了信号灯中的同步互斥机制,本文也会重点讲述这个机制
共享内存的特点:
1、共享内存时最高效的进程间通信机制
2、共享内存独立于进程的,共享内存在进程结束后不会消失。除非手动删除或计算机重启
二、创建一个共享内存
shmget():创建共享内存
#include <sys/ipc.h> #include <sys/shm.h> int shmget(key_t key, size_t size, int shmflg); 功能:创建key或在内核中找到key值的共享内存 参数: 参数1: key_t key:创建/打开 key 对应的共享内存 参数2: size_t size:指定共享内存的大小,以字节为单位 参数3: int shmflg:选项 IPC_CREAT:如果key值对应的共享内存不存在,则创建共享内存 IPC_CREAT | 0664 :创建的同时指定权限,如果共享内存已经存在,则会忽略这个权限 返回值: 成功,返回共享内存id 失败,返回-1,设置errno
shmmat():将共享内存的地址映射到用户空间,也就是进程能读取的地址空间中
#include <sys/types.h> #include <sys/shm.h> void *shmat(int shmid, const void *shmaddr, int shmflg); 功能:将共享内存映射到用户空间中 参数: 参数1: int shmid:共享内存id 参数2: const void *shmaddr:指定要将共享内存映射到用户空间哪个地址 比如:要映射到0x00800000地址,参数 (void *)0x00800000 填 NULL , 让系统自动映射 参数3: int shmflg: 0:默认方式操作,对共享内存可读可写 SHM_RDONLY:只读 返回值:指针 成功,返回共享内存映射到程序空间的地址位置 失败,返回(void *)-1 , 设置errno
shmdt():断开解除共享内存地址的映射,也就是停止映射地址,这时进程找不到这个地址
#include <sys/types.h> #include <sys/shm.h> int shmdt(const void *shmaddr); 功能:断开解除共享内存的映射 参数: const void *shmaddr:将哪一块程序空间映射的首地址与共享内存断开映射 返回值: 成功,返回0 失败,返回-1,设置错误码
shmctl():控制共享内存,可以获取内存空间的属性,还能将内存空间删除
#include <sys/ipc.h> #include <sys/shm.h> int shmctl(int shmid, int cmd, struct shmid_ds *buf); 功能:控制共享内存,常用于删除共享内存 参数: 参数1: int shmid:要控制的共享内存id 参数2: int cmd:控制命令 IPC_STAT:获取共享内存属性,获取的属性值存储到 第三个参数 指针变量 对应的地址空间 IPC_SET:设置共享内存属性,把 第三个参数 指针变量对应空间的属性值 设置到共享内存中 IPC_RMID:删除共享内存, 第三个参数为 NULL
创建一个共享内存:
//创建共享内存
#include<sys/types.h>
#include<sys/shm.h>
#include<stdio.h>
#include<unistd.h>
#include<string.h>
int main(int argc, const char *argv[])
{
//1、创建共享内存shmget
int shmid = shmget(20000,1024,IPC_CREAT|0664);
//2、映射共享内存地址shmat
void * shmaddr=shmat(shmid,NULL,0);
if(shmaddr==(void*)-1)
{
perror("shmat failed:");
return -1;
}
//3,对共享内存操作
char *p=shmaddr;
while(1)
{
//情况内存空间
bzero(p,1024);
//从终端获取数据
scanf("%s",p);
sleep(1);
}
//4、断开解除共享内存地址shmdt
int ret=shmdt(shmaddr);//映射的地址于共享内存空间地址断开
if( ret< 0)
{
perror("shmdat failed:");
return -1;
}
//5、删除共享内存
shmctl(shmid,IPC_RMID,NULL);
return 0;
}
三、信号灯/信号集
在介绍信号灯之前,我们要知道为什么我们需要引入信号灯这个概念
原因:就像上面我们说的在使用共享内存时,如果多个进程或线程同时访问共享内存,可能会出现竞态条件,导致程序行为不一致或错误。信号灯可以用来避免这些竞态条件,通过控制对共享内存的访问顺序来确保数据的一致性。
信号灯还可以用于控制对共享资源的访问,比如在生产者-消费者模型中,信号灯可以帮助管理生产者和消费者之间的协调,确保缓冲区的正确使用。
总之,信号灯作为一种同步机制,能有效地解决在并发环境中访问共享内存时可能出现的各种问题,确保程序的稳定性和正确性。
1、临界资源
临界资源定义:
当多个任务并发执行,访问同一个资源,将这个资源称之为临界资源
在进行通信时,需要引入同步互斥机制,避免产生竟争状态。保证任意时刻,都只有一个进程/线程处理临界资源(共享资源)
临界区:访问临界资源的代码,称之为临界区
例如:上面我们说到的共享内存就是一个临界资源,还有管道文件也是共享资源,如果多个进程或线程试图同时读写同一个管道,就需要进行适当的同步,以防止数据冲突或丢失。所以就引入了下面的同步互斥机制
2、同步互斥机制
1、互斥机制:
定义:保证临界区的完整性,临界区(对临界资源的访问)具有唯一性(同时只有一个进程/线程 进行操作,操作完之后另外的进程才能操作)。只能有一个进程访问共享资源(A访问,其他进程就不能访问;B访问,其他进程也不能访问),但是无法保证访问者的访问顺序
2、同步:
定义:在互斥的基础上,能够限制访问者的访问顺序
了解了上面的基础概念之后,我们来正式学习信号灯,在学习的过程中,可以将其看作日常生活中所看到交通信号灯;
3、信号灯的机制
定义:对要访问的共享资源的进程/线程,执行申请信号量的操作
信号灯三种情况:
1、当信号量/信号灯的值 > 0 ,则表示申请信号量成功(资源可以访问),进入临界区执行操作临界资源的代码(使用共享资源),同时把信号量的值 -1(进行申请操作)
2、当信号量的值 == 0,则申请信号量失败,当前进程/线程进入休眠,阻塞等待信号量的值 > 0
对于使用完共享资源的进程/线程,执行释放信号量的操作
3、将信号量的值+1,通知休眠阻塞的信号量的进程/线程(进行释放操作)
信号量的两种操作:
PV操作:信号量实现多进程、多线程同步互斥的方式
P:申请信号量,== 0 阻塞休眠,否则信号量-1操作
V:释放信号量,信号量 + 1操作
那么下面我们将来具体代码实现信号灯,首先要知道内核中,常用于创建和控制信号灯的函数;
4、信号灯的函数
下面我们说的信号灯和信号灯集是两个概念,信号灯集是信号灯的集合,多个信号灯才能组成一个信号灯集
semget():创建信号灯集
#include <sys/types.h> #include <sys/ipc.h> #include <sys/sem.h> int semget(key_t key, int nsems, int semflg); 功能:通过key值,创建或打开一个信号灯集(多个信号量的集合) 参数: 参数1: key_t key:key值,也要创建打开的信号量集 key 参数2: int nsems:信号 灯集中有几个信号灯 参数3: int semflg:选项 IPC_CREAT:如果key值对应的信号灯集不存在,则创建信号灯 IPC_CREAT | 0664 :创建的同时指定权限,如果信号灯集已经存在,则会忽略这个权限 返回值: 成功,返回信号灯集的id 失败,返回-1,设置errno
semop():控制某个信号灯的PV操作
#include <sys/types.h> #include <sys/ipc.h> #include <sys/sem.h> int semop(int semid, struct sembuf *sops, size_t nsops); 功能:操作信号灯集中信号灯值PV操作,P操作(申请信号量,-1,不够减阻塞),V操作(释放信号量,+1,唤醒) 参数: 参数1: int semid:指定要操作的信号灯集的id 参数2: struct sembuf *sops:P、V操作的选项,结构体指针,操作信息结构体的地址 struct sembuf { unsigned short sem_num;指定要操作的信号灯集中哪个信号灯,编号从0开始 short sem_op; P操作:负整数,如 -2,信号灯的值 -2,不够阻塞等待,一般 : -1 V操作:正整数,如 2,信号灯的值 +2 short sem_flg; 0:阻塞方式运行,上述操作就会阻塞运行 IPC_NOWAIT:非阻塞方式,p操作不够时,不会阻塞等待,通过返回值出错表示申请失败,不能操作共享资源 } 参数3: size_t nsops:要控制的信号灯集中信号灯的个数 返回值: 成功,返回0 失败,返回-1,设置errno
semctl():控制信号灯集
#include <sys/types.h> #include <sys/ipc.h> #include <sys/sem.h> int semctl(int semid, int semnum, int cmd, ...); 功能:控制信号灯集 参数: 参数1: int semid:指定要控制那个信号灯集 参数2: int semnum:指定要控制信号灯集中的哪个信号灯 参数3: int cmd:控制命令 IPC_RMID:删除信号灯集,第二个参数无意义,最后一个参数不用填 IPC_STAT:获取信号灯集的属性,存储到 第四个参数 指针地址对应空间,第四个参数为:struct semid_ds * buf IPC_SET:设置信号灯集的属性,把第四个参数 指针地址对应空间数据,设置到信号灯集中 GETVAL:获取信号灯集中,指定的信号灯的值,最后一个参数不用填,返回值 就是获取到的 信号量的值 SETVAL:设置信号灯集中,指定的信号灯的值,第四个参数,就是要设置的值 类型为:int 参数4: 根据参数3设置 返回值: 成功,返回0(有少量cmd,返回值为cmd对应的结果) 失败,返回-1
仔细看上面各个函数的参数,返回值,实现的功能,下面我们具体实现信号灯控制进程对于共享内存的访问
四、信号灯控制进程对共享内存的访问
首先,创建进程1,和进程2 ;进程当中必须使用同一个共享内存,否则失败。
进程1:
#include <sys/ipc.h>
#include <sys/shm.h>
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <sys/sem.h>
int main()
{
//共享内存通信
//1、创建打开共享内存
int shmid = shmget(20000,1024,IPC_CREAT|0664);
//if
//2、映射共享内存
void * shmaddr = shmat(shmid,NULL,0);
if(shmaddr == (void *)-1)
{
perror("shmat failed:");
return -1;
}
//信号量的通信
//P、V操作
//1、创建打开信号灯集
int semid = semget(20001,1,IPC_CREAT | 0664);
//if(semid < 0)
//获取信号灯集中,信号灯的值
if( semctl(semid,0,GETVAL) != 0 )
{
//设置信号灯的值为0
semctl(semid,0,SETVAL,0);
}
//3、通过共享内存地址 实现 对 共享内存进行操作
// int * p = shmaddr;
// *p = 10;//共享内存前4个字节
// *(p+1) = 20;
char * p = shmaddr;
while(1)
{
//从终端输入字符串到 共享内存
scanf("%s",p);
//2、操作信号量
//释放信号量 V 操作
struct sembuf semops;
semops.sem_num = 0;
semops.sem_op = 1;// +1 V操作
semops.sem_flg = 0;//阻塞方式执行
if(semop(semid,&semops,1) == 0)//判断 < 0
{
printf("v operation success\n");
}
//bzero(p,1024);
//把 hello world 写入 共享内存
//strcpy(p,"hello world");//拷贝字符串到共享内存
//sleep(1);
}
//4、在操作通信结束后,解除共享内存映射
if( shmdt(shmaddr) < 0 )
{
perror("shmdt failed:");
return -1;
}
//5、删除共享内存
shmctl(shmid,IPC_RMID,NULL);
//3、删除信号灯集
semctl(shmid,0,IPC_RMID);
return 0;
}
进程2:
#include <sys/ipc.h>
#include <sys/shm.h>
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <sys/sem.h>
int main()
{
//共享内存通信
//1、创建打开共享内存
int shmid = shmget(20000,1024,IPC_CREAT|0664);
//if
//2、映射共享内存
void * shmaddr = shmat(shmid,NULL,0);
if(shmaddr == (void *)-1)
{
perror("shmat failed:");
return -1;
}
//信号量的通信
//P、V操作
//1、创建打开信号灯集
int semid = semget(20001,1,IPC_CREAT | 0664);
//if(semid < 0)
//3、通过共享内存地址 实现 对 共享内存进行操作
char * p = shmaddr;
char buf[100];
while(1)
{
//2、操作信号量
//申请信号量 P 操作
struct sembuf semops;
semops.sem_num = 0;
semops.sem_op = -1;// -1 P操作
semops.sem_flg = 0;//阻塞方式执行
//阻塞申请信号量 -1
if(semop(semid,&semops,1) == 0)//判断 < 0
{
printf("p operation success\n");
}
printf("%s\n",p);//打印共享内存数据
bzero(p,1024);
}
//4、在操作通信结束后,解除共享内存映射
if( shmdt(shmaddr) < 0 )
{
perror("shmdt failed:");
return -1;
}
//5、删除共享内存
shmctl(shmid,IPC_RMID,NULL);
//3、删除信号灯集
semctl(shmid,0,IPC_RMID);
return 0;
}
注意:代码当中的函数使用不清楚的看上面的函数介绍,我们通过进程1来发送信号灯进行P操作,也就是将原本的信号灯值变为1,进程二调用的后,获取到信号灯值大于0,执行操作,将信号灯值变为0,参数中的-1是传递参数到函数中,将信号灯值-1变为0,而不是直接赋值;然后执行下面的打印输出,如果信号灯值为0,也就是在进程2中,信号灯值1-1=0之后,进程2进入休眠,等待进程1将信号灯值进行+1操作,否则继续休眠;
总结
本文主要简述了信号灯如何对进程访问临界资源进行控制, 这里主要讲述的是对进程访问共享内存进行控制,读者也可以下去使用