eslab2013-ustc / RTFSC_Linux_Kernel

USTC 嵌入式系统实验室2013级研究生阅读内核记录
64 stars 55 forks source link

页框管理 #12

Open hangc0276 opened 10 years ago

hangc0276 commented 10 years ago

对内存的管理涉及两个部分:

对物理内存的管理 是指对“RAM的管理”,对虚拟内存的管理 是指对进程地址空间的管理,它们两者通过page fault(缺页处理)联系起来。在RAM管理部分,主要包括页框管理和内存区管理;内存管理包括内存区管理、内存映射、伙伴系统、slab和内存池等部分。

对于i386这种32位的处理器结构,Linux采用4KB页框大小作为标准的内存分配单元。内核必须记录每一个页框的当前状态,如:区分哪些页框包含的是属于进程的页,而哪些页框包含的是内核代码或内核数据。内核还必须确定动态内存中的页框是否空闲,如果动态内存的页框中不包含有用数据,则该页框是空闲的。在以下情况下页框是不空闲的:包含用户态进程的数据、某个软件高速缓存的数据、动态分配内存数据结构、设备驱动程序缓冲数据、内核模块的代码等。

为了对页框的管理,内核引入了一下两个的数据结构:

page:结构体类型定义,是页框描述符
mem_map:页框描述符数组,用于存放所有的页框描述符

数据结构布局图如下: 内核必须记录每个页框的当前状态,保存在page页描述符结构中,该结构包含两个重要的数据成员

unsigned long flags ;
atomic_t _count ;

对于问题1: 内核提供了virt_to_page(addr)来解决该问题,源代码如下:

#define virt_to_page(kaddr) pfn_to_page(__pa(kaddr) >> PAGE_SHIFT)
#define __pa(x)     __phys_addr((unsigned long)(x))
#define __phys_addr(x)      ((x) - PAGE_OFFSET)
struct page *pfn_to_page(unsigned long pfn)
{
    return __pfn_to_page(pfn);
}
#define __pfn_to_page(pfn)  (mem_map + ((pfn) - ARCH_PFN_OFFSET))

PAGE_SHIFT = 12
PAGE_OFFSET = 0xc0000000

其中,kaddr 表示线性地址。首先将线性地址kaddr - 3G得到物理地址,再将物理地址__pa(kaddr)右移12位得到页框号(因为每一页是4KB,刚好12位,右移12位就可以得到该页的页框号),再将页框号加上mem_map数组的基址,就可以得到该页的页描述符地址(这里的页框号相当于页描述符数组mem_map的偏移量)。

对于问题2: 这个问题在上面问题1中有提到,内核提供pfn_to_page(pfn)函数来解决该问题,源代码如下:

#define __pfn_to_page(pfn)  (mem_map + ((pfn) - ARCH_PFN_OFFSET))

其基本思想就是将页框号加上mem_map数组的基址,从而得到该页的页描述符地址。

对于问题3: 有了mem_map数组,这个问题就变得简单了。因为如果知道了一个页描述符地址pd,将pd减去mem_map就可以得到pd对应的页框号pfn,那么该页的物理地址是physAddr = pfn << PAGE_SHIFT。其中PAGE_SHIFT = 12

对于问题4: 在得到物理地址physAddr之后,就可以根据physAddr的大小得到它的虚拟地址:

1. physAddr > 896M 对应虚拟地址是:physAddr + PAGE_OFFSET(PAGE_OFFSET = 3G)
2. physAddr >= 896M 对应虚拟地址不是静态映射的,通过内核的高端虚拟地址映射得到一个虚拟地址。

在得到该页的虚拟地址之后,内核就可以正常访问这个物理页了。

注意:这里的physAddr虽然表示物理地址,但并不能说明该地址所对应的数据一定存在于物理内存中。那么如何判断这个也到底在不在物理内存中呢?这就要使用到Linux中的分页机制了


问题:系统是怎样为进程或内核分配一个内存空间,或者说怎么给它们分配一个线性页描述符所对应的线性地址页面的呢?

