基本知识
对于linux进程来说,内存分为两部分,用户态,内核态,从用户态到内存态一般需要中断或系统调用来实现
用户态内存划分(0-3G)
地址:高->低
其中:
栈是用户存放程序临时创建的局部变量,地址是从高到低
堆是用于存放进程运行中被动态分配的内存段,它的大小并不固定,可动态扩张或缩减。比如new malloc。地址从低到高
读写段:
bss:未初始化的全局变量
data:已初始化的全局变量和局部静态变量以及它属于静态区域,但是可写
只读段:
代码段, const变量,define宏
文件映射区域 :共享内存等映射物理空间的区域
内核态内存划分(3-4G)
MMU:
CPU的一个单独模块,用来将虚拟地址转换为实际物理地址的组件,MMU通常使用内存中的一个组件,通常称为页表(page table)或PTE(page table entry)来管理虚拟地址到实际物理地址的映射
PTE(页表)
页表存储着映射信息,页表实现了页表置换算法->LRU, 当映射缺失时,则会向CPU发送一个缺页中断, CPU则会在物理内存(RAM)中寻找一个空闲的页, 并更新PTE。如果没有可用的空闲页,则CPU会选择PTE页表中的页,置换到磁盘上并在PTE页面设置标志,这就是通常所说的页面调度(paging),将内存页换出到磁盘或者swap空间
TLB(快表)
是保存虚拟地址到物理地址的一个缓存,无需地址转换直接得到地址
Slab Buddy
Slab 和 Buddy 是linux内核中基础的内存分配器
slab是具有相同大小的内存集合,通过一次性申请较大的内存空间,并将其等分为大小相同的块,来避免内存碎片
buddy按需分配不同大小的内存,分配和归还涉及内存块的合并和分割,保存着一个内存管理链,内存按2的幂次划分,并搜索其内存空间找到最佳适配的内存
slab buddy与new的关系
malloc实现方式
若分配内存小于 128k ,调用 sbrk() ,将堆顶指针向高地址移动,获得新的虚存空间。
若分配内存大于 128k ,调用 mmap() ,在文件映射区域中分配匿名虚存空间。
free实现方式
如果是mmap的会调用munmap返回给OS
如果是堆内的内存,只有释放堆顶的空间,同时堆顶总连续空闲空间大于 128k 才使用 sbrk(-SIZE) 回收内存,真正归还 OS
因此,当我们写程序时,不能完全依赖 glibc 的 malloc 和 free 的实现。更好方式是建立属于进程的内存池,即一次分配 (malloc) 大块内存,小内存从内存池中获得,当进程结束或该块内存不可用时,一次释放 (free) ,可大大减少碎片的产生。
为什么malloc不直接使用mmap实现
mmap/sbrk/munmap属于系统调用,例如使用 mmap 分配 1M 空间,第一次调用产生了大量缺页中断 (1M/4K 次 ) ,大量的缺页中断会导致内核态cpu消耗过大。使用堆可以重复使用空闲内存空间
除了glibc的malloc还有什么内存管理实现
google 的 tcmalloc 和 facebook 的 jemalloc
用户态申请内存流程
调用brk时,只是增加了虚拟内存的地址,并没有真正去分配内存。而是在页表中将虚拟地址映射到同一个零化页面,只有在写这个页的时候才会发生缺页中断,将zero页面拷贝到用户空间。
虚拟地址到物理地址转换
分段机制:将虚拟地址(逻辑地址)转化成线性地址
分页机制:将线性地址转化成物理地址
虚拟地址首先被传送到MMU中查看TLB缓存,如果缓存命中(TLB hit)则返回物理地址,否则(TLB miss) MMU或操作系统会在页表(page table)中查找是否存在(a page walk),如果命中则返回物理地址,并写回TLB缓存。这个时候可能会发生缺页中断,比如上一个问题说的只读的零化页面,或者被置换走。
Linux中逻辑地址等于线性地址,因为Linux所有的段的线性地址都是从 0x00000000 开始,线性地址=逻辑地址+ 0x00000000
页面置换算法
LRU最久未使用 FIFO先进先出 LFU 最少使用 NRU最近未使用