memory-cnblog

linux虚拟内存系统

进程的虚拟内存
用户区分段:代码段、数据段、堆、共享库、栈
内核区:存放进程信息,PID\程序计数器、打开文件列表、task和mm(描述虚拟内存)结构等

Linux加载进程时(exec系列系统调用)会为该地址空间每个段分配VMA,VMA数据结构(vm_area_struct)会描述该段的虚拟空间的开始地址,结束地址,同时会描述该VMA背后映射的文件名、映射空间所在文件的偏移量和大小(file指针指向它的backend硬盘文件)。但是Linux内核不给该空间分配物理内存,所以此时的页表项是空的。进程分配的虚拟内存只有在实际写入数据时,才会变为物理内存,否则不占用空间,因此虽然进程地址空间位4G,但内存可以同时容纳多个进程。在内存不够时,会发生内存交换,将不常用的进程的内存交换到快速磁盘上,从而腾出空间给其他进程使用。

在翻译虚拟内存时,如果发生缺页,则系统要判断:

  • 地址是否合法?处理程序会将地址和mm_struct的mmap成员指针指向的每个区域vm_area_struct的start和end对比,从而判断是否合法,不合法就会报错segment fault
  • 内存访问是否合法?读写或者执行权限判断

共享对象不要求虚拟地址相同,但物理地址相同,这是任何进程的读写对其他进程都是可见的,也会反映到磁盘上。
私有对象采用写时复制

从内存角度看系统调用

  • fork,创建子进程,包括mm_struct(描述虚拟内存当前状态)、区域结构和页表,分配pid,并把两个进程的页面都标记为只读,每个段标记为写时复制。
  • execve,删除用户区,映射私有区域(代码,数据,堆栈),映射共享区域(如标准c库),设置pc。

显式分配器

  • malloc,分配至少为参数大小的内存块(包含边界对齐),返回指向该块的指针。malloc可以使用mmap和sbrk(使用内核的brk指针扩展或者收缩堆)等函数来分配内存
  • free,释放malloc分配的内存块,参数是malloc返回的指针

内存分配器实现(来自csapp)

  • 隐式空闲链表:见csapp 9.9.12,即使用头部和脚部(边界标记)来记录块大小和是否空闲(脚部方便合并),中间用来存储有效载荷,但这样空间浪费,且分配时间是块总数的线性时间。有一种边界标记的优化方法,把上一个块的已分配位存放在当前块中多出来的低位(以书中为例,有3位用于标记,但实际只用了1位),这样上一个块的脚部就可以存放数据。但是空闲块仍然需要脚部,因为合并时要知道上一个块的大小。

  • 显式空闲链表:将空闲块组织为显式数据结构--链表,由于空闲块不需要存数据,所以指针可以放在空闲块主体里

  • 分离空闲链表:维护多个空闲链表,其中每个链表中的块有大致相等的大小。分配器维护着一个空闲链表数组,每个大小类一个空闲链表,按照大小的升序排列。当分配器需要一个块时,它就搜索相应的空闲链表。如果不能找到合适的块与之匹配,它就搜索下一个链表,以此类推。

    • 简单分离存储:每个大小类的空闲链表包含大小相等的块,为了分配一个给定大小的块,我们检査相应的空闲链表。如果链表非空,我们简单地分配其中第一块的全部。空闲块是不会分割以满足分配请求的。如果链表为空,分配器就向操作系统请求一个固定大小的额外内存片(通常是页大小的整数倍), 将这个片分成大小相等的块,并将这些块链接起来形成新的空闲链表。要释放一个块,分配器只要简单将这个块插人到相应的空闲链表的前部。缺点是会带来内部碎片,即分配的块比请求的块大,但是分配器不会将它们分割以满足其他请求。
    • 分离适配:每个链表包含潜在的大小不同的块,为了分配一个块,必须确定请求的大小类,并且对适当的空闲链表做首次适配,査找一个合适的块。如果找到了一个,那么就(可选地)分割它,并将剩余的部分插入到适当的空闲链表中。如果找不到合适的块,那么就搜索下一个更大的大小类的空闲链表。如此重复,直到找到一个合适的块。如果空闲链表中没有合适的块,那么就向操作系统请求额外的堆内存,从这个新的堆内存中分配出一个块,将剩余部分放置在适当的大小类中。要释放一个块,执行合并,并将结果放置到相应的空闲链表中。既减少了搜索时间,也提高了内存效率。书上说GNU malloc采用这种方法?
    • 伙伴系统:是分离适配的一种特例,其中每个大小类都是2的幂,请求块大小向上舍人到最接近的2的幂。初始时只有一个大块,分配时找到第一个大小足够的块,如果正好相等,就分配;否则递归二分这个块,直到大小相等,而其余半块(伙伴)加入到相应的空闲链表。释放块时,与空闲的伙伴合并,直到遇到已分配的伙伴。优点是合并和分配快,缺点是可能导致内部碎片,但对于特定的分配需求,比如分配空间大小为2的幂,是很好的选择。
  • glibc实现为代表的ptmalloc,隐藏头风格,

下面详细介绍malloc方法

ptmalloc

分配器处在用户程序和内核之间,它响应用户的分配请求,向操作系统申请内存,然后将其返回给用户程序,为了保持高效的分配,分配器一般都会预先分配一块大于用户请求的内存,并通过某种算法管理这块内存,来满足用户的内存分配要求,用户释放掉的内存也并不是立即就返回给操作系统,相反,分配器会管理这些被释放掉的空闲空间,以应对用户以后的内存分配要求。因此,分配器不但要管理已分配的内存块,还需要管理空闲的内存块,分配器会首先在空闲空间中寻找一块合适的内存给用户,在空闲空间中找不到的情况下才分配一块新的内存。

