进来看看你对进程虚拟内存的了解有多深?

news2026/3/13 19:56:59
在 Linux 中每个进程都好像是楚门生活在一个别人为它精心构建的世界里而它却以为自己独占了整个系统的内存空间。这正是内核通过虚拟内存机制实现的。本文将带你穿过用户态的表象深入 Linux 内核源码与底层硬件分析这一套复杂的协作机制。在我们正式进入主题之前不妨先试着思考下面几个问题在一个父子进程中如果你同时打印一个全局变量的地址你会发现它们的逻辑地址也就是虚拟地址完全一样。但是当子进程修改这个变量时父进程的值却并没有改变。既然地址一样为什么在子进程中变了父进程中却没变如果你定义一个全局数组int arr[1024*1024] {1}已初始化编译出的可执行文件会多出 4MB。但如果你定义int arr[1024*1024]未初始化可执行文件的大小几乎没变。明明都是定义了 4MB 的数组为什么差别这么大在只有 8GB 物理内存的 Linux 系统上为什么你可以成功malloc出 100GB 的内存而不报错Linux 内核在管理进程的虚拟内存区域VMA时既然已经有了一个双向链表为什么还要大费周章地维护一棵红黑树在什么场景下这棵树会决定你的程序响应速度2018 年Linux 引入了KPTI 机制强制将用户态和内核态的页表分离。这导致了明显的性能下降内核开发者为什么要冒着性能大跌的风险非要在内存布局里建起这堵墙这些问题如果你能答出来那你很厉害了。如果某些地方还有些疑惑也请不要担心下面我会带大家深入了解这背后的底层原理。1. 进程虚拟内存的宏观布局在 Linux 中每个进程眼里的内存都是独占且连续的。但不同架构下进程眼里内存的大小和边界也完全不同。1.1 经典的32位系统在 32 位架构如 x86中地址空间共 2^324GB Linux 默认采用 3:1 分割用户空间 (0 ~ 3GB)进程自己折腾的地方。内核空间 (3GB ~ 4GB)所有进程共享用于存放内核代码、页表、物理内存映射区等。而由于内核空间仅 1GB当物理内存超过 1GB 时内核无法直接映射所有物理内存被迫引入了高端内存机制这极大地增加了内核开发的复杂性。下面介绍一下具体的细节在理想状态下内核希望通过直接映射来工作也就是把这 1GB 的内核虚拟地址直接映射在物理内存的最前面 1GB 上。如果物理内存只有 512MB那么内核这 1GB 的虚拟空间就绰绰有余每一寸物理内存都能在内核里找到相应的映射空间。如果物理内存有 4GB而内核只有 1GB 的虚拟地址。如果内核直接占用了这 1GB 虚拟地址它就只能看到前面 1GB 的物理内存剩下的 3GB 物理内存因为没有对应的虚拟地址映射内核根本无法直接访问。为了解决较小的虚拟内存映射较大的物理内存的问题内核引入了高端内存机制物理内存的前 896MB这部分是直接映射的内核随时可以访问效率极高而 896MB 之后的物理内存内核不给它们分配固定的虚拟地址。当内核需要访问这些 896MB 之后的物理内存时它会使用没有映射物理内存的剩下 128MB 虚拟内存临时建立一个映射指向目标物理内存用完后再拆掉。1.2 现代64位系统在 x86_64 架构下虽然地址线有 64 位但目前 Linux 仅使用了其中的 48 位部分新 CPU 支持 57 位。48 位的地址线提供了 256TB 的虚拟地址空间用户空间 (低 128TB)从 0x0000 0000 0000 0000 到 0x0000 7FFF FFFF F000。内核空间 (高 128TB)从 0xFFFF 8000 0000 0000 到 0xFFFF FFFF FFFF FFFF。由于只使用了 48 位地址线导致中间有一段巨大的、地址位不合法的区域如果程序尝试访问这里硬件会直接抛出通用保护异常。这种设计既精简了硬件电路也为未来扩展留下了空间。在内核空间如此庞大的情况下整块物理内存都会被映射到虚拟地址空间的某一个起始点内核访问任何物理地址都只需要简单的加法偏移。但是带来了便利的同时也付出了一些代价 由于映射范围极大页表本身占用的内存变多了内核现在普遍使用4 级甚至 5 级页表每次翻译地址的开销比 32 位2 级要高。此外所有的指针从 4 字节变成了 8 字节在处理大量包含指针的数据结构时内存占用会显著增加并且对L1/L2 缓存的压力更大因为缓存行能容纳的元素变少了。2. 用户空间详细布局我们从虚拟地址的最低端一路向上扫过看看一个运行中的程序到底把东西都藏在哪了。1.代码段(.text)存放编译后的机器指令。只读、可执行这是为了保护程序不被意外篡改。多个进程运行同一个程序时物理内存中实际只存一份代码。2.常量区 (.rodata)存放const修饰的全局变量、字符串常量。只读尝试修改这里会导致段错误。3.已初始化数据段 (.data)明确赋了初值且初值不为 0 的全局变量和静态变量。可读写它们在程序启动瞬间就有了初始值。4.未初始化数据段 (.bss)未初始化或初值为 0 的全局或静态变量。不占磁盘空间在可执行文件中仅记录一个大小当程序加载到内存时内核会分配内存并将其全部清零。这也是为什么全局变量默认值是 0 的底层原因。5.堆由低地址向高地址生长。6.内存映射区 (mmap)动态链接库、大块内存分配malloc 超过 128KB 时、文件映射。现代 Linux 中通常从高地址向低地址生长紧贴栈底下方。7.栈存放局部变量、函数参数、返回地址。由高地址向低地址生长底部通常设有 Guard Page 保护页一旦触碰即触发溢出报错。8.命令行参数与环境变量用户空间的最高端。存储main(int argc, char argv, char envp)中的参数和系统环境变量。它们由父进程通常是 Shell在execve时压入。3. 内核如何管理这些区域3.1 mm_struct每个进程都有一个task_struct进程控制块而每个进程控制块中都有一个mm指针指向mm_struct它是内存管理的核心数据结构描述了一个进程所拥有的全部虚拟内存视图。所有用户线程共享同一个mm_struct而内核线程mm指针为 NULL因为内核线程没有用户空间它们直接借用上一个进程的内核页表。我们可以把mm_struct的内容分为四大块1.内存映射区域这是最核心的部分进程的虚拟地址空间不是连续的而是由许多离散的块组成的如代码段、数据段、堆、栈、mmap区。mmap指向vm_area_struct的链表按虚拟地址排序方便遍历。mm_rb指向红黑树的根节点红黑树用于快速查找某个地址属于哪个区域也就是寻找 VMA。2.内存段的起止位置start_codeend_code: 可执行代码段的范围。start_dataend_data: 已初始化数据的范围。start_brkbrk: 堆的起点和当前的终点。start_stack: 栈的起点。3.页表指针pgd: 指向当前进程的一级页表也就是全局页目录。当进程切换时内核会把这个pgd的物理地址加载到 CPU 的 CR3 寄存器中。CPU 的 MMU 就能根据这套页表将虚拟地址翻译成物理地址。4.状态统计total_vm: 进程总的虚拟内存大小。rss: 进程当前实际占用的物理内存大小。mm_users/mm_count: 引用计数多个线程共享同一个mm_struct时计数会增加。可能有人会产生这样的疑惑既然有了页表为什么还要mm_struct下面的 VMA 链表其实页表是给内存管理单元 MMU 看的它只负责告诉 CPU地址 A 对应物理地址 B权限是只读。而 VMA 是给内核看的当程序发生缺页中断时内核需要通过mm_struct查表这个地址合法吗如果不合法报段错误。如果合法是因为还没分配物理内存吗如果是则分配一个物理页并填入页表。3.2 为什么有了链表还需要红黑树这其实是 Linux 内核设计中的经典权衡1.双向链表按地址顺序排列方便内核遍历所有的内存区域。比如当你运行cat /proc/pid/maps时内核就是沿着链表走一遍把信息打印出来。2.红黑树当 CPU 访问一个地址时内核需要以极快的速度判断这个地址是否合法并查看这个地址属于哪个 VMA。如果只用链表查找复杂度是O(N)如果进程映射了成千上万个动态库查找会非常慢。而红黑树将查找复杂度降到了O(logN)无论是缺页异常还是内存保护检查红黑树都决定了系统的响应效率。4. 问题解析现在我们来分析一下开头的问题。4.1 延迟分配当你调用malloc分配内存时内核其实是非常聪明的。它并不会立刻跑到物理内存条上去给你占坑而是在mm_struct里加一个 VMA 记录然后告诉你这块内存已经是你的了。这就是为什么你能在 8GB 的机器上申请 100GB 的虚拟内存前提是系统开启了 Overcommit。只有当你真正开始读写这块内存时硬件会发现对应的页表项是空的触发一个缺页异常。这时内核才去分配物理内存并更新页表。4.2 BSS段与已初始化数据段的区别已初始化数据段.data里面存的是具体的初值。这些值必须实实在在地写进磁盘的可执行文件里加载时原样搬进内存。BSS段.bss里面全是初始为 0 的变量。内核只需要记住这里有 4MB 的空间需要清零即可没必要在磁盘上存 4MB 的零。程序启动时内核直接分配一批零页给它既省磁盘又省 IO。4.3 写时复制这是回答开头关于父子进程地址相同但值不同的关键。在 Linux 下fork()产生子进程时并不会复制一份物理内存。相反它让子进程的页表直接指向父进程的物理页并把这些页的权限全部设为只读。对于读操作父子进程共享同一块物理内存相安无事。而对于写操作当子进程尝试修改变量时硬件触发异常。内核查看之后得知这是写时复制页。于是内核会为子进程 **拷贝 **一份物理页再让子进程的页表指向这个新页面然后加上写权限。因此虽然虚拟地址是一样的但背后映射的物理地址已经在你不知不觉中改变了。5. 多级页表与 MMU要理解现代内存管理的精髓必须把MMU和多级页表放在一起看。5.1 页表5.1.1 为什么需要页表假设物理内存是 4GB页面大小为 4KB那么总共有 1M 个页框。如果使用单级页表为了映射这 4GB 空间每个进程都需要一个包含 1M 个表项的数组。如果每个表项占 4B那每个进程光页表就要占用 4MB 内存。此外单级页表要求物理上连续即使中间大片空间没用到你也得为这些空洞预留页表项这太浪费了。5.1.2 多级页表多级页表的核心思想是只有当某个区域真的存了数据才为它建立下一级页表。以 32 位系统两级页表为例一级页表 (PGD/页目录) 将 4GB 分成 1024 份每份 4MB。二级页表 (PTE/页表项) 只有当进程真的访问了某个 4MB 范围时内核才会创建一个二级页表。5.2 MMUMMU是 CPU 内部的一个硬件单元它的唯一任务就是把虚拟地址转换为物理地址。以 64 位 4 级页表为例CPU 从控制寄存器如 x86 的 CR3中读取当前进程第一级页表PGD的物理基地址。逐级拆解地址MMU 会把 64 位的虚拟地址拆成好几段。9bit 索引第一级页表找到第二级地址以此类推。最后一级页表取出物理页的基地址加上虚拟地址末尾的 12bit偏移量得到最终的物理内存地址。5.3 TLB快表由于多级页表需要多次访问内存4 级页表意味着翻译一个地址要查 4 次内存这比 CPU 执行指令慢得多。为了提速MMU 内部有一个超高速缓存TLB。它缓存了最近翻译过的虚拟页到物理页的映射关系。现代系统 TLB 命中率通常在 99% 以上翻译几乎是瞬时的。但是当进程切换也就是切换mm_struct时由于页表变了旧的 TLB 缓存通常必须失效这就是进程切换开销大的原因之一。6. 安全与性能的博弈最后我们聊聊第五个问题。有些系统调用比如gettimeofday频率极高。如果每次都切入内核态上下文切换的开销很大。所以内核会在用户空间最高端映射一个 vDSO 区域里面放的是内核提供的只读代码用户态直接调用无需切换模式。2018 年由于 Intel CPU 的熔断漏洞黑客可以通过预测执行在用户态窃取内核内存的信息。以前用户态和内核态共用一套页表只是权限位不同。现在内核被迫做了隔离用户态运行时页表里几乎不包含内核地址。因此现在每次从用户态进入内核态都要切换页表性能损耗由此而来。但为了安全我们不得不接受这个改变。写在最后:进程虚拟内存不只是为了给进程提供独占空间它是 Linux 内核在效率、性能与安全之间不断权衡的杰作,理解了虚拟内存才算是真正触碰到了操作系统的灵魂。当你下次看到Segmentation Fault时希望你脑海中的认知将不再只是一个简单的报错而是整个 VMA 红黑树在警告“你越界了那是我不曾承诺给你的世界。”本文结束。

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

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

