linux内核虚拟内存之物理内存

内存访问分为两种体系结构:一致性内存访问(UMA)和非一致性内存访问(NUMA)。NUMA指CPU对不同内存单元的访问时间可能不一样,因而这些物理内存被划分为几个节点,每个节点里的内存访问时间一致,NUMA体系结构主要存在大型机器、alpha等,嵌入式的基本都是UMA。UMA也使用了节点概念,只是永远都只有1个节点。本文讲的是UMA模型的嵌入式平台,linux版本:3.10.y。

         每个节点又将物理内存划分为3个管理区,在x86机器上管理区如下:

ZONE_DMA:0~16MB

ZONE_NORMAL:16MB~896MB

ZONE_HIGHMEM:896~末尾(对于64位,则不需要高端内存,虚拟地址足够直接映射)

而在hi3536嵌入式平台上实际的使用时由于系统mem没有超过896M,因此只有一个管理区ZONE_NORMAL(cat /proc/buddyinfo):

Node 0, zone   Normal     70    70     36     22    11     10     2      2      3     2     37

         每个管理区管理该内存区域内的所有页面(linux中每个页面的大小为4KB)。

         以下linux物理内存组织结构标准的关系图:

 

         在Documentation/arm/memory.txt文件中定义了arm平台的线性地址空间布局情况,其与物理内存关系图:

         当用户空间和内核空间比例3:1时,PAGE_OFFSET=0xC0000000。上面橙色虚线地址是一一映射关系,而高于highmem则需要vmalloc申请使用。

         PHYS_OFFSET=0x40000000

         ZRELADDR == virt_to_phys(PAGE_OFFSET +TEXT_OFFSET) = virt_to_phys (TEXT_ADDR) = 0x40008000

         INITRD_PHYS= 0x00800000

         PARAMS_PHYS= 0x00000100

         high_memory和VMALLOC_START之间保留了8M空间间隙

         high_memory=PAGE_OFFSET+ highmem

         当系统内存在0~896MB时,highmem=系统内存

         当系统内存>896MB时,highmem=896MB,多余的内存需要vmalloc映射访问

         VMALLOC_START和VMALLOC_END-1之间用于vmalloc() / ioremap() space映射。

         cat /proc/vmallocinfo:

         0xfb000000-0xfe190000 51970048iotable_init+0x0/0xb0 phys=10000000 ioremap

         映射了CPU寄存器空间

 

1、节点

         每个节点由struct pglist_data定义(include/linux/mmzone.h):

typedef struct pglist_data {

#define MAX_NR_ZONES 3

struct zonenode_zones[MAX_NR_ZONES]; //分别代表3个管理区

#define MAX_ZONELISTS 1

    struct zonelistnode_zonelists[MAX_ZONELISTS]; //按照分配时的管理区顺序排列,如果在ZONE_HIGHMEM中分配失败,就有可能还原成ZONE_NORMAL或ZONE_DMA。

int nr_zones;   //表示管理区的数目,值为1、2、3。

struct page *node_mem_map; //指向该节点第一个物理页面

    struct bootmem_data*bdata; //指向内存引导程序

         unsigned longnode_start_pfn; //该节点的起始页编号

    //calculate_node_totalpages中对以下两个值进行计算

unsigned long node_present_pages;/* total number of physical pages */

    unsigned longnode_spanned_pages; /* total size of physical pagerange, including holes */

    int node_id;      //节点号,在嵌入式上一般为0

    nodemask_treclaim_nodes;   /* Nodes allowed toreclaim from */

    wait_queue_head_tkswapd_wait;         //交换守护进程kswapd使用的等待队列

    wait_queue_head_tpfmemalloc_wait;

    struct task_struct*kswapd;  //指向交换守护进程描述符

    int kswapd_max_order;

enum zone_type classzone_idx; //管理区类型

} pg_data_t;

 

include/linux/mmzone.h中定义了全局节点:

extern struct pglist_data contig_page_data;

#define NODE_DATA(nid)     (&contig_page_data)

 

mm/bootmem.c进行全局节点定义:

