Linux进程虚拟内存

到现在为止我们仍然在讨论Linux内存,最开始内存是一块荒漠,后来我们把Linux这架星际火箭着陆到这块荒漠,并开始对这块荒漠进行地址编排规划,这个规划粒度是细的,是以比特位为基础的划分,每个比特位都对应一个地址。有了这个基础的地址编排,我们开始做减法和抽象,我们以地址为基础进行分段分页,这块荒漠慢慢变得规整了,每个地址都注册在案了,相当于国家人口普查一样。紧接着我们从功能区为出发点将这块规整过的荒漠划分,并创造了空间,节点,域的概念来描述这些功能区,并与内存页进行了映射,把页和区着两个维度联系在一起了。再后来我们组织了基建队(内存分配系统)来供给政府大楼(内核)施工。现在轮到公民住宅区,这便是进程虚拟内存了。这里我们暂不考虑进程的种种细节,仅着眼于用户进程的内存。用户进程的虚拟地址空间向每个运行进程提供了同样的系统视图,使得多个进程同时运行时,与各进程相关的内存的内容不会相互干扰。这使得用户空间的内存管理比内核空间复杂。

一.进程地址空间

从用户的角度来看,进程地址空间是一个平坦的线性地址空间,但从内核的角度来看却大不一样。地址空间分为两个部分:一个是随上下文切换而改变的用户空间部分,一个是保持不变的内核空间部分。PAGE_OFFSET为两者的分界点。虚拟内存的一个主要好处就是可以让每个进程都有属于自己的虚拟地址空间,这种空间通过操作系统映射到物理内存。内核在处理用户地址空间和内核空间上有着很大的不同,内核空间的分配不管此时CPU上运行何种进程,都可以很快得到满足,它对整个系统来说是全局性的。对于进程,它通过一个页表项指针指向一个只读的全局全零的页面来实现进程的线性地址空间。一旦进程对该页面进行写操作,就会发生缺页中断。这时系统会分配一个新的全零的页面,并由一个页表项指定,且标记为可写。

在这里插入图片描述
mm_struct代表进程的虚拟内存空间,通常用户空间载入一段程序运行会创建一个进程,与此进程相关的内存由mm_struct来管理,包括程序使用的代码,程序的堆栈,变量。start_code,end_code表示可执行代码占用虚拟空间的起始结束地址。start_data,end_data代表已初始化数据段起始结束地址。start_brk, brk代表当前堆的起始结束地址,start_stack为栈起始地址。arg_start, arg_end, env_start, env_end分别表示参数列表和环境变量位置。mmap_base表示虚拟地址空间映射起始地址。

inux_kernel/include/linux/mm_types.h

struct mm_struct {
	struct {
		struct vm_area_struct *mmap; //虚拟内存区连表
		struct rb_root mm_rb; //虚拟内存红黑树
		u64 vmacache_seqnum;                   /* per-thread vmacache */
		......
		unsigned long mmap_base;//虚拟空间中用于内存映射的起始地址
		unsigned long mmap_legacy_base;	/* base of mmap area in bottom-up allocations */
		......
		unsigned long task_size;//虚拟空间大小,通常是TASK_SIZE
		unsigned long highest_vm_end;	/* highest vma end address */
		pgd_t * pgd; //每个进程虚拟内存空间对应一个全局页目录
		atomic_t mm_count; //引用计数
		int map_count;//当前进程空间的映射内存区数量
		unsigned long total_vm;	//进程空间映射的内存页数
		unsigned long start_code, end_code, start_data, end_data; //代码段,数据段
		unsigned long start_brk, brk, start_stack; //栈区,未初始化区
		unsigned long arg_start, arg_end, env_start, env_end; //参数和环境变量位置
		struct linux_binfmt *binfmt; //Linux进程程序格式
		mm_context_t context; //架构相关的上下文
		struct core_state *core_state; //状态
		......
		} __randomize_layout;
	unsigned long cpu_bitmap[];
};

Linux通过load_elf_binary载入一个elf格式的二进制程序后会将ELF各段解析映射到进程内存空间中。
linux_kernel/fs/binfmt_elf.c