SpringBoot-17-MyBatis动态SQL标签之常用标签

文章目录 1 代码1.1 实体User.java1.2 接口UserMapper.java1.3 映射UserMapper.xml1.3.1 标签if1.3.2 标签if和where1.3.3 标签choose和when和otherwise1.4 UserController.java2 常用动态SQL标签2.1 标签set2.1.1 UserMapper.java2.1.2 UserMapper.xml2.1.3 UserController.ja…

wordpress后台更新后 前端没变化的解决方法

使用siteground主机的wordpress网站,会出现更新了网站内容和修改了php模板文件、js文件、css文件、图片文件后,网站没有变化的情况。 不熟悉siteground主机的新手,遇到这个问题,就很抓狂,明明是哪都没操作错误&#x…

网络编程(Modbus进阶)

思维导图 Modbus RTU(先学一点理论) 概念 Modbus RTU 是工业自动化领域 最广泛应用的串行通信协议,由 Modicon 公司(现施耐德电气)于 1979 年推出。它以 高效率、强健性、易实现的特点成为工业控制系统的通信标准。 包…

UE5 学习系列(二)用户操作界面及介绍

这篇博客是 UE5 学习系列博客的第二篇,在第一篇的基础上展开这篇内容。博客参考的 B 站视频资料和第一篇的链接如下: 【Note】:如果你已经完成安装等操作,可以只执行第一篇博客中 2. 新建一个空白游戏项目 章节操作,重…

