1.进程页表项中的内核部分是各进程复制的,并且采取了延迟更新的方式:
以vmalloc为例(最常使用),这部分区域对应的线性地址在内核使用vmalloc分配内存时,其实就已经分配了相应的物理内存,并做了相应的映射,建立了相应的页表项,但相关页表项仅写入了“内核页表”,并没有实时更新到“进程页表中”,内核在这里使用了“延迟更新”的策略,将“进程页表”真正更新推迟到第一次访问相关线性地址,发生page fault时,此时在page fault的处理流程中进行“进程页表”的更新:
A. 如果页表、页目录都在在内核空间的低端内存中,那么:
内核通过cr3能获得全局页目录中的物理地址,由于低端内存的线性映射,内核就能据此算出页目录的虚拟地址,进而实现对页目录的读写;同理,根据页目录中的内容可以获得页表的物理地址,如果页表也在内核空间的低端内存中,那么根据线性映射的偏移也能算出页表的虚拟地址,这样就能对页表进行读写。看似行的通,但是,
如果所有进程的页表都存放在低端内存,那进程数量很多时低端内存肯定不堪重负。
B. 如果页表在用户空间(或在内核空间的非线性映射区):
即使通过页目录读取到了页表的物理地址,由于内核只能通过虚拟地址对实际的内存进行访问,所以内核还是无法对页表中的内容进行读写,因为内核不知道页表的虚拟地址(这时已经不能用物理地址减去一个偏移量来计算虚拟地址了)。
所以,页表没有必要放到内核空间的低端内存中。
但是对于页目录,似乎必须放在内核空间。
// 全局页目录、二级页目录和三级页目录的访问(即获取其虚拟地址)都依赖地址的线性映射,因此课件
// 这三级目录表都应放在内核空间的低端内存中。只有最后一级目录即页表本身可以不在低端内存中。
因为每一个外设都是通过总线(内部总线+外部总线)与处理器建立联系,每个外设在内存中都用规定的地址,处理器通过地址,即可与外设取的联系。
![](https://i-blog.csdnimg.cn/blog_migrate/9f67fd16647d3462cd21189f45a0c3c9.png)
Linux内存管理(2):内存描述
内存管理是os中最复杂的管理机制之一。linux中采用了很多有效的管理方法,包括页表管理、高端内存(临时映射区、固定映射区、永久映射区、非连续内存区)管理、为减小外部碎片的伙伴系统、为减小内部碎片的slab机制、伙伴系统未建立之前的页面分配制度以及紧急内存管理等等。
linux使用于广泛的体系结构,因此需要用一种与体系结构无关的方式来描述内存。linux用VM描述和管理内存。在VM中使用的普遍概念就是非一致内存访问。对于大型机器而言,内存会分成许多簇,依据簇与处理器“距离”的不同,访问不同的簇会有不同的代价。每个簇都被认为是一个节点(pg_data_t),每个节点被分成很多的称为管理区(zone)的块,用于表示内存中的某个范围。zone的类型除了ZONE_DMA,ZONE_NORMAL,ZONE_HIGHMEM以外,从linux2.6.32开始引入了ZONE_MOVABLE,用于适应大块连续内存的分配。每个物理页面由一个page结构体描述,所有的页结构都存储在一个全局的mem_map数组中(非平板模式),该数组通常存放在ZONE_NORMAL内存区域的首部,或者就在内存系统中为装入内核映像而预留的区域之后。内存描述的层次结构为pg_data_t--->zone--->mem_map数组(ZONE_XXX类型)--->page,如下图。下面的以2.6.32.45的内核代码为参考来介绍。
图1 内存描述的层次结构
1、节点:pg_data_t内存的每个节点都有pg_data_t描述,在分配一个页面时,linux采用节点局部分配的策略,从最靠近运行中的CPU的节点分配内存。由于进程往往是在同一个CPU上运行,因此从当前节点得到的内存很可能被用到。pg_data_t在include/linux/mmzone.h中,如下:
- /*
- * pg_data_t结构用在带有CONFIG_DISCONTIGMEM编译选项的机器中(最新的NUMA机器),
- * 以表示比zone结构更高一层次的内存区域。
- * 在NUMA机器上,每个NUMA节点由一个pg_data_t来描述它的内存布局。内存使用统计和
- * 页面交换数据结构由每个zone区域来维护
- */
- struct bootmem_data;
- typedef struct pglist_data {
- /* 该节点内的内存区。可能的区域类型用zone_type表示 */
- struct zone node_zones[MAX_NR_ZONES];
- /* 该节点的备用内存区。当节点没有可用内存时,就从备用区中分配内存 */
- struct zonelist node_zonelists[MAX_ZONELISTS];
- /* 可用内存区数目,即node_zones数据中保存的最后一个有效区域的索引 */
- int nr_zones;
- #ifdef CONFIG_FLAT_NODE_MEM_MAP /* means !SPARSEMEM */
- /* 在平坦型的内存模型中,它指向本节点第一个页面的描述符 */
- struct page *node_mem_map;
- #ifdef CONFIG_CGROUP_MEM_RES_CTLR
- /* cgroup相关 */
- struct page_cgroup *node_page_cgroup;
- #endif
- #endif
- /* 在内存子系统初始化以前,即boot阶段也需要进行内存管理。
- * 此结构用于这个阶段的内存管理。
- */
- struct bootmem_data *bdata;
- #ifdef CONFIG_MEMORY_HOTPLUG
- /* 当系统支持内存热插拨时,这个锁用于保护本结构中的与节点大小相关的字段。
- * 当你希望node_start_pfn,node_present_pages,node_spanned_pages仍保持常量时,
- * 需要持有该锁。
- */
- spinlock_t node_size_lock;
- #endif
- unsigned long node_start_pfn; /*起始页面帧号,指出该节点在全局mem_map中的偏移*/
- unsigned long node_present_pages; /* 物理页的总数 */
- unsigned long node_spanned_pages; /* 物理页范围的跨度,包括holes */
- int node_id; /* 节点编号 */
- /* 等待该节点内的交换守护进程的等待队列。将节点中的页帧换出时会用到 */
- wait_queue_head_t kswapd_wait;
- /* 负责该节点的交换守护进程 */
- struct task_struct *kswapd;
- /* 由页交换子系统使用,定义要释放的区域大小 */
- int kswapd_max_order;
- } pg_data_t;
2、管理区:zone
管理区用于跟踪诸如页面使用情况统计数,空闲区域信息和锁信息等。每个管理区由一个zone结构体描述,管理区的类型由zone_type描述,都在include/linux/mmzone.h中。如下:
- enum zone_type {
- #ifdef CONFIG_ZONE_DMA
- /*
- * ZONE_DMA is used when there are devices that are not able
- * to do DMA to all of addressable memory (ZONE_NORMAL). Then we
- * carve out the portion of memory that is needed for these devices.
- * The range is arch specific.
- *
- * Some examples
- *
- * Architecture Limit
- * ---------------------------
- * parisc, ia64, sparc <4G
- * s390 <2G
- * arm Various
- * alpha Unlimited or 0-16MB.
- *
- * i386, x86_64 and multiple other arches
- * <16M.
- */
- ZONE_DMA,
- #endif
- #ifdef CONFIG_ZONE_DMA32
- /*
- * x86_64 needs two ZONE_DMAs because it supports devices that are
- * only able to do DMA to the lower 16M but also 32 bit devices that
- * can only do DMA areas below 4G.
- */
- ZONE_DMA32,
- #endif
- /*
- * Normal addressable memory is in ZONE_NORMAL. DMA operations can be
- * performed on pages in ZONE_NORMAL if the DMA devices support
- * transfers to all addressable memory.
- */
- ZONE_NORMAL,
- #ifdef CONFIG_HIGHMEM
- /*
- * A memory area that is only addressable by the kernel through
- * mapping portions into its own address space. This is for example
- * used by i386 to allow the kernel to address the memory beyond
- * 900MB. The kernel will set up special mappings (page
- * table entries on i386) for each page that the kernel needs to
- * access.
- */
- ZONE_HIGHMEM,
- #endif
- ZONE_MOVABLE,
- __MAX_NR_ZONES
- };
(1)ZONE_DMA:用在当有设备不能通过DMA访问整个可寻址内存(ZONE_NORMAL)的情况下。这时我们就要为这些设备专门开辟出一段内存,通常是低端内存区域。ZONE_DMA的内存范围与体系结构有关,parisc、ia64以及sparc中是小于4G;s390是小于2G;arm中是可变的多种多样的;alpha中是无限或者0-16MB;i386、x86_64以及其他很多体系结构是小于16MB(0-16MB)。
(2)ZONE_DMA32:注意x86_64需要两个ZONE_DMA区域,因为它既支持只能访问16MB以下DMA区域的设备,也支持只能访问4GB以下DMA区域的32位设备,ZONE_DMA32针对后一种情况。
(3)ZONE_NORMAL:正常的可访问内存。如果DMA设备能支持传输数据到整个可访问内存,则DMA操作也能在ZONE_NORMAL类型的页面上进行。
(4)ZONE_HIGHMEM:映射到内核代码本身的内核地址空间,一般是高端内存区域,它只能由内核访问,用户空间访问不到。所有的内核操作都只能使用这个内存区域来进行,因此这是对性能至关重要的区域。例如i386允许内核访问超过900MB的内存,对每个内核需要访问的页面,内核将设置特别的映射项(i386上的页表项)。
(5)ZONE_MOVABLE:这是一个伪内存段。为了防止形成物理内存碎片,可以将虚拟地址对应的物理地址进行迁移,使多个碎片合并成一块连续的大内存。ZONE_MOVABLE类型用于适应大块连续内存的分配。
- struct zone {
- /* 被页面分配器访问的通用域 */
- /* 本管理区的三个水线值:高水线(比较充足)、低水线、MIN水线。会被*_wmark_pages(zone)宏访问 */
- unsigned long watermark[NR_WMARK];
- /* 当可用页数在本水线值以下时,在读取可用页计数值时,需要增加额外的工作以避免每个CPU的计数器
- * 漂移导致水线值被打破
- */
- unsigned long percpu_drift_mark;
- /* 我们不知道即将分配的内存是否可用,以及最终是否会被释放,因此为了避免浪费几GB的RAM,我们
- * 必须额外保留一些低端区域的内存(如DMA区域)供驱动使用。否则我们会面临在低端区域内出现
- * OOM(Out of Memory)的风险,尽管这时高端区域还有大量可用的RAM。本字段是指从上级内存区
- * 退到回内存区时,需要额外保留的内存数量。如果在运行时sysctl_lowmem_reserve_ratio控制
- * 改变,它会被重新计算
- */
- unsigned long lowmem_reserve[MAX_NR_ZONES];
- #ifdef CONFIG_NUMA
- int node; /* 所属的NUMA节点 */
- /* 未映射的页(即可回收的页)超过此值,将进行页面回收 */
- unsigned long min_unmapped_pages;
- /* 管理区中用于slab的可回收页大于此值时,将回收slab中的缓存页 */
- unsigned long min_slab_pages;
- /*
- * 每CPU的页面缓存。
- * 当分配单个页面时,首先从该缓存中分配页面。这样可以:
- * 避免使用全局的锁
- * 避免同一个页面反复被不同的CPU分配,引起缓存页的失效。
- * 避免将管理区中的大块分割成碎片。
- */
- struct per_cpu_pageset *pageset[NR_CPUS];
- #else
- struct per_cpu_pageset pageset[NR_CPUS];
- #endif
- /* 该锁用于保护伙伴系统数据结构。即保护free_area相关数据 */
- spinlock_t lock;
- #ifdef CONFIG_MEMORY_HOTPLUG
- /* 用于保护spanned/present_pages等变量。这些变量几乎不会发生变化,除非发生了内存热插拨操作。
- * 这几个变量并不被lock字段保护。并且主要用于读,因此使用读写锁 */
- seqlock_t span_seqlock;
- #endif
- /* 伙伴系统的主要变量。这个数组定义了11个队列,每个队列中的元素都是大小为2^n的页面 */
- struct free_area free_area[MAX_ORDER];
- #ifndef CONFIG_SPARSEMEM
- /* 本管理区里的pageblock_nr_pages块标志数组,参考pageblock-flags.h
- * 在SPARSEMEM中,本映射存储在结构mem_section中 */
- unsigned long *pageblock_flags;
- #endif /* CONFIG_SPARSEMEM */
- /* 填充的未用字段,确保后面的字段是缓存行对齐的 */
- ZONE_PADDING(_pad1_)
- /* 被页面回收扫描器访问的通用域 */
- /*
- * lru相关的字段用于内存回收。这个锁用于保护这几个回收相关的字段。
- * lru用于确定哪些字段是活跃的,哪些不是活跃的,并据此确定应当被写回到磁盘以释放内存。
- */
- spinlock_t lru_lock;
- /* 匿名活动页、匿名不活动页、文件活动页、文件不活动页链表头 */
- struct zone_lru {
- struct list_head list;
- } lru[NR_LRU_LISTS];
- struct zone_reclaim_stat reclaim_stat; /* 页面回收状态 */
- /* 自从最后一次回收页面以来,扫过的页面数 */
- unsigned long pages_scanned;
- unsigned long flags; /* 管理区标志,参考下面 */
- /* Zone statistics */
- atomic_long_t vm_stat[NR_VM_ZONE_STAT_ITEMS];
- /*
- * prev_priority holds the scanning priority for this zone. It is
- * defined as the scanning priority at which we achieved our reclaim
- * target at the previous try_to_free_pages() or balance_pgdat()
- * invokation.
- *
- * We use prev_priority as a measure of how much stress page reclaim is
- * under - it drives the swappiness decision: whether to unmap mapped
- * pages.
- *
- * Access to both this field is quite racy even on uniprocessor. But
- * it is expected to average out OK.
- */
- int prev_priority;
- /*
- * The target ratio of ACTIVE_ANON to INACTIVE_ANON pages on
- * this zone's LRU. Maintained by the pageout code.
- */
- unsigned int inactive_ratio;
- /* 为cache对齐 */
- ZONE_PADDING(_pad2_)
- /* Rarely used or read-mostly fields */
- /*
- * wait_table -- the array holding the hash table
- * wait_table_hash_nr_entries -- the size of the hash table array
- * wait_table_bits -- wait_table_size == (1 << wait_table_bits)
- *
- * The purpose of all these is to keep track of the people
- * waiting for a page to become available and make them
- * runnable again when possible. The trouble is that this
- * consumes a lot of space, especially when so few things
- * wait on pages at a given time. So instead of using
- * per-page waitqueues, we use a waitqueue hash table.
- *
- * The bucket discipline is to sleep on the same queue when
- * colliding and wake all in that wait queue when removing.
- * When something wakes, it must check to be sure its page is
- * truly available, a la thundering herd. The cost of a
- * collision is great, but given the expected load of the
- * table, they should be so rare as to be outweighed by the
- * benefits from the saved space.
- *
- * __wait_on_page_locked() and unlock_page() in mm/filemap.c, are the
- * primary users of these fields, and in mm/page_alloc.c
- * free_area_init_core() performs the initialization of them.
- */
- wait_queue_head_t * wait_table;
- unsigned long wait_table_hash_nr_entries;
- unsigned long wait_table_bits;
- /*
- * Discontig memory support fields.
- */
- struct pglist_data *zone_pgdat; /* 本管理区所属的节点 */
- /* zone_start_pfn == zone_start_paddr >> PAGE_SHIFT */
- unsigned long zone_start_pfn; /* 管理区的页面在mem_map中的偏移 */
- /*
- * zone_start_pfn, spanned_pages and present_pages are all
- * protected by span_seqlock. It is a seqlock because it has
- * to be read outside of zone->lock, and it is done in the main
- * allocator path. But, it is written quite infrequently.
- *
- * The lock is declared along with zone->lock because it is
- * frequently read in proximity to zone->lock. It's good to
- * give them a chance of being in the same cacheline.
- */
- unsigned long spanned_pages; /* total size, including holes */
- unsigned long present_pages; /* amount of memory (excluding holes) */
- const char *name; /* 很少使用的域 */
- } ____cacheline_internodealigned_in_smp;
3、物理页面:page
系统中每个物理页面都有一个相关联的page用于记录该页面的状态。在include/linux/mm_types.h中,如下:
- /*
- * 系统中每个物理页面有一个相关联的page结构,用于记录该页面的状态。注意虽然当该页面是
- * 一个缓存页时,rmap结构能告诉我们谁正在映射它,但我们并没有一般的方法来跟踪哪个进程正在使用该页面
- */
- struct page {
- unsigned long flags; /* 原子标志,一些可以会被异步更新 */
- atomic_t _count; /* 使用计数,参考下面 */
- union {
- atomic_t _mapcount; /* 在mms中映射的ptes计数,用于表明页面什么时候被映射,
- * 并且限制反向映射搜索
- */
- struct { /* SLUB */
- u16 inuse;
- u16 objects;
- };
- };
- union {
- struct {
- unsigned long private; /* 映射时的私有非透明数据:
- * 如果设置PagePrivate,则用作buffer_heads;
- * 如果设置PageSwapCache,则用作swp_entry_t;
- * 如果设置PG_buddy,则表示在伙伴系统中的顺序编号
- */
- struct address_space *mapping; /* 如果低端bit清除,则指向inode地址空间,或者为null.
- * 如果页面被映射为匿名内存,低端bit设置,则指向
- * anon_vma对象,参看PAGE_MAPPING_ANON
- */
- };
- #if USE_SPLIT_PTLOCKS
- spinlock_t ptl;
- #endif
- struct kmem_cache *slab; /* SLUB: 指向slab的指针 */
- /* 如果属于伙伴系统,并且不是伙伴系统中的第一个页则指向第一个页 */
- struct page *first_page;
- };
- union { /* 如果是文件映射,那么表示本页面在文件中的位置(偏移) */
- pgoff_t index; /* Our offset within mapping. */
- void *freelist; /* SLUB: freelist req. slab lock */
- };
- struct list_head lru; /* Pageout list, eg. active_list
- * protected by zone->lru_lock !
- */
- /*
- * On machines where all RAM is mapped into kernel address space,
- * we can simply calculate the virtual address. On machines with
- * highmem some memory is mapped into kernel virtual memory
- * dynamically, so we need a place to store that address.
- * Note that this field could be 16 bits on x86 ... ;)
- *
- * Architectures with slow multiplication can define
- * WANT_PAGE_VIRTUAL in asm/page.h
- */
- #if defined(WANT_PAGE_VIRTUAL)
- void *virtual; /* 内核虚拟地址(如果没有被内核映射,则为NULL,例如高端内存hignmem) */
- #endif /* WANT_PAGE_VIRTUAL */
- #ifdef CONFIG_WANT_PAGE_DEBUG_FLAGS
- unsigned long debug_flags; /* Use atomic bitops on this */
- #endif
- #ifdef CONFIG_KMEMCHECK
- /* kmemcheck想跟踪一个page中的每个byte的状态,这是一个指向这种状态块的指针。
- * 如果没有被跟踪,则为NULL
- */
- void *shadow;
- #endif
- };
4、全局的mem_map数组: 定义在include/linux/mmzone.h中,如下:
- #ifndef CONFIG_DISCONTIGMEM
- /* 物理页数组,对discontigmem使用pgdat->lmem_map */
- extern struct page *mem_map;
- #endif
Linux内存管理(3):内存探测与初始化
1、内存探测linux在被bootloader加载到内存后, cpu最初执行的内核代码是arch/x86/boot/header.S汇编文件中的_start例程,设置好头部header,其中包括大量的bootloader参数。接着是其中的start_of_setup例程,这个例程在做了一些准备工作后会通过call main跳转到arch/x86/boot/main.c:main()函数处执行,这就是众所周知的x86下的main函数,它们都工作在实模式下。在这个main函数中我们可以第一次看到与内存管理相关的代码,这段代码调用detect_memory()函数检测系统物理内存。如下:
- void main(void)
- {
- /* First, copy the boot header into the "zeropage" */
- copy_boot_params(); /* 把头部各参数复制到boot_params变量中 */
- /* End of heap check */
- init_heap();
- /* Make sure we have all the proper CPU support */
- if (validate_cpu()) {
- puts("Unable to boot - please use a kernel appropriate "
- "for your CPU.\n");
- die();
- }
- /* Tell the BIOS what CPU mode we intend to run in. */
- set_bios_mode();
- /* Detect memory layout */
- detect_memory(); /* 内存探测函数 */
- /* Set keyboard repeat rate (why?) */
- keyboard_set_repeat();
- /* Query MCA information */
- query_mca();
- /* Query Intel SpeedStep (IST) information */
- query_ist();
- /* Query APM information */
- #if defined(CONFIG_APM) || defined(CONFIG_APM_MODULE)
- query_apm_bios();
- #endif
- /* Query EDD information */
- #if defined(CONFIG_EDD) || defined(CONFIG_EDD_MODULE)
- query_edd();
- #endif
- /* Set the video mode */
- set_video();
- /* Parse command line for 'quiet' and pass it to decompressor. */
- if (cmdline_find_option_bool("quiet"))
- boot_params.hdr.loadflags |= QUIET_FLAG;
- /* Do the last things and invoke protected mode */
- go_to_protected_mode();
- }
- int detect_memory(void)
- {
- int err = -1;
- if (detect_memory_e820() > 0)
- err = 0;
- if (!detect_memory_e801())
- err = 0;
- if (!detect_memory_88())
- err = 0;
- return err;
- }
- #define SMAP 0x534d4150 /* ASCII "SMAP" */
- static int detect_memory_e820(void)
- {
- int count = 0; /* 用于记录已检测到的物理内存数目 */
- struct biosregs ireg, oreg;
- struct e820entry *desc = boot_params.e820_map;
- static struct e820entry buf; /* static so it is zeroed */
- initregs(&ireg); /* 初始化ireg中的相关寄存器 */
- ireg.ax = 0xe820;
- ireg.cx = sizeof buf; /* e820entry数据结构大小 */
- ireg.edx = SMAP; /* 标识 */
- ireg.di = (size_t)&buf; /* int15返回值的存放处 */
- /*
- * Note: at least one BIOS is known which assumes that the
- * buffer pointed to by one e820 call is the same one as
- * the previous call, and only changes modified fields. Therefore,
- * we use a temporary buffer and copy the results entry by entry.
- *
- * This routine deliberately does not try to account for
- * ACPI 3+ extended attributes. This is because there are
- * BIOSes in the field which report zero for the valid bit for
- * all ranges, and we don't currently make any use of the
- * other attribute bits. Revisit this if we see the extended
- * attribute bits deployed in a meaningful way in the future.
- */
- do {
- /* 在执行这条内联汇编语句时输入的参数有:
- eax寄存器=0xe820
- dx寄存器=’SMAP’
- edi寄存器=desc
- ebx寄存器=next
- ecx寄存器=size
- 返回给c语言代码的参数有:
- id=eax寄存器
- rr=edx寄存器
- ext=ebx寄存器
- size=ecx寄存器
- desc指向的内存地址在执行0x15中断调用时被设置
- */
- intcall(0x15, &ireg, &oreg);
- ireg.ebx = oreg.ebx; /* 选择下一个 */
- /* BIOSes which terminate the chain with CF = 1 as opposed
- to %ebx = 0 don't always report the SMAP signature on
- the final, failing, probe. */
- if (oreg.eflags & X86_EFLAGS_CF)
- break;
- /* Some BIOSes stop returning SMAP in the middle of
- the search loop. We don't know exactly how the BIOS
- screwed up the map at that point, we might have a
- partial map, the full map, or complete garbage, so
- just return failure. */
- if (oreg.eax != SMAP) {
- count = 0;
- break;
- }
- *desc++ = buf; /* 将buf赋值给desc */
- count++; /* 探测数加一 */
- } while (ireg.ebx && count < ARRAY_SIZE(boot_params.e820_map));
- /* 将内存块数保持到变量中 */
- return boot_params.e820_entries = count;
- }
这里存放中断返回值的e820entry结构,以及表示内存图的e820map结构均位于arch/x86/include/asm/e820.h中,如下:
- struct e820entry {
- __u64 addr; /* 内存段的开始 */
- __u64 size; /* 内存段的大小 */
- __u32 type; /* 内存段的类型 */
- } __attribute__((packed));
- struct e820map {
- __u32 nr_map;
- struct e820entry map[E820_X_MAX];
- };
对于32位的系统,通过调用链arch/x86/boot/main.c:main()--->arch/x86/boot/pm.c:go_to_protected_mode()--->arch/x86/boot/pmjump.S:protected_mode_jump()--->arch/i386/boot/compressed/head_32.S:startup_32()--->arch/x86/kernel/head_32.S:startup_32()--->arch/x86/kernel/head32.c:i386_start_kernel()--->init/main.c:start_kernel(),到达众所周知的Linux内核启动函数start_kernel(),这里会调用setup_arch()完成与体系结构相关的一系列初始化工作,其中就包括各种内存的初始化工作,如内存图的建立、管理区的初始化等等。对x86体系结构,setup_arch()函数在arch/x86/kernel/setup.c中,如下:
- void __init setup_arch(char **cmdline_p)
- {
- /* ...... */
- x86_init.oem.arch_setup();
- setup_memory_map(); /* 建立内存图 */
- parse_setup_data();
- /* update the e820_saved too */
- e820_reserve_setup_data();
- /* ...... */
- /*
- * partially used pages are not usable - thus
- * we are rounding upwards:
- */
- max_pfn = e820_end_of_ram_pfn(); /* 找出最大可用内存页面帧号 */
- /* preallocate 4k for mptable mpc */
- early_reserve_e820_mpc_new();
- /* update e820 for memory not covered by WB MTRRs */
- mtrr_bp_init();
- if (mtrr_trim_uncached_memory(max_pfn))
- max_pfn = e820_end_of_ram_pfn();
- #ifdef CONFIG_X86_32
- /* max_low_pfn在这里更新 */
- find_low_pfn_range(); /* 找出低端内存的最大页帧号 */
- #else
- num_physpages = max_pfn;
- /* ...... */
- /* max_pfn_mapped在这更新 */
- /* 初始化内存映射机制 */
- max_low_pfn_mapped = init_memory_mapping(0, max_low_pfn<<PAGE_SHIFT);
- max_pfn_mapped = max_low_pfn_mapped;
- #ifdef CONFIG_X86_64
- if (max_pfn > max_low_pfn) {
- max_pfn_mapped = init_memory_mapping(1UL<<32,
- max_pfn<<PAGE_SHIFT);
- /* can we preseve max_low_pfn ?*/
- max_low_pfn = max_pfn;
- }
- #endif
- /* ...... */
- initmem_init(0, max_pfn); /* 启动内存分配器 */
- /* ...... */
- x86_init.paging.pagetable_setup_start(swapper_pg_dir);
- paging_init(); /* 建立完整的页表 */
- x86_init.paging.pagetable_setup_done(swapper_pg_dir);
- /* ...... */
- }
(1)建立内存图:setup_memory_map();
(2)调用e820_end_of_ram_pfn()找出最大可用页帧号max_pfn,调用find_low_pfn_range()找出低端内存区的最大可用页帧号max_low_pfn。
(2)初始化内存映射机制:init_memory_mapping();
(3)初始化内存分配器:initmem_init();
(4)建立完整的页表:paging_init()。
2、建立内存图
内存探测完之后,就要建立描述各内存块情况的全局内存图结构了。函数为setup_arch()--->arch/x86/kernel/e820.c:setup_memory_map(),如下:
- void __init setup_memory_map(void)
- {
- char *who;
- /* 调用x86体系下的memory_setup函数 */
- who = x86_init.resources.memory_setup();
- /* 保存到e820_saved中 */
- memcpy(&e820_saved, &e820, sizeof(struct e820map));
- printk(KERN_INFO "BIOS-provided physical RAM map:\n");
- /* 打印输出 */
- e820_print_map(who);
- }
- char *__init default_machine_specific_memory_setup(void)
- {
- char *who = "BIOS-e820";
- u32 new_nr;
- /*
- * 复制BIOS提供的e820内存图,否则伪造一个内存图:一块为0-640k,接着的
- * 下一块为1mb到appropriate_mem_k的大小
- */
- new_nr = boot_params.e820_entries;
- /* 将重叠的去除 */
- sanitize_e820_map(boot_params.e820_map,
- ARRAY_SIZE(boot_params.e820_map),
- &new_nr);
- /* 去掉重叠的部分后得到的内存块个数 */
- boot_params.e820_entries = new_nr;
- /* 将其复制到全局变量e820中,小于0时,为出错处理 */
- if (append_e820_map(boot_params.e820_map, boot_params.e820_entries)
- < 0) {
- u64 mem_size;
- /* compare results from other methods and take the greater */
- if (boot_params.alt_mem_k
- < boot_params.screen_info.ext_mem_k) {
- mem_size = boot_params.screen_info.ext_mem_k;
- who = "BIOS-88";
- } else {
- mem_size = boot_params.alt_mem_k;
- who = "BIOS-e801";
- }
- e820.nr_map = 0;
- e820_add_region(0, LOWMEMSIZE(), E820_RAM);
- e820_add_region(HIGH_MEMORY, mem_size << 10, E820_RAM);
- }
- /* In case someone cares... */
- return who;
- }
- /*
- * 复制BIOS e820内存图到一个安全的地方。如果我们在里面,则要进行重叠检查
- * 如果我们用的是现代系统,则设置代码将给我们提供一个可以使用的内存图,以便
- * 用它来建立内存。如果不是现代系统,则将伪造一个内存图
- */
- static int __init append_e820_map(struct e820entry *biosmap, int nr_map)
- {
- /* Only one memory region (or negative)? Ignore it */
- if (nr_map < 2)
- return -1;
- return __append_e820_map(biosmap, nr_map);
- }
- static int __init __append_e820_map(struct e820entry *biosmap, int nr_map)
- {
- while (nr_map) { /* 循环nr_map次调用,添加内存块到e820 */
- u64 start = biosmap->addr;
- u64 size = biosmap->size;
- u64 end = start + size;
- u32 type = biosmap->type;
- /* Overflow in 64 bits? Ignore the memory map. */
- if (start > end)
- return -1;
- /* 添加函数 */
- e820_add_region(start, size, type);
- biosmap++;
- nr_map--;
- }
- return 0;
- }
- void __init e820_add_region(u64 start, u64 size, int type)
- {
- __e820_add_region(&e820, start, size, type);
- }
- /*
- * 添加一个内存块到内存e820内存图中
- */
- static void __init __e820_add_region(struct e820map *e820x, u64 start, u64 size,
- int type)
- {
- int x = e820x->nr_map;
- if (x >= ARRAY_SIZE(e820x->map)) {
- printk(KERN_ERR "Ooops! Too many entries in the memory map!\n");
- return;
- }
- e820x->map[x].addr = start;
- e820x->map[x].size = size;
- e820x->map[x].type = type;
- e820x->nr_map++;
- }
Linux内存管理(4):内存映射机制
Linux简化了分段机制,使得虚拟地址与线性地址总是一致,因此Linux的虚拟地址空间也为0~4G。Linux内核将这4G字节的空间分为两部分。将最高的1G字节(从虚拟地址0xC0000000到0xFFFFFFFF)供内核使用,称为“内核空间”。而将较低的3G字节(从虚拟地址0x00000000到0xBFFFFFFF)供各个进程使用,称为“用户空间“。因为每个进程可以通过系统调用进入内核,因此Linux内核由系统内的所有进程共享。于是,从具体进程的角度来看,每个进程可以拥有4G字节的虚拟空间。
Linux使用两级保护机制:0级供内核使用,3级供用户程序使用。每个进程有各自的私有用户空间(0~3G),这个空间对系统中的其他进程是不可见的。最高的1GB字节虚拟内核空间则为所有进程以及内核所共享。内核空间中存放的是内核代码和数据,而进程的用户空间中存放的是用户程序的代码和数据。不管是内核空间还是用户空间,它们都处于虚拟空间中。虽然内核空间占据了每个虚拟空间中的最高1GB字节,但映射到物理内存却总是从最低地址(0x00000000)开始。对内核空间来说,其地址映射是很简单的线性映射,0xC0000000就是物理地址与线性地址之间的位移量,在Linux代码中就叫做PAGE_OFFSET。
1、与内存映射相关的宏定义
这些宏定义在include/asm-generic/page.h中,用于定义Linux三级分页模型中的页全局目录项pgd、页中间目录项pmd、页表项pte的数据类型,以及基本的地址转换,如下:
- #ifndef __ASM_GENERIC_PAGE_H
- #define __ASM_GENERIC_PAGE_H
- /*
- * 针对NOMMU体系结构的通用page.h实现,为内存管理提供虚拟定义
- */
- #ifdef CONFIG_MMU
- #error need to prove a real asm/page.h
- #endif
- /* PAGE_SHIFT决定页的大小 */
- #define PAGE_SHIFT 12
- #ifdef __ASSEMBLY__
- /* 页大小为4KB(不使用大内存页时) */
- #define PAGE_SIZE (1 << PAGE_SHIFT)
- #else
- #define PAGE_SIZE (1UL << PAGE_SHIFT)
- #endif
- #define PAGE_MASK (~(PAGE_SIZE-1))
- #include <asm/setup.h>
- #ifndef __ASSEMBLY__
- #define get_user_page(vaddr) __get_free_page(GFP_KERNEL)
- #define free_user_page(page, addr) free_page(addr)
- #define clear_page(page) memset((page), 0, PAGE_SIZE)
- #define copy_page(to,from) memcpy((to), (from), PAGE_SIZE)
- #define clear_user_page(page, vaddr, pg) clear_page(page)
- #define copy_user_page(to, from, vaddr, pg) copy_page(to, from)
- /*
- * 使用C的类型检查..
- */
- typedef struct {
- unsigned long pte;
- } pte_t;
- typedef struct {
- unsigned long pmd[16];
- } pmd_t;
- typedef struct {
- unsigned long pgd;
- } pgd_t;
- typedef struct {
- unsigned long pgprot;
- } pgprot_t;
- typedef struct page *pgtable_t;
- /* 把x转换成对应无符号整数 */
- #define pte_val(x) ((x).pte)
- #define pmd_val(x) ((&x)->pmd[0])
- #define pgd_val(x) ((x).pgd)
- #define pgprot_val(x) ((x).pgprot)
- /* 把无符号整数转换成对应的C类型 */
- #define __pte(x) ((pte_t) { (x) } )
- #define __pmd(x) ((pmd_t) { (x) } )
- #define __pgd(x) ((pgd_t) { (x) } )
- #define __pgprot(x) ((pgprot_t) { (x) } )
- /* 物理内存的起始地址和结束地址 */
- extern unsigned long memory_start;
- extern unsigned long memory_end;
- #endif /* !__ASSEMBLY__ */
- /* 如果内核配置了RAM的基地址,则把页偏移设为这个值,否则为0 */
- #ifdef CONFIG_KERNEL_RAM_BASE_ADDRESS
- #define PAGE_OFFSET (CONFIG_KERNEL_RAM_BASE_ADDRESS)
- #else
- #define PAGE_OFFSET (0)
- #endif
- #ifndef __ASSEMBLY__
- /* 把物理地址x转换为线性地址(即虚拟地址) */
- #define __va(x) ((void *)((unsigned long)(x) + PAGE_OFFSET))
- /* 把内核空间的线性地址x转换为物理地址 */
- #define __pa(x) ((unsigned long) (x) - PAGE_OFFSET)
- /* 根据内核空间的线性地址得到其物理页框号(即第几页) */
- #define virt_to_pfn(kaddr) (__pa(kaddr) >> PAGE_SHIFT)
- /* 根据物理页框号得到其线性地址 */
- #define pfn_to_virt(pfn) __va((pfn) << PAGE_SHIFT)
- /* 根据用户空间的线性地址得到其物理页号 */
- #define virt_to_page(addr) (mem_map + (((unsigned long)(addr)-PAGE_OFFSET) >> PAGE_SHIFT))
- /* 根据物理页号得到其用户空间的线性地址 */
- #define page_to_virt(page) ((((page) - mem_map) << PAGE_SHIFT) + PAGE_OFFSET)
- #ifndef page_to_phys
- #define page_to_phys(page) ((dma_addr_t)page_to_pfn(page) << PAGE_SHIFT)
- #endif
- #define pfn_valid(pfn) ((pfn) < max_mapnr)
- #define virt_addr_valid(kaddr) (((void *)(kaddr) >= (void *)PAGE_OFFSET) && \
- ((void *)(kaddr) < (void *)memory_end))
- #endif /* __ASSEMBLY__ */
- #include <asm-generic/memory_model.h>
- #include <asm-generic/getorder.h>
- #endif /* __ASM_GENERIC_PAGE_H */
2、临时页表的初始化
linux页表映射机制的建立分为两个阶段,第一个阶段是内核进入保护模式之前要先建立一个临时内核页表并开启分页功能,因为在进入保护模式后,内核继续初始化直到建立完整的内存映射机制之前,仍然需要用到页表来映射相应的内存地址。对x86 32位内核,这个工作在保护模式下的内核入口函数arch/x86/kernel/head_32.S:startup_32()中完成。第二阶段是建立完整的内存映射机制,在在setup_arch()--->arch/x86/mm/init.c:init_memory_mapping()中完成。注意对于物理地址扩展(PAE)分页机制,Intel通过在她得处理器上把管脚数从32增加到36已经满足了这些需求,寻址能力可以达到64GB。不过,只有引入一种新的分页机制把32位线性地址转换为36位物理地址才能使用所增加的物理地址。linux为对多种体系的支持,选择了一套简单的通用实现机制。在这里只分析x86 32位下的实现。
arch/x86/kernel/head_32.S中的startup_32()相关汇编代码如下:
- __HEAD
- ENTRY(startup_32)
- /* test KEEP_SEGMENTS flag to see if the bootloader is asking
- us to not reload segments */
- testb $(1<<6), BP_loadflags(%esi)
- jnz 2f
- /* ...... */
- /*
- * 初始化页表。这会创建一个PDE和一个页表集,存放在__brk_base的上面。
- * 变量_brk_end会被设置成指向第一个“安全”的区域。在虚拟地址0(为标识映射)
- * 和PAGE_OFFSET处会创建映射。注意在这里栈还没有被设置
- */
- default_entry:
- #ifdef CONFIG_X86_PAE
- /*
- * 在PAE模式下swapper_pg_dir被静态定义包括足够多的条目以包含VMSPLIT选项(即最高的1,
- * 2或3的条目)。标识映射通过把两个PGD条目指向第一个内核PMD条目来实现
- * 注意在这一阶段,每个PMD或PTE的上半部分总是为0
- */
- #define KPMDS (((-__PAGE_OFFSET) >> 30) & 3) /* 内核PMD的数量 */
- xorl %ebx,%ebx /* %ebx保持为0 */
- movl $pa(__brk_base), %edi
- movl $pa(swapper_pg_pmd), %edx
- movl $PTE_IDENT_ATTR, %eax
- 10:
- leal PDE_IDENT_ATTR(%edi),%ecx /* 创建PMD条目 */
- movl %ecx,(%edx) /* 保存PMD条目 */
- /* 上半部分已经为0 */
- addl $8,%edx
- movl $512,%ecx
- 11:
- stosl
- xchgl %eax,%ebx
- stosl
- xchgl %eax,%ebx
- addl $0x1000,%eax
- loop 11b
- /*
- * 终止条件:我们必须映射到end + MAPPING_BEYOND_END.
- */
- movl $pa(_end) + MAPPING_BEYOND_END + PTE_IDENT_ATTR, %ebp
- cmpl %ebp,%eax
- jb 10b
- 1:
- addl $__PAGE_OFFSET, %edi
- movl %edi, pa(_brk_end)
- shrl $12, %eax
- movl %eax, pa(max_pfn_mapped)
- /* 对fixmap区域做初期的初始化 */
- movl $pa(swapper_pg_fixmap)+PDE_IDENT_ATTR,%eax
- movl %eax,pa(swapper_pg_pmd+0x1000*KPMDS-8)
- #else /* 非PAE */
- /* 得到开始目录项的索引 */
- page_pde_offset = (__PAGE_OFFSET >> 20);
- /* 将基地址__brk_base转换成物理地址,传给edi */
- movl $pa(__brk_base), %edi
- /* 将全局页目录表地址传给edx */
- movl $pa(swapper_pg_dir), %edx
- movl $PTE_IDENT_ATTR, %eax
- 10:
- leal PDE_IDENT_ATTR(%edi),%ecx /* 创建PDE条目 */
- movl %ecx,(%edx) /* 保存标识PDE条目 */
- movl %ecx,page_pde_offset(%edx) /* 保存内核PDE条目 */
- addl $4,%edx
- movl $1024, %ecx
- 11:
- stosl
- addl $0x1000,%eax
- loop 11b
- /*
- * 终止条件:我们必须映射到end + MAPPING_BEYOND_END.
- */
- movl $pa(_end) + MAPPING_BEYOND_END + PTE_IDENT_ATTR, %ebp
- cmpl %ebp,%eax
- jb 10b
- addl $__PAGE_OFFSET, %edi
- movl %edi, pa(_brk_end)
- shrl $12, %eax
- movl %eax, pa(max_pfn_mapped)
- /* 对fixmap区域做初期的初始化 */
- movl $pa(swapper_pg_fixmap)+PDE_IDENT_ATTR,%eax
- movl %eax,pa(swapper_pg_dir+0xffc)
- #endif
- jmp 3f
- /*
- * Non-boot CPU entry point; entered from trampoline.S
- * We can't lgdt here, because lgdt itself uses a data segment, but
- * we know the trampoline has already loaded the boot_gdt for us.
- *
- * If cpu hotplug is not supported then this code can go in init section
- * which will be freed later
- */
- __CPUINIT
- #ifdef CONFIG_SMP
- ENTRY(startup_32_smp)
- cld
- movl $(__BOOT_DS),%eax
- movl %eax,%ds
- movl %eax,%es
- movl %eax,%fs
- movl %eax,%gs
- #endif /* CONFIG_SMP */
- 3:
- /*
- * New page tables may be in 4Mbyte page mode and may
- * be using the global pages.
- *
- * NOTE! If we are on a 486 we may have no cr4 at all!
- * So we do not try to touch it unless we really have
- * some bits in it to set. This won't work if the BSP
- * implements cr4 but this AP does not -- very unlikely
- * but be warned! The same applies to the pse feature
- * if not equally supported. --macro
- *
- * NOTE! We have to correct for the fact that we're
- * not yet offset PAGE_OFFSET..
- */
- #define cr4_bits pa(mmu_cr4_features)
- movl cr4_bits,%edx
- andl %edx,%edx
- jz 6f
- movl %cr4,%eax # 打开分页选项(PSE,PAE,...)
- orl %edx,%eax
- movl %eax,%cr4
- btl $5, %eax # 检查PAE是否开启
- jnc 6f
- /* 检查扩展函数功能是否实现 */
- movl $0x80000000, %eax
- cpuid
- cmpl $0x80000000, %eax
- jbe 6f
- mov $0x80000001, %eax
- cpuid
- /* Execute Disable bit supported? */
- btl $20, %edx
- jnc 6f
- /* 设置EFER (Extended Feature Enable Register) */
- movl $0xc0000080, %ecx
- rdmsr
- btsl $11, %eax
- /* 使更改生效 */
- wrmsr
- 6:
- /*
- * 开启分页功能
- */
- movl pa(initial_page_table), %eax
- movl %eax,%cr3 /* 设置页表指针:cr3控制寄存器保存的是目录表地址 */
- movl %cr0,%eax
- orl $X86_CR0_PG,%eax
- movl %eax,%cr0 /* ..同时设置分页(PG)标识位 */
- ljmp $__BOOT_CS,$1f /* 清除预读取和规格化%eip */
- 1:
- /* 设置栈指针 */
- lss stack_start,%esp
- /*
- * Initialize eflags. Some BIOS's leave bits like NT set. This would
- * confuse the debugger if this code is traced.
- * XXX - best to initialize before switching to protected mode.
- */
- pushl $0
- popfl
- #ifdef CONFIG_SMP
- cmpb $0, ready
- jz 1f /* 初始的CPU要清除BSS */
- jmp checkCPUtype
- 1:
- #endif /* CONFIG_SMP */
- /*
- * 初期标识映射的pte属性宏
- */
- #ifdef CONFIG_X86_64
- #define __PAGE_KERNEL_IDENT_LARGE_EXEC __PAGE_KERNEL_LARGE_EXEC
- #else
- /*
- * For PDE_IDENT_ATTR include USER bit. As the PDE and PTE protection
- * bits are combined, this will alow user to access the high address mapped
- * VDSO in the presence of CONFIG_COMPAT_VDSO
- */
- #define PTE_IDENT_ATTR 0x003 /* PRESENT+RW */
- #define PDE_IDENT_ATTR 0x067 /* PRESENT+RW+USER+DIRTY+ACCESSED */
- #define PGD_IDENT_ATTR 0x001 /* PRESENT (no other attributes) */
- #endif
(1)swapper_pg_dir是临时全局页目录表起址,它是在内核编译过程中静态初始化的。首先 page_pde_offset得到开始目录项的索引。从这可以看出内核是在swapper_pg_dir的第768个表项开始建立页表。其对应线性地址就是__brk_base(内核编译时指定其值,默认为0xc0000000)以上的地址,即3GB以上的高端地址(3GB-4GB),再次强调这高端的1GB线性空间是内核占据的虚拟空间,在进行实际内存映射时,映射到物理内存却总是从最低地址(0x00000000)开始。
(2)将目录表的地址swapper_pg_dir传给edx,表明内核也要从__brk_base开始建立页表,这样可以保证从以物理地址取指令到以线性地址在系统空间取指令的平稳过渡。
(3)创建并保存PDE条目。
(4)终止条件end + MAPPING_BEYOND_END决定了内核到底要建立多少页表,也就是要映射多少内存空间。在内核初始化程中内核只要保证能映射到包括内核的代码段,数据段,初始页表和用于存放动态数据结构的128k大小的空间就行。在这段代码中,内核为什么要把用户空间和内核空间的前几个目录项映射到相同的页表中去呢?虽然在head_32.S中内核已经进入保护模式,但是内核现在是处于保护模式的段式寻址方式下,因为内核还没有启用分页映射机制,现在都是以物理地址来取指令,如果代码中遇到了符号地址,只能减去0xc0000000才行,当开启了映射机制后就不用了。现在cpu中的取指令指针eip仍指向低区,如果只建立内核空间中的映射,那么当内核开启映射机制后,低区中的地址就没办法寻址了,因为没有对应的页表,除非遇到某个符号地址作为绝对转移或调用子程序为止。因此要尽快开启CPU的页式映射机制。
(5)开启CPU页式映射机制:initial_page_table表示目录表起址,传到eax中,然后保存到cr3控制寄存器中(从而前面“内存模型”介绍中可知cr3保存页目录表起址)。把cr0的最高位置成1来开启映射机制(即设置PG位)。
通过ljmp $__BOOT_CS,$1f这条指令使CPU进入了系统空间继续执行,因为__BOOT_CS是个符号地址,地址在0xc0000000以上。在head_32.S完成了内核临时页表的建立后,它继续进行初始化,包括初始化INIT_TASK,也就是系统开启后的第一个进程;建立完整的中断处理程序,然后重新加载GDT描述符,最后跳转到init/main.c中的start_kernel()函数继续初始化。
3、内存映射机制的完整建立
根据前面介绍,这一阶段在start_kernel()--->setup_arch()中完成。在Linux中,物理内存被分为低端内存区和高端内存区(如果内核编译时配置了高端内存标志的话),为了建立物理内存到虚拟地址空间的映射,需要先计算出物理内存总共有多少页面数,即找出最大可用页框号,这包含了整个低端和高端内存区。还要计算出低端内存区总共占多少页面。
在setup_arch(),首先调用arch/x86/kernel/e820.c:e820_end_of_ram_pfn()找出最大可用页帧号(即总页面数),并保存在全局变量max_pfn中,这个变量定义可以在mm/bootmem.c中找到。它直接调用e820.c中的e820_end_pfn()完成工作。如下:
- #ifdef CONFIG_X86_32
- # ifdef CONFIG_X86_PAE
- # define MAX_ARCH_PFN (1ULL<<(36-PAGE_SHIFT))
- # else
- # define MAX_ARCH_PFN (1ULL<<(32-PAGE_SHIFT))
- # endif
- #else /* CONFIG_X86_32 */
- # define MAX_ARCH_PFN MAXMEM>>PAGE_SHIFT
- #endif
- /*
- * 找出最大可用页帧号
- */
- 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; /* 4G地址空间对应的页面数 */
- /* 对e820中所有的内存块,其中e820为从bios中探测到的页面数存放处 */
- for (i = 0; i < e820.nr_map; i++) {
- struct e820entry *ei = &e820.map[i]; /* 第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 */
- last_pfn = end_pfn;
- }
- /* 大于4G空间时 */
- if (last_pfn > max_arch_pfn)
- last_pfn = max_arch_pfn;
- /* 打印输出信息 */
- printk(KERN_INFO "last_pfn = %#lx max_arch_pfn = %#lx\n",
- last_pfn, max_arch_pfn);
- /* 返回最后一个页面帧号 */
- return last_pfn;
- }
- unsigned long __init e820_end_of_ram_pfn(void)
- {
- /* MAX_ARCH_PFN为4G空间 */
- return e820_end_pfn(MAX_ARCH_PFN, E820_RAM);
- }
然后,setup_arch()会调用arch/x86/mm/init_32.c:find_low_pfn_range()找出低端内存区的最大可用页帧号,保存在全局变量max_low_pfn中(也定义在mm/bootmem.c中)。如下:
- static unsigned int highmem_pages = -1;
- /* ...... */
- /*
- * 全部物理内存都在包含在低端空间中
- */
- void __init lowmem_pfn_init(void)
- {
- /* max_low_pfn is 0, we already have early_res support */
- max_low_pfn = max_pfn;
- if (highmem_pages == -1)
- highmem_pages = 0;
- #ifdef CONFIG_HIGHMEM
- if (highmem_pages >= max_pfn) {
- printk(KERN_ERR MSG_HIGHMEM_TOO_BIG,
- pages_to_mb(highmem_pages), pages_to_mb(max_pfn));
- highmem_pages = 0;
- }
- if (highmem_pages) {
- if (max_low_pfn - highmem_pages < 64*1024*1024/PAGE_SIZE) {
- printk(KERN_ERR MSG_LOWMEM_TOO_SMALL,
- pages_to_mb(highmem_pages));
- highmem_pages = 0;
- }
- max_low_pfn -= highmem_pages;
- }
- #else
- if (highmem_pages)
- printk(KERN_ERR "ignoring highmem size on non-highmem kernel!\n");
- #endif
- }
- #define MSG_HIGHMEM_TOO_SMALL \
- "only %luMB highmem pages available, ignoring highmem size of %luMB!\n"
- #define MSG_HIGHMEM_TRIMMED \
- "Warning: only 4GB will be used. Use a HIGHMEM64G enabled kernel!\n"
- /*
- * 物理内存超出低端空间区:把它们放在高端地址空间中,或者通过启动时的highmem=x启动参数进行配置;
- * 如果不配置,在这里进行设置大小
- */
- void __init highmem_pfn_init(void)
- {
- /* MAXMEM_PFN为最大物理地址-(4M+4M+8K+128M);
- 所以低端空间的大小其实比我们说的896M低一些 */
- max_low_pfn = MAXMEM_PFN;
- if (highmem_pages == -1) /* 高端内存页面数如果在开机没有设置 */
- highmem_pages = max_pfn - MAXMEM_PFN; /* 总页面数减去低端页面数 */
- /* 如果highmem_pages变量在启动项设置了,那么在这里就要进行这样的判断,
- 因为可能出现不一致的情况 */
- 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
- /* 最大可用内存是可直接寻址的 */
- printk(KERN_WARNING "Warning only %ldMB will be used.\n", MAXMEM>>20);
- if (max_pfn > MAX_NONPAE_PFN)
- printk(KERN_WARNING "Use a HIGHMEM64G enabled kernel.\n");
- else
- printk(KERN_WARNING "Use a HIGHMEM enabled kernel.\n");
- max_pfn = MAXMEM_PFN;
- #else /* !CONFIG_HIGHMEM */
- #ifndef CONFIG_HIGHMEM64G
- /* 在没有配置64G的情况下,内存的大小不能超过4G */
- if (max_pfn > MAX_NONPAE_PFN) {
- max_pfn = MAX_NONPAE_PFN;
- printk(KERN_WARNING MSG_HIGHMEM_TRIMMED);
- }
- #endif /* !CONFIG_HIGHMEM64G */
- #endif /* !CONFIG_HIGHMEM */
- }
- /*
- * 确定低端和高端内存的页面帧号范围:
- */
- void __init find_low_pfn_range(void)
- {
- /* 会更新max_pfn */
- /* 当物理内存本来就小于低端空间最大页框数时,
- 直接没有高端地址映射 */
- if (max_pfn <= MAXMEM_PFN)
- lowmem_pfn_init();
- else /* 这是一般PC机的运行流程,存在高端映射 */
- highmem_pfn_init();
- }
(1)init_32.c中定义了一个静态全局变量highmem_pages,用来保存用户指定的高端空间的大小(即总页面数)。
(2)在find_low_pfn_range()中,如果物理内存总页面数max_pfn不大于低端页面数上限MAXMEM_PFN(即物理内存大小没有超出低端空间范围),则直接没有高端地址映射,调用lowmem_pfn_init(),将max_low_pfn设成max_pfn。注意若内核编译时通过CONFIG_HIGHMEM指定必须有高端映射,则max_low_pfn的值需要减去高端页面数highmem_pages,以表示低端页面数。
(3)如果物理内存总页面数大于低端页面数上限,则表明有高端映射,因为需要把超出的部分放在高端空间区,这是一般PC机的运行流程。调用highmem_pfn_init(),如果启动时用户没有指定高端页面数,则显然max_low_pfn=MAXMEM_PFN,highmem_pages = max_pfn - MAXMEM_PFN;如果启动时用户通过highmem=x启动参数指定了高端页面数highmem_pages,则仍然有max_low_pfn=MAXMEM_PFN,但max_pfn可能出现不一致的情况,需要更新为MAXMEM_PFN + highmem_pages,如果出现越界(高端空间区太小),则要做相应越界处理。
有了总页面数、低端页面数、高端页面数这些信息,setup_arch()接着调用arch/x86/mm/init.c:init_memory_mapping(0, max_low_pfn<<PAGE_SHIFT)函数建立完整的内存映射机制。该函数在PAGE_OFFSET处建立物理内存的直接映射,即把物理内存中0~max_low_pfn<<12地址范围的低端空间区直接映射到内核虚拟空间(它是从PAGE_OFFSET即0xc0000000开始的1GB线性地址)。这在bootmem初始化之前运行,并且直接从物理内存获取页面,这些页面在前面已经被临时映射了。注意高端映射区并没有映射到实际的物理页面,只是这种机制的初步建立,页表存储的空间保留。代码如下:
- unsigned long __init_refok init_memory_mapping(unsigned long start,
- unsigned long end)
- {
- unsigned long page_size_mask = 0;
- unsigned long start_pfn, end_pfn;
- unsigned long ret = 0;
- unsigned long pos;
- struct map_range mr[NR_RANGE_MR];
- int nr_range, i;
- int use_pse, use_gbpages;
- printk(KERN_INFO "init_memory_mapping: %016lx-%016lx\n", start, end);
- #if defined(CONFIG_DEBUG_PAGEALLOC) || defined(CONFIG_KMEMCHECK)
- /*
- * For CONFIG_DEBUG_PAGEALLOC, identity mapping will use small pages.
- * This will simplify cpa(), which otherwise needs to support splitting
- * large pages into small in interrupt context, etc.
- */
- use_pse = use_gbpages = 0;
- #else
- use_pse = cpu_has_pse;
- use_gbpages = direct_gbpages;
- #endif
- /* 定义了X86_PAE模式后进行调用 */
- set_nx();
- if (nx_enabled)
- printk(KERN_INFO "NX (Execute Disable) protection: active\n");
- /* 激活PSE(如果可用) */
- if (cpu_has_pse)
- set_in_cr4(X86_CR4_PSE);
- /* 激活PGE(如果可用) */
- if (cpu_has_pge) {
- set_in_cr4(X86_CR4_PGE);
- __supported_pte_mask |= _PAGE_GLOBAL;
- }
- /* page_size_mask在这里更新,在后面设置页表时用到 */
- if (use_gbpages)
- page_size_mask |= 1 << PG_LEVEL_1G;
- if (use_pse)
- page_size_mask |= 1 << PG_LEVEL_2M;
- memset(mr, 0, sizeof(mr));
- nr_range = 0;
- /* 作为初始页面帧号值,如果没有大内存页对齐 */
- start_pfn = start >> PAGE_SHIFT; /* 在setup函数中调用时,这里为0 */
- pos = start_pfn << PAGE_SHIFT; /* pos为0 */
- #ifdef CONFIG_X86_32
- /*
- * Don't use a large page for the first 2/4MB of memory
- * because there are often fixed size MTRRs in there
- * and overlapping MTRRs into large pages can cause
- * slowdowns.
- */
- if (pos == 0) /* end_pfn的大小为1k,也就是4M大小的内存 */
- end_pfn = 1<<(PMD_SHIFT - PAGE_SHIFT);
- else
- end_pfn = ((pos + (PMD_SIZE - 1))>>PMD_SHIFT)
- << (PMD_SHIFT - PAGE_SHIFT);
- #else /* CONFIG_X86_64 */
- end_pfn = ((pos + (PMD_SIZE - 1)) >> PMD_SHIFT)
- << (PMD_SHIFT - PAGE_SHIFT);
- #endif
- if (end_pfn > (end >> PAGE_SHIFT))
- end_pfn = end >> PAGE_SHIFT;
- if (start_pfn < end_pfn) { /* 4M空间将这个区间存放在mr数组中 */
- nr_range = save_mr(mr, nr_range, start_pfn, end_pfn, 0);
- pos = end_pfn << PAGE_SHIFT;
- }
- /* 大内存页(2M)范围:对齐到PMD,换算成页面的多少 */
- start_pfn = ((pos + (PMD_SIZE - 1))>>PMD_SHIFT)
- << (PMD_SHIFT - PAGE_SHIFT);
- #ifdef CONFIG_X86_32
- /* 这里的结束地址设置为调用的结束位页面数,也就是
- 所有的物理页面数 */
- end_pfn = (end>>PMD_SHIFT) << (PMD_SHIFT - PAGE_SHIFT);
- #else /* CONFIG_X86_64 */
- end_pfn = ((pos + (PUD_SIZE - 1))>>PUD_SHIFT)
- << (PUD_SHIFT - PAGE_SHIFT);
- if (end_pfn > ((end>>PMD_SHIFT)<<(PMD_SHIFT - PAGE_SHIFT)))
- end_pfn = ((end>>PMD_SHIFT)<<(PMD_SHIFT - PAGE_SHIFT));
- #endif
- if (start_pfn < end_pfn) {
- /* 将这段内存放入mr中,保存后面用到 */
- nr_range = save_mr(mr, nr_range, start_pfn, end_pfn,
- page_size_mask & (1<<PG_LEVEL_2M)); /* 这里保证了运用PSE时为2M页面而不是PSE时,
- 仍然为4K页面(上面的按位或和这里的按位与) */
- pos = end_pfn << PAGE_SHIFT; /* 更新pos */
- }
- #ifdef CONFIG_X86_64
- /* 大内存页(1G)范围 */
- start_pfn = ((pos + (PUD_SIZE - 1))>>PUD_SHIFT)
- << (PUD_SHIFT - PAGE_SHIFT);
- end_pfn = (end >> PUD_SHIFT) << (PUD_SHIFT - PAGE_SHIFT);
- if (start_pfn < end_pfn) {
- nr_range = save_mr(mr, nr_range, start_pfn, end_pfn,
- page_size_mask &
- ((1<<PG_LEVEL_2M)|(1<<PG_LEVEL_1G)));
- pos = end_pfn << PAGE_SHIFT;
- }
- /* 尾部不是大内存页(1G)对齐 */
- start_pfn = ((pos + (PMD_SIZE - 1))>>PMD_SHIFT)
- << (PMD_SHIFT - PAGE_SHIFT);
- end_pfn = (end >> PMD_SHIFT) << (PMD_SHIFT - PAGE_SHIFT);
- if (start_pfn < end_pfn) {
- nr_range = save_mr(mr, nr_range, start_pfn, end_pfn,
- page_size_mask & (1<<PG_LEVEL_2M));
- pos = end_pfn << PAGE_SHIFT;
- }
- #endif
- /* 尾部不是大内存页(2M)对齐 */
- start_pfn = pos>>PAGE_SHIFT;
- end_pfn = end>>PAGE_SHIFT;
- nr_range = save_mr(mr, nr_range, start_pfn, end_pfn, 0);
- /* 合并相同页面大小的连续的页面 */
- for (i = 0; nr_range > 1 && i < nr_range - 1; i++) {
- unsigned long old_start;
- if (mr[i].end != mr[i+1].start ||
- mr[i].page_size_mask != mr[i+1].page_size_mask)
- continue;
- /* move it */
- old_start = mr[i].start;
- memmove(&mr[i], &mr[i+1],
- (nr_range - 1 - i) * sizeof(struct map_range));
- mr[i--].start = old_start;
- nr_range--;
- }
- /* 打印相关信息 */
- for (i = 0; i < nr_range; i++)
- printk(KERN_DEBUG " %010lx - %010lx page %s\n",
- mr[i].start, mr[i].end,
- (mr[i].page_size_mask & (1<<PG_LEVEL_1G))?"1G":(
- (mr[i].page_size_mask & (1<<PG_LEVEL_2M))?"2M":"4k"));
- /*
- * 为内核直接映射的页表查找空间
- * 以后我们应该在内存映射的本地节点分配这些页表。不幸的是目前这需要在
- * 查找到节点之前来做
- */
- if (!after_bootmem) /*如果内存启动分配器没有建立,则直接从e820.map中找到合适的
- 连续内存,找到存放页表的空间首地址为e820_table_start */
- find_early_table_space(end, use_pse, use_gbpages);
- #ifdef CONFIG_X86_32
- for (i = 0; i < nr_range; i++) /* 对每个保存的区域设置页表映射 */
- kernel_physical_mapping_init(mr[i].start, mr[i].end,
- mr[i].page_size_mask);
- ret = end;
- #else /* CONFIG_X86_64 */
- for (i = 0; i < nr_range; i++)
- ret = kernel_physical_mapping_init(mr[i].start, mr[i].end,
- mr[i].page_size_mask);
- #endif
- #ifdef CONFIG_X86_32
- /* 对高端内存固定区域建立映射 */
- early_ioremap_page_table_range_init();
- /* 放入CR3寄存器 */
- load_cr3(swapper_pg_dir);
- #endif
- #ifdef CONFIG_X86_64
- if (!after_bootmem && !start) {
- pud_t *pud;
- pmd_t *pmd;
- mmu_cr4_features = read_cr4();
- /*
- * _brk_end cannot change anymore, but it and _end may be
- * located on different 2M pages. cleanup_highmap(), however,
- * can only consider _end when it runs, so destroy any
- * mappings beyond _brk_end here.
- */
- pud = pud_offset(pgd_offset_k(_brk_end), _brk_end);
- pmd = pmd_offset(pud, _brk_end - 1);
- while (++pmd <= pmd_offset(pud, (unsigned long)_end - 1))
- pmd_clear(pmd);
- }
- #endif
- __flush_tlb_all(); /* 刷新寄存器 */
- /* 将分配给建立页表机制的内存空间保留 */
- if (!after_bootmem && e820_table_end > e820_table_start)
- reserve_early(e820_table_start << PAGE_SHIFT,
- e820_table_end << PAGE_SHIFT, "PGTABLE");
- if (!after_bootmem)
- early_memtest(start, end);
- return ret >> PAGE_SHIFT;
- }
(1)激活PSE和PGE,如果它们可用的话。更新page_size_mask掩码,这会在后面设置页表时用到。这个掩码可以用来区分使用的内存页大小,普通内存页为2KB,大内存页为4MB,启用了物理地址扩展(PAE)的系统上是2MB。
(2)根据传进来的地址范围计算起始页面帧号start_pfn和终止页面帧号end_pfn,调用save_mr()将这段页面范围保存到mr数组中,并更新pos,后面会用到。这里mr是由map_range结构构成的结构体数组,map_range结构封装了一个映射范围。
(3)遍历mr数组,合并相同页面大小的连接页面。
(4)调用find_early_table_space()为内核空间直接映射的页表查找可用的空间。然后对mr中的每个物理页面区域,调用核心函数kernel_physical_mapping_init()设置页表映射,以将它映射到内核空间。
(5)调用early_ioremap_page_table_range_init()对高端内存区建立页表映射,并把临时页表基址swapper_pg_dir加载到CR3寄存器中。
(6)因为将基址放到了CR3寄存器中,所以要调用__flush_tlb_all()对其寄存器刷新,以表示将内容放到内存中。然后,调用reserve_early()将分配给建立页表机制的内存空间保留。
map_range结构、save_mr(),以及find_early_table_space()的实现也都在arch/x86/mm/init.c中,如下:
- unsigned long __initdata e820_table_start;
- unsigned long __meminitdata e820_table_end;
- unsigned long __meminitdata e820_table_top;
- int after_bootmem;
- int direct_gbpages
- #ifdef CONFIG_DIRECT_GBPAGES
- = 1
- #endif
- ;
- /* 查找页表需要的空间 */
- static void __init find_early_table_space(unsigned long end, int use_pse,
- int use_gbpages)
- {
- unsigned long puds, pmds, ptes, tables, start;
- /* 计算需要用到多少pud,当没有pud存在的情况下pud=pgd */
- puds = (end + PUD_SIZE - 1) >> PUD_SHIFT;
- tables = roundup(puds * sizeof(pud_t), PAGE_SIZE);
- if (use_gbpages) {
- unsigned long extra;
- extra = end - ((end>>PUD_SHIFT) << PUD_SHIFT);
- pmds = (extra + PMD_SIZE - 1) >> PMD_SHIFT;
- } else
- pmds = (end + PMD_SIZE - 1) >> PMD_SHIFT;
- /* 计算映射所有内存所要求的所有pmd的个数 */
- tables += roundup(pmds * sizeof(pmd_t), PAGE_SIZE);
- if (use_pse) {
- unsigned long extra;
- extra = end - ((end>>PMD_SHIFT) << PMD_SHIFT);
- #ifdef CONFIG_X86_32
- extra += PMD_SIZE;
- #endif
- ptes = (extra + PAGE_SIZE - 1) >> PAGE_SHIFT;
- } else /* 计算所需要的pte个数 */
- ptes = (end + PAGE_SIZE - 1) >> PAGE_SHIFT;
- tables += roundup(ptes * sizeof(pte_t), PAGE_SIZE);
- #ifdef CONFIG_X86_32
- /* for fixmap */
- /* 加上固定内存映射区的页表数量 */
- tables += roundup(__end_of_fixed_addresses * sizeof(pte_t), PAGE_SIZE);
- #endif
- /*
- * RED-PEN putting page tables only on node 0 could
- * cause a hotspot and fill up ZONE_DMA. The page tables
- * need roughly 0.5KB per GB.
- */
- #ifdef CONFIG_X86_32
- start = 0x7000; /* 页表存放的开始地址,这里为什么从这里开始? */
- #else
- start = 0x8000;
- #endif
- /* 从e820.map中找到连续的足够大小的内存来存放用于映射的页表,
- 返回起始地址 */
- e820_table_start = find_e820_area(start, max_pfn_mapped<<PAGE_SHIFT,
- tables, PAGE_SIZE);
- if (e820_table_start == -1UL)
- panic("Cannot find space for the kernel page tables");
- /* 将页表起始地址的物理页面帧号保存到相关的全局变量中 */
- e820_table_start >>= PAGE_SHIFT;
- e820_table_end = e820_table_start;
- e820_table_top = e820_table_start + (tables >> PAGE_SHIFT);
- printk(KERN_DEBUG "kernel direct mapping tables up to %lx @ %lx-%lx\n",
- end, e820_table_start << PAGE_SHIFT, e820_table_top << PAGE_SHIFT);
- }
- struct map_range {
- unsigned long start;
- unsigned long end;
- unsigned page_size_mask;
- };
- #ifdef CONFIG_X86_32
- #define NR_RANGE_MR 3
- #else /* CONFIG_X86_64 */
- #define NR_RANGE_MR 5
- #endif
- /* 将要映射的页面范围保存到mr数组中 */
- static int __meminit save_mr(struct map_range *mr, int nr_range,
- unsigned long start_pfn, unsigned long end_pfn,
- unsigned long page_size_mask)
- {
- if (start_pfn < end_pfn) {
- if (nr_range >= NR_RANGE_MR)
- panic("run out of range for init_memory_mapping\n");
- mr[nr_range].start = start_pfn<<PAGE_SHIFT;
- mr[nr_range].end = end_pfn<<PAGE_SHIFT;
- mr[nr_range].page_size_mask = page_size_mask;
- nr_range++;
- }
- return nr_range;
- }
(1)save_mr()将要映射的页面范围start_pfn~end_pfn保存到数组mr的一个元素中去。
(2)find_early_table_space()先计算映射所需的pud, pmd, pte个数,对32位系统,页表存放的起始地址为0x7000。然后,调用find_e820_area()从e820.map中找到连续的足够大小的内存来存放用于映射的页表,并将页表起始地址的物理页面帧号保存到相关的全局变量中。
4、内核空间映射kernel_physical_mapping_init()分析
对32位系统,该函数在arch/x86/mm/init_32.c中。它把低端区的所有max_low_pfn个物理内存页面映射到内核虚拟地址空间,映射页表从内核空间的起始地址处开始创建,即从PAGE_OFFSET(0xc0000000)开始的整个内核空间,直到物理内存映射完毕。理解了这个函数,就能大概理解内核是如何建立页表的,从而完整地弄清这个抽象模型。如下:
- unsigned long __init
- kernel_physical_mapping_init(unsigned long start,
- unsigned long end,
- unsigned long page_size_mask)
- {
- int use_pse = page_size_mask == (1<<PG_LEVEL_2M);
- unsigned long start_pfn, end_pfn;
- pgd_t *pgd_base = swapper_pg_dir;
- int pgd_idx, pmd_idx, pte_ofs;
- unsigned long pfn;
- pgd_t *pgd;
- pmd_t *pmd;
- pte_t *pte;
- unsigned pages_2m, pages_4k;
- int mapping_iter;
- /* 得到要映射的起始地址和终止地址所在页在页帧号 */
- start_pfn = start >> PAGE_SHIFT;
- end_pfn = end >> PAGE_SHIFT;
- /*
- * First iteration will setup identity mapping using large/small pages
- * based on use_pse, with other attributes same as set by
- * the early code in head_32.S
- *
- * Second iteration will setup the appropriate attributes (NX, GLOBAL..)
- * as desired for the kernel identity mapping.
- *
- * This two pass mechanism conforms to the TLB app note which says:
- *
- * "Software should not write to a paging-structure entry in a way
- * that would change, for any linear address, both the page size
- * and either the page frame or attributes."
- */
- mapping_iter = 1;
- if (!cpu_has_pse)
- use_pse = 0;
- repeat:
- pages_2m = pages_4k = 0;
- pfn = start_pfn;
- /* 返回页框在PGD表中的索引 */
- pgd_idx = pgd_index((pfn<<PAGE_SHIFT) + PAGE_OFFSET);
- pgd = pgd_base + pgd_idx;
- for (; pgd_idx < PTRS_PER_PGD; pgd++, pgd_idx++) {
- pmd = one_md_table_init(pgd); /* 创建该pgd目录项指向的pmd表 */
- if (pfn >= end_pfn)
- continue;
- #ifdef CONFIG_X86_PAE
- /* 三级映射需要设置pmd,因此得到页框在PMD表中的索引 */
- pmd_idx = pmd_index((pfn<<PAGE_SHIFT) + PAGE_OFFSET);
- pmd += pmd_idx;
- #else
- pmd_idx = 0; /* 两级映射则无需设置 */
- #endif
- for (; pmd_idx < PTRS_PER_PMD && pfn < end_pfn;
- pmd++, pmd_idx++) {
- unsigned int addr = pfn * PAGE_SIZE + PAGE_OFFSET;
- /*
- * 如果可能,用大页面来映射,否则创建正常大小的页表:
- */
- if (use_pse) {
- unsigned int addr2;
- pgprot_t prot = PAGE_KERNEL_LARGE;
- /*
- * first pass will use the same initial
- * identity mapping attribute + _PAGE_PSE.
- */
- pgprot_t init_prot =
- __pgprot(PTE_IDENT_ATTR |
- _PAGE_PSE);
- addr2 = (pfn + PTRS_PER_PTE-1) * PAGE_SIZE +
- PAGE_OFFSET + PAGE_SIZE-1;
- if (is_kernel_text(addr) ||
- is_kernel_text(addr2))
- prot = PAGE_KERNEL_LARGE_EXEC;
- pages_2m++;
- if (mapping_iter == 1)
- set_pmd(pmd, pfn_pmd(pfn, init_prot));
- else
- set_pmd(pmd, pfn_pmd(pfn, prot));
- pfn += PTRS_PER_PTE;
- continue;
- }
- pte = one_page_table_init(pmd); /* 返回PMD中第一个PTE */
- /* PTE的索引 */
- pte_ofs = pte_index((pfn<<PAGE_SHIFT) + PAGE_OFFSET);
- pte += pte_ofs; /* 定位带具体的pte */
- for (; pte_ofs < PTRS_PER_PTE && pfn < end_pfn;
- pte++, pfn++, pte_ofs++, addr += PAGE_SIZE) {
- pgprot_t prot = PAGE_KERNEL;
- /*
- * first pass will use the same initial
- * identity mapping attribute.
- */
- pgprot_t init_prot = __pgprot(PTE_IDENT_ATTR);
- if (is_kernel_text(addr))
- prot = PAGE_KERNEL_EXEC;
- pages_4k++; /* 没有PSE */
- /* 设置页表,根据MAPPING_ITER变量的不同
- 对表设置不同的属性 */
- if (mapping_iter == 1) /* 第一次迭代,属性设置都一样 */
- set_pte(pte, pfn_pte(pfn, init_prot));
- else /* 设置为具体的属性 */
- set_pte(pte, pfn_pte(pfn, prot));
- }
- }
- }
- if (mapping_iter == 1) {
- /*
- * 只在第一次迭代中更新直接映射页的数量
- */
- update_page_count(PG_LEVEL_2M, pages_2m);
- update_page_count(PG_LEVEL_4K, pages_4k);
- /*
- * local global flush tlb, which will flush the previous
- * mappings present in both small and large page TLB's.
- */
- __flush_tlb_all();
- /*
- * 第二次迭代将设置实际的PTE属性
- */
- mapping_iter = 2;
- goto repeat;
- }
- return 0; /* 迭代两后返回 */
- }
- static pmd_t * __init one_md_table_init(pgd_t *pgd)
- {
- pud_t *pud;
- pmd_t *pmd_table;
- #ifdef CONFIG_X86_PAE
- /* 启用了PAE,需要三级映射,创建PMD表 */
- if (!(pgd_val(*pgd) & _PAGE_PRESENT)) {
- if (after_bootmem)
- pmd_table = (pmd_t *)alloc_bootmem_pages(PAGE_SIZE);
- else
- pmd_table = (pmd_t *)alloc_low_page();
- paravirt_alloc_pmd(&init_mm, __pa(pmd_table) >> PAGE_SHIFT);
- /* 设置PGD,将对应的PGD项设置为PMD表 */
- set_pgd(pgd, __pgd(__pa(pmd_table) | _PAGE_PRESENT));
- pud = pud_offset(pgd, 0);
- BUG_ON(pmd_table != pmd_offset(pud, 0));
- return pmd_table;
- }
- #endif
- /* 非PAE模式:只需二级映射,直接返回原来pgd地址 */
- pud = pud_offset(pgd, 0);
- pmd_table = pmd_offset(pud, 0);
- return pmd_table;
- }
- static pte_t * __init one_page_table_init(pmd_t *pmd)
- {
- if (!(pmd_val(*pmd) & _PAGE_PRESENT)) {
- pte_t *page_table = NULL;
- if (after_bootmem) {
- #if defined(CONFIG_DEBUG_PAGEALLOC) || defined(CONFIG_KMEMCHECK)
- page_table = (pte_t *) alloc_bootmem_pages(PAGE_SIZE);
- #endif
- if (!page_table)
- page_table =
- (pte_t *)alloc_bootmem_pages(PAGE_SIZE);
- } else /* 如果启动分配器还没有建立,那么
- 从刚才分配建立的表中分配空间 */
- page_table = (pte_t *)alloc_low_page();
- paravirt_alloc_pte(&init_mm, __pa(page_table) >> PAGE_SHIFT);
- /* 设置PMD,将对应的PMD项设置为页表 */
- set_pmd(pmd, __pmd(__pa(page_table) | _PAGE_TABLE));
- BUG_ON(page_table != pte_offset_kernel(pmd, 0));
- }
- return pte_offset_kernel(pmd, 0);
- }
- static inline int is_kernel_text(unsigned long addr)
- {
- if (addr >= PAGE_OFFSET && addr <= (unsigned long)__init_end)
- return 1;
- return 0;
- }
(1)函数开始定义了几个变量,pgd_base指向临时全局页表起始地址(即swapper_pg_dir)。pgd指向一个页表目录项开始的地址,pmd指向一个中间目录开始的地址,pte指向一个页表开始的地址,start_pfn为要映射的起始地址所在物理页框号,end_pfn为终止地址所在物理页框号。
(2)函数实现采用两次迭代的方式来实现。第一次迭代使用基于use_pse标志的大内存页或小内存页来进行映射,其他属性则与前期head_32.S中的设置一致。第二次迭代设置内核映射需要的一些特别属性(NX, GLOBAL等)。这种两次迭代的实现方式是为了遵循TLB应用程序的理念,即对任何线性地址,软件不应该用改变页面大小或者物理页框及属性的方式来对页表条目进行写操作。TLB即Translation Lookaside Buffer,旁路转换缓冲,或称为页表缓冲;里面存放的是一些页表(虚拟地址到物理地址的转换表)。又称为快表技术。由于“页表”存储在主存储器中,查询页表所付出的代价很大,由此产生了TLB。
在前面的“内存模型”中介绍过,x86系统使用三级页表机制,第一级页表称为页全局目录pgd,第二级为页中间目录pmd,第三级为页表条目pte。TLB和CPU里的一级、二级缓存之间不存在本质的区别,只不过前者缓存页表数据,而后两个缓存实际数据。当CPU执行机构收到应用程序发来的虚拟地址后,首先到TLB中查找相应的页表数据,如果TLB中正好存放着所需的页表,则称为TLB命中(TLB Hit),接下来CPU再依次看TLB中页表所对应的物理内存地址中的数据是不是已经在一级、二级缓存里了,若没有则到内存中取相应地址所存放的数据。既然说TLB是内存里存放的页表的缓存,那么它里边存放的数据实际上和内存页表区的数据是一致的,在内存的页表区里,每一条记录虚拟页面和物理页框对应关系的记录称之为一个页表条目(Entry),同样地,在TLB里边也缓存了同样大小的页表条目(Entry)。
(3)迭代开始时,pgd_idx根据pgd_index宏计算出开始页框在PGD表中的索引,注意内核要从页目录表中第768个表项开始进行设置,因此索引值会从768开始。 从768到1024这个256个表项被linux内核设置成内核目录项,低768个目录项被用户空间使用。 pgd = pgd_base + pgd_idx使得pgd指向页框所在的pgd目录项。接下来的循环是要填充从该索引值到1024的这256个pgd目录项的内容。对其中每个表项,调用one_md_table_init()创建下一级pmd表,并让pgd表中的目录项指向它。其中若启用了PAE,则Linux需要三级分页以处理大内存页,因此创建pmd表;若没启用PAE,则只需二级映射,这会忽略pmd中间目录表的,因此通过pmd_offset直接返回pgd的地址。
(4)对Linux三级映射模型,需要继续设置pmd表。因此用pmd_index宏计算出页框在PMD表中的索引,定位到对应的pmd目录项,然后用一个循环填充各个pmd目录项的内容(二级映射则直接忽略些循环)。对每个pmd目录项,先计算出物理页框要映射到的内核空间线性地址addr,从代码可以看到它从0xc000000开始的,也就是从内核空间开始。根据use_pse标志来决定是使用大内存页映射,如果是使用普通的4K内存页映射,则调用one_page_table_init()创建一个最终的页表pte,并让pmd目录项指向它。在该函数中,若启动分配器已建立,则利用alloc_bootmem_low_pages()分配一个4k大小的物理页面,否则从刚才分配建立的表中分配空间。然后用set_pmd(pmd, __pmd(__pa(page_table) | _PAGE_TABLE))来设置对应pmd表项。page_table显然属于线性地址,先通过__pa宏转化为物理地址,再与上_PAGE_TABLE宏,此时它们还是无符号整数,再通过__pmd宏把无符号整数转化为pmd类型,经过这些转换,就得到了一个具有属性的表项,然后通过set_pmd宏设置pmd表项。
(5)设置pte表也是一个循环。pte表中有1024个表项,先要计算出要映射的页框所在的表项索引值,然后对每个页表项,用__pgprot(PTE_IDENT_ATTR)获取同一个初始化映射属性,因为在第一次迭代中使用这个属性。 is_kernel_text函数判断addr线性地址是否属于内核代码段。PAGE_OFFSET表示内核代码段的开始地址,__init_end是个内核符号,在内核链接的时候生成的,表示内核代码段的终止地址。如果是,那么在设置页表项的时候就要加个PAGE_KERNEL_EXEC属性,如果不是,则加个PAGE_KERNEL属性。第二次迭代会使用这个属性。这些属性定义可以在arch/x86/include/asm/pgtable_types.h中找到。最后通过set_pte(pte, pfn_pte(pfn, ...))来设置页表项,先通过pfn_pte宏根据页框号和页表项的属性值合并成一个页表项值,然户在用set_pte宏把页表项值写到页表项里。注意第一次迭代设置的是init_prot中的属性,第二次迭代设置prot中的属性。
(6)是后,对第一次迭代,还要更新直接映射页面数。并调用__flush_tlb_all()刷新小内存页或大内存页的TLB中的映射内容。
在开始的init_memory_mapping()执行中,当通过kernel_physical_mapping_init()建立完低端物理内存区与内核空间的三级页表映射后,内核页表就设置好了。然后调用early_ioremap_page_table_range_init()初始化高端内存的固定映射区。
5、高端内存固定映射区的初始化
early_ioremap_page_table_range_init()函数也是在arch/x86/mm/init_32.c中。它只是对固定映射区创建页表结构,并不建立实际映射,实际映射将由set_fixmap()来完成。如下:
- void __init early_ioremap_page_table_range_init(void)
- {
- pgd_t *pgd_base = swapper_pg_dir;
- unsigned long vaddr, end;
- /*
- * 固定映射,只是创建页表结构,并不建立实际映射。实际映射将由set_fixmap()来完成:
- */
- vaddr = __fix_to_virt(__end_of_fixed_addresses - 1) & PMD_MASK;
- end = (FIXADDR_TOP + PMD_SIZE - 1) & PMD_MASK;
- /* 这里是对临时映射区域进行映射而为页表等分配了空间,
- 但是没有建立实际的映射 */
- page_table_range_init(vaddr, end, pgd_base);
- /* 置变量after_paging_init为1,表示启动了分页机制 */
- early_ioremap_reset();
- }
- static void __init
- page_table_range_init(unsigned long start, unsigned long end, pgd_t *pgd_base)
- {
- int pgd_idx, pmd_idx;
- unsigned long vaddr;
- pgd_t *pgd;
- pmd_t *pmd;
- pte_t *pte = NULL;
- vaddr = start;
- pgd_idx = pgd_index(vaddr);
- pmd_idx = pmd_index(vaddr);
- pgd = pgd_base + pgd_idx;
- for ( ; (pgd_idx < PTRS_PER_PGD) && (vaddr != end); pgd++, pgd_idx++) {
- pmd = one_md_table_init(pgd);
- pmd = pmd + pmd_index(vaddr);
- for (; (pmd_idx < PTRS_PER_PMD) && (vaddr != end);
- pmd++, pmd_idx++) {
- /* early fixmap可能对临时映射区中的页表项已经分配了页表,
- 为使页表分配的空间连续,需要对临时映射区的页表指定区间重新分配 */
- /* 在这里已经对pte进行了分配和初始化 */
- pte = page_table_kmap_check(one_page_table_init(pmd),
- pmd, vaddr, pte);
- vaddr += PMD_SIZE;
- }
- pmd_idx = 0;
- }
- }
(1)先计算出固定映射区的起始和终止地址,然后调用page_table_range_init(),用新的bootmem页表项初始化这段高端物理内存要映射到的内核虚拟地址空间,但并不建立实际的映射。最后用early_ioremap_reset()设置after_paging_init为1,表示启动分页机制。
(2)在函数page_table_range_init()中,先获取起址的pgd表项索引、pmd表项索引,然后类似地建立下一级pmd表,和最终的pte页表。在建立页表时需要调用page_table_kmap_check()进行检查,因为在前期可能对固定映射区已经分配了页表项,为使页表分配的空间连续,需要对固定映射区的页表指定区间重新分配。
在init_memory_mapping()中,内核设置好内核页表,并初始化完高端固定映射区后,紧接着调用load_cr3(swapper_pg_dir),将页全局目录表基址swapper_pg_dir送入控制寄存器cr3。每当重新设置cr3时, CPU就会将页面映射目录所在的页面装入CPU内部高速缓存中的TLB部分。现在内存中(实际上是高速缓存中)的映射目录变了,就要再让CPU装入一次。由于页面映射机制本来就是开启着的,所以从load_cr3这条指令执行完以后就扩大了系统空间中有映射区域的大小, 使整个映射覆盖到整个物理内存(高端内存除外)。实际上此时swapper_pg_dir中已经改变的目录项很可能还在高速缓存中,所以还要通过__flush_tlb_all()将高速缓存中的内容冲刷到内存中,这样才能保证内存中映射目录内容的一致性。
通过上述对init_memory_mapping()的剖析,我们可以清晰的看到,构建内核页表,无非就是向相应的表项写入下一级地址和属性。在内核空间保留着一部分内存专门用来存放内核页表。当cpu要进行寻址的时候,无论在内核空间,还是在用户空间,都会通过这个页表来进行映射。对于这个函数,内核把整个物理内存空间都映射完了,当用户空间的进程要使用物理内存时,岂不是不能做相应的映射了?其实不会的,内核只是做了映射,映射不代表使用,这样做是内核为了方便管理内存而已。
Linux内存管理(5):分页机制和管理区初始化
在内存子系统初始化以前,即boot阶段也需要进行内存管理,启动内存分配器是专为此而设计的。linux启动内存分配器是在伙伴系统、slab机制实现之前,为满足内核中内存的分配而建立的。本身的机制比较简单,使用位图来进行标志分配和释放。arch/x86/kernel/setup.c:setup_arch()在用init_memory_mapping(0, max_low_pfn<<PAGE_SHIFT)建立完内核页表之后,就会调用arch/x86/mm/init_32.c:initmem_init(0, max_pfn)启动bootmem内存分配器。如下:
- #ifndef CONFIG_NEED_MULTIPLE_NODES
- void __init initmem_init(unsigned long start_pfn,
- unsigned long end_pfn)
- {
- #ifdef CONFIG_HIGHMEM
- highstart_pfn = highend_pfn = max_pfn;
- if (max_pfn > max_low_pfn)
- highstart_pfn = max_low_pfn;
- /* 注册内存活动区 */
- e820_register_active_regions(0, 0, highend_pfn);
- sparse_memory_present_with_active_regions(0);
- printk(KERN_NOTICE "%ldMB HIGHMEM available.\n",
- pages_to_mb(highend_pfn - highstart_pfn));
- num_physpages = highend_pfn;
- /* 计算高端内存地址 */
- high_memory = (void *) __va(highstart_pfn * PAGE_SIZE - 1) + 1;
- #else
- e820_register_active_regions(0, 0, max_low_pfn);
- sparse_memory_present_with_active_regions(0);
- num_physpages = max_low_pfn;
- high_memory = (void *) __va(max_low_pfn * PAGE_SIZE - 1) + 1;
- #endif
- #ifdef CONFIG_FLATMEM
- max_mapnr = num_physpages;
- #endif
- __vmalloc_start_set = true;
- printk(KERN_NOTICE "%ldMB LOWMEM available.\n",
- pages_to_mb(max_low_pfn));
- setup_bootmem_allocator(); /* 启动内存分配器 */
- }
- #endif /* !CONFIG_NEED_MULTIPLE_NODES */
函数e820_register_active_regions()在arch/x86/kernel/e820.c中,它扫描e820内存图,并在一个节点nid上注册活动区。如下:
- /* 扫描e820内存图,并在一个节点上注册活动区 */
- void __init e820_register_active_regions(int nid, unsigned long start_pfn,
- unsigned long last_pfn)
- {
- unsigned long ei_startpfn;
- unsigned long ei_endpfn;
- int i;
- for (i = 0; i < e820.nr_map; i++)
- /* 从全局变量e820中查找活动区 */
- if (e820_find_active_region(&e820.map[i],
- start_pfn, last_pfn,
- &ei_startpfn, &ei_endpfn))
- /* 添加查找到的活动区 */
- add_active_range(nid, ei_startpfn, ei_endpfn);
- }
- /*
- * 在start_pfn到last_pfn的地址范围内查找一个活动区,并在ei_startpfn和ei_endpfn中返回
- * 这个e820内存块的范围
- */
- int __init e820_find_active_region(const struct e820entry *ei,
- unsigned long start_pfn,
- unsigned long last_pfn,
- unsigned long *ei_startpfn,
- unsigned long *ei_endpfn)
- {
- u64 align = PAGE_SIZE;
- *ei_startpfn = round_up(ei->addr, align) >> PAGE_SHIFT;
- *ei_endpfn = round_down(ei->addr + ei->size, align) >> PAGE_SHIFT;
- /* 跳过内存图中比一个页面还小的各个内存块 */
- if (*ei_startpfn >= *ei_endpfn)
- return 0;
- /* 如果内存图中的所有内存块都在节点范围之外,则跳过 */
- if (ei->type != E820_RAM || *ei_endpfn <= start_pfn ||
- *ei_startpfn >= last_pfn)
- return 0;
- /* 检查是否有重叠 */
- if (*ei_startpfn < start_pfn)
- *ei_startpfn = start_pfn;
- if (*ei_endpfn > last_pfn)
- *ei_endpfn = last_pfn;
- return 1;
- }
- /* 添加活动区域,需要对原有的进行检查 */
- void __init add_active_range(unsigned int nid, unsigned long start_pfn,
- unsigned long end_pfn)
- {
- int i;
- mminit_dprintk(MMINIT_TRACE, "memory_register",
- "Entering add_active_range(%d, %#lx, %#lx) "
- "%d entries of %d used\n",
- nid, start_pfn, end_pfn,
- nr_nodemap_entries, MAX_ACTIVE_REGIONS);
- mminit_validate_memmodel_limits(&start_pfn, &end_pfn);
- /* 如果可能,与存在的活动内存区合并 */
- for (i = 0; i < nr_nodemap_entries; i++) {
- if (early_node_map[i].nid != nid)
- continue;
- /* 如果一个存在的活动区包含这个要添加的新区,则跳过 */
- if (start_pfn >= early_node_map[i].start_pfn &&
- end_pfn <= early_node_map[i].end_pfn)
- return;
- /* 如果合适,则向前合并 */
- if (start_pfn <= early_node_map[i].end_pfn &&
- end_pfn > early_node_map[i].end_pfn) {
- early_node_map[i].end_pfn = end_pfn;
- return;
- }
- /* 如果合适,则向后合并 */
- if (start_pfn < early_node_map[i].end_pfn &&
- end_pfn >= early_node_map[i].start_pfn) {
- early_node_map[i].start_pfn = start_pfn;
- return;
- }
- }
- /* 检查early_node_map是否足够大 */
- if (i >= MAX_ACTIVE_REGIONS) {
- printk(KERN_CRIT "More than %d memory regions, truncating\n",
- MAX_ACTIVE_REGIONS);
- return;
- }
- early_node_map[i].nid = nid;
- early_node_map[i].start_pfn = start_pfn;
- early_node_map[i].end_pfn = end_pfn;
- nr_nodemap_entries = i + 1;
- }
回到arch/x86/mm/init_32.c:initmem_init(),最后是调用arch/x86/mm/init_32.c:setup_bootmem_allocator()建立内核引导时的启动内存分配器。在建立启动内存分配器的时候,会涉及到保留内存。也就是说,当分配器进行内存分配时,之前保留给页表、分配器本身(用于映射的位图)、io的这些保留内存就不能再分配了。linux中对保留内存空间的部分用下列数据结构表示,在arch/x86/kernel/e820.c中:
- /*
- * Early reserved memory areas.
- */
- #define MAX_EARLY_RES 20 /* 保留空间最大块数 */
- struct early_res { /* 保留空间结构 */
- u64 start, end;
- char name[16];
- char overlap_ok;
- };
- /* 保留内存空间全局变量 */
- static struct early_res early_res[MAX_EARLY_RES] __initdata = {
- { 0, PAGE_SIZE, "BIOS data page" }, /* BIOS data page */
- {}
- };
- bootmem分配器的数据结构bootmem_data_t用于管理启动内存的分配、释放等,在include/linux/bootmem.h中,如下:
- /* 用于bootmem分配器的节点数据结构 */
- typedef struct bootmem_data {
- unsigned long node_min_pfn;
- unsigned long node_low_pfn;
- void *node_bootmem_map;
- unsigned long last_end_off;
- unsigned long hint_idx;
- struct list_head list;
- } bootmem_data_t;
全局链表定义可在mm/bootmeme.c中找到,如下:
static struct list_head bdata_list __initdata = LIST_HEAD_INIT(bdata_list);
启动分配器的建立主要的流程为初始化映射位图、活动内存区的映射位置0(表示可用)、保留内存区域处理,其中保留区存放在上面介绍的全局数组中,这里只是将分配器中对应映射位图值1,表示已经分配。核心函数是arch/x86/mm/init_32.c:setup_bootmem_allocator(),以及setup_node_bootmem()。如下:
- void __init setup_bootmem_allocator(void)
- {
- int nodeid;
- unsigned long bootmap_size, bootmap;
- /*
- * 初始化引导时的内存分配器(只是低端内存区):
- */
- /* 计算所需要的映射页面大小一个字节一位,所以需要对总的页面大小除以8 */
- bootmap_size = bootmem_bootmap_pages(max_low_pfn)<<PAGE_SHIFT;
- /* 从e820中查找一个合适的内存块 */
- bootmap = find_e820_area(0, max_pfn_mapped<<PAGE_SHIFT, bootmap_size,
- PAGE_SIZE);
- if (bootmap == -1L)
- panic("Cannot find bootmem map of size %ld\n", bootmap_size);
- /* 将用于位图映射的页面保留 */
- reserve_early(bootmap, bootmap + bootmap_size, "BOOTMAP");
- printk(KERN_INFO " mapped low ram: 0 - %08lx\n",
- max_pfn_mapped<<PAGE_SHIFT);
- printk(KERN_INFO " low ram: 0 - %08lx\n", max_low_pfn<<PAGE_SHIFT);
- /* 扫描每个在线节点 */
- for_each_online_node(nodeid) {
- unsigned long start_pfn, end_pfn;
- #ifdef CONFIG_NEED_MULTIPLE_NODES
- /* 计算出当前节点的起始地址和终止地址 */
- start_pfn = node_start_pfn[nodeid];
- end_pfn = node_end_pfn[nodeid];
- if (start_pfn > max_low_pfn)
- continue;
- if (end_pfn > max_low_pfn)
- end_pfn = max_low_pfn;
- #else
- start_pfn = 0;
- end_pfn = max_low_pfn;
- #endif
- /* 对指定节点安装启动分配器 */
- bootmap = setup_node_bootmem(nodeid, start_pfn, end_pfn,
- bootmap);
- }
- /* bootmem的分配制度到这里就已经建立完成,把after_bootmem变量置成1 */
- after_bootmem = 1;
- }
- static unsigned long __init setup_node_bootmem(int nodeid,
- unsigned long start_pfn,
- unsigned long end_pfn,
- unsigned long bootmap)
- {
- unsigned long bootmap_size;
- /* 初始化这个内存节点:将映射位图中的所有位置1。不要触及min_low_pfn */
- bootmap_size = init_bootmem_node(NODE_DATA(nodeid),
- bootmap >> PAGE_SHIFT,
- start_pfn, end_pfn);
- printk(KERN_INFO " node %d low ram: %08lx - %08lx\n",
- nodeid, start_pfn<<PAGE_SHIFT, end_pfn<<PAGE_SHIFT);
- printk(KERN_INFO " node %d bootmap %08lx - %08lx\n",
- nodeid, bootmap, bootmap + bootmap_size);
- /* 将活动内存区对应位图相关位置0,表示可被分配的 */
- free_bootmem_with_active_regions(nodeid, end_pfn);
- /* 将保留内存的相关页面对应位置为1,表示已经分配
- 或者不可用(不能被分配) */
- early_res_to_bootmem(start_pfn<<PAGE_SHIFT, end_pfn<<PAGE_SHIFT);
- /* 返回映射页面的最后地址,下次映射即可以从这里开始 */
- return bootmap + bootmap_size;
- }
mm/bootmem.c实现了完整的引导时物理内存分配器和配置器,包括内存节点初始化、内存分配、释放等各种操作。我们概述一下启动内存分配器的主要操作接口功能:
init_bootmem_node():注册一个节点以作为启动内存。核心操作由init_bootmem_core()完成,每调用它一次来设置自己的分配器。
link_bootmem():按顺序添加一个bdata到全局的bdata_list链表中。
free_all_bootmem_node():释放一个节点的可用页面给伙伴系统。核心操作由free_all_bootmem_core()完成。
free_bootmem_node():将指定节点上的一个页面范围标记为可用(即未分配)。
reserve_bootmem_node():将指定节点上的一个页面范围标记为保留。
__alloc_bootmem_node():为指定节点分配启动内存。核心操作由alloc_bootmem_core()完成。
__free():bootmem分配器的释放内存操作。
__reserve():bootmem分配器的保留内存操作。
alloc_bootmem_core():bootmem分配器的分配内存操作。
2、建立永久的分页机制
在前面的“内存映射机制“介绍中,init_memory_mapping()只是构建了内核页表,作为临时的分页映射。例如只对高端内存固定映射区创建了页表结构,并没有对高端内存区永久映射区进行初始化。setup_arch()在执行完init_memory_mapping()和initmem_init()后,就会调用arch/x86/mm/init_32.c:paging_init()建立虚拟内存管理要用到的完整页表和永久分页机制。如下:
- void __init paging_init(void)
- {
- pagetable_init();
- __flush_tlb_all();
- kmap_init();
- /*
- * NOTE: 在这里bootmem分配器完全可用了
- */
- sparse_init();
- zone_sizes_init();
- }
arch/x86/mm/init_32.c:pagetable_init()函数用于完成页表初始化,并初始化高端内存永久映射区。如下:
- static void __init pagetable_init(void)
- {
- pgd_t *pgd_base = swapper_pg_dir;
- permanent_kmaps_init(pgd_base);
- }
- #ifdef CONFIG_HIGHMEM
- static void __init permanent_kmaps_init(pgd_t *pgd_base)
- {
- unsigned long vaddr;
- pgd_t *pgd;
- pud_t *pud;
- pmd_t *pmd;
- pte_t *pte;
- vaddr = PKMAP_BASE;
- /* 该阶段,也就是永久内存映射区的页表初始化 */
- page_table_range_init(vaddr, vaddr + PAGE_SIZE*LAST_PKMAP, pgd_base);
- pgd = swapper_pg_dir + pgd_index(vaddr);
- pud = pud_offset(pgd, vaddr);
- pmd = pmd_offset(pud, vaddr);
- pte = pte_offset_kernel(pmd, vaddr);
- /* 将永久映射区间映射的第一个页表项保存到pkmap_page_table中 */
- pkmap_page_table = pte;
- }
- /* ...... */
- #else
- static inline void permanent_kmaps_init(pgd_t *pgd_base)
- {
- }
- #endif /* CONFIG_HIGHMEM */
arch/x86/mm/init_32.c:kmap_init()函数用于缓存第一个kmap页表项,如下:
- static void __init kmap_init(void)
- {
- unsigned long kmap_vstart;
- /*
- * Cache the first kmap pte:
- */
- /* 得到高端固定内存映射区域的起始内存的页表,将这个页表
- 放到kmap_pte变量中。确切的说应该是固定内存中的临时内存映射区域 */
- kmap_vstart = __fix_to_virt(FIX_KMAP_BEGIN);
- kmap_pte = kmap_get_fixmap_pte(kmap_vstart);
- kmap_prot = PAGE_KERNEL;
- }
mm/sparse.c:sparse_init()函数用于初始稀疏内存的映射,这里就不展开了。这里重点介绍管理区初始化,这是内存管理的重要组成部分,在arch/x86/mm/init_32.c:zone_sizes_init()中,如下:
- static void __init zone_sizes_init(void)
- {
- /* 初始化各种管理区中的最大页面数,在后面用于具体的初始化工作 */
- unsigned long max_zone_pfns[MAX_NR_ZONES];
- memset(max_zone_pfns, 0, sizeof(max_zone_pfns));
- max_zone_pfns[ZONE_DMA] = /* DMA区的最大页面帧号,后面的类似 */
- virt_to_phys((char *)MAX_DMA_ADDRESS) >> PAGE_SHIFT;
- max_zone_pfns[ZONE_NORMAL] = max_low_pfn;
- #ifdef CONFIG_HIGHMEM
- max_zone_pfns[ZONE_HIGHMEM] = highend_pfn;
- #endif
- /* 内存体系的MMU建立,包括伙伴系统的初步建立 */
- free_area_init_nodes(max_zone_pfns);
- }
- void __init free_area_init_nodes(unsigned long *max_zone_pfn)
- {
- unsigned long nid;
- int i;
- /* Sort early_node_map as initialisation assumes it is sorted */
- sort_node_map(); /* 将活动区域进行排序 */
- /* 记录管理区的界限 */
- memset(arch_zone_lowest_possible_pfn, 0,
- sizeof(arch_zone_lowest_possible_pfn));
- memset(arch_zone_highest_possible_pfn, 0,
- sizeof(arch_zone_highest_possible_pfn));
- /* 找出活动内存中最小的页面 */
- arch_zone_lowest_possible_pfn[0] = find_min_pfn_with_active_regions();
- arch_zone_highest_possible_pfn[0] = max_zone_pfn[0];
- for (i = 1; i < MAX_NR_ZONES; i++) {
- if (i == ZONE_MOVABLE)
- continue;
- /* 假定区域连续,下一个区域的最小页面为上一个区的最大页面 */
- arch_zone_lowest_possible_pfn[i] =
- arch_zone_highest_possible_pfn[i-1];
- arch_zone_highest_possible_pfn[i] =
- max(max_zone_pfn[i], arch_zone_lowest_possible_pfn[i]);
- }
- /* 对ZONE_MOVABLE区域设置为0 */
- arch_zone_lowest_possible_pfn[ZONE_MOVABLE] = 0;
- arch_zone_highest_possible_pfn[ZONE_MOVABLE] = 0;
- /* 找出每个节点上ZONE_MOVABLE区的开始页面号 */
- memset(zone_movable_pfn, 0, sizeof(zone_movable_pfn));
- find_zone_movable_pfns_for_nodes(zone_movable_pfn);
- /* 打印管理区的范围 */
- printk("Zone PFN ranges:\n");
- for (i = 0; i < MAX_NR_ZONES; i++) {
- if (i == ZONE_MOVABLE)
- continue;
- printk(" %-8s %0#10lx -> %0#10lx\n",
- zone_names[i],
- arch_zone_lowest_possible_pfn[i],
- arch_zone_highest_possible_pfn[i]);
- }
- /* 打印每个节点上ZONE_MOVABLE区开始的页面号 */
- printk("Movable zone start PFN for each node\n");
- for (i = 0; i < MAX_NUMNODES; i++) {
- if (zone_movable_pfn[i])
- printk(" Node %d: %lu\n", i, zone_movable_pfn[i]);
- }
- /* 打印early_node_map[] */
- printk("early_node_map[%d] active PFN ranges\n", nr_nodemap_entries);
- for (i = 0; i < nr_nodemap_entries; i++)
- printk(" %3d: %0#10lx -> %0#10lx\n", early_node_map[i].nid,
- early_node_map[i].start_pfn,
- early_node_map[i].end_pfn);
- /* 初始化每个节点 */
- mminit_verify_pageflags_layout(); /* 调试用 */
- setup_nr_node_ids();
- for_each_online_node(nid) {
- pg_data_t *pgdat = NODE_DATA(nid);
- /* zone中数据的初始化,伙伴系统建立,但是没有页面
- 和数据,页面在后面的mem_init中得到 */
- free_area_init_node(nid, NULL,
- find_min_pfn_for_node(nid), NULL);
- /* 对该节点上的任何内存区 */
- if (pgdat->node_present_pages)
- node_set_state(nid, N_HIGH_MEMORY);
- /* 内存的相关检查 */
- check_for_regular_memory(pgdat);
- }
- }
- void __paginginit free_area_init_node(int nid, 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;
- /* 计算系统中节点nid的所有物理页面并保存在数据结构中 */
- calculate_node_totalpages(pgdat, zones_size, zholes_size);
- /* 当节点只有一个时,将节点的map保存到全局变量中 */
- alloc_node_mem_map(pgdat);
- #ifdef CONFIG_FLAT_NODE_MEM_MAP
- printk(KERN_DEBUG "free_area_init_node: node %d, pgdat %08lx, node_mem_map %08lx\n",
- nid, (unsigned long)pgdat,
- (unsigned long)pgdat->node_mem_map);
- #endif
- /* zone中相关数据的初始化,包括伙伴系统,等待队列,相关变量,
- 数据结构、链表等 */
- free_area_init_core(pgdat, zones_size, zholes_size);
- }
- static void __paginginit free_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);
- pgdat->nr_zones = 0;
- init_waitqueue_head(&pgdat->kswapd_wait);
- pgdat->kswapd_max_order = 0;
- pgdat_page_cgroup_init(pgdat);
- for (j = 0; j < MAX_NR_ZONES; j++) {
- struct zone *zone = pgdat->node_zones + j;
- unsigned long size, realsize, memmap_pages;
- enum lru_list l;
- /* 下面的两个函数会获得指定节点的真实内存大小 */
- size = zone_spanned_pages_in_node(nid, j, zones_size);
- realsize = size - zone_absent_pages_in_node(nid, j,
- zholes_size);
- /*
- * Adjust realsize so that it accounts for how much memory
- * is used by this zone for memmap. This affects the watermark
- * and per-cpu initialisations
- */
- memmap_pages = /* 存放页面所需要的内存大小 */
- PAGE_ALIGN(size * sizeof(struct page)) >> PAGE_SHIFT;
- if (realsize >= memmap_pages) {
- realsize -= memmap_pages;
- if (memmap_pages)
- printk(KERN_DEBUG
- " %s zone: %lu pages used for memmap\n",
- zone_names[j], memmap_pages);
- } else
- printk(KERN_WARNING
- " %s zone: %lu pages exceeds realsize %lu\n",
- zone_names[j], memmap_pages, realsize);
- /* Account for reserved pages */
- if (j == 0 && realsize > dma_reserve) {
- realsize -= dma_reserve; /* 减去为DMA保留的页面 */
- printk(KERN_DEBUG " %s zone: %lu pages reserved\n",
- zone_names[0], dma_reserve);
- }
- /* 如果不是高端内存区 */
- if (!is_highmem_idx(j))
- nr_kernel_pages += realsize;
- nr_all_pages += realsize;
- /* 下面为初始化zone结构的相关变量 */
- zone->spanned_pages = size;
- zone->present_pages = realsize;
- #ifdef CONFIG_NUMA
- zone->node = nid;
- zone->min_unmapped_pages = (realsize*sysctl_min_unmapped_ratio)
- / 100;
- zone->min_slab_pages = (realsize * sysctl_min_slab_ratio) / 100;
- #endif
- 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->prev_priority = DEF_PRIORITY;
- zone_pcp_init(zone);
- for_each_lru(l) { /* 初始化链表 */
- INIT_LIST_HEAD(&zone->lru[l].list);
- zone->reclaim_stat.nr_saved_scan[l] = 0;
- }
- zone->reclaim_stat.recent_rotated[0] = 0;
- zone->reclaim_stat.recent_rotated[1] = 0;
- zone->reclaim_stat.recent_scanned[0] = 0;
- zone->reclaim_stat.recent_scanned[1] = 0;
- zap_zone_vm_stats(zone);
- zone->flags = 0;
- if (!size)
- continue;
- /* 需要定义相关宏 */
- set_pageblock_order(pageblock_default_order());
- /* zone中变量pageblock_flags,表示从启动分配器中进行内存申请 */
- setup_usemap(pgdat, zone, size);
- /* zone中的任务等待队列和zone的伙伴系统(MAX_ORDER个链表)的初始化 */
- ret = init_currently_empty_zone(zone, zone_start_pfn,
- size, MEMMAP_EARLY);
- BUG_ON(ret);
- /* zone中page相关属性的初始化工作 */
- memmap_init(size, nid, j, zone_start_pfn);
- zone_start_pfn += size;
- }
- }
(1)free_area_init_nodes()函数用于初始化所有的节点和它们的管理区数据。它会对系统中每个活动节点(即内存簇)调用free_area_init_node(),使用add_active_range()提供的页面范围来计算各节点上每种管理区和洞的大小。如果两个相邻管理区的最大PFN相同,则表明后面这个管理区是空的。例如,如果arch_max_dma_pfn == arch_max_dma32_pfn,则表明arch_max_dma32_pfn没有页面。我们假定管理区是连续的,即后一种管理区的开始位置紧接着前一种管理区的结束位置。例如ZONE_DMA32开始于at arch_max_dma_pfn。函数先计算各种管理区的下限页面号和上限页面号,保存在两个数组中,对于连续的相邻管理区(只有ZONE_MOVABLE管理区的内存是不连续的),后一个管理区的下限页面号为前一个管理区的上限页面号。而ZONE_MOVABLE的上下限页面号均设为0。然后调用find_zone_movable_pfns_for_nodes()找出每个节点上ZONE_MOVABLE的开始PFN。
(2)对每个节点,调用free_area_init_node(),传入参数为节点ID,各个管理区的大小,节点的开始页面号,各洞的大小。该函数先调用calculate_node_totalpages()计算节点上的所有物理页面,并保存在节点的pgdat数据结构中,从“内存描述”一节中可知,节点pg_data_t结构中保存了该节点的所有管理区数据。然后调用free_area_init_core()初始化各个zone中相关数据,包括伙伴系统、等待队列、相关变量、数据结构、链表等。
(3)free_area_init_core()用于设置管理区的各个数据结构,包括标记管理区的所有页面,标记所有内存空队列,清除内存位图。该函数对节点上的每个管理区,计算它需要映射的真实页面数realsize(即真实内存大小),注意对DMA区这需要减去为DMA保留的页面。然后初始化该管理区的zone数据结构中的相关变量,包括总页面数、真实页面数即realsize、未映射页面数的下限(低于此值时将进行页面回收)、用于slab分配器的页面数下限、保护伙伴系统和页面回收的LRU链表的自旋锁、LRU队列初始化、页面回收状态域、用于管理区使用情况统计的vm_stats置0,等等。最后调用init_currently_empty_zone()初始化zone中的任务等待队列和伙伴系统,调用memmap_init()初始化zone中所有page的相关属性。
3、初始化管理区分配机制
从以上分析可以看出,setup_arch()中的内存管理初始化工作是与体系结构相关的,这里介绍的是x86 32位的情况。start_kernel()在执行完setup_arch()后即建立起永久分页机制,然后就会调用mm/page_alloc.c:build_all_zonelists()来初始化管理区分配机制,它通过对每种管理区维护一个管理区队列来实现分配和回收,因此整个初始化工作的核心就是构建所有的管理区队列。一个分配请求在zonelist数据结构上进行操作,该结构在include/linux/mmzone.h中,如下:
- #ifdef CONFIG_NUMA
- #define MAX_ZONELISTS 2
- struct zonelist_cache {
- unsigned short z_to_n[MAX_ZONES_PER_ZONELIST]; /* zone->nid */
- DECLARE_BITMAP(fullzones, MAX_ZONES_PER_ZONELIST); /* zone full? */
- unsigned long last_full_zap; /* when last zap'd (jiffies) */
- };
- #else
- #define MAX_ZONELISTS 1
- struct zonelist_cache;
- #endif
- struct zoneref {
- struct zone *zone; /* Pointer to actual zone */
- int zone_idx; /* zone_idx(zoneref->zone) */
- };
- struct zonelist {
- struct zonelist_cache *zlcache_ptr; // NULL or &zlcache
- struct zoneref _zonerefs[MAX_ZONES_PER_ZONELIST + 1];
- #ifdef CONFIG_NUMA
- struct zonelist_cache zlcache; // optional ...
- #endif
- };
在zonelist中,zlcache_ptr指针用来标识是否有zlcache。如果非空,则就是zlcache的地址;如果为空,则表示没有zlcache。为了加快zonelist的读取速度,zoneref保存了要读取条目的管理区索引。include/linux/mmzone.h中定义了一些访问zoneref的函数。zonelist_zone()函数返回zoneref中的zone,zonelist_zone_idx()为一个条目返回管理区索引,zonelist_node_idx()为一个条目返回zone中的节点索引。
mm/page_alloc.c:build_all_zonelists()函数如下,这里介绍非NUMA的情况:
- static void zoneref_set_zone(struct zone *zone, struct zoneref *zoneref)
- {
- zoneref->zone = zone;
- zoneref->zone_idx = zone_idx(zone);
- }
- /*
- * 构建管理区环形分配队列,把节点上的所有管理区添加到队列中
- */
- static int build_zonelists_node(pg_data_t *pgdat, struct zonelist *zonelist,
- int nr_zones, enum zone_type zone_type)
- {
- struct zone *zone;
- BUG_ON(zone_type >= MAX_NR_ZONES);
- zone_type++;
- do {
- zone_type--;
- zone = pgdat->node_zones + zone_type;
- if (populated_zone(zone)) { /* 如果以页面为单位的管理区的总大小不为0 */
- zoneref_set_zone(zone, /* 将管理区添加到链表中 */
- &zonelist->_zonerefs[nr_zones++]);
- check_highest_zone(zone_type);
- }
- } while (zone_type);
- return nr_zones;
- }
- #ifdef CONFIG_NUMA
- /* ...... */
- static void build_zonelists(pg_data_t *pgdat)
- {
- /* ...... */
- }
- static void build_zonelist_cache(pg_data_t *pgdat)
- {
- /* ...... */
- }
- #else /* non CONFIG_NUMA */
- /* ...... */
- static void build_zonelists(pg_data_t *pgdat)
- {
- int node, local_node;
- enum zone_type j;
- struct zonelist *zonelist;
- local_node = pgdat->node_id;
- zonelist = &pgdat->node_zonelists[0];
- /* 将zone添加到zone链表中,这样,zone中page的
- 分配等操作将依靠这个环形的链表 */
- j = build_zonelists_node(pgdat, zonelist, 0, MAX_NR_ZONES - 1);
- /*
- * Now we build the zonelist so that it contains the zones
- * of all the other nodes.
- * We don't want to pressure a particular node, so when
- * building the zones for node N, we make sure that the
- * zones coming right after the local ones are those from
- * node N+1 (modulo N)
- */
- /* 对其他在线的节点创建zonelist */
- for (node = local_node + 1; node < MAX_NUMNODES; node++) {
- if (!node_online(node))
- continue;
- j = build_zonelists_node(NODE_DATA(node), zonelist, j,
- MAX_NR_ZONES - 1);
- }
- for (node = 0; node < local_node; node++) {
- if (!node_online(node))
- continue;
- j = build_zonelists_node(NODE_DATA(node), zonelist, j,
- MAX_NR_ZONES - 1);
- }
- zonelist->_zonerefs[j].zone = NULL;
- zonelist->_zonerefs[j].zone_idx = 0;
- }
- /* 构建zonelist缓存:对非NUMA的zonelist信息,只是把zlcache_ptr设成NULL */
- static void build_zonelist_cache(pg_data_t *pgdat)
- {
- pgdat->node_zonelists[0].zlcache_ptr = NULL;
- }
- #endif /* CONFIG_NUMA */
- /* 返回int值,因为可能通过stop_machine()调用本函数 */
- static int __build_all_zonelists(void *dummy)
- {
- int nid;
- #ifdef CONFIG_NUMA
- memset(node_load, 0, sizeof(node_load));
- #endif
- for_each_online_node(nid) {
- pg_data_t *pgdat = NODE_DATA(nid);
- /* 创建zonelists,这个队列用来在分配内存时回绕,循环访问 */
- build_zonelists(pgdat);
- /* 创建zonelist缓存信息:在非NUMA中,仅仅是把相关缓存变量设成NULL */
- build_zonelist_cache(pgdat);
- }
- return 0;
- }
- void build_all_zonelists(void)
- {
- /* 设置全局变量current_zonelist_order */
- set_zonelist_order();
- /* 对所有节点创建zonelists */
- if (system_state == SYSTEM_BOOTING) { /* 系统正在引导时 */
- __build_all_zonelists(NULL);
- mminit_verify_zonelist(); /* 调试用 */
- cpuset_init_current_mems_allowed();
- } else {
- /* 非引导时要停止所有cpu以确保没有使用zonelist */
- stop_machine(__build_all_zonelists, NULL, NULL);
- /* cpuset refresh routine should be here */
- }
- /* 计算所有zone中可分配的页面数之和 */
- vm_total_pages = nr_free_pagecache_pages();
- /*
- * Disable grouping by mobility if the number of pages in the
- * system is too low to allow the mechanism to work. It would be
- * more accurate, but expensive to check per-zone. This check is
- * made on memory-hotadd so a system can start with mobility
- * disabled and enable it later
- */
- if (vm_total_pages < (pageblock_nr_pages * MIGRATE_TYPES))
- page_group_by_mobility_disabled = 1;
- else
- page_group_by_mobility_disabled = 0;
- printk("Built %i zonelists in %s order, mobility grouping %s. "
- "Total pages: %ld\n",
- nr_online_nodes,
- zonelist_order_name[current_zonelist_order],
- page_group_by_mobility_disabled ? "off" : "on",
- vm_total_pages);
- #ifdef CONFIG_NUMA
- printk("Policy zone: %s\n", zone_names[policy_zone]);
- #endif
- }
(1)build_all_zonelists()调用__build_all_zonelists()来构建所有管理区队列。如果是系统引导时,则直接调用__build_all_zonelists()对所有节点创建zonelist;如果不是引导时,则要通过stop_machine()来调用__build_all_zonelists(),先停止所有CPU以确保没有使用zonelist。然后用nr_free_pagecache_pages()计算所有zone中可分配的页面总数,如果页面总数太小,则禁用页面分组移动功能(因为这个性能开销比较大)。
(2)在__build_all_zonelists()中,对每个在线节点,调用build_zonelists()创建管理区分配的环形队列,调用build_zonelist_cache()创建队列的缓存信息。这两个函数有NUMA版本和非NUMA版本,这里略去NUMA版本,只介绍非NUMA版本。在build_zonelists()中,对每个在线节点,调用build_zonelists_node()构建环形分配队列,把节点上的所有管理区添加到队列中。在build_zonelist_cache()中,对非NUMA的zonelist信息,只是把zlcache_ptr设成NULL。
(3)在build_zonelists_node()中,通过zoneref_set_zone()将每个产生的管理区添加到队列中。
从以上分析可知,内存管理区初始化主要是借助于引导分配器和已初始化的e820全局变量。内存管理区初始化后相应的伙伴系统、slab机制等等就可以在此基础上建立了。