操作系统(内存篇) |青训营笔记
malloc如何分配空间
- malloc函数主要是通过brk、mmap这两个系统调用实现的;
- 当分配小于128k的内存时,使用brk分配内存,将堆顶指针向高地址移动,获得新的内存空间,通过 brk() 方式申请的内存,free 释放内存的时候,并不会把内存归还给操作系统,而是缓存在 malloc 的内存池中,待下次使用
- 分配大于128k的内存时,使用mmap分配内存,利用私有匿名映射的方式,在文件映射区分配一块内存,也就是从文件映射区“偷”了一块内存;通过 mmap() 方式申请的内存,free 释放内存的时候,会把内存归还给操作系统,内存得到真正的释放
- 这两种方式分配的都是虚拟内存,没有分配物理内存。在第一次访问已分配的虚拟地址空间的时候,发生缺页中断,操作系统负责分配物理内存,然后建立虚拟内存和物理内存之间的映射关系
- malloc是从堆里面申请内存,也就是说函数返回的指针是指向堆里面的一块内存。操作系统中有一个记录空闲内存地址的链表。当操作系统收到程序的申请时,就会遍历该链表,然后就寻找第一个空间大于所申请空间的堆结点,然后就将该结点从空闲结点链表中删除,并将该结点的空间分配给程序
为什么不全部使用 mmap 来分配内存?
- 向操作系统申请内存,是要通过系统调用的,执行系统调用是要进入内核态的,然后在回到用户态,运行态的切换会耗费不少时间,如果都用 mmap 来分配内存,等于每次都要执行系统调用;mmap 分配的内存每次释放的时候,都会归还给操作系统,于是每次 mmap 分配的虚拟地址都是缺页状态的,然后在第一次访问该虚拟地址的时候,就会触发缺页中断
- 频繁通过 mmap 分配的内存话,不仅每次都会发生运行态的切换,还会发生缺页中断(在第一次访问虚拟地址后),这样会导致 CPU 消耗较大
- malloc 通过 brk() 系统调用在堆空间申请内存的时候,由于堆空间是连续的,所以直接预分配更大的内存来作为内存池,当内存释放的时候,就缓存在内存池中。等下次在申请内存的时候,就直接从内存池取出对应的内存块就行了,而且可能这个内存块的虚拟地址与物理地址的映射关系还存在,这样不仅减少了系统调用的次数,也减少了缺页中断的次数,这将大大降低 CPU 的消耗
为什么不全部使用 brk 来分配?(brk频繁调用有什么问题)
- 通过 brk 从堆空间分配的内存,并不会归还给操作系统,对于小块内存,堆内将产生越来越多不可用的碎片,导致“内存泄露”
- malloc 实现中,充分考虑了 brk 和 mmap 行为上的差异及优缺点,默认分配大块内存 (128KB) 才使用 mmap 分配内存空间
free() 函数只传入一个内存地址,为什么能知道要释放多大的内存
- malloc 返回给用户态的内存起始地址比进程的堆空间起始地址多了 16 字节,保存了该内存块的描述信息,比如有该内存块的大小
- 当执行 free() 函数时,free 会对传入进来的内存地址向左偏移 16 字节,然后从这个 16 字节的分析出当前的内存块的大小,自然就知道要释放多大的内存了
知道JeMalloc,tcMalloc,ptMalloc吗
- ptmalloc 是基于 glibc 实现的内存分配器,它是一个标准实现,所以兼容性较好。pt 表示 per thread 的意思。当然 ptmalloc 确实在多线程的性能优化上下了很多功夫。由于过于考虑性能问题,多线程之间内存无法实现共享,只能每个线程都独立使用各自的内存,所以在内存开销上是有很大浪费的。
- tcmalloc 全称是 thread-caching malloc,所以 tcmalloc 最大的特点是带有线程缓存,tcmalloc 为每个线程分配了一个局部缓存,对于小对象的分配,可以直接由线程局部缓存来完成,对于大对象的分配场景,tcmalloc 尝试采用自旋锁来减少多线程的锁竞争问题
- jemalloc 同样都包含 thread cache 的特性。但是 jemalloc 在设计上比 ptmalloc 和 tcmalloc 都要复杂,jemalloc 将内存分配粒度划分为 Small、Large二个分类,并记录了很多 meta 数据,所以在空间占用上要略多于 tcmalloc,不过在大内存分配的场景,jemalloc 的内存碎片要少于 tcmalloc
系统调用的开销
- 当应用程序使用系统调用时,会产生一个中断。发生中断后, CPU 会中断当前在执行的用户程序,转而跳转到中断处理程序,也就是开始执行内核程序。内核处理完后,主动触发中断,把 CPU 执行权限交回给用户程序,回到用户态继续工作,运行态的切换会耗费不少时间
- 如果是在第一次访问虚拟地址还会发生缺页中断,这样会导致 CPU 消耗较大
如何优化brk减少频繁系统调用
- 内存管理的核心目标主要有两点:高效的内存分配和回收,提升单线程或者多线程场景下的性能;减少内存碎片,包括内部碎片和外部碎片,提高内存的有效利用率
- 因此可以从这两方面对brk进行优化,针对内存分配和回收问题,可以采用类似tcmalloc的机制,为每个线程分配一个局部缓存,对于小对象的分配,可以直接由线程局部缓存来完成;对于内存碎片问题,采用类似jemalloc的思想,将内存分配粒度划分为 Small、Large二个分类进行减少碎片
虚拟内存介绍下?
- 为了在多进程环境下,使得进程之间的内存地址不受影响,相互隔离,于是操作系统就为每个进程独立分配一套虚拟地址空间,每个程序只关心自己的虚拟地址就可以,实际上大家的虚拟地址都是一样的,但分布到物理地址内存是不一样的。作为程序,也不用关心物理地址的事情。
- 每个进程都有自己的虚拟空间,而物理内存只有一个,所以当启用了大量的进程,物理内存必然会很紧张,于是操作系统会通过内存交换技术,把不常使用的内存暂时存放到硬盘(换出),在需要的时候再装载回物理内存(换入)