tag 标签: Linux内存管理

相关博文
  • 热度 5
    2023-12-12 08:53
    1002 次阅读|
    0 个评论
    Linux内存管理 | 六、物理内存分配——伙伴系统 上一章,我们了解了物理内存的布局以及Linux内核对其的管理方式,页(page)也是物理内存的最小单元,Linux内核对物理内存的分配主要分为两种:一种是整页的分配,采用的是伙伴系统,另一种是小内存块的分配,采用的是slab技术。 下面我们先来看看什么是伙伴系统! 1、伙伴系统(Buddy System) Linux系统中,对物理内存进行分配的核心是 建立在页面级的伙伴系统之上 。Linux内存管理的页大小为4KB,把所有的空闲页分组为11个页块链表,每个链表分别包含很多个大小的页块,有 1、2、4、8、16、32、64、128、256、512 和 1024 个连续页的页块,最大可以申请 1024 个连续页,对应 4MB 大小的连续内存。每个页块的第一个页的物理地址是该页块大小的整数倍。 如下图所示: image-20231021143420253 第 i 个页块链表中,页块中页的数目为 2^i。——仔细理解这个页块的含义。 在struct zone结构体中,有下面定义 struct free_area free_area ; # define MAX_ORDER11 free_area:存放不同大小的页块 MAX_ORDER:就是指数 当向内核请求分配 (2^(i-1),2^i] 数目的页块时,按照 2^i 页块请求处理。如果对应的页块链表中没有空闲页块,那我们就在更大的页块链表中去找。当分配的页块中有多余的页时,伙伴系统会根据多余的页块大小插入到对应的空闲页块链表中。 举个例子: 例如,要请求一个 128 个页的页块时,先检查 128 个页的页块链表是否有空闲块。如果没有,则查 256 个页的页块链表;如果有空闲块的话,则将 256 个页的页块分成两份,一份使用,一份插入 128 个页的页块链表中。如果还是没有,就查 512 个页的页块链表;如果有的话,就分裂为 128、128、256 三个页块,一个 128 的使用,剩余两个插入对应页块链表。 上面的这套机制就是伙伴系统所做的事情,它主要负责对物理内存页面进行跟踪,记录哪些是被内核使用的页面,哪些是空闲页面。 2、页面分配器(Page Allocator) 由上一章我们知道,物理内存被分为了几个区域:ZONE_DMA、ZONE_NORMAL、ZONE_HIGHMEM,其中前两个区域的物理页面与虚拟地址空间是线性映射的。 image-20231021153403131 页面分配器主要的工作原理如下 : 如果页面分配器分配的物理页面在ZONE_DMA、ZONE_NORMAL区域,那么对应的虚拟地址到物理地址映射的页目录已经建立,因为是线性映射,两者之间有一个差值PAGE_OFFSET。 如果页面分配器分配的物理页面在ZONE_HIGHMEM区域,那么内核此时还没有对该页面进行映射,因此页面分配器的调用者,首先在虚拟地址空间的动态映射区或者固定映射区分配一个虚拟地址,然后映射到该物理页面上。 以上就是页面分配器的原理,对于我们只需要调用相关接口函数就可以了。 页面分配函数主要有两个:alloc_pages和__get_free_pages,而这两个函数最终也会调用到alloc_pages_node,其实现原理完全一样。 下面我们从代码层面来看页面分配器的工作原理 更多干货可见: 高级工程师聚集地 ,助力大家更上一层楼! 3、gfp_mask 我们先来了解一下gfp_mask,它并不是页面分配器函数,而只是这些页面分配函数中一个重要的参数, 是个用于控制分配行为的掩码,并可以告诉内核应该到哪个zone中分配物理内存页面。 /*PlainintegerGFPbitmasks.Donotusethisdirectly.*/ # define ___GFP_DMA0x01u # define ___GFP_HIGHMEM0x02u # define ___GFP_DMA320x04u # define ___GFP_MOVABLE0x08u # define ___GFP_RECLAIMABLE0x10u # define ___GFP_HIGH0x20u # define ___GFP_IO0x40u # define ___GFP_FS0x80u # define ___GFP_WRITE0x100u # define ___GFP_NOWARN0x200u # define ___GFP_RETRY_MAYFAIL0x400u # define ___GFP_NOFAIL0x800u # define ___GFP_NORETRY0x1000u # define ___GFP_MEMALLOC0x2000u # define ___GFP_COMP0x4000u # define ___GFP_ZERO0x8000u # define ___GFP_NOMEMALLOC0x10000u # define ___GFP_HARDWALL0x20000u # define ___GFP_THISNODE0x40000u # define ___GFP_ATOMIC0x80000u # define ___GFP_ACCOUNT0x100000u # define ___GFP_DIRECT_RECLAIM0x200000u # define ___GFP_KSWAPD_RECLAIM0x400000u # ifdef CONFIG_LOCKDEP # define ___GFP_NOLOCKDEP0x800000u # else # define ___GFP_NOLOCKDEP0 # endif ___GFP_DMA:在ZONE_DMA标识的内存区域中查找空闲页。 ___GFP_HIGHMEM:在ZONE_HIGHMEM标识的内存区域中查找空闲页。 ___GFP_MOVABLE:内核将分配的物理页标记为可移动的。 ___GFP_HIGH:内核允许使用紧急分配链表中的保留内存页。该请求必须以原子方式完成,意味着请求过程不允许被中断。 ___GFP_IO:内核在查找空闲页的过程中可以进行I/O操作,如此内核可以将换出的页写到硬盘。 ___GFP_FS:查找空闲页的过程中允许执行文件系统相关操作。 ___GFP_ZERO:用0填充成功分配出来的物理页。 通常意义上,这些以“__”打头的GFP掩码只限于在内存管理组件内部的代码使用,对于提供给外部的接口,比如驱动程序中所使用的页面分配函数,gfp_mask掩码以“GFP_”的形式出现,而这些掩码基本上就是上面提到的掩码的组合。 例如内核为外部模块提供的最常使用的几个掩码如下: #defineGFP_ATOMIC(__GFP_HIGH|__GFP_ATOMIC|__GFP_KSWAPD_RECLAIM) #defineGFP_KERNEL(__GFP_RECLAIM|__GFP_IO|__GFP_FS) #defineGFP_KERNEL_ACCOUNT(GFP_KERNEL|__GFP_ACCOUNT) #defineGFP_NOWAIT(__GFP_KSWAPD_RECLAIM) #defineGFP_NOIO(__GFP_RECLAIM) #defineGFP_NOFS(__GFP_RECLAIM|__GFP_IO) #defineGFP_USER(__GFP_RECLAIM|__GFP_IO|__GFP_FS|__GFP_HARDWALL) #defineGFP_DMA__GFP_DMA #defineGFP_DMA32__GFP_DMA32 #defineGFP_HIGHUSER(GFP_USER|__GFP_HIGHMEM) #defineGFP_HIGHUSER_MOVABLE(GFP_HIGHUSER|__GFP_MOVABLE) #defineGFP_TRANSHUGE_LIGHT((GFP_HIGHUSER_MOVABLE|__GFP_COMP|\ __GFP_NOMEMALLOC|__GFP_NOWARN)&~__GFP_RECLAIM) #defineGFP_TRANSHUGE(GFP_TRANSHUGE_LIGHT|__GFP_DIRECT_RECLAIM) GFP_ATOMIC:内核模块中最常使用的掩码之一,用于原子分配。此掩码告诉页面分配器,在分配内存页时,绝对不能中断当前进程或者把当前进程移出调度器。 GFP_KERNEL:内核模块中最常使用的掩码之一,带有该掩码的内存分配可能导致当前进程进入睡眠状态。 GFP_USER:用于为用户空间分配内存页,可能引起进程的休眠。 GFP_NOIO:在分配过程中禁止I/O操作 GFP_NOFS:禁止文件系统相关的函数调用 GFP_DMA:限制页面分配器只能在ZONE_DMA域中分配空闲物理页面,用于分配适用于DMA缓冲区的内存。 通过gfp_mask掩码,更加方便我们控制页面分配器到哪个区域去分配物理内存 ,分配内存的优先级如下: 指定__GFP_HIGHMEM:先在ZONE_HIGHMEM域中查找空闲页,如果无法满足当前分配,页分配器将回退到ZONE_NORMAL域中继续查找,如果依然无法满足当前分配,分配器将回退到ZONE_DMA域,或者成功或者失败。 指定__GFP_DMA:只能在ZONE_DMA中分配物理页面,如果无法满足,则分配失败。 没有__GFP_NORMAL这样的掩码,但是前面已经提到,如果gfp_mask中没有明确指定__GFP_HIGHMEM或者是__GFP_DMA,默认就相当于__GFP_NORMAL,优先在ZONE_NORMAL域中分配,其次是ZONE_DMA域。 4、alloc_pages alloc_pages函数负责分配2^order个连续的物理页面并返回起始页的struct page实例。 alloc_pages的实现源码如下: static inline structpage* alloc_pages ( gfp_t gfp_mask, unsigned int order) { return alloc_pages_current(gfp_mask,order); } /** *alloc_pages_current-Allocatepages. * *@gfp: *%GFP_USERuserallocation, *%GFP_KERNELkernelallocation, *%GFP_HIGHMEMhighmemallocation, *%GFP_FSdon'tcallbackintoafilesystem. *%GFP_ATOMICdon'tsleep. *@order:Poweroftwoofallocationsizeinpages.0isasinglepage. * *Allocateapagefromthekernelpagepool.Whennotin *interruptcontextandapplythecurrentprocessNUMApolicy. *ReturnsNULLwhennopagecanbeallocated. */ structpage* alloc_pages_current ( gfp_t gfp, unsigned order) { struct mempolicy * pol =& default_policy ; struct page * page ; if (!in_interrupt()&&!(gfp&__GFP_THISNODE)) pol=get_task_policy(current); /* mempolicy *norsystemdefault_policy */ if mode==MPOL_INTERLEAVE) page=alloc_page_interleave(gfp,order,interleave_nodes(pol)); else page=__alloc_pages_nodemask(gfp,order, policy_node(gfp,pol,numa_node_id()), policy_nodemask(gfp,pol)); return page; } EXPORT_SYMBOL(alloc_pages_current); alloc_pages调用alloc_pages_current,其中 gfp参数:即上文的gfp_mask,表明我们想要在哪个物理内存区域进行内存分配 order参数:表示分配 2 的 order 次方个页。 __alloc_pages_nodemask为伙伴系统的核心实现,它会调用 get_page_from_freelist。 static structpage* get_page_from_freelist ( gfp_t gfp_mask, unsigned int order, int alloc_flags, const structalloc_context*ac) { ...... nodemask){ struct page * page ; ...... zone,zone,order, migratetype); ...... } 这里面的逻辑也很容易理解,就是在一个循环中先看当前节点的 zone。如果找不到空闲页,则再看备用节点的 zone。 每一个 zone,都有伙伴系统维护的各种大小的队列,就像上面伙伴系统原理里讲的那样。 这里调用 rmqueue 就很好理解了,就是找到合适大小的那个队列,把页面取下来。 伙伴系统的实现代码,感兴趣的可以深入探究。 image-20231022103859552 在调用这个函数的时候,有几种情况: 如果gfp_mask中没有指定__GFP_HIGHMEM,那么分配的物理页面必然来自ZONE_NORMAL或者ZONE_DMA,由于这两个区域内核在初始化的时候就已经建立了映射关系,所以内核很容易就能找到对应的虚拟地址KVA(Kernel Virtual Address) 如果gfp_mask中指定了__GFP_HIGHMEM,那么页分配器将优先在ZONE_HIGHMEM域中分配物理页,但也不排除因为ZONE_HIGHMEM没有足够的空闲页导致页面来自ZONE_NORMAL与ZONE_DMA域的可能性。对于新分配出的高端物理页面,由于内核尚未在页表中为之建立映射关系,所以此时需要: 在内核的动态映射区分配一个KVA 通过操作页表,将第一步中的KVA映射到该物理页面上,通过kmap实现 5、__get_free_pages __get_free_pages该函数负责分配2^ordev个连续的物理页面,返回起始页面所在内核线性地址。 函数的实现如下: /* *Commonhelperfunctions.Neverusewith__GFP_HIGHMEMbecausethereturned *addresscannotrepresenthighmempages.Usealloc_pagesandthenkmapif *youneedtoaccesshighmem. */ unsigned long __get_free_pages( gfp_t gfp_mask, unsigned int order) { struct page * page ; page=alloc_pages(gfp_mask&~__GFP_HIGHMEM,order); if (!page) return 0 ; return ( unsigned long )page_address(page); } EXPORT_SYMBOL(__get_free_pages); 我们可以看到,函数内部调用了alloc_pages函数,并且不能从__GFP_HIGHMEM高端内存分配物理页,最后通过page_address来返回页面的起始页面的内核线性地址。 6、get_zeroed_page get_zeroed_page用于分配一个物理页同时将页面对应的内容填充为0,函数返回页面所在的内核线性地址。 可以看下内核代码: unsigned long get_zeroed_page ( gfp_t gfp_mask) { return __get_free_pages(gfp_mask|__GFP_ZERO, 0 ); } EXPORT_SYMBOL(get_zeroed_page); 仅仅是在__get_free_pages基础上,使用了 __GFP_ZERO标志,来初始化分配页面的初始内容。 7、总结 以上,就是建立在伙伴系统之上的页面级分配器,常用的函数有:alloc_pages、__get_free_pages、get_zeroed_page、__get_dma_pages等,其底层实现都是一样的,只是gfp_mask不同。
  • 热度 4
    2023-11-14 08:31
    1523 次阅读|
    0 个评论
    Linux内存管理 | 五、物理内存空间布局及管理 上章,我们介绍了物理内存的访问内存模型和组织内存模型,我们再来回顾一下: 物理内存的访问内存模型分为 : UMA:一致内存访问 NUMA:非一致内存访问 物理内存的组织模型 : FLATMEM:平坦内存模型 DISCONTIGMEM:不连续内存模型 SMARSEMEM:稀疏内存模型 Linux内核为了用统一的代码获取最大程度的兼容性,对物理内存的定义方面,引入了:内存结点(node)、内存区域(zone),内存页(page)的概念,下面我们来一一探究。 更多干货可见: 高级工程师聚集地 ,助力大家更上一层楼! 1、内存节点node 内存节点的引入,是Linux为了最大程度的提高兼容性,将UMA和NUMA系统统一起来,对于UMA而言是只有一个节点的系统 。 下面的代码部分,我们尽可能的只保留暂时用的到的部分,不涉及太多的体系架相关的细节。 在Linux内核中,我们使用 typedef struct pglist_data pg_data_t表示一个节点 /* *OnNUMAmachines,eachNUMAnodewouldhaveapg_data_ttodescribe *it'smemorylayout.OnUMAmachinesthereisasinglepglist_datawhich *describesthewholememory. * *Memorystatisticsandpagereplacementdatastructuresaremaintainedona *per-zonebasis. */ typedef struct pglist_data { ... int node_id; struct page * node_mem_map ; unsigned long node_start_pfn; unsigned long node_present_pages; /*totalnumberofphysicalpages*/ unsigned long node_spanned_pages; /*totalsizeofphysicalpage range,includingholes*/ ... } pg_data_t ; node_id:每个节点都有自己的ID node_mem_map:当前节点的struct page数组,用来管理这个节点的所有的页 node_start_pfn:这个节点的起始页号 node_present_pages:这个节点的真正可用的物理内存的页面数 node_spanned_pages:这个节点所包含的物理内存的页面数,包括不连续的内存空洞 例如,64M 物理内存隔着一个 4M 的空洞,然后是另外的 64M 物理内存。 这样换算成页面数目就是,16K 个页面隔着 1K 个页面,然后是另外 16K 个页面。 这种情况下,node_spanned_pages 就是 33K 个页面,node_present_pages 就是 32K 个页面。 内核使用了一个大小为 MAX_NUMNODES ,类型为 struct pglist_data 的全局数组 node_data ; structzonelistnode_zonelists ; intnr_zones; ... }pg_data_t; nr_zones:用于统计 NUMA 节点内包含的物理内存区域个数, 不是每个 NUMA 节点都会包含以上介绍的所有物理内存区域,NUMA 节点之间所包含的物理内存区域个数是不一样的 。 事实上只有第一个 NUMA 节点可以包含所有的物理内存区域,其它的节点并不能包含所有的区域类型,因为有些内存区域比如:ZONE_DMA,ZONE_DMA32 必须从物理内存的起点开始。这些在物理内存开始的区域可能已经被划分到第一个 NUMA 节点了,后面的物理内存才会被依次划分给接下来的 NUMA 节点。因此后面的 NUMA 节点并不会包含 ZONE_DMA,ZONE_DMA32 区域。 ZONE_NORMAL、ZONE_HIGHMEM 和 ZONE_MOVABLE 是可以出现在所有 NUMA 节点上的。 node_zones :node_zones该数组包括了所有的zone物理内存区域 node_zonelists :是 struct zonelist 类型的数组,它包含了备用 NUMA 节点和这些备用节点中的物理内存区域。 下面我们看一下struct zone结构体 struct zone { ...... struct pglist_data * zone_pgdat ; struct per_cpu_pageset __ percpu * pageset ; unsigned long zone_start_pfn; /* *spanned_pagesisthetotalpagesspannedbythezone,including *holes,whichiscalculatedas: *spanned_pages=zone_end_pfn-zone_start_pfn; * *present_pagesisphysicalpagesexistingwithinthezone,which *iscalculatedas: *present_pages=spanned_pages-absent_pages(pagesinholes); * *managed_pagesispresentpagesmanagedbythebuddysystem,which *iscalculatedas(reserved_pagesincludespagesallocatedbythe *bootmemallocator): *managed_pages=present_pages-reserved_pages; * */ unsigned long managed_pages; unsigned long spanned_pages; unsigned long present_pages; const char *name; ...... /*freeareasofdifferentsizes*/ struct free_area free_area ; /*zoneflags,seebelow*/ unsigned long flags; /*Primarilyprotectsfree_area*/ spinlock_t lock; ...... }____cacheline_internodealigned_in_ zone_start_pfn:表示属于这个zone的第一个页 spanned_pages:看注释我们可以知道,spanned_pages = zone_end_pfn - zone_start_pfn,表示该区域的所有物理内存的页面数,包括内存空洞 present_pages:看注释我们可以知道,present_pages = spanned_pages - absent_pages(pages in holes),表示该区域真实存在的物理内存页面数,不包括空洞 managed_pages:看注释我们可以知道,managed_pages = present_pages - reserved_pages,被伙伴系统管理的所有页面数。 per_cpu_pageset:用于区分冷热页, 什么叫冷热页呢?咱们讲 x86 体系结构的时候讲过,为了让 CPU 快速访问段描述符,在 CPU 里面有段描述符缓存。CPU 访问这个缓存的速度比内存快得多。同样对于页面来讲,也是这样的。如果一个页被加载到 CPU 高速缓存里面,这就是一个热页(Hot Page),CPU 读起来速度会快很多,如果没有就是冷页(Cold Page)。由于每个 CPU 都有自己的高速缓存,因而 per_cpu_pageset 也是每个 CPU 一个。 image.png 更多干货可见: 高级工程师聚集地 ,助力大家更上一层楼! 3、内存页page 内存页是物理内存最小单位,有时也叫页帧(page frame),Linux会为系统的物理内存的每一个页都创建了struct page对象,并用全局变量struct page *mem_map来存放所有物理页page对象的指针,页的大小取决于MMU(Memory Management Unit),后者主要用来将虚拟地址空间转换为物理地址空间。 看一下page的结构体 struct page { unsigned long flags; /*Atomicflags,somepossibly *updatedasynchronously*/ union { struct { /*Pagecacheandanonymouspages*/ /** *@lru:Pageoutlist,eg.active_listprotectedby *zone_lru_lock.Sometimesusedasagenericlist *bythepageowner. */ struct list_head lru ; /*Seepage-flags.hforPAGE_MAPPING_FLAGS*/ struct address_space * mapping ; pgoff_t index; /*Ouroffsetwithinmapping.*/ /** *@private:Mapping-privateopaquedata. *Usuallyusedforbuffer_headsifPagePrivate. *Usedforswp_entry_tifPageSwapCache. *IndicatesorderinthebuddysystemifPageBuddy. */ unsigned long private ; }; struct { /*slab,slobandslub*/ union { struct list_head slab_list ; /*useslru*/ struct { /*Partialpages*/ struct page * next ; # ifdef CONFIG_64BIT int pages; /*Nrofpagesleft*/ int pobjects; /*Approximatecount*/ # else short int pages; short int pobjects; # endif }; }; struct kmem_cache * slab_cache ; /*notslob*/ /*Double-wordboundary*/ void *freelist; /*firstfreeobject*/ union { void *s_mem; /*slab:firstobject*/ unsigned long counters; /*SLUB*/ struct { /*SLUB*/ unsigned inuse: 16 ; unsigned objects: 15 ; unsigned frozen: 1 ; }; }; }; ..... } 我们能够看到struct page有很多union组成,union 结构是在 C 语言中被用于 同一块内存根据情况保存不同类型数据的一种方式 。这里之所以用了 union,是因为一个物理页面使用模式有多种。 第一种模式:直接用一整页,这一整页的物理内存直接与虚拟地址空间建立映射关系,我们把这种称为匿名页(Anonymous Page)。或者用于关联一个文件,然后再和虚拟地址空间建立映射关系,这样的文件,我们称为内存映射文件(Memory-mapped File),这种分配页级别的,Linux采用一种被称为 伙伴系统 (Buddy System)的技术。 第二种模式:仅需要分配小的内存块。有时候,我们不需要一下子分配这么多的内存,例如分配一个 task_struct 结构,只需要分配小块的内存,去存储这个进程描述结构的对象。为了满足对这种小内存块的需要,Linux 系统采用了一种被称为 slab allocator 的技术 上面说的两种,都是页的分配方式,也就是物理内存的分配方式,下一章,我们继续深入分析物理内存的这两种分配方式。 img
  • 热度 4
    2023-10-24 12:27
    1823 次阅读|
    0 个评论
    Linux内存管理 | 四、物理地址空间设计模型 前面几篇文章,主要讲解了虚拟内存空间的布局和管理,下面同步来聊聊物理内存空间的布局和管理。 1、物理内存 什么是物理内存? 我们平时聊的内存,也叫随机访问存储器(random-access memory),也叫RAM。 RAM分为两类: SRAM:静态RAM,其主要用于CPU高速缓存 L1Cache,L2Cache,L3Cache,其特点是访问速度快,访问速度为 1 - 30 个时钟周期,但是容量小,造价高。 CPU缓存结构.png DRAM:动态RAM,其主要用于我们常说的主存上,其特点的是访问速度慢(相对高速缓存),访问速度为 50 - 200 个时钟周期,但是容量大,造价便宜些(相对高速缓存)。 image.png DRAM经过组合起来,就作为我们的计算机内存,也是物理内存。 2、物理内存访问模型 上面介绍了物理内存的基本组成,那么CPU是如何访问物理内存的呢? 对于CPU访问物理内存,Linux提供了两种架构:UMA(Uniform Memory Access)一致内存访问,NUMA(Non-Uniform Memory Access)非一致内存访问。 2.1 UMA 在UMA架构下,多核处理器中的多个CPU,位于总线的一侧,所有的内存条组成的物理内存位于总线的另一侧。 所有的CPU访问内存都要经过总线,并且距离都是一样的,所以在UMA架构下, 所有CPU具有相同的访问特性,即对内存的访问具有相同的速度。 image-20231013075142500 2.2 NUMA 这种架构,系统中的 各个处理器都有本地内存 ,处理器与处理器之间也通过总线连接,以便于其他处理器对本地内存的访问。 与UMA不同的是,处理器访问本地内存的速度要快于对其他处理器本地内存的访问。 image-20231013074823586 3、物理内存组织模型 内存页是物理内存管理中最小单位,有时也成为页帧(Page Frame)。 内核对物理内存划分为一页一页的连续的内存块,每页大小4KB,并且使用struct page结构体来表示页结构,其中封装了每个页的状态信息,包括:组织结构,使用信息,统计信息等。 page结构体较为复杂,我们后续再深入了解。 顺便介绍一下 我的圈子: 高级工程师聚集地 ,期待大家的加入。 3.1 FLATMEM平坦内存模型 FLATMEM即:flat memory model。 我们把物理内存想象成它是由连续的一页一页的块组成的,我们从0开始对物理页编号,这样每个物理页都会有页号。 由于物理地址是连续的,页也是连续的,每个页大小也是一样的。因而对于任何一个地址,只要直接除一下每页的大小,很容易直接算出在哪一页。 如果是这样,整个物理内存的布局就非常简单、易管理,这就是最经典的 平坦内存模型 (Flat Memory Model)。 image.png 如上图,平坦内存模型中,内核使用一个mem_map的全局数组,来组织所有划分出来的物理内存页,下标由PFN表示。 在平坦内存模型下 ,page_to_pfn 与 pfn_to_page 的计算逻辑就非常简单,本质就是基于 mem_map 数组进行偏移操作。 # ifndef ARCH_PFN_OFFSET # define ARCH_PFN_OFFSET(0UL) # endif # if defined(CONFIG_FLATMEM) # define __pfn_to_page(pfn)(mem_map+((pfn)-ARCH_PFN_OFFSET)) # define __page_to_pfn(page)((unsignedlong)((page)-mem_map)+ARCH_PFN_OFFSET) # endif ARCH_PFN_OFFSET 是 PFN 的起始偏移量。 3.2 DISCONTIGMEM 不连续内存模型 DISCONTIGMEM即:discontiguous memory model。 我们早期内核使用的是FLATMEM模型,该模型对于较小的,连续的物理空间是方便使用的,但是当物理内存不连续时,使用mem_map管理,就会出现空洞,这会浪费mem_map数组本身占用的内存空间。 image.png 对于NUMA访问内存模型,物理内存分布就是不连续的,为了有效管理,DISCONTIGMEM 不连续内存模型出现了。 image.png 在不连续的物理内存中,DISCONTIGMEM不连续内存模型,将物理内存分成了一个个的node,然后每个node管理一块连续的物理内存,连续的物理内存仍然使用FLATMEM平坦内存模型来管理,从而避免了内存空洞的浪费。 我们可以看出 DISCONTIGMEM 非连续内存模型其实就是 FLATMEM 平坦内存模型的一种扩展。 DISCONTIGMEM是个稍纵即逝的内存模型,在SPARSEMEM出现后即被完全替代 。 3.3 SPARSEMEM稀疏内存模型 随着内存技术的发展,内核可以支持物理内存的热插拔了(像我们的内存条,可以直接插入拔出),这样不连续物理内存已然称为常态。 SPARSEMEM稀疏内存模型的核心思想就是 对粒度更小的连续内存块进行精细的管理 ,用于管理连续内存块的单元被称作 section 。 物理页大小为 4k 的情况下, section 的大小为 128M ,物理页大小为 16k 的情况下, section 的大小为 512M。 在内核中,使用struct mem_section结构体表示SPARSEMEM模型中的section struct mem_section { unsigned long section_mem_map; ... } 每个mem_section管理一片小的,物理内存连续的区域,并且支持对该区域的offline/online状态 所有的mem_section都保存在一个全局数组中 整体的框架如下: image.png 在 SPARSEMEM 稀疏内存模型下 page_to_pfn 与 pfn_to_page 的计算逻辑又发生了变化。 # if defined(CONFIG_SPARSEMEM) /* *Note:section'smem_mapisencodedtoreflectitsstart_pfn. *section .section_mem_map==mem_map'saddress-start_pfn; */ # define __page_to_pfn(pg)\ ({conststructpage*__pg=(pg);\ int__sec=page_to_section(__pg);\ (unsignedlong)(__pg-__section_mem_map_addr(__nr_to_section(__sec)));\ }) # define __pfn_to_page(pfn)\ ({unsignedlong__pfn=(pfn);\ structmem_section*__sec=__pfn_to_section(__pfn);\ __section_mem_map_addr(__sec)+__pfn;\ }) # endif 在 page_to_pfn 的转换中,首先需要通过 page_to_section 根据 struct page 结构定位到 mem_section 数组中具体的 section 结构。然后在通过 section_mem_map 定位到具体的 PFN。 在 pfn_to_page 的转换中,首先需要通过 __pfn_to_section 根据 PFN 定位到 mem_section 数组中具体的 section 结构。然后在通过 PFN 在 section_mem_map 数组中定位到具体的物理页 Page 。 4、总结 以上,我们先对物理内存空间有一个基础的了解,明白物理内存空间的内存访问模型和组织模型,下面我们再详细介绍物理内存空间的布局和管理。
  • 热度 3
    2023-10-18 08:33
    1164 次阅读|
    0 个评论
    Linux内存管理 | 三、虚拟地址空间管理 上一节,我们主要了解了虚拟内存空间的布局情况,趁热打铁,我们直接从源代码的视角,来看一下Linux内核是如何管理虚拟内存空间的。 废话不多说,直接开始! 1、用户态空间管理 读完上一节我们知道,用户态的布局情况如下: image-20231005160139650 我们运行的可执行程序,被加载进内存后,会作为一个进程存在,这个进程Linux内核会将其抽象成一个结构体。没错,它就是task_struct。 1.1 task_struct结构体 task_struct结构体是进程的抽象,进程所涉及到的内容非常多,下面只列举出一些重要的数据结构,方面理解。 //include/linux/sched.h struct task_struct { ... pid_t pid; //进程PID pid_t tgid; //线程PID struct files_struct * files ; //进程打开的文件信息 struct mm_struct * mm ; //进程虚拟内存空间的内存描述符 ... } 如上,进程抽象为task_struct结构体,通过mm_struct结构体来管理虚拟内存空间。 1.2 mm_struct结构体 每个进程都有唯一的 mm_struct 结构体,也就是前边提到的每个进程的虚拟地址空间都是独立,互不干扰的。 mm_struct的结构体如下: //include/linux/mm_types.h struct mm_struct { ... struct { ... unsigned long task_size; /*sizeoftaskvmspace*/ ... unsigned long mmap_base; /*baseofmmaparea*/ unsigned long total_vm; /*Totalpagesmapped*/ unsigned long locked_vm; /*PagesthathavePG_mlockedset*/ unsigned long pinned_vm; /*Refcountpermanentlyincreased*/ unsigned long data_vm; /*VM_WRITE&~VM_SHARED&~VM_STACK*/ unsigned long exec_vm; /*VM_EXEC&~VM_WRITE&~VM_STACK*/ unsigned long stack_vm; /*VM_STACK*/ unsigned long start_code,end_code,start_data,end_data; unsigned long start_brk,brk,start_stack; unsigned long arg_start,arg_end,env_start,env_end; ... struct vm_area_struct * mmap ; /*listofVMAs*/ struct rb_root mm_rb ; ... }__randomize_layout; ... } 1.3 内核态和用户态的划分 mm_struct里面定义的task_size变量,就是用来划分虚拟内存的用户空间和内核空间的。 unsigned long task_size; task_size也就是两者的分界线,下面我们看下task_size是如何被赋值的。 当我们执行一个新的进程的时候,Linux内核会执行load_elf_binary的API接口,进而调用setup_new_exec函数来实现新进程的创建。 在setup_new_exec函数中,会执行 task_size=TASK_SIZE; 这个TASK_SIZE就是我们设置的内核空间地址和用户空间地址的分界线,由我们自定义配置。 # ifdef CONFIG_X86_32 /* *Userspaceprocesssize:3GB(default). */ # define TASK_SIZEPAGE_OFFSET # define TASK_SIZE_MAXTASK_SIZE /* configPAGE_OFFSET hex default0xC0000000 dependsonX86_32 */ # else /* *Userspaceprocesssize.47bitsminusoneguardpage. */ # define TASK_SIZE_MAX((1UL<<47)-PAGE_SIZE) # define TASK_SIZE(test_thread_flag(TIF_ADDR32)?\ IA32_PAGE_OFFSET:TASK_SIZE_MAX) ...... 这里我们只需要知道TASK_SIZE默认值3为PAGE_OFFSET,并且默认为0xC0000000为分界线的,即用户空间3GB,内核空间1GB;当然这个可以由我们动态配置,可以配置PAGE_OFFSET为0x80000000,即用户空间和内核空间均为2GB,取决于我们的应用场合,当你看到与我们讲解不同时,也不用大惊小怪。 以上,表达的概念很简单,如下图: image.png 1.4 位置信息描述 我们知道用户态内存空间分为几个区域:代码段、数据段、BSS段、堆、文件映射和匿名映射区、栈等几个部分,同样在mm_struct中,定义了这些区域的统计信息和位置。 unsigned long mmap_base; /*baseofmmaparea*/ unsigned long total_vm; /*Totalpagesmapped*/ unsigned long locked_vm; /*PagesthathavePG_mlockedset*/ unsigned long pinned_vm; /*Refcountpermanentlyincreased*/ unsigned long data_vm; /*VM_WRITE&~VM_SHARED&~VM_STACK*/ unsigned long exec_vm; /*VM_EXEC&~VM_WRITE&~VM_STACK*/ unsigned long stack_vm; /*VM_STACK*/ unsigned long start_code,end_code,start_data,end_data; unsigned long start_brk,brk,start_stack; unsigned long arg_start,arg_end,env_start,env_end; total_vm:总映射页面的数目。(这么大的虚拟内存空间,不可能全部映射到真实的物理内存,都是按需映射的,这里表示当前映射的页面总数目) 由于物理内存比较小,当内存吃紧的时候,就会发生 换入换出 的操作,即将暂时不用的页面换出到硬盘上,有的页面比较重要,不能换出。 locked_vm:被锁定不能换出的页面 pinned_vm :不能换出、也不能移动的页面 data_vm:存放数据页的页的数目 exec_vm:存放可执行文件的页的数目 stack_vm:存放堆栈信息页的数目 start_code、end_code:表示可执行代码开始和结束的位置 start_data、end_data:表示已初始化数据的开始位置和结束位置 start_brk、brk:堆的起始地址,结束地址 start_stack:是栈的起始位置,在 RBP 寄存器中存储,栈的结束位置也就是栈顶指针,在 RSP 寄存器中存储。在栈中内存地址的增长方向也是由高地址向低地址增长。 arg_start、arg_end:参数列表的起始位置和结束位置 env_start、env_end:环境变量的起始位置和结束位置 整体的布局情况如下 : image.png 1.5 区域属性描述 尽管已经有了一些变量来描述每一个段的信息,但是Linux内核在mm_struct结构体里面,还有一个专门的数据结构vm_area_struct来管理每个区域的属性。 struct vm_area_struct * mmap ; /*listofVMAs*/ struct rb_root mm_rb ; mmap:为一个单链表,将所有的区域串联起来 mm_rb:为一个红黑树,方便查找和修改内存区域。 下面看一下vm_area_struct数据结构: struct vm_area_struct { /*ThefirstcachelinehastheinfoforVMAtreewalking.*/ unsigned long vm_start; /*Ourstartaddresswithinvm_mm.*/ unsigned long vm_end; /*Thefirstbyteafterourendaddresswithinvm_mm.*/ /*linkedlistofVMareaspertask,sortedbyaddress*/ struct vm_area_struct * vm_next ,* vm_prev ; struct rb_node vm_rb ; struct mm_struct * vm_mm ; /*Theaddressspacewebelongto.*/ struct list_head anon_vma_chain ; /*Serializedbymmap_sem& *page_table_lock*/ struct anon_vma * anon_vma ; /*Serializedbypage_table_lock*/ /*Functionpointerstodealwiththisstruct.*/ const struct vm_operations_struct * vm_ops ; struct file * vm_file ; /*Filewemapto(canbeNULL).*/ void *vm_private_data; /*wasvm_pte(sharedmem)*/ }__randomize_layout; vm_start、vm_end:为该区域在用户空间的起始和结束地址 vm_next、vm_prev:将该区域添加到链表上,便于管理。 vm_rb:将这个区域放到红黑树上 vm_ops:对该区域可以进行的内存操作 anon_vma:匿名映射 vm_file:文件映射 用户态空间的每个区域都由该结构体来管理,最终形成下面的这个结构: image-20231008184824770 顺便介绍一下 我的圈子: 高级工程师聚集地 ,期待大家的加入。 2、内核态空间管理 上面,我们从源码角度了解了用户态空间管理,下面我们看内核态空间管理。 回顾一下,我们内核态的布局情况是怎么样的呢,还记得吗? image-20231005155942462 我们要知道: 内核态的虚拟空间和任何一个进程都没有关系,所有的进程看到的内核态虚拟空间都是一样的。 在内核态,我们直接操作的依旧是虚拟地址,而非物理地址 不同CPU结构下,内核态空间的布局格式是不变的,但是大小会有所调整,比如ARM和X86的大小空间有所不同。 内核态空间管理并不像用户态那样使用结构体来统一管理,而是直接使用宏来定义每个区域的分界线, 下面我们以x86架构来分析内核态空间的管理 2.1 分界线定义 /* *Userspaceprocesssize:3GB(default). */ # define TASK_SIZEPAGE_OFFSET /*PAGE_OFFSET-thevirtualaddressofthestartofthekernelimage*/ # define PAGE_OFFSET((unsignedlong)__PAGE_OFFSET) # define __PAGE_OFFSET__PAGE_OFFSET_BASE # define __PAGE_OFFSET_BASE_AC(CONFIG_PAGE_OFFSET,UL) configPAGE_OFFSET hex default 0xB0000000 if VMSPLIT_3G_OPT default 0x80000000 if VMSPLIT_2G default 0x78000000 if VMSPLIT_2G_OPT default 0x40000000 if VMSPLIT_1G default 0xC0000000 dependsonX86_32 TASK_SIZE:内核态空间与用户态空间的分界线 PAGE_OFFSET:该宏表示内核镜像起始的虚拟地址。 CONFIG_PAGE_OFFSET:这个宏定义的值,根据实际情况自行设定,默认为0XC0000000,可以设置为0X80000000等。 以上,TASK_SIZE就被定义为0XC0000000作为用户态空间和内核态空间的分界线,将4G虚拟内存分配为3G/1G结构。 image-20231010072937276 2.2 直接映射区定义 直接映射区是定义在PAGE_OFFSET和high_memory之间的区域。 PAGE_OFFSET:表示内核镜像的起始地址,上文已经说明。 high_memory也是表示的就是896M这个值,表示高端内存的分界线。 顺便说明以下,TASK_SIZE和PAGE_OFFSET在不同架构下是不同的,在ARM架构下,两者并不相等,本文以X86架构为例 image-20231010073949813 2.3 安全保护区定义 系统会在high_memory和VMALLOC_START之间预留8M的安全保护区,防止访问越界。 VMALLOC_OFFSET表示的是内核动态映射区的偏移,也就是所谓的安全保护区。 # define VMALLOC_START(((unsignedlong)high_memory+VMALLOC_OFFSET)&~(VMALLOC_OFFSET-1)) # define VMALLOC_OFFSET(8*1024*1024) 可以很清楚的看到VMALLOC_OFFSET定义了8M的空间,VMALLOC_START在high_memory基础上,偏移了VMALLOC_OFFSET 8M空间大小作为安全保护区,以防越界访问。 image-20231010074810831 2.3 动态映射区定义 VMALLOC_START和VMALLOC_END之间称为内核动态映射区。 和用户态进程使用 malloc 申请内存一样,在这块动态映射区内核是使用 vmalloc 进行内存分配。 # define VMALLOC_START(((unsignedlong)high_memory+VMALLOC_OFFSET)&~(VMALLOC_OFFSET-1)) # ifdef CONFIG_HIGHMEM # define VMALLOC_END(PKMAP_BASE-2*PAGE_SIZE) # else # define VMALLOC_END(LDT_BASE_ADDR-2*PAGE_SIZE) # endif PKMAP_BASE:是永久映射区的起始地址。 VMALLOC_END:在永久映射区的起始地址下,偏移2个PAGE_SIZE作为安全保护区。 image-20231010075717944 2.4 永久映射区定义 PKMAP_BASE 到 FIXADDR_START 的空间称为永久内核映射,在内核的这段虚拟地址空间中允许建立与物理高端内存的长期映射关系。 比如内核通过 alloc_pages() 函数在物理内存的高端内存中申请获取到的物理内存页,这些物理内存页可以通过调用 kmap 映射到永久映射区中。 # define PKMAP_BASE\ ((LDT_BASE_ADDR-PAGE_SIZE)&PMD_MASK) # define LDT_BASE_ADDR\ ((CPU_ENTRY_AREA_BASE-PAGE_SIZE)&PMD_MASK) # define CPU_ENTRY_AREA_BASE\ ((FIXADDR_TOT_START-PAGE_SIZE*(CPU_ENTRY_AREA_PAGES+1))\ &PMD_MASK) # define FIXADDR_TOT_START(FIXADDR_TOP-FIXADDR_TOT_SIZE) # define FIXADDR_TOP((unsignedlong)__FIXADDR_TOP) # define FIXADDR_TOT_SIZE(__end_of_fixed_addresses<
  • 热度 4
    2023-10-12 08:22
    1628 次阅读|
    0 个评论
    Linux内存管理 | 二、虚拟地址空间布局 上一章,我们了解了内存管理的由来以及核心思想,下面我们按照顺序,先来介绍一下Linux虚拟内存空间的管理。 同样,我们知道Linux内核抽象出来虚拟内存空间,主要是为了让每个进程都独享该空间,那虚拟内存空间是如何布局的呢? 前提 :针对于不同位数的CPU,寻址能力不同,抽象出来的虚拟内存空间大小也不同,我们以常见的32位的CPU为例。 1、虚拟内存空间布局 对于32位的CPU,寻址范围为0~2^32,也就是0x00000000-0xFFFFFFFF,即最多抽象出来4G的虚拟内存空间。 这4GB的内存空间,在Linux中,又分为 用户空间和内核空间 ,其中0x0000000-0xBFFFFFFF,共3G为用户空间,0xC00000000-0xFFFFFFFF,共1G为内核空间,如下: image-20230924173124939 无论内核空间还是用户空间,其仍然是在虚拟内存空间基础之上进行划分的,其直接访问的依旧都是虚拟地址,而非物理地址! 我们编写代码后,所生成的可执行程序,运行之后就成为一个系统进程,我们在"虚"的角度来看,每个进程都是独享这4G虚拟地址空间的, 2、用户态空间布局 如上所述,用户空间在虚拟内存中分布在0x0000000-0xBFFFFFFF,大小为3G。 每一个用户进程,按照 访问属性一致的地址空间存放在一起 的原则,划分成5个不同的内存区域(访问属性一致指的是:可读,可写,可执行): 代码段 :Text Segment,也就是我们的二进制程序,代码段需要防止在运行时被非法修改, 所以该段为只读 。 数据段 :Data Segment,主要存放初始化了的变量,主要包括:静态变量和全局变量, 该段为读写 。 BSS段 :BSS Segment,主要存放未初始化的全局变量,在内存中 bss 段全部置零, 该段为读写 。 堆段 :Heap Segment,主要存放进程运行过程中动态分配的内存段,大小不固定,可动态扩张和缩减,通常使用malloc和free来分配释放,并且堆的增长方向是向上的。 文件映射和匿名映射段 :Memory Mapping Segment,主要存放进程使用到的文件或者依赖的动态库,从低地址向上增长。 栈段 :Stack Segment,主要存放进程临时创建的局部变量,函数调用上下文信息等,栈向下增长。 image-20231005160139650 一个可执行程序,可以通过size命令,查看编译出来的可执行文件大小,其中包括了代码段,数据段等数据信息,如下: donge@Donge:$sizeDonge-Demo textdatabssdechexfilename 1253819164363258086e2e6Donge-Demo text:代码段大小 data:数据段大小 bss:bss段大小 dec:十进制表示的可执行文件大小 hex:十六进制表示的可执行文件大小 运行该程序后,可以通过cat /proc/PID/maps命令,或者pmap PID命令,来查看该进程在虚拟内存空间中的分配情况,其中PID为进程的PID号,如下: donge@Donge:$cat/proc/16508/maps 55976ff9e000-55976ffa0000r--p0000000008:10184922/home/donge/WorkSpace/Program/Donge_Programs/Donge_Demo/build/Donge-Demo 55976ffa0000-55976ffa2000r-xp0000200008:10184922/home/donge/WorkSpace/Program/Donge_Programs/Donge_Demo/build/Donge-Demo 55976ffa2000-55976ffa3000r--p0000400008:10184922/home/donge/WorkSpace/Program/Donge_Programs/Donge_Demo/build/Donge-Demo 55976ffa3000-55976ffa4000r--p0000400008:10184922/home/donge/WorkSpace/Program/Donge_Programs/Donge_Demo/build/Donge-Demo 55976ffa4000-55976ffa5000rw-p0000500008:10184922/home/donge/WorkSpace/Program/Donge_Programs/Donge_Demo/build/Donge-Demo 55976ffa5000-55976ffaf000rw-p0000000000:000 559771d91000-559771db2000rw-p0000000000:000 7fec1ad84000-7fec1ad87000rw-p0000000000:000 7fec1ad87000-7fec1adaf000r--p0000000008:1022282/usr/lib/x86_64-linux-gnu/libc.so.6 7fec1adaf000-7fec1af44000r-xp0002800008:1022282/usr/lib/x86_64-linux-gnu/libc.so.6 7fec1af44000-7fec1af9c000r--p001bd00008:1022282/usr/lib/x86_64-linux-gnu/libc.so.6 7fec1af9c000-7fec1afa0000r--p0021400008:1022282/usr/lib/x86_64-linux-gnu/libc.so.6 7fec1afa0000-7fec1afa2000rw-p0021800008:1022282/usr/lib/x86_64-linux-gnu/libc.so.6 7fec1afa2000-7fec1afaf000rw-p0000000000:000 7fec1afb5000-7fec1afb7000rw-p0000000000:000 7fec1afb7000-7fec1afb9000r--p0000000008:1022068/usr/lib/x86_64-linux-gnu/ld-linux-x86-64.so.2 7fec1afb9000-7fec1afe3000r-xp0000200008:1022068/usr/lib/x86_64-linux-gnu/ld-linux-x86-64.so.2 7fec1afe3000-7fec1afee000r--p0002c00008:1022068/usr/lib/x86_64-linux-gnu/ld-linux-x86-64.so.2 7fec1afef000-7fec1aff1000r--p0003700008:1022068/usr/lib/x86_64-linux-gnu/ld-linux-x86-64.so.2 7fec1aff1000-7fec1aff3000rw-p0003900008:1022068/usr/lib/x86_64-linux-gnu/ld-linux-x86-64.so.2 7ffce385d000-7ffce387e000rw-p0000000000:000 7ffce394e000-7ffce3952000r--p0000000000:000 7ffce3952000-7ffce3953000r-xp0000000000:000 上面能大致看出该进程的代码段、堆、文件映射段,栈的内存分布等情况,以上就是我们的可执行程序被加载进入内存之后,在用户态虚拟内存空间的布局情况。 顺便介绍一下 我的圈子: 高级工程师聚集地 ,期待大家的加入。 3、内核态空间布局 下面我们来看一下内核态的虚拟空间布局,首先我们要知道: 在Linux系统中,用户进程通常只能访问用户空间的虚拟地址,只有在执行内陷操作或系统调用时才能访问内核空间。 所有的进程通过系统调用进入内核态之后,看到的虚拟地址空间都是一样的,他们是共享内核态虚拟内存空间的。 32位的内核态虚拟空间在虚拟内存中分布在0xC00000000-0xFFFFFFFF上,大小为1G,其要分为以下几个区 : 直接映射区(Direct Memory Region) :顾名思义,直接映射区就是直接与物理内存建立一一映射关系。从内核空间起始地址开始,到896M的内核空间地址区间,为直接内存映射区,该区域线性地址和分配的物理地址都是连续的。 896M以上的内核地址空间,又称为高端内存区域。 安全保护区 :也成为内存空洞,大小为8M,其主要目的是为了避免 非连续区的非法访问, 动态映射区 :也就是vmalloc Region,该区域由Vmalloc函数分配,特点是:虚拟地址空间连续,但是物理地址空间不一定连续。 永久映射区(Persistent Kernel Mapping Region) :该区域主要用于访问高端内存,通过alloc_page (_GFP_HIGHMEM)接口分配高端内存页,可以使用kmap函数将分配到的高端内存映射到该区域。 固定映射区(Fixing kernel Mapping Region) :该区域虚拟内存地址可以自由映射到物理内存的高端地址上,“固定”表现在“虚拟内存空间地址是固定的”,被映射的物理地址是可变的。 为什么会有固定映射这个概念呢 ? 比如:在内核的启动过程中,有些模块需要使用虚拟内存并映射到指定的物理地址上,而且这些模块也没有办法等待完整的内存管理模块初始化之后再进行地址映射。因此,内核固定分配了一些虚拟地址,这些地址有固定的用途,使用该地址的模块在初始化的时候,将这些固定分配的虚拟地址映射到指定的物理地址上去。 image-20231005155942462 4、总结 以上就是整个虚拟地址空间的划分,总结如下: image-20231005160802093