本文最后更新于:2021年4月4日 下午
概览:Linux内存映射、共享内存。
内存映射
内存映射(Memory-mapped I/O)是将磁盘文件的数据映射到内存,用户通过修改内存就能修改磁盘文件。
可以做进程间通信,而且效率较高。
通信方式
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| 使用内存映射实现进程间通信: 1.有关系的进程(父子进程) - 还没有子进程的时候 - 通过唯一的父进程,先创建内存映射区 - 有了内存映射区以后,创建子进程 - 父子进程共享创建的内存映射区
2.没有关系的进程间通信 - 准备一个大小不是0的磁盘文件 - 进程1 通过磁盘文件创建内存映射区 - 得到一个操作这块内存的指针 - 进程2 通过磁盘文件创建内存映射区 - 得到一个操作这块内存的指针 - 使用内存映射区通信
注意:内存映射区通信,是非阻塞。
|
注意:内存映射区域通信,是非阻塞的。
函数
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36
| #include <sys/mman.h> void *mmap(void *addr, size_t length, int prot, int flags,int fd, off_t offset);
int munmap(void *addr, size_t length);
|
父子进程间通过内存映射沟通
思路:父进程先创建内存映射区域,然后创建子进程,之后父子进程就共享同一块内存映射区域。
子进程能通过文件描述符的方式写文件吗???可以
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55
| #include <stdio.h> #include <sys/mman.h> #include <fcntl.h> #include <sys/types.h> #include <unistd.h> #include <string.h> #include <stdlib.h> #include <wait.h>
int main(){
int fd = open("test.txt",O_RDWR); if(fd <= 0){ perror("open"); exit(0); } int len = lseek(fd,0,SEEK_END); void * addr = mmap(NULL,len,PROT_READ|PROT_WRITE,MAP_SHARED,fd,0); if(addr == MAP_FAILED){ perror("mmap"); exit(0); } pid_t pid = fork(); if(pid > 0){ wait(NULL); char buf[128]; strcpy(buf,(char *)addr); printf("read data: %s \n",buf);
}else if(pid == 0){ char *str1 = "你好啊!"; strcpy((char *)addr,str1); sleep(2); strcpy((char *)addr + strlen(str1),"今天的风甚是喧嚣!");
}else{ perror("fork"); exit(0); } munmap(addr,len); return 0; }
|
- 父进程、子进程读取方式
- 能否用fd来读写:可以
- 读写之后,内容还在不在? 修改都保存在了内容之中
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| if(pid > 0){ wait(NULL);
char buf[128]; strcpy(buf,(char *)addr); printf("read data: %s \n",buf); }else if(pid == 0){ char *str1 = "你好啊!"; write(fd,str1,strlen(str1));
}
|
- 子进程向文件写入,默认就写到了文件的末尾,而父进程读取的话,就会将整个文件内容都读入。
无关系的两个进程通信的话,方式就是打开同一个文件。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26
| int main(){ int fd = open("test.txt",O_RDWR); if(fd <= 0){ perror("open"); exit(0); } int len = lseek(fd,0,SEEK_END); void * addr = mmap(NULL,len,PORT_READ|PORT_WRITE,MAP_SHARED,fd,0); if(addr == MAP_FAILED){ perror("mmap"); exit(0); } munmap(addr,len); return 0; }
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25
| int main(){ int fd = open("test.txt",O_RDWR); if(fd <= 0){ perror("open"); exit(0); } int len = lseek(fd,0,SEEK_END); void * addr = mmap(NULL,len,PORT_READ|PORT_WRITE,MAP_SHARED,fd,0); if(addr == MAP_FAILED){ perror("mmap"); exit(0); } munmap(addr,len); return 0; }
|
使用内存映射做文件拷贝
一般不用做内存拷贝,文件太大, 内存可能放不下。
方法:建立一个新文件,并且扩展大小,之后将源文件和新文件都映射到内存之中,再使用内存拷贝memcpy的方式来完成文件拷贝,最后释放资源即可。
内存映射注意事项
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33
| 1.如果对mmap的返回值(ptr)做++操作(ptr++), munmap是否能够成功? void * ptr = mmap(...); ptr++; 可以对其进行++操作 munmap(ptr, len); // 错误,要保存地址
2.如果open时O_RDONLY, mmap时prot参数指定PROT_READ | PROT_WRITE会怎样? 错误,返回MAP_FAILED open()函数中的权限建议和prot参数的权限保持一致。
3.如果文件偏移量为1000会怎样? 偏移量必须是4K的整数倍,返回MAP_FAILED
4.mmap什么情况下会调用失败? - 第二个参数:length = 0 - 第三个参数:prot - 只指定了写权限 - prot PROT_READ | PROT_WRITE 第5个参数fd 通过open函数时指定的 O_RDONLY / O_WRONLY
5.可以open的时候O_CREAT一个新文件来创建映射区吗? - 可以的,但是创建的文件的大小如果为0的话,肯定不行 - 可以对新的文件进行扩展 - lseek() - truncate()
6.mmap后关闭文件描述符,对mmap映射有没有影响? int fd = open("XXX"); mmap(,,,,fd,0); close(fd); 映射区还存在,创建映射区的fd被关闭,没有任何影响。
7.对ptr越界操作会怎样? 越界操作操作的是非法的内存 -> 段错误
|
匿名映射
不需要文件实体的一种内存映射。只能用于父子关系之间的映射。
需要一个特殊的设置,MAP_SAHRED | MAP_ANONYMOUS
,同时,文件描述符传递-1,offset必须从0开始。
1
| void * addr = mmap(NULL,4096,MAP_READ|MAP_WRITE,MAP_SHARED | MAP_ANONYMOUS,-1,0);
|
共享内存
共享内存允许两个或者多个进程共享物理内存的同一块区域(通常被称为段)。由于一个共享内存段会成为一个进程用户空间的一部分,因此这种 IPC 机制无需内核介入。所有需要做的就是让一个进程将数据复制进共享内存中,并且这部分数据会对其他所有共享同一个段的进程可用。
与管道等要求发送进程将数据从用户空间的缓冲区复制进内核内存和接收进程将数据从内核内存复制进用户空间的缓冲区的做法相比,这种 IPC 技术的速度更快。
共享内存使用步骤
调用 shmget() 创建一个新共享内存段或取得一个既有共享内存段的标识符(即由其他进程创建的共享内存段)。这个调用将返回后续调用中需要用到的共享内存标识符。
使用 shmat() 来附上共享内存段,即使该段成为调用进程的虚拟内存的一部分。
此刻在程序中可以像对待其他可用内存那样对待这个共享内存段。为引用这块共享内存,程序需要使用由 shmat() 调用返回的 addr 值,它是一个指向进程的虚拟地址空间中该共享内存段的起点的指针。
调用 shmdt() 来分离共享内存段。在这个调用之后,进程就无法再引用这块共享内存了。这一步是可选的,并且在进程终止时会自动完成这一步。
调用 shmctl() 来删除共享内存段。只有当当前所有附加内存段的进程都与之分离之后内存段才会销毁。只有一个进程需要执行这一步。
共享内存操作函数
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68
| #include <sys/ipc.h> #include <sys/shm.h>
int shmget(key_t key, size_t size, int shmflg);
void *shmat(int shmid, const void *shmaddr, int shmflg);
int shmdt(const void *shmaddr);
int shmctl(int shmid, int cmd, struct shmid_ds *buf);
key_t ftok(const char *pathname, int proj_id);
|
实例
一个进程创建共享内存,并且向内存中写入数据
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29
| #include <stdio.h> #include <sys/ipc.h> #include <sys/shm.h> #include <string.h>
int main(){
int shmid = shmget(100,4096,IPC_CREAT|0664); printf("shmid : %d\n",shmid);
void *ptr = shmat(shmid,NULL,0);
char *str = "hello world"; memcpy(ptr,str,strlen(str)+1);
printf("按任意键继续\n"); getchar();
shmdt(ptr);
shmctl(shmid,IPC_RMID,NULL);
return 0; }
|
另一个进程关联那块共享内存,然后读取其中的数据。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28
| #include <stdio.h> #include <sys/ipc.h> #include <sys/shm.h> #include <string.h>
int main(){
int shmid = shmget(100,0,IPC_CREAT); printf("shmid : %d\n",shmid); void *ptr = shmat(shmid,NULL,0);
printf("recv: %s\n",(char*)ptr);
printf("按任意键继续\n"); getchar();
shmdt(ptr);
shmctl(shmid,IPC_RMID,NULL);
return 0; }
|
其他问题
问题1:操作系统如何知道一块共享内存被多少个进程关联?
- 共享内存维护了一个结构体
struct shmid_ds
这个结构体中有一个成员 shm_nattch
shm_nattach
记录了关联的进程个数
linux中有一些命令可以来查看相关信息
1 2 3 4 5 6 7 8 9 10 11 12 13
| ipcs 用法 ipcs -a // 打印当前系统中所有的进程间通信方式的信息 ipcs -m // 打印出使用共享内存进行进程间通信的信息,常用 ipcs -q // 打印出使用消息队列进行进程间通信的信息 ipcs -s // 打印出使用信号进行进程间通信的信息
ipcrm 用法 ipcrm -M shmkey // 移除用shmkey创建的共享内存段 ipcrm -m shmid // 移除用shmid标识的共享内存段 ipcrm -Q msgkey // 移除用msqkey创建的消息队列 ipcrm -q msqid // 移除用msqid标识的消息队列 ipcrm -S semkey // 移除用semkey创建的信号 ipcrm -s semid // 移除用semid标识的信号
|
例如执行上面的写入程序
1 2 3 4 5 6 7 8 9 10 11
| aliyun@ali-colourso:~/learn/02sig$ ipcs -m
------ Shared Memory Segments -------- key shmid owner perms bytes nattch status 0x00000064 32768 aliyun 664 4096 1 aliyun@ali-colourso:~/learn/02sig$ ipcrm -m 32768 aliyun@ali-colourso:~/learn/02sig$ ipcs -m
------ Shared Memory Segments -------- key shmid owner perms bytes nattch status 0x00000000 32768 aliyun 664 4096 1 dest
|
key值其实就是100的十六进制形式,删除之后key变成了0,表示被标记删除,但是当前程序还没执行完。
问题2:可不可以对共享内存进行多次删除 shmctl
- 可以的,因为shmctl 标记删除共享内存,不是直接删除
什么时候真正删除呢?
- 当和共享内存关联的进程数为0的时候,就真正被删除
- 当共享内存的key为0的时候,表示共享内存被标记删除了
- 如果一个进程和共享内存取消关联,那么这个进程就不能继续操作这个共享内存。也不能再次进行关联。
共享内存和内存映射的区别
内存映射,需要文件,匿名映射不需要,但只能用于有亲缘关系的进程之间。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| 共享内存和内存映射的区别 1.共享内存可以直接创建,内存映射需要磁盘文件(匿名映射除外) 2.共享内存效果更高 3.内存 所有的进程操作的是同一块共享内存。 内存映射,每个进程在自己的虚拟地址空间中有一个独立的内存。 4.数据安全 - 进程突然退出 共享内存还存在 内存映射区消失 - 运行进程的电脑死机,宕机了 数据存在在共享内存中,没有了 内存映射区的数据 ,由于磁盘文件中的数据还在,所以内存映射区的数据还存在。
5.生命周期 - 内存映射区:进程退出,内存映射区销毁 - 共享内存:进程退出,共享内存还在,标记删除(所有的关联的进程数为0),或者关机 如果一个进程退出,会自动和共享内存进行取消关联。
|