Linux内存管理:ARM Memory Layout以及mmu配置

http://blog.csdn.net/hongzg1982/article/details/47341881


在内核进行page初始化以及mmu配置之前,首先需要知道整个memory map。

1. ARM Memory Layout

Linux内存管理:ARM Memory Layout以及mmu配置_第1张图片

  1. PAGE_OFFSET
    Start address of Kernel space
    0xC000_0000

  2. lowmem
    Kernel direct-mapped RAM region (1:1 mapping)
    Maximum 896M

  3. HIGH_MEMORY
    End address of lowmem
    PAGE_OFFSET + MEMORY_SIZE

  4. pkmap
    用来把HIGHMEM page 永久映射到 kernel space
    2MB (这个大小每个平台不一样)
    kmap() / kunmap()

  5. Page gap
    To against out-of-bounds errors
    8MB

  6. vmalloc
    vmalloc() / ioremap() space

  7. DMA
    DMA memory mapping region

  8. Fixmap
    kmap()可能会进入睡眠,所以不能用在中断上下文等地方.
    所以Fixmap就是用于在中断上下文中把 highmem映射到内核空间的.
    Mapping HIGHMEM pages atomically
    kmap_atomic() :Fixmap在使用这个函数,所以可以在中断上下文中使用

  9. Vector
    CPU vectors are mapped here

  10. Modules
    Kernel modules inserted via insmod are placed here
    16MB (14MB, if HIGHMEM is enabled)

在内核初始化的时候,上面说的lowmemory中,还需要去除一些reserved memory。这些预留的内存是供一些外设使用的。下面来看一下预留内存的去除方式以及内核怎么读取预留的。
(这里不包含具体的内存分配内容,比如slab或者buddy系统等)。

2. 在bootloader判断物理内核地址范围之后,会修改相应的device tree节点。

以高通平台为例,bootloader中有如下函数会负责更新device tree中的memory node

int update_device_tree() {
    ...
    ret = fdt_path_offset(fdt, "/memory");
    offset = ret;
    ret = target_dev_tree_mem(fdt, offset);
    ...
}

“/memory”一般定义在sekeleton.dtsi,这也是为什么虽然skeleton.dtsi文件里边都是空的内容,但还是需要include这个文件的原因。

//skeleton64.dtsi
/ {
    #address-cells = <2>;
    #size-cells = <2>;
    cpus { };
    soc { };
    chosen { };
    aliases { };
    memory { device_type = "memory"; reg = <0 0 0 0>; };
};

然后在kernel里调用如下函数来读取memory大小等赋值给memblock变量:

setup_machine_fdt(){
    ...
    of_scan_flat_dt(early_init_dt_scan_memory, NULL);
    ...

}

int __init early_init_dt_scan_memory(unsigned long node, const char *uname,
                     int depth, void *data)
{
    const char *type = of_get_flat_dt_prop(node, "device_type", NULL);
    const __be32 *reg, *endp;
    int l;

    /* We are scanning "memory" nodes only */
    if (type == NULL) {
        /*
         * The longtrail doesn't have a device_type on the
         * /memory node, so look for the node called /memory@0.
         */
        if (!IS_ENABLED(CONFIG_PPC32) || depth != 1 || strcmp(uname, "memory@0") != 0)
            return 0;
    } else if (strcmp(type, "memory") != 0)
        return 0;

    reg = of_get_flat_dt_prop(node, "linux,usable-memory", &l);
    if (reg == NULL)
        reg = of_get_flat_dt_prop(node, "reg", &l);
    if (reg == NULL)
        return 0;

    endp = reg + (l / sizeof(__be32));

    pr_debug("memory scan node %s, reg size %d, data: %x %x %x %x,\n",
        uname, l, reg[0], reg[1], reg[2], reg[3]);

    while ((endp - reg) >= (dt_root_addr_cells + dt_root_size_cells)) {
        u64 base, size;

        base = dt_mem_next_cell(dt_root_addr_cells, ®);
        size = dt_mem_next_cell(dt_root_size_cells, ®);

        if (size == 0)
            continue;
        pr_debug(" - %llx ,  %llx\n", (unsigned long long)base,
            (unsigned long long)size);

        early_init_dt_add_memory_arch(base, size);
    }

    return 0;
}

