聊聊内存管理

这篇文章我们聊聊内存管理。

本来我想不针对于任何具体的操作系统来谈内存管理,但是又觉得不接地气、言之无物。所以我决定在阐述概念的同时,还针对IA32平台Linux下的内存管理做简要的介绍,并且以实验来证明结论。以下内容分拆为几个大标题和小节,内容前后承接。

物理地址空间

首先,什么是物理地址空间?我们知道CPU与外部进行信息传递的公用通道就是总线,一般而言,CPU有三大总线:控制总线、数据总线、地址总线。这三类总线在一定程度上决定了CPU对外部设备的控制和数据传送能力。其中地址总线决定了CPU能向外部输出的地址宽度,也就是CPU的寻址能力。

通过/proc/cpuinfo可以查看具体的数据:

本文只需要关心红色框内的信息即可,我的CPU拥有36位地址总线,其寻址范围是2^36=64G. 那么其物理上理论能编址的上限就是这么大了。关于48 bits virtual的相关信息以及64 bit的现有实现,则可以参考这里

《Intel 64 and IA-32 Architectures Software Developer’ s Manual》对物理地址空间的解释如下:

重点标记的区域请认真阅读,物理地址空间一定就是内存(DRAM)么?显然不是,文档里也指出了物理地址空间可以映射到read-write memory、read-only memory和memory mapped I/O.

也就是说,除了常见的内存(DRAM),还有主板上的ROM和EEPROM(BIOS、显存等等)也在这个地址空间里。常见的编址方案有IO独立编址和IO统一编址,具体的讨论可以参考这里。下面就假定读者接受了这一基本事实。

启动时的内存信息获取

BIOS的中断调用

上一节说到,物理地址空间被映射到了主存储器,主板上芯片的存储区域等位置。那么,操作系统如何得知这一映射关系,显然不同品牌的机器和主板不可能完全一致么。答案就在BIOS(Basic Input/Output System)了,那BIOS又如何得知呢?嗯,我查到的资料是……探测。具体说来,BIOS其实是一个通称,显卡、网卡、键盘接口电路等外设上都会有一块ROM芯片用于其初始化检测和功能调用。按照规范,这个部分前两个字节必须是0x55和0xAA(和可启动存储介质的第一个扇区结尾字符一样,注意区别),第三个字节是其ROM以512为单位的代码长度,之后就是代码了。从物理地址A0000~FFFFF之间的区域就是保留给外围设备的,如果外设存在,其自带的ROM就会被映射到这个区域。主板BIOS在机器加电后,会以2KB为单位在C0000~E0000之间检索0x55和0xAA并校验长度,执行ROM的代码。有兴趣你可以去读《BIOS研发技术剖析》类似的书,这里我个人不怎么了解,就不敢再多说了。

啰嗦了这么多,其实只需要明白最初的地址映射表是由BIOS检测并提供给操作系统的就好。而操作系统获取这个表的方法自然就是BIOS的中断调用了。e820调用即可获取这些信息,终端下使用dmesg命令打印内核日志就可以看到内核打印出的物理内存布局了。

简要解释下上面的输出:

  • Usable:是已经被映射到物理内存(DRAM)的物理地址。
  • Reserved:这些区间是没有被映射到任何地方,不能当作内存来使用。(内核可以修改这些映射,/proc/iomem文件描述了具体的映射)
  • ACPI data:映射到用来存放ACPI数据的RAM空间,ACPI Table应读入到这个区间内。
  • ACPI NVS:映射到存放ACPI数据的空间,操作系统不能使用。

至于具体的e820调用怎么用,就不展开说了。这段之后的信息有兴趣的同学可以接着去读,内核对内存的映射和统计信息也会随后打印出来。

Shadow RAM

这个小节其实是次要的,留在这里只是为了完整性,本文也不展开去描述了。还是那句话,有探究兴趣的话,请自行去Google.

物理内存管理

通过前文的描述,我想大家已经知道了操作系统终归是拿到了一张物理地址空间的映射表了。那么所谓的内存管理,最主要的部分就是如何来管理主存也就是DRAM的空间了。此处主要的挑战就是实现具体的数据结构和算法,使得内存分配的时候高效的分配内存,并且在内存释放时进行相邻内存块的合并回收以避免内存外部碎片的产生。

Linux内核采用的伙伴内存分配算法就是用来解决这一问题的。 关于Linux的内存管理有无数的好文章和好书在描述了,本文的定位就是梳理脉络,所以我只给出链接,请大家自行去了解。

《物理内存管理中的基本数据结构》

《Linux物理内存管理概述》

伙伴算法的实现自然不只有一种思路,看看下面的文章也是一种启发:

《伙伴分配器的一个极简实现》

虚拟内存管理