struct pglist_data __refdata contig_page_data = {

    .bdata =&bootmem_node_data[0]

};

EXPORT_SYMBOL(contig_page_data);

 

2、管理区

         每个管理区由structzone描述(include/linux/mmzone.h):

struct zone {

unsigned longwatermark[NR_WMARK]; //该管理区的三个水平线值,min, low, high

unsigned long percpu_drift_mark;

unsigned long       lowmem_reserve[MAX_NR_ZONES]; //每个管理区必须保留的页框数

unsigned long       dirty_balance_reserve;

struct per_cpu_pageset __percpu*pageset;         //CPU的页面缓存

    spinlock_t      lock;     //保护该管理区的自旋锁

int            all_unreclaimable; /* All pagespinned */

struct free_area    free_area[MAX_ORDER]; //通过伙伴算法管理的空闲页面

 

ZONE_PADDING(_pad1_)

    spinlock_t      lru_lock;

    struct lruvec       lruvec;

    unsigned long       pages_scanned; //管理区回收页框时使用的计数器,记录上一次回收一同扫描过的页框

    unsigned long       flags;         /* zone flags, see below */

    /* Zone statistics */

    atomic_long_t       vm_stat[NR_VM_ZONE_STAT_ITEMS];

    unsigned int inactive_ratio;

ZONE_PADDING(_pad2_)

 

wait_queue_head_t   * wait_table; //进程等待的散列表,这些进程正在等待管理区中的某页

    unsigned long       wait_table_hash_nr_entries; //散列表数组的大小

    unsigned long       wait_table_bits; //散列表数组的大小对2取log的结果

 

    struct pglist_data  *zone_pgdat; //管理区属于的节点

    /* zone_start_pfn ==zone_start_paddr >> PAGE_SHIFT */

unsigned long       zone_start_pfn;  //管理区的起始页号

unsigned long       spanned_pages; //管理区的大小包括洞

unsigned long       present_pages; //管理区的大小不包括洞

unsigned long       managed_pages;

 

const char      *name; //管理区名字,DMA、NORMAL orHIGHMEM

}

         当系统中的可用内存很少时,守护程序kswapd被唤醒释放页面。每个管理区通过数组watermark来决定唤醒还是睡眠kswapd守护进程,这个数组通过下面的枚举来分别代表page_min, page_low, page_high,他们的之间的关系图如下图:

enum zone_watermarks {

    WMARK_MIN,

    WMARK_LOW,

    WMARK_HIGH,

    NR_WMARK

};

page_low:当空闲页面数达到pages_low时,伙伴算法分配器就会唤醒kswapd释放页面。

page_min:当达到pages_min时,kswap没唤醒则唤醒,同时同步进程释放内存,如果申请内存远远超过实际内存,就会出现out_of_memory。page_min包含里管理区最小保留内存,因此这是GFP_ATOMIC还可以分配出内存。

page_high:当释放页面达到这个值,认为该管理区已经平衡,kswapd开始睡眠。

         当多个进程对同一个页面进行IO操作(个人理解觉得更应该是有写操作)时,比如页面换入或换出,为了防止访问数据的不一致性,该页面会被锁住,而其他进程则通过wait_on_page()函数被添加到等待队列中。如果每个页面都有等待队列则系统会花费大量的内存存放,linux则是将等待队列存储在管理区的wait_table散列表中,这样就只有一个等待队列。其简单流程图如下:

当申请分配标志为GFP_ATOMIC时则不会睡眠,因为该标志不能睡眠的内存分配标志,用在中断处理程序、下半部、持有自旋锁以及其他不能睡眠的地方。

 

3、物理页面

         系统中的每个物理页面都由structpage用以记录该页面的状态,结构体包含了很多union,因为现在有slab/slob/slub三种分配器使用其中一种即可,定义如下(include/linux/mm_types.h):

struct page {

/* First double word block */

         unsigned long flags;         //存放页的状态

         struct address_space*mapping; //如果低位是clear的,则内存映射到文件或设备,它指向文件或设备节点inode的address_space,或者NULL;如果内存映射到匿名则低位被设置,指向匿名对象objects。

         /* Second double word*/

         struct {

        union {

            pgoff_tindex;     //页面是文件映射的一部分,它就是页面在文件中的偏移;页面是交换高速缓存一部分,它就是在交换地址空间中address_space的偏移量

            void*freelist;     //指向slub/slob第一个空闲对象

            boolpfmemalloc;   

        };

 

        union {

            unsigned counters;

            struct {

                union {

                    atomic_t_mapcount;

 

                    struct {/* SLUB */

                       unsigned inuse:16;

                       unsigned objects:15;

                       unsigned frozen:1;

                    };

                    intunits;  /* SLOB */

                };

                atomic_t _count; //页的引用计数

            };

        };

    };

 

    /* Third double word block*/

    union {

        struct list_headlru;   //最近最少使用(LRU)链表的链表首部

        struct {        /* slub per cpu partial pages */

            struct page*next;  /* Next partial slab */

            short int pages;

            short intpobjects;

        };

 

        struct list_headlist;  //slob列表

        struct slab *slab_page;/* slab fields */

};

    /* Remainder is not doubleword aligned */

    union {

        unsigned long private;

        struct kmem_cache*slab_cache;  /* SL[AU]B: Pointer to slab*/

        struct page*first_page;    /* Compound tail pages */

};

#if defined(WANT_PAGE_VIRTUAL)

    void *virtual;          /* Kernel virtual address (NULL if

                       notkmapped, ie. highmem) */

#endif /* WANT_PAGE_VIRTUAL */

}

         Flags的最高ZONES_SHIFT位记录该页面所属的管理区。set_page_zone函数设置页面的管理区。

 

4、源码解析

linux内核的内存管理分三个阶段。
A. 启动---->bootmem初始化完成为第一阶段。此阶段只能使用memblock_reserve函数分配内存。
B. bootmem初始化完--->buddy完成前。该阶段使用引导内存分配器(boot memoryallocator)分配内存。
C. 全部内存初始化完毕,可以用cache和buddy分配内存。

 

(1)~(4)点完成第一阶段,该阶段主要在start_kernel—> setup_arch函数实现:

(1)获取总内存

parse_early_param—> early_mem函数根据uboot传递进来的命令行参数mem=size@start计算出起始和内存大小通过arm_add_memory添加到meminfo全局数组里。

这里需要注意parse_early_param之前setup_machine_tags里parse_tags中的parse_tag_mem32也会解析uboot通过tags方法传递进来的系统内存,但是被early_mem覆盖重新计算。

 

(2)计算高端内存

         sanity_check_meminfo(arch/arm/mm/mmu.c)对meminfo里的内存进行判断,是否需要划分为高端内存。

 

(3)计算保留内存块

         保留内存块包括内核(数据段,代码段等)、页目录等占用的内存块。通过arm_memblock_init(arch/arm/mm/init.c)实现:

A. 将meminof内存通过memblock_add加到memblock.memory类型内存块里(包含全部物理内存)

B.通过memblock_reserve将保留内存添加到memblock.reserved类型内存块里

memblock_reserve(__pa(_stext), _end -_stext); 内核通过查看System.map可以看到

arm_mm_memblock_reserve(); //页表

最终结果显示如下:

MEMBLOCK configuration:

 memory size = 0xfa00000(250M) reservedsize = 0x6ba3cc

 memory.cnt = 0x1

 memory[0x0]   [0x00000040000000-0x0000004f9fffff], 0xfa00000 bytes

 reserved.cnt = 0x2

 reserved[0x0] [0x00000040004000-0x00000040007fff], 0x4000 bytes //页表

 reserved[0x1] [0x000000400081c0-0x000000406be58b], 0x6b63cc bytes //内核

 

在伙伴算法完成之前,内存的分配通过该方式进行。

 

(4)paging_init

         该函数arch/arm/mm/mmu.c实现,完成页表设置、管理区zone、设置零页等。

/*

 *paging_init() sets up the page tables, initialises the zone memory

 *maps, and sets up the zero page, bad page and bad page tables.

 */

void __init paging_init(struct machine_desc*mdesc)

{

   void *zero_page;

 

   memblock_set_current_limit(arm_lowmem_limit);

                                                                                                                                                 

build_mem_type_table();//建立各种类型页表的属性(内存MEMORY类型,设备DEVICE,中断向量表是HIGH_VECTORS),根据不同arm体系进行初始化,这是因为地址转换由MMU硬件单元根据页表处理,每个体系的MMU单元有差别(不是很明白)。

         /*以下见第二章页表管理*/

   prepare_page_table(); //清除在这之前建立的临时页表,以便下面建立正式链表

   map_lowmem(); //为低端内存建立一一的映射表(0~ arm_lowmem_limit),存放在swapper_pg_dir位置,用到前面的页表类型:MT_MEMORY

   dma_contiguous_remap(); //建立DMA映射表,类型为:MT_MEMORY_DMA_READY

   devicemaps_init(mdesc); //完成ffff0000开始的中断向量映射表,完成CPU IO映射表(从0x10000000~0x13190000就是控制器地址空间)、刷新一下TLB

   kmap_init(); //申请kmap高端内存永久映射的页表项,虚拟空间为2M(0xbfe00000 - 0xc0000000),如果未定义高端内存(CONFIG_HIGHMEM)什么也不做。

   tcm_init(); //do nothing

 

         //关于页表见第二章,此处返回中断向量表0xffff0000在pgd中的偏移量

   top_pmd = pmd_off_k(0xffff0000);

 

   /* allocate the zero page. */

   zero_page = early_alloc(PAGE_SIZE);

 

    bootmem_init(); //见下一节重点分析

 

         //一种特殊的页,供初始化为0的数据和写时复制使用

   empty_zero_page = virt_to_page(zero_page);

   __flush_dcache_page(NULL, empty_zero_page);

}

 

(4-1)bootmem_init

         在arch/arm/mm/init.c中定义,完成buddy管理内存需要的工作。

void __init bootmem_init(void)

{

unsigned longmin, max_low, max_high;

 

   max_low = max_high = 0;

find_limits(&min,&max_low, &max_high); //最小物理页号,低端内存最大物理页号,高端内存最大物理页号

/* 申请低端内存所需位图的空间,然后赋值给节点pgdat->bdatapgdat = NODE_DATA(0);只有一个节点,实际为一个全局变量contig_page_data)并将位图所在内存空间保留(memblock.reserved),bdata->node_bootmem_map指向位图空间(bdata struct bootmem_data);先将memblock.memory内存(所有物理内存)的位图清零—表示未使用,再将memblock.reserved 内存的位图置1—表示使用

node_bootmem_map:cf9f7000,该部分内存空间在建立伙伴系统的时候释放,所以放在内存的末端

*/

   arm_bootmem_init(min, max_low);

         arm_memory_present();  //do nothing

         sparse_init();//do nothing

         arm_bootmem_free(min, max_low, max_high); //见下一节介绍

max_low_pfn =max_low - PHYS_PFN_OFFSET; //低端内存的物理页数目

max_pfn = max_high- PHYS_PFN_OFFSET; //高端内存的物理页数目

}

 

(4-2)arm_bootmem_free

         该函数(arch/arm/mm/init.c)先计算zone_size和zhole_size,然后调用free_area_init_node(mm/page_alloc.c),因此这里主要分析free_area_init_node函数:

void __paginginit free_area_init_node(intnid, unsigned long *zones_size,

       unsigned long node_start_pfn, unsigned long *zholes_size)

