内存之ioremap内存映射
1、概述
Linux内核驱动模块中常常使用ioremap对Soc/外设寄存器进行映射,以获取内核态虚拟地址。ioremap只有两个入参,一个是起始地址,一个是所要映射长度;相信很多驱动开发者对此都比较熟悉。接口使用上大家应该都不存在问题,但接口选择和接口实现上是否也比较清楚呢?若是对上面的问题存有疑虑,请往下看。
Note:本文是基于arm64架构平台进行的展开。
2、接口类型
在内核源码arch/arm64/include/asm/io.h中有如下几种ioremap接口
| **ioremap(addr, size)** |
| --------------------------- |
| ioremap_nocache(addr, size) |
| ioremap_wc(addr, size) |
| ioremap_wt(addr, size) |
| ioremap_cache(addr, size) |
| |
从接口命名中大致可以区分开各自的功能,但为了真正明白还是要看下具体属性定义。
ioremap:
__ioremap((addr), (size), __pgprot(PROT_DEVICE_nGnRE))
ioremap_nocache:
__ioremap((addr), (size), __pgprot(PROT_DEVICE_nGnRE))
ioremap_wc:
__ioremap((addr), (size), __pgprot(PROT_NORMAL_NC))
ioremap_wt:
__ioremap((addr), (size), __pgprot(PROT_DEVICE_nGnRE))
ioremap_cache:
从上面的接口定义看,除了addr和size外还有一个pgprot(X)参数,上面接口中X有PROT_DEVICE_nGnRE和PROT_NORMAL_NC两种类型。实际在pgtable-prot.h中还有PROT_DEVICE_nGnRnE、PROT_NORMAL_WT和PROT_NORMAL。ioremap_接口的功能属性都是由pgprot(X)决定的,那X具体是什么含义呢?
2.1、内存属性
在内核源码memory.h中有定义ioremap接口中可用的内存类型:
/*
\* 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
各字段具体含义可参照ARMv8手册中对内存属性的描述,内存可以分为DEVICE和NORMAL两大类型,Device memory依据是否可合并、无序分为:
Normal memory依据是否可共享、是否可cache分为下面几类
从上面的描述可见,只有Normal memory具备共享和cache属性。
2.2、ioremap涉及到的页表属性
上面ioremap_接口中用到的属性参数主要是PROT_DEVICE_nGnRE和PROT_NORMAL_NC;下面来看下这两个prot_val分别代表什么含义。内容摘自pgtable-prot.h
\#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))
Bit字段拆解
具体每个bit的含义,我们在内存之页表章节中有描述,此处不再重复。
3、接口实现
3.1、整体结构
从流程结构上看ioremap的实现主要有两步:
1、 获取一段合适的虚拟地址空间,使用get_vm_area_caller接口;
2、 进行物理地址和虚拟地址的映射,使用ioremap_page_range接口;
虽然看似只有两步就完成了整体功能,但在每一步的里面又包含了许多的设计与接口调用。另外,ioremap接口的内部设计都是按照页对齐来设计的,因此在上图中第一步之前有一些对phys_addr和size进行页对齐的操作,此处不做详细描述。
Ioremap接口映射的虚拟地址处在VMALLOC区,这点是由接口设计实现决定的;在使用类似usb设备的dma功能时要注意地址类型,因为有些设计会判断地址若是处在vmalloc_start~vmalloc_end则判定其没有连续的物理地址而直接异常退出。个人理解,以连续的物理地址进行ioremap接口进行映射,所得到的结果是虚拟连续、物理也连续;但并不通用,不能从任何字段或标识上判定地址的映射方式,因此在dma相关使用中将其排除在外是合情合理的。
get_vm_area_caller前后涉及两个数据结构vm_struct和vmap_area;在最外层联通上下文的是vm_struct因为其是内核态虚拟地址管理的数据结构,而vmap_area仅是vmalloc区内存申请分配的管理单元。该接口的核心功能是找到一个还未使过用的大小合适hole。
Ioremap_page_range是将已确定的虚拟地址和物理地址进行页表的映射,这和我们在业务程序中使用malloc申请内存页表建立过程不同(malloc流程是内核先提供一段虚拟地址,在用户实际读写时通过page_fault触发为其分配真正的物理内存,建立页表映射)。使用的平台内核配置为39bit虚拟地址、三级页表。
3.2、核心接口实现
下面分别仔细看下两个核心接口get_vm_area_caller和ioremap_page_range的具体实现。
3.2.1、get_vm_area_caller
get_vm_area_caller的大体流程结构如下图所示:
此处涉及两个主要数据结构:struct vm_struct和struct vmap_area。前文也有提到vm_struct是内核态用来管理虚拟地址的;vmap_area是内核态管理vmalloc内存区虚拟地址的。
struct vm_struct的成员如下:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-0a9dH1N1-1650286768125)(file:///C:/Users/gaoyuke/AppData/Local/Temp/msohtmlclip1/01/clip_image002.jpg)]
strcut vmap_area的成员如下:
在设备启动过程中一般都会打印出当前设备内核虚拟内存地址的分布情况,下面是nt98336平台的数据:
kernel space 中常用的内存分配接口kmalloc、kzalloc、get_free_pages获取的地址位于memory区;vmalloc接口申请的地址位于vmalloc区,其获取的地址范围位于:VMALLOC_START~VMALLOC_END。
在整个get_vm_area_caller接口中vmap_area是核心,还是先看下大概流程再研究细节。从下图看到整个流程由几个分支构成:
1、 当struct rb_node free_vmap_cache不为空时,获取该节点上vmap_area first;
2、 当free_vmap_cache为空时,获取满足条件的的vmap_area first;
3、 遍历以first为索引的list上记录的vmap_area 节点,寻找满足条件的hole;
4、 Found时将对应的va进行插入操作并返回对应的vmap_area 地址;
5、 历经上述步骤没有满足条件的地址时,执行lazy_area区域内存的释放,再次从头来过;
各分支代码流程
------------------------------------------------------------------------
1、free_vmap_cache不为空时
if (free_vmap_cache)
{
/*以rb_node为成员,获取free_vmap_cache中的vmap_area*/
first = rb_entry(free_vmap_cache, struct vmap_area, rb_node);
addr = ALIGN(first->va_end, align);
if (addr < vstart)
goto nocache; //free_vmap_cache中记录的地址不满足当前需求
if (addr + size < addr)
goto overflow;
}
2、free_vmap_cache为空时
addr = ALIGN(vstart, align); /*其实就是VMALLOC_START*/
if (addr + size < addr)
goto overflow;
n = vmap_area_root.rb_node; /*vmap_area rb_root根节点*/
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; /*tmp变量结束地址大于addr时 取左节点*/
}
else
n = n->rb_right; /*相反取右值*/
}
if (!first) /*若rb_root为空,说明从未有内存分配过,直接使用vmalloc_start地址*/
goto found;
测试数据如下:
{
PS:
从上图应该可以管中窥豹大致看清虚拟内存分配的规则,从对应的区域由小到大逐次使用,当然里面是有空洞的,应有地址对齐和大小是否满足的需要
}
1、 从找到的struct vmap_area *first对应的链表上找满足条件的hole
while (addr + size > first->va_start && addr + size <= vend)
{
if (addr + cached_hole_size < first->va_start)
cached_hole_size = first->va_start - addr;
addr = ALIGN(first->va_end, align);
if (addr + size < addr)
goto overflow;
if (list_is_last(&first->list, &vmap_area_list))
goto found; /*取该链表上最后一个元素*/
first = list_next_entry(first, list);
}
2、 found流程
if (addr + size > vend || addr < vstart)
goto overflow;
va->va_start = addr; /*使用找到的虚拟地址填充va*/
va->va_end = addr + size;
va->flags = 0;
/*将va->rb_node添加到vmap_area_root根节点,将va->list添加到list中*/
__insert_vmap_area(va);
/*以最新使用的va对free_vmap_cache进行赋值,提高系统中同样请求的处理效率*/
free_vmap_cache = &va->rb_node;
spin_unlock(&vmap_area_lock);
return va;
3、 lazy area释放
spin_unlock(&vmap_area_lock);
if (!purged)
{
purge_vmap_area_lazy(); /*主要是处理vmap_purge_list中的要素*/
purged = 1;
goto retry;
}
if (gfpflags_allow_blocking(gfp_mask))
{
unsigned long freed = 0;
blocking_notifier_call_chain(&vmap_notify_list, 0, &freed);
if (freed > 0)
{
purged = 0;
goto retry;
}
}
if (!(gfp_mask & __GFP_NOWARN) && printk_ratelimit())
pr_warn("vmap allocation for size %lu failed: use vmalloc=<size> to increase size\n",
size);
kfree(va);
return ERR_PTR(-EBUSY);
虚拟地址的寻找流程大致如此,insert和purge的流程并未展开分析,感兴趣的读者可以再剖析下。
3.2、vm_struct 与 vmap_area关系的建立
setup_vmalloc_vm(area, va, flags, caller);
static void setup_vmalloc_vm(struct vm_struct *vm, struct vmap_area *va,
unsigned long flags, const void *caller)
{
spin_lock(&vmap_area_lock);
vm->flags = flags;
vm->addr = (void *)va->va_start;
vm->size = va->va_end - va->va_start;
vm->caller = caller;
va->vm = vm;
va->flags |= VM_VM_AREA;
spin_unlock(&vmap_area_lock);
}
3.3、页表映射
只有将虚拟地址和物理地址进行了映射后,CPU才能有效使用获取的地址进行一系列操作。映射关系的建立使用ioremap_page_range接口。
从图中看映射流程貌似使用了五级页表:pgd—>p4d—>pud—>pmd—>pte,实际上只有三层(pgd(p4d/pud)->pmd->pte),在内核config配置中可找到明确参数;代码中如此设计仅是为了兼容。
该接口的入参有:addr(分配得到的虚拟地址的起始)、addr + size(分配得到的虚拟地址的结尾)、phys_addr(物理地址的起始)和prot(属性);个人理解页表映射一是将虚拟地址和物理地址建立起一定的对应关系;二是将预设的属性信息记录起来。
下面一点一点的来看具体的操作流程:
int ioremap_page_range(unsigned long addr,
unsigned long end, phys_addr_t phys_addr, pgprot_t prot)
{
pgd_t *pgd;
unsigned long start;
unsigned long next;
int err;
might_sleep();
BUG_ON(addr >= end);
start = addr;
phys_addr -= addr;
pgd = pgd_offset_k(addr); /*查找内核空间addr对应的页目录项pgd条目,具体定义见附录1*/
do {
next = pgd_addr_end(addr, end); /*获取addr对应的下一个边界,具体定义见附录2*/
err = ioremap_p4d_range(pgd, addr, next, phys_addr+addr, prot); /**/
if (err)
break;
} while (pgd++, addr = next, addr != end); /*do…while仅执行一次*/
flush_cache_vmap(start, end); /*空函数,aarch64不需要,PIPT or VIPT non-aliasing D-cache*/
return err;
}
static inline int ioremap_p4d_range(pgd_t *pgd, unsigned long addr,
unsigned long end, phys_addr_t phys_addr, pgprot_t prot)
{
p4d_t *p4d;
unsigned long next;
phys_addr -= addr;
p4d = p4d_alloc(&init_mm, pgd, addr);
if (!p4d)
return -ENOMEM;
do {
next = p4d_addr_end(addr, end); /*获取addr对应的下一个边界,具体定义见附录3*/
if (ioremap_p4d_enabled() &&
((next - addr) == P4D_SIZE) &&
IS_ALIGNED(phys_addr + addr, P4D_SIZE)) {
if (p4d_set_huge(p4d, phys_addr + addr, prot)) /*空函数*/
continue;
}
if (ioremap_pud_range(p4d, addr, next, phys_addr + addr, prot))
return -ENOMEM;
} while (p4d++, addr = next, addr != end);
return 0;
}
static inline int ioremap_pud_range(p4d_t *p4d, unsigned long addr,
unsigned long end, phys_addr_t phys_addr, pgprot_t prot)
{
pud_t *pud;
unsigned long next;
phys_addr -= addr;
pud = pud_alloc(&init_mm, p4d, addr);
if (!pud)
return -ENOMEM;
do {
next = pud_addr_end(addr, end);
if (ioremap_pud_enabled() &&
((next - addr) == PUD_SIZE) &&
IS_ALIGNED(phys_addr + addr, PUD_SIZE) &&
pud_free_pmd_page(pud, addr)) {
if (pud_set_huge(pud, phys_addr + addr, prot))
continue;
}
if (ioremap_pmd_range(pud, addr, next, phys_addr + addr, prot))
return -ENOMEM;
} while (pud++, addr = next, addr != end);
return 0;
}
static inline int ioremap_pmd_range(pud_t *pud, unsigned long addr,
unsigned long end, phys_addr_t phys_addr, pgprot_t prot)
{
pmd_t *pmd;
unsigned long next;
phys_addr -= addr;
pmd = pmd_alloc(&init_mm, pud, addr);
if (!pmd)
return -ENOMEM;
do {
next = pmd_addr_end(addr, end);
if (ioremap_pmd_enabled() &&
((next - addr) == PMD_SIZE) &&
IS_ALIGNED(phys_addr + addr, PMD_SIZE) &&
pmd_free_pte_page(pmd, addr))
{
if (pmd_set_huge(pmd, phys_addr + addr, prot))
continue;
}
if (ioremap_pte_range(pmd, addr, next, phys_addr + addr, prot))
return -ENOMEM;
} while (pmd++, addr = next, addr != end);
return 0;
}
static int ioremap_pte_range(pmd_t *pmd, unsigned long addr,
unsigned long end, phys_addr_t phys_addr, pgprot_t prot)
{
pte_t *pte;
u64 pfn;
pfn = phys_addr >> PAGE_SHIFT; /*获取物理地址对应的页帧号*/
pte = pte_alloc_kernel(pmd, addr); /*分配内存,也可能是查找已分配过的内存地址*/
if (!pte)
return -ENOMEM;
do {
BUG_ON(!pte_none(*pte));
set_pte_at(&init_mm, addr, pte, pfn_pte(pfn, prot)); /*对pte内容赋值*/
pfn++;
} while (pte++, addr += PAGE_SIZE, addr != end);
return 0;
}
static inline void set_pte_at(struct mm_struct *mm, unsigned long addr,
pte_t *ptep, pte_t pte)
{
pte_t old_pte;
/*判断pte是否在内存中、用户态可执行、special?*/
if (pte_present(pte) && pte_user_exec(pte) && !pte_special(pte))
__sync_icache_dcache(pte);
/*
\* If the existing pte is valid, check for potential race with
\* hardware updates of the pte (ptep_set_access_flags safely changes
\* valid ptes without going through an invalid entry).
*/
old_pte = READ_ONCE(*ptep);
if (IS_ENABLED(CONFIG_DEBUG_VM) && pte_valid(old_pte) && pte_valid(pte) &&
(mm == current->active_mm || atomic_read(&mm->mm_users) > 1)) {
VM_WARN_ONCE(!pte_young(pte),
"%s: racy access flag clearing: 0x%016llx -> 0x%016llx",
__func__, pte_val(old_pte), pte_val(pte));
VM_WARN_ONCE(pte_write(old_pte) && !pte_dirty(pte),
"%s: racy dirty state clearing: 0x%016llx -> 0x%016llx",
__func__, pte_val(old_pte), pte_val(pte));
}
set_pte(ptep, pte);
}
static inline void set_pte(pte_t *ptep, pte_t pte)
{
WRITE_ONCE(*ptep, pte);
/*
\* Only if the new pte is valid and kernel, otherwise TLB maintenance
\* or update_mmu_cache() have the necessary barriers.
*/
if (pte_valid_not_user(pte)) {
dsb(ishst);
isb();
}
}
附录1
/* to find an entry in a page-table-directory */
\#define pgd_index(addr) (((addr) >> PGDIR_SHIFT) & (PTRS_PER_PGD - 1))
\#define pgd_offset_raw(pgd, addr) ((pgd) + pgd_index(addr))
\#define pgd_offset(mm, addr) (pgd_offset_raw((mm)->pgd, (addr)))
/* to find an entry in a kernel page-table-directory */
\#define pgd_offset_k(addr) pgd_offset(&init_mm, addr)
附录2
\#define pgd_addr_end(addr, end) \
({ unsigned long __boundary = ((addr) + PGDIR_SIZE) & PGDIR_MASK; \
(__boundary - 1 < (end) - 1)? __boundary: (end); \
alid_not_user(pte)) {
dsb(ishst);
isb();
}
}