接下来就是虚拟内存管理了,[保护模式汇编系列之四] 段页式内存管理,请先看看我以前写得这篇文章了解下虚拟内存出现的原因和解决的问题。我不想太纠结于细节,以免这篇文章过于冗长,但是有些细节不交代清楚有没有办法继续下去。只有自己真正理解掌握了所有细节,才敢站在较高的层次上俯视整个知识脉络,这里的Linux虚拟内存管理容我自己理解深入之后再行补充。

大家可以先参考下武特学长的博文,然后就可以进到下一个章节。

Linux进程的内存布局

又要偷懒贴文章了,之前有写过一篇《进程眼中的线性地址空间》,这里描述的即是虚拟地址空间里的4G线性地址的映射:

学过操作系统的同学一定知道PCB(Process Control Block,进程控制块)吧。

用课本上的话说,进程控制块是用来描述进程的当前状态,本身特性的数据结构,是进程中组成的最关键部分,其中含有描述进程信息和控制信息,是进程的集中特性反映,是操作系统对进程具体进行识别和控制的依据

具体到Linux内核中,所谓的PCB其实就是task_struct这个结构体了。既然PCB描述了进程的信息,自然也就包括了进程内存空间的相关描述信息了。内存相关信息在struct mm_struct mm, active_mm字段,mm指向进程所拥有的内存描述符,而active_mm指向进程运行时所使用的内存描述符。mm_struct里的pgd_t * pgd字段即指向进程的页目录。struct vm_area_struct

  • mmap字段指向虚拟区间(VMA)链表。如下图所示:

了解了这些,就可以到本文的最后一部分了。

探究malloc的效率与写时映射

开始描述前,写点代码玩玩先。通过上文的描述,我想大家已经了解了32bit下linux进程拥有的4G线性地址空间只有3G是属于进程所有的。那么我们容易想到,malloc函数从堆里获取到的内存最多也不会超出这个范畴。而程序代码和链接库部分也占据了一定的空间,所以可以申请到的内存的总数应该略与3G这个数字(视进程本身代码和数据占据大小而定)。代码如下:

#include <stdio.h>
#include <stdlib.h>

int main(int argc, char *argv[])
{
    int count = 0;

    while (malloc(1*1024*1024) != NULL) {
        count++;
    }
    printf("count is %f G\n", count / 1024.0f);

    sleep(10000);

    return EXIT_SUCCESS;
}
 

编译运行,结果如下:

这基本也证实了我们的猜想。接着我们去掉sleep语句用time命令记录内存申请时间可以看到:

以1MB为单位分配接近3G的内存也太快了点吧,就是逐一建立页表也没有这般迅速吧?
更神奇的还在后面,如果我们让程序sleep,在另一个终端执行free命令查看内存占用的话,会看到神奇的结果:

used那里居然几乎没有增加,这不科学!

Linux系统调用提供的内存获取的函数是brk/sbrk和mmap,而且时以页(通常4K)为单位进行内存的分配的。而Glibc实现的malloc/free是建立在系统调用之上的内存“批发后零售”的函数。问题肯定出在brk/sbrk等系统调用上。这两个系统调用是增加程序可用的堆区的空间,其实内核仅仅只是修改里进程PCB里vm_area_struct链表中堆那个节点的结束位置(许可地址范围),并没有真正去获取物理内存并建立页表的映射。为什么要这么做呢?原因大家可以自己去思考,比如进程申请的内存不一定立即就会完全用到,可以延迟到使用的时候再去分配,以暂时节省物理内存。那么具体流程是怎样的呢?

其实,CPU在分页开启后,对于给出的线性地址(此处略过分段)会由MMU进行页表的查阅来翻译为最终的物理地址,如果在页表中查阅不到或者该页不存在呢?此时CPU会产生一个内部异常: 14 #PF 页故障。此时处理流程会转入到内核为该异常创建的对应异常处理函数去执行,内核此处的代码首先会遍历当前进程的vm_area_struct链表,检查该地址是否在许可的地址范围内,如果是为其申请物理内存并建立映射。之后异常返回到触发了异常的代码出继续执行,所以程序接着运行下去。如果发现该地址是非法的地址,内核为给进程发信号SIGSEGV,该信号的默认处理函数即会打印出段错误,然后结束进程。流程即是:

缺页异常->异常处理函数->task_struct、mm_struct、vm_area_struct->页分配->中断返回

具体的函数调用细节,大家可以看看 malloc()之后,内核发生了什么?

这篇定位为科普的文章到此就结束了。几个小时的描述难免有些仓卒,先贴出来大家看看,后续再慢慢修改。

时间: 2024-10-31 17:59:59

聊聊内存管理的相关文章

c#内存管理.

尽管在.net framework中我们不太需要关注内存管理和垃圾回收这方面的问题,但是出于提高我们应用程序性能的目的,在我们的脑子里还是需要有这方面的意识.明白内存管理的基本行为将有助于我们解释我们程序中变量是如何操作的.在本文中我将讨论栈和堆的一些基本知识,变量的类型和某些变量的工作原理. 当你在执行程序的时候内存中有两个地方用于存储程序变量.如果你还不知道,那么就来看看堆和栈的概念.堆和栈都是用于帮助我们程序运行的,包含某些特殊信息的操作系统内存模块.那么堆和栈有什么不同呢? 堆VS栈的区

