聊聊VAS、LAS、PAS和mmap

很多人开口就来mmap增加性能,但是问具体点聊不清楚这三个地址空间,随便看了看网上对于这些个描述,大部分都有问题,不知道是不是

因为都是复制解决的,这里给出确切答案,附上Intel的手册描述,毕竟Talk Is Cheap,Show Your Code!!!

VAS

1、虚拟地址空间(Virtual Address Space)

所有运行在CPU保护模式的进程看到的地址:0x00000000~0xFFFFFFFF(不多不少4*8=32位地址空间),为什么叫做

虚拟地址空间呢?因为这玩意儿就是假的,想想你们在写业务的时候写的map集合,key为integer,value也为integer,通过将一个整形映射到另一个整形,这里的虚拟指的就是这样的地址,比如你写个地址:0x00000abc那么可能这个value就是0x00000cba。

LAS

2、线性地址空间(Linear Address Space)

所有运行在CPU保护模式的虚拟地址通过:段选择子+偏移量生成的地址。想象一下,你创建了一个List,里面放着一堆List结构,而内层List中存放一堆递增的整形值。用Java语言来描述就是这样一个结构:List<List>,那么还是这个虚拟地址:0x00000abc,

那么如何找到我要的线性地址?也即找到List<List>中内层的list中的integer,这很简单吧?给一个外层的List的偏移量:selector,这样就找到了对应的List集合,然后再给定一个偏移量:offset即可(加入List中的值按公差为1byte=8位来保存的,是不是我给一个list的地址+8*offset)。那么来按照这个套路我们来找找0x00000abc的线性地址值,首先我们可以这样拆为 selector:00000和offset:abc这不就找到了对应的值。那么这里的selector叫段选择子,offset叫段内偏移量。list中的list成为线性地址空间。

PAS

3、物理地址空间(Physics Address Space)

所有运行在CPU实模式的真实地址,也即你写个0x00000abc,那么这个地址就是内存上的地址,不用转换。最终我们的数据就要存在这个真实的地址里。很明显上面的虚拟地址是进程看到的,最终的数据就在物理地址空间里。那么怎么根据虚拟地址转换为物理地址?这里给出底层原理:虚拟地址空间 + GDTR -> 线性地址 + 多级页表 -> 物理地址,这几部由MMU来处理,毕竟硬件加速肯定比OS自己干来得快。这里我打算直接忽略介绍GDTR和页表,感兴趣的可以自己查阅相关资料,不过谨慎点吧,大部分都是错的,这里给出一个一级页表的Java的泛型描述:Map<Integer,List>。虚拟地址通过GDTR(Map<Integer,List>)找到对应的线性地址空间(List),然后根据页表Map<Integer,Integer>找到线性地址对应的物理地址。

为什么要这么设计?想想每个进程都有自己的虚拟地址空间,就好像所有进程独占所有内存一样,底层转化CPU来处理。会有一个什么优点?首先是内存的保护,比如进程A就不能随便访问进程B的数据,保证了安全性。由于物理内存分成一块块page,通常为4096字节,那么可以避免外碎片,因为是映射关系,所有不连续的page都可以映射使用,当然除了需要连续物理地址的DMA。这样咱们在编写进程代码的时候好处多多啊,啥都不用想,

就假如我有所有空间比如32位中的4G,那么每个进程都可以指定0x08048000虚拟地址上,由CPU来映射,这样就不用为每个进程产生不同的起始代码对吧。

mmap

聊聊VAS、LAS、PAS和mmap之二

那么聊完了VAS、LAS、PAS,现在可以来聊聊mmap了。

来看看函数原型:void *mmap(void *addr, size_t length, int prot, int flags,int fd, off_t offset);

再来看看man mmap给出的描述:
mmap() creates a new mapping in the virtual address space of the calling process. The starting address for the new mapping is specified in addr. The length argument specifies the length of the mapping.
翻译:mmap函数创建一个当前进程虚拟地址空间的映射。开始的地址为指定的地址,而长度为传入的length。那么prot、flags、fd、offset又是个啥?继续看描述:

The contents of a file mapping (as opposed to an anonymous mapping; see MAP_ANONYMOUS below), are initialized using length bytes starting at offset offset in the file (or other object) referred to by the file descriptor fd. offset must be a multiple of the pagesize as returned by sysconf(_SC_PAGE_SIZE).The prot argument describes the desired memory protection of the mapping (and must not conflict with the open mode of the file).
翻译:可以看到fd和offset是用来指定要映射的文件和文件的偏移量(当然也可以用不映射文件的匿名映射),而offset为文件偏移量而且offset必须是sysconf(_SC_PAGE_SIZE)返回的pagesize的倍数。prot参数描述了映射所需的内存保护(并且不能与文件的打开模式冲突)。它可以有以下值:PROT_EXEC 页被执行、PROT_READ 页可读、PROT_WRITE 页可写、PROT_NONE 页不可被访问。别忘了还有个flag:

The flags argument determines whether updates to the mapping are visible to other processes mapping the same region, and whether updates are carried through to the underlying file.
翻译:flag参数决定flags参数确定映射的更新对于映射同一区域的其他进程是否可见,以及是否将更新传递到底层文件。这里就给出几个常用的:
1、MAP_SHARED(共享这个映射。映射的更新对映射该文件的其他进程可见,并将其传递到底层文件。在调用msync()或munmap()之前,文件可能不会真正更新。)
2、MAP_PRIVATE(创建私有写时拷贝映射。对映射的更新对映射同一文件的其他进程是不可见的,并且不会传递到底层文件。)
3、MAP_ANONYMOUS(该映射没有任何文件支持,它的内容被初始化为零。fd和offset参数会被忽略,但是,如果指定了MAP_ANONYMOUS(或MAP_ANON),某些实现要求fd为-1,可移植应用程序应该确保这一点这一点。从内核2.4开始,Linux才支持MAP_ANONYMOUS和MAP_SHARED的结合使用。)

看不懂?木有关系,看我的。考虑三个东西:虚拟地址空间、物理内存Page、文件。这下明了了吧?创建一个进程的虚拟地址空间,如果传递了fd和offset,那么把文件映射到虚拟地址空间,然后当读取物理内存Page时将文件的内容copy到物理内存中。如果没有文件呢?那就是mmap虚拟地址空间和物理地址空间。这下明了了吧,那么是不是认为mmap可以创建进程内存?对的对的。就是mmap创建一个private,然后不指定fd。毕竟alloc()C语言堆内存分配函数底层就用到了brk和mmap来创建内存,从__edata指针在.data段往上推或者用mmap直接映射。默认是128kb大小阈值来确定用brk()或者mmap()。

那么mmap还能干嘛?答案是内存共享。你想想当你在父进程上mmap一个页面,然后fork系统调用,Duang一下,这下子进程和父进程一样拥有相同的虚拟地址空间,而虚拟地址空间都映射同一个物理地址空间,是不是内存共享了。

啊,本想这里就介绍epoll源码的,无奈,太长了,写累了,颈椎疼,最近PDD刚死一个23岁的姑娘,各位一定保重身体。感谢阅读。希望这篇文字能够铺垫上接下来介绍epoll的源码,也给后面介绍linux内存系统打下基调吧。

最后的最后,贴上Intel对于三个地址空间的描述,保证编写正确性

在这里插入图片描述

在这里插入图片描述