虚拟存储器

2020-05-04  本文已影响0人  尧字节

这篇仍然是深入理解计算机系统的学习笔记,结合自己理解体会所写。程序是运行在内存中的,而程序的运行时候不是直接操作物理内存的,为什么不直接操作物理内存那,有几个原因:首先容易想到的是,计算机中运行的程序不可能只是一个,如果大家都直接操作物理内存,有安全问题,即A程序有可能会破坏B程序的内容; 其次,如果直接操作物理内存,那么每个程序使用的空间都不是一样的,这也带来了不方便。所以前辈们给出的方案是采用虚拟内存的方式,虚拟内存是物理内存和磁盘的抽象,提供一个统一的虚拟内存,每个程序的虚拟范围是一致的,简化了编程操作;通过一个叫MMU(存储器管理单元)的芯片将虚拟地址转成物理地址;还有一点,虚拟内存将内存和磁盘结合一起看待,将不常用的数据存放在磁盘上,需要用的时候,才从磁盘调到内存中,从而提升内存的利用率。


虚拟内存寻址

一 虚拟内存空间划分

1.1 基本概述

虚拟内存有多大那,在32位系统上,虚拟内存可以访问的存储空间为:232; 64位系统中虚拟存储地址空间范围不是264,而是一般为:248,以为现在还用不了这么大的空间,可以通过命令:

[root@izbp14xswj2tx6qgnz9dllz ~]# cat /proc/cpuinfo 
......
address sizes   : 46 bits physical, 48 bits virtual
power management:

结果中:

address sizes   : 46 bits physical, 48 bits virtual

表示物理地址为:46位,虚拟地址为48位。

这么大的空间是如何划分的那:


linux下32位虚拟地址的空间划分

每个进程启动之后,都会有自己的页表(存储的是虚拟地址和物理地址的映射),结合MMU完成虚拟地址的转换。好处除了上面所说的外,还可以简化共享,比如我们多个C程序在运行的时候都需要libc库,如果每个程序都加载一份的话,势必会造成资源的浪费,我们可以在内存中只保留一份数据。每个应用程序关于库的虚拟地址,映射到共享存储,简化了数据的共享。

虚拟内存采用页为单位进行存储管理的,典型的页面大小为4KB或1MB,linux下可以通过getconf命令查看。页面大小顺便说一句,有些特殊的场景喜欢设置超级大页,比如在DPDK这种高性能网络处理库中,常设置大页为1GB,目的是为了减少页表的条目,可以让页表完全保存的高速缓存中,提升内存分配效率。

[root@ ~]# getconf PAGE_SIZE
4096

1.2 虚拟空间的磁盘部分

我们知道,虚拟空间是内存和磁盘的抽象,如果内存空间不够的时候,如果我们再运行程序,势必需要将内存的保存数据换出内存,保存到磁盘中,将需要加载到内存中的数据从磁盘再加载到内存中,这就是常说的换入换出。

磁盘上保存内存换出的空间一般叫swap空间,在linux下,通过free命令查看swap空间大小,下面机器是

[root@localhost ~]# cat /proc/swaps
Filename                Type        Size    Used    Priority
/dev/dm-1                               partition   2097148 0   -1
[root@localhost ~]# free -h
              total        used        free      shared  buff/cache   available
Mem:           2.8G        155M        2.4G        9.8M        234M        2.4G
Swap:          2.0G          0B        2.0G
[root@localhost ~]# swapon -s
文件名             类型      大小  已用  权限
/dev/dm-1                               partition   2097148 0   -1

这里面想说的是,一些运行大数据软件的主机上,很多人倾向于将swap空间直接关掉,因为linux下,有时候内存还是够的情况下,仍然使用swap空间,造成了java一些程序在gc的时候会超时。当时是不能直接关闭的,因为关闭的话,如果系统的内存不够的话,会把内存占用大的进程kill掉,所以我们只是将swap空间的使用倾向更小,而不是完全关闭。

echo "vm.swappiness=1" >> /etc/sysctl.conf
sysctl -p
sysctl -a|grep swappiness

注意:swap空间大小限制当前运行进程可以分配的虚拟存储页面的总数。

