本片博客会粘贴部分代码,想要了解更多代码信息,可访问小编的GitHub关于本篇的代码
这里有涉及的mmap的知识
下图为共享内存原理图
因为共享内存是直接将申请来的一块物理内存映射到虚拟地址空间中,允许两个或多个进程共享,因此进行数据传输的时候相较于其它进程间通信方式,少了两步用户态与内核态数据拷贝的过程,因此共享内存是最快的进程间通信方式。
- 创建共享内存
int shmget(key_t key, size_t size, int shmflg);
key: 操作系统上ipc标识
size: 要创建的共享内存大小
shmflg:
IPC_CREAT|IPC_EXCL|0664
返回值:操作句柄 失败:-1
- 内存映射
void *shmat(int shmid, const void *shmaddr, int shmflg);
shmid: 操作句柄
shmaddr: 映射起始地址,NULL(操作系统分配)
shmflg: SHM_RDONLY–只读 否则读写
成功返回:映射的虚拟地址空间首地址
失败返回:(void*)-1
- 解除映射
int shmdt(const void *shmaddr);
shmaddr 共享内存的映射首地址
返回值:成功:0 失败:-1
- 删除共享内存
int shmctl(int shmid, int cmd, struct shmid_ds *buf);
shmid: 句柄
cmd: IPC_RMID 删除
buf: 用于接收共享内存描述信息,不关心可以置空
操作系统:如果有进程依然与共享内存保持映射连接关系,那么共享内存将不会被立即删除,而是等最后一个映射断开后删除 ,在这期间,将拒绝其他进程映射。
服务端
/*这是共享内存的服务端,目的是:往共享内存写入数据后*/
#include<stdio.h>
#include<unistd.h>
#include<stdlib.h>
#include<errno.h>
#include<string.h>
#include<sys/ipc.h>
#include<sys/shm.h>
//定义一个ipc标识符
#define KEY 0x520
int main()
{
umask(0);
//创建共享内存
//int shmget(key_t key, size_t size, int shmflg);
int shmid=-1;
shmid=shmget(KEY,520,IPC_CREAT|0664);
if(shmid<0){
perror("shmget error");
return -1;
}else
{
//映射到虚拟地址空间
//void *shmat(int shmid, const void *shmaddr, int shmflg);
void*shmhead=shmat(shmid,NULL,0);//shmaddr是要映射的起始地址,NULL表示由系统分配,
//shmflg如果是SHM_RDONLY则只读权限,否则读写
if(shmhead==(void*)-1)
{
perror("shmat error");
return -1;
}
while(1)
{
//映射成功,写操作
memset(shmhead,0x00,520);
scanf("%s",(char*)shmhead);
sleep(1);
}
//解除映射int shmdt(const void *shmaddr);
shmdt(shmhead);
//删除共享内存int shmctl(int shmid, int cmd, struct shmid_ds *buf);
shmctl(shmid,IPC_RMID,NULL);
}
return 0;
}
客户端
/*这是共享内存的客户端,目的:隔一秒钟,去共享内存取一次数据*/
#include<stdio.h>
#include<unistd.h>
#include<stdlib.h>
#include<errno.h>
#include<string.h>
#include<sys/ipc.h>
#include<sys/shm.h>
//定义一个ipc标识符
#define KEY 0x520
int main()
{
umask(0);
//创建共享内存
//int shmget(key_t key, size_t size, int shmflg);
int shmid=shmget(KEY,520,IPC_CREAT|0664);
if(shmid<0){
perror("shmget error");
return -1;
}else
{
//映射到虚拟地址空间
//void *shmat(int shmid, const void *shmaddr, int shmflg);
void*shmhead=shmat(shmid,NULL,SHM_RDONLY);//shmaddr是要映射的起始地址,NULL表示由系统分配,
//shmflg如果是SHM_RDONLY则只读权限,否则读写
if(shmhead==(void*)-1)
{
perror("shmat error");
return -1;
}
while(1)
{
//映射成功,读取数据
printf("memshare:%s\n",(char*)shmhead);
sleep(1);
}
//解除映射int shmdt(const void *shmaddr);
shmdt(shmhead);
//删除共享内存int shmctl(int shmid, int cmd, struct shmid_ds *buf);
shmctl(shmid,IPC_RMID,NULL);
}
return 0;
}
用于实现进程间的同步与互斥(进程/线程安全概念),保证进程间对临界资源的安全有序访问。
多个进程同时操作一个临界资源的时候就需要通过同步与互斥机制来实现临界资源的安全访问
同步:保证对临界资源访问的时序的可控性
互斥:对临界资源同一时间的唯一访问性
本质:具有一个等待队列的计数器(代表现在还有没有资源使用),当信号量没有资源可用时,这时候需要阻塞等待。
同步:只有信号量资源计数从0转变为1的时候,才会通知别人,打断阻塞等待,去操作临界资源。
互斥:同一时间,A获取了信号量的资源,其它进程就没有办法获取资源了。
二元信号量:信号量如果想要实现互斥,那么它的计数器只能是0或1
*P操作: 获取信号量资源说的是对计数器进行-1操作
*V操作:释放信号量资源说的是对计数器进行+1操作
进程在操作临界资源之前先获取信号量资源,判断是否可以对临界资源进行操作,如果信号量没有资源了(计数器为0),则需要等待,当别人释放信号量计数器变为1,才会唤醒等待的进程去重新获取信号量资源。
-
信号量数据大于0,代表信号量有资源,可以操作,
信号量资源等于0,代表信号量没有资源,需要等待。 -
信号量作为进程间通讯方式,意味着大家都能访问到信号量,实际上也是一个临界资源,但是信号量的这个临界资源的操作是不会出问题的,因为信号量的操作是一个原子操作。
-
创建信号量
int semget(key_t key, int nsems, int semflg);
key: IPC_KEY标识
nsems:指定这次要创建的信号量个数
semflg:IPC_CREAT|IPC_EXCL|0664
返回值:操作信号量句柄
失败:-1
- 设置信号量初值
int semctl(int semid, int semnum, int cmd, ...);
semid:信号量句柄
semnum:指定操作的是第几个信号量
cmd:SETVAL设置单个信号量的初值,SETALL设置所有信号量的初值,semnum将被忽略,IPC_RMID删除信号量,…将被NULL
成功返回值:0
失败返回值:-1
...
是一个不定参数:
对于不同的操作 | 函数会对对应的操作的联合结构中的对象操作 |
---|---|
设置单个信号量的初值 | int val; /* Value for SETVAL */ |
设置所有信号量的初值 | unsigned short *array; /* Array for GETALL, SETALL */ |
union semun
{
int val; /* Value for SETVAL */
struct semid_ds *buf; /* Buffer for IPC_STAT, IPC_SET */
unsigned short *array; /* Array for GETALL, SETALL */
struct seminfo *__buf; /* Buffer for IPC_INFO
(Linux-specific) */
};
- 获取/释放信号量资源
int semop(int semid, struct sembuf *sops, unsigned nsops);
semid:信号量句柄
nsops操作的信号量个数
这个sembuf在库里是这样的
struct sembuf
{
unsigned short sem_num; /* semaphore number */
short sem_op; /* semaphore operation */
short sem_flg; /* operation flags */
}
PV操作:为了方便操作获取、释放资源,我们封装了两个函数
获取资源:
void sem_P(int id)
{
struct sembuf buf;
buf.sem_num = 0; //信号量编号,因为下面的操作值创建了一个信号量,编号为0
buf.sem_op = -1; //信号量操作,获取资源,信号量资源-1
buf.sem_flg = SEM_UNDO; //SEM_UNDO,如果该进程意外退出,则会自动释放该资源
semop(id, &buf, 1);
}
释放资源:
void sem_V(int id)
{
struct sembuf buf;
buf.sem_num = 0; //信号量编号,因为下面的操作值创建了一个信号量,编号为0
buf.sem_op = 1; //信号量操作,释放资源,信号量资源+1
buf.sem_flg = SEM_UNDO; //SEM_UNDO,如果该进程意外退出,则会自动释放该资源
semop(id, &buf, 1);
}
- 删除信号量
semctl(semid, 0,IPC_RMID,NULL);
信号量实现同步
//信号量实现同步
#include<stdio.h>
#include<unistd.h>
#include<stdlib.h>
#include<string.h>
#include<sys/ipc.h>
#include<errno.h>
#include<sys/sem.h>
#define IPC_KEY 0x999
union semun {
int val; /* Value for SETVAL */
struct semid_ds *buf; /* Buffer for IPC_STAT, IPC_SET */
unsigned short *array; /* Array for GETALL, SETALL */
struct seminfo *__buf; /* Buffer for IPC_INFO
(Linux-specific) */
};
void sem_P(id)
{
struct sembuf buff;
buff.sem_num=0;
buff.sem_op=-1;
buff.sem_flg=SEM_UNDO;
semop(id,&buff,1);
}
void sem_V(id)
{
struct sembuf buff;
buff.sem_num=0;
buff.sem_op=1;
buff.sem_flg=SEM_UNDO;
semop(id,&buff,1);
}
int main()
{
umask(0);
int semid = semget(IPC_KEY,1,IPC_CREAT|0644);
if(semid<0){
perror("semget error");
return -1;
}
union semun val;
//设置信号量初值int semctl(int semid, int semnum, int cmd, ...);
val.val=0;
semctl(semid,0,SETVAL,val);
int pid = -1;
pid=fork();
if(pid<0){
perror("pid error");
return -1;
}else if(pid==0)
{
//子进程去获取资源,吃方便面
while(1)
{
sem_P(semid);
printf("我吃了一包方便面\n");
sleep(1);
}
}
else{
while(1){
sem_V(semid);
printf("我制造了一包方便面\n");
sleep(1);
}
}
semctl(semid,0,IPC_RMID,NULL);
}
信号量实现互斥
这是一个基于信号量的互斥操作, 让子进程打印A睡1000us然后再打印一个A 让父进程打印B睡1000us然后再打印一个B
检查结果是否是连续的?
如何让打印结果是我们预期的AA BB这种形式
关键点就在于两个进程的打印操作都不能被打断,这时候就需要使用一个一元信号量来完成互斥操作
#include<stdio.h>
#include<unistd.h>
#include<stdlib.h>
#include<errno.h>
#include<string.h>
#include<sys/ipc.h>
#include <sys/sem.h>
//定义IPC标识
#define IPC_KEY 0x666
union semun {
int val; /* Value for SETVAL */
struct semid_ds *buf; /* Buffer for IPC_STAT, IPC_SET */
unsigned short *array; /* Array for GETALL, SETALL */
struct seminfo *__buf; /* Buffer for IPC_INFO
(Linux-specific) */
};
void sem_P(int id)
{
struct sembuf buf;
buf.sem_num=0;
buf.sem_op=-1;
buf.sem_flg=SEM_UNDO;
semop(id,&buf,1);
}
void sem_V(int id)
{
struct sembuf buf;
buf.sem_num=0;
buf.sem_op=1;
buf.sem_flg=SEM_UNDO;
semop(id,&buf,1);
}
int main()
{
umask(0);
//1、创建信号量int semget(key_t key, int nsems, int semflg);
int semid=-1;
semid=semget(IPC_KEY,1,IPC_CREAT|0664);
if(semid<0){
perror("semget error");
return -1;
}
union semun godo;
godo.val=1;
//2、设置信号量初值int semctl(int semid, int semnum, int cmd, ...);
semctl(semid,0,SETVAL,godo);
//创建子进程
int pid=-1;
pid=fork();
if(pid<0){
perror("fork error");
return -1;
}else if(pid==0){
//子进程获取信号量,打印A,睡1000us再打印A,再释放资源,轮回
while(1)
{
//获取资源
sem_P(semid);
printf("A");
fflush(stdout);
usleep(1000);
printf("A");
fflush(stdout);
//释放资源
sem_V(semid);
}
}else{
//父进程
while(1)
{
sem_P(semid);
printf("B");
fflush(stdout);
usleep(1000);
printf("B ");
fflush(stdout);
//释放资源
sem_V(semid);
}
}
//删除信号量
semctl(semid,0,IPC_RMID,NULL);
return 0;
}
- 将二元信号量P/V操作,封装成动态/静态库,并分别使用并测试
-
生产者消费者原理
- 同步:生产者消费者问题,主要是通过生产者生产出产品放入缓冲区,然后消费者从缓冲区中拿出产品消费。
- 互斥:生产者和消费者不能同时在临界区进行操作,同一时间只能有一个生产者或者一个消费者在临界区进行操作。