库与运行库 内存
一.栈
栈保存了一个函数调用所需要的维护信息,称为堆栈帧或活动记录,包含的内容:
- 函数的返回地址和参数
- 临时变量:包含函数的非静态局部变量以及编译器自动生成的其他临时变量
- 保存的上下文:包括在函数调用前后需要保持不变的寄存器
一个函数的活动记录用ebp和esp两个寄存器划定范围。esp寄存器始终指向栈的顶部,同时也就指向了当前函数的活动记录的顶部。而ebp寄存器指向了函数活动记录的一个固定位置,ebp寄存器又被称为栈指针,ebp指向的数据是调用该函数前ebp的值,这样在函数返回的时候,ebp可以通过读取这个值恢复到调用前的值。之所以要保存一些寄存器,在于编译器可能要求某些寄存器在调用前后保持不变,那么函数就可以在调用开始时将这些寄存器的值压入栈中,在结束后再取出。活动记录:
函数返回值传递:
代码:
typedef struct big_thing
{
char buf[128];
} big_thing;
bit_thing return_test()
{
big_thing b;
b.buf[0] = 0;
return b;
}
int main()
{
big_thing n = return_test();
}
- 首先main函数在栈上额外开辟一片空间,将这块空间的一部分作为传递返回值的临时对象,这里称为temp;
- 将temp对象的地址作为隐藏参数传递给return_test函数;
- return_test函数将数据拷贝给temp对象,并将temp对象的地址用eax传出;
- return_test返回之后,main函数将eax指向的temp对象的内容拷贝给n。
伪代码:
void return_test(void *temp)
{
big_thing b;
b.buf[0] = 0;
memcpy(temp, &b, sizeof(big_thing));
eax = temp;
}
int main()
{
big_thing temp;
big_thing n;
return_test(&temp);
memcpy(&n, eax, sizeof(big_thing));
}
如果返回值类型的尺寸太大,C语言在函数返回时会使用一个临时的栈上内存区域作为中转,结果返回值对象会被拷贝两次。因而不到万不得已,不要轻易返回大尺寸的对象。
二.堆
Linux进程堆管理:
Linux提供了两种堆空间分配方式:一个是brk()系统调用,另一个是mmap()。
-
brk():设置进程数据段的结束地址,即它可以扩大或者缩小数据段。如果我们将数据段的结束地址向高地址移动,那么扩大的那部分空间就可以被我们使用。
-
mmap():向操作系统申请一段虚拟地址空间,当然这块虚拟地址空间可以映射到某个文件,当它不将地址空间映射到某个文件时,我们又称这块空间为匿名空间,匿名空间就可以拿来作为堆空间。
void *mmap( void *start, size_t length, int prot, int flags, int fd, off_t offset);
prot/flags这两个参数用于设置申请的空间的权限(可读、可写、可执行)以及映射类型(文件映射、匿名空间等),最后两个参数是用于文件映射时指定文件描述符和文件偏移的。
Q&A:
-
调用malloc会不会最后调用到系统调用或者API?
这个取决于当前进程操作系统批发的那些空间还够不够用,如果够用了,那么它可以直接在仓库里取出来卖给用户;如果不够用了,它就只能通过系统调用或者API向操作系统再进一批货了。
-
malloc申请的空间是不是连续的?
如果“空间”是指虚拟空间的话,那么答案就是连续的,即每一次malloc分配后返回的空间都可以看作是一块连续的地址;如果空间是指“物理空间”的话,则答案是不一定连续,因为一块连续的虚拟地址空间有可能是若干个不连续的物理页拼凑而成的。
堆分配算法:
常见的几种算法:
-
空闲链表:
空闲链表的方法实际上就是把堆中各个空闲的块按照链表的方式连接起来,当用户请求一块空间时,可以遍历整个链表,知道找到合适大小的块并且将它拆分;当用户释放空间时将它合并到空闲链表中。
-
位图:
位图的核心思想是将整个堆划分为大量的块,每个块的大小相同。当用户请求内存的时候,总是分配整个块的空间给用户,第一个块我们称为已分配区域的头,其余的称为已分配区域的主体。而我们可以使用一个整数数组来记录块的使用情况,由于每个块只有头/主体/空闲三种状态,因此仅仅需要两位即可表示一个块,因此称为位图。
-
对象池:
对象池的思路很简单,如果每一次分配的空间大小都一样,那么久可以按照这个每次申请分配的大小作为一个单位,把整个堆空间划分为大量的小块,每次请求的时候只需要找到一个小块就可以了。