static int load_elf_binary(struct linux_binprm *bprm)
{
	......
	elf_bss = 0;
	elf_brk = 0;

	start_code = ~0UL;
	end_code = 0;
	start_data = 0;
	end_data = 0;

	//通过循将elf个段映射到进程地址空间
	for(i = 0, elf_ppnt = elf_phdata;
	    i < loc->elf_ex.e_phnum; i++, elf_ppnt++) {
		int elf_prot, elf_flags;
		unsigned long k, vaddr;
		unsigned long total_size = 0;
		//映射
		error = elf_map(bprm->file, load_bias + vaddr, elf_ppnt,
				elf_prot, elf_flags, total_size);
		......

	}

}

二.内存映射

由于所有用户进程的总的虚拟地址空间比可用的物理内存大得多,因此只有最常用的部分才与物理页帧关联。因为大多数程序只占用实际可用内存的一小部分,所以可以允许运行多个进程。考虑一个文本编辑应用程序,用户通常只关注文件结尾处,虽然整个文件映射到内存中,但实际只使用了几个页帧来存储文件尾的数据即可。文件的其他数据,内核只需在地址空间保存文件数据在磁盘上的位置,待编辑相应数据时再为其分配相应的内存页。代码段也类似,始终需要的只是其中一部分。为此内核必须提供数据结构,以建立虚拟地址空间的区域到相关元数据所在位置之间的关联。

在这里插入图片描述
此外内核通过address_space结构,提供一组方法从后备存储器(文件系统之类)读取数据。address_space形成了一个辅助层,将映射的数据表示为连续的区域。

在这里插入图片描述
当用户进程访问虚拟地址空间中的一个地址时,通过页表会发送一个缺页异常到内核,内核检查缺页区域所属进程的虚拟地址空间数据结构,找到适当的后备存储器,分配物理内存页并从后备存储器提取数据填充物理页,最后将新分配的物理页通过页表并入进程虚拟地址空间,恢复应用执行。

这相当于账本与库存的关系,虚拟地址空间是账面数据,硬盘上的文件是实际仓库的货,实际物理内存相当于搬运车。当有人来仓库提货时,通过仓库前台查帐本,如果想要提的货刚好在搬运车上,那么直接从搬运车上把货提走,如果不在则产生缺货异常,那么这个时候需要派人去仓库提货放到搬运车上,同时更新账本数据,再把货提走。

三.虚拟内存域

用户虚拟地址空间的每个区域由vm_area_struct的开始结束地址描述,现存的区域按起始地址以递增次序归入链表,通过扫描区域链表找到与地址相关的区域是耗时的,内核通过红黑树来管理这些区域来提高效率查找效率。
在这里插入图片描述
inux_kernel/include/linux/mm_types.h
vm_area_struct表示一块虚拟内存区域

struct vm_area_struct {
	unsigned long vm_start;//映射起始
	unsigned long vm_end;//映射结束地址
	struct vm_area_struct *vm_next, *vm_prev; //映射区双向链表
	struct rb_node vm_rb; //红黑树
	struct mm_struct *vm_mm;//指向所属进程内存空间
	pgprot_t vm_page_prot;//权限
	unsigned long vm_flags;		/* Flags, see mm.h. */
	......
	struct list_head anon_vma_chain; 
	struct anon_vma *anon_vma;//管理匿名映射的共享页,指向相同页的映射都保存在一个双链表上
	const struct vm_operations_struct *vm_ops; //虚拟内存区域操作函数
	unsigned long vm_pgoff; //偏移
	struct file * vm_file;//指向被映射的文件
	......

} __randomize_layout;

vm_operations_struct提供了一组函数来操作虚拟内存域
inux_kernel/include/linux/mm.h

struct vm_operations_struct {
	void (*open)(struct vm_area_struct * area);
	void (*close)(struct vm_area_struct * area);
	int (*split)(struct vm_area_struct * area, unsigned long addr);
	int (*mremap)(struct vm_area_struct * area);
	vm_fault_t (*fault)(struct vm_fault *vmf);
	......
}

内核每个打开的文件都用struct file表示,结构中的i_mapping指向struct address_space,address_space通过优先查找树建立文件区间与文件映射的进程地址空间进行关联。address_space在文件系统空间与进程虚拟地址空间之间充当了桥梁作用。
inux_kernel/include/linux/fs.h

struct address_space {
	struct inode		*host; 
	...
	struct rb_root_cached	i_mmap;
	const struct address_space_operations *a_ops;
	...
} __attribute__((aligned(sizeof(long)))) __randomize_layout;

内核为address_space 提供了address_space_operations结构来进行空间映射操作,以及与后备设备操作,页回写,页换出换入操作,当然因为涉及到文件系统我们在这里暂不讨论,等后续梳理了文件系统我们再来看后被缓冲区,页缓存等概念和内容。

inux_kernel/include/linux/fs.h

struct address_space_operations {
	int (*writepage)(struct page *page, struct writeback_control *wbc);
	int (*readpage)(struct file *, struct page *);
	....
}

在这里插入图片描述

1.映射

linux_kernel/mm/mmap.c
内存映射

unsigned long do_mmap(struct file *file, unsigned long addr,
			unsigned long len, unsigned long prot,
			unsigned long flags, vm_flags_t vm_flags,
			unsigned long pgoff, unsigned long *populate,
			struct list_head *uf)
{
	struct mm_struct *mm = current->mm;
	int pkey = 0;

	*populate = 0;
	//在进程地址空间找一块未映射的区域
	addr = get_unmapped_area(file, addr, len, pgoff, flags);
	if (offset_in_page(addr))
		return addr;
	//映射区域
	addr = mmap_region(file, addr, len, vm_flags, pgoff, uf);
	return addr;
}

linux_kernel/mm/mmap.c
虚拟内存域分配

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;
	int error;
	struct rb_node **rb_link, *rb_parent;
	unsigned long charged = 0;

    ......
    //尝试合并
	vma = vma_merge(mm, prev, addr, addr + len, vm_flags,
			NULL, file, pgoff, NULL, NULL_VM_UFFD_CTX);
	if (vma)
		goto out;
    //从slub创建一个新的vm_area_struct
	vma = vm_area_alloc(mm);
	if (!vma) {
		error = -ENOMEM;
		goto unacct_error;
	}

	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;

	if (file) {
	 ......
	} else if (vm_flags & VM_SHARED) {
		error = shmem_zero_setup(vma);
		if (error)
			goto free_vma;
	} else {
		vma_set_anonymous(vma);
	}

   //加入虚拟内存域链表
	vma_link(mm, vma, prev, rb_link, rb_parent);
	//关联文件
	file = vma->vm_file;
out:
    ......
	return addr;

unmap_and_free_vma:
	....
	//失败处理
free_vma:
	vm_area_free(vma);
unacct_error:
	if (charged)
		vm_unacct_memory(charged);
	return error;
}

2.解除映射

linux_kernel/mm/mmap.c

int do_munmap(struct mm_struct *mm, unsigned long start, size_t len,
	      struct list_head *uf)
{
	return __do_munmap(mm, start, len, uf, false);
}

linux_kernel/mm/mmap.c

 */
int __do_munmap(struct mm_struct *mm, unsigned long start, size_t len,
		struct list_head *uf, bool downgrade)
{
	unsigned long end;
	struct vm_area_struct *vma, *prev, *last;
	len = PAGE_ALIGN(len);
	end = start + len;
	arch_unmap(mm, start, end);
	//找到映射区域
	vma = find_vma(mm, start);
	vma = prev ? prev->vm_next : mm->mmap;

	unmap_region(mm, vma, prev, start, end);
	//从链表移除
	remove_vma_list(mm, vma);

	return downgrade ? 1 : 0;
}

linux_kernel/mm/mmap.c
释放映射区,释放占用的内存

static void unmap_region(struct mm_struct *mm,
		struct vm_area_struct *vma, struct vm_area_struct *prev,
		unsigned long start, unsigned long end)
{
	struct vm_area_struct *next = prev ? prev->vm_next : mm->mmap;
	struct mmu_gather tlb;

	lru_add_drain();
	tlb_gather_mmu(&tlb, mm, start, end);
	update_hiwater_rss(mm);
	unmap_vmas(&tlb, vma, start, end);
	free_pgtables(&tlb, vma, prev ? prev->vm_end : FIRST_USER_ADDRESS,
				 next ? next->vm_start : USER_PGTABLES_CEILING);
	tlb_finish_mmu(&tlb, start, end);
}

四.缺页异常

在实际需要某个虚拟内存区域的数据之前,虚拟和物理内存之间的关联不会建立。如果进程访问的虚拟地址空间部分尚未与页帧关联,处理器自动引发一个缺页异常,内核截获到此异常后去分配物理页并关联到进程虚拟空间相关页表,然后通过address_space 去后备存储设备提取数据填充到物理内存中。

