Linux内存管理(四)用户态内存映射

Linux内存管理

Linux内存管理(一)Linux进程空间管理

Linux内存管理(二)物理内存管理(上)

Linux内存管理(三)物理内存管理(下)

Linux内存管理(四)用户态内存映射

Linux内存管理(五)内核态内存映射

Linux内存管理(四)用户态内存映射

文章目录

  • Linux内存管理(四)用户态内存映射
    • 一、mmap的原理
    • 二、用户态缺页异常
    • 三、总结

前面讲解了虚拟地址空间是如何组织的,以及物理页是如何管理的。这篇文章将讲解这两者之间是如何映射起来的

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

内存映射不仅仅是物理内存和虚拟内存之间的映射,还包括将文件中的内容映射到虚拟内存空间。这个时候,访问内存空间就能访问到文件里的内容。而仅有物理内存和虚拟内存的映射是一种特殊情况

Linux内存管理(四)用户态内存映射_第1张图片

对于 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,这是页表相关的概念

Linux内存管理(四)用户态内存映射_第2张图片

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,其中存储着当前最可能被访问到的页表项,其内容是部分页表的一个副本

Linux内存管理(四)用户态内存映射_第3张图片

有了 TLB 后,每次内存访问都先查快表,如果快表中没有缓存,那么就查询页表

三、总结

  • 用户态内存映射函数 mmap,它用来做匿名映射还有文件映射
  • 用户态的最顶级页目录项,存储在 mm_struct 中
  • 在用户态访问没有映射的地址时,会产生缺页异常,分配物理页表,补齐页表。如果时匿名映射则分配物理内存;如果时swap,则将 swap 文件读入物理内存;如果时文件,则分配物理内存,文件内容读入到物理内存中

Linux内存管理(四)用户态内存映射_第4张图片

你可能感兴趣的:(Linux内核)