ioremap函数分析

版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
本文链接:https://blog.csdn.net/qq_16777851/article/details/82261185

 

开始之前,先说一下ioremap的作用,ioremap主要是把寄存器做映射。

为什么要映射?

内核空间只能访问虚拟地址的3~4G的地址空间,通常3~4G的空间一部分是映射物理内存,通常默认不会映射寄存器,如果想要访问某个寄存器,则需要把这个寄存的虚拟地址映射到高端内存上。这样内核空间才能直接访问。

 

下面这篇文章,对3~4G的内核空间和io映射分析的比较好,值得好好看一下。

https://blog.csdn.net/godleading/article/details/18702029

 

打开源码,搜索ioremap,可以看得到有三个函数原型

其中一个是在include/sam-generic/目录下,仔细看它里面的源码,它并没有做任何形式的映射,只有在不开MMU的时候才可能用它

剩下的两个都在下面函数中定义的

从名字上可以看到我们是使用ram相关的,同时我也是在source insght中没加其它架构相关的源码,所以__arch_ioremap中也没高亮

 

上面相关的函数比较多,就以最长用的ioremap函数为例来分析

#define ioremap(cookie,size)		__arm_ioremap(cookie, size, MT_DEVICE)

我们知道第一个参数是寄存器的起始物理地址,第二个是要映射的寄存器的范围

__arm_ioremap第三个参数,表示上面列出来ioremap的几种形式,我们主要是使用普通的形式,下面的分析都默认以传入参数0来分析

#define MT_DEVICE		0
#define MT_DEVICE_NONSHARED	1
#define MT_DEVICE_CACHED	2
#define MT_DEVICE_WC		3

查找,共有两个函数,一个是nommu版本的,如下,直接返回传入地址,肯定不是我们要分析的

另一个则是我们需要分析的内容,这里它除了调用传入的三个参数外,还调用了另一个函数作为它的入口参数。。

void __iomem *
__arm_ioremap(unsigned long phys_addr, size_t size, unsigned int mtype)
{
	return __arm_ioremap_caller(phys_addr, size, mtype,
			__builtin_return_address(0));
}

__builtin_return_address(0)的含义是,得到当前函数返回地址,即此函数被别的函数调用,然后此函数执行完毕后,返回,所谓返回地址就是那时候的地址。如下所示。

6000 0000: ldr r0, =0x666
6000 0004: bl  call_func    /* 函数调用 */
6000 0008: str r0, =0x555   /* 调用返回地址 */

所以这个__arm_ioremap函数第四个参数的位置是调用ioremap函数完后,下一条指定的地址

 

 

接下来就看这个函数做了什么吧,

#ifdef __ASSEMBLY__    /* 是否把X强制转换成XY还是X */
#define _AC(X,Y)	X
#define _AT(T,X)	X
#else
#define __AC(X,Y)	(X##Y)
#define _AC(X,Y)	__AC(X,Y)
#define _AT(T,X)	((T)(X))
#endif

#define PAGE_SHIFT		12

/* 1和1UL对我们是一样的,这里是把1<<12位,即确定一个页的大小为0x1000即4096字节 */
#define PAGE_SIZE		(_AC(1,UL) << PAGE_SHIFT)

/* 0x1000 - 1 -->  0xfff 后  ~0xfff,在32位系统中是0xffff,f000,这里用这个的作用是
 * mmu的内存管理最小是一个页,即4096字节,所以要叶对齐时&使用 */
#define PAGE_MASK		(~(PAGE_SIZE-1))

#define	__phys_to_pfn(paddr)	((paddr) >> PAGE_SHIFT)    /* 物理地址到所属页 */


void __iomem *__arm_ioremap_caller(unsigned long phys_addr, size_t size,
	unsigned int mtype, void *caller)
{
	unsigned long last_addr;
 	unsigned long offset = phys_addr & ~PAGE_MASK;    /* 确定物理地址在页内的偏移 */
 	unsigned long pfn = __phys_to_pfn(phys_addr);     /* 找到物理地址所在的页地址 */

 	/*
 	 * Don't allow wraparound or zero size
         * 注释说的很明白,不允许映射的范围为0,也不允许结束地址小于起始地址(即phys_addr + size - 1 > 0xffff,ffff)
	 */
	last_addr = phys_addr + size - 1;
	if (!size || last_addr < phys_addr)
		return NULL;
    
        /* 这里传入参数分别是  页位置 页内偏移  映射大小  类型(我们默认是0) caller(ioremap后面的指定的地址) */
	return __arm_ioremap_pfn_caller(pfn, offset, size, mtype,
			caller);
}

 

/* 仔细追这里面每个宏的定义,可以发现是页表的一些定义Hardware page table definitions. */
static struct mem_type mem_types[] = {
	[MT_DEVICE] = {		  /* Strongly ordered / ARMv6 shared device */
		.prot_pte	= PROT_PTE_DEVICE | L_PTE_MT_DEV_SHARED |
				  L_PTE_SHARED,
		.prot_l1	= PMD_TYPE_TABLE,
		.prot_sect	= PROT_SECT_DEVICE | PMD_SECT_S,
		.domain		= DOMAIN_IO,
	},
	[MT_DEVICE_NONSHARED] = { /* ARMv6 non-shared device */
		.prot_pte	= PROT_PTE_DEVICE | L_PTE_MT_DEV_NONSHARED,
		.prot_l1	= PMD_TYPE_TABLE,
		.prot_sect	= PROT_SECT_DEVICE,
		.domain		= DOMAIN_IO,
        },
        .....................
    
	[MT_MEMORY] = {    /* 内存 */
		.prot_sect = PMD_TYPE_SECT | PMD_SECT_AP_WRITE,
		.domain    = DOMAIN_KERNEL,
	},
	[MT_ROM] = {       /* NOR 之类n */
		.prot_sect = PMD_TYPE_SECT,
		.domain    = DOMAIN_KERNEL,
	},
	[MT_MEMORY_NONCACHED] = {
		.prot_sect = PMD_TYPE_SECT | PMD_SECT_AP_WRITE,
		.domain    = DOMAIN_KERNEL,
	},
};

 

上面的表,其实就是我们页表中包括cache、权限管理等一些组合起来的配置,做成表后方便后建立页表时直接使用其中某一项。

#define	__pfn_to_phys(pfn)	((pfn) << PAGE_SHIFT)        /* 页到物理地址转换 */

/*
 * ARMv6 supersection address mask and size definitions.
 */
#define SUPERSECTION_SHIFT	24
#define SUPERSECTION_SIZE	(1UL << SUPERSECTION_SHIFT)  /* 0x1000000 */
#define SUPERSECTION_MASK	(~(SUPERSECTION_SIZE-1))     /* 0xff00,0000 */



const struct mem_type *get_mem_type(unsigned int type)
{
	return type < ARRAY_SIZE(mem_types) ? &mem_types[type] : NULL;
}


void __iomem * __arm_ioremap_pfn_caller(unsigned long pfn,
	unsigned long offset, size_t size, unsigned int mtype, void *caller)
{
	const struct mem_type *type;
	int err;
	unsigned long addr;
 	struct vm_struct * area;

	/*
	 * High mappings must be supersection aligned
     * 对高映射,即大于4G空间映射,必须是16M对齐,我们32位的一般不会出错在这里
	 */
	if (pfn >= 0x1000 && (__pfn_to_phys(pfn) & ~SUPERSECTION_MASK))
		return NULL;

	type = get_mem_type(mtype);    /* 我们传入的MT_DEVICE(设备[寄存器]),可以得到对应的在表中的该项的地址 */
	if (!type)
		return NULL;

	/*
	 * Page align the mapping size, taking account of any offset.
         * 将页面大小对其, 例如起始地址0x5fff,fff8,范围0x20字节,则也要映射两页
	 */
	size = PAGE_ALIGN(offset + size);   

    /* 找到一块满足size大小的区域 */
	area = get_vm_area_caller(size, VM_IOREMAP, caller);
 	if (!area)
 		return NULL;
    /* 将来映射好后,在高端内存区域的首地址 */
 	addr = (unsigned long)area->addr;


    /* 下面就是根据高端内存区域动态映射区域查找的地址,以及页信息,进行映射*/
#ifndef CONFIG_SMP    /* 我们不是多核CPU,所以要运行 */
	if (DOMAIN_IO == 0 &&
	    (((cpu_architecture() >= CPU_ARCH_ARMv6) && (get_cr() & CR_XP)) ||
	       cpu_is_xsc3()) && pfn >= 0x100000 &&
	       !((__pfn_to_phys(pfn) | size | addr) & ~SUPERSECTION_MASK)) {
		area->flags |= VM_ARM_SECTION_MAPPING;
		err = remap_area_supersections(addr, pfn, size, type);
	} else if (!((__pfn_to_phys(pfn) | size | addr) & ~PMD_MASK)) {
		area->flags |= VM_ARM_SECTION_MAPPING;
		err = remap_area_sections(addr, pfn, size, type);
	} else
#endif
		err = remap_area_pages(addr, pfn, size, type);

	if (err) {
 		vunmap((void *)addr);
 		return NULL;
 	}

	flush_cache_vmap(addr, addr + size);
	return (void __iomem *) (offset + addr);    /* 返回的地址是映射到内核空间的地址了 */
}

#define VMALLOC_OFFSET		(8*1024*1024)        /* 8M */
#define VMALLOC_START		(((unsigned long)high_memory + VMALLOC_OFFSET) & ~(VMALLOC_OFFSET-1))
#define VMALLOC_END	          (0xFC000000)


/* GFP_KERNEL这个标志位分配内存的一个选项,GFP_KERNEL是内核内存分配时最常用的,无内存可用时可引起休眠 */
#define GFP_KERNEL	(__GFP_WAIT | __GFP_IO | __GFP_FS)

struct vm_struct *get_vm_area_caller(unsigned long size, unsigned long flags,
				void *caller)
{
    /*  */
	return __get_vm_area_node(size, 1, flags, VMALLOC_START, VMALLOC_END,
						-1, GFP_KERNEL, caller);
}


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;

    /* #define VM_IOREMAP 0x00000001 /* ioremap() and friends */在前面定义传进来的 */
	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;

    /* 申请一个struct vm_struct空间,存放未使用空间信息初始化 */
	area = kzalloc_node(sizeof(*area), gfp_mask & GFP_RECLAIM_MASK, node);    

	if (unlikely(!area))
		return NULL;

	/*
	 * We always allocate a guard page. 每次都多申请一个保护页面
	 */
	size += PAGE_SIZE;

    /* start 和 end 分别为 VMALLOC_START 和 VMALLOC_END   align 为 1   
     * 已经使用的 vm 的信息分别存在各个 vmap_area 结构体中  
     * 所有的 vmap_area 结构体都在红黑树 vmap_area_root 中  
     * alloc_vmap_area 函数的主要功能是,查找红黑树 vmap_area_root ,找到 start 和 end 之间满足 size 大小的未使用空间,  
     */
	va = alloc_vmap_area(size, align, start, end, node, gfp_mask);
	if (IS_ERR(va)) {
		kfree(area);
		return NULL;
	}

    /* 将该结构体插入到红黑树  */
	insert_vmalloc_vm(area, va, flags, caller);
	return area;
}


/* 初始化该结构体,并将其插入到红黑树中 */
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);
}

主要做了下面几件重要的事:

1.参数检查,确定不是RAM,地址不要越界等

2.在VMALLOC_START到VMALLOC_END之间找到所需要大小的地址空间。

3.建立页表

 

iounmap

明显就是做一些,移除红黑树节点,释放内存空间之类的事情了。

 

说明:

在一个进程中,同一个(一组)寄存器可以多次ioremap,每次ioremap,都会为其建立新的页表,即也会有不同的虚拟地址。但这些虚拟地址都是映射的同一片物理地址,所以无论操纵那一个ioremap返回的虚拟地址,最终都操作能够的是那块物理地址。

展开阅读全文

没有更多推荐了,返回首页