文章

linux内核笔记(八)vmalloc、持久映射、固定映射

vmalloc、持久映射、固定映射

回顾一下第五、六章的内容:在32位的内核虚拟地址空间中, 只有$1G$的可用空间 (总共$2^{32} = 4G$ - 用户空间$3G$),如果物理内存超出$1G$, 那么kernel将无法寻址超出的那部分物理内存 (即高端内存 HIGH_MEMORY), 为了解决这个问题,kernel将内核空间分了两部分:可直接线性映射物理内存的$896MiB$普通内存区域和剩余的高端内存区域。

vitual address space

为了适配不同的内存需求,内核又将高端内存区域划分了三种不同的用途,分别是vmalloc、持久映射、固定映射。

kernel memory segment

vmalloc 不连续的物理内存映射

虚拟内存地址空间与物理内存的连续线性映射性能是最好的,但由于各种原因(32位中的高端内存、物理内存中的空洞、bios等默认占用的内存空间),这种线性映射并不能总是顺利使用。如果在进程要求分配连续的地址空间时,不存在连续的物理内存,那么就需要一种方法来将不连续的物理内存通过逻辑地址映射成一段连续的虚拟地址空间。

vmalloc是内核中一种虚拟内存中连续但在物理内存中不一定连续的内存分配方式,通过使用vmalloc函数分配,仅需要指定所需内存区的长度。因为vmalloc的内存页是映射在内核地址空间中,因此使用高端内存域的页要优于其他内存域。这使得内核可以节省更宝贵的较低端内存域。

内核加载模块时使用的vmalloc分配的内存,如果模块数据比较多,那么无法保证有足够的连续内存可用,特别是在系统已经运行了比较长时间的情况下。

API

1
2
3
4
5
6
7
8
9
10
11
12
13
14
/* 申请长度为size的内存 */
void *vmalloc(unsigned long size);
/* 申请长度为size的内存 并将内存区初始化为0 */
void *vzalloc(unsigned long size);
/* 将pages的页映射到连续的虚拟地址空间中 */
void *vmap(struct page **pages, unsigned int count,
        unsigned long flags, pgprot_t prot);

/* 释放地址为addr的vmalloc分配的内存 = __vunmap(addr,1) */
void vfree(const void *addr);
/* 释放由vmap 或ioremap 创建的映射  = __vunmap(addr,0) */
void vunmap(const void *addr);
/* 上面的释放函数最终将工作委托给__vunmap deallocate_pages表示是否将page返回给buddy */
void __vunmap(const void *addr, int deallocate_pages);

数据结构

内核在管理虚拟内存中的vmalloc区域时,内核必须跟踪哪些子区域被使用、哪些是空闲的,内核将所有使用的部分保存在一个红黑树中。

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
#define VM_IOREMAP  0x00000001  /* 将取自物理地址空间、由系统总线用于I/O操作的一个内存块,映射到内核的地址空间中。 */
#define VM_ALLOC    0x00000002  /* 表示由vmalloc函数产生的子区域 */
#define VM_MAP      0x00000004  /* 将现存pages集合映射到连续的虚拟地址空间中 */

struct vm_struct {
    struct vm_struct    *next;      /* 指向下一个子区域的vm_struct */
    void                *addr;      /* 子区域在虚拟地址空间中的起始地址 */
    unsigned long       size;       /* 子区域大小 */
    unsigned long       flags;      /* 子区域标志 VM_ALLOC、VM_MAP、VM_IOREMAP */
    struct page         **pages;    /* 指向物理页page指针的数组指针 */
    unsigned int        nr_pages;   /* pages数组项的数目 */
    phys_addr_t         phys_addr;  /* 映射的区域类型为VM_IOREMAP时使用*/
    void                *caller;    /* vmalloc调用者的函数地址 调试用 */
};

/* vmalloc.c */
static LIST_HEAD(vmap_area_list);
static struct rb_root vmap_area_root = RB_ROOT;

struct vmap_area {
    unsigned long va_start;         // 该区域的起始地址
    unsigned long va_end;           // 该区域的结束地址
    unsigned long flags;            // 该区域的类型
    struct rb_node rb_node;         // 在RB_ROOT红黑树的位置
    struct list_head list;          // 在vmap_area_list顺序链表中的位置
    struct list_head purge_list;    // 用于lazy purge的链表
    void *private;                  // 指向对应的vm_struct
    struct rcu_head rcu_head;       // rcu相关
};

API的实现

内存分配

vmalloc函数经过一些对flag、node等的前置处理后,最终委托__vmalloc_node_range函数完成内存分配的主要逻辑。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
void *vmalloc(unsigned long size)
{
    /*优先使用高端内存域*/
    return __vmalloc_node_flags(size, -1, GFP_KERNEL | __GFP_HIGHMEM);
}
EXPORT_SYMBOL(vmalloc);

