什么?内存爆了?详细解读虚拟内存机制

不知道大家在运行自己写的程序时,有没有发现一个问题:就是物理机器明明只有8G内存,但是我们运行的程序却可以申请到16G的内存?或者说机器上运行的多个进程,占用的总内存已经远超物理内存了,却还能正常工作。其实,这都要归功于CPU和操作系统设计的虚拟内存的机制。所谓虚拟内存,就是机器上运行的一个个的进程,访问的都是虚拟的内存,比如C语言中的指针指向的内存地址,或者gdb调试工具看到的地址,都是虚拟的,并不是机器上的实际物理内存。

而物理内存,简单说就是那根内存条,是机器真正实际可以访问的物理内存空间。你的内存条是 1G 的,那计算机可用的物理内存就是 1G。这个内存条加电以后就可以存储数据了。在早期的 CPU 指令集里,从内存中加载数据,向内存中写入数据都是直接操作物理内存的。也就是说每一个数据存储在内存的什么位置,都由程序员自己负责。例如,8086 这款 40 年前的 CPU 的 mov 指令就可以直接访问物理内存。

1、为什么要使用虚拟内存如果只有单个进程独享整个物理内存,当然没有什么问题。但是如果有多个进程,必然要求程序员手动对数据进行布局,那么内存不够用怎么办呢?而且,每个进程分配多少内存,如何保证指令中访存地址的正确性,这些问题都全部要程序员来负责。

还有,当两个进程要同时对同一个物理内存地址进行读写时,显然是有冲突的。随着计算机上要运行的程序越来越多,这个问题也越来越突出。

​那既然直接访问物理内存效率那么低,现在还有开发人员用这种模式吗?

其实也还是有的。在嵌入式设备中,手动管理内存的操作还是广泛存在的。这是因为在嵌入式开发中,往往没有进程的概念,也就是说整个应用独享全部内存,所以手动管理内存才有可能性。在单进程的系统中,所有的物理资源都是单一进程在管理,直接管理物理内存的操作复杂度还可以接受。

因为直接使用物理地址存在前面所说的问题,所以CPU和操作系统联合设计出了虚拟地址的机制,就是给所有程序可见的都是虚拟地址,这块虚拟地址非常大,并且是连续的,每个程序都可以操作虚拟内存地址,至于说这个虚拟内存对应的是哪块物理内存,交给CPU和操作系统就好了,这样就大大提高了程序的开发效率。

Intel从80286 CPU开始,改变了8086直接访问物理内存的方式,在CPU芯片内部集成了内存管理单元(MMU),进程访问的虚拟内存地址通过MMU,转换成物理地址,然后再通过物理地址访问内存。

​2、进程的虚拟内存空间是什么样的计算机的虚拟内存大小是不一样的。虚拟地址空间往往与机器字宽有关系,下面是32位和64位系统下进程的虚拟地址空间:

​可以看出:

  • 32 位系统上,指向内存的指针是 32 位的,所以它的虚拟地址空间是 2 的 32 次方,也就是 4G。其中,内核空间占用 1G,位于最高处,剩下的 3G 是用户空间。

  • 64 位系统上,指向内存的指针是 64 位的,但在 64 位系统里只使用了低 48 位,所以它的虚拟地址空间是 2 的 48 次方,也就是 256T。其中,内核空间和用户空间都是 128T,分别占据整个内存空间的最高和最低处,剩下的中间部分是未定义的。

进程在用户态时,只能访问用户空间内存;只有进入内核态后,才可以访问内核空间内存。虽然每个进程的地址空间都包含了内核空间,但这些内核空间,其实关联的都是相同的物理内存。这样,进程切换到内核态后,就可以很方便地访问内核空间内存。

​3、进程申请内存时就会对应到物理内存吗既然每个进程都有一个这么大的地址空间,那么所有进程的虚拟内存加起来,自然要比实际的物理内存大得多。所以,并不是所有的虚拟内存都会分配物理内存,只有那些实际使用的虚拟内存才分配物理内存,并且分配后的物理内存,是通过内存映射来管理的。

虚拟空间页面与物理空间页面的映射关系,如下图:

