艾巴生活网

您现在的位置是:主页>科技 >内容

科技

Linux的内存管理是什么Linux的内存管理详解

2024-12-04 20:35:02科技帅气的蚂蚁
Linux的内存管理Linux的内存管理是一个非常复杂的过程,主要分为两部分:内核内存管理和进程虚拟内存。内核的内存管理是Linux内存管理的核

Linux的内存管理是什么Linux的内存管理详解

Linux的内存管理

Linux的内存管理是一个非常复杂的过程,主要分为两部分:内核内存管理和进程虚拟内存。内核的内存管理是Linux内存管理的核心,所以我们先介绍一下内核的内存管理。

一、物理内存型号

有两种物理内存模型:统一内存访问(UMA)和NUMA(非统一内存访问)。

UMA模型是指物理内存是连续的,SMP系统中每个处理器对每个内存区域的访问速度是一样的;NUMA模型意味着SMP中的每个CPU都有自己的物理内存区域。虽然CPU可以访问其他CPU的内存区域,但是比定位自己的内存区域要慢很多。一般我们用的物理模型都是UMA模型。对于NUMA模型,Linux提供了三种可能的内存布局配置:平面内存、稀疏内存和不连续内存。

平面内存是物理内存的简单线性组织,一般是没有内存空洞的UMA架构采用。对于NUMA模式,只能采用后两种模式。后两种模式的区别在于,稀疏内存配置一般被认为是实验性的,不那么稳定,但有一些新的功能和性能优化,而不连续内存配置一般被认为是稳定的,但不具备内存热插拔等新功能。

二、物理内存组织

物理内存的组织主要分为两部分:节点和内存和内存区。Node主要是为NUMA设计的。在NUMA的SMP系统中,每个处理器都有自己的节点,而在UMA模型中,只有一个节点。对于每个节点中的内存,Linux分为几个内存域,在mmzone.h的zone_type中定义,常用的有ZONE_DMA、ZONE_DMA32、ZONE_NORMAL、ZONE_HIGHMEM内存和ZONE _ MOVABLE。ZONE_NORMAL是最常用的,表示内核可以直接映射的一般内存区域;ZONE_DMA表示DMA存储区;ZONE_DMA32表示64位系统中用于32位DMA设备的内存;ZONE_HIGHMEM表示32位系统中高地址内存的区域;ZONE _ MOVABLE与伙伴系统的内存碎片消除有关。稍后将详细描述相关部分。物理内存管理过程中有一些名词:页框(或称页框):它是系统内存管理的最小单位,系统中的每个页框都是struct page的一个实例。IA-32系统的页帧大小为4KB。Hot-n-Code Pages:指内存管理中页帧的分类。访问次数较多或最近访问的页面为热页面,否则为冷页面。该标志主要与内存交换有关。页表:是内存寻址过程中的辅助数据结构。分层页表对快速高效地管理地球具有重要意义的空间。一般来说,Linux支持四级页表:PGD(页全局目录)、PUD(页上级目录)、PMD(页中间目录)和PTE(页表项)。在IA-32系统中,默认只使用两级寻呼系统,即只有PGD和PTE。三、X86架构下的X86内存布局

内核在内存中的布局

Linux内核在初始化时会被加载到内存区的固定位置(这里不讨论可重定位的内核),而内核占用的内存区布局是固定的,如图:

内存的第一页帧不实用,主要被BIOS用来初始化;之后连续的640KB内存不被内核使用,主要用于映射各种ROM(一般是BIOS和图形ROM);剩余的空间是空闲的,因为内核必须放在连续的内存空间中。从0x100000开始,就是内核部分,包括代码段、数据段和附加段。

IA-32架构的布局

IA-32架构可以访问4GB地址空间(不考虑PAE)。正常情况下,4GB的线性空间会被分成3:1的两部分:低位地址的3/4是用户空间,而高位地址的1GB是内核空间,即内核地址空间从偏移量x-0xC0000000开始,每个虚拟地址X对应物理地址x-0xC000000。这种设计加速了内核空间寻址(简单的减法运算)。在进程切换过程中,只会切换用户空间中低3GB内存对应的页表,高地址空间共享内核页表。

