作者: LemonNan
原文: https://juejin.im/post/5ee3c34a518825430c3ad31d
前言
本篇是对Linux内存分配的一个学习笔记.
程序内存结构
下面是在 Linux/x86-32 中典型的一个进程内存结构
文本段包含了进程运行的程序机器语言指令. 文本段具有只读属性, 以防止进程通过勘误指针意外修改自身指令. 因为多个进程可以同时运行同一程序, 所以又将
文本段设为可共享
, 这样一份程序代码的拷贝可以映射到所有这些进程到虚拟地址空间中.初始化数据段包含显示初始化的全局变量和静态变量. 程序加载到内存时, 从可执行文件中读取这些变量的值.
未初始化数据段包含了未进行显示初始化的全局变量和静态变量
. (书上写了一堆, 实际上就是懒加载
)栈(stack)是一个动态增长和收缩的段, 由栈帧(stack frames)组成
. 系统会为每个当前调用的函数分配一个栈帧.栈帧中存储局部变量(所谓自动变量), 实参和返回值.
堆(heap)时刻在运行时(为变量)动态进行内存分配的一块区域, 堆顶端称做
program break
.
3g(32位)以上的虚拟内存地址, 程序无法访问.
程序的起始地址为 0x08048000 (32位)、0x00400000 (64位)
malloc 和 free
malloc
void *malloc(size_t size);
栈向下增长超出之前曾达到的位置
挡在堆中分配或者释放内存时, 通过调用 brk()、sbrk() 或 malloc 函数族来提升 program break 的位置.
调用 shmat() 连接 System V 共享内存区或者当调用 shmdt() 脱离共享内存区时.
当
调用 mmap() 创建内存映射或者 munmap() 解除内存映射
特点
malloc() 返回的内存快所采用的字节对齐的方式, 在大多数的硬件架构上, 意味着 malloc 是基于 8字节 或者 16字节 边界来分配内存的.
malloc 之后的内存, 在不使用的需要需要手动 free, malloc 和 free 一一对应, 否则可能会导致
未知错误(多次free) 或者 内存泄漏(没有调用free)
.允许分配小块内存
允许随意释放内存快, 它们被维护于一张空闲内存列表中, 在后续内存分配调用时循环使用
free
free() 函数释放 ptr 参数所指向的内存快
void free(void *ptr);
特点
free 并不降低 program break 的位置, 而是将这块内存添加到空闲内存列表, 供后续的 malloc() 函数循环使用.这么做有几个原因:
被释放的内存快通常位于堆的中间, 而非堆堆顶部, 因而降低 program break 不能达到效果
最大限度减少程序必须执行 sbrk() 调用次数(减少系统调用的开销)
free 传入空指针不会做任何处理(从设计上来说这不是错误代码)
调用 free 后堆参数 ptr 的使用, 比如再次调用 free, 会产生错误并且可能导致不可预知的结果.
为什么是8/16字节对齐
CPU 读取8字节对齐, 比如 double/long, 不对齐的话需要读写2次
CPU高速缓存行大小通常是 32 或者 64 字节. 如果对象是8字节对齐的数据, 则只需要占用一个缓存行, 如果不是8字节对齐的话, 则可能一部分数据在一个缓存行, 另一部分数据在其它的缓存行, 所以读写这个数据需要用到2个缓存行的数据而不是一个, 所有(目前1、2、3)级别的缓存都会受到此影响.
对于在磁盘中的数据, 都是以512字节为最低的单位(一个扇区的数据大小), 如果是8字节对齐的话, 则数据会被存放在一个扇区里, 可以只通过一次读取将数据都读取出来, 如果数据不是8字节对齐, 则
数据可能会被存放到不同的扇区中, 并且还有可能不是相邻的扇区
, 这就会导致随机I/O
, 降低数据处理的效率, 消耗更多的硬件资源. 对于上层来说, 数据是相连的(逻辑), 但是对于底层的物理硬件来说, 数据很有可能位于不相邻的扇区(数据处理最小单元).
so, 总结下来就是, 非对齐的数据访问 会因为增加硬件访问次数 比对齐的数据访问效率低.
说起缓存行, Java中有一些框架(比如Disruptor)考虑到了不同的CPU架构, 使用了CPU支持的缓存行填充, 以防止 伪共享(这里暂不做过多描述) 的发生从而降低效率.
通过 sysctl -a
查看
# 我的电脑中的数据hw.cachelinesize: 64hw.l1icachesize: 32768hw.l1dcachesize: 32768hw.l2cachesize: 262144hw.l3cachesize: 3145728
虚拟内存管理
内核为每一个进程都维护一张页表(page table)
, 页表中的每个条目要么指出一个虚拟页面在 RAM 中的所在位置, 要么表明其当前驻留在磁盘上, 若进程访问的地址并无页表条目与之对应, 进程将会收到一个 SIGSEGV 信号.
Q: 虚拟页面的数据为什么会在磁盘上?
A: 每个程序中只有一部分 page 会驻留在 物理内存(RAM) 中, 未使用的 page 会被拷贝保存到交换区(swap area)内, 这是磁盘空间中的保留区域, 作为 RAM 的补充, 只有在需要的时候才会载入 物理内存.
进程在读取的时候, 如果访问的页面没有驻留在物理内存中, 将会发生页面错误(page fault), 内核即刻挂起的执行, 同时从磁盘中将该页面载入内存.
在 x86-32 中, page size 为 4096 字节(4KB), 一些其它的Linux使用的页面比 4096 字节更大.
Alpha 使用的 page size = 8192 字节(8KB), IA-64 的page size是可以改变的, 默认为 16384 字节.程序通过调用 sysconf(_SC_PAGESIZE) 获取系统虚拟内存的 page size.
虚拟内存的实现需要硬件中分页内存管理单元(PMMU)的支持, PMMU 把要访问的每个虚拟内存地址转换成相应的物理内存地址, 当特定虚拟内存地址所对应的页没有驻留于 RAM 中时, 将以页面错误(page fault)通知内核.
有效虚拟内存范围
由于 内核能为进程分配和释放页(和页表条目)
, 所以进程的有效虚拟地址范围在其生命周期中可以发生变化. 如下场景会导致范围变化:
栈向下增长超出之前曾达到的位置
挡在堆中分配或者释放内存时, 通过调用 brk()、sbrk() 或 malloc 函数族来提升 program break 的位置.
调用 shmat() 连接 System V 共享内存区或者当调用 shmdt() 脱离共享内存区时.
当
调用 mmap() 创建内存映射或者 munmap() 解除内存映射
局部性原理
在计算机中大多数程序都有一个共同特点, 访问局部性
.
访问局部性包含两方面:
空间局部性: 程序倾向于访问在最近访问过的内存地址附近的内存(由于指令是顺序执行的, 并且有时会按顺序处理数据结构)
时间局部性: 这意味着数据被访问到, 在之后较短的时间内会被再次访问到(可能是由于循环)
优点
虚拟内存使得进程的虚拟地址空间和RAM的物理地址空间隔离开, 有以下一些好处
进程与进程、进程与内核相互隔离, 所以一个进程不能读取其它进程或内核的内存, 因为每个进程的页表条目指向截然不同的物理内存地址.
适当情况下, 多个进程鞥狗共享内存. 因为不同的进程页表条目可以指向相同的物理内存(RAM)地址.通常发生在如下的场景:
执行同一程序的多个进程, 共享一份程序代码副本. 当多个进程执行相同的程序文件(或加载相同的共享库), 会隐式实现这一类型的共享.
进程通过 shmget() 和 mmap() 系统调用显示请求与其它进程共享内存, 这样的目的是为了进程间的通信.
实现保护机制: 相同的内存, 不同的进程可以设置不同的访问权限, 某些进程只读、某些拥有所有权限等.
因为需要驻留在内存中的仅是程序的一部分, 所以程序的加载和运行都变快了, 而且一个程序所占用的大小(虚拟内存) 能够超出 RAM 容量.(因为有的事通过虚拟内存管理存放到了磁盘上)
一个进程所使用的RAM减少了, RAM中同时可容纳的进程数量增多. 这样的话加大了在任一时刻CPU可执行至少一个进程的概率, 这样往往也会提高CPU的利用率.