static inline void *__vmalloc_node_flags(unsigned long size,
                int node, gfp_t flags)
{
    /* __builtin_return_address 返回当前函数的调用栈地址 */
    return __vmalloc_node(size, 1, flags, PAGE_KERNEL,
                node, __builtin_return_address(0));
}
static void *__vmalloc_node(unsigned long size, unsigned long align,
                gfp_t gfp_mask, pgprot_t prot,
                int node, void *caller)
{
    /* 默认从VMALLOC_START至VMALLOC_END区间分配虚拟地址空间 */
    return __vmalloc_node_range(size, align, VMALLOC_START, VMALLOC_END,
                gfp_mask, prot, node, caller);
}

__get_vm_area_node

__vmalloc_node_range函数的逻辑比较简单,通过__get_vm_area_node函数找到并初始化虚拟地址空间,将其放入红黑树,然后通过__vmalloc_area_node函数初始化page页并对物理页进行页表映射。

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
void *__vmalloc_node_range(unsigned long size, unsigned long align,
                unsigned long start, unsigned long end, gfp_t gfp_mask,
                pgprot_t prot, int node, void *caller)
{
    struct vm_struct *area;
    void *addr;
    unsigned long real_size = size;
    /* 对齐页长度 */
    size = PAGE_ALIGN(size);
    if (!size || (size >> PAGE_SHIFT) > totalram_pages)
        return NULL;
    /* 找到并初始化area */
    area = __get_vm_area_node(size, align, VM_ALLOC, start, end, node,
                    gfp_mask, caller);

    if (!area)
        return NULL;
    /* 初始化page页并对物理页进行页表映射 */
    addr = __vmalloc_area_node(area, gfp_mask, prot, node, caller);

    /* 内存泄露检测 */
    kmemleak_alloc(addr, real_size, 3, gfp_mask);

    return addr;
}

__get_vm_area_node函数处理vm_struct与vmap_area的创建过程,核心是匹配到合适地址和初始化vmap_area,这块的工作委托给alloc_vmap_area函数。

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
static struct vm_struct *__get_vm_area_node(unsigned long size,
        unsigned long align, unsigned long flags, unsigned long start,
        unsigned long end, int node, gfp_t gfp_mask, void *caller)
{
    static struct vmap_area *va;
    struct vm_struct *area;
    /* vmalloc期间不允许中断 */
    BUG_ON(in_interrupt());
    if (flags & VM_IOREMAP) {
        int bit = fls(size);
        if (bit > IOREMAP_MAX_ORDER)
            bit = IOREMAP_MAX_ORDER;
        else if (bit < PAGE_SHIFT)
            bit = PAGE_SHIFT;
        align = 1ul << bit;
    }
    size = PAGE_ALIGN(size);
    if (unlikely(!size))
        return NULL;
    /* 通过slab 申请一个vm_struct内存 */
    area = kzalloc_node(sizeof(*area), gfp_mask & GFP_RECLAIM_MASK, node);
    if (unlikely(!area))
        return NULL;

    /* 加一个空白页当保护页 */
    size += PAGE_SIZE;
    /* 找到一块合适的虚拟空间vmap_area */
    va = alloc_vmap_area(size, align, start, end, node, gfp_mask);
    if (IS_ERR(va)) {
        kfree(area);
        return NULL;
    }
    /* 将vmap_area 转义至vm_struct 并将vm_struct添加到vm_list中去 */
    insert_vmalloc_vm(area, va, flags, caller);
    return area;
}

alloc_vmap_area是vmalloc的核心函数,通过遍历红黑树快速找到合适的vmap_area,并初始化新的area放到红黑树中

alloc_vmap_area

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
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
static DEFINE_SPINLOCK(vmap_area_lock);
/* 缓存的vmap_area(以便下次快速定位vmalloc地址空间) 通过vmap_area_lock保护 */
static struct rb_node *free_vmap_cache;
static unsigned long cached_hole_size;
static unsigned long cached_vstart;
static unsigned long cached_align;

