Linux内存管理 vmap/vmalloc/ioremap

背景

  • Read the fucking source code! --By 鲁迅
  • A picture is worth a thousand words. --By 高尔基

说明:

  1. Kernel版本:4.14
  2. ARM64处理器,Contex-A53,双核
  3. 使用工具:Source Insight 3.5, Visio

1. 概述

在之前的系列文章中,分析到了Buddy System的页框分配,Slub分配器的小块内存对象分配,这些分配的地址都是物理内存连续的。当内存碎片后,连续物理内存的分配就会变得困难,可以使用vmap机制,将不连续的物理内存页框映射到连续的虚拟地址空间中。vmalloc的分配就是基于这个机制来实现的。

还记得下边这张图吗?

vmap/vmalloc的区域就是在VMALLOC_START ~ VMALLOC_END之间。

开启探索之旅吧。

2. 数据结构

2.1 vmap_area/vm_struct

这两个数据结构比较简单,直接上代码:

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

struct vmap_area {
	unsigned long va_start;
	unsigned long va_end;
	unsigned long flags;
	struct rb_node rb_node;         /* address sorted rbtree */
	struct list_head list;          /* address sorted list */
	struct llist_node purge_list;    /* "lazy purge" list */
	struct vm_struct *vm;
	struct rcu_head rcu_head;
};

struct vmap_area用于描述一段虚拟地址的区域,从结构体中va_start/va_end也能看出来。同时该结构体会通过rb_node挂在红黑树上,通过list挂在链表上。
struct vmap_areavm字段是struct vm_struct结构,用于管理虚拟地址和物理页之间的映射关系,可以将struct vm_struct构成一个链表,维护多段映射。

关系如下图:

2.2 红黑树

红黑树,本质上是一种二叉查找树,它在二叉查找树的基础上增加了着色相关的性质,提升了红黑树在查找,插入,删除时的效率。在红黑树中,节点已经进行排序,对于每个节点,左侧的的元素都在节点之前,右侧的元素都在节点之后。
红黑树必须满足以下四条规则:

  1. 每个节点不是红就是黑;
  2. 红黑树的根必须是黑;
  3. 红节点的子节点必须为黑;
  4. 从节点到子节点的每个路径都包含相同数量的黑节点,统计黑节点个数时,空指针也算黑节点;

定义如下:

struct rb_node {
	unsigned long  __rb_parent_color;
	struct rb_node *rb_right;
	struct rb_node *rb_left;
} __attribute__((aligned(sizeof(long))));
    /* The alignment might seem pointless, but allegedly CRIS needs it */

由于内核会频繁的进行vmap_area的查找,红黑树的引入就是为了解决当查找数量非常多时效率低下的问题,在红黑树中,搜索元素,插入,删除等操作,都会变得非常高效。至于红黑树的算法操作,本文就不再深入分析,知道它的用途即可。

3. vmap/vunmap分析

3.1 vmap

vmap函数,完成的工作是,在vmalloc虚拟地址空间中找到一个空闲区域,然后将page页面数组对应的物理内存映射到该区域,最终返回映射的虚拟起始地址。

整体流程如下:

操作流程比较简单,来一个样例分析,就清晰明了了:

vmap调用中,关键函数为alloc_vmap_area,它先通过vmap_area_root二叉树来查找第一个区域first vm_area,然后根据这个first vm_area去查找vmap_area_list链表中满足大小的空间区域。

alloc_vmap_area函数中,有几个全局的变量:

static struct rb_node *free_vmap_cache;
static unsigned long cached_hole_size;
static unsigned long cached_vstart;
static unsigned long cached_align;

用于缓存上一次分配成功的vmap_area,其中cached_hole_size用于记录缓存vmap_area对应区域之前的空洞的大小。缓存机制当然也是为了提高分配的效率。

3.2 vunmap

vunmap执行的是跟vmap相反的过程:从vmap_area_root/vmap_area_list中查找vmap_area区域,取消页表映射,再从vmap_area_root/vmap_area_list中删除掉vmap_area,页面返还给伙伴系统等。由于映射关系有改动,因此还需要进行TLB的刷新,频繁的TLB刷新会降低性能,因此将其延迟进行处理,因此称为lazy tlb

来看看逆过程的流程:

4. vmalloc/vfree分析

主要分以下三步:

  1. 从VMALLOC_START到VMALLOC_END查找空闲的虚拟地址空间(hole)
  2. 根据分配的size,调用alloc_page依次分配单个页面.
  3. 把分配的单个页面,映射到第一步中找到的连续的虚拟地址。把分配的单个页面,映射到第一步中找到的连续的虚拟地址。

#include <linux/vmalloc.h>
void *vmalloc(unsigned long size);     //使用vfree();释放内存
1
2
两者区别
1、 vmalloc()分配的内存虚拟地址是连续的,而物理地址无须连续。而kmalloc()确保页在物理地址上是连续的,自然虚拟地址也是连续的。硬件设备用的的任何内存区都必须是物理上连续的块,而不仅仅是虚拟地址连续上的块。
2、 vmalloc()相比较于kmalloc()效率不高,因为获得的页必须转换为虚拟地址空间上连续的页,必须专门建立页表项。
3、 vmalloc()仅在不得已时才使用——典型的就是为了申请大块内存。该函数可能睡眠,因此不能从终端上下文中调用,也不能从其他不允许阻塞的情况下进行调用。
顺带提一句,kmalloc和vmalloc分配的是内核的内存,malloc分分配的是用户的内存。

kmem_cache_alloc

void *kmem_cache_alloc(struct kmem_cache *cachep, gfp_t flags)
1
kmalloc是基于kmem_cache_create实现的;
如果是申请固定大小的内存空间,kmalloc和kmem_cache_alloc时间效率相当。kmem_cache_create申请的内存大小是固定的。
因为kmalloc所利用的内存块的大小是事先定义好的,所以很多情况下会产生内部碎片,浪费空间,而kmem_cache_alloc由于内存大小也是量身定做的缘故则不会。但是,有一点是kmem_cache_alloc所不能做到的,那就是动态大小的内存空间,这个任务是非kmalloc莫属。

