Linux(内核剖析):38---内存管理之(页(struct page)、区(struct zone))

一、页

  • 内核把物理页作为内存管理的基本单位。尽管处理器的最小可寻址单位通常为字(甚至字节),但是,内存管理单元(MMU,管理内存并把虚拟地址转换为物理地址的硬件)通常以页为单位进行处理。正因为如此,MMU以页(page)大小为单位来管理系统中的页表(这也是页表名的来由)。从虚拟内存的角度来看,页就是最小单位
  • 在后面“可移植性”中我们将会看到,体系结构不同,支持的页大小也不尽相同,还有些体系结构甚至支持几种不同的页大小。大多数32位体系结构支持4KB的页,而64位体系结构一般会支持8KB的页。这就意味着,在支持4KB页大小并有1GB物理内存的机器上,物理内存会被划分为262144个页

struct page

  • 内核用struct page结构表示系统中的每个物理页,该结构位于<linux/mm_types.h>中——下面给出了一个简略版的定义,去除了两个容易混淆我们讨论主题的联合结构体:

  • flag域用来存放页的状态。这些状态包括页是不是脏的,是不是被锁定在内存中等。flag的每一位单独表示一种状态,所以它至少可以同时表示出32种不同的状态。这些标志定义在<linux/page-flags.h>中
  • _count域存放页的引用计数——也就是这一页被引用了多少次。当计数值变为-1时,就说明当前内核并没有引用这一页,于是,在新的分配中就可以使用它。内核代码不应当直接检査该域,而是调用page_count()函数进行检査,该函数唯一的参数就是page结构。当页空闲时,尽管该结构内部的_count值是负的,但是对page_count()函数而言,返回0表示页空闲,返回一个正整数表示页在使用。一个页可以由页缓存使用(这时,mapping域指向和这个页关联的addresss_space对象),或者作为私有数(由private指向),或者作为进程页表中的映射
  • virtual域是页的虚拟地址。通常情况下,它就是页在虚拟内存中的地址。有些内存(即所谓的高端内存)并不永久地映射到内核地址空间上。在这种情况下,这个域的值为NULL,需要的时候,必须动态地映射这些页。稍后我们将讨论高端内存
  • 须要理解的一点是page结构与物理页相关,而并非与虚拟页相关。因此,该结构对页的描述只是短暂的。即使页中所包含的数据继续存在,由于交换等原因,它们也可能并不再和同一个page结构相关联。内核仅仅用这个数据结构来描述当前时刻在相关的物理页中存放的东西。 这种数据结构的目的在于描述物理内存本身,而不是描述包含在其中的数据
  • 内核用这一结构来管理系统中所有的页,因为内核需要知道一个页是否空闲(也就是页有没有被分配)。如果页已经被分配,内核还需要知道谁拥有这个页。拥有者可能是用户空间进程、动态分配的内核数据、静态内核代码或页高速缓存等
  • 系统中的每个物理页都要分配一个这样的结构体,开发者常常对此感到惊讶。他们会想 “这得浪费多少内存呀”!让我们来算算对所有这些页都这么做,到底要消耗掉多少内存。就算struct page占40个字节的内存吧,假定系统的物理页为8KB大小,系统有4GB物理内存。那么, 系统中共有页面 524288个,而描述这么多页面的page结构体消耗的内存只不过20MB:也许绝对值不小,但是相对系统4GB内存而言,仅是很小的一部分罢了。因此,要管理系统中这么多物理页面,这个代价并不算太

二、区

  • 由于硬件的限制,内核不能对所有的页一视同仁。有些页位于内存中特定的物理地址上,所以不能将其用于一些特定的任务。由于存在这种限制,所以内核把页划分为不同的区(zone)。内核使用区对具有相似特性的页进行分组
  • Linux必须处理如下两种由于硬件存在缺陷而引起的内存寻址问题:
    • 一些硬件只能用某些特定的内存地址来执行DMA
    • 一些体系结构其内存的物理寻址范围远大于虚拟寻址范围,这样,就有一些内存不能永久地映射到内核空间
  • 因为存在这些限制条件,Linux主要使用了四种区:
    • ZONE_DMA:这个区包含的页能用来执行DMA操作
    • ZONE_NORMAL:和ZOME_DMA类似,该区包含的页面可用来执行DMA操作;而和ZONE_DMA不同之处在于,这些页面只能被32位设备访问。在某些体系结构中,该区将比ZONE_DMA更大
    • ZONE_NORMAL:这个区包含的都是能正常映射的页
    • ZONE_HIGHEM:这个区包含“高端内存”,其中的页并不能永久地映射到内核地址空间
  • 这些区(还有两种不大重要的)在<linux/mmzone.h>中定义
  • 区的实际使用和分布是与体系结构相关

ZONE_DMA

  • 区的实际使用和分布是与体系结构相关的。例如,某些体系结构在内存的任何地址上执行DMA没问题。在这些体系结构中,ZONE_DMA为空,ZONE_NORMAL就可以直接用于分配。与此相反,在x86体系结构上,ISA设备就不能在整个32位的地址空间中执行DMA,因为ISA设备只能访问物理内存的前16MB。因此,ZONE_DMA在x86上包含的页都在0-16MB的内存范围里

ZONE_HIGHEM

  • ZONE_HIGHMEM的工作方式也差不多。能否直接映射取决于体系结构。在32位x86系统 上,ZONE_HIGHMEM为高896MB的所有物理内存。在其他体系结构上,由于所有内存都被直接映射,所以ZONE_HIGHMEM为空
  • ZONE_HIGHMEM所在的内存就是所谓的高端内。系统的其余内存就是所谓的低端内存