static struct vmap_area *alloc_vmap_area(unsigned long size,
                unsigned long align,
                unsigned long vstart, unsigned long vend,
                int node, gfp_t gfp_mask)
{
    struct vmap_area *va;
    struct rb_node *n;
    unsigned long addr;
    int purged = 0;
    struct vmap_area *first;

    BUG_ON(!size);
    BUG_ON(size & ~PAGE_MASK);
    BUG_ON(!is_power_of_2(align));
    /* 通过slab 申请一块vmap_area内存 */
    va = kmalloc_node(sizeof(struct vmap_area),
            gfp_mask & GFP_RECLAIM_MASK, node);
    if (unlikely(!va))
        return ERR_PTR(-ENOMEM);

retry:
    spin_lock(&vmap_area_lock);
    /* 如果有缓存vmap_area但与本次vmalloc的请求不适配,重置area缓存 */
    if (!free_vmap_cache ||
            size < cached_hole_size ||
            vstart < cached_vstart ||
            align < cached_align) {
nocache:
        cached_hole_size = 0;
        free_vmap_cache = NULL;
    }
    /* 更新缓存起始地址和对齐长度 */
    cached_vstart = vstart;
    cached_align = align;

    /* 如果命中缓存vmap_area,从缓存的vmap_area.va_end之后寻找适配的地址空间 */
    if (free_vmap_cache) {
        first = rb_entry(free_vmap_cache, struct vmap_area, rb_node);
        addr = ALIGN(first->va_end + PAGE_SIZE, align);
        if (addr < vstart)
            goto nocache;
        if (addr + size - 1 < addr)
            goto overflow;

    } else {
        addr = ALIGN(vstart, align);
        if (addr + size - 1 < addr)
            goto overflow;
        /* 没有缓存 遍历红黑树,找到在请求的vstart和vend区间内的第一个vmap_area红黑树结点 */
        n = vmap_area_root.rb_node;
        first = NULL;
        while (n) {
            struct vmap_area *tmp;
            tmp = rb_entry(n, struct vmap_area, rb_node);
            if (tmp->va_end >= addr) {
                first = tmp;
                if (tmp->va_start <= addr)
                    break;
                n = n->rb_left;
            } else
                n = n->rb_right;
        }
        /* 没有找到结点,说明是第一次分配,直接分配area */
        if (!first)
            goto found;
    }

    /* 
     * 从vstart和vend区间内的第一个结点后面开始,找到能放的下的size大小的虚拟地址空间
     * 经过此循环后,会有两种情况
     *   1.addr + size < first->va_start 说明可以在first结点前面有空间
     *   2.addr + size <= vend  说明地址空间不够了
     *   3.first的后面有空间长度足够,跳到found代码段
     */
    while (addr + size >= first->va_start && addr + size <= vend) {
        /* 记录上个area后addr的位置与当前节点的hole大小 */
        if (addr + cached_hole_size < first->va_start)
            cached_hole_size = first->va_start - addr;
        addr = ALIGN(first->va_end + PAGE_SIZE, align);
        if (addr + size - 1 < addr)
            goto overflow;

        n = rb_next(&first->rb_node);
        if (n)
            first = rb_entry(n, struct vmap_area, rb_node);
        else
            goto found;
    }

found:
    if (addr + size > vend)
        goto overflow;
    /* 初始化vmap_area */
    va->va_start = addr;
    va->va_end = addr + size;
    va->flags = 0;
    /* 将此结点插入红黑树合适的位置 并添加到vmap_area_list中 即初始化rb_node和list字段 */
    __insert_vmap_area(va);
    /* 记录此结点为缓存,下次可从此结点后面继续寻找适配空间 */
    free_vmap_cache = &va->rb_node;
    spin_unlock(&vmap_area_lock);

    BUG_ON(va->va_start & (align-1));
    BUG_ON(va->va_start < vstart);
    BUG_ON(va->va_end > vend);
    return va;

overflow:
    spin_unlock(&vmap_area_lock);
    if (!purged) {
        /* 懒释放一批vmap_area 再重试匹配空间 */
        purge_vmap_area_lazy();
        purged = 1;
        goto retry;
    }
    if (printk_ratelimit())
        printk(KERN_WARNING
            "vmap allocation for size %lu failed: "
            "use vmalloc=<size> to increase size.\n", size);
    kfree(va);
    return ERR_PTR(-EBUSY);
}

/* 通过alloc_vmap_area初始化好vmap_area后,将得到的地址空间初始化vm_struct 并添加到 vmlist*/
static void insert_vmalloc_vm(struct vm_struct *vm, struct vmap_area *va,
                    unsigned long flags, void *caller)
{
    struct vm_struct *tmp, **p;
    vm->flags = flags;
    vm->addr = (void *)va->va_start;
    vm->size = va->va_end - va->va_start;
    vm->caller = caller;
    va->private = vm;
    va->flags |= VM_VM_AREA;
    write_lock(&vmlist_lock);
    for (p = &vmlist; (tmp = *p) != NULL; p = &tmp->next) {
        if (tmp->addr >= vm->addr)
            break;
    }
    vm->next = *p;
    *p = vm;
    write_unlock(&vmlist_lock);
}

