vmalloc内核实现

每个进程都拥有一段连续而且平坦的虚拟地址空间,这段连续的空间被划分为两大部分:用户空间和内核空间。在x86-32架构下,用户空间占据最低端的3G,内核空间占据最高的1G。事实上,每个进程并不会同时使用掉整个3G的地址空间,因此整个用户空间又进一步被划分为若干个虚拟内存区域(struct vm_area_struct),每个内存区域都有相应的访问权限,并且针对当前的内存区域还有具体的操作函数。

对于内核空间而言,根据不同的映射规则,整个内核空间划分为四大部分:物理内存映射区、vmalloc区、永久内核映射区和固定映射的线性地址区域。内核空间的映射情况如下图所示:

其中vmalloc区(struct vm_struct)跟用户空间的虚拟内存区域有些类似,它们都是利用分散的物理页框构建连续的虚拟地址区间。

非连续内存区的数据结构

vmalloc区也被称为非连续内存区域,整个非连续内存区的起始地址定义为VMALLOC_START宏,结束地址定义为VMALLOC_END宏。它由若干个vmalloc区组成,每个vmalloc区之间间隔4KB,这是为了防止非法的内存访问。内核中使用vm_struct结构来表示每个vmalloc区,也就是说,每次调用vmalloc()函数在内核中申请一段连续的内存后,都对应着一个vm_struct,系统中所有的vmalloc区组成一个链表,链表头指针为vmlist。vm_sttruct结构在最新内核源码的描述如下(本文所涉及的内核源码均来自v3.0.4)

struct vm_struct {
        struct vm_struct        *next;
        void                    *addr;
        unsigned long           size;
        unsigned long           flags;
        struct page             **pages;
        unsigned int            nr_pages;
        unsigned long           phys_addr;
        void                    *caller;
};

下面是这个结构中各个字段的解释:

next:所有的vm_struct结构组成一个vmlist链表,该字段指向下一个节点;

addr:vmalloc()最终是在内核空间中申请一个内存区域,addr代表这段子区域的起始地址;

size:表示子区域的大小;

flags:表示该非连续内存区的类型,VM_ALLOC表示由vmalloc()映射的内存区,VM_MAP表示通过vmap()映射的内存区,VM_IOREMAP表示通过ioremap()将硬件设备的内存映射到内核的一段内存区;

pages:指针数组,该数组的成员是struct page*类型的指针,每个成员都关联一个映射到该虚拟内存区的物理页框;

nr_pages:pages数组中page结构的总数;

phys_addr:通常为0,当使用ioremap()映射一个硬件设备的物理内存时才填充此字段;

caller:表示一个返回地址;

vmalloc()的实现

vmalloc()内部封装了__vmalloc_node(),该函数的原型和调用如下代码所示。其中,size表示要分配子内存区的大小,它通过vmalloc()传递过来的;align表示将所申请长度的内存区分为几部分,1表示将size大小的虚拟内存区作为一个整体;gfp_mask描述页面分配的标志,GFP_KERNEL|__GFP_HIGHMEM表明内存管理子系统将从高端内存区(ZONE_HIGHMEM)中分配内存空间;prot描述当前页的保护标志;node表示在哪个节点(struct pg_data_t)上为这段子内存区分配空间,-1表明在当前节点中分配;caller表示该函数的返回地址。

static void *__vmalloc_node(unsigned long size, unsigned long align,
                            gfp_t gfp_mask, pgprot_t prot,
                            int node, void *caller)
void *vmalloc(unsigned long size)
{
        return __vmalloc_node(size, 1, GFP_KERNEL | __GFP_HIGHMEM, PAGE_KERNEL,
                                        -1, __builtin_return_address(0));
}

__vmalloc_node函数的主要功能分为两部分:

1.在非连续内存区的起始和终止地址之间查找一个空闲的内存区域,这部分由__get_vm_area_node()完成。

2.为该子内存区分配物理页框,并将分散的物理页框分别映射到连续的vmalloc区中,这部分由__vmalloc_area_node()完成。

__vmalloc_node()一开始会先修正一下自内存取的大小,PAGE_ALIGN将size的大小修改成页大小的倍数。假如要申请1KB的内存区,那么事实上分配的是4KB大小(一个页大小)的区域。接着进行size合法性的检查,如果size为0,或者size所占页框数大于系统当前空闲的页框数(totalram_pages),将返回NULL,既申请失败。

如果子内存区大小合法,__get_vm_area_node()将在整个非连续内存区中查找一个size大小的子内存区。该函数先遍历整个vmlist链表,依次比对每个vmalloc区,直到找到满足要求的子内存区为止。接着为这个子内存区建立一个vm_struct结构,再将这个结构插入到整个vmlist链表中。该函数的详细实现过程本文不做分析。

static void *__vmalloc_node(unsigned long size, unsigned long align,
                            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 = __get_vm_area_node(size, align, VM_ALLOC, VMALLOC_START,
                                  VMALLOC_END, node, gfp_mask, caller);

        if (!area)
                return NULL;

        addr = __vmalloc_area_node(area, gfp_mask, prot, node, caller);

        kmemleak_alloc(addr, real_size, 3, gfp_mask);

        return addr;
}

__vmalloc_area_node()的实现

当__get_vm_area_node()创建了一个新的vm_struct结构后,接下来就要通过__vmalloc_area_node()为这个子内存区分配真正的物理页。
首先计算通过右移PAGE_SHIFT位来计算nr_pages,它表示这个子内存区映射的页数。接着根据子内存区所映射的页框数计算pages数组的大小,这个数组的元素为struct page*型,每个元素都指向一个用来描述物理页框的page结构。

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;
        array_size = (nr_pages * sizeof(struct page *));

        area->nr_pages = nr_pages;

接着,__vmalloc_area_node()为页描述符指针数组分配空间。如果这个指针数组的大小超过一个页的大小,那么递归调用__vmalloc_node()为其分配空间,也就是说pages数组本身就采用vmalloc区来存储;否则,通过kmalloc_node()为pages数组分配一段连续的空间,这段空间既位于内核空间的物理内存线性映射区。

接下来用刚才的局部变量pages更新area中的pages。如果pages数组分配失败,则调用remove_vm_area()将__get_vm_area_node()的到的vm_struct结构从vmlist中移除,并返回NULL,表示vmalloc()申请失败。

if (array_size > PAGE_SIZE) {
        pages = __vmalloc_node(array_size, 1, nested_gfp|__GFP_HIGHMEM,
                        PAGE_KERNEL, node, caller);
        area->flags |= VM_VPAGES;
} else {
        pages = kmalloc_node(array_size, nested_gfp, node);
}
area->pages = pages;
area->caller = caller;
if (!area->pages) {
        remove_vm_area(area->addr);
        kfree(area);
        return NULL;
}

现在到了最关键的时刻,通过一个循环依次为pages数组中的每个页面描述符分配真正的物理页框。需要注意的是page结构并不是代表一个具体的物理页框,只是用来描述物理页框的数据结构而已。如果node小于0,也就是未指定物理内存所在节点,那么使用alloc_page()分配一个页框,并将该页框对应的页描述符指针赋值给page临时变量;否则通过alloc_pages_node()在指定的节点上分配物理页框。接着将刚刚分配的物理页框对应的页描述符赋值给pages数组的第i个元素。一旦某个物理页框分配失败则直接返回NULL,表示本次vmalloc()操作失败。

        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;
}

到目前为止,__vmalloc_area_node()已经分配了所需的物理页框,但是这些分散的页框并没有映射到area所代表的那个连续vmalloc区中。map_vm_area()将完成映射工作,它依次修改内核使用的页表项,将pages数组中的每个页框分别映射到连续的vmalloc区中。

vmalloc()的实现-分散的物理页框与连续vmalloc区的映射

在vmalloc()的实现过程中,首先遍历vmlist链表找到一个所需大小的vmalloc区,接着为这个子内存区依次分配物理页框。从这两部分的实现过程可以看到,虚拟的子内存区是在整个vmalloc区中查找一块合适大小的子区域,而物理内存则是分散的依次分配。因此,vmalloc()的实现就落在了如何将这些分散的物理页框映射到连续的vmalloc区上。整个映射过程可以简单的看作是不断修改内核页表的过程。

map_vm_area()的实现

vmalloc()通过map_vm_area()完成物理页框与虚拟子内存区的映射。map_vm_area()首先计算虚拟内存子区域的起始地址addr和终止地址end。由于vm_struct中的size是实际子区间长度加上一个页大小,因此终止地址必须减去PAGE_SIZE。接着它调用了vmap_page_range()。下面的代码表明了map_vm_area()的实际调用过程(本文所有内核源码均取自v2.6.34)。

int map_vm_area(struct vm_struct *area, pgprot_t prot, struct page ***pages)
{
        unsigned long addr = (unsigned long)area->addr;
        unsigned long end = addr + area->size - PAGE_SIZE;
        int err;

        err = vmap_page_range(addr, end, prot, *pages);
        if (err > 0) {
                *pages += err;
                err = 0;
        }

        return err;
}

vmap_page_range()又再次封装了真正用于映射的函数vmap_page_range_noflush()。当映射完毕后,它调用flush_cache_vmap(),即将刚刚修改的内核页表项刷新到CPU高速缓存

static int vmap_page_range(unsigned long start, unsigned long end,
                           pgprot_t prot, struct page **pages)
{
        int ret;

        ret = vmap_page_range_noflush(start, end, prot, pages);
        flush_cache_vmap(start, end);
        return ret;
}

修改内核页表

map_vm_area()经过重重调用,终于来到了进行实际映射工作的vmap_page_range_noflush()中。这个函数中首先通过pgd_offset_k()计算出addr在主内核页全局目录中对应的页表项地址。接着通过一个循环,为start到end之间的子内存区修改内核页表。 在每次循环的过程中,next为当前页表项所映射的内存区的终止地址。通过vmap_pud_range()继续修改start到next之间所对应的页上级目录

static int vmap_page_range_noflush(unsigned long start, unsigned long end,
                                   pgprot_t prot, struct page **pages)
{
        pgd_t *pgd;
        unsigned long next;
        unsigned long addr = start;
        int err = 0;
        int nr = 0;

        BUG_ON(addr >= end);
        pgd = pgd_offset_k(addr);
        do {
                next = pgd_addr_end(addr, end);
                err = vmap_pud_range(pgd, addr, next, prot, pages, &nr);
                if (err)
                        return err;
        } while (pgd++, addr = next, addr != end);

        return nr;
}

vmap_pud_range()所做的工作和vmap_page_range_noflush()类似,只不过它是针对页上级目录的页表项做出对应的修改。首先pud_alloc()为addr分配对应的页上级目录,同时也将该页上级目录对应的物理地址写入页全局目录对应的表项中。接着再次进入一个循环来依次修改addr对应的页中间目录,每次循环时计算出当前页上级目录所映射的内存区间范围addr和next。

static int vmap_pud_range(pgd_t *pgd, unsigned long addr,
                unsigned long end, pgprot_t prot, struct page **pages, int *nr)
{
        pud_t *pud;
        unsigned long next;

        pud = pud_alloc(&init_mm, pgd, addr);
        if (!pud)
                return -ENOMEM;
        do {
                next = pud_addr_end(addr, end);
                if (vmap_pmd_range(pud, addr, next, prot, pages, nr))
                        return -ENOMEM;
        } while (pud++, addr = next, addr != end);
        return 0;
}

vmap_pmd_range()为页中件目录所指向的所有页表执行和上述类似的循环。

static int vmap_pmd_range(pud_t *pud, unsigned long addr,
                unsigned long end, pgprot_t prot, struct page **pages, int *nr)
{
        pmd_t *pmd;
        unsigned long next;

        pmd = pmd_alloc(&init_mm, pud, addr);
        if (!pmd)
                return -ENOMEM;
        do {
                next = pmd_addr_end(addr, end);
                if (vmap_pte_range(pmd, addr, next, prot, pages, nr))
                        return -ENOMEM;
        } while (pmd++, addr = next, addr != end);
        return 0;
}

现在为addr更新最后一级页表,首先由pte_alloc_kernel()生成页表pte,再通过循环依次更新每个页表项。在每次循环过程中,先通过pages数组得到第nr个页框的页描述符page,再将其传入mk_pte生成对应页表项。最后由set_pte_at()将此页表项更新到pte所指的页表的对应项中。页表项的增加过程由当前线性地址addr加上PAGE_SIZE完成。

static int vmap_pte_range(pmd_t *pmd, unsigned long addr,
                unsigned long end, pgprot_t prot, struct page **pages, int *nr)
{
        pte_t *pte;

        pte = pte_alloc_kernel(pmd, addr);
        if (!pte)
                return -ENOMEM;
        do {
                struct page *page = pages[*nr];

                if (WARN_ON(!pte_none(*pte)))
                        return -EBUSY;
                if (WARN_ON(!page))
                        return -ENOMEM;
                set_pte_at(&init_mm, addr, pte, mk_pte(page, prot));
                (*nr)++;
        } while (pte++, addr += PAGE_SIZE, addr != end);
        return 0;
}

经过对每级页表的层层修改,最终start到end之间的连续vmalloc区都与相应的物理页框相映射。整个vmalloc()完成。

  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值