内存映射 及 共享内存
文章目录
1.内存映射(mmap)
推荐B站的大丙老师,讲课很干货
1.1 创建内存映射区
如果要实现进程间通信,可以通过函数创建一块内存映射区 ,和管道不同的是管道对应的内存空间在内核中,而内核映射区对应的内存空间在进程的用户区(用于加载动态库的区域),及
进程通信使用的内存映射区不是一块,而是在每个进程内部都有一块
;
每个进程的地址空间是独立的,各个进程之间也不能直接访问其他地址的内存映射区,需要通信的进程需要将各自的内存映射区和同一磁盘文件进行映射 ,这样进程之间就可以通过磁盘文件这个唯一的桥梁完成数据的交互。
小结:
磁盘文件数据可以完全加载到进程的内存映射区也可以部分加载到进程的内存映射区,当进程A中的内存映射区数据被修改了,数据会自动同步到磁盘文件,同时和磁盘文件建立映射关系的其他进程内存映射区中的数据也会和磁盘文件进行数据实时同步,该同步机制保证了各进程之间的数据共享;
使用内存映射区可以使无论有无血缘关系的进程间通信
#include <sys/mman.h>
//创建内存映射区
void *mmap(void *addr , size_t length , int port , int flags , int fd , off_t offset);
参数:
-
addr: 从动态库加载区的具体位置开始创建内存映射区,默认为NULL,委托内核分配;
-
length: 创建内存映射区的大小(字节),实际上按照4k的整数倍分配;
-
port: 对内存映射区的操作权限;
PROT_READ :
读内存映射区;PROT_WRITE :
写内存映射区;PROT_READ | PROT_WRITE :
读写权限;
flags:
MAP_SHARED :
多个进程可以共享数据,进行映射数据同步;MAP_PRIVATE :
映射区是私有的,不能同步给其他进程;
fd: 文件描述符,对应一个打开的磁盘文件,内存映射区通过该文件描述符和磁盘文件建立关联;
offset: 磁盘文件的偏移量,文件从偏移的位置开始进行数据映射,使用这个参数需要注意:
- 偏移量必须大于4k的整数倍 , 写0代表不偏移;
- 这个参数必须是大于0的;
返回值:
成功: 返回一个内存映射区的其实地址;
失败: MAP_FAILED
(that is ,(void*)-1)
mmap () 函数的参数相对较多,在使用该函数创建用于进程间通信的内存映射区的时候,各参数的指定都有一些注意事项,具体如下:
/*
1. 第一个参数:addr 指定 NULL
2. 第二个参数:length 必须要 >0
3. 第三个参数: port ,进程间通信需要对内存映射区有读写权限:PORT_READ|PORT_WRITE
4. 第四个参数:flags,如果需要进程通信,指定:MAP_SHARED
5. 第五个参数:fd,打开的文件必须大于0,进程间通信需要文件操作权限和映射区操作权限相同
内存映射区创建成功之后,关闭这个文件描述符不会影响进程间通信;
6. 第六个参数:offset ,不偏移指定为0,如果偏移必须是4k的整数倍;
*/
内存映射区使用完之后也需要释放,释放函数原型:
int munmap(void *addr,size_t length);
参数:
addr: mmap()的返回值,创建内存映射区的起始地址;
length: 和mmap()第二个参数相同;
返回值: 函数调用成功 返回0 ,失败返回 -1;
1.2 进程间通信
操作内存映射区和操作管道是不一样的,得到内存映射区之后直接对内存地址进行操作 ,管道是通过文件描述符读写队列中的数据,管道的读写是阻塞的,内存映射区的读写是非阻塞的 。内存映射区创建成功之后,得到映射区的起始地址,使用相关的内存操作函数读写数据即可;
1.2.1 有血缘关系
由于创建子进程会将虚拟地址空间复制,那么在父进程中创建的内存映射区也会被复制到子进程中,在子进程里面就可以直接使用这块内存映射区,即对有血缘关系的进程,进行进程间通信相对简单;
/*
1. 先创建一个内存映射区,获得一个起始地址,使用ptr指针保存这个地址
2. 通过fork()函数创建子进程 ->子进程中有一个内存映射区,子进程中也有一个ptr指针指向该地址
3. 父进程向自己内存映射区写数据,数据同步到磁盘文件中,磁盘文件又同步到子进程的映射区,子 进程从自己的映射区往外读数据, 这个数据就是父进程写的
*/
#include <sys/mman.h>
#include <fcntl.h>
int main()
{
// 1. 打开一个磁盘文件
int fd = open("./english.txt", O_RDWR);
// 2. 创建内存映射区
void* ptr = mmap(NULL, 4000, PROT_READ|PROT_WRITE,
MAP_SHARED, fd, 0);
if(ptr == MAP_FAILED)
{
perror("mmap");
exit(0);
}
// 3. 创建子进程
pid_t pid = fork();
if(pid > 0)
{
// 父进程, 写数据
const char* pt = "我是你爹, 你是我儿子吗???";
memcpy(ptr, pt, strlen(pt)+1);
}
else if(pid == 0)
{
// 子进程, 读数据
usleep(1); // 内存映射区不阻塞, 为了让子进程读出数据
printf("从映射区读出的数据: %s\n", (char*)ptr);
}
// 释放内存映射区
munmap(ptr, 4000);
return 0;
}
1.2.2 没有血缘关系
没有血缘关系的进程间通信,需要在每个进程中分别创建内存映射区,但是这些进程的内存映射区必须关联相同的磁盘文件才能实现进程间的数据同步。
A进程代码:
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <sys/mman.h>
#include <fcntl.h>
int main()
{
// 1. 打开一个磁盘文件
int fd = open("./english.txt", O_RDWR);
// 2. 创建内存映射区
void* ptr = mmap(NULL, 4000, PROT_READ|PROT_WRITE,
MAP_SHARED, fd, 0);
if(ptr == MAP_FAILED)
{
perror("mmap");
exit(0);
}
const char* pt = "==================我是你爹, 你是我儿子吗???****************";
memcpy(ptr, pt, strlen(pt)+1);
// 释放内存映射区
munmap(ptr, 4000);
return 0;
}
B进程代码:
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <sys/mman.h>
#include <fcntl.h>
int main()
{
// 1. 打开一个磁盘文件
int fd = open("./english.txt", O_RDWR);
// 2. 创建内存映射区
void* ptr = mmap(NULL, 4000, PROT_READ|PROT_WRITE,
MAP_SHARED, fd, 0);
if(ptr == MAP_FAILED)
{
perror("mmap");
exit(0);
}
// 读内存映射区
printf("从映射区读出的数据: %s\n", (char*)ptr);
// 释放内存映射区
munmap(ptr, 4000);
return 0;
}
1.3 拷贝文件
使用内存映射区除了可以实现进程间通信,同样可以实现文件的拷贝,使用该方式拷贝文件可以减少程序员的工作量,我们只需要负责创建内存映射区和打开的磁盘文件;
使用内存映射区拷贝文件思路:
- 打开被拷贝文件,得到文件描述符fd1 ,并计算出这个文件的大小 size;
- 创建内存映射区A并且和被拷贝文件关联,和fd1关联,得到映射区地址ptrA;
- 创建新文件,得到文件描述符fd2,用于存储被拷贝的数据,并将该文件大小拓展为size;
- 创建内存映射区B并且和新创建的文件关联,及和fd2关联起来,得到映射区地址ptrB;
- 进程地址空间之间的数据拷贝,memcpy(ptrB,ptrA,size) ,数据自动同步到新建文件中;
- 关闭内存映射区;
文件拷贝代码:
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <fcntl.h>
#include <sys/mman.h>
int main()
{
// 1. 打开一个操盘文件english.txt得到文件描述符
int fd = open("./english.txt", O_RDWR);
// 计算文件大小
int size = lseek(fd, 0, SEEK_END);
// 2. 创建内存映射区和english.txt进行关联, 得到映射区起始地址
void* ptrA = mmap(NULL, size, PROT_READ|PROT_WRITE, MAP_SHARED, fd, 0);
if(ptrA == MAP_FAILED)
{
perror("mmap");
exit(0);
}
// 3. 创建一个新文件, 存储拷贝的数据
int fd1 = open("./copy.txt", O_RDWR|O_CREAT, 0664);
// 拓展这个新文件
ftruncate(fd1, size);
// 4. 创建一个映射区和新文件进行关联, 得到映射区的起始地址second
void* ptrB = mmap(NULL, size, PROT_READ|PROT_WRITE, MAP_SHARED, fd1, 0);
if(ptrB == MAP_FAILED)
{
perror("mmap----");
exit(0);
}
// 5. 使用memcpy拷贝映射区数据
// 这两个指针指向两块内存, 都是内存映射区
// 指针指向有效的内存, 拷贝的是内存中的数据
memcpy(ptrB, ptrA, size);
// 6. 释放内存映射区
munmap(ptrA, size);
munmap(ptrB, size);
close(fd);
close(fd1);
return 0;
}
2.共享内存
共享内存不同于内存映射区,它不属于任何进程,不受进程生命周期的影响;
通过调用Linux系统提供的系统函数就可以得到这块共享内存,使用之前让进程和共享内存进行关联,得到共享内存的起始地址之后就可以直接进行读写操作了。同样进程也可以和这块共享内存解除关联,解除关联之后就不能操作这块共享内存了。在所有进程间通信的方式中共享内存的效率是最高的
共享内存默认不阻塞,
当多个进程同时读写共享内存,可能会导致数据混乱
;共享内存需要借助其他机制保证进程间的数据同步(信号量)
2.1 创建/打开共享内存
2.1.1 shmget 函数
如果共享内存不存在就需要先创建出来,如果已经存在了就需要先打开该共享内存; 打开/创建共享内存的函数原型:
#include <sys/ipc.h>
#Include <sys/shm.h>
int shmget(key_t key , size_t size , int shmflg);
参数:
-
key: key_t整形数,
通过key可以创建或打开一块共享内存,该参数的值一定要大于0;
-
size: 创建共享内存的时候,指定共享内存的大小;当打开一块存在的共享内存,size没有意义;
-
shmflg: 创建共享内存的时候指定的属性;
IPC_CREAT:
创建新的共享内存,如果创建共享内存,需要指定对共享内存的操作权限(例:IPC_CREAT|0664);IPC_EXCL:
检测共享内存是否已经存在了,必须和IPC_CREAT 一起使用;
返回值: 共享内存创建或打开成功返回标识共享内存的唯一的ID
,失败返回 -1;
函数使用案例:
#include <sys/ipc.h>
#include <sys/shm.h>
//创建一个大小为4k的共享内存
shmget(100 , 4096 ,IPC_CREAT|0644);
//创建一个大小为4k的共享内存,并且检测是否存在
//如果共享内存已经存在,创建失败,返回-1
shmget(100,4096,IPC_CREAT|0644|IPC_EXCL);
//打开一块已经存在的共享内存
// 函数参数虽然指定了大小和IPC_CREAT, 但是都不起作用, 因为共享内存已经存在, 只能打开, 参数4096也没有意义
shmget(100, 4096, IPC_CREAT|0664);
shmget(100, 0, 0);
//打开一块共享内存 ,如果不存在就新建
shmget(100,4096,IPC_CREAT|0644);
2.1.2 ftok 函数
shmget()函数的第一个参数是一个大于0的正整数 ,如果不想自己指定可以通过ftok()函数直接生成key值
;
#include <sys/types.h>
#include <sys/ipc.h>
//将两个参数作为种子,生成一个 key_t 类型的数值
key_t ftok(const char *pathname , int proj_id);
参数:
pathname: 当前操作系统中一个存在的路径;
proj_id: 该参数只占据 int 中一个字节 ,传参的时候要将其作为char进行操作,取值:1~255
返回值: 函数调用成功返回一个可用于创建、打开共享内存的key值 ,调用失败返回: -1;
#include <sys/ipc.h>
#include <sys/types.h>
//根据路径生成一个key_t
key_t key = ftok("/home/liu",'a');
//创建或打开共享内存
shmget(key,4096,IPC_CREAT|0644);
2.2 关联和解除关联
2.2.1 shmat 函数
创建
/打开共享内存之后必须和共享内存进行关联
,这样才能得到共享内存的起始地址,通过得到的内存地址进行读写操作;
void *shmat(int shmid , const void *shmaddr , int shmflg);
参数:
shmid: 要操作的共享内存ID,是shmget()函数返回值;
shmaddr: 共享内存的起始地址 ,内核指定,写NULL;
shmflg: 对共享内存操作权限
SHM_RDONLY:
读权限0:
读写权限
返回值: 关联成功,返回共享内存的起始地址
; 失败返回 (void*)-1
2.2.2 shmdt 函数
进程和共享内存解除关联
,如果没有该操作,进程结束后进程和共享内存的关联也会自动解除;
int shmdt(const void *shmaddr);
参数: shmat() 函数的返回值 ,共享内存的起始地址;
返回值: 关联解除成功返回 0
; 失败返回 -1;
2.3 删除共享内存
2.3.1 shmctl 函数
shmctl()函数是多功能函数,可以设置、获取共享内存的状态也可以将共享内存标记为删除状态。当共享内存被标记为删除状态,并不会马上被删除,知道所有进程全部和共享内存解除关联后,共享内存才会被删除。
通过shmtcl()函数只是标记删除的共享内存,在程序中多次调用时没用的;
//共享内存控制函数
int shmctl (int shmid , int cmd , struct shmid_ds *buf);
//参数 struct shmid_ds 结构体原型
struct shmid_ds {
struct ipc_perm shm_perm; /* Ownership and permissions */
size_t shm_segsz; /* Size of segment (bytes) */
time_t shm_atime; /* Last attach time */
time_t shm_dtime; /* Last detach time */
time_t shm_ctime; /* Last change time */
pid_t shm_cpid; /* PID of creator */
pid_t shm_lpid; /* PID of last shmat(2)/shmdt(2) */
// 引用计数, 多少个进程和共享内存进行了关联
shmatt_t shm_nattch; /* 记录了有多少个进程和当前共享内存进行了管联 */
...
};
参数:
-
shmid: 要操作的共享内存ID,shmget()函数的返回值;
-
cmd: 下一步要做的操作
IPC_STAT:
得到当前共享内存的状态;IPC_SET:
设置当前共享内存的状态;IPC_RMID:
标记共享内存要被删除;
buf:
cmd == IPC_STAT
,作为传出参数,会得到共享内存的相关属性;cmd == IPC_SET
,作为传入参数,将用户的自定义属性设置到共享内存中;
返回值: 函数调用成功 返回 大于等于 0
; 调用失败返回 -1;
2.3.2 相关shell命令
- 使用
ipcs
添加参数-m
可以查看系统中共享内存的详细信息;
liu@liu-Ubuntu:~$ ipcs -m
------------ 共享内存段 --------------
键 shmid 拥有者 权限 字节 连接数 状态
0x00000000 6 liu 600 16777216 2 目标
0x00000000 9 liu 600 134217728 2 目标
0x00000000 12 liu 600 524288 2 目标
0x00000000 13 liu 600 524288 2 目标
0x00000000 14 liu 600 524288 2 目标
0x00000000 17 liu 600 524288 2 目标
0x00000000 18 liu 600 524288 2 目标
0x00000000 19 liu 600 19488 2 目标
0x00000000 21 liu 600 68400 2 目标
使用 ipcrm
命令可以标记删除某块共享内存
# key == shmget的第一个参数
$ ipcrm -M shmkey
# id == shmget的返回值
$ ipcrm -m shmid
2.3.3 共享内存状态
// 参数 struct shmid_ds 结构体原型
struct shmid_ds {
struct ipc_perm shm_perm; /* Ownership and permissions */
size_t shm_segsz; /* Size of segment (bytes) */
time_t shm_atime; /* Last attach time */
time_t shm_dtime; /* Last detach time */
time_t shm_ctime; /* Last change time */
pid_t shm_cpid; /* PID of creator */
pid_t shm_lpid; /* PID of last shmat(2)/shmdt(2) */
// 引用计数, 多少个进程和共享内存进行了关联
shmatt_t shm_nattch; /* 记录了有多少个进程和当前共享内存进行了管联 */
...
};
共享内存的信息存储在 struct shmid_ds 的结构体
中,其中有一个非常重要的成员 shm_nattch
,该成员变量记录着当前共享内存关联的进程个数(引用计数)。当共享内存被标记为删除状态,并且引用计数为0之后共享内存才会被删除。
当共享内存被标记为删除状态之后,共享内存的状态也会发生变化,共享内存内部维护的 key 从一个正整数变为 0,其属性从公共的变为私有的。这里的私有是指只有已经关联成功的进程才允许继续访问共享内存,不再允许新的进程和这块共享内存进行关联了。 下图演示了共享内存的状态变化:
2.4 进程间通信
使用共享内存实现进程间通信的流程:
/*
1. 调用Linux系统API创建一块共享内存
---此块内存不属于任何进程 , 默认 进程不能对其进行操作;
2. 准备好 进程A 、进程B,两个进程需要和创建的共享内存进行关联
--- 关联操作: 调用Linux系统API
--- 关联成功之后 ,得到这块共享内存的起始地址
3. 在进程A 或 进程B中对共享内存进行读写操作
--- 读内存: printf() 等
--- 写内存: memcpy() 等
4. 通信完成,让进程A\B和共享内存解除关联
--- 解除成功 ,进程A、B不能再操作共享内存
--- 共享内存不受进程生命周期的影响
5. 共享内存不在使用后,将其删除
--- 调用Linux系统API,删除后,该内存被内核回收
*/
写进程内存代码:
//写进程
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/fcntl.h>
#include<sys/ipc.h>
#include <sys/shm.h>
int main()
{
//1. 创建共享内存 ,大小4k
int shmid = shmget(100,4096,IPC_CREAT|0664);
if (shmid ==-1)
{
perror("shmget error");
return -1;
}
//2. 当前进程和共享进程关联
void *ptr = shmat(shmid,NULL,0);
if (ptr == (void*) -1)
{
perror("shmat error");
return -1;
}
//3. 写共享内存
const char *p = "共享内存效率高";
memcpy(ptr ,p ,strlen(p)+1);
//阻塞程序
printf("按任意键继续,删除共享内存\n");
getchar();
// shmdt(ptr);
// //删除共享内存
// shmctl(shmid,IPC_RMID,NULL);
printf("共享内存已经被删除\n");
return 0;
}
读进程代码
//读进程
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/fcntl.h>
#include<sys/ipc.h>
#include <sys/shm.h>
int main()
{
//1. 创建(打开)共享内存
int shmid = shmget(100,0,0);
if(shmid ==-1)
{
perror("shmget error");
return -1;
}
//2. 当前进程和共享内存关联
void *ptr =shmat(shmid,NULL,0);
if (ptr ==(void *)-1)
{
perror("shmat error");
return -1;
}
//3. 读共享内存
printf("共享内存中数据 : %s \n",(char*)ptr);
//阻塞程序
printf("按任意键继续 ,删除共享内存\n");
getchar();
shmdt(shmid);
//删除共享内存
shmctl(shmid,IPC_RMID,NULL);
printf("共享内存已经被删除\n");
return 0;
}
2.5 shm 和mmap区别
共享内存
和 内存映射区
都可以实现进程间通信 ,其两者是有区别的:
- 实现进程间通信的方式:
- shm: 多个进程只需要一块共享内存,共享内存不属于进程,需要和进程关联才能使用;
- 内存映射区: 位于每个进程的虚拟地址空间中,且需要关联一个磁盘文件才能实现进程间数据通信;
- 效率:
- shm: 直接对内存操作,效率高;
- 内存映射区: 需要内存和文件之间的数据同步 ,效率低;
- 生命周期:
- shm: 进程退出对共享内存没有影响 , 调用相关函数/命令/关机 才能删除共享内存
- 内存映射区: 进程退出,内存映射区没了
- 数据完整性(突然断电)
- shm: 数据直接存储在物理内存上 ,断电后系统关闭,内存数据丢失;
- 内存映射区: 可以完整的保存数据 ,内存映射区数据会同步到磁盘文件;