在初始化好vmap_area与vm_struct后,内核还需要配置好page与物理页的映射关系,注意在这里,每个page是从伙伴系统拿的,这样伙伴中不连续的内存页也可以组成一块连续的虚拟地址空间。

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
static void *__vmalloc_area_node(struct vm_struct *area, gfp_t gfp_mask,
                    pgprot_t prot, int node, void *caller)
{
    struct page **pages;
    unsigned int nr_pages, array_size, i;
    gfp_t nested_gfp = (gfp_mask & GFP_RECLAIM_MASK) | __GFP_ZERO;
    /* 计算需要的页数 减去了警戒页 */
    nr_pages = (area->size - PAGE_SIZE) >> PAGE_SHIFT;
    /* 管理页数的page实例需要的内存空间 */
    array_size = (nr_pages * sizeof(struct page *));

    area->nr_pages = nr_pages;
    if (array_size > PAGE_SIZE) {
        /* page初始需要的内存空间超出了一页,递归调用__vmalloc_node 先申请page的虚拟内存空间 */
        pages = __vmalloc_node(array_size, 1, nested_gfp|__GFP_HIGHMEM,
                PAGE_KERNEL, node, caller);
        /* 标记vm_struct中的pages是由vmalloc申请的 */
        area->flags |= VM_VPAGES;
    } else {
        /* 没超过一页,直接用slab分配page的内存空间 */
        pages = kmalloc_node(array_size, nested_gfp, node);
    }
    area->pages = pages;
    area->caller = caller;
    if (!area->pages) {
        /* page内存分配失败,释放此次申请的虚拟内存的vmap_area和vm_struct */
        remove_vm_area(area->addr);
        kfree(area);
        return NULL;
    }

    /* 遍历页数,初始化每个页 */
    for (i = 0; i < area->nr_pages; i++) {
        struct page *page;
        /* 从伙伴系统拿页 掩码中已表达从高端内存中拿取 */
        if (node < 0)
            page = alloc_page(gfp_mask);
        else
            page = alloc_pages_node(node, gfp_mask, 0);

        if (unlikely(!page)) {
            /* 将前面初始化成功的页的数量计下来,后面释放掉 */
            area->nr_pages = i;
            goto fail;
        }
        area->pages[i] = page;
    }
    /*初始化物理页映射  设置对应的页目录项,页中间目录项,以及页表项 */
    if (map_vm_area(area, prot, &pages))
        goto fail;
    return area->addr;

fail:
    vfree(area->addr);
    return NULL;
}
内存释放

__vunmap函数的主要工作是调用remove_vm_area将addr地址所在的vm_struct、vmap_area释放掉(实际并不马上释放),然后根据参数决定是否将vm_struct中的pages归还给伙伴系统。

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
static void __vunmap(const void *addr, int deallocate_pages)
{
    struct vm_struct *area;
    if (!addr)
        return;

    if ((PAGE_SIZE-1) & (unsigned long)addr) {
        WARN(1, KERN_ERR "Trying to vfree() bad address (%p)\n", addr);
        return;
    }
    /* 移除并释放vmap_area */
    area = remove_vm_area(addr);
    if (unlikely(!area)) {
        WARN(1, KERN_ERR "Trying to vfree() nonexistent vm area (%p)\n",
                addr);
        return;
    }

    debug_check_no_locks_freed(addr, area->size);
    debug_check_no_obj_freed(addr, area->size);

    if (deallocate_pages) {
        int i;
        /* 将vm_struct中的页归还给buddy */
        for (i = 0; i < area->nr_pages; i++) {
            struct page *page = area->pages[i];

            BUG_ON(!page);
            __free_page(page);
        }
        /* 如果vm_struct中的pages数组是通过vmalloc分配的,递归调用释放函数,否则通过slab释放 */
        if (area->flags & VM_VPAGES)
            vfree(area->pages);
        else
            kfree(area->pages);
    }
    kfree(area);
    return;
}

虚拟地址空间的vm_struct由slab的kfree函数直接释放掉了,但是vmap_area需要特殊处理(平衡vmap_area红黑树),因此是懒释放的。remove_vm_area函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
struct vm_struct *remove_vm_area(const void *addr)
{
    struct vmap_area *va;
    /* 从vmap_area_root.rb_node开始找到对应的vmap_area结点 */
    va = find_vmap_area((unsigned long)addr);
    if (va && va->flags & VM_VM_AREA) {
        struct vm_struct *vm = va->private;
        struct vm_struct *tmp, **p;
        /*从vmlist中移除vmap_area映射的vm_struct*/
        write_lock(&vmlist_lock);
        for (p = &vmlist; (tmp = *p) != vm; p = &tmp->next)
            ;
        *p = tmp->next;
        write_unlock(&vmlist_lock);
        vmap_debug_free_range(va->va_start, va->va_end);
        /* 解除虚拟地址空间映射,并懒释放vmap_area */
        free_unmap_vmap_area(va);
        vm->size -= PAGE_SIZE;
        return vm;
    }
    return NULL;
}

free_unmap_vmap_area解除虚拟地址空间映射懒释放vmap_area的函数交给了vunmap_page_range(解除各页表项)和free_vmap_area_noflush函数,后者根据vmap_area待释放的数量决定是否真正释放vmap_area。