iOS开发系列—Objective-C之内存管理

概述 我们知道在程序运行过程中要创建大量的对象,和其他高级语言类似,在ObjC中对象时存储在堆中的,系统并不会自动释放堆中的内存(注意基本类型是由系统自己管理的,放在栈上).如果一个对象创建并使用后没有得到及时释放那么就会占用大量内存.其他高级语言如C#.Java都是通过垃圾回收来(GC)解决这个问题的,但在OjbC中并没有类似的垃圾回收机制,因此它的内存管理就需要由开发人员手动维护.今天将着重介绍ObjC内存管理: 引用计数器 属性参数 自动释放池 引用计数器 在Xcode4.2及之后的版本中

内存管理 之 存储器硬件知识

接下来,为了顺应Linux Kernel的学习,在操作系统方面首先学习的是内存管理.首先主要讲解物理内存的相关知识.本节主要讲解存储器的基础硬件知识,下一节讲解存储器的层次结构.   存储器是计算机系统的重要组成部分,它在计算机系统中的作用是存放程序和数据.存储器不仅使计算机具有记忆功能,而且是计算机高速自动运行的基础. 作为计算机的核心部件之一,存储器直接关系到整个计算机系统性能的高低.如何以合理的成本搭建出容量和速度都满足要求的存储器系统,始终是计算机体系结构设计中的关键问题之一:一方面,人

iOS ARC 内存管理要点

前言 在讨论 ARC 之前,我们需要知道 Objective-C 采用的是引用计数式的内存管理方式,这一方式的特点是: 自己生成的对象自己持有.比如:NSObject * __strong object = [NSObject alloc] init];. 非自己生成的对象自己也能持有.比如:NSMutableArray * __strong array = [NSMutableArray array];. 自己持有的对象不再需要时释放. 非自己持有的对象自己无法释放. 而 ARC 则是帮助我们

.NET中的内存管理,GC机制,内存释放过程

引言 作为一个.NET程序员,我们知道托管代码的内存管理是自动的..NET可以保证我们的托管程序在结束时全部释放,这为我们编程人员省去了不少麻烦,我们可以连想都不想怎么去管理内存,反正.NET自己会保证一切.好吧,有道理,有一定的道理.问题是,当我们用到非托管资源时.NET就不能自动管理了.这是因为非托管代码不受CLR(Common Language Runtime)控制,超出CLR的管理范围.那么如何处理这些非托管资源呢,.NET又是如何管理并释放托管资源的呢? 自动内存管理和GC 在原始程序

C# 语言规范--1.4 自动内存管理

规范 手动内存管理要求开发人员管理内存块的分配和回收.手动内存管理可能既耗时又麻烦.在 C# 中提供了自动内存管理,使开发人员从这个繁重的任务中解脱出来.在绝大多数情况下,自动内存管理可以提高代码质量和开发人员的工作效率,并且不会对表达能力或性能造成负面影响. 示例 using System; public class Stack {    private Node first = null;    public bool Empty {       get {          return

[share]深入探讨PHP中的内存管理问题

一. 内存 在PHP中,填充一个字符串变量相当简单,这只需要一个语句"<?php $str = "hello world "; ?>"即可,并且该字符串能够被自由地修改.拷贝和移动.而在C语言中,尽管你能够编写例如"char *str = "hello world ";"这样的一个简单的静态字符串:但是,却不能修改该字符串,因为它生存于程序空间内.为了创建一个可操纵的字符串,你必须分配一个内存块,并且通过一个函数(

PHP原理之内存管理中难懂的几个点

PHP的内存管理, 分为俩大部分, 第一部分是PHP自身的内存管理, 这部分主要的内容就是引用计数, 写时复制, 等等面向应用的层面的管理. 而第二部分就是今天我要介绍的, zend_alloc中描写的关于PHP自身的内存管理, 包括它是如何管理可用内存, 如何分配内存等. 另外, 为什么要写这个呢, 因为之前并没有任何资料来介绍PHP内存管理中使用的策略, 数据结构, 或者算法. 而在我们平时开发扩展, 修复PHP的bug的时候, 却对这一部分的知识需要有一个良好的理解. PHP开发组内的很多

深入SQL SERVER 2000的内存管理机制(二)

server 深入SQL SERVER 2000的内存管理机制(二)     http://msdn.microsoft.com/data/default.aspx?pull=/library/en-us/dnsqldev/html/sqldev_01262004.asp   可访问大地址的应用 (Large-Address-Aware Executables) 在Windows增加支持/3GB参数以前,一个应用程序是无法访问一个带有高位设置的指针.一个32位的指针只有前31位地址空间可以被用户