IDEA运行Tomcat出现乱码问题解决汇总

最近正值期末周,有很多同学在写期末Java web作业时,运行tomcat出现乱码问题,经过多次解决与研究,我做了如下整理: 原因: IDEA本身编码与tomcat的编码与Windows编码不同导致,Windows 系统控制台…

利用最小二乘法找圆心和半径

#include <iostream> #include <vector> #include <cmath> #include <Eigen/Dense> // 需安装Eigen库用于矩阵运算 // 定义点结构 struct Point { double x, y; Point(double x_, double y_) : x(x_), y(y_) {} }; // 最小二乘法求圆心和半径 …

使用docker在3台服务器上搭建基于redis 6.x的一主两从三台均是哨兵模式

一、环境及版本说明 如果服务器已经安装了docker,则忽略此步骤,如果没有安装,则可以按照一下方式安装: 1. 在线安装(有互联网环境): 请看我这篇文章 传送阵>> 点我查看 2. 离线安装(内网环境):请看我这篇文章 传送阵>> 点我查看 说明&#xff1a;假设每台服务器已…

XML Group端口详解

在XML数据映射过程中&#xff0c;经常需要对数据进行分组聚合操作。例如&#xff0c;当处理包含多个物料明细的XML文件时&#xff0c;可能需要将相同物料号的明细归为一组&#xff0c;或对相同物料号的数量进行求和计算。传统实现方式通常需要编写脚本代码&#xff0c;增加了开…