Add
phys_to_virt/virt_to_phys

对于提供了MMU的处理器而言,Linux提供了复杂的存储管理系统,使得进程所能访问的内存达到4GB。进程的4GB内存空间被人为的分为两个部分:用户空间与内核空间。用户空间地址分布从0到3GB(PAGE_OFFSET,在0x86中它等于0xC0000000),3GB到4GB为内核空间。

内核空间中,从3G到vmalloc_start这段地址是物理内存映射区域(该区域中包含了内核镜像、物理页框表mem_map等等),比如VMware虚拟系统内存是160M,那么3G~3G+160M这片内存就应该映射物理内存。在物理内存映射区之后,就是vmalloc区域。
对于160M的系统而言,vmalloc_start位置应在3G+160M附近(在物理内存映射区与vmalloc_start期间还存在一个8M的gap来防止跃界),vmalloc_end的位置接近4G(最后位置系统会保留一片128k大小的区域用于专用页面映射),
  kmalloc和get_free_page申请的内存位于物理内存映射区域,而且在物理上也是连续的,它们与真实的物理地址只有一个固定的偏移,因此存在较简单的转换关系,virt_to_phys()可以实现内核虚拟地址转化为物理地址

4.1 vmalloc

vmalloc用于分配一个大的连续虚拟地址空间,该空间在物理上不连续的,因此也就不能用作DMA缓冲区。vmalloc分配的线性地址区域,在文章开头的图片中也描述了:VMALLOC_START ~ VMALLOC_END

直接分析调用流程:

从过程中可以看出,vmallocvmap的操作,大部分的逻辑操作是一样的,比如从VMALLOC_START ~ VMALLOC_END区域之间查找并分配vmap_area, 比如对虚拟地址和物理页框进行映射关系的建立。不同之处,在于vmap建立映射时,page是函数传入进来的,而vmalloc是通过调用alloc_page接口向Buddy System申请分配的。

  • vmalloc VS kmalloc
    到现在,我们应该能清楚vmallockmalloc的差异了吧,kmalloc会根据申请的大小来选择基于slub分配器或者基于Buddy System来申请连续的物理内存。而vmalloc则是通过alloc_page申请order = 0的页面,再映射到连续的虚拟空间中,物理地址不连续,此外vmalloc可以休眠,不应在中断处理程序中使用。
    vmalloc相比,kmalloc使用ZONE_DMA和ZONE_NORMAL空间,性能更快,缺点是连续物理内存空间的分配容易带来碎片问题,让碎片的管理变得困难。

4.2 vfree

直接上代码:

void vfree(const void *addr)
{
	BUG_ON(in_nmi());

	kmemleak_free(addr);

	if (!addr)
		return;
	if (unlikely(in_interrupt()))
		__vfree_deferred(addr);
	else
		__vunmap(addr, 1);
}

如果在中断上下文中,则推迟释放,否则直接调用__vunmap,所以它的逻辑基本和vunmap一致,不再赘述了。

前面我们学习了linux内核中对于物理上连续的分配方式,采用伙伴系统和slub分配器分配内存,但是我们知道物理上连续的映射是最好的分配方式,但并不总能成功地使用。在分配一大块内存时,可能竭尽全力也无法找到连续的内存块。针对这种情况内核提供了一种申请一片连续的虚拟地址空间,但不保证物理空间连续,也就是vmalloc接口。

vmalloc的工作方式类似于kmalloc,只不过前者分配的内存虚拟地址连续,而物理地址则无需连续,因此不能用于dma缓冲区
通过vmalloc获得的页必须一个一个地进行映射,效率不高,因此不得已时才使用,同时vmalloc分配的一般是大块内存
vmalloc分配的一般是高端内存,只有当内存不够的时候,才会分配低端内存
1. 数据结构
在进行vmalloc代码走读之前,先简单看一下两个重要的数据结构:struct vm_struct(vmalloc描述符)和struct vmap_area(记录在vmap_area_root中的vmalooc分配情况和vmap_area_list列表中)。内核在管理虚拟内存中的vmalloc区域时,必须跟踪哪些区域被使用,哪些是空闲的,为此定义了一个数据结构,将所有的部分保存在一个链表中

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

结构体变量    描述
*next    所有的vm_struct通过next 组成一个单链表,表头为全局变量vmlist
*addr    定义了这个虚拟地址空间子区域的起始地址
size    定义了这个虚拟地址空间子区域的大小
flags    存储了与该内存区关联的标志位
**pages    指向page指针的数组,每个数组成员都表示一个映射到这个地址空间的物理页面的实例
nr_pages    page指针数据的长度
phys_addr    仅当用ioremap映射了由物理地址描述的物理内存区域才有效
在创建一个新的虚拟内存区之前,必须要找到一个适合的位置。v

struct vmap_area {
    unsigned long va_start;
    unsigned long va_end;
    unsigned long flags;
    struct rb_node rb_node;         /* address sorted rbtree */
    struct list_head list;          /* address sorted list */
    struct llist_node purge_list;    /* "lazy purge" list */
    struct vm_struct *vm;
    struct rcu_head rcu_head;
};

成员变量    描述
va_start    vmalloc区的虚拟区间起始地址
va_end    vmalloc区的虚拟区间结束地址
flags    类型标识
rb_node    插入红黑树vmap_area_root的节点
list    用于加入链表vmap_area_list的节点
purge_list    用于加入到全局链表vmap_purge_list中
vm    指向对应的vm_struct
struct vmap_area用于描述一段虚拟地址的区域,从结构体中va_start/va_end也能看出来。同时该结构体会通过rb_node挂在红黑树上,通过list挂在链表上。
struct vmap_area中vm字段是struct vm_struct结构,用于管理虚拟地址和物理页之间的映射关系,可以将struct vm_struct构成一个链表,维护多段映射。

2. 初始化
在系统初始化的时候,会通过mm_init初始化vmalloc,其初始化流程如下

void __init vmalloc_init(void)
{
    struct vmap_area *va;
    struct vm_struct *tmp;
    int i;

    for_each_possible_cpu(i) {                               ------------(1)       
        struct vmap_block_queue *vbq;
        struct vfree_deferred *p;

        vbq = &per_cpu(vmap_block_queue, i);
        spin_lock_init(&vbq->lock);
        INIT_LIST_HEAD(&vbq->free);
        p = &per_cpu(vfree_deferred, i);
        init_llist_head(&p->list);
        INIT_WORK(&p->wq, free_work);
    }

    /* Import existing vmlist entries. */
    for (tmp = vmlist; tmp; tmp = tmp->next) {              -------------(2)
        va = kzalloc(sizeof(struct vmap_area), GFP_NOWAIT);
        va->flags = VM_VM_AREA;
        va->va_start = (unsigned long)tmp->addr;
        va->va_end = va->va_start + tmp->size;
        va->vm = tmp;
        __insert_vmap_area(va);
    }

    vmap_area_pcpu_hole = VMALLOC_END;

    vmap_initialized = true;
}



先遍历每CPU的vmap_block_queue和vfree_deferred变量并进行初始化。其中vmap_block_queue是非连续内存块队列管理结构,主要是队列以及对应的保护锁;而vfree_deferred是vmalloc的内存延迟释放管理,除了队列初始外,还创建了一个free_work()工作队列用于异步释放内存。
接着将挂接在vmlist链表的各项__insert_vmap_area()输入到非连续内存块的管理中,而vmlist的初始化是通过iotable_init初始化(arm32),最终所有的vmalloc的eara都会挂到vmap_area_list链表中
3. vmalloc分配
vmalloc为内核分配了一个连续的@size虚拟地址空间,然后调用__vmalloc_node_flags()函数void *vmalloc(unsigned long size)
{
    return __vmalloc_node_flags(size, NUMA_NO_NODE,
                    GFP_KERNEL | __GFP_HIGHMEM);
}

__vmalloc_node_flags请求节点为内核分配连续的虚拟内存

static inline void *__vmalloc_node_flags(unsigned long size,
                    int node, gfp_t flags)
{
    return __vmalloc_node(size, 1, flags, PAGE_KERNEL,
                    node, __builtin_return_address(0));
}

对于内核,@node分配连续的虚拟内存,但是虚拟地址使用VMALLOC地址空间的空白空间映射虚拟地址[VMALLOC_START, VMALLOC_END]

static void *__vmalloc_node(unsigned long size, unsigned long align,
                gfp_t gfp_mask, pgprot_t prot,
                int node, const void *caller)
{
    return __vmalloc_node_range(size, align, VMALLOC_START, VMALLOC_END,
                gfp_mask, prot, 0, node, caller);
}

请求节点分配连续的虚拟内存,但虚拟地址使用指定范围内的空白空间映射虚拟地址

void *__vmalloc_node_range(unsigned long size, unsigned long align,
            unsigned long start, unsigned long end, gfp_t gfp_mask,
            pgprot_t prot, unsigned long vm_flags, int node,
            const void *caller)
{
    struct vm_struct *area;
    void *addr;
    unsigned long real_size = size;

    size = PAGE_ALIGN(size);                                                                   -------------------(1)
    if (!size || (size >> PAGE_SHIFT) > totalram_pages)
        goto fail;

    area = __get_vm_area_node(size, align, VM_ALLOC | VM_UNINITIALIZED |                       -------------------(2)
                vm_flags, start, end, node, gfp_mask, caller);
    if (!area)
        goto fail;

    addr = __vmalloc_area_node(area, gfp_mask, prot, node);                                   --------------------(3)
    if (!addr)
        return NULL;
    clear_vm_uninitialized_flag(area);                                                        --------------------(4)-
    kmemleak_alloc(addr, real_size, 2, gfp_mask);

    return addr;

fail:
    warn_alloc(gfp_mask,
              "vmalloc: allocation failure: %lu bytes", real_size);
    return NULL;
}

检查size正确性,不能为0且不能大于totalram_pages,totalram_pages是bootmem分配器移交给伙伴系统的物理内存页数总和,则通过故障标签返回null
请求虚拟地址范围内找到一个可以包含请求大小的空位置,并使用该虚拟地址配置vmap_area和vm_struct信息,然后将其返回,其最终是分配一个vm_struct结构,获取对应长度(注意额外加一页)高端连续地址,最终插入vmlist链表,也就是向内核请求一个空间大小相匹配的虚拟地址空间,返回管理信息结构vm_struct
alloc_vmap_area作用就是根据所要申请的高端地址的长度size(注意这里的size已经是加上一页隔离带的size),在vmalloc区找到一个合适的区间并把起始虚拟地址和结尾地址通知给内核
标示内存空间初始化,最后调用kmemleak_alloc()进行内存分配泄漏调测,并将虚拟地址返回
操作流程比较简单,下面是整个流程图如下:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Q790xtvj-1598885616423)(D:\学习总结\内存管理单元\image-20200830230800108.png)]

__vmalloc_node_range是vmalloc的核心函数,其主要是完成找到符合大小的空闲vmalloc区域,其代码流程如下

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, const void *caller)
{
    struct vmap_area *va;
    struct vm_struct *area;

    BUG_ON(in_interrupt());                                                             //vmalloc不能中在中断中被调用
    size = PAGE_ALIGN(size);                                                            //页对齐操作
    if (unlikely(!size))
        return NULL;

    if (flags & VM_IOREMAP)
        align = 1ul << clamp_t(int, get_count_order_long(size),
                       PAGE_SHIFT, IOREMAP_MAX_ORDER);

    area = kzalloc_node(sizeof(*area), gfp_mask & GFP_RECLAIM_MASK, node);              //分配一个struct vm_struct来描述vmalloc区域
    if (unlikely(!area))
        return NULL;

    if (!(flags & VM_NO_GUARD))                                                         //加一页作为安全区间
        size += PAGE_SIZE;

    va = alloc_vmap_area(size, align, start, end, node, gfp_mask);                     //申请一个vmap_area并将其插入vmap_area_root中
    if (IS_ERR(va)) {
        kfree(area);
        return NULL;
    }

    setup_vmalloc_vm(area, va, flags, caller);                                        //填充vmalloc描述符vm_struct area

    return area;
}

__vmalloc_area_node则进行实际的页面分配,并建立页表映射,更新页表cache,其代码流程如下

static void *__vmalloc_area_node(struct vm_struct *area, gfp_t gfp_mask,
                 pgprot_t prot, int node)
{
    struct page **pages;
    unsigned int nr_pages, array_size, i;
    const gfp_t nested_gfp = (gfp_mask & GFP_RECLAIM_MASK) | __GFP_ZERO;                          //添加__GFP_ZERO,仅保留与页数相关的标志
    const gfp_t alloc_mask = gfp_mask | __GFP_NOWARN;                                             //将__GFP_NOWARN添加到gfp_mask

    nr_pages = get_vm_area_size(area) >> PAGE_SHIFT;                                             //申请多少pages
    array_size = (nr_pages * sizeof(struct page *));                                             //需要多大的存放page指针的空间

    area->nr_pages = nr_pages;
    /* Please note that the recursion is strictly bounded. */
    if (array_size > PAGE_SIZE) {                                                               // 这里默认page_size 为4k 即4096
        pages = __vmalloc_node(array_size, 1, nested_gfp|__GFP_HIGHMEM,
                PAGE_KERNEL, node, area->caller);
    } else {                                                                                   //小于一页,则直接利用slab机制申请物理空间地址给pages
        pages = kmalloc_node(array_size, nested_gfp, node);
    }
    area->pages = pages;
    if (!area->pages) {
        remove_vm_area(area->addr);
        kfree(area);
        return NULL;
    }

    for (i = 0; i < area->nr_pages; i++) {                                                //每次申请一个page利用alloc_page直接申请物理页面
        struct page *page;

        if (node == NUMA_NO_NODE)
            page = alloc_page(alloc_mask);
        else
            page = alloc_pages_node(node, alloc_mask, 0);

        if (unlikely(!page)) {
            /* Successfully allocated i pages, free them in __vunmap() */
            area->nr_pages = i;
            goto fail;
        }
        area->pages[i] = page;                                                       //分配的地址存放在指针数组
        if (gfpflags_allow_blocking(gfp_mask))
            cond_resched();
    }

    if (map_vm_area(area, prot, pages))                                             //修改页表 ,一页一页的实现映射,以及flush cache保持数据的一致性
        goto fail;
    return area->addr;

fail:
    warn_alloc(gfp_mask,
              "vmalloc: allocation failure, allocated %ld of %ld bytes",
              (area->nr_pages*PAGE_SIZE), area->size);
    vfree(area->addr);
    return NULL;
}

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

1.通过__get_vm_area_node()函数查找一个足够大的空间的虚拟地址段,然后再通过kmalloc分配一个新的vm_struct结构体
2.计算当前分配的内存大小需要占用多少个page,然后通过kmalloc分配一组struct page指针数组,再通过调用buddy allocator接口alloc_page()每次获取一个物理页框填入到vm_struct中的struct page* 数则
3.分配PMD,PTE更新内核页表,返回映射后的虚拟地址。
此次,vmalloc在虚拟内存空间给出一块连续的内存区,实质上,这片连续的虚拟内存在物理内存中并不一定连续,所以vmalloc申请的虚拟内存和物理内存之间也就没有简单的换算关系,正因如此,vmalloc()通常用于分配远大于__get_free_pages()的内存空间,它的实现需要建立新的页表,此外还会调用使用GFP_KERN的kmalloc,一定不要在中断处理函数,tasklet和内核定时器等非进程上下文中使用vmalloc!

4. 其他分配
除了vmalloc之外,还有其他可以创建虚拟连续映射。

vmalloc_32分配适用于32位地址的内存区域。该函数会保证物理pages是从ZONE_NORMAL中进行分配并且要求当前设备是32位的,其工作方式与vmalloc相同
vmap使用一个page数组作为七点,来创建虚拟连续内存区。与vmalloc相比,该函数所用的物理内存位置不是隐式分配的,而需要先行分配好,作为参数传递。
本节的重点是介绍下vmap的流程,vmap函数完成的工作是,在vmalloc虚拟地址空间中找到一个空闲区域,然后将page页面数组对应的物理内存映射到该区域,最终返回映射的虚拟起始地址。

void *vmap(struct page **pages, unsigned int count,
        unsigned long flags, pgprot_t prot)
{
    struct vm_struct *area;
    unsigned long size;        /* In bytes */

    might_sleep();                                                           ---------------(1)

    if (count > totalram_pages)
        return NULL;

    size = (unsigned long)count << PAGE_SHIFT;                               ---------------(2)
    area = get_vm_area_caller(size, flags, __builtin_return_address(0));
    if (!area)
        return NULL;

    if (map_vm_area(area, prot, pages)) {                                    ---------------(3)
        vunmap(area->addr);
        return NULL;
    }

    return area->addr;
}

1.如果有任务紧急请求重新安排作为抢占点,则它将休眠;如果请求的页数比所有内存页数多,则放弃处理并返回null
2.VM分配配置vmap_area和vm_struct信息。失败时,将返回null
3.map_vm_area尝试映射具有vm_struct信息的页面,如果不成功,则在取消后返回null,如果成功,就返回映射的虚拟地址空间的起始地址
其整个代码流程如下:

下图通过在vmap虚拟地址空间中找到空白空间来显示请求的物理页面的映射。

5. 释放内存
有两个函数用于向内核释放内存,这两个函数都最终归结到vunmap

vfree用于释放vmalloc和vmalloc_32分配的区域
vunmap用于释放由vmap和Ioremap创建的映射
释放由vmalloc()分配和映射的连续虚拟地址内存。当通过中断处理程序调用时,映射的连续虚拟地址内存无法立即释放,因此在将工作队列中注册的free_work()函数添加到每个CPU vfree_deferred中以进行延迟处理后,对其进行调度。

void vfree(const void *addr)
{
    BUG_ON(in_nmi());

    kmemleak_free(addr);

    if (!addr)
        return;
    if (unlikely(in_interrupt())) {
        struct vfree_deferred *p = this_cpu_ptr(&vfree_deferred);
        if (llist_add((struct llist_node *)addr, &p->list))
            schedule_work(&p->wq);
    } else
        __vunmap(addr, 1);
}

vmap()函数取消映射到vmalloc地址空间的虚拟地址区域的映射。但是,物理页面不会被释放。

void vunmap(const void *addr)
{
    BUG_ON(in_interrupt());
    might_sleep();
    if (addr)
        __vunmap(addr, 0);
}

vunmap执行的是跟vmap相反的过程:从vmap_area_root/vmap_area_list中查找vmap_area区域,取消页表映射,再从vmap_area_root/vmap_area_list中删除掉vmap_area,页面返还给伙伴系统等。由于映射关系有改动,因此还需要进行TLB的刷新,频繁的TLB刷新会降低性能,因此将其延迟进行处理,因此称为lazy tlb。

static void __vunmap(const void *addr, int deallocate_pages)
{
    struct vm_struct *area;

    if (!addr)
        return;

    if (WARN(!PAGE_ALIGNED(addr), "Trying to vfree() bad address (%p)\n",
            addr))
        return;

    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, get_vm_area_size(area));
    debug_check_no_obj_freed(addr, get_vm_area_size(area));

    if (deallocate_pages) {
        int i;

        for (i = 0; i < area->nr_pages; i++) {
            struct page *page = area->pages[i];

            BUG_ON(!page);
            __free_pages(page, 0);
        }

        kvfree(area->pages);
    }

    kfree(area);
    return;
}


addr表示要释放的区域的起始地址,deallocate_pages指定了是否将与该区域相关的物理内存页返回给伙伴系统。其代码流程如下图

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-9SydmqZV-1598885616429)(D:\学习总结\内存管理单元\image-20200831221309379.png)]

6. 总结
本章大致梳理了对于物理内存不连续,虚拟内存连续的内存申请的方式,vmalloc和vmap的操作,大部分的逻辑操作是一样的,比如从VMALLOC_START ~ VMALLOC_END区域之间查找并分配vmap_area, 比如对虚拟地址和物理页框进行映射关系的建立。

不同之处,在于vmap建立映射时,page是函数传入进来的,而vmalloc是通过调用alloc_page接口向Buddy System申请分配的。

我们应该能清楚vmalloc和kmalloc的差异了吧,kmalloc会根据申请的大小来选择基于slub分配器或者基于Buddy System来申请连续的物理内存。而vmalloc则是通过alloc_page申请order = 0的页面,再映射到连续的虚拟空间中,物理地址不连续,此外vmalloc可以休眠,不应在中断处理程序中使用。
与vmalloc相比,kmalloc使用ZONE_DMA和ZONE_NORMAL空间,性能更快,缺点是连续物理内存空间的分配容易带来碎片问题,让碎片的管理变得困难,结合前面学习的伙伴系统和slab分配方式,总结如下

在这里插入图片描述

这些都是内核空间的内存分配的方式,后面章节会讲解对于用户空间的内存分配的方式。

5. IOremap

对于提供了MMU(存储管理器,辅助操做系统进行内存管理,提供虚实地址转换等硬件支持)的处理器而言,Linux提供了复杂的存储管理系统,使得进程所能访问的内存达到4GB。

  进程的4GB内存空间被人为的分为两个部分--用户空间与内核空间。用户空间地址分布从0到3GB(PAGE_OFFSET,在0x86中它等于0xC0000000),3GB到4GB为内核空间。

  1.无论是在用户空间仍是在内核空间,软件一概不能去直接访问设备的物理地址;
  2.在内核驱动中若是要访问设备的物理地址,须要利用ioremap将设备的物理地址映射到内核虚拟地址上(动态内存映射区),之后驱动程序访问这个内核虚拟地址就是在间接得访问设备的物理地址(MMU,TLB,TTW)
  3.若是用户要访问硬件设备,不能直接访问,也不能在用户空间访问,只能经过系统调用(open,close,read,write,ioctl)来访问映射好的内核虚拟地址,经过这种间接的访问来访问硬件设备,可是若是涉及到数据的拷贝,还须要借助4个内存拷贝函数!

  1.应用程序经过read,write,ioctl来访问硬件设备,它们都要通过两次的数据拷贝,一次是用户空间和内核空间的数据拷贝,另一次是内核空间和硬件之间的数据拷贝,若是设备拷贝的数据量比较小,那么read,write,ioctl的两次数据拷贝的过程对系统的影响几乎能够忽略不计,若是设备的数据量很是大,例如显卡(独立),LCD屏幕(显存共享主存),摄像头,声卡这类设备涉及的数据量比较庞大,若是仍是用read,write,ioctl进行访问设备数据,无形对系统的性能影响很是大。
  2.用户访问设备,最终其实涉及的用户和硬件,而read,write,ioctl自己会牵扯到内核,因此这些函数涉及2次的数据拷贝,用户要直接去访问硬件设备,只须要将硬件设备的物理地址信息映射到用户的虚拟地址空间便可,一旦完毕,不会在牵扯到内核空间,之后用户直接访问用户的虚拟地址就是在访问设备硬件,由2次的数据拷贝的转换为一次的数据拷贝。未使用mmap,应用程序只能经过系统调用(read,write,ioctl)内核函数,内核函数得到硬件数据,应用程序再从内核函数获取数据;使用mmap后,用户程序能够直接访问映射后的虚拟地址得到硬件数据。

mmap:将硬件物理地址映射到用户虚拟地址空间,应用程序能够直接访问映射后的地址。由2次数据拷贝变成1次数据拷贝!mmap用于在用户程序中映射整个设备的物理地址。
ioremap:是将物理地址转换为内核虚拟地址,内核能够直接访问映射后的地址。ioremap用于在内核程序中映射设备的一个物理寄存器。


mmap是将设备内存线性地址映射到用户地址空间,用于大片数据好比音视频存储空间。
最后,咱们要特别强调驱动程序中mmap函数的实现方法。用mmap映射一个设备,意味着使用户空间的一段地址关联到设备内存上,这使得只要程序在分配的地址范围内进行读取或者写入,实际上就是对设备的访问。

linux的线程只能访问虚拟地址,无论是否是内核,ioremap应用,好比有个寄存器地址是0xe8000000你要用ioremap映射后,才能访问地址0xe8000000。这两个地址是不一样的,mmu会帮你搞定,对你是透明的

一、ioremap函数定义  linux

 ioremap宏定义在asm/io.h内:

#define ioremap(cookie,size)           __ioremap(cookie,size,0)

__ioremap函数原型为(arm/mm/ioremap.c):

void __iomem * __ioremap(unsigned long phys_addr, size_t size, unsigned long flags);

参数:

phys_addr:要映射的起始的IO地址

size:要映射的空间的大小

flags:要映射的IO空间和权限有关的标志

该函数返回映射后的内核虚拟地址(3G-4G). 接着即可以经过读写该返回的内核虚拟地址去访问之这段I/O内存资源。

文章目录
1.1 背景
1.1.1 ARM32 内存空间
1.1.2 ioremap 实现
1.1.3 Linux内存属性
上篇文章:ARM Linux 内存管理入门及渐进 4 - 常用接口实现(memcpy/copy_to_user)

1.1 背景
在编写 linux 驱动过程中,不可避免的会涉及操作外设,而外设的地址空间与 DDR的地址空间一般不连续,在 linux上电时,并不会为外设地址空间建立页表,又因为linux 访问内存使用的都是虚拟地址,因此如果想访问外设的寄存器(一般包括数据寄存器、控制寄存器与状态寄存器),需要在驱动初始化中将外设所处的物理地址映射为虚拟地址,linux 为应对该问题提供了较多接口以应对不同的场景需求,arch/arm64(arm)/include/asm/io.h中有如下几种 ioremap 接口:

ioremap
ioremap_wc
devm_ioremap
devm_ioremap_resource
1

ioremap:用来映射 memory type 为 device memory 的设备,同时不使用cache(device memory本身就没有 cacheable 这个属性),即 CPU 的读写操作直接操作设备内存。
ioremap_nocache:的实现与 ioremap 完全相同,保留该符号是因为向后兼容使用ioremap_nocache接口的驱动程序。
ioremap_cached 用来映射 memory type 为 normal memory 的设备,同时使用cache,这会提高内存的访问速度,提高系统的性能。
ARM Linux 引入设备树特性后,一些支持设备树的设备驱动不再使用直接 ioremap(),改用 drivers/of/address.c/of_iomap(),of_iomap() 的内部仍然会调用 ioremap()

1.1.1 ARM32 内存空间
通常进程的4GB内存空间被人为的分为两个部分:

用户空间与, 用户空间地址分布从0到3GB;
内核空间,3GB-4GB。
内核空间中,从3G到 vmalloc_start 这段地址是物理内存映射区域(该区域中包含了内核镜像、物理页框表mem_map等等)。在物理内存映射区之后,就是vmalloc区域,vmalloc_end 的位置接近4G(最后位置系统会保留一片128k大小的区域用于专用页面映射)。
1.1.2 ioremap 实现
arch/arm64/include/asm/io.h

#define ioremap(addr, size)             __ioremap((addr), (size), __pgprot(PROT_DEVICE_nGnRE))
#define ioremap_nocache(addr, size)     __ioremap((addr), (size), __pgprot(PROT_DEVICE_nGnRE))
#define ioremap_wc(addr, size)          __ioremap((addr), (size), __pgprot(PROT_NORMAL_NC))
#define ioremap_wt(addr, size)          __ioremap((addr), (size), __pgprot(PROT_DEVICE_nGnRE))
#define iounmap                         __iounmap
1
2
3
4
5
从上面的接口定义看,除了addr和size外还有一个__pgprot()参数,上面接口中参数类型有:

PROT_DEVICE_nGnRE;
PROT_NORMAL_NC。
两种类型。实际在 pgtable-prot.h 中还有 PROT_NORMAL_WT和PROT_NORMAL。ioremap_xxx 接口的功能属性都是由__pgprot()决定的,那__pgprot()具体是什么含义呢?
1.1.3 Linux内存属性
在内核源码memory.h中有定义ioremap接口中可用的内存类型:

arch/arm64/include/asm/memory.h

/*
 * Memory types available.
 */
#define MT_DEVICE_nGnRnE        0
#define MT_DEVICE_nGnRE         1
#define MT_DEVICE_GRE           2
#define MT_NORMAL_NC            3
#define MT_NORMAL               4
#define MT_NORMAL_WT            5

/*
 * Memory types for Stage-2 translation
 */
#define MT_S2_NORMAL            0xf
#define MT_S2_DEVICE_nGnRE      0x1
上面 ioremap_xxx 接口中用到的属性参数主要是PROT_DEVICE_nGnRE和PROT_NORMAL_NC。看下这两个prot_val分别代表什么含义:

#define PROT_DEFAULT            (_PROT_DEFAULT | PTE_MAYBE_NG)
#define PROT_SECT_DEFAULT       (_PROT_SECT_DEFAULT | PMD_MAYBE_NG)

#define PROT_DEVICE_nGnRnE      (PROT_DEFAULT | PTE_PXN | PTE_UXN | PTE_WRITE | PTE_ATTRINDX(MT_DEVICE_nGnRnE))
#define PROT_DEVICE_nGnRE       (PROT_DEFAULT | PTE_PXN | PTE_UXN | PTE_WRITE | PTE_ATTRINDX(MT_DEVICE_nGnRE))
#define PROT_NORMAL_NC          (PROT_DEFAULT | PTE_PXN | PTE_UXN | PTE_WRITE | PTE_ATTRINDX(MT_NORMAL_NC))
#define PROT_NORMAL_WT          (PROT_DEFAULT | PTE_PXN | PTE_UXN | PTE_WRITE | PTE_ATTRINDX(MT_NORMAL_WT))
#define PROT_NORMAL             (PROT_DEFAULT | PTE_PXN | PTE_UXN | PTE_WRITE | PTE_ATTRINDX(MT_NORMAL))

#define PROT_SECT_DEVICE_nGnRE  (PROT_SECT_DEFAULT | PMD_SECT_PXN | PMD_SECT_UXN | PMD_ATTRINDX(MT_DEVICE_nGnRE))
#define PROT_SECT_NORMAL        (PROT_SECT_DEFAULT | PMD_SECT_PXN | PMD_SECT_UXN | PMD_ATTRINDX(MT_NORMAL))
#define PROT_SECT_NORMAL_EXEC   (PROT_SECT_DEFAULT | PMD_SECT_UXN | PMD_ATTRINDX(MT_NORMAL))
参照ARMv8手册中对内存属性的描述,内存可以分为DEVICE和NORMAL两大类型以及Device memory依据是否可合并等属性。

Normal型:sram或者dram那样的内存空间,一般都是过cache的(当然也可不过cache,如外设访问的地址空间,标记为NC)
Device型:设备寄存器那样的io空间,都不会过cache。
Device属性的内存空间还有下面三种子属性,都有打开和关闭的定义。

G(gather:对多个memory的访问可以合并) nG与之相反;
R(Reordering:对内存访问指令进行重排) nR与之相反;
E(Early Write Acknowledgement hint:写操作的ack可提早应答) nE与之相反。
MAIR寄存器定义如下:
Linux 预先定义了6种内存属性,分别存在MAIR寄存器的attr0~attr5。内存页表属性部分可以选择这个寄存器的某个index,范围(0~5)作为自己的属性。

上篇文章:ARM Linux 内存管理入门及渐进 4 - 常用接口实现(memcpy/copy_to_user)

推荐阅读:
https://blog.csdn.net/m0_51717456/article/details/124259339
https://blog.csdn.net/suifengershi2000/article/details/122805722
————————————————
版权声明:本文为CSDN博主「CodingCos」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/sinat_32960911/article/details/127969010

我们知道默认外设I/O资源部在linux的内核空间中,如果我们想要访问外设I/O,必须先将其地址映射到内核空间中,然后才能在内核空间中访问它。Linux内核访问外设I/O内存资源的方式有两种

动态映射(ioremap)
静态映射(map_desc)
动态映射的方式是我们使用的比较多,而且比较简单的方式,即直接通过内核提供的ioremap函数动态创建一段外设I/O内存资源到内核虚拟地址的映射表,从而就可以在内核空间中访问了。同时内核也提供了在系统启动时通过map_desc结构体静态创建I/O资源到内核空间的线性映射表(即page_table)的方式,内核的devicemaps_init函数就是实现这个功能,本章主要是分析其代码流程。

static void __init devicemaps_init(const struct machine_desc *mdesc)
{
    struct map_desc map;
    unsigned long addr;
    void *vectors;

    /*
     * Allocate the vector page early.
     */
    vectors = early_alloc(PAGE_SIZE * 2);                                       ----------------(1)

    early_trap_init(vectors);

    /*
     * Clear page table except top pmd used by early fixmaps
     */
    for (addr = VMALLOC_START; addr < (FIXADDR_TOP & PMD_MASK); addr += PMD_SIZE)  --------------(2)
        pmd_clear(pmd_off_k(addr));

    /*
     * Map the kernel if it is XIP.
     * It is always first in the modulearea.
     */
#ifdef CONFIG_XIP_KERNEL
    map.pfn = __phys_to_pfn(CONFIG_XIP_PHYS_ADDR & SECTION_MASK);
    map.virtual = MODULES_VADDR;
    map.length = ((unsigned long)_exiprom - map.virtual + ~SECTION_MASK) & SECTION_MASK;
    map.type = MT_ROM;
    create_mapping(&map);
#endif

    /*
     * Map the cache flushing regions.
     */
#ifdef FLUSH_BASE
    map.pfn = __phys_to_pfn(FLUSH_BASE_PHYS);
    map.virtual = FLUSH_BASE;
    map.length = SZ_1M;
    map.type = MT_CACHECLEAN;
    create_mapping(&map);
#endif
#ifdef FLUSH_BASE_MINICACHE
    map.pfn = __phys_to_pfn(FLUSH_BASE_PHYS + SZ_1M);
    map.virtual = FLUSH_BASE_MINICACHE;
    map.length = SZ_1M;
    map.type = MT_MINICLEAN;
    create_mapping(&map);
#endif

    /*
     * Create a mapping for the machine vectors at the high-vectors
     * location (0xffff0000).  If we aren't using high-vectors, also
     * create a mapping at the low-vectors virtual address.
     */
    map.pfn = __phys_to_pfn(virt_to_phys(vectors));                               -------------(3)
    map.virtual = 0xffff0000;
    map.length = PAGE_SIZE;
#ifdef CONFIG_KUSER_HELPERS
    map.type = MT_HIGH_VECTORS;
#else
    map.type = MT_LOW_VECTORS;
#endif
    create_mapping(&map);

    if (!vectors_high()) {
        map.virtual = 0;
        map.length = PAGE_SIZE * 2;
        map.type = MT_LOW_VECTORS;
        create_mapping(&map);
    }

    /* Now create a kernel read-only mapping */
    map.pfn += 1;
    map.virtual = 0xffff0000 + PAGE_SIZE;
    map.length = PAGE_SIZE;
    map.type = MT_LOW_VECTORS;
    create_mapping(&map);

    /*
     * Ask the machine support to map in the statically mapped devices.
     */
    if (mdesc->map_io)                                                       -------------------(4)
        mdesc->map_io();
    else
        debug_ll_io_init();
    fill_pmd_gaps();

    /* Reserve fixed i/o space in VMALLOC region */
    pci_reserve_io();

    /*
     * Finally flush the caches and tlb to ensure that we're in a
     * consistent state wrt the writebuffer.  This also ensures that
     * any write-allocated cache lines in the vector page are written
     * back.  After this point, we can start to touch devices again.
     */
    local_flush_tlb_all();
    flush_cache_all();

    /* Enable asynchronous aborts */
    early_abt_enable();
}
1.分配两个page的物理页帧,然后通过early_trap_init会初始化异常向量表
2.主要是清除vmalloc区的相应页表项,该开发板对应的区间是0xe080 0000 ~ 0xffc0 0000
3.实际开始创建页表,为一个物理存储空间创建映射,先获取vectors的物理页地址,0xffff0000是arm默认的中断向量所在页,长度为1页, 注意,这里映射大小为4K,小于1M,将使用二级映射!,映射0xffff0000的那个page frame,地址是0xc0007ff8,如果SCTLR.V的值设定为low vectors,那么还要映射0地址开始的memory,然后映射high vecotr开始的第二个page frame,也就是对应到最开始申请的两个物理页帧
4.如果当前机器信息的mdesc的成员变量map_io有效,就调用map_io(),该函数指针依据不同机型均有不同的设置,主要是用于当前机器的IO设备映射到内核地址空间,也就是开始描述的静态映射。如果没有提供,使用debug_ll_io_init()自己实现,那么只需要实现debug_ll_addr(&map.pfn, &map.virtual); 这个函数。把map.pfn和map.virtual填上串口物理地址和映射的虚拟地址就行。
而early_trap_init做初始化异常向量表,其代码如下:

void __init early_trap_init(void *vectors_base)
{
#ifndef CONFIG_CPU_V7M
    unsigned long vectors = (unsigned long)vectors_base;
    extern char __stubs_start[], __stubs_end[];
    extern char __vectors_start[], __vectors_end[];
    unsigned i;

    vectors_page = vectors_base;

    /*
     * Poison the vectors page with an undefined instruction.  This
     * instruction is chosen to be undefined for both ARM and Thumb
     * ISAs.  The Thumb version is an undefined instruction with a
     * branch back to the undefined instruction.
     */
    for (i = 0; i < PAGE_SIZE / sizeof(u32); i++)
        ((u32 *)vectors_base)[i] = 0xe7fddef1;

    /*
     * Copy the vectors, stubs and kuser helpers (in entry-armv.S)
     * into the vector page, mapped at 0xffff0000, and ensure these
     * are visible to the instruction stream.
     */
    memcpy((void *)vectors, __vectors_start, __vectors_end - __vectors_start);
    memcpy((void *)vectors + 0x1000, __stubs_start, __stubs_end - __stubs_start);

    kuser_init(vectors_base);

    flush_icache_range(vectors, vectors + PAGE_SIZE * 2);
#else /* ifndef CONFIG_CPU_V7M */
    /*
     * on V7-M there is no need to copy the vector table to a dedicated
     * memory area. The address is configurable and so a table in the kernel
     * image can be used.
     */
#endif
}
将整个vector table那个page frame填充为0xe7fd def1,未定义指令,
这个函数把定义在 arch/arm/kernel/entry-armv.S 中的异常向量表和异常处理程序的 stub 进行重定位:异常向量表拷贝到 0xFFFF_0000,异常向量处理程序的 stub 拷贝到 0xFFFF_0200。然后调用 modify_domain()修改了异常向量表所占据的页面的访问权限,这使得用户态无法访问该页,只有核心态才可以访问。
下面就进入到本章的重点内容,设备区域映射,主要使用的是machine_desc这个结构体,而这个是在setup_arch中定义

    mdesc = setup_machine_fdt(__atags_pointer);
    if (!mdesc)
        mdesc = setup_machine_tags(__atags_pointer, __machine_arch_type);
对于支持DTB格式,直接通过compatible = “fsl,imx6ull-14x14-evk”, "fsl,imx6ull"来寻找到对应的machine_desc,其在arch/arm/mach-imx/mach-imx6ul.c 中

DT_MACHINE_START(IMX6UL, "Freescale i.MX6 UltraLite (Device Tree)")
        .map_io         = imx6ul_map_io,
        .init_irq       = imx6ul_init_irq,
        .init_machine   = imx6ul_init_machine,
        .init_late      = imx6ul_init_late,
        .dt_compat      = imx6ul_dt_compat,
MACHINE_END
这章我们分析了devicemaps_init的用途,其主要有以下两个用途:

为中断向量分配内存,为中断向量虚拟地址映射的页表分配内存,建立虚拟地址到物理地址的映射
调用mdesc->map_io()进行SOC相关的初始化,通过map_desc结构体静态创建I/O资源映射表

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值