IA-32架构的这种设计有一个问题:由于内核只能处理1GB的空间(其实内核能处理的空间不到1GB,后面会详细解释),如果物理内存大于1GB,那么剩下的内存会怎么处理?在这种情况下,内核将无法直接映射所有的物理内存,从而使用上面提到的高地址内存域(ZONE_HIGHMEM)。具体的内存分配如下:

可以看到,内核区的映射是从__PAGE_OFFSET(0xC00000)开始的,也就是3GiB位置开始映射到4gb,第一段用于直接映射,后面还有128MB的VMALLOC空间(这个空间的使用后面会介绍),然后还有永久映射和固定映射的空间(从PKMAP_BASE开始)。所以实际上物理内存可以直接映射的空间是1GB-VMALLOC-固定映射-永久映射,所以只有850MB多一点。也就是说,物理内存中只有前850mb可以直接映射到内核空间,超出的部分会作为高地址空间(HIGHMEM内存)。高地址空间可用于VMALLOC、永久映射和固定映射。

这里可能会有一个问题:如果内核只能处理896MB的空间,那么如果内存很大(比如3GB),不是剩余空间的利用率和效率很低?我们需要注意这个问题:这里说的是什么1、。这里的内存都是内核在内核区1GB空间内访问的。用户对物理内存的访问不是通过直接映射,还有另一种机制;2、这里的内存只是直接映射得到的内存,内核还可以通过其他方式访问地址更高的内存。

还有一个普遍的问题:内核直接映射占用800多MB的空间,那么如果我们有3GB的物理内存,那么实际可用的内存是不是只有2GB多一点?这个说法是错误的。上图只是描述了内核的分布线性地址空间。它里面的任何区域都赢如果没有真正的物理内存映射到物理内存,它就不会真正占用物理内存。在物理内存分配过程中(用户申请内存,VMALLOC部分等。),更倾向于先分配高地址内存,只有当高地址内存耗尽时,才会使用低850MB内存。

AMD64架构布局

AMD64架构采用了与IA-32完全不同的布局模式。因为64位的地址空间是64位长的,而64位长的地址在实际实现过程中会造成很大的开销,所以Linux目前只适用于48位长的地址空间,但为了向后兼容,还是适用于64位的地址空间表示。在布局方面,如果只采用类似48位IA-32的布局,很难保证向后兼容。所以Linux在AMD64架构下的内存布局采用了一种特殊的方式,如图所示:

Linux内存分为两部分:高位地址部分和低位地址部分,即下半部分空间0 ~0x0000 7FFF FFFFFFFFFFFF和上半部分空间0x ffff 8000 0000 0000 ~0x FFFFFFFFFFFF。可以看出,虚拟地址的低47位,即[0,46]是有效位,[47,63]的值总是相同的:要么全是0,要么全是1。其他值无效。这样,内存在虚拟内存空间中被分为两部分:内存空间的下半部分和上半部分。下半部分是用户空间,上半部分是内核空间。我们考虑内核空间部分。下部第一个MAXMEM size (64TB)是直接映射地址,后面是一个洞,主要目的是处理内存越界访问;然后是32TB的VMALLOC空间,接着是VMMEMMAP空间、内核文本段空间和模块空间。

在这里,我们赢了详细讲AMD64架构的布局,后面部分主要讲IA-32架构。

四、启动过程中的内存管理

启动时,虽然内存管理还没有初始化,但是内核还是需要分配内存来创建各种数据结构。Bootmem分配器用于在启动初期分配内存。因为这部分内存的分配侧重于简单性,而不是性能和通用性,所以使用了最适合的分配器。分配器使用位图来管理页面,其中1表示使用页面,0表示不使用页面。当需要分配内存时,分配器扫描位图,直到找到一个可以提供足够连续页面的位置,即第一个最佳或第一个自适应位置。

在这个分配过程中,需要处理一些未分配的页面,比如IA-32系统中的页面0。另外,对于IA-32系统,bootmem只使用了低地址部分,高地址部分的操作太麻烦,这里就放弃了。

这一节有一件很有意思的事情。当我们编写内核模块时,我们将为模块的初始化函数使用__init标签或__init_data标签。这两个关键字标记的函数和数据只在初始化阶段使用,在bootmem退出时都会被回收。这部分代码和数据将放在。init.text和。init.data段在内核链接时,会统一放在内核末尾,启动后便于回收。