​这个图中,我们需要理解的有这几点:

  • 虽然虚拟内存提供了很大的空间,但实际上进程启动之后,这些空间并不是全部都能使用的。

  • 开发者必须要使用 malloc 等分配内存的接口才能将内存从未分配状态变成已分配状态。在你得到一块虚拟内存以后,这块内存就是未映射状态,因为它并没有被映射到相应的物理内存;直到对该块内存进行读写时,CPU 就会去访问这个虚拟内存, 这时会发现这个虚拟内存没有映射到物理内存, CPU 就会产生缺页中断,进程会从用户态切换到内核态,并将缺页中断交给内核的 Page Fault Handler (缺页中断函数)处理,这时才会真正地为它分配物理内存。然后这个页面才能成为正常页面。

  • 在虚拟内存中连续的页面,在物理内存中不必是连续的。只要维护好从虚拟内存页到物理内存页的映射关系,你就能正确地使用内存了。这种映射关系是操作系统通过页表来自动维护的,不必你操心。

相关视频推荐

Linux内核源码分析之《物理内存与虚拟内存》

剖析linux内核MMU机制详解

庞杂的内存问题,如何理出自己的思路出来,让你开发与面试双丰收

免费学习地址:c/c++ linux服务器开发/后台架构师

需要C/C++ Linux服务器架构师学习资料加qun812855908获取(资料包括C/C++,Linux,golang技术,Nginx,ZeroMQ,MySQL,Redis,fastdfs,MongoDB,ZK,流媒体,CDN,P2P,K8S,Docker,TCP/IP,协程,DPDK,ffmpeg等),免费分享

​4、虚拟地址和物理地址是怎么映射的虚拟地址和物理地址的映射机制,经历了从内存分段到分页的过程,我们先来看内存分段。

4.1 内存分段内存分段机制,简单理解就是根据程序申请使用内存的需要,来把物理内存分成一段一段内存来管理,比如程序需要100M的内存,分段机制就给1段100M连续空间的物理内存与之对应。

在地址寻址上,分段机制维护有段表,并且将虚拟地址分为两部分,一部分是段表项的编号,另外一部分是段内偏移量。每个段表项里面有段的起始地址和段的边界长度,其中段内偏移量应该小于段的边界长度。每个段在段表中有一个项,在这一项找到段的基地址,再加上偏移量,于是就能找到物理内存中的地址。

比如要访问的虚拟地址A,段表项编号为2,段内偏移量是400 ,段表项2的起始地址是1000,边界长度是500,我们可以计算出A对应的物理地址为,段表项2起始地址 1000 + 偏移量 400 = 1400。

分段机制解决了程序使用物理地址存在的问题,但是也有一些不足之处:

  • 存在外部内存碎片:因为每段大小长度不一样,各个段之间会存在多段大小不一样且不连续的空隙,比如存在两段不连续的100M空间,这个时候即使刚好有个进程(例如下图中的进程B)需要申请200M空间,也是没法直接使用这两段100M空间的,因为它们是不连续的。

  • 换入换出效率低:针对两段不连续100M空间不能给需要200M空间的进程B的问题,其实也是可以有办法解决的,就是将其它进程(比如进程A)暂时不用的内存,先通过swap机制写入到磁盘中(换入),等给这个进程B分配好内存后,再从磁盘把进程A回写到内存的另外一段空间(换出)。但是因为磁盘的读写速度比内存读写慢太多了,这个过程会产生性能瓶颈。

​4.2 内存分页由于分段存在前面说的问题,所以提出了内存分页机制。内存分页改变了分段这种粗犷的内存管理方式,它将整个虚拟内存和物理内存空间分成一段段固定大小的片,虚拟内存和物理内存的映射以这个片为最小单位进行管理,我们把这个片称为页,在linux系统上,页的大小为4KB。

分段机制用段表来进行虚拟地址和物理地址的映射,分页机制是用页表来进行映射的。在分页机制下,虚拟地址分为两部分,页编号和页偏移。页编号作为页表的索引,页表包含物理页编号,这个物理页编号与页偏移的组合就形成了物理内存地址。

​内存分页由于内存空间都是预先划分好的,页与页之间是紧密排列的,不会存在像分段中存在的段与段之间的空隙,所以不会有外部碎片。但是分页最小的管理单位是页,即使需要的内存没有一页,也会分配一个页,所以也会存在内部碎片,有内存浪费的情况。