3. 内核读到device tree节点之后,会把所有的内存范围保留在memblock里边。然后去掉所有预先保留的内存(比如高通msm平台预留给modem的内存等)。把内核分成lowmemory和highmemory等

在内核启动之后,

start_kernel()->setup_arch()->setup_arch()->sanity_check_meminfo()

的时候打印的memblock的内容为:

<6>[0.000000]  [0:wapper:0] sanity_check_meminfo memblock.memory.cnt=2
<6>[0.000000]  [0:wapper:0] pys_addr vmalloc_limit = 0xa9c00000
<6>[0.000000]  [0:wapper:0] count = 1 , reg->base =0x80000000 , reg->size =0x2fd00000
<6>[0.000000]  [0:wapper:0] count = 2 , reg->base =0xb0000000 , reg->size =0x30000000
<6>[0.000000]  [0:wapper:0] arm_lowmem_limit =0xa9c00000

内存分为两个CS:
CS1基地址为0x80000000,大小为0x30000000。
CS2基地址为0xb0000000,大小为0x30000000。
所以物理内存开始地址为0x800000000,总的大小为1.5GB。
但中间缺了0x2fd00000到0x30000000的3MB大小的内存,哪里去了??(应该是bootloader改的~~,预留了sec_debug相关的内存)

这段3MB里边,包含了sec_dbg的内容,但大小没有3MB这么大,其余的用作什么了还得查
<0>[0.000000] [0:swapper:0] sec_dbg_setup: str=@0xaff00008
<0>[0.000000] [0:swapper:0] sec_dbg_setup: secdbg_paddr = 0xaff00008
<0>[0.000000] [0:swapper:0] sec_dbg_setup: secdbg_size = 0x80000

之后会调用如下函数,读取memory相关的device tree内容,预留modem,audio等相关的内存:

setup_arch()->arm_memblock_init()->dma_contiguous_reserve()->dma_contiguous_early_removal_fixup()

这时打印的内容为:

<6>[0.000000]  [0:swapper:0] arm_lowmem_limit =0xa9c00000
<6>[0.000000]  [0:swapper:0] cma: Found external_image__region@0, memory base 0x85500000, size 19 MiB, limit 0xffffffff
<6>[0.000000]  [0:swapper:0] cma: Found modem_adsp_region@0, memory base 0x86800000, size 88 MiB, limit 0xffffffff
<6>[0.000000]  [0:swapper:0] cma: Found pheripheral_region@0, memory base 0x8c000000, size 6 MiB, limit 0xffffffff
<6>[0.000000]  [0:swapper:0] cma: Found venus_region@0, memory base 0x8c600000, size 5 MiB, limit 0xffffffff
<6>[0.000000]  [0:swapper:0] cma: Found secure_region@0, memory base 0x00000000, size 109 MiB, limit 0xffffffff
<6>[0.000000]  [0:swapper:0] cma: Found qseecom_region@0, memory base 0x00000000, size 13 MiB, limit 0xffffffff
<6>[0.000000]  [0:swapper:0] cma: Found audio_region@0, memory base 0x00000000, size 3 MiB, limit 0xffffffff
<6>[0.000000]  [0:swapper:0] cma: Found splash_region@8E000000, memory base 0x8e000000, size 20 MiB, limit 0xffffffff

读取的dts文件内容可以找到,,内容如下:

{
    memory {
        #address-cells = <2>;
        #size-cells = <2>;

/* Additionally Reserved 6MB for TIMA and Increased the TZ app size
 * by 2MB [total 8 MB ]
 */
        external_image_mem: external_image__region@0 {
            linux,reserve-contiguous-region;
            linux,reserve-region;
            linux,remove-completely;
            reg = <0x0 0x85500000 0x0 0x01300000>;
            label = "external_image_mem";
        };

        modem_adsp_mem: modem_adsp_region@0 {
            linux,reserve-contiguous-region;
            linux,reserve-region;
            linux,remove-completely;
            reg = <0x0 0x86800000 0x0 0x05800000>;
            label = "modem_adsp_mem";
        };

        peripheral_mem: pheripheral_region@0 {
            linux,reserve-contiguous-region;
            linux,reserve-region;
            linux,remove-completely;
            reg = <0x0 0x8C000000 0x0 0x0600000>;
            label = "peripheral_mem";
        };

        venus_mem: venus_region@0 {
            linux,reserve-contiguous-region;
            linux,reserve-region;
            linux,remove-completely;
            reg = <0x0 0x8C600000 0x0 0x0500000>;
            label = "venus_mem";
        };

        secure_mem: secure_region@0 {
            linux,reserve-contiguous-region;
            reg = <0 0 0 0x6D00000>;
            label = "secure_mem";
        };

        qseecom_mem: qseecom_region@0 {
            linux,reserve-contiguous-region;
            reg = <0 0 0 0xD00000>;
            label = "qseecom_mem";
        };

        audio_mem: audio_region@0 {
            linux,reserve-contiguous-region;
            reg = <0 0 0 0x314000>;
            label = "audio_mem";
        };

        cont_splash_mem: splash_region@8E000000 {
            linux,reserve-contiguous-region;
            linux,reserve-region;
            reg = <0x0 0x8E000000 0x0 0x1400000>;
            label = "cont_splash_mem";
        };
    };
};

之后在

setup_arch()->arm_memblock_init()->dma_contiguous_reserve()->dma_contiguous_early_removal_fixup()还会调用一次sanity_check_meminfo()函数

这时打印的内容变成了

<6>[0.000000]  [0:swapper:0] pys_addr vmalloc_limit = 0xa9c00000
<6>[0.000000]  [0:swapper:0] count = 1 , reg->base =0x80000000 , reg->size =0x5500000
<6>[0.000000]  [0:swapper:0] count = 2 , reg->base =0x8cb00000 , reg->size =0x23200000
<6>[0.000000]  [0:swapper:0] count = 3 , reg->base =0xb0000000 , reg->size =0x30000000
<6>[0.000000]  [0:swapper:0] arm_lowmem_limit =0xb1200000

比较两次调用sanity_check_meminfo()函数打印的log,可以看到扣除的内存范围,这些里边只有external_image_mem,modem_adsp_mem,peripheral_mem,venus_mem这几个被扣除了。
后面的secure_region,qseecom_region,audio_region,splash_region哪去了??(这部分被ion memory预留!!)

以下是扣除的内容

external_image_mem: 0x85500000~0x86800000 大小为 19MB
modem_adsp_mem :0x86800000 ~0x8C000000 大小为 88MB
peripheral_mem : 0x8C000000 ~ 0x8C600000 大小为6MB
venus_mem:0x8c600000 ~ 0x8cb00000 大小为5MB
secure_mem : 0xd9000000~ 0xe0000000 大小为112MB //这个与上面的109MB相比大小被调整,为什么?
qseecom_region : 0xd8000000 ~ 0xd9000000 大小为16MB////这个与上面的109MB相比大小也被调整,为什么?
audio_mem : 0xd7c00000 大小为4MB//大小被调整
splash_region : 0x8E000000~ 0x8F400000 大小为20MB
default region :0xa9400000 ~ 0xa9c00000 大小为8MB

external_image_mem,modem_adsp_mem,peripheral_mem,venus_mem这些被扣除前后,memblock的
内容如下:

<6>[0.000000]  [0:swapper:0] count = 1 , reg->base =0x80000000 , reg->size =0x2fd00000
<6>[0.000000]  [0:swapper:0] count = 2 , reg->base =0xb0000000 , reg->size =0x30000000
//第一次打印的时候是这样的,第二次打印就变成下面这样了

<6>[0.000000]  [0:swapper:0] count = 1 , reg->base =0x80000000 , reg->size =0x5500000
<6>[0.000000]  [0:swapper:0] count = 2 , reg->base =0x8cb00000 , reg->size =0x23200000
<6>[0.000000]  [0:swapper:0] count = 3 , reg->base =0xb0000000 , reg->size =0x30000000
<6>[0.000000]  [0:swapper:0] arm_lowmem_limit =0xb1200000
//vmalloc被cmdline设置为了340MB,所以vmalloc_limit= 0xb1200000
//(0xff000000 - 0x15400000(340MB)的值,也就是从0xff00000开始减去vmalloc大小得到的值)。
//这个值被调整完之后变成arm_lowmem_limit = 0xa9c00000。 
//但第二次被sanity_check_meminfo()函数打印的时候被调整成了0xb1200000,怎么调整的??
//arm_lowmem_limit这个是最终划分Lowmemory和其他vmalloc区域的标准。
//从下面的可以看到lowmemory地址最大的区域就是0xf000000~0xf120000。最大地址就到0xf1200000,和arm_lowmem_limit是一样的。
//highmemory的开始地址是high_memory的值,大小如下:
//high_memory = __va(arm_lowmem_limit - 1) + 1; 
//这个值加上VMALLOC_OFFSET即为vmalloc的开始地址
//#define VMALLOC_START ((unsigned long)high_memory + VMALLOC_OFFSET)
//VMALLOC_OFFSET一般为8MB

整个内存的示意图
这里写图片描述

<6>[0.000000]  [0:swapper:    0] Memory: 1243908K/1448960K available (10539K kernel code, 1363K rwdata, 4472K rodata, 1417K init, 5844K bss, 205052K reserved, 632832K highmem)
<6>[0.000000]  [0:swapper:    0] Virtual kernel memory layout:
<6>[0.000000]  [0:swapper:    0]     vector  : 0xffff0000 - 0xffff1000   (   4 kB)
<6>[0.000000]  [0:swapper:    0]     fixmap  : 0xfff00000 - 0xfffe0000   ( 896 kB)
<6>[0.000000]  [0:swapper:    0]     arm_lowmem_limit = 0xf1200000 
<6>[0.000000]  [0:swapper:    0]  
<6>[0.000000]  [0:swapper:    0]       start_phys  : 0xf0000000    end_phys :  0x20000000  
<6>[0.000000]  [0:swapper:    0]       vmalloc : 0xf1200000 - 0xff000000   ( 222 MB)
<6>[0.000000]  [0:swapper:    0]       lowmem  : 0xf0000000 - 0xf1200000   (  18 MB)
<6>[0.000000]  [0:swapper:    0]       start_phys  : 0xccb00000    end_phys :  0xefd00000  
<6>[0.000000]  [0:swapper:    0]       vmalloc : 0xefd00000 - 0xf0000000   (   3 MB)
<6>[0.000000]  [0:swapper:    0]       lowmem  : 0xccb00000 - 0xefd00000   ( 562 MB)
<6>[0.000000]  [0:swapper:    0]       start_phys  : 0xc0000000    end_phys :  0xc5500000  
<6>[0.000000]  [0:swapper:    0]       vmalloc : 0xc5500000 - 0xccb00000   ( 118 MB)
<6>[0.000000]  [0:swapper:    0]       lowmem  : 0xc0000000 - 0xc5500000   (  85 MB)
<6>[0.000000]  [0:swapper:    0]       pkmap   : 0xbfe00000 - 0xc0000000   (   2 MB)
<6>[0.000000]  [0:swapper:    0]       modules : 0xbf000000 - 0xbfe00000   (  14 MB)
<6>[0.000000]  [0:swapper:    0]       .text : 0xc0008000 - 0xc0fa8ec4   (16004 kB)
<6>[0.000000]  [0:swapper:    0]       .init : 0xc1000000 - 0xc1162480   (1418 kB)
<6>[0.000000]  [0:swapper:    0]       .data : 0xc1164000 - 0xc12b8de4   (1364 kB)
<6>[0.000000]  [0:swapper:    0]        .bss : 0xc12c1b3c - 0xc1876b78   (5845 kB)