还记着上面的alloc_vmap_area函数吗,在找不到area的范围时(overflow代码段),调用了purge_vmap_area_lazy函数,和free_vmap_area_noflush一样,他们将真正释放vmap_area的工作都交给了__purge_vmap_area_lazy

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
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
static void free_vmap_area_noflush(struct vmap_area *va)
{
    /* 将vmap_area 标记为待懒释放 */
    va->flags |= VM_LAZY_FREE;
    atomic_add((va->va_end - va->va_start) >> PAGE_SHIFT, &vmap_lazy_nr);
    /* 如果待懒释放的内存数量超过阈值,尝试批量释放vmap_area */
    if (unlikely(atomic_read(&vmap_lazy_nr) > lazy_max_pages()))
        try_purge_vmap_area_lazy();
}

static void __purge_vmap_area_lazy(unsigned long *start, unsigned long *end,
                    int sync, int force_flush)
{
    static DEFINE_SPINLOCK(purge_lock);
    LIST_HEAD(valist);
    struct vmap_area *va;
    struct vmap_area *n_va;
    int nr = 0;

    /* 锁定purge_lock */
    if (!sync && !force_flush) {
        if (!spin_trylock(&purge_lock))
            return;
    } else
        spin_lock(&purge_lock);
    /* 如果是同步状态,让所有cpu分发并行释放 */
    if (sync)
        purge_fragmented_blocks_allcpus();

    rcu_read_lock();
    /* 遍历所有区域 计算需要释放的区域位置与长度 并标记为正在释放中 */
    list_for_each_entry_rcu(va, &vmap_area_list, list) {
        if (va->flags & VM_LAZY_FREE) {
            if (va->va_start < *start)
                *start = va->va_start;
            if (va->va_end > *end)
                *end = va->va_end;
            nr += (va->va_end - va->va_start) >> PAGE_SHIFT;
            /* 将区域添加到purge_list列表中 */
            list_add_tail(&va->purge_list, &valist);
            va->flags |= VM_LAZY_FREEING;
            va->flags &= ~VM_LAZY_FREE;
        }
    }
    rcu_read_unlock();
    /* 减去待释放数量 */
    if (nr)
        atomic_sub(nr, &vmap_lazy_nr);

    /* 刷新释放区域的tlb */
    if (nr || force_flush)
        flush_tlb_kernel_range(*start, *end);

    if (nr) {
        spin_lock(&vmap_area_lock);
        /* 遍历purge_list,释放其对应的vmap_area */
        list_for_each_entry_safe(va, n_va, &valist, purge_list)
            __free_vmap_area(va);
        spin_unlock(&vmap_area_lock);
    }
    spin_unlock(&purge_lock);
}

static void __free_vmap_area(struct vmap_area *va)
{
    BUG_ON(RB_EMPTY_NODE(&va->rb_node));
   
    if (free_vmap_cache) {
        if (va->va_end < cached_vstart) {
             /* 如果存在vmap_area的cache 并且cache前的vmap_area需要被释放,则重置缓存 */
            free_vmap_cache = NULL;
        } else {
            /* 如果cache后的vmap_area需要被释放,将缓存往前移一结点 */
            struct vmap_area *cache;
            cache = rb_entry(free_vmap_cache, struct vmap_area, rb_node);
            if (va->va_start <= cache->va_start) {
                free_vmap_cache = rb_prev(&va->rb_node);
            }
        }
    }
    /* 删除并平衡红黑树 删除list结点 */
    rb_erase(&va->rb_node, &vmap_area_root);
    RB_CLEAR_NODE(&va->rb_node);
    list_del_rcu(&va->list);
    if (va->va_end > VMALLOC_START && va->va_end <= VMALLOC_END)
        vmap_area_pcpu_hole = max(vmap_area_pcpu_hole, va->va_end);
    /* 通过kfree 释放vmap_area */
    call_rcu(&va->rcu_head, rcu_free_va);
}
static void rcu_free_va(struct rcu_head *head)
{
    struct vmap_area *va = container_of(head, struct vmap_area, rcu_head);
    kfree(va);
}

kmap kunmap 持久映射

系统提供了kmap()和kunmap()函数,用于在持久映射区中映射/解除映射高端内存页。持久映射区的范围在初始化时根据配置指定范围的大小为LAST_PKMAP个页,然后计算出PKMAP_BASE的起始地址,

1
2
3
4
5
6
7
8
#ifdef CONFIG_X86_PAE
#define LAST_PKMAP 512
#else
#define LAST_PKMAP 1024
#endif

#define PKMAP_BASE ((FIXADDR_BOOT_START - PAGE_SIZE * (LAST_PKMAP + 1)) \
            & PMD_MASK)

持久映射的数据结构

持久映射的数据结构

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
static int pkmap_count[LAST_PKMAP];  /* 每个虚拟地址的引用计数 */
static unsigned int last_pkmap_nr;

/* page与虚拟地址映射的结构体 */
struct page_address_map {
    struct page *page;
    void *virtual;
    struct list_head list;
};

/* page与虚拟地址映射的hash表结构 */
static struct page_address_slot {
    struct list_head lh;            /* page_address_map 列表*/
    spinlock_t lock;                /* Protect this bucket's list */
} ____cacheline_aligned_in_smp page_address_htable[1<<PA_HASH_ORDER];

pkmap_count是持久映射区每个虚拟地址的引用计数,各项数值有别常见:

  • 0:表示该虚拟地址没有被映射
  • 1:表示该位置关联的页已经映射,但由于CPU的TLB没有更新而无法使用
  • n:已经建立了映射,有 n-1 个使用者

page_address()函数通过以上的数据结构,就可以将page与虚拟地址的映射关系建立起来。

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
oid *page_address(struct page *page)
{
    unsigned long flags;
    void *ret;
    struct page_address_slot *pas;

    /* 低端内存 直接线性计算 = __va(PFN_PHYS(page_to_pfn(page))) */
    if (!PageHighMem(page))
        return lowmem_page_address(page);
    
    /* 通过page的hash值找到对应的page_address_slot */
    pas = page_slot(page);
    ret = NULL;
    spin_lock_irqsave(&pas->lock, flags);
    if (!list_empty(&pas->lh)) {
        /* 遍历page_address_map列表 找到对应的page_address_map */
        struct page_address_map *pam;
        list_for_each_entry(pam, &pas->lh, list) {
            if (pam->page == page) {
                /* 找到page 直接返回对应的虚拟地址 */
                ret = pam->virtual;
                goto done;
            }
        }
    }
done:
    spin_unlock_irqrestore(&pas->lock, flags);
    return ret;
}
static struct page_address_slot *page_slot(struct page *page)
{
    return &page_address_htable[hash_ptr(page, PA_HASH_ORDER)];
}

持久映射的API实现

kmap函数

kmap函数

kmap函数过滤掉低端内存的page页,并将映射持久映射区的工作交给kmap_high函数

1
2
3
4
5
6
7
8
9
10
11
void *kmap(struct page *page)
{
    might_sleep();
    /* 低端内存直接返回线性地址 */
    if (!PageHighMem(page))
        return page_address(page);
    /* 尝试将page映射成固定映射虚拟地址 */
    return kmap_high(page);
}
EXPORT_SYMBOL(kmap);

kmap_high函数将在获得kmap锁后,通过map_new_virtual函数将page映射到持久映射区(如需),并返回虚拟地址。

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
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
void *kmap_high(struct page *page)
{
    unsigned long vaddr;
    /* 加锁 spin_lock(&kmap_lock) */
    lock_kmap();
    /* 查询是否已经被映射过了 */
    vaddr = (unsigned long)page_address(page);
    if (!vaddr)
        vaddr = map_new_virtual(page);
    /* 增加引用 */
    pkmap_count[PKMAP_NR(vaddr)]++;
    BUG_ON(pkmap_count[PKMAP_NR(vaddr)] < 2);
    unlock_kmap();
    return (void*) vaddr;
}

static inline unsigned long map_new_virtual(struct page *page)
{
    unsigned long vaddr;
    int count;
start:
    count = LAST_PKMAP;
    /*从尾向头遍历 */
    for (;;) {
        /* 上一次建立映射的idx来遍历可用地址坑位 */
        last_pkmap_nr = (last_pkmap_nr + 1) & LAST_PKMAP_MASK;
        if (!last_pkmap_nr) {
            /* 为0说明已经遍历结束了,将没有映射的坑位剔除掉 并刷新tlb */
            flush_all_zero_pkmaps();
            /* 再次从遍历一次 */
            count = LAST_PKMAP;
        }
        if (!pkmap_count[last_pkmap_nr])
            break;  /* 找到一个可用的虚拟地址坑位 */
        if (--count)
            continue;

        /* 到这说明没等到到可用的虚拟地址坑位,进程进入等待状态 */
        {
            DECLARE_WAITQUEUE(wait, current);
            __set_current_state(TASK_UNINTERRUPTIBLE);
            add_wait_queue(&pkmap_map_wait, &wait);
            unlock_kmap();
            /* 自己进入等待状态,将CPU让其他进程 */
            schedule();
            /* 从等待被唤醒 */
            remove_wait_queue(&pkmap_map_wait, &wait);
            lock_kmap();

            /* 也许有其他进程已经将此page映射过了 */
            if (page_address(page))
                return (unsigned long)page_address(page);

            /* 还没被映射 再尝试映射一次 */
            goto start;
        }
    }
    /* 通过坑位,换算出虚拟地址  =  (PKMAP_BASE + ((nr) << PAGE_SHIFT)) */
    vaddr = PKMAP_ADDR(last_pkmap_nr);
    /* 设置页表pte项*/
    set_pte_at(&init_mm, vaddr,
            &(pkmap_page_table[last_pkmap_nr]), mk_pte(page, kmap_prot));
    /* 设置引用计数 */*/
    pkmap_count[last_pkmap_nr] = 1;
    /* 将page和虚拟地址关联起来 */
    set_page_address(page, (void *)vaddr);
    return vaddr;
}

