《程序员的自我修养》读书笔记——内存

前面几篇文章都是关于静态库,动态库相关的介绍。平时开发中接触并不是很多。从这篇开始进入和工作中关系比较大的部分,库和运行库。第一篇就从内存开始介绍。

本文导图


内存布局

现代应用程序都运行在一个内存空间里面,在32系统里面这个内存空间拥有4GB的寻址能力。16位时代是用段地址加偏移地址的寻址模式用尽不用了。如今的应用程序直接使用32位的地址进行寻址。这种方式叫做平坦的内存模型。整个内存是一个统一的地址空间,用户可以使用一个32位指针访问任何一个内存位置。

int *p=(int *)0x12345678;
++*p;

如上可以直接读写指定内存的数据。但是真真的情况下并不是这么平坦的。因为大多数操作系统会把内存空间的一部分给内核使用,应用程序时无法访问的,对应的内存空间也就叫做内核空间。相对内核空间,应用程序能够访问到的就是永恒空间。一般而言,在用户空间,很多区域有特殊含义。一个应用程序有如下默认的区域:

  • 栈:用于维护函数调用的上下文,通常是在用户空间高地址除分配,有数兆字节大小
  • 堆:堆用来容纳应用程序动态分配的内存区域。比如使用malloc、new分配内存的时候。堆通常位于栈的下方(低地址方向),并且堆一般没有统一的区域,而且范围也比栈大很多。
  • 可执行文件映像:存储可执行文件在内存中的映像。之前介绍过,由装载器在装载的时候将可执行文件的内存读取或映射到这里。
  • 保留区:不是一个单一的内存区域,而是对内存中受保护而禁止访问内存区域的总称。比如大多数操作系统绩效的地址通常不允许访问比如NULL,因为0地址正常情况不可能有有效的可访问数据。

如下图是Linux一个进程的内存布局:


注意还有一个动态链接库映射区,这个区域用于映射装载的动态链接库,比如在Linux下,如果有其他共享库链接,那么系统就会从0x4000 0000开始地址分配。

栈最大的特点就是先进后出。在数据结构中,栈被定义为一个特殊的容器,可以压栈和出栈。在计算机系统中,栈则是一个具有数据结构中栈的属性的一个动态内存区域。

栈总是向下增长的,栈顶由称为esp(有些也叫sp)寄存器定位。


栈保存了一个函数调用所需要的维护信息,被称作为堆栈帧或者活动记录,堆栈帧一般有如下内容:

  • 函数的返回地址和参数
  • 临时变量,包括函数的非晶体局部变量及编译器自动生成的其他临时变量
  • 保存的上下文,包括在函数调用前后保持不变的寄存器

在i386中,栈帧用ebp和esp这链两个寄存器划定范围。esp指向栈顶,ebp指向栈帧的一个固定位置,ebp又叫做帧指针。

比如:


在参数之后的数据就是函数的活动记录,ebp固定在其中一个位置,ebp不会随着函数的执行变化而变化,esp指向栈顶,会随着函数的执行变化而变化。ebp用来定位函数活动记录的各个数据。

ebp之前就是函数返回地址(ebp-4),再往前就是压入栈的参数 ,地址分配是ebp-8、ebp-12,根据参数大小不同而定。

ebp指向的数据是调用函数前的ebp的值,这样函数返回的时候ebp可以通过这个值恢复到调用前的值。

i386函数调用过程:

  • 把所有或者一部分参数压入栈中,如果有些参数没有入栈则使用特定的寄存器保存。——参数
  • 把当前指令的下一条指令地址压入栈中。——返回地址
  • 跳转到函数体执行——上一步和这一步由指令call一起执行
  • push ebp;将ebp压入栈中——old ebp
  • mov ebp,esp ebp =esp——这个时候ebp指向栈顶,恰好这个时候栈顶就是old ebp
  • sub esp,xxx——在栈上分配xxx字节的临时空间
  • push xxx——如果有必要,保存名为xxx寄存器的内容
  • pop xxx——将栈顶数据恢复到xxx寄存器
  • mov esp,ebp——恢复esp,回收栈局部变量空间
  • pop ebp——从栈中恢复保存的ebp的值
  • ret ——从栈中取回返回地址,并跳转到该位置。

一个例子:

调用惯例(一种调用方和被调用的约定)

如果函数调用方决定利用寄存器传递参数,而函数本身却以栈的方式传递,那么函数无法获取正确的参数。调用方和被调用方对于函数如何调用有一个明确的约定,只有双方遵守同样的约定,函数才能被正确调用,这样的约定被称作调用惯例。

  • 参数传递顺序和方式
  • 栈的维护方式
  • 名字修饰的策略

c语言中使用的cdel惯例:


看一个例子:


  • 虚线表示指令执行后的栈状态
  • 实线表示程序的跳转状态

最好按照执行过程走一遍,弄清楚整个流程。

函数返回值传递

函数返回值存储在eax中,返回后函数的调用方再读取eax。但是exa只有4个字节,那么如何返回大数据的返回值呢。

几乎所有的调用惯例都是采用eax和edx联合返回的方式进行。exa存储低1-4字节,edx存储高1-4字节。

可以通过反汇编来分析eax和edx

源代码:


分析结果:


如果返回值的尺寸太大,C语言在函数返回时会使用一个临时的栈上内存区域作为中转,结果返回值的对象会被拷贝两次,所以不到万不得已不要轻易返回大尺寸的对象。

堆的内存管理比栈更为复杂,在任意时刻程序都可能发出请求,要么申请一段内存,要么释放一段已经申请过的内存。

栈在函数返回时数据就会被清空,所以无法将数据传递至函数外部,而全局变量没有办法动态产生,只能在编译的时候定义,所以堆是唯一的选择。

