文档地址:https://www.gnu.org/software/libc/manual/html_node/Memory.html#Memory
3 虚拟内存分配和分页
3.1 进程内存概念
内存是进程可以使用的最基本资源之一。典型的,每个进程都有一个虚拟地址空间,从0到一个最大值(4G)。
不需要连续,也就是说,并不是所有地址都可以用来存储数据。
虚拟内存分割成4KB大小的页,对应物理内存的页框或磁盘空间。可以是交换分区或磁盘文件。
相同的物理内存可以映射到多个虚拟页。
访问虚拟页的一部分时,该虚拟页需要被映射到页框中。
虚拟内存通常大于物理内存,这就需要进行换入换出。
当进程访问的虚拟页不在物理内存中时,产生页错。内核会挂起进程,将页调入物理页框。
对进程来说,所有的页似乎一直在物理内存中。
在每一个虚拟地址空间,进程需要记录每个地址有些什么,这一过程叫内存分配。
进程的内存分配主要确保不将两个不同的东西放到同一个字节中。
进程分配内存主要有两种方式:调用exec和调用内存分配函数。
第三种方式是fork。
exec主要做以下操作:为进程创建虚拟内存空间,加载程序,执行程序。
由exec族函数完成(如execl)。
这些操作读取可执行文件,分配可执行文件中的数据空间,加载代码,将控制权转移给可执行文件。
这些数据通常指程序的文本段,也包括字符串、常量及静态变量等。
程序开始执行后,使用分配函数获得更多的内存。
使用GNU C Library的C程序有两种分配方式:自动和动态。
内存映射I/O是另一种获取动态虚拟内存的方式。
进程的虚拟地址空间划分为多个段。每个段是一个连续的虚拟地址。
三个重要的段:文本段、数据段、栈段。
3.2 为程序数据分配存储空间
3.2.1 C程序中的内存分配
C语言支持两种通过C程序中变量进行内存分配的方式:
声明静态变量或全局变量的时候进行静态分配。
声明自动变量如函数参数和局部变量的时候进行自动分配。
C变量不支持动态分配方式,但是可以通过GNU C Library完成。
3.2.1.1 动态内存分配
动态内存分配是进程运行过程中决定信息存储位置的技术。
系统调用是获取动态分配内存的唯一方式(通过调用GNU C Library函数),
指针是引用动态分配内存的唯一方式。
3.2.2 自由分配
3.2.2.1 基本内存分配
通过调用malloc分配一块内存。
void *malloc(size_t size)
分配内存的内容是未定义的,需要自行初始化(或使用calloc)。
malloc的返回值可以不经强制转换的赋给任意指针变量,
因为ISO C标准中定义,在需要的时候void *类型可以自动转换为另一个指针类型。
但是在赋值运算以外的上下文或老式C代码中需要进行显式转换。
在给字符串分配空间的时候,记得将长度加1。因为字符串的结束符并未记入其长度之内。
3.2.2.2 malloc示例
空间不足的时候,malloc返回空指针。应该检查每一个malloc的返回值。
malloc返回的内存块是经过对齐的,以便保存任意类型的数据。
在GNU系统,地址总是8的倍数,64位机上是16的倍数。
需要对齐到页边界的时候,使用memalign。
该内存块后面的内存可能已作它用,也许分配给了另一个malloc。
如果越界使用的话,就会破坏另一个malloc记录内存分配的元数据,或破坏其他内存块的数据。
如果已经分配的内存块不够用了,使用realloc扩大它。
3.2.2.3 释放malloc分配的内存
使用函数free释放malloc分配的内存块。
void free(void *ptr)
释放一块内存会改变其中的内容。别指望释放完了后还能再找到有用的数据。
在释放前将需要的数据复制出来。
有时候,free会真的将内存归还给操作系统让进程看起来变小了。
但通常只是让后续的malloc重用这块内存空间。
与此同时,这些空间仍在进程中以空闲链表的方式供malloc使用。
程序结束的时候没有必要显式的释放内存块,因为进程结束的时候,所有的空间都会归还给系统。
3.2.2.4 修改内存块大小
使用realloc改变内存块的大小。
void *realloc(void *ptr, size_t newsize)
ptr块后面的空间不够的时候,会重新找一块更大的空闲空间。
realloc的返回值是新块的地址。内存块需要移动的时候,realloc会复制原来的内容。
如果传的ptr为空指针,则与调用malloc(newsize)一样。但要注意ISO C标准之前的实现可能不支持。
与malloc一样,空间不足的时候realloc返回空指针。原来的块保持不变。
也可以使用realloc缩小块。
新大小与原大小一样的时候,realloc什么也不做,并返回传入的地址。
3.2.2.5 分配已清理的内存
calloc函数分配内存并将其清0。
void *calloc(size_t count, size_t eltsize)
3.2.2.6 malloc的效率考量
与其他C库不同,GNU C Library没有将内存块取整到2的幂次,不论内存块的大小。
free释放后临近的内存块可以进行合并。
这样可以减少内存碎片。
在本实现中,超过一页的大内存块通过mmap进行分配(匿名或通过/dev/zero)。
好处是这些块释放后会立即归还给系统。
这样可以避免两个小内存块之间因为一个大内存块的存在而无法合并带来的浪费。
使用mallopt调整mmap的阈值。也可以禁用mmap的分配方式。
3.2.2.7 分配对齐的内存块
GNU系统中,malloc和realloc返回的地址总是8的倍数(64位机是16的倍数)。
使用memalign,posix_memalign,或valloc获取一个比这两个数更大的2的幂次的倍数的地址。
在GNU C Library中,使用free释放这些块。BSD不适用。
void *memalign(size_t boundary, size_t size)
boundary必须的2的幂次。
int posix_memalign(void **memptr, size_t alignment, size_t size)
posix_memalign与memalign相似。
但参数alignment多了一个限制:必须的2的幂次乘以sizeof(void *)。
void *valloc(size_t size)
等于使用memalign按页大小对齐。
3.2.2.8 malloc的调节参数
使用mallopt函数调整一些动态内存分配的参数。
int mallopt(int param, int value)
M_TRIM_THRESHOLD
M_TOP_PAD
M_MMAP_THRESHOLD
M_MMAP_MAX
M_PERTURB
3.2.2.9 堆一致性检查
使用mcheck函数告诉malloc进行一致性检查。
int mcheck(void (*abortfn)(enum mcheck_status status))
在使用malloc分配内存前就要开始检查。
在程序链接的时候使用-lmcheck选项。
enum mcheck_status mprobe(void *pointer)
使用mprobe函数显式的检查一个已分配内存块的一致性。
enum mcheck_status
MCHECK_DISABLED
MCHECK_OK
MCHECK_HEAD
MCHECK_TAIL
MCHECK_FREE
使用环境变量MALLOC_CHECK_。
0:忽略。
1:打印诊断信息到stderr。
2:立即调用abort。
设置了SUID或SGID的程序需要创建/dec/suid-debug文件。
3.2.2.10 内存分配钩子
GNU C Library支持设置钩子函数来改变malloc,realloc和free的行为。
可以使用这些钩子函数进行动态内存分配的调试。
__malloc_hook
void *function(size_t size, const void *caller)
__realloc_hook
void *function(void *ptr, size_t size, const void *caller)
__free_hook
void function(void *ptr, const void *caller)
__memalign_hook
void *function(szie_t alignment, size_t size, const void *caller)
要保证在未将hook函数恢复为旧值之前,不在hook函数内递归调用其所hook的函数。
__malloc_initialize_hook
在malloc第一次初始化的时候会调用该函数指针。
void (*__malloc_initialize_hook)(void) = my_init_hook;
3.2.2.11 malloc内存分配的统计
使用函数mallinfo获取动态内存分配的相关信息。
3.2.2.12 malloc相关函数摘要
3.2.3 内存分配调试
使用没有垃圾回收机制的语言编程,查找内存泄露是一项很复杂的工作。
3.2.3.1 如果安装跟踪功能
void mtrace(void)
定义环境变量MALLOC_TRACE为一个可写的文件。
这是一个GNU扩展函数。
void muntrace(void)
3.2.3.2 示例片段
不只是进程本身会使用这些跟踪函数,C库也会使用。
最好是在程序一开始就调用mtrace,而从不调用muntrace。
3.2.3.3 一些好的想法
3.2.3.4 解释跟踪信息
使用perl脚本mtrace。
3.2.4 对象栈
obstack是一个以栈方式组织对象的内存池。
3.2.4.1 创建对象栈
头文件obstack.h。
3.2.4.2 使用obstacks的准备
定义obstack_chunk_alloc和obstack_chunk_free。
使用obstack_init初始化结构obstack实例。
3.2.4.3 在obstack上分配内存
void *obstack_alloc(struct obstack *obstack-ptr, int size)
3.2.4.4 释放obstack上的对象
3.2.4.5 obstack函数与宏
3.2.4.6 对象增长
3.2.4.7 快速增长对象
自行检查可用空间大小。减少大量增长小对象时的检查开销。
3.2.4.8 obstack的状态
3.2.4.9 obstacks上的数据对齐
3.2.4.10 obstack的块
块通常是4096字节。有8字节的开销。
3.2.4.11 obstack函数摘要
3.2.5 变长变量的自动存储
函数alloca是一种半动态的分配方式,动态分配自动释放。
显式的调用alloca分配内存块;运行时计算大小。
所有的块在调用alloca的函数退出时释放,就像函数的自动变量一样。
没有显式的释放函数。
该函数是BSD的扩展。
void *alloca(size_t size)
不要在函数的参数中调用alloca函数。
3.2.5.1 alloca示例
3.2.5.2 alloca优点
占地少运行快。
alloca无需不同大小的内存块池,任意大小重用。alloca不会带来内存碎片。
使用longjmp的时候也会自动释放。
3.2.5.3 alloca缺点
栈空间不足时,不会有明确的错误信息。很可能是直接段错。
一些非GNU系统不支持alloca,可移植性差些。
3.2.5.4 GNU C变长数组
3.3 改变数据段的大小
头文件unistd.h。
这些是系统调用的接口函数。
int brk(void *addr)
brk将调用进程的数据段尾部设置为addr。
段的结束地址定义为段最后一个字节地址加1。
函数命名基于数据存储与栈在同一个段的历史情况。
数据存储向上增长,栈向下增长,中间的部分叫做break。
void *sbrk(ptrdiff_t delta)
使用与当前段结束地址相对的偏移值做参数。
返回修改后数据段的结束地址。
使用sbrk(0)可以获取当前数据段的结束地址。
3.4 锁定页
锁定页是指让系统将虚拟页和物理页框关联后就不再换出,不再产生页错。
3.4.1 为什么要锁定页
页错会透明的换入页,进程通常不需要关心页的锁定。
两个情况时需要考虑:
速度。
隐私。
如果内存中有一些秘密信息而这些内存被换出,这会增加泄密的风险。
例如密码被换出到了磁盘上的交换分区,内存中的信息已经清理,但是磁盘上的信息可能会持续很久。
3.4.2 内存锁定的细节
内存锁定关乎虚拟页,而不是物理页框。
页面调度的规则是:如果页框关联了一个锁定的虚拟页,则不将其换出。
内存锁定不叠加。要么是锁定,要么是不锁定。
一个内存锁定一直持续到进程对齐进行显式的解锁。
(进程的终止和exec会结束虚拟内存的生存期,相当于自动解锁。)
子进程不继承内存锁。
只有超级用户才可以锁定页。
系统对进程锁定页的数量有限制。
在Linux中,锁定的页并非你所想的锁定那样。即使虚拟页锁定了,但对应的物理页框仍会被映射到多个虚拟页。
当进程修改页内容的时候,会重新分配物理页框。即写时复制页错。
不要仅仅锁定页,要立即进行一次写操作。
为了保证有一个已经分配好的栈帧,先进入另一个作用域,声明一个比你需要的栈空间大的C自动变量,
进行一次写操作,然后返回。
3.4.3 锁定和解锁页的函数
头文件sys/mman.h。
这些函数在POSIX.1b中定义,是否可用依赖使用的内核。
Linux内核可用。
GNU C Libray遵循POSIX.1b关于宏_POSIX_MEMLOCK_RANGE、PAGESIZE、_POSIX_MEMLOCK定义的要求。
int mlock(const void *addr, size_t len)
事实上锁定的是整个页,包含这些地址范围任一部分的页都被锁定。
int munlock(const void *addr, size_t len)
int mlockall(int flags)
int munlockall(void)