原文地址:https://blog.csdn.net/HUAERBUSHI521/article/details/118599134
直接使用物理内存面临的问题:
- 内存缺乏访问控制,安全性不足
- 各进程同时访问物理内存,可能会互相产生影响,没有独立性
- 物理内存极小,而并发进程所需又大,容易导致内存不足
- 进程所需空间不一,容易导致内存碎片化问题
基于以上几种原因,Linux通过mm_struct结构体描述了一个虚拟的,连续的,独立的地址空间.也就是所说的虚拟地址空间.
程序运行时,在建立了虚拟地址空间后,并没有分配实际的物理内存,而是当进程需要实际访问内存资源的时候就会由内核的请求分页机制产生缺页中断,这时才会建立虚拟地址和物理地址的映射,调入物理内存页.通过这种方式,就能够保证我们的物理内存只有在实际使用时才进行分配,避免了内存浪费的问题.
在linux中,虚拟地址空间的内部又被划分为用户空间与内核空间.
内核空间即进程陷入内核态后才能访问的空间.虽然每个进程都具有自己独立的虚拟地址空间,但是这些虚拟地址空间中的内核空间(前896M),其实关联的都是同一块物理内存.
通过这种方法,保证了进程在切换至内核态后能够快速的访问内核空间.
内核空间虚拟地址X 对于的物理内存地址:X-0xc0000000
内核空间虚拟地址分布:
内核空间主要分为直接映射区和高端内存映射区.
在Linux系统中,通过分段分页的机制,将物理内存划分为4k大小的内存页,并且将页作为物理内存分配与回收的基本单位.
内核会为每一个物理页帧创建一个struct page结构体,其中包含的重要信息有:
Linux内核通过一个管理区页框分配器管理着物理内存上所有的页框,在管理分配器里的核心系统就是伙伴系统和每CPU高速缓存. 在linux系统中,管理区页框分配器管理着所有物理内存,无论是内核还是用户进程,需要将一些内存占为己有时,都需要请求管理区页框分配器,这时才会给你分配物理内存页框.
一个具体的区域结构vm_area_struct包含的重要字段:
在Linux中,通过分段和分页的机制,将物理内存划分为4K大小的内存页,并且将页作为物理内存分配与回收的基本单位.通过分页机制可以灵活的对内存进行管理.
但是这种直接的内存分配存在着大量的问题,非常容易导致内存碎片的问题.下面就分别介绍两种内存碎片:内部碎片和外部碎片.
当我们需要分配大块内存的时,操作系统会将连续的页框组合起来形成大块内存,再将其分配给用户.但是频繁的申请和释放内存页,就会带来内存外碎片的问题.
当需要分配大块内存的时候,要用好几页组合起来才够,而系统分配物理内存页的时候会尽量分配连续的内存页面,频繁的分配与回收物理页导致大量的小块内存夹杂在已分配页面中间,形成内存外碎片.
由于页是物理内存分配的基本单位,因此即使我们需要的内存很小,Linux也会至少给我们分配4K的内存页.
倘若我们需求的只有几个字节,那么该内存中有大量的空间未被使用,造成了内存浪费的问题.而我们频繁进行小块内存的申请,这种浪费现象就会愈发严重,这也就是内存内碎片的问题.
要想解决内存外碎片的问题,无非就两种方法
- 内存外碎片问题的本质就是空间不连续,所以可以将非连续的空闲页框映射到连续的虚拟地址空间
- 记录现存的空闲连续页框块的情况,尽量避免为了满足小块内存的请求而分割大的空闲块。
Linux选择了第二种方法来解决这个问题,即引入伙伴系统算法,来解决内存外碎片的问题。
伙伴系统就是把相同大小的连续空闲页框块用链表串起来,这样页框之间看起来就像是手拉手的伙伴,这也是其名字的由来.
伙伴系统将所有的空闲页框分组为11块链表,每个块链表分别包含大小为1,2,4,8,16,32,64,128,256,512和1024个连续页框的页框块,即2的0~10次方,最大可以申请1024个连续页框,对应4MB(1024*4K)大小的连续内存.每个页框块的第一个页框的物理地址应该是该块大小的整数倍.
因为任何正整数都可以由 2^n 的和组成,所以我们总能通过拆分与合并,来找到合适大小的内存块分配出去,减少了外部碎片产生 。
伙伴系统很好的解决了内存外碎片的问题,但是它还是以页作为内存分配和释放的基本单位.而我们在实际的应用中则是以字节为单位.例如我们申请2个字节的空间,但是其还是会向我们分配一页,也就是4096字节的内存,因此还是会存在内存碎片的问题.
为了解决这个问题,slab分配器就应运而生了。其以字节为基本单位,专门用于对小块内存进行分配。slab分配器并未脱离伙伴系统,而是对伙伴系统的补充,它将伙伴系统分配的大内存进一步细化为小内存分配。
对于内核对象,生命周期通常是这样的:分配内存->初始化->释放内存。而内核中如文件描述符、pcb等小对象又非常多,如果按照伙伴系统按页分配和释放内存,不仅存在大量的空间浪费,还会因为频繁对小对象进行分配-初始化-释放这些操作而导致性能的消耗。
所以为了解决这个问题,对于内核中这些需要重复使用的小型数据对象,slab通过一个缓存池来缓存这些常用的已初始化的对象。
当我们需要申请这些小对象时,就会直接从缓存池中的slab列表中分配一个出去。而当我们需要释放时,我们不会将其返回给伙伴系统进行释放,而是将其重新保存在缓存池的slab列表中。通过这种方法,不仅避免了内存内碎片的问题,还大大的提高了内存分配的性能。
下面就由大到小,来画出底层的数据结构
kmem_cache是一个cache_chain的链表,描述了一个高速缓存,这个缓存可以看做是同类型对象的一种储备,每个高速缓存包含了一个slab的列表,这通常是一段连续的内存块,并包含3种类型的slabs链表:
slab是slab分配器的最小单位,在具体实现上一个slab由一个或者多个连续的物理页组成(通常只有一页)。单个slab可以在slab链表中进行移动,例如一个未满的slab节点,其原本在slabs_partial链表中,如果它由于分配对象而变满,就需要从原先的slabs_partial中删除,插入到完全分配的链表slabs_full中
从上面可以看出,slab分配器的本质其实就是通过将内存按使用对象不同再划分成不同大小的空间,即对内核对象的缓存操作.
slab分配器的优点: