趣談Linux操做系統學習筆記:第二十五講

1、mmap原理

在虛擬內存空間那一節,咱們知道,每個進程都有一個列表vm_area_struct,指向虛擬地址空間的不一樣內存塊,這個變量名字叫mmapnode

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) */

其實內存映射不只僅是物理內存和虛擬內存之間的映射,還包括將文件中的內容映射到虛擬內存空間,這個時候訪問內存空間就可以訪問到文件裏面的數據。api

而僅有物理內存和虛擬內存的映射是一種特殊狀況緩存

一、mmap系統調用

一、如何分配一大塊內存app

若是申請一大塊內存就用mmap,mmap是映射內存空間到物理內存async

另外,若是一個進程想映射一個文件到本身的虛擬內存空間,也要經過mmap系統調用這個時候mmap是映射內存空間到物理內存再到文件。可見mmap這個系統調用時核心,ide

二、咱們如今來看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會傳進來一個文件描述符,而且mmap_pgoff裏面經過fget函數,根據文件描述符得到struct file、struct file表示打開一個文件佈局

接下來的調用鏈是:this

這裏主要乾了兩件事情atom

一、調用 get_unmapped_area 找到一個沒有映射的區域

二、調用 mmap_region 映射這個區域。

三、咱們先來看 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;
	} 
......
}

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_region 函數,看它如何映射這個虛擬內存區域

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;
.....

一、還記得我們剛找到了虛擬內存區域的前一個 vm_area_struct,咱們首先要看,是否可以基於它進行擴展,也即調用 vma_merge,和前一個 vm_area_struct 合併到一塊兒。

二、若是不能,就須要調用 kmem_cache_zalloc,在 Slub 裏面建立一個新的 vm_area_struct對象,設置起始和結束位置,將它加入隊列。若是是映射到文件,則設置 vm_file 爲目標文件,

  調用 call_mmap。其實就是調用 file_operations 的 mmap 函數

三、對於 ext4 文件系統,調用的是 ext4_file_mmap。從這個函數的參數能夠看出,這一刻文件和內存開始發生關係了。這裏咱們將vm_area_struct 的內存操做設置爲文件系統操做,也就是說,

  讀寫內存其實就是讀寫文件系統。

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_space 結構,這裏面有棵變量名爲 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);
	}

到這裏,內存映射的內容要告一段落,你可能會困惑,好像尚未和物理內存法神過任何關係、仍是在虛擬內存裏面折騰呀?對的,由於到目前爲止,咱們尚未開始真正訪問內存呀!

這個時候,內存管理並不直接分配物理內存,由於物理內存相對於虛擬地址空間太寶貴了,只要等你真正用的那一刻纔會開始分配

2、用戶態缺頁異常

一旦開始訪問虛擬內存的某個地址,若是咱們發現,並無對應的物理頁,那就出發缺頁中斷,調用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,這就是和我們前面學過的虛擬內存的佈局對應上了

二、在內核裏面,vmalloc區域須要內核頁表映射到物理頁,我們這裏把內核的這部分放放,接着看用戶空間的部分

三、接下來在用戶空間裏面,找到你訪問的那個地址所在的區域 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,這就是前面講頁表的時候,講述的四級頁表的概念,由於暫且不考慮五級頁表,咱們暫時忽略P4G

一、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,分配全局、頁目錄項,賦值給 mm_struct 的 pdg 成員變量。

static inline int mm_alloc_pgd(struct mm_struct *mm)
{
	mm->pgd = pgd_alloc(mm);
	return 0;
}

pgd_alloc 裏面除了分配 PDG 以外,還作了很重要的一個事情,就是調用 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幹了什麼

二、load_new_mm_cr3 爲何要將虛擬地址轉換爲物理地址?

由於cr3存放的是物理地址,只有將虛擬地址轉換爲物理地址才能加載到 cr3 裏面去

三、load_new_mm_cr3將虛擬地址轉換爲虛擬地址的調用鏈

四、地址轉換的過程無需進入內核態

五、觸發缺頁異常調用鏈

只有訪問內存的時候發現沒有映射多物理內存,頁表也沒有建立過,才觸發缺頁異常

繞了一大圈,終於將頁表整個機制的各個部分串了起來。可是我們的故事還沒講完,物理的內存、還沒找到。咱們還得接着分析 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);
......
}

匿名頁調用

這個函數你還記得嗎?就是我們夥伴系統的核心函數,專門用來分配物理頁面的。do_anonymous_page 接下來要調用 mk_pte,將頁表項指向新分配的物理頁,set_pte_at 會將頁表項塞到頁表裏面。

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_fault,最終咱們會調用 __do_fault

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;
	}
......
}

這裏調用了struct vm_operations_struct vm_ops的fault函數,還記得我們上面用mmap映射文件的時候,對於ext4文件系統,vm_ops指向了ext4_file_vm_ops也就是調用了函數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;
}

ext4_filemap_fault裏面的邏輯咱們很容易就能讀懂,vm_file就是我們當時mmap的時候映射的那個文件,而後咱們須要調用filemap_fault

對於文件映射來講,通常這個文件會在物理內存裏面有頁面做爲它的緩存,find_get_page就是找那個頁,若是找到了,就調用,預讀一些數據到內存裏面;若是沒有,就跳到no_cached_page

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);
......
}

一、若是沒有物理內存中的緩存頁

二、那咱們就調用 page_cach—_read

三、在這裏顯示分配一個緩存頁

四、將這一頁加到 lru 表裏面

五、而後在 address_space 中調用 aaddress_space_operations 的readpage 函數,將文件內容讀到內存中。address_space 的做用我們上面也介紹過了。

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);
......
}

struct address_space_operations對於 ext4 文件系統的定義以下所示。這麼說來,

上面的 readpage 調用的實際上是 ext4_readage。由於咱們還沒講到文件系統,這裏咱們不詳細介紹

ext4_readpage 具體幹了什麼。你只要知道,最後會調用 ext4_read_inline_page,這裏面有部分邏輯和內存映射有關就好了。

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);
......
}

一、爲何要在內核裏面映射一把?

一、在 ext4_read_inline_page 函數裏,咱們須要先調用 kmap_atomic,將物理內存映射到內核的虛擬地址空間,獲得內核中的地址kaddr

二、kaddr它是用來作臨時內核映射的。原本把物理內存映射到用戶虛擬地址空間,不須要在內核裏面映射一把。

可是,如今由於要從文件裏面讀取數據並寫入這個物理頁面,又不能使用物理地址,

咱們只能使用虛擬地址,這就須要在內核裏面臨時映射一把。臨時映射後,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 文件清理。由於從新加載回內存了,再也不須要 swap 文件了。

四、swapin_readahead 會最終調用 swap_readpage,在這裏,咱們看到了熟悉的readpage 函數,也就是說讀取普通文件和讀取 swap 文件,

過程是同樣的,一樣須要用 kmap_atomickmap_atomic 作臨時映射。

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 頁表的 Cache

爲了提升映射速度,咱們引入了TLB(Translation Lookaside Buffer)咱們常常稱爲快表,專門用來作地址映射的硬件設備。

它不在內存中、可存儲的數據比較少,可是比內存要快。因此,咱們能夠想象,TLB 就是頁表的 Cache,其中存儲了當前最可能被訪問到的頁表項,其內容是部分頁表項的一個副本。

三、有了 TLB 以後,地址映射的過程就像圖中畫的

一、咱們先查塊表,塊表中有映射關係,而後直接轉換爲物理地址。
二、若是在 TLB 查不到映射關係時,纔會到內存中查詢頁表。

總結時刻

用戶態的內存映射機制,咱們解析的差很少了,咱們來總結一下,用戶態的內存映射機制包含如下幾個部分
用戶態內存映射函數 mmap,包括用它來作匿名映射和文件映射...
用戶態的頁表結構,存儲位置在 mm_struct 中。
在用戶態訪問沒有映射的內存會引起缺頁異常,分配物理頁表,補齊頁表。若是是匿名映射則
分配物理內存;若是是 swap,則將 swap 文件讀入;若是是文件映射,則將文件讀入

相關文章
相關標籤/搜索