void set_page_address(struct page *page, void *virtual)
{
    unsigned long flags;
    struct page_address_slot *pas;
    struct page_address_map *pam;

    BUG_ON(!PageHighMem(page));
    /* 通过page找到hash表中的slot */
    pas = page_slot(page);
    if (virtual) {
        /* 如果传递的虚拟地址不为空  则将其与page添加到映射组合中 */
        BUG_ON(list_empty(&page_address_pool));
        spin_lock_irqsave(&pool_lock, flags);
        pam = list_entry(page_address_pool.next,
                struct page_address_map, list);
        list_del(&pam->list);
        spin_unlock_irqrestore(&pool_lock, flags);
        /* 关联page与虚拟地址 将其放入slot中 */
        pam->page = page;
        pam->virtual = virtual;
        spin_lock_irqsave(&pas->lock, flags);
        list_add_tail(&pam->list, &pas->lh);
        spin_unlock_irqrestore(&pas->lock, flags);
    } else {
        /* 虚拟地址为空 将page从slot中移除 */
        spin_lock_irqsave(&pas->lock, flags);
        list_for_each_entry(pam, &pas->lh, list) {
            if (pam->page == page) {
                list_del(&pam->list);
                spin_unlock_irqrestore(&pas->lock, flags);
                spin_lock_irqsave(&pool_lock, flags);
                list_add_tail(&pam->list, &page_address_pool);
                spin_unlock_irqrestore(&pool_lock, flags);
                goto done;
            }
        }
        spin_unlock_irqrestore(&pas->lock, flags);
    }
done:
    return;
}

注意在map_new_virtual函数中,如果发现游标已经回归到头部时,代表所有坑位都被占用过1次了(也可能已经被释放了)。此时将会调用flush_all_zero_pkmaps函数,将引用计数为1(代表已经没有进程使用的坑位)重置,并刷新tlb。

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
static void flush_all_zero_pkmaps(void)
{
    int i;
    int need_flush = 0;

    flush_cache_kmaps();

    /* 从头向尾遍历引用计数数组 */
    for (i = 0; i < LAST_PKMAP; i++) {
        struct page *page;
        /* 只处理引用计数为1的  它们代表需要清理 */
        if (pkmap_count[i] != 1)
            continue;
        /* 重置引用计数 */
        pkmap_count[i] = 0;
        BUG_ON(pte_none(pkmap_page_table[i]));

        /* 清除页表pte项 */
        page = pte_page(pkmap_page_table[i]);
        pte_clear(&init_mm, (unsigned long)page_address(page),
                &pkmap_page_table[i]);
        /* 将page从hash表的slot中移除 */
        set_page_address(page, NULL);
        need_flush = 1;
    }
    if (need_flush)
        flush_tlb_kernel_range(PKMAP_ADDR(0), PKMAP_ADDR(LAST_PKMAP));
}
kunmap函数

kunmap函数比较好理解,是kmap函数的相反操作。

1
2
3
4
5
6
7
8
9
void kunmap(struct page *page)
{
    if (in_interrupt())
        BUG();
    if (!PageHighMem(page))
        return;
    kunmap_high(page);
}
EXPORT_SYMBOL(kunmap);

kunmap比较简单,主要是将引用计数器-1,当发现没有人在用此坑位时,唤醒等待固定映射坑位的队列。而后者在找不到坑位时,会刷新tlb并把引用计数为1的坑位设置为空闲状态,并将引用计数置为0。

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
void kunmap_high(struct page *page)
{
    unsigned long vaddr;
    unsigned long nr;
    unsigned long flags;
    int need_wakeup;

    lock_kmap_any(flags);
    vaddr = (unsigned long)page_address(page);
    BUG_ON(!vaddr);
    /* 将虚拟地址换算成坑位 =((virt-PKMAP_BASE) >> PAGE_SHIFT) */
    nr = PKMAP_NR(vaddr);

    need_wakeup = 0;
    /* 只有在刷新tlb时 计数器才应该为0 */
    switch (--pkmap_count[nr]) {
    case 0:
        BUG();
    case 1:
        /* 检测等待队列是否为空,如果不空则将其唤醒 */
        need_wakeup = waitqueue_active(&pkmap_map_wait);
    }
    unlock_kmap_any(flags);

    if (need_wakeup)
        wake_up(&pkmap_map_wait);
}

