内存管理

1.用户层内存管理:

STL:自动分配/释放内存资源(调C++接口)
C++:new/delete,构造/析构(调标C接口)
标准C:malloc/calloc/realloc/free(调POSIX接口)
POSIX:brk/sbrk(调Linux接口)
Linux:mmap/munmap(调Kernel接口)

2.进程内存映象

  1. 程序是保存在磁盘上的可执行文件。
  2. 运行程序时,需要将可执行文件加载到内存,形成进程。
  3. 一个程序(文件)可以同时存在多个进程(内存)。
  4. 进程在内存空间中的布局就是进程映像,从低地址到高地址依次为:
    .代码区(text):可执行指令、字面值常量、具有常属性且初始化的全局和静态局部变量,只读。
    .数据区(data):不具常属性且初始化的全局和静态局部变量。
    .BSS区:未初始化的全局和静态局部变量,进程一经加载此区即被清0。注释:数据区和BSS区有时被合称为全局区或静态区。
    .堆区(heap):动态内存分配,从低地址向高地址扩展。
    .栈区(stack):非静态局部变量,包括函数的参数和返回值。从高地址向低地址扩展。堆区和栈区之间存在一块间隙,一方面为堆和栈的增长预留空间,同时共享库、共享内存等亦位于此。
    命令行参数与环境区:命令行参数和环境变量。

3.虚拟内存

  1. 每个进程都有各自互独立的4G字节虚拟地址空间。
  2. 用户程序中使用的都是虚拟地址空间中的地址,永远无法直接访问实际物理内存地址。
  3. 虚拟内存到物理内存的映射由操作系统动态维护。
  4. 虚拟内存一方面保护了操作系统的安全,另一方面允许应用程序,使用比实际物理内存更大的地址空间。
  5. 4G进程地址空间分成两部分:0到3G-1为用户空间, 如某栈变量的地址0xbfc7fba0=3,217,554,336,约3G;
    3G到4G-1为内核空间。
  6. 用户空间中的代码,不能直接访问内核空间中的代码和数据,但可以通过系统调用进入内核态,间接地与系统内核交互。
  7. 对内存的越权访问,或试图访问没有映射到物理内存的虚拟内存,将导致段错误。
  8. 用户空间对应进程,进程一切换,用户空间即随之变化。内核空间由操作系统内核管理,不会随进程切换而改变。
    内核空间由内核根据独立且唯一的页表init_mm.pgd进行内存映射,而用户空间的页表则每个进程一份。
  9. 每个进程的内存空间完全独立,不同进程之间交换虚拟内存地址是毫无意义的。
  10. 标准库内部通过一个双向链表,管理在堆中动态分配的内存。malloc函数分配内存时会附加若干(通常是12个)字节,
    存放控制信息,该信息一旦被意外损坏,可能在后续操作中引发异常。
  11. 虚拟内存到物理内存的映射以页(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的虚拟内存空间,虚拟内存地址只是一个数字,并没有和实际的物理内存将关联。
所谓内存分配与释放,其本质就是建立或取消虚拟内存和物理内存间的映射关系。

posted @ 2015-04-12 19:50  Jackson3756  阅读(205)  评论(0编辑  收藏  举报