Linux-3.14.12内存管理笔记【建立内核页表(1)】

摘要:
我们分析了英特尔的内存映射和Linux_32的基本用法。S只创建临时页表。内核仍然需要创建一个内核页表来实现全面的映射。e820_ end_ of _ ram_ pfn()函数的调用位置:start_ kernel()#init/main.c└─˃setup_arch()#arch/x86/kernel/setup.c├─˃e820_ end_ of _ ram_ pfn()#arch/x86/kernel/e820.c└─˃find_ low_ pfn_Range()#arch/x86/kernel/e820。其中find_low_pfn_Range()用于查找低端内存中的最大页数,max_low_pfn在这里初始化。

前面已经分析过了Intel的内存映射和linux的基本使用情况,已知head_32.S仅是建立临时页表,内核还是要建立内核页表,做到全面映射的。下面就基于RAM大于896MB,而小于4GB ,切CONFIG_HIGHMEM配置了高端内存的环境情况进行分析。

建立内核页表前奏,了解两个很关键的变量:

  • max_pfn:最大物理内存页面帧号;
  • max_low_pfn:低端内存区(直接映射空间区的内存)的最大可用页帧号;

max_pfn 的值来自setup_arch()中,setup_arch()函数中有:

max_pfn = e820_end_of_ram_pfn();

那么接下来看一下e820_end_of_ram_pfn()的实现:

804762

【file:/arch/x86/kernel/e820.c】
unsigned long __init e820_end_of_ram_pfn(void)
{
    return e820_end_pfn(MAX_ARCH_PFN, E820_RAM);
}

e820_end_of_ram_pfn()直接封装调用e820_end_pfn(),而其入参为MAX_ARCH_PFN和E820_RAM,其中MAX_ARCH_PFN的定义(x86的32bit环境)为:

#  define MAX_ARCH_PFN              (1ULL<<(32-PAGE_SHIFT))

最终值为0x100000,它表示的是4G物理内存的最大页面帧号;而E820_RAM为:

#define E820_RAM                 1

接下来看一下e820_end_pfn()函数实现:

【file:/arch/x86/kernel/e820.c】
/*
 * Find the highest page frame number we have available
 */
static unsigned long __init e820_end_pfn(unsigned long limit_pfn, unsigned type)
{
    int i;
    unsigned long last_pfn = 0;
    unsigned long max_arch_pfn = MAX_ARCH_PFN;
 
    for (i = 0; i < e820.nr_map; i++) {
        struct e820entry *ei = &e820.map[i];
        unsigned long start_pfn;
        unsigned long end_pfn;
 
        if (ei->type != type)
            continue;
 
        start_pfn = ei->addr >> PAGE_SHIFT;
        end_pfn = (ei->addr + ei->size) >> PAGE_SHIFT;
 
        if (start_pfn >= limit_pfn)
            continue;
        if (end_pfn > limit_pfn) {
            last_pfn = limit_pfn;
            break;
        }
        if (end_pfn > last_pfn)
            last_pfn = end_pfn;
    }
 
    if (last_pfn > max_arch_pfn)
        last_pfn = max_arch_pfn;
 
    printk(KERN_INFO "e820: last_pfn = %#lx max_arch_pfn = %#lx
",
             last_pfn, max_arch_pfn);
    return last_pfn;
}

这个函数用来查找最大物理的页面帧号,通过对e820图的内存块信息得到内存块的起始地址,将起始地址右移PAGE_SHIFT,算出其起始地址对应的页面帧号,如果足够大,超出了limit_pfn则设置最大页面帧号为limit_pfn,否则则设置为遍历中找到的最大的last_pfn。

e820_end_of_ram_pfn()函数的调用位置:

start_kernel()                           #init/main.c

└─>setup_arch()                        #arch/x86/kernel/setup.c

├─>e820_end_of_ram_pfn()              #arch/x86/kernel/e820.c

└─>find_low_pfn_range()               #arch/x86/kernel/e820.c

其中find_low_pfn_range()用于查找低端内存的最大页面数的 ,max_low_pfn则在这里面初始化。

find_low_pfn_range()代码实现:

【file:/arch/x86/mm/init_32.c】
/*
 * Determine low and high memory ranges:
 */
void __init find_low_pfn_range(void)
{
    /* it could update max_pfn */
 
    if (max_pfn <= MAXMEM_PFN)
        lowmem_pfn_init();
    else
        highmem_pfn_init();
}

函数实现很简单,根据max_pfn是否大于MAXMEM_PFN,从而判断是否初始化高端内存,也可以认为是启用。那么来看一下MAXMEM_PFN的宏定义:

(file:/arch/x86/include/asm/setup.h)

#define MAXMEM_PFN               PFN_DOWN(MAXMEM)

其中PFN_DOWN(x)的定义为:

(file:/include/linux/pfn.h)

#define PFN_DOWN(x)              ((x) >> PAGE_SHIFT)

PFN_DOWN(x)是用来返回小于x的最后一个页面号,对应的还有个PFN_UP(x)是用来返回大于x的第一个页面号,此外有个PFN_PHYS(x)返回的是x的物理页面号。接着看MAXMEM的定义:

(file:arch/x86/include/asm/pgtable_32_types.h)

#define MAXMEM                   (VMALLOC_END - PAGE_OFFSET - __VMALLOC_RESERVE)

那么VMALLOC_END的定义则为:

(file:arch/x86/include/asm/pgtable_32_types.h)

#define VMALLOC_END              (PKMAP_BASE - 2 * PAGE_SIZE)

//永久内存映射
#define PKMAP_BASE ((FIXADDR_BOOT_START - PAGE_SIZE * (LAST_PKMAP + 1)) & PMD_MASK)

其中PKMAP_BASE是永久映射空间的起始地址,LAST_PKMAP则是永久映射空间的映射页面数,定义为:

#define LAST_PKMAP 1024

另外PAGE_SHIFT和PAGE_SIZE的定义为:

#define PAGE_SHIFT               12
#define PAGE_SIZE                (_AC(1,UL) << PAGE_SHIFT)

而FIXADDR_BOOT_START是临时固定映射空间起始地址,其的相关宏定义:

临时内存映射:

#define FIXADDR_BOOT_SIZE        (__end_of_fixed_addresses << PAGE_SHIFT)

#define FIXADDR_BOOT_START       (FIXADDR_TOP - FIXADDR_BOOT_SIZE)

unsigned long __FIXADDR_TOP = 0xfffff000;

extern unsigned long __FIXADDR_TOP;

#define FIXADDR_TOP              ((unsigned long)__FIXADDR_TOP)

这里其中的__end_of_fixed_addresses是来自fixed_addresses枚举值,是固定映射的一个标志。此外这里的FIXADDR_TOP是固定映射区末尾,而另外还有一个这里未列出的FIXADDR_START,是固定映射区起始地址。

既然到此,顺便介绍一下内核空间映射情况。

image

内核空间如上图,分为直接内存映射区(低端内存,线性)和高端内存映射区。其中直接内存映射区是指3G到3G+896M的线性空间,直接对应物理地址就是0到896M(前提是有超过896M的物理内存),其中896M是high_memory值,使用kmalloc()/kfree()接口操作申请释放;

而高端内存映射区则是至超多896M物理内存的空间,它又分为动态映射区、永久映射区和固定映射区。

  • 动态内存映射区,又称之为vmalloc映射区或非连续映射区,是指VMALLOC_START到VMALLOC_END的地址空间,申请释放操作接口是vmalloc()/vfree(),通常用于将非连续的物理内存映射为连续的线性地址内存空间;
  • 而永久映射区,又称之为KMAP区或持久映射区,是指自PKMAP_BASE开始共LAST_PKMAP个页面大小的空间,操作接口是kmap()/kunmap(),用于将高端内存长久映射到内存虚拟地址空间中;
  • 最后的固定映射区,也称之为临时内核映射区,是指FIXADDR_START到FIXADDR_TOP的地址空间,操作接口是kmap_atomic()/kummap_atomic(),用于解决持久映射不能用于中断处理程序而增加的临时内核映射。

下图是根据个人的实验环境绘制的一张关于内核空间映射情况。

image

PMD_MASK涉及的宏定义:

(file:/include/asm-generic/pgtable-nopmd.h)

#define PMD_SHIFT                PUD_SHIFT

#define PMD_SIZE                 (1UL << PMD_SHIFT)

#define PMD_MASK                 (~(PMD_SIZE-1))

(file:/include/asm-generic/pgtable-nopud.h)

#define PUD_SHIFT                PGDIR_SHIFT

(file:arch/x86/include/asm/Pgtable-2level_types.h)

#define PGDIR_SHIFT              22

PMD_MASK计算结果是:0xFFC00000,其实是用于数据对齐而已。

已知PAGE_OFFSET默认的为0xC0000000,而__VMALLOC_RESERVE为:

unsigned int __VMALLOC_RESERVE = 128 << 20;

最后在个人的实验环境上,得出MAXMEM_PFN的值为0x377fe。

Linux是一个支持多硬件平台的操作系统,各种硬件芯片的分页并非固定的2级(页全局目录和页表),仅仅Intel处理器而言,就存在3级的情况(页全局目录、页中间目录和页表),而到了64位系统的时候就成了4级分页所以Linux为了保持良好的兼容性和移植性,系统设计成了以下的4级分页模型,根据平台环境和配置的情况,通过将页上级目录和页中间目录的索引位设置为0,从而隐藏了页三级目录和页中间目录的存在。也就是为什么存在PMD_SHIFT、PUD_SHIFT和PGDIR_SHIFT,还有pgtable-nopmd.h、pgtable-nopud.h和Pgtable-2level_types.h的原因了。

image

由此管中窥豹,看到了Linux内存分页映射模型的存在和相关设计,暂且也就先了解这么多。

分析宏是一件很乏味的事情,不过以小见大却是一件很有意思的事情。

【file:/arch/x86/mm/init_32.c】
/*

 * We have more RAM than fits into lowmem - we try to put it into
 * highmem, also taking the highmem=x boot parameter into account:
 */
static void __init highmem_pfn_init(void)
{
    max_low_pfn = MAXMEM_PFN;
 
    if (highmem_pages == -1)
        highmem_pages = max_pfn - MAXMEM_PFN;
 
    if (highmem_pages + MAXMEM_PFN < max_pfn)
        max_pfn = MAXMEM_PFN + highmem_pages;
 
    if (highmem_pages + MAXMEM_PFN > max_pfn) {
        printk(KERN_WARNING MSG_HIGHMEM_TOO_SMALL,
            pages_to_mb(max_pfn - MAXMEM_PFN),
            pages_to_mb(highmem_pages));
        highmem_pages = 0;
    }
#ifndef CONFIG_HIGHMEM
    /* Maximum memory usable is what is directly addressable */
    printk(KERN_WARNING "Warning only %ldMB will be used.
", MAXMEM>>20);
    if (max_pfn > MAX_NONPAE_PFN)
        printk(KERN_WARNING "Use a HIGHMEM64G enabled kernel.
");
    else
        printk(KERN_WARNING "Use a HIGHMEM enabled kernel.
");
    max_pfn = MAXMEM_PFN;
#else /* !CONFIG_HIGHMEM */
#ifndef CONFIG_HIGHMEM64G
    if (max_pfn > MAX_NONPAE_PFN) {
        max_pfn = MAX_NONPAE_PFN;
        printk(KERN_WARNING MSG_HIGHMEM_TRIMMED);
    }
#endif /* !CONFIG_HIGHMEM64G */
#endif /* !CONFIG_HIGHMEM */
}

highmem_pfn_init()看起来很长,貌似很复杂,实际上仅仅是把max_low_pfn设置为MAXMEM_PFN,而highmem_pages设置为max_pfn - MAXMEM_PFN,至于后面的几乎都是为了防止某些数据过大过小引起翻转而做的保障性工作。需要说明的是这里的max_low_pfn作为直接映射空间区的内存最大可用页帧号,并不是896M大小内存的页面数。896M只是定义高端内存的一个界限,至于直接映射内存大小只定义了不超过896M而已。

此外还有一个准备操作,在setup_arch()函数中调用的页表缓冲区申请操作:

early_alloc_pgt_buf():

【file:/arch/x86/mm/init.c】
void __init early_alloc_pgt_buf(void)
{
    unsigned long tables = INIT_PGT_BUF_SIZE;
    phys_addr_t base;
 
    base = __pa(extend_brk(tables, PAGE_SIZE));
 
    pgt_buf_start = base >> PAGE_SHIFT;
    pgt_buf_end = pgt_buf_start;
    pgt_buf_top = pgt_buf_start + (tables >> PAGE_SHIFT);
}

免责声明:文章转载自《Linux-3.14.12内存管理笔记【建立内核页表(1)】》仅用于学习参考。如对内容有疑问,请及时联系本站处理。

上篇枚举的使用方法《(学习笔记)两天进步一点点》(3)——应用BindingSource实现数据同步下篇

宿迁高防,2C2G15M,22元/月;香港BGP,2C5G5M,25元/月 雨云优惠码:MjYwNzM=

相关文章

Windows中像在Linux里一样使用CMake和make

1. 安装GCC环境 1.1 安装MinGW(Minimalist GNU for Windows) 首先下载MinGW,并安装。安装完成之后运行MinGW Installer。界面如下。勾选自己需要安装的包,然后点击Apply Changes执行。 其中除了一些必须的包之外,mingw32-make是执行make命令需要。 参考教程:https://z...

go语言——实现函数名到函数处理的映射关系(反射的应用)

模拟这样一个场景:当我们知道我们要执行操作的名字,让其作为参数传入,如何通过这个名字取执行相应的函数处理呢? 解决方法:(这里设定一个结构A,其变量a,实现相应函数为fun1、fun2)   ①:首先在编码过程中,我们可以将相应的处理函数进行“同一结构”实现,让其函数名和相应处理函数用map结构形成映射;   ②:通过创建该结构体的变量,通过反射(vf :...

Windows系统中CreateFileMapping实现的共享内存及用法

在32位的Windows系统中,每一个进程都有权访问他自己的4GB(232=4294967296)平面地址空间,没有段,没有选择符,没有near和far指针,没有near和far函数调用,也没有内存模式。 每个进程都有独立的4GB逻辑地址空间,32位的Windows系统允许每一个进程独立访问自己的内存,即独立于其它进程,也即它自己的32位逻辑地址空间。操作...

PowerDesigner将PDM导出生成WORD文档

PowerDesigner将PDM导出生成WORD文档 环境 PowerDesigner15 1.点击Report Temlates 制作模板 2.如果没有模板,单击New图标创建。有直接双击进入。 3.在弹出的类型(Type)对话框中想选择PBM(Physical data Model),如果有中文汉化包则选择simolified Chinese(中...

Linux驱动虚拟地址和物理地址的映射(转)

原文地址:http://blog.chinaunix.net/uid-20792373-id-2979673.html 参考链接: Linux 虚拟地址与物理地址的映射关系分析https://blog.csdn.net/ordeder/article/details/41630945 虚拟地址映射到物理地址的学习(linux篇)https://blog.c...

【Zynq UltraScale+ MPSoC解密学习7】Zynq UltraScale+的PMU

一、简单介绍1.1 概念PMU,platform measurement unit,平台管理单元。很多人会将它当做power measurement unit(电源管理单元)的缩写,认为就是管理电源和功耗的。其实PMU除了电源管理功能,还具有其他功能。官方描述如下: 系统启动前的初始化 电源管理 软件测试库执行(可选) 系统错误处理 1.2 总览在...