内存布局
一般有如下布局几个区:
栈:维护函数调用上下文,离开栈,函数调用没办法实现。
堆:容纳应用程序动态分配的内存区域。
可执行文件映像:可执行文件在内存里的映像。
保留区:队内存中受到保护,禁止访问的内存区域总称。
一个进程里典型的内存布局如下:
栈
栈总是向下增长。栈顶由esp寄存器进行定位,压栈使得栈顶减小,出栈使得栈顶增加。单纯减小esp值等于在栈上开辟空间,单纯增加esp值等于在栈上回收空间。
堆栈针:保存函数调用所需要的维护信息,包括:
1.函数的返回值和参数。
2.临时变量:函数非静态局部变量和编译器自动生成其他临时变量。
3.保存上下文:包括函数调用前后需要保持不变的寄存器。
esp和ebp
esp:指向栈顶,指向当前函数的活动记录顶部。
ebp:指向函数活动记录的一个固定位置。
调用惯例:
调用惯例:确保函数能够被正常调用。包括:
1.函数参数的传递顺序和方式。最常见的是通过栈传递。函数调用方将参数压入栈,函数自己把参数从栈里取出来,有一些调用惯例允许使用寄存器传递参数。
2.栈的维护方式。数据出栈的工作可以由函数的调用方式来完成,也可以由函数本身完成。
3.名字修饰的策略。链接的时候对调用惯例进行区分,调用惯例要对函数本身的名字进行修饰,不同的调用惯例有不同的修饰名字。
C 语言默认的调用惯例是cdecl,它传参时,从右到左的顺序把参数压入栈,由函数调用本身完成出栈,在函数名称前面加一个下划线作为名字修饰。
堆与内存管理
出现堆的原因:栈上的数据在函数返回时会被释放,无法将数据传递到函数外部,全局变量没有办法动态地产生,因此出现堆。
linux进程堆管理:
两种堆空间的分配方式:
brk();
mmap();
brk()设置进程数据段的结束地址,它可以扩大或者缩小数据段,如果把数据段的结束地址向高地址移动,,那么扩大的部分作为堆是常见做法。
mmap()向操作系统申请一段虚拟地址空间,当这段空间不用来映射某个文件的时候,称这块空间是匿名空间。可以用来作为堆。
void *mmap(
void *start,//申请的起始空间
size_t length,//申请的长度
int prot,//设置申请的空间权限
int flags,//设置申请的映射类型
int fd,//文件映射时指定文件描述符
off_t offset//文件映射时指定文件偏移
);
mmap所申请的起始空间和申请长度必须是系统页的整数倍。
堆分配算法:
1.空闲链表:那堆中各个空闲的块按照链表的方式连接起来,当用户请求一块空间时,可以遍历整个链表,找到合适大小的块并把他们拆分,当用户释放空间的时候把他们合并到空闲链表中。
分配方式:
把空闲块分成两部分,一部分作为程序请求的空间,另一部分为剩下来的剩余空间,把空闲块更新为剩余空间,如果剩余块是0,那么直接把它从空闲块的链表中删除。
缺点:一旦链表被破坏或者记录长度的int被破坏,整个堆就无法正常工作。
2位图:整个堆划分为大量的块,每一块大小相同。当用户请求时,总是分配整数个块给用户,可以用一组整数数组来记录块的使用情况。由于每一个块只有头/主体/空闲三种状态,因此只需要两位便能表示一个块。
优点:速度快,稳定性好,块不需要额外信息容易管理。
缺点:容易产生碎片,位图如果很大,查找命中率低。
3对象池:被分配的对象是几个较为固定的值。如果每一次分配的空间大小都一样,那么可以把每次请求分配的大小作为一个单位,把整个堆空间划分为大量小块,每次请求时候找到一个小块就可以了。对于它的管理,空闲链表和位图都可以。