在解决这个问题之前,需要一些数据结构的支撑,现介绍如下:

非一致内存访问(NUMA)

Motivation: Linux2.6支持非统一内存访问(NUMA)模型,在这种模型中,给定CPU对不同内存单元的访问时间可能不一样。系统的物理内存被划分为几个节点(node)。在一个单独的节点内,任一给定CPU访问页面所需要的时间都是相同的,而对于不同的CPU,这个时间就不同。对每个CPU而言,内核都试图把耗时节点的访问次数减到最少,这就必须要将那些CPU最常引用的内核数据结构的存放位置选好。

每个节点由一个类型为pg_data_t的描述符表示,所有节点的描述符存放在一个单链表中(在Linux 2.6.26版本中没有见到连接该链表节点的指针),它的第一个元素有内核全局变量pgdat_list指向,在x86体系结构中,即使是多核,内存访问时间也是相同的,所以不需要NUMA,但是内核还是使用节点,不过这只是一个单独的节点,它包含了系统中所有的物理内存。这种方式有助于内核代码的处理更具有可移植性,因为内核假定在所有体系结构中物理内存都被划分为一个或多个节点。因此,在x86体系结构中,pgdat_list指向一个链表,此链表只有一个元素组成,这个元素就是节点0描述符,它被存放在contig_page_data变量中。

pg_data_t描述符中要注意到的三个字段分别是node_zones、node_zonelists、node_mem_map,分别是zone_t[]、zonelist_t[]和page类型。前两个是用来描述内存管理区的,下面马上要谈到;node_mem_map是本节点所有页的页描述符数组。内核将这三个字段放在里边,就是为内存区、页框建立一些列的联系。

内存管理区

Motivation: 在一个理想计算机体系结构中,一个页框就是一个内存存储单元,可用于任何事情:存放内核数据和用户数据、缓冲磁盘数据等等。任何种类的数据页都可以存放在任何页框中,没有什么限制。但实际的计算机体系结构中有硬件制约,限制了页框使用的方式。Linux内核必须处理80x86体系结构中的两种硬件制约:

基于以上两种限制,Linux2.6把每个内存节点的物理内存划分为3个管理区(zone)。在80x86的UMA体系结构中的管理区为:

ZONE_DMAZONE_NORMAL区包含内存“常规”页框,通过把他们线性地址映射到线性地址空间的第4个GB,内核就可以直接进行访问。ZONE_HIGHMEM区包含的内存页不能由内核直接访问,尽管它们也可以通过高端内存内核映射,线性映射到线性地址空间的第4个GB。

每个内存管理区都有自己的描述符zone_t,其字段中很多用于回收页框时使用。其实每个页描述符page都有到内存节点和到内存节点管理区的链接。那我们为啥看不到呢,原因是为了节省空间,这些链接的存放方式和典型的指针不同,是被编码成索引存放在flags字段的高位。

实际上,刻画页框的标志的数目是有限的,因此保留flags字段的最高位来编码内存节点和管理区是绰绰有余的。Linux提供page_zone()函数用来接收一个页描述符的地址作为它的参数;它读取该描述符中的flags字段的最高位,然后通过查看zone_table数组来确定相应管理区描述符的地址。顺便提一下,在系统启动时用,内核将所有内存节点的所有管理区描述符的地址放到这个zone_table数组里边。

当内核调用一个内存分配函数时,必须指明请求页框所在的管理区。内核通常指明它愿意使用哪个管理区。为了在内存分配请求中指定首选管理区,内核使用zonelist数据结构,这就是管理区描述符指针数组,在80x86中只有三个zone,所以zonelist数据结构中指向这三个zone的指针按照一定规则排列。如图,则zonelist数组就是这三个zone的排列组合。 例如,要分配一个用来做DMA的页框,则在指定zonelist数组中的某个zonelist元素中获得首选的zone,应该是ZONE_DMA,如果该区空间已使用完,就选ZONE_NORMA区,随后再是ZONE_HIGHMEM。