4.3 多级页表上面的页表映射关系看起来还是比较简单的,不过我们得考虑一下页表的大小问题,因为页表是存储在内存当中的,而内存大小比较有限。

以32位linux系统为例,我们来计算一下:32位系统,一个进程的虚拟内存空间为4GB,linux的页大小为4KB,所以需要4GB/4KB=(1024*1024)页,内存分页每一个页表项大小为4字节,可以对应1个页,所以需要的页表项大小就是1024*1024*4 = 4M字节。

4M大小看起来没多大,但是这只是一个进程需要的页表项大小,如果有100个进程,1000个进程呢?显然页表项占用的内存空间就会非常多,而且这还只是32位系统,如果是64位系统,所需要的页表项就更多了。

为了解决页表项过多的问题,Linux 提供了两种机制,也就是大页(HugePage)和多级页表。

大页,顾名思义,就是比普通页更大的内存块,常见的大小有 2MB 和 1GB。页大小变大了,所需要的页自然就少了,页表项也变少了,页表占用的空间也就变少了。大页通常用在使用大量内存的进程上,比如 Oracle、DPDK 等。

多级页表就是把内存分成区块来管理,将原来的映射关系改成区块索引和区块内的偏移。由于虚拟内存空间通常只用了很少一部分,那么,多级页表就只保存这些使用中的区块,这样就可以大大地减少页表的项数。

32位系统使用的是两级页表,将虚拟地址分为三部分:

  • 22~31(10位):一级页号(页目录表)

  • 12~21(10位):二级页号

  • 0~11(12位): 页内偏移量

10位的一级页号,可以表示0~1023,每一个一级页号可以对应一个内存块号,通过这个内存块号,可以找到一个二级页表,在二级页表中可以找到二级页号对应的内存块,然后再加上12位的页内偏移量,就可以算出对应的物理内存。

​所以我们可以知道:

  • 一级页表只有1个,一个一级页表有1024个页表项,每个页表项记录一个二级页表号,总共可以记录1024个二级页表,占用内存大小1024*4 = 4KB

  • 二级页表有1024个,每个页表有1024个页表项,总共占用内存大小1024*1024*4 = 4M

看起来二级页表总共需要4KB+4M的内存空间来存放所有页表项,比一级页表项占用的空间还要多,但实际并不是这样的。

根据局部性原理可知,很多时候,进程在一段时间内只需要访问某几个页面就可以正常运行了。因此没有必要让整个页面都常驻内存,不用把所有的页表都调入内存,只在需要它时才调入,所以只需要一张索引表来告诉我们第几张页表该上哪里去找,就能解决页表的查询问题。建立多级页表的目的在于建立索引,以便不用浪费主存空间去存储无用的页表项,也不用盲目地顺序式查找页表项。

所以,如果某个一级页表的页表项没有被用到,也就不需要创建这个页表项对应的二级页表了,即可以在需要时才创建二级页表,所以两级页表的方式实际上比一级页表大大节省了页表项的内存空间占用。

上面为了理解简单,是以32位系统为例,在我们现在实际使用的linux 64位系统上,采用的方案是4级页表,分别是:

  • PGD:page Global directory(47-39), 页全局目录

  • PUD:Page Upper Directory(38-30),页上级目录

  • PMD:page middle directory(29-21),页中间目录

  • PTE:page table entry(20-12), 直接页表

​4.4 TLBCPU在执行指令时,通过MMU进行虚拟地址到物理地址的转换,这个转换关系是存在页表中的,而页表是存在于内存中的,相对于访问CPU的寄存器或者Cache,访问内存还是要慢得多,所以,根据程序运行的局部性原理以及参照CPU的三级缓存设计思想,我们把页表中的热点表项存到了CPU中叫做TLB(Translation Lookaside Buffer)的硬件里面了。

有了 TLB 后,那么 CPU 在寻址时,会先查 TLB,如果没找到,才会继续查常规的页表。TLB的访问速度非常快,和寄存器相当,比L1访问还快。有了TLB之后,CPU访问某个虚拟内存地址的过程如下:

​5、Linux系统中如何查看进程实际占用的内存大小前面说的是CPU和操作系统对于虚拟内存的映射管理机制,由于程序使用的都是虚拟内存,那在Linux系统上,如何查看进程实际占用的内存大小呢?

我们一般都会用top命令去查看单个进程占用的内存大小,但是我们得结合前面的理论知识去理解top命令查看的参数的实际意义:

​这些数据,包含了进程最重要的几个内存使用情况,我们来看一下:

  • VIRT 是进程虚拟内存的大小,只要是进程申请过的内存,即便还没有真正分配物理内存,也会计算在内。

  • RES 是常驻内存的大小,也就是进程实际使用的物理内存大小,但不包括 Swap 和共享内存。

  • SHR 是共享内存的大小,比如与其他进程共同使用的共享内存、加载的动态链接库以及程序的代码段等。

  • %MEM 是进程使用物理内存占系统总内存的百分比。

这里我们需要注意两点:

  • 虚拟内存通常并不会全部分配物理内存。从上面的输出,你可以发现每个进程的虚拟内存都比常驻内存大得多。

  • 共享内存 SHR 并不一定是共享的,比方说,程序的代码段、非共享的动态链接库,也都算在 SHR 里。当然,SHR 也包括了进程间真正共享的内存。所以在计算多个进程的内存使用时,不要把所有进程的 SHR 直接相加得出结果。

好了,虚拟内存机制的内容就先讲到这里了。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/101013.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

pdf转换成图片免费软件用哪个?pdf转换成图片就用它

随着技术的发展,现在企业办公运用到的电子文档各种各样,我们日常需要掌握的技能越来越高要求,其中pdf和图片是我们经常接触的文件格式之一,而且这两个文件格式我们会经常将它们进行转换,那么pdf转换成图片怎么操作呢?…

LDAP服务器如何重启

1、find / -name ldap 该命令只会从根路径下查看ldap文件夹 find / -name ldap2、该命令会从根路径/查看所有包含ldap路径的文件夹,会查询出所有,相当于全局查询 find / -name *ldap*2、启动OpenLADP 找到LDAP安装目录后,执行以下命令 #直…

数字孪生智慧仓储的关键特点和优势有哪些

数字孪生智慧仓储是一种基于数字孪生技术的智能仓储解决方案。数字孪生是指使用数字模型来模拟和仿真现实世界中的物理实体或系统的技术。在智慧仓储的上下文中,数字孪生被用来创建虚拟的仓储环境,以实时监测、优化和管理仓储操作。 数字孪生智慧…

vue3升级了些什么

Vue 3 升级了以下几个方面的内容: 响应式系统:Vue 3 使用了 Proxy 对象来替代 Vue 2 中的 Object.defineProperty,这使得响应式系统更加高效和灵活。Vue 3 的响应式系统可以追踪更细粒度的依赖关系,提供了更好的性能和更细致的响应…

Apache的简单介绍(LAMP架构+搭建Discuz论坛)

文章目录 1.Apache概述1.1什么是apache1.2 apache的功能及特性1.2.1功能1.2.2特性 1.3 MPM 工作模式1.3.1 prefork模式1.3.2 worker模式1.3.3 event模式 2.LAMP概述2.1 LAMP的组成2.2 LAMP各组件的主要作用2.3 LAMP的工作过程2.4CGI和FastCGI 3.搭建Discuz论坛所需4.编译安装Ap…

BEVFusion复现 (Ubuntu RTX3090)

https://github.com/ADLab-AutoDrive/BEVFusion 1.环境安装 我的机器是RTX3090,CUDA11.1 1.创建虚拟环境 conda create -n bevfusion python3.8.3 2.安装PyTorch 和 torchvision pip install torch1.8.0cu111 torchvision0.9.0cu111 torchaudio0.8.0 -f https://…

C# 如何将使用的Dll嵌入到.exe应用程序中?

文章目录 前言详细实操简要步骤 前言 有没有想自己开发的exe保留一点神秘,不想让他人知道软件使用了哪些dll; 又或许是客户觉得一个软件里面的dll文件太多了,能不能简单一点,直接双击.exe就可以直接运行了,别搞那么多乱七八糟的。…

vue3-vuex持久化实现

vue3-vuex持久化实现 一、背景描述二、实现思路1.定义数据结构2.存值3.取值4.清空 三、具体代码1.定义插件2.使用插件 四、最终效果 一、背景描述 有时候我们可能需要在vuex中存储一些静态数据,比如一些下拉选项的字典数据。这种数据基本很少会变化,所以…