二 虚拟存储器工作原理

书中图

虚拟地址到物理地址的翻译,其实就是一个从虚拟地址空间向物理内存空的映射,通过MMU结合页表来实现地址的翻译,页表的地址又在哪里了,cpu中有个专门保存页表的寄存器:PTBR指向当前页表的基地址。

每个虚拟地址有两部分组成:虚拟页号(VPN)+ 虚拟页偏移量(VPO),如上图,当cpu生成一个虚拟地址的时候,将地址传递给MMU开始翻译,MMU利用虚拟地址的VPN来选择PTE(页表项),如果位置有效,则将页表项中的物理页号PPN取出来,结合虚拟地址中的VPO生成物理地址。

2.1 缺页

书中缺页图

处理步骤如下:

  1. 处理器生成一个虚拟地址,将它传递给MMU。
  2. MMU将虚拟地址的VPN取出来,结合CPU的寄存器保存的页表地址去取此虚拟地址对应的PTE
  3. 从高速缓存或内存中获取PTE,检查PTE的有效位是否为0,即此虚拟地址是否已经对应真正的物理内存地址。
  4. 有效位为0,发生缺页异常,调用缺页异常处理程序,将磁盘上对应地址的新页调入到内存中,如果内存满了,则需要进行内存页面的换出,换出页面,用新加载的页面替换。

其实我们注意到,虚拟内存的翻译过程还是挺麻烦的,需要先取出PTE,再通过物理地址真正去取数据,当时如果页表不在高速缓存中,或者此物理地址页面不在高速缓存中,需要从内存再加载下,先将数据加载到高速缓存中。

2.2 TLB

为了加快地址翻译的速度,在MMU中包含一个关于PTE的小缓存,称为TLB,每一行都保存一个PTE,MMU在翻译地址的时候,先到TLB中查找,如果命中,直接取回PTE,如果不命中,则需要从高速缓存L1或内存中取到PTE,替换TLB中数据,再进入下一步的翻译:


TLB引入

这和前面说的内存大页结合起来,大的页面,页表的条目数必然小,可能TLB就可以完全存放下,这样就可以加快地址翻译的速度了。

2.3 多级页表

现实永远比理论要复杂,我们前面分析的是单页表,即通过一个页表完成地址翻译,当时我们知道每个进程都有一个地址范围很大的地址空间,比如在32位系统中位4G,而我们实际上是使用不了这么多的空间的,如果用页表来存储所有的页表项的话,按照每个页表项4B来计算,每个页面4KB来计算,一个进程将需要4MB的页表占用,这是极大的浪费,因为很多虚拟地址空间是程序没用的,为解决这个问题引入了多级页表:


多级页表

一个一级页表对应了1024个二级页表,而每个二级PTE对应4KB的页面,所以一个一级页表对应的空间为4MB,1024个一级页表就满足了4GB的总空间。一级页表项的数据不是都有值,如果程序没使用这个范围内的空间则对应的一级页表的PTE为空,如果有这个范围内地址一级页表保存的是指向二级页表的基地址。

2.4 奔腾处理器系列地址翻译

奔腾处理器地址翻译

说明:

  1. cpu产生虚拟地址
  2. 虚拟地址送MMU进行地址翻译,MMU先查看TLB是否命中,命中直接得到物理地址
  3. 不命中,则需要从内存或高速缓存中查到PTE,通过四级页表得到物理地址
  4. 物理地址得到后查询L1缓存,看是否命中,如果不命中到其他缓存和内存中查询加载。

三 虚拟存储区域

在虚拟内存划分图中,程序将整个虚拟内存空间分为不同的段,text,data,bss都是不同的段,每个虚拟页面都要在特定的段中,不存在段中的虚拟页面或地址是无法使用的,我们在程序中通过fork命令创建进程的时候,会在内核中创建各种数据结构,比如pid,为了给子进程创建新的虚拟存储,需要创建task_struct数据结构和复制原理的页表,task_struct数据结构图如下:


task_struct

重点看两个:pgd指向页面目录表的基地址;mmap保存的是区域结构,我理解就是段结构。保存着区域的开始地址,结束地址,权限以及是否共享等。

3.1 MMAP