LBE-LEX系列工业语音播放器|预警播报器|喇叭蜂鸣器的上位机配置操作说明

LBE-LEX系列工业语音播放器|预警播报器|喇叭蜂鸣器专为工业环境精心打造&#xff0c;完美适配AGV和无人叉车。同时&#xff0c;集成以太网与语音合成技术&#xff0c;为各类高级系统&#xff08;如MES、调度系统、库位管理、立库等&#xff09;提供高效便捷的语音交互体验。 L…

(LeetCode 每日一题) 3442. 奇偶频次间的最大差值 I (哈希、字符串)

题目&#xff1a;3442. 奇偶频次间的最大差值 I 思路 &#xff1a;哈希&#xff0c;时间复杂度0(n)。 用哈希表来记录每个字符串中字符的分布情况&#xff0c;哈希表这里用数组即可实现。 C版本&#xff1a; class Solution { public:int maxDifference(string s) {int a[26]…

【大模型RAG】拍照搜题技术架构速览:三层管道、两级检索、兜底大模型

摘要 拍照搜题系统采用“三层管道&#xff08;多模态 OCR → 语义检索 → 答案渲染&#xff09;、两级检索&#xff08;倒排 BM25 向量 HNSW&#xff09;并以大语言模型兜底”的整体框架&#xff1a; 多模态 OCR 层 将题目图片经过超分、去噪、倾斜校正后&#xff0c;分别用…

