1.用户层内存管理:
STL:自动分配/释放内存资源(调C++接口)
C++:new/delete,构造/析构(调标C接口)
标准C:malloc/calloc/realloc/free(调POSIX接口)
POSIX:brk/sbrk(调Linux接口)
Linux:mmap/munmap(调Kernel接口)
2.进程内存映象
- 程序是保存在磁盘上的可执行文件。
- 运行程序时,需要将可执行文件加载到内存,形成进程。
- 一个程序(文件)可以同时存在多个进程(内存)。
- 进程在内存空间中的布局就是进程映像,从低地址到高地址依次为:
.代码区(text):可执行指令、字面值常量、具有常属性且初始化的全局和静态局部变量,只读。
.数据区(data):不具常属性且初始化的全局和静态局部变量。
.BSS区:未初始化的全局和静态局部变量,进程一经加载此区即被清0。注释:数据区和BSS区有时被合称为全局区或静态区。
.堆区(heap):动态内存分配,从低地址向高地址扩展。
.栈区(stack):非静态局部变量,包括函数的参数和返回值。从高地址向低地址扩展。堆区和栈区之间存在一块间隙,一方面为堆和栈的增长预留空间,同时共享库、共享内存等亦位于此。
命令行参数与环境区:命令行参数和环境变量。
3.虚拟内存
- 每个进程都有各自互独立的4G字节虚拟地址空间。
- 用户程序中使用的都是虚拟地址空间中的地址,永远无法直接访问实际物理内存地址。
- 虚拟内存到物理内存的映射由操作系统动态维护。
- 虚拟内存一方面保护了操作系统的安全,另一方面允许应用程序,使用比实际物理内存更大的地址空间。
- 4G进程地址空间分成两部分:0到3G-1为用户空间, 如某栈变量的地址0xbfc7fba0=3,217,554,336,约3G;
3G到4G-1为内核空间。 - 用户空间中的代码,不能直接访问内核空间中的代码和数据,但可以通过系统调用进入内核态,间接地与系统内核交互。
- 对内存的越权访问,或试图访问没有映射到物理内存的虚拟内存,将导致段错误。
- 用户空间对应进程,进程一切换,用户空间即随之变化。内核空间由操作系统内核管理,不会随进程切换而改变。
内核空间由内核根据独立且唯一的页表init_mm.pgd进行内存映射,而用户空间的页表则每个进程一份。 - 每个进程的内存空间完全独立,不同进程之间交换虚拟内存地址是毫无意义的。
- 标准库内部通过一个双向链表,管理在堆中动态分配的内存。malloc函数分配内存时会附加若干(通常是12个)字节,
存放控制信息,该信息一旦被意外损坏,可能在后续操作中引发异常。 - 虚拟内存到物理内存的映射以页(4K=4096字节)为单位。
通过malloc函数首次分配内存,至少映射33页。
即使通过free函数释放掉全部内存,
最初的33页仍然保留。
返回内存页的字节数:
#include <unistd.h>
int getpagesize (void);
char* pc = malloc (sizeof (char));
|
v<--------------- 33页 --------------->|
------+-------+----------+-------------------+------
| 1字节 | 控制信息 | |
------+-------+----------+-------------------+------
^ ^ ^ ^ ^
段错误 OK 后续错误 不稳定 段错误
4.内存管理APIs
1). 增量方式分配虚拟内存
#include <unistd.h>
void* sbrk (
intptr_t increment // 内存增量(以字节为单位)
);
返回上次调用brk/sbrk后的末尾地址,失败返回-1,increment取值:
0 - 获取末尾地址。
>0 - 增加内存空间。
<0 - 释放内存空间。
内部维护一个指针,指向当前堆内存最后一个字节的下一个位置。
sbrk函数根据增量参数调整该指针的位置,同时返回该指针原来的位置。
若发现页耗尽或空闲,则自动追加或取消页映射。
void* p=sbrk(4); p=sbrk(0);
^ ^
| |
返回 *-- increment ->* 返回
| |
v v
--+---+---+---+---+---+---+--
| B | B | B | B | B | B |
--+---+---+---+---+---+---+--
|<--------- 页 --------
2). 修改虚拟内存块末尾地址
#include <unistd.h>
int brk (
void* end_data_segment // 内存块末尾地址
);
成功返回0,失败返回-1。
内部维护一个指针,指向当前堆内存最后一个字节的下一个位置。
brk函数根据指针参数设置该指针的位置。
若发现页耗尽或空闲,则自动追加或取消页映射。
void* p=sbrk(0); brk(p+4);
^ |
| v
返回 * * 设置
| |
v v
--+---+---+---+---+---+---+--
| B | B | B | B | B | B |
--+---+---+---+---+---+---+--
|<--------- 页 --------
sbrk/brk底层维护一个指针位置,以页(4K)为单位分配和释放虚拟内存。
简便起见,可用sbrk分配内存,用brk释放内存。
3). 创建虚拟内存到物理内存或文件的映射
#include <sys/mman.h>
void* mmap (
void* start, // 映射区内存起始地址,
// NULL系统自动选定,成功返回之
size_t length, // 字节长度,自动按页(4K)对齐
int prot, // 映射权限
int flags, // 映射标志
int fd, // 文件描述符
off_t offset // 文件偏移量,自动按页(4K)对齐
);
成功返回映射区内存起始地址,失败返回MAP_FAILED(-1)。
prot取值:
PROT_EXEC - 映射区域可执行。
PROT_READ - 映射区域可读取。
PROT_WRITE - 映射区域可写入。
PROT_NONE - 映射区域不可访问。
flags取值:
MAP_FIXED - 若在start上无法创建映射,则失败(无此标志系统会自动调整)。
MAP_SHARED - 对映射区域的写入操作直接反映到文件中。
MAP_PRIVATE - 对映射区域的写入操作只反映到缓冲区中,不会真正写入文件。
MAP_ANONYMOUS - 匿名映射,将虚拟地址映射到物理内存而非文件,忽略fd。
MAP_DENYWRITE - 拒绝其它对文件的写入操作。
MAP_LOCKED - 锁定映射区域,保证其不被置换。
4). 销毁虚拟内存到物理内存或文件的映射
int munmap (
void* start, // 映射区内存起始地址
size_t length, // 字节长度,自动按页(4K)对齐
);
成功返回0,失败返回-1。
mmap/munmap底层不维护任何东西,只是返回一个首地址,
所分配内存位于堆中。
brk/sbrk底层维护一个指针,记录所分配的内存结尾,所分配内存位于堆中,底层调用mmap/munmap。
malloc底层维护一个双向链表和必要的控制信息,不可越界访问,所分配内存位于堆中,底层调用brk/sbrk。
每个进程都有4G的虚拟内存空间,虚拟内存地址只是一个数字,并没有和实际的物理内存将关联。
所谓内存分配与释放,其本质就是建立或取消虚拟内存和物理内存间的映射关系。