一: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 <stdio.h> #include <sys/mman.h> 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