五、物理内存的管理

1、合作伙伴系统

伙伴系统是物理内存管理中最重要的系统,它也是基于一个相对简单但功能惊人的算法,到现在已经用了快40年了。合作伙伴在此不再赘述,只需Google一下算法的描述(it 非常简单。这里主要说一下Linux内核的伙伴系统,以及2.6.24之后系统中伙伴系统的改进。

如上所述,物理内存的惯例是分成几个节点,每个节点又有几个区。对于每个区域,都会有一个对应的合作伙伴系统,如下图所示:

图中的回退列表是指:在有多个节点的系统中,如果一个节点没有足够的内存空间,那么内存将被分配到回退列表中指定的节点中。

我们可以执行cat /proc/buddyinfo,可以看到如下信息:

/proc/buddyinfo:Wolfgang @ meit ner cat/proc/buddyinfoNode 0,zone DMA 3 5 7 4 6 3 3 3 1 1 1Node 0,zone DMA 32 130 546 695 271 107 38 2 1 4 479 node 0,Normal 23 66 81 43 0000中显示的三个域是我们使用的内存域。

系统普遍存在一个问题:系统使用时间长了,内存中往往会有更多的碎片。在这种情况下,内核将内存页面分为五种类型:

迁移_不可移动

迁移_可回收

迁移_保留

迁移_可移动

迁移_隔离

MIGRATE_RESERVE表示的内存是系统预留的,以备紧急使用;MIGRATE_UNMOVABLE是不可移动的,比如BIOS信息页;MIGRATE _ RECLAIMABLE用于交换系统;MIGRATE_ISOLATE表示不能从这里分配的内存;MIGRATE _ MOVABLE表示可以移动的内存。对于内核来说,MIGRATE _ MOVABLE部分的内存可以通过某种算法进行移动,减少了内存中的碎片。此外,内核还维护一个后备列表,以指示如果某一类型的页面分配不成功,将分配哪些类型的页面。

具体信息可以在/proc/pagetypeinfo中找到

2、伙伴系统的内存分配API

基本上可以从下面两个图看出来:

函数的命名基本不言而喻。主要区别是带有双下划线的函数的返回值或参数(__get_free_page、__free_page等。)是struct page *,而其他函数的返回值是无符号long,也就是线性地址。

3、内核中不连续页面的分配

根据以上所述,我们知道物理上连续的映射对内核来说是最好的,但它并不总是被成功地使用。所以内核提供了一种类似于用户空间访问内存的机制(vmalloc)来分配内核中不连续的页面。这部分就是上面提到的vmalloc区。这部分主要是一个vmalloc函数:

void *vmalloc(无符号长整型);在实现这个功能的过程中,需要先申请一部分虚拟内存空间vm_area,然后将这部分空间映射到vmalloc区域。对于映射的物理内存,内核更喜欢使用高地址空间(ZONE_HIGHMEM)来节省宝贵的地址空间。会有一个洞把不同vmalloc调用应用的vm_area隔离开,避免越界访问。

注意,vmalloc系统的底层也是使用伙伴系统来分配内存,所以应用内存的大小只能是整个页面(页面大小对齐)。

这部分有个有趣的事情:IA-32中vmalloc区的预设大小是128MB,一般是内核模块使用的。vmalloc区域的大小可以定制。在新内核中,可以在内核启动选项中添加vmalloc=xxxMB来修改它,或者修改内核代码对应的宏:

unsigned int _ _ VM alloc _ RESERVE=128 20;如果修改了vmalloc区域的大小,那么内核可以直接映射的区域就会减少,也就是说kmalloc可以使用的内存会减少(kmalloc是slab分配器分配的,后面会介绍),但是内核实际使用的物理内存和vmalloc区域的大小没有直接关系。所以在编写内核模块的过程中,要根据需求使用vmalloc和kmalloc,了解它们的内存分配机制是有好处的。

4、内核映射

虽然可以使用vmalloc函数族将页框从高端内存映射到内核,但这并不是这些函数的实际用途。内核提供了其他函数来显式地将ZONE_HIGHMEM页面框架映射到内核空间。

如果需要将高端页框长期映射到内核地址空间,也就是持久映射,就需要使用kmap函数,映射的空间指向上图中的持久映射。使用内核kunmap接触贴图。持久映射kmap函数不能用于处理中断处理程序,因为kmap进程可能会进入睡眠状态。

为了原子地执行映射过程(逻辑上称为kmap_atomic),内核提供了临时映射机制,也称为固定映射,页面也会被映射到Fixmaps区域。映射的API分别是kmap_atomic和kunmap_atomic。固定映射可以用在中断处理程序中。

对于不支持高端内存的架构(如64位架构),上述映射函数通过预编译选项指向对应的兼容函数。这些架构的映射其实就是简单的返回对应的内存地址,因为内核在直接映射区就可以简单的找到对应的地址。

六、板坯分配器

在上述物理内存管理机制中,最小粒度的内存管理单位是页面帧,其大小一般为4KB。然而,每当请求内存时,在内存中分配一个页面是不合适的,因此引入了一个新的管理机制,slab分配器。Slab是Sun公司的员工Jeff Bonwick在Solaris 2.4中设计并实现的。Slab分配器将相同大小的内核对象放在一起。当对象空闲时,它们不直接返回给伙伴系统,而是保存这些对象的页面,并将其分配给这些对象的下一个内存应用程序中的新对象。该机制的优点如下1、可以根据CPU缓存的大小来组织和分配对象的位置。一般来说,几个相同的对象会放在一个cacheline中,对象占用的内存不会跨越两个cache line。这种设计可以保证slab分配器分配的对象能够长期存在于CPU缓存中。2、使用LIFO来管理对象。这种方法基于这样一个事实,即最近释放的对象空间最有可能存在于缓存中。这也可以有效利用缓存。

每个缓存管理的对象将被合并到一个更大的组中,覆盖一个或多个连续的页面帧。这个组称为slab,每个缓存由几个slab组成。这也是板坯分配器名称的由来。

1、板坯、粗纱、竹节分配器

Linux内核目前支持三种分配器,其中slab在前面已经简单介绍过,另外两种分配器是备选分配器,可以在内核编译选项中指定。因为提供给上层的API是固定的,只有下层的实现不同,所以内核开发者不不必考虑下层的分配。

Slab分配器有很大的优势,但是有两个问题1、 slab分配器在小型内存系统中过于复杂。例如,slab在嵌入式环境中有点太大。2、在内存较大的超级计算机上,slab分配器本身的数据结构占用的内存空间过大,最大可达2GB以上。

对于前一种情况,slob分配器被设计,它围绕一个简单的块链表(这也是slob的起源)。在分配内存时,采用最先适应算法。

在后一种情况下,设计了slub分配器,它将页面帧打包成组,并通过struct page中未使用的字段来管理这些组,试图最小化内存开销。竹节分配器实际上是基于板坯分配器的优化结构。在主机上的竹节分配器上性能更好。

2、板坯分配器的实验室原理

内核中一般的内存分配和释放函数有kmalloc、kzaloc和kcalloc。这三个函数的区别是:kmalloc只申请一个空格,kzalloc申请一个空格后将其设置为0。很少使用Kcalloc,即对数组进行空间分配,并设置0。

通过cat /proc/slabinfo可以看到所有活动的slab缓存。

Slab allocator由紧密交织的数据和内存结构网络组成,如图所示可分为两部分:存储托管数据的缓存对象和存储托管对象的每个Slab。

每个slab缓存只负责一种对象类型,或者提供一个通用缓冲区。下图给出了缓存的精细结构:

可以看出,对于每个slab缓存,它将被保存为一个struct kmem_cache。每个结构包含一个线程连接在一起的链表,以及三个链表头:free、partial和full,分别代表自由链、部分自由链和完整链。意思和字面意思一样。对象在板中不是连续排列的。用户可以要求对象根据硬件缓存或BYTES_PER_WORD对齐,这表示void指针所需的字节数。

要创建新的slab缓存,需要调用kmem_cache_create函数来返回struct kmem_cache结构。创建缓存时,需要指定缓存的可读名称(将出现在/proc/slabinfo中)、托管对象的字节大小、对齐数据时使用的偏移量以及flags标志。此外,还需要计算出构造函数/析构函数ctor/dtor。

分配对象时,调用kmem_cache_alloc。该函数需要指定创建的板缓存和标志。内核支持的所有GFP_xxx宏都可以用来指定标志。

下图显示了分配对象的过程:

3、通用高速缓存

如果你不涉及对象缓存,但是传统意义上的分配/释放内存,需要调用kmalloc和kfree函数,这两个函数的后端还是由slab allocator分配。kmalloc的基础是一个数组,数组中使用了一些内存长度不同的slab缓存,数组项是cache_sizes的实例。数据结构定义如下:

struct cache _ size { size _ t cs _ size;kmem _ cache _ t * cs _ cachepkmem _ cache _ t * cs _ dmacachep# ifdef CONFIG _ ZONE _ DMA struct kmem _ cache * cs _ dmacachep;#endif}cs_size指定此项负责的内存区域的长度。每个长度对应两个平板缓存,其中一个提供适合DMA访问的内存。从cat /proc/slabinfo可以看出,本节提供了kmalloc-xxx和dma-kmalloc-xxx。

Kmalloc在中定义。该函数首先检查所需的缓存是否由常数指定。如果是这样,可以在编译时静态确定需要的缓存,这样可以提高速度(内核的优化真的是无所不能!)。否则函数调用__kmalloc查找长度匹配的缓存,缓存是__do_kmalloc的前端,提供参数转换函数。

mm/slab . c void * _ _ do _ kmalloc(size _ t size,GFP _ t flags){ kmem _ cache _ t * cachep;cachep=_ _ find _ general _ cachep(size,flags);if(不太可能(ZERO_OR_NULL_PTR(cachep)))返回NULL;return __cache_alloc(cachep,flags);}__find_general_cachep在上面提到的缓存中找到合适的,然后使用__cache_alloc函数完成最终分配。

七、处理器缓存和TLB控制

这里简单总结一下内核中与TLB/缓存相关的一些函数。TLB的实现机制与架构息息相关,所以我赢了不要详细总结它们。

flush_tlb_all和flush_cache_all清除整个TLB/缓存。只有在操作内核页表时才需要这个操作,因为这样的修改不仅会影响所有进程,还会影响系统中的所有处理器。

flush _ TLB _ mm(struct mm _ struct * mm)和flush_cache_mm刷出属于地址空间mm的所有TLB/缓存条目

flush _ TLB _ range(struct VM _ area _ struct * VMA,unsigned long start,unsigned long end)和flush _ cache _ range (VMA,start,end)刷出地址范围VMA-VM _ mm中虚拟地址开始和结束之间的所有TLB/缓存条目

flush _ TLB _页面(struct VM _ area _ struct * VMA,unsigned long page)和flush_cache_page(vma,page)刷出虚拟地址在[page,page PAGE_SIZE]范围内的所有TLB/缓存条目。

页面失效后调用update _ MMU _ cache(struct VM _ area _ struct * VMA,无符号长地址,pte _ t pte)。它向处理器的内存管理单元MMU添加信息,虚拟地址由页表条目pte描述。只有当有外部MMU时,才需要此功能,MMU通常集成在处理器内部。

此外,flush_cache_和flush_tlb_函数经常成对出现。例如,当使用一个fork进程来复制一个进程的地址空间时,那么1、清除缓存,2、操作内存,而3、清除TLB。这个顺序很重要,因为

如果顺序颠倒,则在TLB被清除之后和提供正确信息之前,多处理器系统中的另一个CPU可能从进程的页表条目中获得错误的信息。

当刷新缓存时,一些体系结构需要依赖虚拟到物理TLB的翻译规则。为了确保这一点,flush_tlb_mm必须在flush_cache_mm之后执行。

总结

这部分东西太多了,简单总结一下就够了。以下是对以上内容的简要总结。

内核正常运行后,内存管理分为两级:伙伴系统负责物理页框的管理。在伙伴系统上,所有的内存管理都基于此,主要分为:slab分配器处理小块内存;vmalloc模块为不连续的物理页帧提供映射;永久映射区和固定映射区提供对高地址物理页帧的访问。

内存管理的初始化非常具有挑战性。内核通过引入一个非常简单的bootmem来解决这个问题,这个bootmem在正式的分配机制(伙伴系统)启用后被停用。审计福冈江