linux_kernel/arch/x86/mm/fault.c

static noinline void
__do_page_fault(struct pt_regs *regs, unsigned long hw_error_code,
		unsigned long address)
{
	prefetchw(&current->mm->mmap_sem);

	if (unlikely(kmmio_fault(regs, address)))
		return;

	/* Was the fault on kernel-controlled part of the address space? */
	if (unlikely(fault_in_kernel_space(address)))
		do_kern_addr_fault(regs, hw_error_code, address);
	else
		do_user_addr_fault(regs, hw_error_code, address);
}

linux_kernel/arch/x86/mm/fault.c

static inline
void do_user_addr_fault(struct pt_regs *regs,
			unsigned long hw_error_code,
			unsigned long address)
{
	struct vm_area_struct *vma;
	struct task_struct *tsk;
	struct mm_struct *mm;
	vm_fault_t fault, major = 0;
	unsigned int flags = FAULT_FLAG_ALLOW_RETRY | FAULT_FLAG_KILLABLE;

	tsk = current;
	mm = tsk->mm;

	vma = find_vma(mm, address);
	if (unlikely(!vma)) {
		bad_area(regs, hw_error_code, address);
		return;
	} 
	if (likely(vma->vm_start <= address))  //没有越界
		goto good_area;
	if (unlikely(!(vma->vm_flags & VM_GROWSDOWN))) {
		bad_area(regs, hw_error_code, address);
		return;
	}
	if (unlikely(expand_stack(vma, address))) {//不在栈区
		bad_area(regs, hw_error_code, address);
		return;
	}

	/*
	 * Ok, we have a good vm_area for this memory access, so
	 * we can handle it..
	 */
go
	fault = handle_mm_fault(vma, address, flags);
	......
}
NOKPROBE_SYMBOL(do_user_addr_fault);

linux_kernel/mm/memory.c
分配物理内存并关联页表

static vm_fault_t __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),
	};
	unsigned int dirty = flags & FAULT_FLAG_WRITE;
	struct mm_struct *mm = vma->vm_mm;
	pgd_t *pgd;
	p4d_t *p4d;
	vm_fault_t ret;

	pgd = pgd_offset(mm, address);
	p4d = p4d_alloc(mm, pgd, address);
	if (!p4d)
		return VM_FAULT_OOM;
	vmf.pud = pud_alloc(mm, p4d, address);
	......
	vmf.pmd = pmd_alloc(mm, vmf.pud, address);
	.......
	return handle_pte_fault(&vmf);
}

inux_kernel/mm/memory.c
从后被存储区调数据填充内存

static vm_fault_t handle_pte_fault(struct vm_fault *vmf)
{
	pte_t entry;
	......
	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); //换页

	if (pte_protnone(vmf->orig_pte) && vma_is_accessible(vmf->vma))
		return do_numa_page(vmf);

	vmf->ptl = pte_lockptr(vmf->vma->vm_mm, vmf->pmd);
	spin_lock(vmf->ptl);
	entry = vmf->orig_pte;
	if (unlikely(!pte_same(*vmf->pte, entry)))
		goto unlock;
	if (vmf->flags & FAULT_FLAG_WRITE) {
		if (!pte_write(entry))
			return do_wp_page(vmf);
		entry = pte_mkdirty(entry);
	}
	......
}

好了,我们了解了地址空间的通用结构以及内核地址空间的管理方式。关于进程虚拟内存暂且到这里,到现在位置我们已经了解了内存管理相关的大部分必要的内容,始终地,内存管理是内核基础中的基础,一切都与内存相关。接下来我们将探索Linux文件系统,因为内存管理最后一块拼图缓存和换页与文件系统密切相关,此外文件系统统一了外设的访问方式,当然网络没有归纳到文件系统的分类方案中,选择文件系统下一个探究的对象,一来是为了完善内存管理的最后一块拼图,而来为探究外设驱动做准备,因为除了网络外的外设可以归入文件系统的分类方案中。我们把进程和网络放到最后面去探究,因为进程依赖于内存,文件,等基础,而网络属于独立的分类。这样的路线有个好处就是不容易混乱,能够一步一步构建必要的备用的知识,使得后续的内容理解难度降低一些。OK,下一篇我们探究文件系统。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 3
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值