堆是一块巨大的空间,占有虚拟内存很大一部分。程序可以在堆上主动申请一块连续的内存,直到程序主动放弃。

int main(){
  char* p=(char *)malloc(1000);
  //p指针指向1000大小的数组
  free(p);
}

malloc一种简单的实现是通过操作系统内核去完成的,把进程的内存管理给操作系统内核,系统提供一个系统调用。但是这样性能比较差,因为程序申请释放堆的操作比较频繁,每次都需要在用户太和内核态切换会消耗非常大的开销。比较好的做法就是把程序向操作系统申请一块适当大小的堆空间,然后由程序自己管理这块空间,往往是通过运行库来管理的。

运行库充当的而是像操作系统批发一块较大堆空间角色,然后零售给程序使用。当全部使用完或者程序有大量内存需求的时候,运行库再向操作系统进货。通过堆分配算法来保证分配的内存不会出现冲突。

Linux进程堆管理

Linux 提供了两种堆空间的分配方式(两种系统调用),一种是brk系统调用,一种是mmap系统调用。brk原型int brk(void* end_data_segment)

brk

brk作用实际上就是设置进程数据段的结束地址,扩大 或者缩小数据端,如果数据段结束地址向高地址移动那么扩大的部分就可以被使用。另一个和brk类似的是sbrk,只不过sbrk传入的是需要增加空间的大小(负数就是减小),返回时值数据段的结束为止,其实就是对brk的一次包装。

mmap

作用是向操作系统申请一段虚拟地址空间,这块虚拟地址空间可以映射到某个文件,当不再映射到某个文件的时候,又叫这个空间为匿名空间——虚拟内存和磁盘交换

声明如下:


字段 含义
start 申请空间的起始地址
length 申请空间的大小,和start决定虚拟空间
prot 设置申请空间的权限,可读、可写、可执行
flags 映射类型,比如文件映射、匿名空间等
fd 文件描述符
offset 文件偏移

malloc

glibc中是通过malloc申请空间的,小于128kb的请求来讲会在现有的堆空间里面,安装堆分配算法分配一块空间并返回;如果大于128kb的请求会通过mmap申请一块匿名空间,然后在匿名空间中为用户分配空间。

申请空间的其实地址和代销都必须是系统页大小的整数倍。所以对于字节很小的请求用mmap的话,会非常浪费。

Windows进程堆管理

关于Windows的内容这里暂时先省略,有感兴趣的同学可以自己去看一看。

值得注意的几个问题

  • 一个堆里面的空间,如果被多次释放,那么会在重复释放的时候产生错误
  • 调用malloc首先会从系统分配给应用功能的堆里面去取,如果不够用才会通过系统调用再申请空间
  • malloc申请的空间在进程结束后都不会存在了,不仅内存如此。凡是和进程相关的资源,比如地址空间,物理内存,打开的文件,网络链接等都会被系统回收
  • malloc申请的空间在虚拟空间是连续的,在对应的物理空间不一定连续

堆分配算法

如何管理一大块连续的内存空间,能够按照需求 分配和释放,这就是堆分配算法。堆分配算法有很多种,这里介绍简单的几种

空闲链表

空闲链表就是把堆中各个空闲的块通过链表的方式联系起来,当前用申请一块空间的时候 ,可以遍历整个链表,直到找到合适大小的块,并将它拆分,当用户释放空间的时候将它合并到空闲链表中。

当找到了足够容纳请求的一个空闲快,会把块分为两个部分,一部分为程序申请的,一部分是没有用到剩下的的(申请的和块大小不一定完全一样)。下图表示申请了一块和空闲块2相等堆后的状态


在释放的时候,需要知道这个块的大小,那就在后面多加4个字节,用于存储分配的大小。这样在释放的时候就知道大小了。

位图

核心思想就是把整个堆分为大量的块,每个块的大小相同,当用户申请的时候,总是分配给整数个块给用户。第一个块称为分配区域的头,其余的我主体。那么所有的块只有三种状态头(H)、主体(B)、空闲(F)。所有可用两个二进制就能表示出来。


其中11表示H,10表示主体,00表示空闲
如上图,这个堆分配3个片内存,分别有2、4、1块。对应的位图是

  • 速度快:整个堆空闲信息存储在一个数组内,隐藏访问数组时cache容易命中。
  • 稳定性好
  • 快不需要额外的信息
  • 但是分配内存的时候容易造成随便,比如分配300字节,实际分配了3个快,也就是384.

对象池

上面两种是最为基本的。被分配对象大小是较为固定的几个值。对象池是一个对于这种场景更为高效的算法。

思路:如果一次分配的空间大小都一样,那么就可以按照这个每次请求分配的大小作为一个单位,把整个堆空间划分为大量的小块,没次请求的时候只需要找到以小块就可以了。

核心在于与上面两种的区别是假定每次请求的都是一个固定的大小,因此实现起来容易。

实际中堆分配算法往往是结合起来的,比如glilbc,对于64字节空念的申请,采用的是类似于对象池的方式,对于大于512字节的采用最佳适配算法,对于大于128KB的,会直接使用mmap机制直接向操作系统申请。

小结

这一节总体看来不是特别复杂,因为这一层平时接触比较多。相对于编译连接,静态、动态库之类的要简单很多。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 215,874评论 6 498
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,102评论 3 391
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 161,676评论 0 351
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,911评论 1 290
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,937评论 6 388
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,935评论 1 295
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,860评论 3 416
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,660评论 0 271
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,113评论 1 308
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,363评论 2 331
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,506评论 1 346
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,238评论 5 341
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,861评论 3 325
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,486评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,674评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,513评论 2 368
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,426评论 2 352

推荐阅读更多精彩内容