contig_page_data里边node_zones的Normal和HighMem的
zone_start_pfn,spanned_pages正好对应上面的地址。

Normal:
        zone_start_pfn = 0x80000000
        zone_start_pfn加上spanned_pages的个数,算一下地址正好是arm_lowmem_limit的值
HighMem:
    zone_start_pfn的值也是正好等于arm_lowmem_limit的值。
    zone_start_pfn加上spanned_pages的值也正好等于0xE0000000

4. 根据上述处理之后,内核得到可用的内存大小以及范围。然后通过mmu配置等,做内存分页(paging)。

不管是x86架构还是ARM架构,现在大部分CPU访问内存,一般通过MMU来实现虚拟内存和物理内存的转换。
以下是一个简单的示意图。(如果要详细分析的话,要看MMU分几层,每个page大小怎么配置等等!!参考ARM架构的书)
Linux内存管理:ARM Memory Layout以及mmu配置_第2张图片

在ARM平台,二级页表和三级页表可以选择用。但目前为止没有见过三级页表的,所以略过三级页表,只看一下二级页表的。

//在/kernel/arch/arm/include/asm/pgtable.h文件里边
#ifdef CONFIG_ARM_LPAE 
#include 
#else
#include 
#endif

设置一个page大小。这里先略过去寄存器的设置以及page大小类型等。这部分可以参考arm developer’s guide。
先看一下Linux里边在哪里定义page大小的。

//kernel/include/asm-generic/page.h文件里边
#define PAGE_SHIFT  12
#define PAGE_SIZE   (1UL << PAGE_SHIFT)
//12是最常看到的4k大小的page。

以ARM二级页表为例,一级页表和二级页表的种类有两种。

//page大小为4K,按下面的组织方式都可以map最大4G的内存地址空间。
1. 一级页表是4096,二级页表是256
2. 一级页表是2048,二级页表是512
//在ARM Linux中,分别定义了PTRS_PER_PGD,PTRS_PER_PMD,PTRS_PER_PTE分别表示原本三级的页表,但如果是二级页表的话。这三个值分别定义为如下:
#define PTRS_PER_PTE        512
#define PTRS_PER_PMD        1
#define PTRS_PER_PGD        2048
//上面的值正好对应1级页表2048,二级页表512的组织方式。二级页表中,PUD,PMD没有用。

//一级页表4096,二级页表256这样的配置,就可以定义成如下:
#define PTRS_PER_PTE        256
#define PTRS_PER_PMD        1
#define PTRS_PER_PGD        4096

页表的示意图如下:

Linux内存管理:ARM Memory Layout以及mmu配置_第3张图片

create_mapping()函数具体负责页表的生成。

//create_mapping()有几个调用路径
1. devicemaps_init()->create_mapping()
2. map_lowmem()->create_mapping()
3. iotable_init()->create_mapping()
4. debug_ll_io_init()->create_mapping()