ZONE_NORMAL

  • 前两个区各取所需之后,剩余的就由ZONE_NORMAL区独享了。在x86上,ONE_NORMAL是从16MB到896MB的所有物理内存。在其他(更幸运)的体系结构上,ZONE_NORMAL是所有的可用物理内存
  • 每个区及其在x86-32上所占页的列表:

  • Linux把系统的页划分为区,形成不同的内存池,这样就可以根据用途进行分配了。例如,ZONE_DMA内存池让内核有能力为DMA分配所需的内存。如果需要这样的内存,那么,内核就可以从ZONE_DMA中按照请求的数目取出页
  • 注意,区的划分没有任何物理意义,这只不过是内核为了管理页而采取的一种逻辑上的分组
  • 某些分配可能需要从特定的区中获取页,而另外一些分配则可以从多个区中获取页。比如, 尽管用于DMA内存必须从ZONE_DMA中进行分配,但是:般用途的内存却既能从ZONE_DMA分配,也能从ZONE_NORMAL分配,不过不可能同时从两个区分配,因为分配是不能跨区接线的。当然,内核更希望一般用途的内存从常规区分配,这样能节省ZONE_DMA中的页,保证满足DMA的使用需求。但是,如果可供分配的资源不够用了(如果内存已经变得很少了),那么,内核就会去占用其他可用区的内存
  • 不是所有的体系结构都定义了全部区,有些64位的体系结构,如Intel的x86-64体系结构可以映射和处理64位的内存空间,所以x86-64没有ZONE_HIGHMEM区,所有的物理内存都处于ZONE_DMA和ZONE_NORMAL区

struct zone

  • 每个区都用这个结构体表示,在<linux/mmzone.h>中定义如下:

  • 这个结构很大,但是,系统中只有三个区,因此,也只有三个这样的结构
  • 一些重要的域如下:
    • lock:是一个自旋锁,它放置该结构被并发访问。注意,这个域只保护结构,而不保护驻留在这个区中的所有页。没有特定的锁来保护单个页,但是,部分内核可以锁住在页中驻留的数据
    • watermark:该数组持有该去的最小值、最低和最高水位值。内核使用水位为每个内存区设置合适的内存消耗基准。该水位随空闲内存的多少而变化
    • name:是一个以NULL结束的字符串表示这个区的名字。内核启动期间初始化你这个值,其代码位于mm/page_alloc.c中。三个区的名字分别为DMA、Normal、HighMem

三、页分配

  • 下面是 内核实现的接口,通过这些接口在内核内分配和释放内存
  • 内核提供了一种请求内存的底层机制,并提供了对它进行访问的几个接口。所有这些接口都以页为单位分配内存,定义于<linux/gfp.h>中

alloc_page

  • 该函数分配2^{order}(1<<order)个连续的物理页
  • 返回一个指针,该指针指向于第一个页的page结构体
  • 如果出错返回NULL
  • gft_t数据类型和gft_mask参数在后面介绍kmalloc的时候再介绍

page_address

  • 可以使用这个函数把给定的页转换成它的逻辑地址
  • 该函数返回一个指针,指向给定物理页当前所在的逻辑地址

__get_free_pages

  • 如果你无需使用到struct page,那么你可以调用这个函数
  • 这个函数与alloc_pages()作用相同,不过它直接返回锁请求的第一个页的逻辑地址
  • 因为页是连续的,所以其他页也会紧随其后
  • 如果你只需一页,就可以用下面的两个函数,这两个函数与其兄弟函数工作方式相同,只不过传递给order的值为0

get_zeroed_page

  • 这个函数与__get_free_pages()工作方式相同,只不过把分配好的页都填充成了0——字节中的每一位都要取消设置
  • 如果分配的页是给用户空间的,这个函数就非常有用了
  • 虽说分配好的页中应该包含的都是随机产生的垃圾信息,但其实这些信息可能并不是完全随机的——它很可能“随机地”包含某些敏感数据。用户空间的页在返回之前,所有数据必须填充为0,或做其他清理工作,在保证系统安全这一点上,我们绝不妥协
  • 下表是所有页分配方法的列表:

四、释放页

  • 当你不再需要页时,可以用下面的函数释放它们:

  • 释放页时要谨慎,只能释放属于你的页。传递了错误的struct page或地址,用了错误的order值,这些都可能导致系统崩溃。请记住,内核是完全信赖自己的。这点与用户空间不同,如果你有非法操作,内核会开开心心地把自己挂起来,停止运行
  • 下面看一个例子,其中想获得8个页:

  • 在此,我们使用完这8个页之后释放它们

  • GFP_KERNEL参数是gfp_mask标志的一个例子
  • 调用_get_free_pages(*)之后要注意进行错误检查。内核分配可能失败,因此你的代码必须进行检查并做相应的处理。这意味在此之前,你所做的所有工作可能前功尽弃,甚至还需要回归到原来的状态。正因为如此,在程序开始时就先进行内存分配时很有意义的,这能让错误处理得到容易一点。如果你不这么做,那么在你想要分配内存的时候如果失败了,局面可能很难以控制了
  • 当你需要以页为单位的一族连续物理页时,尤其是在你只需要一两页时,这些低级页函数很有用。对于常用的以字节为单位的分配来说,内核提供的函数时kmalloc()
发布了1481 篇原创文章 · 获赞 1026 · 访问量 38万+

猜你喜欢

转载自blog.csdn.net/qq_41453285/article/details/104373714
今日推荐