webassembly003 ggml ADAM (暂记)

Adam优化器的工作方式是通过不断更新一阶矩估计和二阶矩估计来自适应地调整学习率,并利用动量法来加速训练过程。这种方式可以在不同的参数更新方向和尺度上进行自适应调整,从而更有效地优化模型。 https://arxiv.org/pdf/1412.6980.pdf 参数 这些参数…

CSS实现白天/夜晚模式切换

目录 功能介绍 示例 原理 代码 优化 总结 功能介绍 在网页设计和用户体验中,模式切换功能是一种常见的需求。模式切换可以为用户提供不同的界面外观和布局方案,以适应其个人偏好或特定环境。在这篇博客中,我们将探索如何使用纯CSS实现一…

Python所有方向的学习路线图!!

学习路线图上面写的是某个方向建议学习和掌握的知识点汇总,举个例子,如果你要学习爬虫,那么你就去学Python爬虫学习路线图上面的知识点,这样学下来之后,你的知识体系是比较全面的,比起在网上找到什么就学什…

数据结构——哈希表

哈希表 这里没有讲哈希表底层的概念,什么转红黑树,什么链表的,这篇文章主要讲的是如何用C实现哈希表,以及哈希表的基本概念。后面我会出一篇文章来讲C中hashmap中的底层逻辑的知识。 哈希表的概念 哈希表是一种数据结构&#xff0…

Unity3D 如何在ECS架构下,用Unity引擎进行游戏开发详解

前言 Unity3D是一款强大的游戏引擎,它提供了丰富的功能和工具,可以帮助开发者快速构建高质量的游戏。而Entity Component System(ECS)是Unity3D中一种新的架构模式,它可以提高游戏的性能和可扩展性。本文将详细介绍在…

一米ip流量池系统

PC端快速切换移动网络IP 支持全网通sim卡槽,国内三大运营商IP池动态切换,实现真实移动端IP切换。从此换IP再也不用vpn或代理,一个设备搞定 1.兼容国内电信,移动,联通三网通的sim卡4G连接,快速稳定2.可直接…

layui框架学习(40:数据表格_主要事件)

Layui数据表格模块主要通过各类事件响应工具栏操作、单元格编辑或点击等交互操作,本文学习table数据表格模块中的主要事件及处理方式。   头部工具栏事件。通过代码“table.on(‘toolbar(test)’, function(obj))”获取lay-filter属性为test的数据表格的头部工具栏…

MySQL中日期、时间直接相减的坑

前言 在牛客网上写一道 SQL 题时,需要计算两个日期之间相隔的秒数,我在写的时候直接将两个日期进行相减,得出来的值却不是相差的秒数。 情景再现 我在 MySQL 中进行了测试,得出的结论是:如果日期类型直接相减&#…

LeetCode 面试题 02.04. 分割链表

文章目录 一、题目二、C# 题解 一、题目 给你一个链表的头节点 head 和一个特定值 x,请你对链表进行分隔,使得所有 小于 x 的节点都出现在 大于或等于 x 的节点之前。 你不需要 保留 每个分区中各节点的初始相对位置。 点击此处跳转题目。 示例 1&#…

【实操干货】如何开始用Qt Widgets编程?(四)

Qt 是目前最先进、最完整的跨平台C开发工具。它不仅完全实现了一次编写,所有平台无差别运行,更提供了几乎所有开发过程中需要用到的工具。如今,Qt已被运用于超过70个行业、数千家企业,支持数百万设备及应用。 在本文中&#xff0…

性能测试工具Jmeter你所不知道的东西····

谈到性能测试,大家一定会联想到Jmeter和LoadRunner,这两款工具目前在国内使用的相当广泛,主要原因是Jmeter是开源免费,LoadRunner 11在现网中存在破解版本。商用型性能测试工具对于中小型企业很难承担相关的费用。国内的性能测试工具有&#…

openGauss学习笔记-53 openGauss 高级特性-Ustore

文章目录 openGauss学习笔记-53 openGauss 高级特性-Ustore53.1 设计原理53.2 核心优势53.3 使用指导 openGauss学习笔记-53 openGauss 高级特性-Ustore Ustore 存储引擎,又名 In-place Update 存储引擎(原地更新),是 openGauss …