可以看一下create_mapping()函数怎么按照物理和对应的虚拟内存,构建页表。

  1. Present flag: 表示pte指向的page地址已经存在于内存当中。linux内核中pte_present()函数来检查present flag。
  2. Accessed flag : Set each time the paging unit addresses the corresponding page frame. This flag may be
    used by the operating system when selecting pages to be swapped out. The paging unit
    never resets this flag; this must be done by the operating system
  3. Dirty flag : Applies only to the Page Table entries. It is set each time a write operation is performed on
    the page frame. As with the Accessed flag, Dirty may be used by the operating system
    when selecting pages to be swapped out. The paging unit never resets this flag; this must
    be done by the operating system.
  4. Read/Write flag : Contains the access right (Read/Write or Read) of the page or of the Page Table (see the
    section “Hardware Protection Scheme” later in this chapter)
  5. User/Supervisor flag : Contains the privilege level required to access the page or Page Table (see the later section
    “Hardware Protection Scheme”).
  6. PCD and PWT flags : Controls the way the page or Page Table is handled by the hardware cache (see the section
    “Hardware Cache” later in this chapter).
  7. Page Size flag : Applies only to Page Directory entries. If it is set, the entry refers to a 2 MB- or 4 MB-long
    page frame (see the following sections).
  8. Global flag : Applies only to Page Table entries. This flag was introduced in the Pentium Pro to prevent
    frequently used pages from being flushed from the TLB cache (see the section “Translation
    Lookaside Buffers (TLB)” later in this chapter). It works only if the Page Global Enable (PGE)
    flag of register cr4 is set.

关于这部分的操作,将在详解函数do_page_fault()函数的时候说明~~

下面举一个例子看一下某个task访问某个虚拟地址是怎么一步一步转成物理地址的。

Linux内核进程,访问的地址都是内核范围之内的,只要做一个简单的偏移就可以在物理地址和虚拟地址之间进行转换,就不多说了。
用户进程,其page table的地址,都会保存在其task struct的mm或者active_mm的pgd中。可以根据这个地址,按照页表的分配方式来算。
Linux内存管理:ARM Memory Layout以及mmu配置_第4张图片

从用户进程的task_struct中可以知道pgd的地址,当然页表分配方式上面已经讲了,这里是4096,256的分配方式。如果这个进程中,访问的虚拟地址是0x01206000。按照下面的方式可以算出来是0x578DB000。
Linux内存管理:ARM Memory Layout以及mmu配置_第5张图片

按照ARM Developer’s Guide中的图,来看一下是怎么一步一步算出来的。

Linux内存管理:ARM Memory Layout以及mmu配置_第6张图片

  1. 虚拟地址是:0x01206000
  2. Translation table base addre就是pgd的地址,也就是一级页表的地址(保存在协处理器CP15:C2中),从上面的task_struct->active_mm->pgd可以看到就是0xDD318000
  3. 虚拟地址0x01206000 * 0xFFF000000 ,这个是取虚拟地址前面12bit,然后右移20位,就是0x12,等于18。这个值要乘以4,加上pgd地址。因为第一级页表有4096个,页表的每一个项是4个字节,所以就要乘以4。故,要取的地址就是0xDD318048。这个地址里边的值就是0x53C6381。这个值乘以0xFFFFFF00就是第二级页表的基地址0x53C6300。
  4. 取0x01206000虚拟地址的中间8bit,右移8位,然后乘以4,加到上面算出来的二级页表基地址0x53C6300这个上面去。算出来的值就是0x53C6318。这个地址的值是0x578DBC7F。
  5. 0x578DBC7F * 0xFFFFF000 加上虚拟地址*0x00000FFF的值,就是0x578DB000。这个就是最终要访问的物理地址。

用户进程的内存管理

Linux内存管理:ARM Memory Layout以及mmu配置_第7张图片
1. 进程数据结构: task_struct
2. 进程内存管理数据结构: mm_struct
mmap: 进程分配的所有内存的链表头
pgd: page global directory 的地址
3. 进程分配的内存,由vm_area_struct管理
vm_start and vm_end: 虚拟内存的开始地址和结束地址

下图是用户进程访问的虚拟内存通过pgd转换成物理地址的示意图,在前面已经详细讲过:
Linux内存管理:ARM Memory Layout以及mmu配置_第8张图片


你可能感兴趣的:(Linux知识)