本文主要是介绍虚拟内存与mmap,brk,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!
虚拟内存与mmap,brk
- 基本概念及相关术语
1.1 基本概念
虚拟内存使得应用程序认为它拥有连续的可用的内存(一个连续完整的地址空间),而实际上,它通常是被分隔成多个物理内存碎片,还有部分暂时存储在外部磁盘存储器上,在需要时进行数据交换。即将不完整,不连续的物理内存映射为连续的虚拟内存。虚拟内存主要有以下三个作用:
(1) 它将主存看成是磁盘的一个高速缓存,只在主存中保存活动区域(通常一个进程只有执行上下文被加载到主存,其余的在磁盘中,随用随加载);
(2) 为每个进程提供一致的地址空间,简化了内存管理;
(3) 它保护每个进程的地址空间不被其他进程破坏(在页表的PTE条目中加入额外控制信息实现内存保护)。
虚拟内存有两个重要的地址,虚拟地址(virtual address, VA)和物理地址(physical address)。在访问某个对象时,CPU给出虚拟地址,通过查询计算得到物理地址,然后访问物理地址上的对象。整个过程如下图:
图1 CPU访问主存
1.2 相关术语
在表述虚拟内存相关概念时,有些约定的缩写和表达方式
N=2n:虚拟地址数量,n表示虚拟地址位数;
M=2m:物理地址数量,m表示物理地址位数;
P=2p:页大小,p表示页偏移量的位数;
VPO(virtual page offset):虚拟地址页偏移;
VPN(virtual page number):虚拟地址页号;
PPO(physical page offset):物理地址页偏移;
PPN(physical page number):物理地址页号。
页表(Page Table, PT):记录虚拟地址到物理地址的映射的表
页表项(Page Table Entry, PTE):页表中一行,PTE的索引即VPN;
页表项地址(PTEA):在CPU中有个页表基址寄存器,记录页表起始地址,页表基址寄存器+PTE索引=PTEA;
MMU(Memory Management Unit):内存管理单元,用于虚拟地址到物理地址寻址的硬件。
一个页表的常见结构如下图:
图2 页表常见结构(有效位表示该PTE是否有VP到PP的映射)
eg: 给定一个32位虚拟地址空间和一个24位物理地址空间,,对于下面的页大小,确定VPN,VPO,PPN,PPO的位数。
P VPN位数 VPO位数 PPN位数 PPO位数
1KB 22 10 14
10
4KB 20 12 12
12
注:VPO表示对象在页中的偏移,VPO=PPO,VPO位数=log2§,VPN表示虚拟页号,对应PTE表索引,PPN表示物理页号。
一个虚拟地址翻译成物理地址,方法如下图:
图3 虚拟地址翻译为物理地址
地址翻译时,给定虚拟地址,低p位表示页偏移,其中VPO=PPO,高n-p位表示虚拟页号,即PTE的索引号,找到对应PTE记录,得到物理页号PPN,跟PPO组合得到物理地址。所以访问一个对象,首先访问页表,从虚拟地址转化为物理地址,再从访问物理地址得到对象。由于页表和物理地址都在内存中,因此存在两次内存访问。
1.3 地址翻译加速
从1.2中得知为了访问对象,需要两次内存访问,每次内存访问一般几十到几百个周期,为了加快地址翻译,减少内存访问次数,有两种辅助设备:SRAM缓存和TLB缓存。
SRAM缓存:在CPU和主存(DRAM)之间,还有L1, L2, L3三级高速缓存(SRAM)。因此,可以将部分PTE条目和对象存到SRAM中,减少内存访问次数,添加了SRAM的访问机制如下图。
图4 加入SRAM的对象访问过程
可见,在访问时,优先访问SRAM获取PTE和数据,没有再访问主存,还没有则引起缺页中断。SRAM的访问通常几个时间周期。
TLB缓存:在MMU中的虚拟地址缓存器,称为翻译后备寄存器(Translantion Lookaside Buffer)。每一行由一个或多个PTE条目组成,其中TLBI用于行号索引,TLBT用于同一行某个PTE的选择。
图5 虚拟地址在TLB中的含义
比如某一时刻,TLB中的快照如下:
图6 TLB快照,四组,四路组相联
页面大小64字节,虚拟地址长度14位,物理地址长度为12位。给定虚拟地址0x03d4,其二进制表示为0b 00 0011 1101 0100,低6位0b 01 0100为VPO,因为四路组相联,所以第6-7位为TLBI(TLB索引),为0b 11,剩余为TLBT(TLB标记),TLBI表示TLB表的行号,找到TLBT为0x03的位置,得到PPN为0D。结合VPO,得到物理地址为0b 0011 0101 0100,即0x0354。加入TLB之后的对象访问过程如下:
图7 加入TLB的对象访问过程
- Linux虚拟内存
2.1 Linux虚拟内存组织机制
Linux系统为每个进程维护一个单独的地址空间,如图8(a)所示,同时为每个进程维护一个结构体,其中包含虚拟内存相关信息,如图8(b)所示。
(a) Linux进程的虚拟内存 (b)管理虚拟内存的结构体
图8 Linux虚拟内存
其中vm_prot描述虚拟内存页的读写权限,vm_flags记录该虚拟页是共享还是私有等其他常见信息。
2.2 内存映射
Linux系统将虚拟内存和一个磁盘对象关联起来,以初始化虚拟内存区域的内容,称为内存映射。有两种类型的内存映射:
(1) 映射到Linux文件系统中的普通文件;
(2) 映射到匿名文件,匿名文件是由内核创建的全是二进制0的文件,CPU第一次使用该虚拟页面时,内核就选择一个物理页面进行覆盖(整个过程没有跟磁盘发生数据交互)。
一个对象映射到虚拟内存中,要么以共享对象存在,要么以私有对象存在。不论哪一种模式,在物理内存中只有一份副本。共享对象一个进程的写操作,其他进程都可见,并且能反映到磁盘上;私有对象一个进程的写操作,其他进程不可见,并且不能反映到磁盘上。
(a) 内存映射到共享区域 (b) 内存映射到私有区域图9 多个进程映射同一对象
对于多个进程内存映射到私有区域时,物理内存只有一份副本,此时采用一种"写时复制"策略。即进程在写时,复制修改的部分到内存其他区域。这样对其他进程来说,对象没有修改过。
2.3 mmap函数
mmap函数提供用户级的内存映射,该函数能够把某个磁盘文件映射到内存中,函数的主要格式如下:
复制代码
void* mmap(void* start,size_t length,int prot,int flags,int fd,off_t offset);
start:内存起始地址,通常为NULL,让系统自己选择
length:内存的长度
prot:
PROT_READ:数据可读
PROT_WRITE:数据可写
PROT_EXEC:数据可执行
PROT_NONE:数据不可访问
flags:
MAP_SHARED:共享对象,进程间可察觉修改,并能反映到磁盘
MAP_PRIVATE:私有对象,一切操作只在本进程可见,修改不会写入磁盘
MAP_FIXED:基本不用
fd:映射的文件的描述符,通常应先打开文件,再调用mmap,此后关闭文件映射仍然存在
offset:文件偏移量,一般为0
该函数返回内存中对应的地址
复制代码
调用mmap之后,内存与磁盘文件之间就建立了映射关系,如下图所示:
munmap用于解除映射关系
int munmap(void* start,size_t length);
使用mmap的作用主要有以下两个:
(1) 将磁盘文件映射到内存中,这样所有读写均针对内存读写(可以使用memcpy等内存操作函数,而不是read,write等IO操作函数),加快访问速度;
(2) 在无亲缘关系的进程间提供共享内存。
使用mmap函数,需要注意以下问题:
(1) 在文件映射之前,必须打开该文件,而且mmap的prot权限不能超过打开的权限。比如open打开时只设置了读文件,那么prot就不能设置PROT_WRITE;
(2) 内存映射通常都是按虚拟内存的页为基本单位的。比如一个页512字节,但是映射的文件只有12字节。那么剩下的500字节会自动填充为零,即时修改了后面的500字节,也不会写入到文件(所以较好的操作是直到文件大小,直接加长文件);
(3) 如果试图访问不存在的映射关系,比如页面大小512字节,实际文件大小为12字节,用mmap映射的时候映射1000个字节,那么实际可操作的结果如下:
(4) 将内存写入磁盘的操作通常由页守护进程完成,如果想人为控制将内存数据写入磁盘,可以调用以下函数:
复制代码
#include <sys/mman.h>
int msync(void *addr,size_t len,int flags);
flags:
MS_ASYC:异步写入
MS_SYC:同步写入,等待写入之后才会返回
复制代码
(5) 进程终止或调用munmap时解除映射关系,关闭文件描述符不会解除映射关系。
下面举一个简单的例子:父子进程同时修改一个文件写入数据:
复制代码
1 #include <sys/mman.h>
2 #include <stdio.h>
3 #include <fcntl.h>
4 #include <unistd.h>
5 #include <sys/wait.h>
6 #include <sys/types.h>
7 #include <stdlib.h>
8 #include <string.h>
9
10 #define FILE_MODE (S_IRUSR|S_IWUSR|S_IRGRP|S_IWGRP|S_IROTH)
11
12 int main()
13 {
14 int fd;
15 if((fd=open(“map.txt”,O_RDWR|O_CREAT|O_TRUNC,FILE_MODE))<0)
16 {
17 printf(“open file failed\n”);
18 exit(1);
19 }
20
21
22 if(ftruncate(fd,50)<0) //文件大小50字节
23 {
24 printf(“ftruncate error\n”);
25 exit(1);
26 }
27
28 char buf;//起始地址
29
30 buf=(char)mmap(NULL,50,PROT_READ|PROT_WRITE,MAP_SHARED,fd,0);
31 close(fd);
32 pid_t pid;
33 if((pid=fork())<0)
34 printf(“fork error\n”);
35
36 char* msg=“hello world\n”;
37 char* msg1= “good news”;
38 if(pid==0) //子进程
39 {
40 memcpy(buf,msg,strlen(msg));
41 exit(0);
42 }
43 else
44 {
45 int stat;
46 wait(&stat);
47 memcpy(buf+strlen(msg),msg1,strlen(msg1));
48 }
49 return 0;
50
51 }
复制代码
第22-26行就是申请文件大小为50字节,那么实际内存可修改的部分就是buf~(buf+49)。注释该段再执行就会报SIGBUS错误。
执行结果是当前目录多了map.txt,其内容为:
hello world
good news
2.4 Linux进程分配内存的方式
关于此部分详细介绍参考博文:https://www.cnblogs.com/vinozly/p/5489138.html
简单来说,当我们调用分配内存的函数时(如malloc),底层通过调用brk()或mmap()实现。当遇到小于128KB的内存时,调用brk()函数将数据段堆的_edata地址往高地址推(即图8a中brk指向的指针,此时只分配虚拟内存,没有物理内存。当产生缺页中断时,才调用物理内存)。当申请内存大于128KB时,调用mmap()在堆栈之间的共享区域分配内存(此部分内存可以单独释放)。
标签: 虚拟内存 , 计算机基础 , 操作系统 , mmap
好文要顶 关注我 收藏该文 微信分享
晨枫1
粉丝 - 2 关注 - 0
+加关注
00
升级成为会员
« 上一篇: QT下多线程调用TCP的问题及可能的解决方案
» 下一篇: epoll,select,poll的区别
posted @ 2020-05-18 12:17 晨枫1 阅读(1291) 评论(0) 编辑 收藏 举报
这篇关于虚拟内存与mmap,brk的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!