文章

linux内核笔记(五)通用内存的表示

通用内存的表示

基础概念

内存管理涉及的领域

  • 内存中的物理内存页的管理;

  • 分配大块内存的伙伴系统;

  • 分配较小块内存的slab、slub和slob分配器;

  • 分配非连续内存块的vmalloc 机制;

  • 进程的地址空间。

内存地址空间、32位系统的寻址限制

vitual address space

Linux内核将地址空间划分为两个部分。底部比较大的部分用于用户进程,顶部则专用于内核。虽然(在两个用户进程之间的)上下文切换期间会改变下半部分,但虚拟地址空间的内核部分总是保持不变。在IA-32系统上,地址空间在用户进程和内核之间划分的典型比例为3∶1。

如果物理内存比可以映射到内核地址空间中的数量要多,那么内核必须借助于高端内存(highmem)方法来管理超出内存。在IA-32系统上,可以直接管理的物理内存数量不超过896 MiB。超过该值(直到最大4 GiB为止)的内存只能通过高端内存寻址。在64位计算机上,由于可用的地址空间非常巨大 $2^{64}= 16EiB$,因此不需要高端内存模式。

UMA 和 NUMA

cpu memory access

在管理物理内存的方式上,有两种类型的计算机:

  1. UMA计算机(一致内存访问 ,uniform memory access)将可用内存以连续方式组织起来(可能有小的缺口)。SMP系统中的每个处理器访问各个内存区都是同样快。
  2. NUMA计算机(非一致内存访问 ,non-uniform memory access)多处理器计算机。系统的各个CPU都有本地内存,可支持特别快速的访问。各个处理器之间通过总线连接起来,以支持对其他CPU的本地内存的访问(比访问本地内存慢些)。

物理模型对应的内存结构

内核对UMA和非NUMA使用相同的数据结构,因此针对各种不同形式的内存布局,各个算法几乎没有什么差别。在UMA系统上,只使用一个NUMA结点来管理整个系统内存。而内存管理的其他部分则相信它们是在处理一个伪NUMA系统。

结点

内存划分为结点的虚拟结构,每个结点关联到系统中的一个处理器。

结点的数据结构
1
2
3
4
5
6
7
8
9
10
11
12
13
typedef struct pglist_data {
    struct zone node_zones[MAX_NR_ZONES];           //结点中各内存域
    struct zonelist node_zonelists[MAX_ZONELISTS];  //备用结点及其内存域的列表(在当前结点没有可用空间时,在备用结点分配内存)
    int nr_zones;                                   // 结点中可用内存域的数量
    struct page *node_mem_map;                      // 指向page实例数组的指针,用于描述结点的所有物理内存页
    unsigned long node_start_pfn;                   // 该结点第一个页帧的逻辑编号。系统中所有 结点的页帧是依次编号的,每个页帧的号码都是全局唯一的
    unsigned long node_present_pages;               // 结点中可用页帧的数量
    unsigned long node_spanned_pages;               // 结点中物理内存的总量(包含物理内存洞)
    int node_id;                                    // 全局结点ID。系统中的NUMA结点都从0开始编号。
    wait_queue_head_t kswapd_wait;                  // 是交换守护进程(swap daemon)的等待队列,在将页帧换出结点时会用到
    struct task_struct *kswapd;                     //指向负责该结点的交换守护进程的task_struct
    int kswapd_max_order;                           //用于页交换子系统的实现,用来定义需要释放的区域的长度
} pg_data_t;
  • 物理内存洞:物理内存中,某些页帧被保留,不能被分配。这些页帧称为洞。
    • 硬件保留区域:某些硬件设备(如显卡、BIOS、PCI设备等)需要使用特定的物理地址空间。这些设备保留了部分物理地址,导致这部分地址不能用于实际的物理内存。
    • 内存映射 I/O(MMIO): 内存映射 I/O 是指将设备的寄存器映射到系统的地址空间,以便 CPU 可以通过内存地址访问设备。这些映射区域会占用部分物理地址空间,导致实际的内存不能使用这些地址。
    • 系统保留区域:系统引导过程中,BIOS 或 EFI 可能会保留部分内存用于特定用途,例如存储系统表或内存映射表。这些保留区域也会形成内存洞。
结点的状态管理
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
enum node_states {
    N_POSSIBLE,         // 结点在某个时候可能变为联机
    N_ONLINE,           // 结点是联机的
    N_CPU,              //结点有一个或多个CPU
    /* 以上三个状态适用于cpu与内存的热插拨*/
    N_NORMAL_MEMORY,    // 结点有普通内存域
#ifdef CONFIG_HIGHMEM
    N_HIGH_MEMORY,      //结点有普通或高端内存域
#else
    N_HIGH_MEMORY = N_NORMAL_MEMORY,
#endif
    
    NR_NODE_STATES
};
//设置和清除node的状态函数
static inline void node_set_state(int node, enum node_states state)
static inline void node_clear_state(int node, enum node_states state)

内存域

内存域可以是普通内存、高端内存、DMA 内存等,每个内存域都属于一个特定的结点。

内存域的数据结构
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
struct zone {
    unsigned long watermark[NR_WMARK];  //内存域水印,通过*_wmark_pages(zone) 宏访问
    unsigned long percpu_drift_mark;    //当节点可用页面低于此点时,在读取可用页面数时会采取其他步骤,以避免每个cpu计数器偏移导致水印被破坏
    unsigned long   lowmem_reserve[MAX_NR_ZONES];   // 储备的低端内存域,用于一些无论如何都不能失败的关键性内存分配
    struct per_cpu_pageset __percpu *pageset;       // 每个cpu的内存页帧集合 被__percpu修饰,每个cpu会获得此变量的副本

    spinlock_t  lock;
    int                     all_unreclaimable;  // 不可回收的页数
    //#ifdef CONFIG_MEMORY_HOTPLUG    //如果启用了内存热插拔功能
        seqlock_t   span_seqlock;   //见zone_start_pfn下面的注释
    //#endif
    struct free_area    free_area[MAX_ORDER];   //用于实现伙伴系统。每个数组元素都表示某种固定长度的一些连续内存区。对于包含在每个区域中的空闲内存页的管理,free_area 是一个起点

    ZONE_PADDING(_pad1_)        
    /* 
    由ZONE_PADDING 将该结构分隔为几个部分。这是因为内核对此结构的访问非常频繁。在多处理器系统上,通常会有不同的CPU试图同时访问结构成员。因此使用锁防止它们彼此干扰,避免错误和不一致。
    如果数据保存在CPU高速缓存中,那么会处理得更快速。高速缓存分为行,每一行负责不同的内存区。内核使用ZONE_PADDING宏生成填充字段添加到此结构中,以确保每个自旋锁都处于自身的缓存行中。还使用了编译器关键字__cacheline_maxaligned_in_smp ,用以实现最优的高速缓存对齐方式。
    */

    /* 页面回收扫描程序通常访问的字段 */
    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_flags_t
    atomic_long_t   vm_stat[NR_VM_ZONE_STAT_ITEMS]; //维护了大量有关该内存域的统计信息
    unsigned int inactive_ratio;        //此内存域的lru 活动与不活动页面的比例

    ZONE_PADDING(_pad2_)
    /* 很少使用 或 大部分只用于读取的字段*/

    /* 以下三个字段维护了一个等待内存页的进程队列,当内存页可用,系统将唤醒等待队列的进程使其运行 */
    wait_queue_head_t   * wait_table;               // 指向hash_table的数组
    unsigned long       wait_table_hash_nr_entries; //wait_table的数组大小
    unsigned long       wait_table_bits;            // (1 << wait_table_bits) == wait_table_size

    /* 支持非连续内存的字段. */
    struct pglist_data  *zone_pgdat;        //指向该内存域所属的pg_data_t内存结点结构
    unsigned long       zone_start_pfn;     //指向内存域第一个页帧的索引  zone_start_pfn == zone_start_paddr >> PAGE_SHIFT
    /*
    zone_start_pfn, spanned_pages and present_pages 被 span_seqlock 保护(这是个顺序锁),由于填充的原因,它没有被zone->lock所保护。span_seqlock与zone->lock 一起声明是因为它们经常被同时访问,放在同一个缓存行中提高命中率。
    */
    unsigned long   spanned_pages;      //内存域所包含的页帧总数(包含空洞)
    unsigned long   present_pages;      //内存数量(除去空洞)
    const char  *name;      //内存域的名字 基本不使用
} ____cacheline_internodealigned_in_smp;

typedef enum {
    ZONE_RECLAIM_LOCKED,    // 表示当前内存区域正在进行回收操作,防止并发回收
    ZONE_OOM_LOCKED,        // 表示当前内存区域正处于“内存不足”的处理状态。内核在检测到严重的内存不足情况时,会触发OOM 杀手(OOM killer)来释放内存,防止在 OOM 处理期间进行新的内存分配或回收操作
    ZONE_CONGESTED,         //表示当前内存区域处于拥塞状态。拥塞状态通常意味着该区域内的页帧回收或I/O操作速度较慢,导致内存分配的效率降低。
} zone_flags_t; 

//设置和清除zone的状态函数
static inline void zone_set_flag(struct zone *zone, zone_flags_t flag)
static inline int zone_test_and_set_flag(struct zone *zone, zone_flags_t flag)
static inline void zone_clear_flag(struct zone *zone, zone_flags_t flag)
内存域中的冷热页

zone->pageset是CPU冷热页的实现,热页代表页已经加载到CPU高速缓存,与在内存中的页相比,其数据能够更快地访问。冷页则不在高速缓存中。在多处理器系统上每个CPU都有一个或多个高速缓存,各个CPU的管理必须是独立的。因此pageset字段使用了__percpu修饰,每个CPU会获得此变量的副本。

尽管zone归属于某个pg_data_t从而归属于某个特定的CPU,但其他CPU的高速缓存仍然可以包含该内存域中的页。每个处理器都可以访问系统中所有的页,尽管速度不同。因此,特定于内存域的数据结构不仅要考虑到所属结点相关的CPU,还必须照顾到系统中其他的CPU。

1
2
3
4
5
6
7
8
9
struct per_cpu_pageset {
    struct per_cpu_pages pcp;
};
struct per_cpu_pages {
    int count;      // 该链表中物理页的个数
    int high;       // 链表中的物理页个数超过该数值,会将部分页返还给zone buddy系统
    int batch;      // 每次返还给buddy系统的物理页的个数 */
    struct list_head lists[MIGRATE_PCPTYPES];   // 对于不同的迁移类型的页帧有不同的链表(对内存碎片的优化),其中热页在链表头部,冷页存放在链表尾部。
}; 

页帧

页帧是内存的物理页,是代表系统内存的最小单位,每个物理页都有一个struct page结构体与之对应, 其中记录了该物理页的许多状态:该页中是否保存着数据、该页是否可回收、该页在页表中是否被映射等等。 由于页的数目巨大,因此对page结构的小改动,也可能导致保存所有page实例所需的物理内存暴涨。所有的page都存放在一个全局数组中(mem_map)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
struct page {
    unsigned long flags;        // 页帧的原子标志,有些情况下会异步更新 flags可被设置成page-flags.h文件中的pageflags枚举
    atomic_t _count;            // 页帧的引用计数,表示该页帧被多少个进程映射到内存中
    union {
        atomic_t _mapcount;     // 页帧的映射计数,表示该页帧被多少个进程映射到内存中
        struct {                // 用于slub分配器 对象的分配数
            u16 inuse;
            u16 objects;
        };
    };
    union {
        struct {
        unsigned long private;          // 页帧的私有数据,用于slub分配器
        struct address_space *mapping;  // 页帧的映射对象 (根据位值确定是否匿名页)
        };
        struct kmem_cache *slab;        // 用于slub分配器,指向slab
        struct page *first_page;        // 用于复合页的尾页,指向首页
    };
    union {
        pgoff_t index;                  // 在映射的虚拟空间(vma_area)内的偏移;
        void *freelist;                 // sl[aou]b 第一个空闲页
    };
    struct list_head lru;               // 用于在各种链表上维护该页,以便将页按不同类别分组,比如活动和不活动页,受zone->lru_lock保护。
    #if defined(WANT_PAGE_VIRTUAL)
        void *virtual;                  // 用于高端内存区域中的页 存储该页的虚拟地址
    #endif 

小结

mm struct hierarchy

每个CPU关联到内核中的结点,结点下管理了不同状态的内存域,内存域中按照冷热页和迁移类型的分类,管理不同状态的页帧。

页表

页表用于建立用户进程的虚拟地址空间和系统物理内存(内存、页帧)之间的关联。上文主要描述了内存的结构层次(结点、内存域、page页帧),其中包含的页帧的数量和状态(使用中或空闲)。页表用于向每个进程提供一致的虚拟地址空间。应用程序看到的地址空间是一个连续的内存区。该表也将虚拟内存页映射到物理内存,因而支持共享内存的实现(几个进程同时共享的内存),还可以在不额外增加物理内存的情况下,将页换出到块设备来增加有效的可用内存空间。

内核内存管理总是使用四级页表,而不管底层处理器是否如此。IA-32系统是不支持四级页表的,该体构只使用两级分页系统(不使用PAE扩展)。因此,第三和第四级页表必须由特定于体系结构的代码模拟。

内存地址与页表的关系

根据四级页表结构的需要,虚拟内存地址分为5部分(4个表项用于选择页,1个索引表示页内位置)

虚拟地址与页表

将虚拟地址与各项的位掩码按位与,即可提取出各项的分量

页表在内核中的表示

由于不同体系架构的差异,内核提供一个抽象的页表结构与相应的辅助函数,每种体系结构需要提供对应的实现。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
/** 各页表层级的结构 **/
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;

/** 地址与结构互相转换的宏 **/
#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)

#define __pte(x)    ((pte_t) { (x) } )
#define __pmd(x)    ((pmd_t) { (x) } )
#define __pgd(x)    ((pgd_t) { (x) } )
#define __pgprot(x) ((pgprot_t) { (x) } )

pte_t页表中的指针不仅指向了内存位置,在不同的体系结构下还包含了相应的附加信息(在多余的比特位中),下列表格提供比较常见的内存页的函数与。

函数描述
pte_present页是否在内存中(是否被换出)
pte_read从用户空间可以读取该页吗
pte_write可以写入到该页吗
…………
mk_pte创建页表项
pte_page获得页表项描述的页对应的page实例地址
pgd/…/pte_alloc分配并初始化一个对应项的内存
pgd/…/pte_free释放对应项占据的内存
set_pgd/…/pte设置对应项中的值
本文由作者按照 CC BY 4.0 进行授权