内存管理简单地说主要解决程序放入内存何处, 以及运行时如何进行地址映射找到程序并执行的问题。 其核心问题有两方面, 一是内存空间要充分利用, 另一方面是根据其分配内存的机制形成地址映射, 在运行时能快速定位到代码或数据的内存位置, 执行或操作数据。 这些底层原理的内容, 我们不深入分析, 但基于进程运行离不开内存的管理, 本节更多是从程序员或应用程序角度上认识内存, 从而进一步理解与内存管理机制密切相关的一些情况下进程的运行原理。
内存是计算机系统中重要的存储资源, 其空间有限, 所以需要有效的管理和使用。 生活中很多场合也有分配空间的问题。 以学校为学生学习分配教室为例, 如果以班级为单位分配一个能容纳下的大教室, 这实际就是按分区分配空间的连续存放数据的管理方式。 但固定的分区大小不一定和班级人数一致, 必然有空闲浪费的空座, 改进的话可以根据班级人数划定等量座位的教室, 这就是动态分区的思想了。 但是动态分区也存在问题, 各班使用教室的人数不确定, 用完的教室回收后再划给另外的班级, 总会有一些差值数量的座位剩余, 而这些碎片空间不足以分给某个班级使用。 于是, 现代操作系统对内存的管理从粗放的分区分配, 发展出了精细的离散分配的方式。 仍以分配自习教室为例, 一个班的学生不要求坐在一起, 可以以10 人一组为单位, 教学楼的所有座位也按10 人一排为单位进行分配, 一个班分到的座位可能是教学楼中的任何排上。 这种管理可以使教室的座位资源尽可能的利用起来, 浪费的空间最多也就是不满10 人一组时的个别座位。 但这种管理也会有代价, 一个班学生在教室的位置, 需要以组为单位登记地址形成登记表。 这种管理方式下, 如果我们知道3 组同学在登记表中登记的位置, 只要找到改组位置, 再顺次找到第36 个同学的位置并不难。 在这种哪里有空用哪里的离散存放数据的思想下, 需要更细地划分空间、 管理登记表, 看似管理变复杂了, 但是如果这个场景放在计算机系统中, 以看似复杂的登记表(页表)为代价, 换来内存空间的充分利用也是值得的; 而实际上计算机使用二进制表达数据的特性会使管理和计算并不像想象的那么复杂。
讨论了空的分配问题, 还要考虑程序运行时数据的逻辑含义和组织问题。 如同班级中有男女生的区别, 程序运行时的各种数据也有不同性质意义, 有的数据是执行代码、 有的则是各种代表不同含义的数值数据。 一般程序从产生到运行, 总要经过从高级语言代码编译成可执行文件(可执行指令及数据的集合), 然后将可执行文件装入内存执行的过程。 在装入内存执行的过程中, 先要处理数据的逻辑组织问题, 然后才是把数据放入到分配的内存空间。 可以说Linux 系统执行可执行程序时, 组织数据逻辑的过程完成了对程序虚拟内存空间的描述, 而放入内存的过程就形成了虚拟内存到实际物理内存的映射关系。 虚拟内存概念比较抽象, 可结合图5.22 理解。
图5.22 虚拟内存的形成及其与物理内存的关系
所谓虚拟就是并不真实存在的意思, 比如有一个人形容他有一栋楼的产业, 一楼是划分的各种工作室, 二楼是仓库, 三楼是根据需要随时划分的大平层, 但当他实际领你去看的时候, 可能才发现工作室是在某栋实体楼的三层, 而仓库是在另一处位置, 三楼的大平层也不和工作室、 仓库在一起。 这就是虚拟, 可以说描述出的空间和实际的空间并不相同, 但两者间有映射关系。 换到计算机系统场景下, 假设一个系统有4GB 的内存空间, 某程序进入内存时, 分析可执行程序的内容了解到一些关键数据, 比如代码指令有8kB, 数据变量需要1kB。 现在, 我们可以说我们形成了一个虚拟描述: 程序执行有一个4GB 的内存空间, 从0 字节地址开始, 先放8kB 的指令代码形成代码段, 然后再放1kB 的数据形成数据段。 如果程序运行中又有动态申请空间的代码执行, 就再从剩余空间中分配需要的量。
那么0 字节地址(即虚拟地址为0)的这个字节数据到底在内存的什么位置呢?(www.daowen.com)
这是由内存分配决定的, 只要分配时登记好映射表, 执行时通过映射表就能定位到0字节地址的这个数据在物理内存的位置。
那么如果用户程序在运行中调用了malloc 类的内存申请函数要求1kB 的空间, 系统如何分配空间返回地址呢?
从虚拟内存的角度看, 用户已经用了4GB 内存的8kB+1kB, 也就是0~9kB-1 这些字节地址已经被占了, 动态申请的内存就是从9kB 字节开始在剩余的内存中划1kB 的内存即可。 至于这1kB 到底在物理内存的哪里并不需要告诉用户, 只需要在映射表中登记好9kB开始1kB 大小的虚拟内存区域及其实际对应的物理地址即可。 当用户用9kB 这个地址访问这块内存时, 系统利用映射表帮其定位过去执行即可。
可见, 用户编程时得到的都是内存虚拟地址, 面对的也都是抽象的虚拟内存。 虚拟内存的空间大小是地址寻址位决定的, 一个32 位的寻址系统, 可以用32 位表达232B=4GB 的地址范围。 32 位的Linux 系统一般是从地址部分开始放程序代码, 但是也不是绝对的从0 地址开始, 一部分低地址用于映射系统的中断向量表等信息了, 实际上其中0x08048000~0xbfffffff是用户空间, 0xc0000000~0xffffffff 是内核空间。 而64 位的系统中, 理论上讲可以支持的虚拟内存空间规模大得多, 但很多系统对虚拟地址可用位数做了限制, 虚拟地址空间不是264,而一般是限制虚拟地址可用比特数为48 位, 其中0x0000000000000000~0x00007fffffffffff 表示用户空间, 0xFFFF800000000000~0xFFFFFFFFFFFFFFFF 表示内核空间, 提供256TB(248)的寻址空间(类比一下Windows 系统是将地址位削减为44 位)。 这种划分下, 用户空间和内核空间的区别是第47 位与第48~63 位相同, 若这些位为0 表示用户空间, 否则表示内核空间。 用户空间由低地址到高地址仍然是只读段、 数据段、 堆、 文件映射区域和栈, 实际上虚拟内存的结构还有更多需要深入理解的部分; 其最高地址部分会划分一块用于映射操作系统程序(内核程序), 供所有用户程序共享调用。 图5.21 中我们只画出了虚拟内存的用户区部分, 其高地址部分是内核区没有画出。 虚拟内存管理中把用于动态划分内存的剩余的这块空间定义为堆, 而程序执行时存在跳转和传参, 这些临时变量和返回地址, 一般按规定保存在这块剩余空间的高地址部分, 由于该部分数据采用先进后出的方式, 此处称为栈。 为了防止攻击者通过数据溢出等跳转到内存的特定位置, Linux 在内核版本2.6.12 后添加ASLR(Address Space Layout Randomization, 又称地址空间配置随机化、 地址空间布局随机化), 随机放置进程关键数据区域的地址空间, 以防范恶意程序。 更多内容本节不再展开。
从应用的角度上, 我们主要是为了明确一个结论: 用户编程面对的始终是虚拟内存,通过各种函数调用操作内存时, 系统返回的地址也都是虚拟地址; 而虚拟地址到物理地址的映射和管理由系统管理, 用户不需操心, 这也体现了操作系统屏蔽底层细节, 方便用户使用的特性。
免责声明:以上内容源自网络,版权归原作者所有,如有侵犯您的原创版权请告知,我们将尽快删除相关内容。