{

   pg_data_t *pgdat = NODE_DATA(nid);

 

   pgdat->node_id = nid;

   pgdat->node_start_pfn = node_start_pfn;

init_zone_allows_reclaim(nid);

//计算节点的node_spanned_pages和node_present_pages,没有高端内存两者相等

   calculate_node_totalpages(pgdat, zones_size, zholes_size);

 

         //申请所有物理页描述结构体(structpage)所需要的内存空间,mem_map= NODE_DATA(0)->node_mem_map指向该空间

   alloc_node_mem_map(pgdat); //空间大小:page size:200000//2M

   printk("free_area_init_node: node %d, pgdat %08lx, node_mem_map%08lx\n",

           nid, (unsigned long)pgdat,  (unsigned long)pgdat->node_mem_map);

free_area_init_node:node 0, pgdat c06535c0(内核数据区), node_mem_map c06bf000(内核bss后面申请的一个内存地址)

    free_area_init_core(pgdat, zones_size, zholes_size); //见下节

}

 

(4-3)free_area_init_core

         该函数(mm/page_alloc.c)实现如下:

static void __paginginitfree_area_init_core(struct pglist_data *pgdat,

       unsigned long *zones_size, unsigned long *zholes_size)

{

   enum zone_type j;

   int nid = pgdat->node_id;

   unsigned long zone_start_pfn = pgdat->node_start_pfn;

   int ret;

 

         //初始化自旋锁和相关等待队列

   pgdat_resize_init(pgdat);

   init_waitqueue_head(&pgdat->kswapd_wait);

   init_waitqueue_head(&pgdat->pfmemalloc_wait);

pgdat_page_cgroup_init(pgdat);

//初始化各个管理区

   for (j = 0; j < MAX_NR_ZONES; j++) {

       struct zone *zone = pgdat->node_zones + j;

       unsigned long size, realsize, freesize, memmap_pages;

 

                   /*重新计算管理区的大小(减去struct page所占用的内存空间)*/

       size = zone_spanned_pages_in_node(nid, j, zones_size);

       realsize = freesize = size - zone_absent_pages_in_node(nid, j,

                                zholes_size);

       memmap_pages = calc_memmap_size(size, realsize);

       if (freesize >= memmap_pages) {

           freesize -= memmap_pages;

       }

      if (!is_highmem_idx(j))

           nr_kernel_pages += freesize;

       /* Charge for highmem memmap if there are enough kernel pages */

       else if (nr_kernel_pages > memmap_pages * 2)

           nr_kernel_pages -= memmap_pages;

       nr_all_pages += freesize;

 

       zone->spanned_pages = size;

       zone->present_pages = realsize;

       /*

        * Set an approximate value for lowmem here, it will be adjusted

        * when the bootmem allocator frees pages into the buddy system.

        * And all highmem pages will be managed by the buddy system.

        */

       zone->managed_pages = is_highmem_idx(j) ? realsize : freesize;

       zone->name = zone_names[j];

                   //初始化其他自旋锁

       spin_lock_init(&zone->lock);

       spin_lock_init(&zone->lru_lock);

       zone_seqlock_init(zone);

                   //指向节点

       zone->zone_pgdat = pgdat;

 

       zone_pcp_init(zone); //初始化CPU页面缓存

       lruvec_init(&zone->lruvec); //初始化lru

       if (!size)

           continue;

 

       set_pageblock_order();

       setup_usemap(pgdat, zone, zone_start_pfn, size);

                   //初始化该管理区的wait_table和free_area(伙伴算法)

       ret = init_currently_empty_zone(zone, zone_start_pfn,

                        size, MEMMAP_EARLY);

       BUG_ON(ret);

                   //初始化该管理区的所有物理页结构体struct page

       memmap_init(size, nid, j, zone_start_pfn);

       zone_start_pfn += size;

    }

}

 

(5)setup_per_cpu_areas

在init/main.c的start_kernel调用,在mm/percpu.c中定义,只有在SMP系统下才有用,在UP不做任何处理。

该函数主要是为每个处理器设置per-cpu数据区域,per_cpu数据由各个CPU独立使用,即使不锁访问,十分有效。per-cpu数据按照不同的CPU类型使用,以将性能低下引发的缓存一致性问题减小到最小。

 

(6)build_all_zonelists

         在init/main.c的start_kernel调用,在mm/page_alloc.c中定义。初始化每个节点内的zonelists。

 

(7)mm_init 设置内存分配器

         在init/main.c中定义,主要用于设设置伙伴算法内存分配器。源码如下:

static void __init mm_init(void)

{  

   /*

    * page_cgroup requires contiguous pages,

    * bigger than MAX_ORDER unless SPARSEMEM.

    */

   page_cgroup_init_flatmem();

    mem_init(); //见下面

   kmem_cache_init(); //建立kmem_cache和kmem_cache_node两个高速缓存(slab分配器使用)。

   percpu_init_late();

   pgtable_cache_init(); //do nothing

   vmalloc_init(); //vmalloc分配的内存虚拟地址连续,而物理地址无需连续,这里初始vmalloc要用的相关链表等准备工作。

}

 

void __init mem_init(void)

{

   unsigned long reserved_pages, free_pages;

   struct memblock_region *reg;

int i;

 

   max_mapnr   = pfn_to_page(max_pfn+ PHYS_PFN_OFFSET) - mem_map;

 

//以下两者都是释放空闲内存到伙伴系统:memblock的空闲内存,后者是bootmem的空闲内存,其实两者有交叠,伙伴系统会尝试与前后连续页框组成更大的页框块。

/* bootmem分配器核心就是node_bootmem_map这个位图,每一位代表这个node的一个页,当需要分配时就会扫描这个位图,然后获取一段物理页框进行分配,一般都会从开始处向后进行分配,并没有什么特殊的算法在其中。而伙伴系统初始化时页会根据这个位图,将位图中空闲的页释放回到伙伴系统中,而已经分配出去的页则不会在初始化阶段释放回伙伴系统,不过有可能会在系统运行过程中释放回伙伴系统中*/

    free_unused_memmap(&meminfo); //根据代码只有一个bank,所以没做啥事情

totalram_pages+= free_all_bootmem();

//释放高端内存到伙伴系统

   free_highpages();

 

   reserved_pages = free_pages = 0;

 

         //统计空闲内存和保留内存并打印出来

   for_each_bank(i, &meminfo) {

       struct membank *bank = &meminfo.bank[i];

       unsigned int pfn1, pfn2;

       struct page *page, *end;

 

        pfn1 = bank_pfn_start(bank);

       pfn2 = bank_pfn_end(bank);

 

       page = pfn_to_page(pfn1);

       end  = pfn_to_page(pfn2 - 1) + 1;

 

       do {

           if (PageReserved(page))

                reserved_pages++;

           else if (!page_count(page))

                free_pages++;

           page++;

       } while (page < end);

}

}

 

(8)伙伴算法建立后物理内存分布

经过上面源码分析后,下图是该阶段物理内存分布情况:

4000:内核全局页表swapper_pg_dir起始地址,共16K

8000~6be58c:内核存放空间(text\data\bss等)

6bf000~6bf000+200000:存放所有页框描述数据结构structpage

f9f7000:存放早期bootmem分配内存使用的位图空间起始地址,建立伙伴算法后会释放掉

916000~fa00000:空闲页表,全部有伙伴系统管理

前面的9304K保留内存永远不释放,也不归伙伴系统管理

 

内核信息打印如下:

Memory: 250MB = 250MB total

Memory: 246696k/246696k available, 9304k reserved, 0

                   Virtual kernel memory layout:

                       vector  : 0xffff0000 - 0xffff1000   (   4kB)

                       fixmap  : 0xfff00000 - 0xfffe0000   ( 896 kB)

                       vmalloc: 0xd0000000 - 0xff000000   ( 752 MB)

                       lowmem  : 0xc0000000 - 0xcfa00000   ( 250 MB)

                       pkmap   : 0xbfe00000 - 0xc0000000   (   2MB)

                       modules: 0xbf000000 - 0xbfe00000   (  14 MB)

                             .text: 0xc0008000 - 0xc05e85c8   (6018 kB)

                             .init: 0xc05e9000 - 0xc0617ec0   ( 188 kB)

                             .data: 0xc0618000 - 0xc06542c0   ( 241 kB)

                            .bss: 0xc06542c0 - 0xc06be58c   ( 425 kB)

  • 0
    点赞
  • 11
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值