固定映射 临时映射

接下来聊到最后的固定映射区域,在内核有些地址是特殊的,比如为特定的内存区域和硬件设备提供固定永久性的虚拟地址映射。固定映射区域的主要目的是为了方便内核访问特定的内存区域和硬件资源,而不需要频繁地创建和销毁映射,这些映射在fixed_addresses枚举中。

固定映射 临时映射

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
enum fixed_addresses {
#ifdef CONFIG_X86_32
    FIX_HOLE,
    FIX_VDSO,
#else
    VSYSCALL_LAST_PAGE,
    VSYSCALL_FIRST_PAGE = VSYSCALL_LAST_PAGE
                + ((VSYSCALL_END-VSYSCALL_START) >> PAGE_SHIFT) - 1,
    VSYSCALL_HPET,
#endif
    FIX_DBGP_BASE,
    ……
#ifdef CONFIG_X86_32
    FIX_KMAP_BEGIN, /* 临时映射区的开始边界 */
    FIX_KMAP_END = FIX_KMAP_BEGIN+(KM_TYPE_NR*NR_CPUS)-1, /* 临时映射区的结束边界 */
}

需要注意到的是在这些枚举中,有一块区域为临时映射区,它的作用是在某些不合适使用kmap函数的场景下的成为其替代者。由于kmap函数有可能会让进程进入睡眠状态,因此不适合一些紧急的场景使用(比如中断处理程序)。临时映射区的内存管理函数kmap_atomic,提供一个备选的虚拟内存映射方案,内核认为这种映射是紧急但临时的,应该很快被释放掉。

临时映射的数据结构

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
/* 每个CPU一个游标 */
DECLARE_PER_CPU(int, __kmap_atomic_idx);

static inline int kmap_atomic_idx(void)
{
    /* 读取当前cpu的游标 */
    return __this_cpu_read(__kmap_atomic_idx) - 1;
}
static inline int kmap_atomic_idx_push(void)
{
    /* 在此cpu中的游标加1 */
    int idx = __this_cpu_inc_return(__kmap_atomic_idx) - 1;
    return idx;
}
static inline void kmap_atomic_idx_pop(void)
{
    /* 在此cpu中的游标减1 */
    __this_cpu_dec(__kmap_atomic_idx);
}

临时映射API与实现

临时内存映射申请
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
/* include/linux/highmem.h */
#define kmap_atomic(page, args...) __kmap_atomic(page)

void *__kmap_atomic(struct page *page)
{
    return kmap_atomic_prot(page, kmap_prot);
}
EXPORT_SYMBOL(__kmap_atomic);

void *kmap_atomic_prot(struct page *page, pgprot_t prot)
{
    unsigned long vaddr;
    int idx, type;
    pagefault_disable();
    /* 过滤低端内存 */
    if (!PageHighMem(page))
        return page_address(page);
    /* 获取自增游标 */
    type = kmap_atomic_idx_push();
    /* 通过cpu的id计算临时映射坑位 KM_TYPE_NR = 20 */
    idx = type + KM_TYPE_NR*smp_processor_id();
    /* 计算出虚拟地址 */
    vaddr = __fix_to_virt(FIX_KMAP_BEGIN + idx);
    BUG_ON(!pte_none(*(kmap_pte-idx)));
    /* 设置页表pte项 */
    set_pte(kmap_pte-idx, mk_pte(page, prot));
    return (void *)vaddr;
}
EXPORT_SYMBOL(kmap_atomic_prot);

临时映射的坑位转为虚拟地址的计算比较简单,直接在地址尾部偏移坑位的距离即可,由__fix_to_virt宏实现。

1
#define __fix_to_virt(x)    (FIXADDR_TOP - ((x) << PAGE_SHIFT))
临时内存映射释放
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
/* include/linux/highmem.h */
#define kunmap_atomic(addr, args...)                    \
do {                                \
    BUILD_BUG_ON(__same_type((addr), struct page *));   \
    __kunmap_atomic(addr);                      \
} while (0)

void __kunmap_atomic(void *kvaddr)
{
    unsigned long vaddr = (unsigned long) kvaddr & PAGE_MASK;

    if (vaddr >= __fix_to_virt(FIX_KMAP_END) &&
        vaddr <= __fix_to_virt(FIX_KMAP_BEGIN)) {
        int idx, type;

        type = kmap_atomic_idx();
        idx = type + KM_TYPE_NR * smp_processor_id();
        /* 清除页表项 */
        kpte_clear_flush(kmap_pte-idx, vaddr);
        /* 游标减1 */
        kmap_atomic_idx_pop();
    }
    pagefault_enable();
}
EXPORT_SYMBOL(__kunmap_atomic);
本文由作者按照 CC BY 4.0 进行授权