先看一幅图
linux采用四级分页模型,这四种页表是:页全局目录(PGD)、页上级目录(PUD)、页中间目录(PMD)、页表(PTE)。这里的所有页全局目录、页上级目录、页中间目录、页表,它们的大小都是一个页。linux下各个硬件上并不一定都是使用四级目录的,当使用于没有启动物理地址扩展(PAE)的32位系统上时,只使用二级页表,linux会把页上级目录和页中间目录置空。而在启用了物理地址扩展的32位系统上时,linux使用的是三级页表,页上级目录被置空。而在64位系统上,linux根据硬件的情况会选择三级页表或者四级页表。这个整个由线性地址转换到物理地址的过程,是由CPU自动进行的。每个进程都有它自己的页全局目录,当进程运行时,系统会将该进程的页全局目录基地址保存到cr3寄存器中;而当进程被换出时,会将这个cr3保存的页全局目录地址保存到进程描述符中。之后我们还会介绍一个cr2寄存器,用于缺页异常处理的。当进程运行时,它使用的是它自己的一套页表,当它通过系统调用或陷入内核态时,使用的是内核页表,实际上,对于所有的进程页表来说,它们的线性地址0xC0000000以上所涉及到的页表都是主内核页全局目录(保存在init_mm.pgd),它们的内容等于主内核页全局目录的相应表项,这样就实现了所有进程的进程空间相互隔离,但是内核空间相互共享的情况。当某个进程修改了内核页表的一些映射情况后,系统只会相应的修改主内核页全局目录中的表项(只能修改高端内存中非连续内存区的映射),当其他进程访问这些线性地址时,会出现缺页异常,然后修改该进程的页表项重新映射该地址。因为说到每个进程都有它自己的页全局目录,如果有100个进程,内存中就要保存100个进程的整个页表集,看起来会耗费相当多的内存。实际上,只有进程使用到的情况下系统才会分配给进程一条路径,比如我们要求访问一个线性地址,但是这个地址可能对应的页上级目录、页中间目录、页表和页都不存在的,这时系统会产生一个缺页异常,在缺页异常处理中再给进程的这个线性地址分配页上级目录、页中间目录、页表和页所需的物理页框。地址空间一个线性地址经过分页机制转为一个对应的物理地址,我们称之为映射,比如我们的一个线性地址0x00000001经过分页机制处理后,对应的物理地址可能是0xffffff01。在linux系统中分两个地址空间,一个是进程地址空间,一个是内核地址空间。对于每个进程来说,他们都有自己的大小为3G的进程地址空间,这些进程地址空间是相互隔离的,也就是进程A的0x00000001线性地址和进程B的0x00000001线性地址并不是同一个地址,进程A也不能通过自己的进程空间直接访问进程B的进程地址空间。而当线性地址大于3G时(也就是0xC0000000),这里的线性地址属于内核空间,内核地址空间的大小为1G,地址从0xC0000000到0xFFFFFFFF。在内核地址空间中,内核会把前896MB的线性地址直接与物理地址的前896MB进行映射,也就是说,内核地址空间的线性地址0xC0000001所对应的物理地址为0x00000001,它们之间相差一个0xC0000000。linux内核会将物理内存分为3个管理区,分别是:ZONE_DMA:包含0MB~16MB之间的内存页框,可以由老式基于ISA的设备通过DMA使用,直接映射到内核的地址空间。ZONE_NORMAL:包含16MB~896MB之间的内存页框,常规页框,直接映射到内核的地址空间。ZONE_HIGHMEM:包含896MB以上的内存页框,不进行直接映射,可以通过永久映射和临时映射进行这部分内存页框的访问。整个结构如下图
对于ZONE_DMA和ZONE_NORMAL这两个管理区,内核地址都是进行直接映射,只有ZONE_HIGHMEM管理区系统在默认情况下是不进行直接映射的,只有在需要使用的时候进行映射(临时映射或者永久映射)。结点和管理区描述符为了用于NUMA架构,使用了node用来描述一个地方的内存。对于我们PC来说,一台PC就是一个node。node用struct pglist_data结构表示:
typedef struct pglist_data { struct zone node_zones[MAX_NR_ZONES]; struct zonelist node_zonelists[MAX_ZONELISTS]; int nr_zones;#ifdef CONFIG_FLAT_NODE_MEM_MAP struct page *node_mem_map;#ifdef CONFIG_MEMCG struct page_cgroup *node_page_cgroup;#endif#endif#ifndef CONFIG_NO_BOOTMEM struct bootmem_data *bdata;#endif#ifdef CONFIG_MEMORY_HOTPLUG spinlock_t node_size_lock;#endif unsigned long node_start_pfn; unsigned long node_present_pages; unsigned long node_spanned_pages; int node_id; wait_queue_head_t kswapd_wait; wait_queue_head_t pfmemalloc_wait; struct task_struct *kswapd; int kswapd_max_order; enum zone_type classzone_idx;#ifdef CONFIG_NUMA_BALANCING spinlock_t numabalancing_migrate_lock; unsigned long numabalancing_migrate_next_window; unsigned long numabalancing_migrate_nr_pages;#endif} pg_data_t;
我们再看看管理区描述符:
struct zone { unsigned long watermark[NR_WMARK]; long lowmem_reserve[MAX_NR_ZONES];#ifdef CONFIG_NUMA int node;#endif unsigned int inactive_ratio; struct pglist_data *zone_pgdat; struct per_cpu_pageset __percpu *pageset; unsigned long dirty_balance_reserve;#ifndef CONFIG_SPARSEMEM unsigned long *pageblock_flags;#endif #ifdef CONFIG_NUMA unsigned long min_unmapped_pages; unsigned long min_slab_pages;#endif unsigned long zone_start_pfn; unsigned long managed_pages; unsigned long spanned_pages; unsigned long present_pages; const char *name; int nr_migrate_reserve_block;#ifdef CONFIG_MEMORY_ISOLATION unsigned long nr_isolate_pageblock;#endif#ifdef CONFIG_MEMORY_HOTPLUG seqlock_t span_seqlock;#endif wait_queue_head_t *wait_table; unsigned long wait_table_hash_nr_entries; unsigned long wait_table_bits; ZONE_PADDING(_pad1_) spinlock_t lock; struct free_area free_area[MAX_ORDER]; unsigned long flags; ZONE_PADDING(_pad2_) spinlock_t lru_lock; struct lruvec lruvec; atomic_long_t inactive_age; unsigned long percpu_drift_mark;#if defined CONFIG_COMPACTION || defined CONFIG_CMA unsigned long compact_cached_free_pfn; unsigned long compact_cached_migrate_pfn[2];#endif#ifdef CONFIG_COMPACTION unsigned int compact_considered; unsigned int compact_defer_shift; int compact_order_failed;#endif#if defined CONFIG_COMPACTION || defined CONFIG_CMA bool compact_blockskip_flush;#endif ZONE_PADDING(_pad3_) atomic_long_t vm_stat[NR_VM_ZONE_STAT_ITEMS];} ____cacheline_internodealigned_in_smp;
管理区分配器主要做的事情就是将页框通过伙伴系统或者每CPU页框高速缓存分配出去,这里涉及到三个结构,页描述符,伙伴系统,每CPU高速缓存。我们先说说页描述符,页描述符实际上并不专属于描述页框,它还用于描述一个SLAB分配器和SLUB分配器,这个之后再说,我们先说关于页的:struct page { unsigned long flags; union { struct address_space *mapping; void *s_mem; }; struct { union { pgoff_t index; void *freelist; bool pfmemalloc; }; union {#if defined(CONFIG_HAVE_CMPXCHG_DOUBLE) && defined(CONFIG_HAVE_ALIGNED_STRUCT_PAGE) unsigned long counters;#else unsigned counters;#endif struct { union {atomic_t _mapcount;struct { unsigned inuse:16; unsigned objects:15; unsigned frozen:1;};int units; }; atomic_t _count; }; unsigned int active; }; }; union { struct list_head lru; struct { struct page *next; #ifdef CONFIG_64BIT int pages; int pobjects; #else short int pages; short int pobjects;#endif }; struct slab *slab_page; struct rcu_head rcu_head;#if defined(CONFIG_TRANSPARENT_HUGEPAGE) && USE_SPLIT_PMD_PTLOCKS pgtable_t pmd_huge_pte; #endif }; union { unsigned long private; #if USE_SPLIT_PTE_PTLOCKS#if ALLOC_SPLIT_PTLOCKS spinlock_t *ptl;#else spinlock_t ptl;#endif#endif struct kmem_cache *slab_cache; struct page *first_page; };#if defined(WANT_PAGE_VIRTUAL) void *virtual; #endif #ifdef CONFIG_WANT_PAGE_DEBUG_FLAGS unsigned long debug_flags; #endif#ifdef CONFIG_KMEMCHECK void *shadow;#endif#ifdef LAST_CPUPID_NOT_IN_PAGE_FLAGS int _last_cpupid;#endif}
struct free_area { struct list_head free_list[MIGRATE_TYPES]; unsigned long nr_free;};
enum { MIGRATE_UNMOVABLE, MIGRATE_RECLAIMABLE, MIGRATE_MOVABLE, MIGRATE_PCPTYPES, MIGRATE_RESERVE = MIGRATE_PCPTYPES, #ifdef CONFIG_CMA MIGRATE_CMA,#endif#ifdef CONFIG_MEMORY_ISOLATION MIGRATE_ISOLATE, #endif MIGRATE_TYPES};
在从伙伴系统中申请页框时,有可能会遇到一种情况,就是当前需求的连续页框链表上没有可用的空闲页框,这时后,伙伴系统会从下一级获取一个连续长度的页框块,将其拆分放入这级列表;当然在拥有者释放连续页框时伙伴系统也会适当地进行连续页框的合并,并放入下一级中。比如:我需要申请4个页框,但是长度为4个连续页框块链表没有空闲的页框块,伙伴系统会从连续8个页框块的链表获取一个,并将其拆分为两个连续4个页框块,放入连续4个页框块的链表中。释放时道理也一样,会检查释放的这几个页框的之前和之后的物理页框是否空闲,并且能否组成下一级长度的块。每CPU页框高速缓存每CPU页框高速缓存也是一个分配器,配合着伙伴系统进行使用,这个分配器是专门用于分配单个页框的,它维护一个单页框的双向链表,为什么需要这个分配器,原因主要有两点:因为每个CPU都有自己的硬件高速缓存,当对一个页进行读取写入时,首先会把这个页装入硬件高速缓存,而如果进程对这个处于硬件高速缓存的页进行操作后立即释放掉,这个页有可能还保存在硬件高速缓存中,这样我另一个进程需要请求一个页并立即写入数据的话,分配器将这个处于硬件高速缓存中的页分配给它,系统效率会大大增加。减少锁的竞争,假设单页框都是使用free_area来管理,那么多个CPU同时频繁访问时,每次都是只能单CPU获取到页框,其他CPU等待,这会造成大量的锁竞争,导致分配效率降低。在每CPU页框高速缓存中用一个链表来维护一个单页框的双向链表,每个CPU都有自己的链表(因为每个CPU有自己的硬件高速缓存),那些比较可能处于硬件高速缓存中的页被称为“热页”,比较不可能处于硬件高速缓存中的页称为“冷页”。其实系统判断是否为热页还是冷页很简单,越最近释放的页就比较可能是热页,所以在双向链表中,从链表头插入可能是热页的单页框,在链表尾插入可能是冷页的单页框。分配时热页就从链表头获取,冷页就从链表尾获取。在每CPU页框高速缓存中也可能会遇到没有空闲的页框(被分配完了),这时候每CPU页框高速缓存会从伙伴系统中拿出页框放入每CPU页框高速缓存中,相反,如果每CPU页框高速缓存中页框过多,也会将一些页框放回伙伴系统。在内核中使用struct per_cpu_pageset结构描述一个每CPU页框高速缓存,其中的struct per_cpu_pages是核心结构体,如下:struct per_cpu_pageset { struct per_cpu_pages pcp;#ifdef CONFIG_NUMA s8 expire;#endif#ifdef CONFIG_SMP s8 stat_threshold; s8 vm_stat_diff[NR_VM_ZONE_STAT_ITEMS];#endif};struct per_cpu_pages { int count; int high; int batch; struct list_head lists[MIGRATE_PCPTYPES];};