进程是如何使用内存的?_操作系统

程序运行概述

程序(我们这里只讨论单进程情况,存在多进程的程序如淘宝微信等不展开讨论)镜像存在磁盘中,运行时将镜像加载至内存RAM中,然后开始执行。

先来看一下CPU的多级存储结构,CPU通用寄存器访问速度最快,其次是Cache,再次是内存,磁盘访问速度最慢。

进程是如何使用内存的?_java_02

CPU的多级存储结构

对于进程而言,可使用的地址空间为2^32=4G,那么对于只有2G内存甚至只有256M内存的嵌入式设备怎么办?这个时候就需要MMU负责将进程的虚拟地址转换为内存的真实物理地址。如何管理这种映射关系呢?内核中为每个进程分配了进程页表。对应下图可以看到,只有正在使用的虚拟地址空间才会真正分配到物理页框;同时对于内核空间地址映射对于不同进程物理地址一样。

进程是如何使用内存的?_java_03

                                                 进程地址映射

TLB工作原理

上图中不同进程映射到物理内存使用到了TLB表(地址变换高速缓存),流程为:CPU获取数据或指令:获取进程页表,拿到物理地址;访问内存物理地址拿到真实数据。

每次执行指令,先从TLB表中获取,如果未命中,则从内存中获取,同时根据LRU方法更新TLB表。这里提一下,LRU更新方法在很多地方都用得到,像CDN页面缓存、CPU cache缓存等,如何消除cache颠簸的影响,是另一个话题。

局部性原理:时间及空间局部性,即CPU访问某个逻辑地址的数据时,大概率会继续访问该虚拟地址相邻的地址。因此为了保证cache的命中率,一般CPU采用多级流水线设计,将预取指令及相邻的地址空间存放至TLB表中。

每个核都有自己的TLB,由MMU内存管理单元模块执行,流程为:CPU发送执行进程的虚拟地址给MMU模块,MMU对应的硬件电路获取TLB表物理地址并访问数据。

如果进程使用了4G虚拟地址,那么所需要的页表项条目为:

4G/4K=1M

每个页表项为4Byte,因此进程页表占据了4M物理内存空间。这种状态下可以使用多级页表减少内存占用,且可以离散存储。

 

====

从malloc说起

使用malloc分配16k空间,这16k不会立即占用16k真实内存,而是采用写时复制的方式使用。如果物理内存已经写满数据怎么办(可能被多个进程占用)?这个时候就继续使用上面提到的LRU方式进行页表置换。置换过程中会将换出的页表真实写入磁盘中,一般由daemon守护进程完成。

前面使用malloc分配空间之后,linux内核并未真正给该进程分配物理页,如果对该地址进行写动作,会由MMU触发缺页中断,这时进入内核终端处理程序,将数据从磁盘加载至内存。

 

进程是如何使用内存的?_内存管理_04

                                                    CPU寻址流程

下面总结一下CPU寻址流程:CPU将进程虚拟地址通过地址总线发送给MMU,由MMU硬件电路转换成物理地址,然后通过数据总线访问内存获取数据。

CPU获取物理地址流程如下:

1、CPU发送虚拟地址,MMU查询自身TLB表,如果命中:根据物理地址访问数据页;如果不命中,通过cr3寄存器取出进程页表物理地址访问进程页表。

2、这里访问进程页表先是从Cache中获取缓存页框,如果Cache命中:从Cache中获取得到物理地址,更新TLB表。如果不命中,直接访问内存页表。

3、进程页表保存了进程使用过程中所有的虚拟地址对应的表项,因此通过页表地址偏移可直接获取到页表项,并更新至Cache中。

4、继续第一步,MMU从Cache中获取页表项,并查看虚拟地址是否已分配物理页框。若分配,则使用LRU算法更新TLB表并通过内存物理地址访问数据;若没分配物理页框,则触发Page Fault缺页中断,进入并执行中断接管程序。

5、中断处理程序为发送的虚拟地址分配真实物理页框,如果内存数据已满,根据LRU算法淘汰最久未用的页面,置换磁盘的进程映像至物理页框,并更新进程页表。(用户空间的缺页中断还会判断是否非法访问等权限校验,这里不展开)

6、中断处理程序返回,CPU获取执行权,继续执行指令。

获取到了物理地址,根据物理地址获取实际数据流程为:MMU通过物理地址查询Cache,如果缓存命中,CPU直接获取Cache中数据并继续执行;如果不命中,那么根据物理地址获取内存中的数据,硬件电路将物理页框存入Cache中,CPU从Cache中获取数据。

 

进程是如何使用内存的?_java_05

CPU获取数据流程

理想状态下,当进程局部性较高时,如执行while循环,MMU获取TLB表命中拿到物理地址,通过物理地址访问Cache命中拿到真实数据。

 

具体示例代码分析代码数据流及执行流

char* ptr =malloc(1*1024*1024);  //1、分配内存
memcpy(ptr, 'a',10);     //2、写入数据
ptr[100] = 'b';    //3、赋值

第一行:char* ptr =malloc(1*1024*1024);

假定malloc分配的虚拟地址是0x00000040,表示【0x00000040-0x00100040】这1M进程虚拟空间被分配成功。接下来我们看一下进程页表情况。

0x00000040  ->  64

0x00100040  ->  1048640

也就是这段虚拟空间占了1048576块页表项。这里注意,如果是一级页表,不管有没有执行malloc,这些页表项都存在,但如果是多级页表,只有真实访问数据的时候这些页表才会占用物理内存空间。接下来计算页号和页内偏移:

起始地址虚拟页号:64/(4*1024)= 0,页内偏移为64%(4*1024)= 0x40;

结束地址虚拟页号:1048640/(4*1024)= 256,页内偏移为1048640%(4*1024)= 0x40;

进程是如何使用内存的?_java_06

第二行:memcpy (ptr,'g', 10);

根据ptr虚拟地址找到页表项,发现并没有分配物理页框,触发缺页中断后分配得到第100页框。然后CPU将ptr对应虚拟地址往后的10字节空间写为‘a’。这里说一下,虽然进程页号对应的物理页框序号不一定相同,因为页框大小为4k,所以虚拟地址在进程页号中的偏移等于映射的物理地址在物理页框中的偏移。

CPU根据虚拟地址按照上一章的流程找到对应物理地址,将第100物理页框加载至Cache中,CPU将10字节’a’写入Cache。

进程是如何使用内存的?_操作系统_07

第三行:ptr[100] = 'b';

根据局部性原理,这里访问的就是同一个虚拟地址附近的数据,因此TLB和Cache都命中。

可以看到,进程在执行malloc时是将物理页框直接分配给虚拟地址,里面的初始数据有内存的电气特性决定,是随机值,因此maoolc出来的空间使用前需要赋值或者执行初始化操作。


 

进程是如何使用内存的?_操作系统_08