一:vmalloc
https://www.cnblogs.com/arnoldlu/p/8251333.html
vmalloc创建内核空间的连续的虚拟地址的内存块。(主要是在vmalloc区域找到合适的hole,然后逐页分配内存从屋里上填充hole)特点:可能连续,虚拟地址连续,物理地址不连续,size页对齐(不适合小内存分配)。
struct vm_struct(vmalloc描述符)和struct vmap_area(记录在vmap_area_root中的vmalooc分配情况和vmap_area_list列表中)。
struct vm_struct {
struct vm_struct *next;----------下一个vm。
void *addr;--------------指向第一个内存单元虚拟地址
unsigned long size;----------该内存区对应的大小
unsigned long flags;---------vm标志位,如下。
struct page **pages;---------指向页面没描述符的指针数组
unsigned int nr_pages;-------vmalloc映射的page数目
phys_addr_t phys_addr;-------用来映射硬件设备的IO共享内存,其他情况下为0
const void *caller;----------调用vmalloc类函数的返回地址
};
VMALLOC_START和VMALLOC_END是vmalloc中重要的宏,在arch/arm/include/pgtable.h头文件中它是在High_memory制定的高端内存开始地址加上8mb的安全区域内,vmalloc的内存范围是在0xf0000000~0xff000000大小为240M的区域内,
vmap_area表示内核空间的vmalloc区域的一个vmalloc,由rb_node和list进行串联。
struct vmap_area {
unsigned long va_start;--------------malloc区的起始地址
unsigned long va_end;----------------malloc区的结束地址
unsigned long flags;-----------------类型标识
struct rb_node rb_node; /* address sorted rbtree */----按地址的红黑树
struct list_head list; /* address sorted list */------按地址的列表
struct list_head purge_list; /* "lazy purge" list */
struct vm_struct *vm;------------------------------------------指向配对的vm_struct
struct rcu_head rcu_head;
};
执行函数vmalloc->_vmalloc_node_range->_get_vm_area_node(找到符合要求的空闲vmalloc区域的hole,分配页面,并且创建页表映射关系)。
__vmalloc_node_range----------------vmalloc的核心函数
__get_vm_area_node--------------找到符合大小的空闲vmalloc区域
alloc_vmap_area-------------从vmap_area_root中找到合适的hole,填充vmap_area结构体,并插入到vmap_area_root红黑树中
setup_vmalloc_vm------------将vmap_area的参数填入vm_struct
__vmalloc_area_node-------------计算需要的页面数,分配页面,并创建页表映射关系
alloc_page------------------分配页面
map_vm_area-----------------建立PGD/PTE页表映射关系
map_vm_area对分配的页面进行了映射,map_vm_area-->vmap_page_range-->vmap_page_range_noflush。
二:vma操作
用户空间拥有3gb的空间,我们如何管理这些虚拟地址空间,用户进程多次调用malloc,mmap接口文件文件来进行读写操作,,这些操作要求在虚拟地址空间中分配内存块,内存在物理上是离散的,
进程地址空间使用struct vm_area_struct的数据结构来描述,简称VMA,被称为进程地址空间或者进程线性区域。
struct vm_area_struct {
unsigned long vm_start; /* Our start address within vm_mm. */--------VMA在进程地址空间的起始结束地址
unsigned long vm_end; /* The first byte after our end address
within vm_mm. */
/* linked list of VM areas per task, sorted by address */
struct vm_area_struct *vm_next, *vm_prev;----------------------------------VMA链表的前后成员
struct rb_node vm_rb;------------------------------------------------------VMA作为一个节点加入到红黑树中,每个进程的mm_struct中都有一个红黑树mm->mm_rb。
unsigned long rb_subtree_gap;
/* Second cache line starts here. */
struct mm_struct *vm_mm; /* The address space we belong to. */--------指向VMA所属进程的struct mm_struct结构。
pgprot_t vm_page_prot; /* Access permissions of this VMA. */------VMA访问权限
unsigned long vm_flags; /* Flags, see mm.h. */--------------------VMA标志位
struct {
struct rb_node rb;
unsigned long rb_subtree_last;
} shared;
struct list_head anon_vma_chain; /* Serialized by mmap_sem &-----------用于管理RMAP反向映射。
* page_table_lock */
struct anon_vma *anon_vma; /* Serialized by page_table_lock */------用于管理RMAP反向映射。
/* Function pointers to deal with this struct. */
const struct vm_operations_struct *vm_ops;-----------------------------VMA操作函数合集,常用于文件映射。
/* Information about our backing store: */
unsigned long vm_pgoff; /* Offset (within vm_file) in PAGE_SIZE-指定文件映射的偏移量,单位是页面。
units, *not* PAGE_CACHE_SIZE */
struct file * vm_file; /* File we map to (can be NULL). */------描述一个被映射的文件。
void * vm_private_data; /* was vm_pte (shared mem) */
#ifndef CONFIG_MMU
struct vm_region *vm_region; /* NOMMU mapping region */
#endif
#ifdef CONFIG_NUMA
struct mempolicy *vm_policy; /* NUMA policy for the VMA */
#endif
}
struct mm_struct {
struct vm_area_struct *mmap; /* list of VMAs */-----单链表,按起始地址递增的方式插入,所有的VMA都连接到此链表中。链表头是mm_struct->mmap。
struct rb_root mm_rb;--------------------------------------所有的VMA按照地址插入mm_struct->mm_rb红黑树中,mm_struct->mm_rb是根节点,每个进程都有一个红黑树。
...
}
2.1:查找vma
find_vma()通过虚拟地址查找vma
调用:vma = vmacache_find(mm, addr);内核中查找vma的优化方法,里面存放一个最近访问过的vma数组vmaacache[VMACACHE_SIZE]可以存放四个最近使用的vma,还没找到,就遍历用户进程的mm_rb红黑树,该红黑树存放进程所有的VMA
insert_vm_struct()插入vma的核心函数。想vma链表的红黑树插入一个新的vma,
vma_merge()合并vma
三:malloc
先说malloc的调用流程:
malloc->GLibC(用户空间)->brk(内核空间,新边界为brk)->find_vma_intersection(查找是否存在vma)->get_umapped_area(判断是否有足够的空间)->vma_merge(判断是否可以合并附近的vma)->分配一个新的vma->吧新的vma插入mm系统中(mm_populate->_M,ock_vma_pages_rangs->_get_user_pages->find_extend_vma->follow_page_mask(page页面分配映射))->返回新的brk边界。
没有初始化的内存分配:当使用内存时,CPU去查询页表,发现页表为空,cpu触发缺页中断,然后在缺页中断中一页一页的分配,然后虚拟地址空间建立映射关系。
分出初始化的内存,需要的虚拟内存都已近分配了物理内存并且建立了页表映射。
系统调用接口brk()(mm/mmap.c)。
内核空间为用户空间划分3GB的虚拟空间,用户空间有可执行的代码段和数据段组成,用户空间是从3gb虚拟空间的顶部开始,由顶部向下延伸,二brk分配的空间是由end_data到用户栈的底部,所以动态分配空间是从end_data开始,没分配一次空间,就把边界往上推,内核和进程都会记录当前的位置。
struct mm_struct {
...
unsigned long start_code, end_code, start_data, end_data;-----代码段从start_code到end_code;数据段从start_code到end_code。
unsigned long start_brk, brk, start_stack;--------------------堆从start_brk开始,brk表示堆的结束地址;栈从start_stack开始。
unsigned long arg_start, arg_end, env_start, env_end;---------表示参数列表和环境变量的起始和结束地址,这两个区域都位于栈的最高区域。
...
}
malloc是libc实现的接口,主要通过sys_brk这个系统调用分配内存。 调用SYSCALL_DEFINE1->do_brk(判断虚拟地址是否足够,然后查找VMA的插入点,并判断是否能够进行VMA合并,如果找不到VMA插入点,就创建一个VMA,并且更新到mm->mmap中去)。
static unsigned long do_brk(unsigned long addr, unsigned long len)
{
struct mm_struct *mm = current->mm;
struct vm_area_struct *vma, *prev;
unsigned long flags;
struct rb_node **rb_link, *rb_parent;
pgoff_t pgoff = addr >> PAGE_SHIFT;
int error;
len = PAGE_ALIGN(len);
if (!len)
return addr;
flags = VM_DATA_DEFAULT_FLAGS | VM_ACCOUNT | mm->def_flags;
error = get_unmapped_area(NULL, addr, len, 0, MAP_FIXED);---------------------------判断虚拟地址空间是否有足够的空间,这部分代码是跟体系结构紧耦合的。
if (error & ~PAGE_MASK)
return error;
error = mlock_future_check(mm, mm->def_flags, len);
if (error)
return error;
/*
* mm->mmap_sem is required to protect against another thread
* changing the mappings in case we sleep.
*/
verify_mm_writelocked(mm);
munmap_back:
if (find_vma_links(mm, addr, addr + len, &prev, &rb_link, &rb_parent)) {----------循环遍历用户进程红黑树中的VMA,然后根据addr来查找合适的插入点
if (do_munmap(mm, addr, len))
return -ENOMEM;
goto munmap_back;
}
/* Check against address space limits *after* clearing old maps... */
if (!may_expand_vm(mm, len >> PAGE_SHIFT))
return -ENOMEM;
if (mm->map_count > sysctl_max_map_count)
return -ENOMEM;
if (security_vm_enough_memory_mm(mm, len >> PAGE_SHIFT))
return -ENOMEM;
/* Can we just expand an old private anonymous mapping? */
vma = vma_merge(mm, prev, addr, addr + len, flags,------------------------------去找有没有可能合并addr附近的VMA。
NULL, NULL, pgoff, NULL);
if (vma)
goto out;
/*
* create a vma struct for an anonymous mapping
*/
vma = kmem_cache_zalloc(vm_area_cachep, GFP_KERNEL);----------------------------如果没办法合并,只能新创建一个VMA,VMA地址空间是[addr, addr+len]。
if (!vma) {
vm_unacct_memory(len >> PAGE_SHIFT);
return -ENOMEM;
}
INIT_LIST_HEAD(&vma->anon_vma_chain);
vma->vm_mm = mm;
vma->vm_start = addr;
vma->vm_end = addr + len;
vma->vm_pgoff = pgoff;
vma->vm_flags = flags;
vma->vm_page_prot = vm_get_page_prot(flags);
vma_link(mm, vma, prev, rb_link, rb_parent);------------------------------------将新创建的VMA加入到mm->mmap链表和红黑树中。
out:
perf_event_mmap(vma);
mm->total_vm += len >> PAGE_SHIFT;
if (flags & VM_LOCKED)
mm->locked_vm += (len >> PAGE_SHIFT);
vma->vm_flags |= VM_SOFTDIRTY;
return addr;
}
从arch_pick_mmap_layout中可知,current->mm->get_ummapped_area对应的是arch_get_unmapped_area_topdown。
所以get_unmapped_area指向arch_get_unmapped_area_topdown(用来判断虚拟地址是否有足够的空间,返回一块没有映射 过的空间的起始地址)。
3.1:VM_LOCK的情况
表示马上为这块进程虚拟地址空间分配物理页面并建立映射关系。
mm_populate调用__mm_populate来分配页面,同时ignore_erros。
int __mm_populate(unsigned long start, unsigned long len, int ignore_errors)
{
struct mm_struct *mm = current->mm;
unsigned long end, nstart, nend;
struct vm_area_struct *vma = NULL;
int locked = 0;
long ret = 0;
VM_BUG_ON(start & ~PAGE_MASK);
VM_BUG_ON(len != PAGE_ALIGN(len));
end = start + len;
for (nstart = start; nstart < end; nstart = nend) {----------------------------以start为起始地址,先通过find_vma()查找VMA。
/*
* We want to fault in pages for [nstart; end) address range.
* Find first corresponding VMA.
*/
if (!locked) {
locked = 1;
down_read(&mm->mmap_sem);
vma = find_vma(mm, nstart);
} else if (nstart >= vma->vm_end)
vma = vma->vm_next;
if (!vma || vma->vm_start >= end)
break;
/*
* Set [nstart; nend) to intersection of desired address
* range with the first VMA. Also, skip undesirable VMA types.
*/
nend = min(end, vma->vm_end);
if (vma->vm_flags & (VM_IO | VM_PFNMAP))
continue;
if (nstart < vma->vm_start)
nstart = vma->vm_start;
/*
* Now fault in a range of pages. __mlock_vma_pages_range()
* double checks the vma flags, so that it won't mlock pages
* if the vma was already munlocked.
*/
ret = __mlock_vma_pages_range(vma, nstart, nend, &locked);------------------为vma分配物理内存
if (ret < 0) {
if (ignore_errors) {
ret = 0;
continue; /* continue at next VMA */
}
ret = __mlock_posix_error_return(ret);
break;
}
nend = nstart + ret * PAGE_SIZE;
ret = 0;
}
if (locked)
up_read(&mm->mmap_sem);
return ret; /* 0 or negative error code */
}
__mlock_vma_pages_range为vma指定虚拟地址空间的物理页面:
long __mlock_vma_pages_range(struct vm_area_struct *vma,
unsigned long start, unsigned long end, int *nonblocking)
{
struct mm_struct *mm = vma->vm_mm;
unsigned long nr_pages = (end - start) / PAGE_SIZE;
int gup_flags;
VM_BUG_ON(start & ~PAGE_MASK);
VM_BUG_ON(end & ~PAGE_MASK);
VM_BUG_ON_VMA(start < vma->vm_start, vma);
VM_BUG_ON_VMA(end > vma->vm_end, vma);
VM_BUG_ON_MM(!rwsem_is_locked(&mm->mmap_sem), mm);------------------------一些错误判断
gup_flags = FOLL_TOUCH | FOLL_MLOCK;
/*
* We want to touch writable mappings with a write fault in order
* to break COW, except for shared mappings because these don't COW
* and we would not want to dirty them for nothing.
*/
if ((vma->vm_flags & (VM_WRITE | VM_SHARED)) == VM_WRITE)
gup_flags |= FOLL_WRITE;
/*
* We want mlock to succeed for regions that have any permissions
* other than PROT_NONE.
*/
if (vma->vm_flags & (VM_READ | VM_WRITE | VM_EXEC))
gup_flags |= FOLL_FORCE;
/*
* We made sure addr is within a VMA, so the following will
* not result in a stack expansion that recurses back here.
*/
return __get_user_pages(current, mm, start, nr_pages, gup_flags,----------为进程地址空间分配物理内存并且建立映射关系。
NULL, NULL, nonblocking);
}
__get_user_pages是很重要的分配物理内存的接口函数,很多驱动使用这个API用于为用户空间分配物理内存
long __get_user_pages(struct task_struct *tsk, struct mm_struct *mm,
unsigned long start, unsigned long nr_pages,
unsigned int gup_flags, struct page **pages,
struct vm_area_struct **vmas, int *nonblocking)
{
long i = 0;
unsigned int page_mask;
struct vm_area_struct *vma = NULL;
if (!nr_pages)
return 0;
VM_BUG_ON(!!pages != !!(gup_flags & FOLL_GET));
/*
* If FOLL_FORCE is set then do not force a full fault as the hinting
* fault information is unrelated to the reference behaviour of a task
* using the address space
*/
if (!(gup_flags & FOLL_FORCE))
gup_flags |= FOLL_NUMA;
do {
struct page *page;
unsigned int foll_flags = gup_flags;
unsigned int page_increm;
/* first iteration or cross vma bound */
if (!vma || start >= vma->vm_end) {
vma = find_extend_vma(mm, start);------------------------------查找VMA,如果vma->vm_start大于查找地址start,那么它会尝试去扩增vma,吧vma->vm_start边界扩大到start中。
if (!vma && in_gate_area(mm, start)) {
int ret;
ret = get_gate_page(mm, start & PAGE_MASK
gup_flags, &vma,
pages ? &pages[i] : NULL);
if (ret)
return i ? : ret;
page_mask = 0;
goto next_page;
}
if (!vma || check_vma_flags(vma, gup_flags))
return i ? : -EFAULT;
if (is_vm_hugetlb_page(vma)) {
i = follow_hugetlb_page(mm, vma, pages, vmas,
&start, &nr_pages, i,
gup_flags);
continue;
}
}
retry:
/*
* If we have a pending SIGKILL, don't keep faulting pages and
* potentially allocating memory.
*/
if (unlikely(fatal_signal_pending(current)))-----------------------如果收到一个SIGKILL信号,不需要继续内存分配,直接退出。
return i ? i : -ERESTARTSYS;
cond_resched();----------------------------------------------------判断当前进程是否需要被调度。
page = follow_page_mask(vma, start, foll_flags, &page_mask);-------查看vma中的虚拟地址是否已经分配了物理内存。
if (!page) {
int ret;
ret = faultin_page(tsk, vma, start, &foll_flags,
nonblocking);
switch (ret) {
case 0:
goto retry;
case -EFAULT:
case -ENOMEM:
case -EHWPOISON:
return i ? i : ret;
case -EBUSY:
return i;
case -ENOENT:
goto next_page;
}
BUG();
}
if (IS_ERR(page))
return i ? i : PTR_ERR(page);
if (pages) {-------------------------------------------------------flush页面对应的cache
pages[i] = page;
flush_anon_page(vma, page, start);
flush_dcache_page(page);
page_mask = 0;
}
next_page:
if (vmas) {
vmas[i] = vma;
page_mask = 0;
}
page_increm = 1 + (~(start >> PAGE_SHIFT) & page_mask);
if (page_increm > nr_pages)
page_increm = nr_pages;
i += page_increm;
start += page_increm * PAGE_SIZE;
nr_pages -= page_increm;
} while (nr_pages);
return i;
}
follow_page_mask函数:由mm和地址address找到当前进程页表对应的PGD页面目录项,用户进程内存管理mm_struct的pgd成员指向用户进程的页表的基地址。如果pgd表项的页表为空,则返回报错。 vm_normal_page:根据pte来返回normal mapping页面的struct page数据结构体。
一些特殊映射的页面是不会返回struct page结构的,这些页面不希望被参与到内存管理的一些活动中,如页面回收、页迁移和KSM等。
内核尝试用pte_mkspecial()宏来设置PTE_SPECIAL软件定义的比特位,主要用途有
- 内核的零页面zero page
- 大量的驱动程序使用remap_pfn_range()函数来实现映射内核页面到用户空间。这些用户程序使用的VMA通常设置了(VM_IO|VM_PFNMAP|VM_DONTEXPAND|VM_DONTDUMP)
- vm_insert_page()/vm_insert_pfn()映射内核页面到用户空间
eg:malloc(30k)函数调用brk,将_edata指针往高推30k,完成虚拟内存分配,(这块内存现在还没有物理页与之对应),等到进程第一次读写这块内存的时候,发生缺页中断,内核才分配内存的物理页面。
四:mmap映射
mmap作用:常用的一个系统接口调用,用户程序分配内存,读写大文件,连接动态库文件,多进程间共享内存。
可以分为私有内存和共享内存。
私有内存:常见作用为glibc分配大块内存
共享内存:让相关进程共享一块内存区域。,通常用于父子进程的通信。
mmap的两个小问题:
1:两次对相同地址执行mmap是否成功?
#include#include void main(void) { char *pmap1, *pmap2; pmap1 = (char *)mmap(0x20000000, 10240, PROT_READ, MAP_PRIVATE|MAP_FIXED|MAP_ANONYMOUS, -1, 0); if(MAP_FAILED == pmap1) printf("pmap1 failed\n"); pmap2 = (char *)mmap(0x20000000, 1024, PROT_READ, MAP_PRIVATE|MAP_FIXED|MAP_ANONYMOUS, -1, 0); if(MAP_FAILED == pmap2) printf("pmap1 failed\n"); }
第二次没有返回错误:原因find_vma_links()会遍历进程中所有的vmas当检查到当前映射区域和已有映射区域有重叠时返回错误,然后在mmap_reion函数中调用do_munmap函数吧这段将要映射的区域先销毁然后重新映射。
问题2:在一个播放系统中同时打开几十个不同高清视频文件,发现播放有些卡顿,打开文件使用的是mmap,分析原因并解决。
mmap建立文件映射时,只建立了VMA,而没有分配对应的页面和建立映射关系。当播放器真正读取文件时才产恒缺页中断读取文件内容到pagecache中去。每次读取文件时,会频繁的产生缺页中断。
播放时会不同发生缺页异常去读取文件内容,导致性能较差。
解决方法:1.对mmap映射后的地址用madvise(addr, len, MADV_SEQUENTIAL)。MADV_SEQUENTIAL会立刻启动io进行预读,增大内核默认的预读窗口。
2.通过"blockdev --setra"来增大内核默认预读窗口,默认是128KB