MMAP是一个函数,通过这个函数可以创建一个新的虚拟存储器区域,可以映射一个普通的文件到一个虚拟存储器区域,也可以映射一个匿名文件到虚拟内存区域;如果是后者,匿名文件由内核来创建,包含内容全部是0,比如用在.bss段初始化等。

mmap函数

说明:
start:从地址start开始处创建,通常为NULL;length:连续对象的大小;
port:访问权限(PROT_EXEC\PROT_READ\PROT_WRITE\PROT_NONE);
flags:被映射对象的位(MAP_ANOE\MAP_PRIVATE\MAP_SHARED);
fd: 指定的磁盘文件;offset:距离磁盘文件偏移的位置处开始;
返回值:调用成功,返回新区域的地址。

也许我们更关心这个函数什么优势那,这个函数在映射之后,并没有实际将文件读入到内存,而是只是建立了虚拟内存段和文件的映射,真正操作的时候,会像前面介绍的一样,发生缺页异常,会真正将文件内容从磁盘加载到物理内存中。

这样读文件有什么好处那,可以减少一次内存拷贝,提升读文件的性能:
一般情况下读文件:磁盘 -> 内核缓存 -> 用户内存;用mmap将fd进行映射后:
磁盘 -> 用户内存 可以看到只有一次拷贝。
读取和修改文件内容:

#include <stdio.h>
#include <stdlib.h>
#include <sys/mman.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <unistd.h>
#include <fcntl.h>
#include <string.h>



int main(int argc, char *argv[])
{
    int fd=open("test.file",O_RDWR,0644);
    struct stat statbuf;
    char* start;
    char buf[2]={ 0 };
    int ret=0;
    fstat(fd,&statbuf);
    start=(char*)mmap(NULL,statbuf.st_size,PROT_READ|PROT_WRITE,MAP_SHARED,fd,0);
    do{
        *buf=start[ret++];
        printf("%c", *buf);
     }while (ret<statbuf.st_size);
   getchar();
    memset(start,0x0,statbuf.st_size);
    sprintf(start,"%s","test");
    munmap(start,statbuf.st_size);
    close(fd);
    return 0;
}

通过以下命令可以查看虚拟内存的映射情况:

[root@localhost ~]# cat /proc/5123/smaps |grep "test.file" -A15
7fe7b8f22000-7fe7b8f23000 -w-s 00000000 fd:00 3185332                    /home/miaohq/tests/test.file
Size:                  4 kB
Rss:                   0 kB
Pss:                   0 kB
Shared_Clean:          0 kB
Shared_Dirty:          0 kB
Private_Clean:         0 kB
Private_Dirty:         0 kB
Referenced:            0 kB
Anonymous:             0 kB
AnonHugePages:         0 kB
Swap:                  0 kB
KernelPageSize:        4 kB
MMUPageSize:           4 kB
Locked:                0 kB
VmFlags: wr sh mr mw me ms sd 

mmap 还可以实现不同不同进程间的内存共享,从而实现通信;还可以实现用户空间和内核空间的高效交互,不过我没用过无法提供什么例子。

3.2 Malloc

相对于mmap这种低级函数,我们在c中编程更多用的是malloc进行内存分配,其实malloc看做是mmap和sbrk的封装:

#include <unistd.h>
void * sbrk(int incr);

看上面虚拟内存结构图中有brk指针,这个函数通过将内核的brk指针的增加来扩充或收缩堆。如果成功返回brk的旧值,如果失败返回-1。

一般来说,对于小于128k的内存,malloc采用在现有的堆空间中搜索,按照堆分配算法分配并返回,对于大于128k的内存,一般采用mmap分配一块匿名空间。
可以通过如下方式实现malloc,不过现实中这样分配的都是以内存页为单位的即一般为4KB为单位做内存分配的,会造成很多浪费。

 void * malloc(size_t bytes)
{
   void * ret = mmap(0,bytes,PROT_READ|PROT_WRITE,MAP_PRIVATE|MAP_ANONYMOUS,0,0);
   if (ret == MAP_FAILED) {
       return 0;
    }
   return ret;
}
上一篇 下一篇

猜你喜欢

热点阅读