虚拟地址和内存分配

这篇仍然是深入理解计算机系统的学习笔记,结合自己理解体会所写。程序是运行在内存中的,而程序的运行时候不是直接操作物理内存的,为什么不直接操作物理内存那,有几个原因:首先容易想到的是,计算机中运行的程序不可能只是一个,如果大家都直接操作物理内存,有安全问题,即 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 位。

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

每个进程启动之后,都会有自己的页表(存储的是虚拟地址和物理地址的映射),结合 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 就可以完全存放下,这样就可以加快地址翻译的速度了。

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 数据结构图如下:

重点看两个: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;
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值