一、共享内存原理
共享内存为多个进程之间共享和传递数据提供了一种有效的方式。共享内存是先在物理内存上申请一块空间,多个进程可以将其映射到自己的虚拟地址空间中。所有进程都可以访问共享内存中的地址,就好像它们是由malloc分配的一样。如果某个进程向共享内存写入了数据,所做的改动将立刻被可以访问同一段共享内存的任何其他进程看到。由于它并未提供同步机制,所以我们通常需要用其他的机制来同步对共享内存的访问。
如果将一块物理内存分配给了A进程,那么就不能再把它分配给B进程了。但是对于共享内存来说是可以的,可以把一块共享内存既映射到A进程的虚拟地址空间中,也可以把它映射到B进程的虚拟地址空间中,也就是说进程A和进程B的逻辑内存中实际底层对应的物理内存有一部分是重叠的,进程A和进程B共同使用这同一块物理内存。
【注意】
共享内存和管道的区别:
对于管道来说,首先要有一个进程把数据写到管道中,然后另一个进程再从管道中将数据读出来,相当于一个进程把数据复制到管道中,另一个进程又将数据从管道中复制出来。对于共享内存,两个或者多个进程用的是同一块物理空间,数据放到共享内存中,每一个进程可以直接将数据写入共享内存或者直接读取共享内存中的数据,只要有一个进程将数据写入了共享内存,这个进程和其他进程都可以访问这些数据。
二、共享内存的使用
1.操作共享内存的接口介绍
(1)shmget()创建共享内存
int shmget(key_t key, size_t size, int shmflg);
shmget()用于创建或者获取共享内存
shmget()成功返回共享内存的ID, 失败返回-1
key
:不同的进程使用相同的key值可以获取到同一个共享内存
size
:创建共享内存时,指定要申请的共享内存空间大小
shmflg
: 如果是创建新的共享内存需要使用IPC_CREAT,IPC_EXCL;如果是已经存在的可以使用IPC_EXCL或者直接传0。
(2)shmat()映射共享内存
void* shmat(int shmid, const void *shmaddr, int shmflg);
shmat()将申请的共享内存的物理内存映射到当前进程的虚拟地址空间上
shmat()成功返回返回共享内存的首地址,失败返回NULL
shmid
:共享内存标识符
shmaddr
:shmaddr=0,则存储段连接到由内核选择的第一个可用地址上。所以一般给NULL,由系统自动选择映射的虚拟地址空间 。
shmflg
: 一般给0, 可以给SHM_RDONLY为只读模式,其他的为读写 。
(3)shmdt()断开共享内存
当进程间完成通信时,就需要去关联共享内存,需要将进程从之前映射的共享内存上脱离下来。
int shmdt(const void *shmaddr);
shmdt()断开当前进程的shmaddr指向的共享内存映射
shmdt()成功返回0, 失败返回-1
(4)shmctl()释放共享内存
当结构体中shm_nattch为0时就需要释放这块共享内存,否则就会一直占用下去,因为共享内存的生命周期是跟随内核的。
int shmctl(int shmid, int cmd, struct shmid_ds *buf);
shmctl()控制共享内存
shmctl()成功返回0,失败返回-1
shmid
:共享内存标识符
cmd
:指定执行的操作,设置为 IPC_RMID 时表示释放共享内存
buf
:设置为空就行
2.使用共享内存
(1)进程a向共享内存中写入数据,进程b从共享内存中读取数据并显示
a.c代码:
b.c代码:
运行结果:
(2)进程a从键盘循环获取数据并写入到共享内存中,进程b从共享内存中获取并打印数据。要求进程a输入一次,进程b输出一次,进程a不输入,进程b也不输出。
a.c代码:
运行结果:
由结果可以看出,进程a循环向共享内存中写入数据之后,进程b就会循环且不停地从共享内存中读取数据。
要做到进程a输入一次,进程b输出一次,进程a不输入,进程b也不输出,就要将信号量引入,通过p、v操作解决这个问题。
【分析】需要创建两个信号量,第一个信号量的初始值为1,第二个信号量的初始值为0。在a程序执行向共享内存中写入数据之前执行第一个信号量的p操作,第一个信号量的值变为了0,在a程序执行完向共享内存中写入数据之后执行第二个信号量的v操作,第二个信号量的值变为了1,在b程序执行向共享内存中读取数据之前执行第二个信号量的p操作,第二个信号量的值变为0,在b程序执行完向共享内存中读取数据之后执行第一个信号量的v操作,第一个信号量的值变为1。
也就是说,现在如果先执行b程序,此时因为第二个信号量的初始值为0,执行不了p操作,就会发生阻塞。接着执行a程序,因为第一个信号量的初始值为1,执行p操作成功,第一个信号量的值变为0,然后写入数据到共享内存成功,执行第二个信号量的v操作,因为第二个信号量的初始值为0,执行完v操作之后变为1,这时b程序就可以执行对第二个信号量的p操作,执行成功,第二个信号量的值又变为0,b程序读取共享内存的数据成功,然后执行对第一个信号量的v操作,第一个信号量变为1。就这样通过信号量的p、v操作一直循环写入和读取,就实现了a进程输入一次,b进程读取一次,a进程不输入,b进程也不输出。
信号量接口的实现:
sem.h代码:
sem.c代码:
a.c代码:
b.c代码:
运行结果:
三、查看共享内存的信息
在命令行通过ipcs -m
查看共享内存的信息:
在运行a程序之后,可以看到此时共享内存被创建,且连接数有1个。
结束a进程之后,共享内存仍然存在,但是连接数变为了0:
通过命令行释放该共享内存:
方法:
ipcrm -m 1 # ipcrm -m 共享内存的id
结果:
【总结】
(1)共享内存允许两个不相关的进程访问同一个逻辑内存。共享内存是在两个正在运行的进程之间传递数据的一种非常有效的方式。大多数共享内存的具体实现,都把由不同进程之间共享的内存安排为同一段物理内存。
(2)共享内存是由进程间通信创建的一个特殊的地址范围,它将出现在该进程的地址空间中。其他进程可以将同一段共享内存连接到它们自己的地址空间中。所有进程都可以访问共享内存中的地址。如果某个进程向共享内存写入了数据,所做的改动将立刻被可以访问同一段共享内存的任何进程看到。
(3)共享内存为在多个进程之间共享和传递数据提供了一种有效的方式。
(4)共享内存并没有提供同步机制,通常需要其他的机制来同步对共享内存的访问。一般用共享内存来提供对大块内存区域的有效访问,同时通过传递小消息来同步对该内存的访问。
(5)在第一个进程结束对共享内存的写操作之前,并没有自动的机制可以阻止第二个进程开始对它进行读取。对共享内存访问的同步控制必须由程序员来负责。
(6)共享内存的生命周期是跟随内核的,进程退出后,共享内存依然存在,共享内存不属于任何进程,不归进程管理,除非调用释放函数,否则要不要释放是OS决定的,因此共享内存的生命周期是随内核的,所以如果不调用释放函数释放这块共享内存,否则就会一直占用下去。释放共享内存的方式有三种,分别是关机、调用释放共享内存的函数 shmctl和命令行释放,命令行释放的方法: