Linux內存管理
Linux內存管理(四)用戶態內存映射
前面講解了虛擬地址空間是如何組織的,以及物理頁是如何管理的。這篇文章將講解這兩者之間是如何映射起來的
一、mmap的原理
每個進程都有一個 vm_area_struct 列表,用於描述虛擬內存空間的不同內存塊,這個變量的名稱爲 mmap
struct mm_struct {
struct vm_area_struct *mmap; /* list of VMAs */
......
}
struct vm_area_struct {
/*
* For areas with an address space and backing store,
* linkage into the address_space->i_mmap interval tree.
*/
struct {
struct rb_node rb;
unsigned long rb_subtree_last;
} shared;
/*
* A file's MAP_PRIVATE vma can be in both i_mmap tree and anon_vma
* list, after a COW of one of the file pages. A MAP_SHARED vma
* can only be in the i_mmap tree. An anonymous MAP_PRIVATE, stack
* or brk vma (with NULL file) can only be in an anon_vma list.
*/
struct list_head anon_vma_chain; /* Serialized by mmap_sem &
* page_table_lock */
struct anon_vma *anon_vma; /* Serialized by page_table_lock */
/* Function pointers to deal with this struct. */
const struct vm_operations_struct *vm_ops;
/* Information about our backing store: */
unsigned long vm_pgoff; /* Offset (within vm_file) in PAGE_SIZE
units */
struct file * vm_file; /* File we map to (can be NULL). */
void * vm_private_data; /* was vm_pte (shared mem) */
內存映射不僅僅是物理內存和虛擬內存之間的映射,還包括將文件中的內容映射到虛擬內存空間。這個時候,訪問內存空間就能訪問到文件裏的內容。而僅有物理內存和虛擬內存的映射是一種特殊情況
對於 malloc 函數,如果申請小塊內存,那麼就調用 brk,如果申請大塊內存,就使用到了 mmap,對於堆的申請來說,mmap是映射虛擬內存空間到物理內存
另外,如果一個進程想映射一個文件到自己的虛擬地址空間,也要通過 mmap 系統調用。這時候 mmap 映射虛擬地址空間到物理內存再到文件。下面來看一看 mmap 這個系統調用
SYSCALL_DEFINE6(mmap, unsigned long, addr, unsigned long, len,
unsigned long, prot, unsigned long, flags,
unsigned long, fd, unsigned long, off)
{
......
error = sys_mmap_pgoff(addr, len, prot, flags, fd, off >> PAGE_SHIFT);
......
}
SYSCALL_DEFINE6(mmap_pgoff, unsigned long, addr, unsigned long, len,
unsigned long, prot, unsigned long, flags,
unsigned long, fd, unsigned long, pgoff)
{
struct file *file = NULL;
......
file = fget(fd);
......
retval = vm_mmap_pgoff(file, addr, len, prot, flags, pgoff);
return retval;
}
如果要映射文件,fd 會傳進來一個文件描述符,然後通過 fget 獲取 struct file,struct file 表示一個打開的文件
接下倆的調用鏈是:vm_mmap_pgoff -> do_mmap_pgoff -> do_mmap。這裏面主要做了兩件事情
- 調用 get_unmapped_area 找到一個沒有映射的區域
- 調用 mmap_regin 映射這個區域
首先看 get_unmapped_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);
......
get_area = current->mm->get_unmapped_area;
if (file) {
if (file->f_op->get_unmapped_area)
get_area = file->f_op->get_unmapped_area;
}
......
}
如果是匿名映射,就會調用 mm_struct 中的 get_unmapped_area 函數。這個函數其實是 arch_get_ummapped_area。它會調用 find_vma_prev,再表示虛擬內存區域的紅黑樹上找到找到相應的位置。之所以叫 prev,是這個時候這個虛擬內存區域還沒有建立,這裏找到是前一個 vm_area_struct
如果是文件映射,那麼就通過 struct file 中的 file_ops 操作集合中的 get_unmapped_area 方法。如果是 ext4 文件系統,那麼 get_unmapped_area 對應 thp_get_unmapped_area。這個函數最終還是調用了 mm_struct 中的 get_unmapped_area 方法
const struct file_operations ext4_file_operations = {
......
.mmap = ext4_file_mmap
.get_unmapped_area = thp_get_unmapped_area,
};
unsigned long __thp_get_unmapped_area(struct file *filp, unsigned long len,
loff_t off, unsigned long flags, unsigned long size)
{
unsigned long addr;
loff_t off_end = off + len;
loff_t off_align = round_up(off, size);
unsigned long len_pad;
len_pad = len + size;
......
addr = current->mm->get_unmapped_area(filp, 0, len_pad,
off >> PAGE_SHIFT, flags);
addr += (off - addr) & (size - 1);
return addr;
}
下面再來看 mmap_regin,看它是如何映射這個虛擬內存區域的
unsigned long mmap_region(struct file *file, unsigned long addr,
unsigned long len, vm_flags_t vm_flags, unsigned long pgoff,
struct list_head *uf)
{
struct mm_struct *mm = current->mm;
struct vm_area_struct *vma, *prev;
struct rb_node **rb_link, *rb_parent;
/*
* Can we just expand an old mapping?
*/
vma = vma_merge(mm, prev, addr, addr + len, vm_flags,
NULL, file, pgoff, NULL, NULL_VM_UFFD_CTX);
if (vma)
goto out;
/*
* Determine the object being mapped and call the appropriate
* specific mapper. the address has already been validated, but
* not unmapped, but the maps are removed from the list.
*/
vma = kmem_cache_zalloc(vm_area_cachep, GFP_KERNEL);
if (!vma) {
error = -ENOMEM;
goto unacct_error;
}
vma->vm_mm = mm;
vma->vm_start = addr;
vma->vm_end = addr + len;
vma->vm_flags = vm_flags;
vma->vm_page_prot = vm_get_page_prot(vm_flags);
vma->vm_pgoff = pgoff;
INIT_LIST_HEAD(&vma->anon_vma_chain);
if (file) {
vma->vm_file = get_file(file);
error = call_mmap(file, vma);
addr = vma->vm_start;
vm_flags = vma->vm_flags;
}
......
vma_link(mm, vma, prev, rb_link, rb_parent);
return addr;
.....
還記得上一步 get_unmapped_area 找到了虛擬內存區域的前一個 vm_area_struct 嗎?這裏就通過調用 vma_merge 看能否基於它來擴展虛擬內存區域
如果不能,則通過 kmem_cache_zalloc 分配一個新的 vm_area_struct,在 slub 裏面創建一個新的 vm_area_struct 對象,設置好對應的虛擬內存區域,然後添加到 anon_vma_chain 鏈表還有紅黑樹中
如果是映射到文件,就設置 vm_file,然後調用 call_mmap。其實就是調用 file_ops 中的 mmap 函數。對於 ext4 文件系統,調用的就是 ext4_file_mmap。從中可以看到,將 vma->vm_ops 設置爲相應的文件系統操作 ext4_file_vm_ops,這樣,文件和內存就關聯起來了
static inline int call_mmap(struct file *file, struct vm_area_struct *vma)
{
return file->f_op->mmap(file, vma);
}
static int ext4_file_mmap(struct file *file, struct vm_area_struct *vma)
{
......
vma->vm_ops = &ext4_file_vm_ops;
......
}
再回到 mmap_region 函數,最終,vma_link 函數就會將新創建的 vm_area_struct 添加到 mm_struct 中的紅黑樹中
這個時候,從內存到文件的映射至少從邏輯層面已經建立起來了。那麼從文件到內存的關聯呢?
vma_link 還做了一件事情,就是 _vma_link_file。這個東西要用於建立這層映射關係
對於打開的文件有一個 struct file,裏面有一個 struct address_apace,裏面有一棵紅黑樹 i_mmap,對於所有與該文件相關的 vm_area_struct 都回插入到這棵紅黑樹中
struct address_space {
struct inode *host; /* owner: inode, block_device */
......
struct rb_root i_mmap; /* tree of private and shared mappings */
......
const struct address_space_operations *a_ops; /* methods */
......
}
static void __vma_link_file(struct vm_area_struct *vma)
{
struct file *file;
file = vma->vm_file;
if (file) {
struct address_space *mapping = file->f_mapping;
vma_interval_tree_insert(vma, &mapping->i_mmap);
}
到這裏,虛擬內存空間的設置已經告一段落了,但是從虛擬內存空間到物理內存空間還沒有發生映射
因爲物理內存非常寶貴,所以只有等到真正需要訪問內存的時候,纔會分配物理內存
二、用戶態缺頁異常
一旦開始訪問某個虛擬地址,如果發現沒有對應的物理頁,那麼就回觸發缺頁異常,進入缺頁處理,調用 do_page_fault
dotraplinkage void notrace
do_page_fault(struct pt_regs *regs, unsigned long error_code)
{
unsigned long address = read_cr2(); /* Get the faulting address */
......
__do_page_fault(regs, error_code, address);
......
}
/*
* This routine handles page faults. It determines the address,
* and the problem, and then passes it off to one of the appropriate
* routines.
*/
static noinline void
__do_page_fault(struct pt_regs *regs, unsigned long error_code,
unsigned long address)
{
struct vm_area_struct *vma;
struct task_struct *tsk;
struct mm_struct *mm;
tsk = current;
mm = tsk->mm;
if (unlikely(fault_in_kernel_space(address))) {
if (vmalloc_fault(address) >= 0)
return;
}
......
vma = find_vma(mm, address);
......
fault = handle_mm_fault(vma, address, flags);
......
__do_page_fault 首先回判斷該虛擬地址是否在內核空間,如果是,那麼就調用 vmalloc_fault ,這個函數是內核態的空間映射,下一篇文章再講解
如果是用戶虛擬內存空間,那麼就會通過 find_vma 找到這個地址所在的 vm_area_struct 區域,然後調用 handle_mm_fault 處理這個區域
static int __handle_mm_fault(struct vm_area_struct *vma, unsigned long address,
unsigned int flags)
{
struct vm_fault vmf = {
.vma = vma,
.address = address & PAGE_MASK,
.flags = flags,
.pgoff = linear_page_index(vma, address),
.gfp_mask = __get_fault_gfp_mask(vma),
};
struct mm_struct *mm = vma->vm_mm;
pgd_t *pgd;
p4d_t *p4d;
int ret;
pgd = pgd_offset(mm, address);
p4d = p4d_alloc(mm, pgd, address);
......
vmf.pud = pud_alloc(mm, p4d, address);
......
vmf.pmd = pmd_alloc(mm, vmf.pud, address);
......
return handle_pte_fault(&vmf);
}
這裏終於見到了 PGD、P4G、PUD、PMD、PTE,這是頁表相關的概念
pgd_t 用於全局頁目錄項,pud_t 用於上層頁目錄項,pmd_t 用於中間頁目錄項,pte_t 用於直接頁目錄項
每個進程都有自己獨立的虛擬地址空間,這些地址空間需要通過頁表映射到不同的物理地址,所以每個進程都有自己的頁表,這些頁表最頂級的頁表 pgd 就存放在 task_struct 的 mm_struct 的 pgd 變量裏面
創建一個進程會調用 fork,裏面對於內存部分調用了 copy_mm,裏面調用了 dup_mm,其定義如下
/*
* Allocate a new mm structure and copy contents from the
* mm structure of the passed in task structure.
*/
static struct mm_struct *dup_mm(struct task_struct *tsk)
{
struct mm_struct *mm, *oldmm = current->mm;
mm = allocate_mm();
memcpy(mm, oldmm, sizeof(*mm));
if (!mm_init(mm, tsk, mm->user_ns))
goto fail_nomem;
err = dup_mmap(mm, oldmm);
return mm;
}
這裏處理分配一個 mm_struct,還通過 memcpy 將其複製得跟父進程一樣,接下來還調用 mm_init 初始化。mm_init 調用 mm_alloc_pgd,分配一個全局頁目錄表 pgd,並將其賦值給 mm_struct 中的 pgd 變量
static inline int mm_alloc_pgd(struct mm_struct *mm)
{
mm->pgd = pgd_alloc(mm);
return 0;
}
mm_alloc_pgd 除了分配 PGD外,還通過 pgd_ctor 來完成一個重要操作
static void pgd_ctor(struct mm_struct *mm, pgd_t *pgd)
{
/* If the pgd points to a shared pagetable level (either the
ptes in non-PAE, or shared PMD in PAE), then just copy the
references from swapper_pg_dir. */
if (CONFIG_PGTABLE_LEVELS == 2 ||
(CONFIG_PGTABLE_LEVELS == 3 && SHARED_KERNEL_PMD) ||
CONFIG_PGTABLE_LEVELS >= 4) {
clone_pgd_range(pgd + KERNEL_PGD_BOUNDARY,
swapper_pg_dir + KERNEL_PGD_BOUNDARY,
KERNEL_PGD_PTRS);
}
......
}
pgd_ctor 做了什麼呢?它拷貝 swapper_pg_dir 到 進程的 PGD 中,swapper_pg_dir 是內核頁表最頂級的全局頁目錄
一個進程的虛擬地址分爲用戶態和內核態兩部分。頁表分爲用戶地址空間的頁表也分爲內核頁表,在PGD中,有一部分是用戶地址空間,一部分是內核空間。而所有進程內核空間都是一樣的,所以這裏複製內核部分的頁目錄項到 PGD 中
至此,一個進程 fork 完後,有了自己的頂級目錄項表 PGD,也有自己的內核頁表,但對於用戶地址空間,還完全沒有映射過,這就需要等待對用戶地址空間進行訪問的那一刻
當這個進程被調用到某個CPU上運行的時候,要調用 context_switch 進行上下文切換。對於內存方面切換回調用 switch_mm_irqs_off,這裏面會調用 load_new_mm_cr3
cr3 是 CPU 的一個寄存器,它指向當前進程的頂級 pgd。如果 CPU 要訪問進程的虛擬地址的時候,它需要從cr3中獲取 pgd 在物理內存中的地址,然後根據裏面的頁表解析虛擬地址對應的物理地址,從而訪問到真正的物理地址上的數據
只有在訪問地址的時候,發現沒有映射到物理內存,纔會觸發缺頁異常。進入內核調用 do_page_fault,一直調用到 __handle_mm_fault,於是 __handle_mm_fault 調用 pud_alloc 和 pmd_alloc,來創建相應的頁表目錄項,最後調用 handle_pte_fault 來創建頁表項
到了這裏還沒完,接下來繼續看 handle_pte_fault
static int handle_pte_fault(struct vm_fault *vmf)
{
pte_t entry;
......
vmf->pte = pte_offset_map(vmf->pmd, vmf->address);
vmf->orig_pte = *vmf->pte;
......
if (!vmf->pte) {
if (vma_is_anonymous(vmf->vma))
return do_anonymous_page(vmf);
else
return do_fault(vmf);
}
if (!pte_present(vmf->orig_pte))
return do_swap_page(vmf);
......
}
總的分爲三種情況
如果 PTE,也就是頁表項,從來沒有出現過,那就是新映射的頁
-
映射的是匿名的頁,應該映射到一個物理頁,在這裏調用 do_anonymous_page
-
如果是映射到文件,那麼這裏就調用 do_fault
如果 PTE 原來出現過
- 說明原來頁面存在於物理頁中,後來換出到磁盤中,現在需要換入到物理內存中,調用 do_swap_page
首先看第一種情況,do_anonymous_page。對於匿名頁,首先需要通過 pte_alloc 分配一個頁表項,然後通過 alloc_zeroed_user_highpage_movable 分配一個頁。之後它會調用它會調用 alloc_pages_vma,並最終調用 __alloc_pages_nodemask,它是我們前面說的夥伴系統的核心函數,它用來分配物理頁。
static int do_anonymous_page(struct vm_fault *vmf)
{
struct vm_area_struct *vma = vmf->vma;
struct mem_cgroup *memcg;
struct page *page;
int ret = 0;
pte_t entry;
......
if (pte_alloc(vma->vm_mm, vmf->pmd, vmf->address))
return VM_FAULT_OOM;
......
page = alloc_zeroed_user_highpage_movable(vma, vmf->address);
......
entry = mk_pte(page, vma->vm_page_prot);
if (vma->vm_flags & VM_WRITE)
entry = pte_mkwrite(pte_mkdirty(entry));
vmf->pte = pte_offset_map_lock(vma->vm_mm, vmf->pmd, vmf->address,
&vmf->ptl);
......
set_pte_at(vma->vm_mm, vmf->address, vmf->pte, entry);
......
}
do_anonymous_page 接下來還會調用 mk_pte,將頁表項指向新分配的物理頁,set_pte_at 會將頁表項塞到頁表中
第二種情況,映射到文件 do_fault,最終調用 __do_fault
static int __do_fault(struct vm_fault *vmf)
{
struct vm_area_struct *vma = vmf->vma;
int ret;
......
ret = vma->vm_ops->fault(vmf);
......
return ret;
}
這裏調用 vm_ops 中的 fault 函數,這個文件操作集在 mmap 系統調用中被設置。如果是 ext4 文件系統,那麼它對應 ext4_filemap_fault
static const struct vm_operations_struct ext4_file_vm_ops = {
.fault = ext4_filemap_fault,
.map_pages = filemap_map_pages,
.page_mkwrite = ext4_page_mkwrite,
};
int ext4_filemap_fault(struct vm_fault *vmf)
{
struct inode *inode = file_inode(vmf->vma->vm_file);
......
err = filemap_fault(vmf);
......
return err;
}
filemap_fault 的定義如下
int filemap_fault(struct vm_fault *vmf)
{
int error;
struct file *file = vmf->vma->vm_file;
struct address_space *mapping = file->f_mapping;
struct inode *inode = mapping->host;
pgoff_t offset = vmf->pgoff;
struct page *page;
int ret = 0;
......
page = find_get_page(mapping, offset);
if (likely(page) && !(vmf->flags & FAULT_FLAG_TRIED)) {
do_async_mmap_readahead(vmf->vma, ra, file, page, offset);
} else if (!page) {
goto no_cached_page;
}
......
vmf->page = page;
return ret | VM_FAULT_LOCKED;
no_cached_page:
error = page_cache_read(file, offset, vmf->gfp_mask);
......
}
對於文件映射到內存,一般有物理頁作爲緩存,find_get_page 就是找到對應的物理頁。如果找到了,就調用 do_async_mmap_readahead,如果找不到,就跳到 page_cache_read
page_cache_read 的定義如下
static int page_cache_read(struct file *file, pgoff_t offset, gfp_t gfp_mask)
{
struct address_space *mapping = file->f_mapping;
struct page *page;
......
page = __page_cache_alloc(gfp_mask|__GFP_COLD);
......
ret = add_to_page_cache_lru(page, mapping, offset, gfp_mask & GFP_KERNEL);
......
ret = mapping->a_ops->readpage(file, page);
......
}
首先會分配一個緩存頁,然後加入 lru 列表中,然後在 address_space 中調用 address_space_operations 的 readpage 函數,將文件內容讀到內存中
對於 ext4,address_space_operations 定義如下
static const struct address_space_operations ext4_aops = {
.readpage = ext4_readpage,
.readpages = ext4_readpages,
......
};
static int ext4_read_inline_page(struct inode *inode, struct page *page)
{
void *kaddr;
......
kaddr = kmap_atomic(page);
ret = ext4_read_inline_data(inode, kaddr, len, &iloc);
flush_dcache_page(page);
kunmap_atomic(kaddr);
......
}
所以 readpage 就是對應 ext4_readpage,最終會調用到 ext4_read_inline_page
在 ext4_read_inline_page 函數中,首先調用 kmap_atomic,將物理內存映射到內核的虛擬地址空間,得到內核的虛擬地址 kaddr。kmap_atomic 是用來做臨時映射內核空間虛擬地址的,本來物理內存已經映射到進程的用戶虛擬空間,不需要在內核中再映射一次。但是需要將文件內容讀取到內存中,而此時只能使用虛擬地址,不能使用物理地址,內核又不能訪問用戶虛擬地址,所以只能將物理內存臨時映射到內核地址空間
之後通過 ext4_read_inline_data 讀取內容到物理內存中,然後再通過 kunmap_atomic 取消地址映射
下面再來看第三種情況,do_swap_page。如果物理內存長時間不使用,就要換出到磁盤種,也就是 swap,現在這部分數據需要使用,那麼就需要將其換入到內存中
int do_swap_page(struct vm_fault *vmf)
{
struct vm_area_struct *vma = vmf->vma;
struct page *page, *swapcache;
struct mem_cgroup *memcg;
swp_entry_t entry;
pte_t pte;
......
entry = pte_to_swp_entry(vmf->orig_pte);
......
page = lookup_swap_cache(entry);
if (!page) {
page = swapin_readahead(entry, GFP_HIGHUSER_MOVABLE, vma,
vmf->address);
......
}
......
swapcache = page;
......
pte = mk_pte(page, vma->vm_page_prot);
......
set_pte_at(vma->vm_mm, vmf->address, vmf->pte, pte);
vmf->orig_pte = pte;
......
swap_free(entry);
......
}
do_swap_page 會先查 swap 文件有沒有緩存頁。如果沒有,就調用 swapin_readahead,將 swap 文件讀入到內存中來,形成內存頁,並通過 mk_pte 生成頁表項,set_pte_at 將頁表項插入頁表中,swap_free 將 swap 文件清理掉。因爲 swap 文件已經被加載到內存中,所以就不需要了
swapin_readahead 最終會調用 swap_readpage,這裏可以看到 readpage,也就是說讀取 swap 文件和讀取普通文件是一樣的
int swap_readpage(struct page *page, bool do_poll)
{
struct bio *bio;
int ret = 0;
struct swap_info_struct *sis = page_swap_info(page);
blk_qc_t qc;
struct block_device *bdev;
......
if (sis->flags & SWP_FILE) {
struct file *swap_file = sis->swap_file;
struct address_space *mapping = swap_file->f_mapping;
ret = mapping->a_ops->readpage(swap_file, page);
return ret;
}
......
}
通過上面一系列複雜的操作,用戶的缺頁異常已經處理完畢。物理內存中有了頁面,頁表映射也已經建立好,接下來用戶訪問虛擬內存空間,可以通過頁錶轉換到對應物理頁表
頁表一般很大,只能存放在內存中,操作系統每次訪問內存都需要先查詢頁表,轉換爲物理地址,然後再到物理內存中讀取數據
爲了加快映射速度,不需要每一次從虛擬地址到物理地址的轉換都需要經過一輪頁錶轉換。引入了 TLB(Translation lookaside buffer),常稱爲快表,專門用來做地址映射的硬件設備。它不在內存中,但是訪問速度比內存快,所以可以認爲 TLB 是頁表的 Cache,其中存儲着當前最可能被訪問到的頁表項,其內容是部分頁表的一個副本
有了 TLB 後,每次內存訪問都先查快表,如果快表中沒有緩存,那麼就查詢頁表
三、總結
- 用戶態內存映射函數 mmap,它用來做匿名映射還有文件映射
- 用戶態的最頂級頁目錄項,存儲在 mm_struct 中
- 在用戶態訪問沒有映射的地址時,會產生缺頁異常,分配物理頁表,補齊頁表。如果時匿名映射則分配物理內存;如果時swap,則將 swap 文件讀入物理內存;如果時文件,則分配物理內存,文件內容讀入到物理內存中