ptmalloc 中使用一个 chunk 来表示内存块,无论是分配的还是空闲的。
chunk 的结构如下:

struct malloc_chunk {
  INTERNAL_SIZE_T      prev_size;  /* Size of previous chunk (if free).  */
  INTERNAL_SIZE_T      size;       /* Size in bytes, including overhead. */
  struct malloc_chunk* fd;         /* double links -- used only if free. */
  struct malloc_chunk* bk;
  /* Only used for large blocks: pointer to next larger size.  */
  struct malloc_chunk* fd_nextsize; /* double links -- used only if free. */
  struct malloc_chunk* bk_nextsize;
};

size除了包括大小外,还包括了chuck是否使用等位信息。

  • P: PREV_INUSE. P=0则前一个chunk空闲,P=1则前一个chunk被分配。
  • M: IS_MMAPPED. M=1为mmap映射区域分配,M=0为heap区域分配
  • A: NON_MAIN_ARENA. A=0为主分配区分配,A=1为非主分配区分配。

links只有在空闲时有用,被分配时也就不需要了,可以用于存放数据。
fd_nextsize 指向下一个比当前 chunk 大小大的第一个空闲 chunk , bk_nextszie 指向前一个比当前 chunk 大小小的第一个空闲 chunk。被分配时存放数据。加快在large bin中查找最近匹配的空闲chunk

  • 使用中的chunk

  • 空闲chunk

将相似大小的chunk用双向链表链接起来,这样一个链表被称为一个bin,基于chunk的大小,有下列几种可用bins:

  • fast
  • unsorted
  • small
  • large

使用bins数组保存large bin之外的所有bins,共有128个。使用fastbinsY数组保存fast bins,共有10个。

fast bins

对应的bins记录在数组fastbinsY中,共有10个大小不同的bins,每个chunk的大小为 16-64B。最后释放的 chunk 被链接到链表的头部,而申请 chunk 是从链表头部开始查找。fast bins 中的 chunk 不会被合并,当用户释放一块不大于max_fast(默认值64B)的chunk的时候,会默认会被放到fast bins上。,当需要给用户分配的 chunk 小于或等于 max_fast 时,malloc 首先会到fast bins上寻找是否有合适的chunk。

unsorted bin

是第1个bin,大小无严格限制。当用户释放的内存大于max_fast或者fast bins合并后的chunk都会首先进入unsorted bin上。在进行 malloc 操作的时候,如果在 fast bins 中没有找到合适的 chunk,则 ptmalloc 会先在 unsorted bin 中查找合适的空闲 chunk,如果没有合适的bin,ptmalloc会将unsorted bin上的chunk放入bins上,然后才查找 bins。
具体遍历过程如下:

如果只有一个chunk并且大于待分配的chunk,则进行切割,并且剩余的chunk继续放入 unsorted bins
如果有大小和待分配chunk相等的,则返回,并从unsorted bins删除;
如果某一chunk大小 属于small bins的范围,则放入small bins的头部;
如果某一chunk大小 属于large bins的范围,则找到合适的位置放入。

small bins

small bin是第2-64个bin,大小为16-512B同一个small bin中的chunk具有相同的大小。两个相邻的small bin中的chunk大小相差8bytes。small bins 中的 chunk 按照最近使用顺序进行排列(FIFO),最后释放的 chunk 被链接到链表的头部,而申请 chunk 是从链表尾部开始查找。可以通过 chunk 的大小计算出它所在的 small bin 的索引,即最优适配,这样就可以快速定位到 small bin。
当空闲的 chunk 被链接到 bin 中的时候,ptmalloc 会把表示该 chunk 是否处于使用中的标志 P 设为 0(这个标志实际上处在下一个 chunk 中),同时 ptmalloc 还会检查它前后的 chunk 是否也是空闲的,如果是的话,ptmalloc 会首先把它们合并为一个大的 chunk,然后将合并后的 chunk 放到 unstored bin 中。

large bins

大小大于等于512字节的chunk被称为large chunk,保存在bins的第65-128个bin中,large bins中的每一个bin分别包含了一个给定范围内的chunk(大小类),其中的chunk按大小递减排序,大小相同则按照最近使用时间排列。使用最优适配,多余部分分割后加入到unsorted bin中。合并方法和small bins相同。

特殊情况

  • top chunk。top chunk相当于分配区的顶部空闲内存,当bins上都不能满足内存分配要求的时候,就会来top chunk上分配。
    • 当top chunk大小比用户所请求大小还大的时候,top chunk会分为两个部分:User chunk(用户请求大小)和Remainder chunk(剩余大小)。其中Remainder chunk成为新的top chunk。
    • 当top chunk大小小于用户所请求的大小时,top chunk就通过sbrk(main arena)或mmap(thread arena)系统调用来扩容。
  • mmaped chunk。当分配的内存非常大(大于分配阀值,默认128K)的时候,需要被mmap映射,则会放到mmaped chunk上,当释放mmaped chunk上的内存的时候会直接交还给操作系统。
  • last remainder chunk。当需要分配一个small chunk,但在small bins中找不到合适的chunk,如果last remainder chunk的大小大于所需要的small chunk大小,last remainder chunk被分裂成两个chunk,其中一个chunk返回给用户,另一个chunk变成新的last remainder chunk。

ptmalloc参考链接

posted @ 2024-04-18 23:35  trashwin  阅读(5)  评论(0编辑  收藏  举报