一、struct vm_area_struct
内核中的每个vm_area_struct对象都表示用户进程地址空间的一段区域,它是访问用户进程中mmap地址空间的最小单元。
struct vm_area_struct {/*当前vm_area_struct对象所表示的虚拟地址段的的起始地址*/unsigned long vm_start;/*当前vm_area_struct对象所表示的虚拟地址段的的结束地址*/unsigned long vm_end;/*当前vm_area_struct对象所表示的虚拟地址段所归属的进程虚拟地址空间*/struct mm_struct *vm_mm;/*在将当前struct vm_area_struct对象所表示的虚拟地址段映射到设备内存时的页保护属性,主要体现在页目录(表)项的映射属性当中*/pgprot_t vm_page_prot;/*当前vm_area_struct对象所表示的虚拟地址段的访问属性,比如VM_READ,VM_WRITE,VM_EXEC,VM_SHARED*/unsigned long vm_flags;union {struct {struct rb_node rb;unsigned long rb_subtree_last;} shared;struct anon_vma_name *anon_name;};struct list_head anon_vma_chain;struct anon_vma *anon_vma;const struct vm_operations_struct *vm_ops;unsigned long vm_pgoff;struct file * vm_file; /* File we map to (can be NULL). */void * vm_private_data; /* was vm_pte (shared mem) */#ifdef CONFIG_SWAPatomic_long_t swap_readahead_info;
#endif
#ifndef CONFIG_MMUstruct vm_region *vm_region; /* NOMMU mapping region */
#endif
#ifdef CONFIG_NUMAstruct mempolicy *vm_policy; /* NUMA policy for the VMA */
#endifstruct vm_userfaultfd_ctx vm_userfaultfd_ctx;
} __randomize_layout;
二、用户空间MMAP区域布局的建立和映射管理
linux系统运行一个应用程序时系统调用exec()调用load_elf_binary()来将应用程序对应的elf二进制文件加载到进程的用户虚拟地址空间中,布局由此建立。
以x86_64为例,用户空间虚拟地址为0x0000000000000000 - 0x00007fffffffffff
其中MMAP区域的布局建立流程如下:
int load_elf_binary(struct linux_binprm *bprm)|--setup_new_exec(bprm);|--arch_pick_mmap_layout(me->mm, &bprm->rlim_stack);
Linux6.1/arch/x86/mm/mmap.c
这里需要注意其中的mm->get_unmapped_area会在系统调用时的get_unmapped_area()函数中用到。
static int mmap_is_legacy(void)
{/*如果current->personality设置了ADDR_COMPAT_LAYOUT位则采用传统布局*/if (current->personality & ADDR_COMPAT_LAYOUT)return 1;return sysctl_legacy_va_layout;
}void arch_pick_mmap_layout(struct mm_struct *mm, struct rlimit *rlim_stack)
{/*Linux内核为进程的虚拟地址空间提供了二种布局方案,它们的区别主要就是MMAP区域的扩展方向*/if (mmap_is_legacy())/*传统布局,用来获得用户进程空间MMAP区域尚未被映射的一段内存。该函数将从低地址向高地址方向分配空闲的vm_area_struct对象,每个对象代表一段连续的虚拟地址空间。这里暗含的概念是的对进程的用户虚拟地址空间中的MMAP区域的地址进行分配,目的是找到一块尚未被映射的区域。具体详细实现在下面的系统调用中描述。*/ mm->get_unmapped_area = arch_get_unmapped_area;else/*新式布局,topdown指示了在MMAP区域分配空闲vm_area_struct对象的方式为从高地址向低地址。*/ mm->get_unmapped_area = arch_get_unmapped_area_topdown;arch_pick_mmap_base(&mm->mmap_base, &mm->mmap_legacy_base,arch_rnd(mmap64_rnd_bits), task_size_64bit(0),rlim_stack);#ifdef CONFIG_HAVE_ARCH_COMPAT_MMAP_BASESarch_pick_mmap_base(&mm->mmap_compat_base, &mm->mmap_compat_legacy_base,arch_rnd(mmap32_rnd_bits), task_size_32bit(),rlim_stack);
#endif
}
Linux内核会管理好MMAP区域,管理该区域的最小单位是由vm_area_struct表示的对象,也就
是说此处管理的语义是分配和释放一个vm_area_struct对象。系统中一个实际进程的用户虚拟地址空间中的MMAP区域看起来可能如下所示:
因为响应应用程序mmap和munmap等系统调用的缘故,MMAP区域充斥了映射的区域和尚未被映射的空闲区域。每个区域由一个vm_area_struct对象表示。因此内核必须跟踪MMAP区域的分配情况(和vmalloc机制非常相似),并且能很好地处理从MMAP区域分配一个待映射的vm_area_struct对象或者释放一个被映射的vm_area_struct对象。
大体上,这种用户空间虚拟地址映射到设备内存的过程可以概括为:内核先在进程虚拟地址空间
的MMAP区域分配一个空闲的(未映射)的vm_area_struct对象,然后通过页目录表项的方式将vm_area_struct对象所代表的虚拟地址空间映射到设备的存储空间中。如此,用户进程可以直接访问设备的存储空间,从而提高系统的性能。页目录表项的介入也意味着每个vm_area_struct对象表示的地址空间应该页对齐的,大小是页的整数倍。
三、mmap系统调用过程
mmap用来将将用户进程虚拟地址空间布局中的MMAP区域的某段地址映射到设备物理内存(比如Framebuffer,当然Framebuffer也可能位于系统物理内存;PCIe设备的IO内存等)或系统物理内存,这样一来,用户空间将可以直接访问呢设备内存。在用户层的函数原型如下所示,如果一切正常,mmap()将返回已经被映射的MMAP区域中一段虚拟地址的起始地址,应用程序因此可以访问到对应的物理内存。
/*
addr表示映射区的起始地址(实际使用时若设为NULL则表示让系统在MMAP区域找到一个合适的空闲区域),
length是映射区的长度,
prot表示用户进程在映射区被映射时所期望的保护方式(PROT_READ/PROT_WRITE/PROT_EXEC等),
flags用户指定映射区的类型,
fd是当前正在操作的文件的描述符,
offset是实际数据在映射区中的偏移值。
*/
void *mmap(void *addr, size_t length, int prot, int flags,int fd, off_t offset);
当用户空间调用mmap时,Linux系统将通过系统调用进入内核,由当前设备文件中实现的mmap方法来完成用户程序所要求的映射。
SYSCALL_DEFINE6(mmap_pgoff, unsigned long, addr, unsigned long, len,unsigned long, prot, unsigned long, flags,unsigned long, fd, unsigned long, pgoff)|--ksys_mmap_pgoff(addr, len, prot, flags, fd, pgoff);|--vm_mmap_pgoff(file, addr, len, prot, flags, pgoff);|--do_mmap(file, addr, len, prot, flag, pgoff, &populate, &uf);/*得到未使用的虚拟地址,函数中会用到上面提到的mm->get_unmapped_area*/|--addr = get_unmapped_area(file, addr, len, pgoff, flags);/*完成实际的映射工作*/|--addr = mmap_region(file, addr, len, vm_flags, pgoff, uf);/*在MMAP区域得到一个空闲未被映射的vm_area_struct,由于mmap_region函数被调用时参数addr已经指向了一块空闲的待映射MMAP区域中的区域地址,所以需要首先调用kmem_cache_alloc来分配一个vm_area_struct对象,然后来对其进行初始化*/|--vma = vm_area_alloc(mm);|--vma = kmem_cache_alloc(vm_area_cachep, GFP_KERNEL);/*设置vm_area_struct*/|--vma->vm_start = addr;vma->vm_end = end;vma->vm_flags = vm_flags;vma->vm_page_prot = vm_get_page_prot(vm_flags);vma->vm_pgoff = pgoff;|--if (file) {.../*到目前为止内核在MMAP区域分配了一个空闲的vma对象,然后调用file对应的设备驱动中的mmap方法*/call_mmap(file, vma);/*我们只需要实现:1)提供物理地址2)设置属性:cache, buffer3)给vm_area_struct和物理地址建立映射*/|--file->f_op->mmap(file, vma);} else if (vm_flags & VM_SHARED) {shmem_zero_setup(vma);} else {vma_set_anonymous(vma);}
在上述的get_unmapped_area()函数内部,调用mm->get_umapped_area()或者filp->get_umapped_area()来实现空闲虚拟地址的分配(这二个回调函数主要作用都是在用户进程的虚拟地址空间中分配空闲的内存区域)。
unsigned long
get_unmapped_area(struct file *file, unsigned long addr, unsigned long len,unsigned long pgoff, unsigned long flags)
{unsigned long (*get_area)(struct file *, unsigned long,unsigned long, unsigned long, unsigned long);....../*用到mm->get_unmapped_area*/get_area = current->mm->get_unmapped_area;if (file) {/*file_operations中实现的get_unmapped_area函数*/if (file->f_op->get_unmapped_area)get_area = file->f_op->get_unmapped_area;} else if (flags & MAP_SHARED) {/** mmap_region() will call shmem_zero_setup() to create a file,* so use shmem's get_unmapped_area in case it can be huge.* do_mmap() will clear pgoff, so match alignment.*/pgoff = 0;get_area = shmem_get_unmapped_area;}addr = get_area(file, addr, len, pgoff, flags);......
}
如果驱动在其file_operations对象中没有定义get_unmapped_area方法,则利用当前进程mm对象中的get_unmapped_area函数来分配空闲的虚拟地址空间,否则使用驱动中实现的。由于现实驱动程序中很少去实现,所以接下来分析内核提供的标准分配函数;对于传统布局而言,内核提供的分配MMAP区域中空闲虚拟地址空间的标准函数如下:
unsigned long
generic_get_unmapped_area(struct file *filp, unsigned long addr,unsigned long len, unsigned long pgoff,unsigned long flags)
{struct mm_struct *mm = current->mm;struct vm_area_struct *vma, *prev;struct vm_unmapped_area_info info;const unsigned long mmap_end = arch_get_mmap_end(addr, len, flags);if (len > mmap_end - mmap_min_addr)return -ENOMEM;/*如果mmap()中flags参数指定了MAP_FIXED,表面映射将从参数中指定的addr起始地址处开始,因此这种情况下直接返回addr*/if (flags & MAP_FIXED)return addr;/*检查mmap()参数中有没有指定要优先映射的虚拟地址,如果有内核将检查addr和len所确定的待映射的虚拟地址空间是否与已经被映射的虚拟地址空间重叠,如果不重叠则直接返回addr*/if (addr) {addr = PAGE_ALIGN(addr);vma = find_vma_prev(mm, addr, &prev);if (mmap_end - len >= addr && addr >= mmap_min_addr &&(!vma || addr + len <= vm_start_gap(vma)) &&(!prev || addr >= vm_end_gap(prev)))return addr;}info.flags = 0;info.length = len;info.low_limit = mm->mmap_base;info.high_limit = mmap_end;info.align_mask = 0;info.align_offset = 0;/*如果调用者没有优先指定一个需要映射的addr(mmap()的addr参数传NULL的情况),那么就遍历进程用户空间中所有MMAP的可用区域,设法找到一个合适大小的空闲区域。*/return vm_unmapped_area(&info);
}#ifndef HAVE_ARCH_UNMAPPED_AREA
/*未定义上面这个宏时,调用这个函数来在用户进程的MMAP区域分配尚未分配的内存块*/
unsigned long
arch_get_unmapped_area(struct file *filp, unsigned long addr,unsigned long len, unsigned long pgoff,unsigned long flags)
{return generic_get_unmapped_area(filp, addr, len, pgoff, flags);
}
#endif
四、驱动中的mmap
从上面的系统调用流程可以看出内核为即将进行的内存映射准备了vm_area_struct对象,并且调用了设备驱动程序中的file_operations中的mmap方法。
/*
主要功能是将内核提档的用户进程空间中来自MMAP区域的一段内存(内核将这段需要映射的内存
以vm_area_struct对象作为参数告诉设备驱动程序)映射到设备内存上。
int (*mmap) (struct file *, struct vm_area_struct *);
在驱动程序中,需要完成页目录项的配置以便将vma对象代表的用户空间地址映射到设备物理内存和系统物理内存中。实现方式可以有多种,比如:
可以在驱动中注册vm_ops的的fault函数,在fault函数中完成映射,这样当在用户mmap后来动态分配的内存是虚拟内存,并没有获得物理内存;当用户访问这段空间时就会触发缺页中断,然后在该中断中调用fault来完成映射,此时才真正获得物理内存;
当然也可以直接使用内核中提供的操作页目录表项以建立页面映射的一些接口函数,比如remap_pfn_range()和io_remap_pfn_range()函数。
1)remap_pfn_range
/*1)将参数addr起始的大小为size的虚拟地址空间映射到pfn(页框号,page fram number)表示的一组
连续的物理页面上,在页面大小为4KB的系统中,一个物理地址右移12位就可以得到对应的pfn。简而言之,
该函数为[addr, addr+size]的虚拟地址建立页目录表项,将其映射到pfn开始的物理页上。2)通常若将用户空间的地址映射到设备物理内存上,尤其是设备的寄存器所在的地址空间,都不希望cache
机制发挥作用,驱动程序可以通过最后一个参数prot来影响页表项中属性位的建立,比如使
用pgprot_noncached()。
*/
int remap_pfn_range(struct vm_area_struct *vma, unsigned long addr,unsigned long pfn, unsigned long size, pgprot_t prot)
{int err;err = track_pfn_remap(vma, &prot, pfn, addr, PAGE_ALIGN(size));if (err)return -EINVAL;err = remap_pfn_range_notrack(vma, addr, pfn, size, prot);if (err)untrack_pfn(vma, pfn, PAGE_ALIGN(size));return err;
}
其中的remap_pfn_range_notrack:
int remap_pfn_range_notrack(struct vm_area_struct *vma, unsigned long addr,unsigned long pfn, unsigned long size, pgprot_t prot)
{pgd_t *pgd;unsigned long next;/*将addr+size调整到下一个页面的边界处,因为内存映射的最小单位是页*/unsigned long end = addr + PAGE_ALIGN(size);struct mm_struct *mm = vma->vm_mm;int err;if (WARN_ON_ONCE(!PAGE_ALIGNED(addr)))return -EINVAL;/*copy on write*/if (is_cow_mapping(vma->vm_flags)) {if (addr != vma->vm_start || end != vma->vm_end)return -EINVAL;vma->vm_pgoff = pfn;}vma->vm_flags |= VM_IO | VM_PFNMAP | VM_DONTEXPAND | VM_DONTDUMP;BUG_ON(addr >= end);pfn -= addr >> PAGE_SHIFT;/*获得某一虚拟地址在页目录表中的对应单元的地址pgd*/pgd = pgd_offset(mm, addr);/*体系结构相关的,用来将(addr, end)地址范围对应的cache内容同步到主存*/flush_cache_range(vma, addr, end);/*该循环用来在页目录中建立对应的映射页表项*/do {/*用来获取addr对应页表项的下一个entry对应的虚拟起始地址*/next = pgd_addr_end(addr, end);/*用来做实际的页目录表项的操作*/err = remap_p4d_range(mm, pgd, addr, next,pfn + (addr >> PAGE_SHIFT), prot);if (err)return err;} while (pgd++, addr = next, addr != end);return 0;
}
其中remap_p4d_range如下所示,目前Linux 4.11版本之后的内核统一采用pgd、p4d、pud、pmd和pte五级映射,当然具体的使用几级依据体系结构而定,比如若使用四级则p4d=pud
static inline int remap_p4d_range(struct mm_struct *mm, pgd_t *pgd,unsigned long addr, unsigned long end,unsigned long pfn, pgprot_t prot)|-- remap_pud_range(mm, p4d, addr, next,pfn + (addr >> PAGE_SHIFT), prot);|--remap_pmd_range(mm, pud, addr, next,pfn + (addr >> PAGE_SHIFT), prot);|--remap_pte_range(mm, pmd, addr, next,pfn + (addr >> PAGE_SHIFT), prot);
2)io_remap_pfn_range
由于映射的核心是MMU,而对MMU来说无需区分映射的目标物理地址类型。因此io_remap_pfn_range和remap_pfn_range的内部实现差别很小。
但是从代码易读性角度,一般如果映射的目标是在系统DDR或者PCIe设备的MM空间,使用remap_pfn_range,如果映射的目标地址在设备的I/O空间,则使用io_remap_pfn_range
io_remap_pfn_range和ioremap的区别就是前者将设备的I/O空间映射到用户空间,而后者及那个设备的I/O空间映射到内核空间。
五、munmap
/*addr为mmap函数返回的地址,即虚拟地址段的起始地址*/
int munmap(void *addr, size_t length);
内核会根据虚拟地址找到对应的页目录项和页表项,然后清除这些表项的内核,从而撤销掉mmap建立的虚拟地址到物理地址的映射关系。其系统调用中的核心是do_munmap()。
int do_munmap(struct mm_struct *mm, unsigned long start, size_t len,struct list_head *uf)|--do_mas_munmap(&mas, mm, start, len, uf, false);|--do_mas_align_munmap(mas, vma, mm, start, end, uf, downgrade);|--unmap_region(mm, &mt_detach, vma, prev, next, start, end);|--free_pgtables(&tlb, mt, vma, prev ? prev->vm_end : FIRST_USER_ADDRESS,next ? next->vm_start : USER_PGTABLES_CEILING);|--free_pgd_range(tlb, addr, vma->vm_end,floor, next ? next->vm_start : ceiling);|--free_p4d_range(tlb, pgd, addr, next, floor, ceiling);|--free_pud_range(tlb, p4d, addr, next, floor, ceiling);|--free_pmd_range(tlb, pud, addr, next, floor, ceiling);|--free_pte_range(tlb, pmd, addr);
驱动程序并不需要为其提供任何支持。
六、一些使用注意事项
1)malloc的底层调用了mmap
在Linux中,malloc()的底层调用了mmap()。mmap函数是以页为单位获取内存的,而malloc函数是以字节为单位获取内存的。为了以字节为单位获取内存,glibc事先通过系统调用mmap()向内核请求一块大的内存区域作为内存池,当程序调用malloc时,从内存池中根据申请量划分出相应字节大小的内存并返回给程序。当内存池中的内存消耗完后,glibc会再次调用mmap()以申请新的内存区域。
需要注意即使Python这类将内存管理代码隐藏起来的脚本语言,其项目底层代码依然是通过C语言的malloc函数或mmap函数来获取内存的(可以通过strace命令追踪Python脚本的运行查看)。
2)mmap匿名映射(fd传-1, flag加MAP_ANONYMOUS)
void *new_memory;
mew_memory = mmap(NULL, ALLOC_SIZE, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANNOYMOUS, -1, 0);
if(new_memory == (void *)-1)err(EXIT_FAILURE, "mmap() failed");
其中:offset参数为被映射对象内容的起点,一般设为0,表示从文件头开始映射。ALLOC_SIZE是映射到调用进程地址空间的字节数,它从被映射文件开头offset个字节开始算起。比如使用上面示例可以分配到ALLOC_SIZE大小的内存。
执行如上对于的testcase后,cat /proc/pid/maps后可以发现heap中多出了:
7fabdc50e000-7fabe290e000 rw-p 00000000 00:00 0
3)文件映射
对于普通的文件,mmap()可以将文件的内容从外部存储器读到内存中,然后把这个内存区域映射到进程虚拟地址空间某区域中。这样就可以按照访问内存的方式来访问被映射的文件了。被访问的区域会在规定的时间内写入外部存储器上的文件(由此可见文件映射运用了Linux虚拟内存机制)。在进程中open和mmap文件后也会在/proc/pid/maps中看到。
4)标准大页
使用这种页面能有效减少进程页表自身所需的内存量,提升系统性能,比如提高fork()的执行速度。使用时可以在mmap()函数的flags参数赋MAP_HUGETLB标志,可以获取标准大页。
注意,Linux还有一个透明大页机制。当虚拟内存地址空间有连续多个4KB的页面符合特定条件时,透明大页机制开启后能将它们自动换成一个大页。这也有问题比如重新拆分为4K页时就会引起性能下降,一般都禁止掉。其中madvise表示仅对madvise()系统调用设定的内存区域启用透明大页机制。
相关reference:
linux6.1/drivers/staging/media/deprecated/cpia2/cpia2_v4l.c中的cpia2_mmap()实现
https://blog.csdn.net/maybeYoc/article/details/123414705
https://bbs.csdn.net/topics/390699538
上一篇:数据挖掘(作业汇总)
下一篇:RHCSA-磁盘分区(3.16)