【Axure高保真原型】引导弹窗

今天和大家中分享引导弹窗的原型模板&#xff0c;载入页面后&#xff0c;会显示引导弹窗&#xff0c;适用于引导用户使用页面&#xff0c;点击完成后&#xff0c;会显示下一个引导弹窗&#xff0c;直至最后一个引导弹窗完成后进入首页。具体效果可以点击下方视频观看或打开下方…

接口测试中缓存处理策略

在接口测试中&#xff0c;缓存处理策略是一个关键环节&#xff0c;直接影响测试结果的准确性和可靠性。合理的缓存处理策略能够确保测试环境的一致性&#xff0c;避免因缓存数据导致的测试偏差。以下是接口测试中常见的缓存处理策略及其详细说明&#xff1a; 一、缓存处理的核…

龙虎榜——20250610

上证指数放量收阴线&#xff0c;个股多数下跌&#xff0c;盘中受消息影响大幅波动。 深证指数放量收阴线形成顶分型&#xff0c;指数短线有调整的需求&#xff0c;大概需要一两天。 2025年6月10日龙虎榜行业方向分析 1. 金融科技 代表标的&#xff1a;御银股份、雄帝科技 驱动…

观成科技:隐蔽隧道工具Ligolo-ng加密流量分析

1.工具介绍 Ligolo-ng是一款由go编写的高效隧道工具&#xff0c;该工具基于TUN接口实现其功能&#xff0c;利用反向TCP/TLS连接建立一条隐蔽的通信信道&#xff0c;支持使用Let’s Encrypt自动生成证书。Ligolo-ng的通信隐蔽性体现在其支持多种连接方式&#xff0c;适应复杂网…

铭豹扩展坞 USB转网口 突然无法识别解决方法

当 USB 转网口扩展坞在一台笔记本上无法识别,但在其他电脑上正常工作时,问题通常出在笔记本自身或其与扩展坞的兼容性上。以下是系统化的定位思路和排查步骤,帮助你快速找到故障原因: 背景: 一个M-pard(铭豹)扩展坞的网卡突然无法识别了,扩展出来的三个USB接口正常。…

未来机器人的大脑:如何用神经网络模拟器实现更智能的决策?

编辑&#xff1a;陈萍萍的公主一点人工一点智能 未来机器人的大脑&#xff1a;如何用神经网络模拟器实现更智能的决策&#xff1f;RWM通过双自回归机制有效解决了复合误差、部分可观测性和随机动力学等关键挑战&#xff0c;在不依赖领域特定归纳偏见的条件下实现了卓越的预测准…

Linux应用开发之网络套接字编程(实例篇)

服务端与客户端单连接 服务端代码 #include <sys/socket.h> #include <sys/types.h> #include <netinet/in.h> #include <stdio.h> #include <stdlib.h> #include <string.h> #include <arpa/inet.h> #include <pthread.h> …

华为云AI开发平台ModelArts

华为云ModelArts&#xff1a;重塑AI开发流程的“智能引擎”与“创新加速器”&#xff01; 在人工智能浪潮席卷全球的2025年&#xff0c;企业拥抱AI的意愿空前高涨&#xff0c;但技术门槛高、流程复杂、资源投入巨大的现实&#xff0c;却让许多创新构想止步于实验室。数据科学家…

深度学习在微纳光子学中的应用

深度学习在微纳光子学中的主要应用方向 深度学习与微纳光子学的结合主要集中在以下几个方向&#xff1a; 逆向设计 通过神经网络快速预测微纳结构的光学响应&#xff0c;替代传统耗时的数值模拟方法。例如设计超表面、光子晶体等结构。 特征提取与优化 从复杂的光学数据中自…