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

Linux内存管理

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

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

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

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

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

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

一、用户态和内核态的空间划分

每个进程或者线程都是一个task_struct,它们都有一个mm_struct用于管理内存

struct mm_struct		*mm;

在mm_struct中有这样的一个变量

unsigned long task_size;		/* size of task vm space */

前面说过进程的空间被一分为二,一部分为用户空间,一部分为内核空间,那么这个分割线在哪呢?就是这个task_size定义的

内核里有TASK_SIZE的定义

#ifdef CONFIG_X86_32
/*
 * User space process size: 3GB (default).
 */
#define TASK_SIZE		PAGE_OFFSET
#define TASK_SIZE_MAX		TASK_SIZE
/*
config PAGE_OFFSET
        hex
        default 0xC0000000
        depends on X86_32
*/
#else
/*
 * User space process size. 47bits minus one guard page.
*/
#define TASK_SIZE_MAX	((1UL << 47) - PAGE_SIZE)
#define TASK_SIZE		(test_thread_flag(TIF_ADDR32) ? \
					IA32_PAGE_OFFSET : TASK_SIZE_MAX)
......

当执行一个新进程的时候,会做以下操作

current->mm->task_size = TASK_SIZE;

对于32位,高1G位内核空间,低3G位用户空间

img

二、用户态的布局

用户虚拟空间有几类数据,代码,全局变量,堆,内存映射区,栈。在mm_struct中,有下面这些变量统计着这些信息

unsigned long mmap_base;	/* base of mmap area,内存映射区起始地址 */
unsigned long total_vm;		/* Total pages mapped,总共映射页的数目 */
unsigned long locked_vm;	/* Pages that have PG_mlocked set,被锁定不能换出的页数目 */
unsigned long pinned_vm;	/* Refcount permanently increased,不能换出也不能移动的页数目 */
unsigned long data_vm;		/* VM_WRITE & ~VM_SHARED & ~VM_STACK,存放数据的页数目 */
unsigned long exec_vm;		/* VM_EXEC & ~VM_WRITE & ~VM_STACK,存放可运行程序的页数目 */
unsigned long stack_vm;		/* VM_STACK,栈所占的页数目 */
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; //参数列表和环境变量的位置

img

除了mm_struct之外,还有一个专门的结构体vm_area_struct,来描述这些区域的属性

struct vm_area_struct *mmap;		/* list of VMAs,链表 */
struct rb_root mm_rb; /* 红黑树树根 */

struct vm_area_struct {
	/* The first cache line has the info for VMA tree walking. */
	unsigned long vm_start;		/* Our start address within vm_mm. */
	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;
	struct rb_node vm_rb;
	struct mm_struct *vm_mm;	/* The address space we belong to. */
	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;
	struct file * vm_file;		/* File we map to (can be NULL). */
	void * vm_private_data;		/* was vm_pte (shared mem) */
} __randomize_layout;

vm_start和vm_end指定了区域在用户空间的起始地址和结束地址。vm_next和vm_prev维护一个链表

vm_rb是红黑树节点,用于放置到红黑树中,vm_ops是这个区域的操作

vm_area_struct是如何一上面的区域关联起来的呢?

这个事情是在load_elf_binary中做的,加载内核是它,启动第一个用户态进程init是它,fork完调用exec执行一个程序也是它

当执行exec时,除了解析elf格式之外,还有一件重要的事情就是建立内存映射

static int load_elf_binary(struct linux_binprm *bprm)
{
......
  setup_new_exec(bprm);
......
  retval = setup_arg_pages(bprm, randomize_stack_top(STACK_TOP),
				 executable_stack);
......
  error = elf_map(bprm->file, load_bias + vaddr, elf_ppnt,
				elf_prot, elf_flags, total_size);
......
  retval = set_brk(elf_bss, elf_brk, bss_prot);
......
  elf_entry = load_elf_interp(&loc->interp_elf_ex,
					    interpreter,
					    &interp_map_addr,
					    load_bias, interp_elf_phdata);
......
  current->mm->end_code = end_code;
  current->mm->start_code = start_code;
  current->mm->start_data = start_data;
  current->mm->end_data = end_data;
  current->mm->start_stack = bprm->p;
......
}

load_elf_binary会做下面的事

  • 调用setup_new_exec设置内存映射区mmap_base
  • 调用setup_arg_pages设置栈的vm_area_struct,这里设置mm->arg_start指向栈底
  • elf_map会将elf文件中代码段映射到内存中
  • set_brk设置堆的vm_area_struct,这里设置mm->start_brk=mm->brk,即堆位空
  • load_elf_interp将依赖的so文件映射到内存中来

最终会形成下面形式

img

映射完后,什么情况会修改这些信息?

第一种情况是函数调用,涉及函数栈的改变,主要修改栈顶指针

第二种是调用malloc分配内存,底层要么调用brk,要么调用mmap,关于mmap后面再讲,这里先讨论brk

SYSCALL_DEFINE1(brk, unsigned long, brk)
{
	unsigned long retval;
	unsigned long newbrk, oldbrk;
	struct mm_struct *mm = current->mm;
	struct vm_area_struct *next;
......
    /* 页对齐 */
	newbrk = PAGE_ALIGN(brk);
	oldbrk = PAGE_ALIGN(mm->brk);
	/* 如果两个内存刚好在同一页内 */
    if (oldbrk == newbrk)
		goto set_brk;


	/* Always allow shrinking brk. */
    /* 如果新堆顶小于旧的堆顶,说明要释放内存 */
	if (brk <= mm->brk) {
		if (!do_munmap(mm, newbrk, oldbrk-newbrk, &uf))
			goto set_brk;
		goto out;
	}


	/* Check against existing mmap mappings. */
    /* 在红黑树中找到当前当前堆顶处下一个vm_area_struct */
	next = find_vma(mm, oldbrk);
    /* 如果空闲区域不大于1页,那么就退出 */
	if (next && newbrk + PAGE_SIZE > vm_start_gap(next))
		goto out;


	/* Ok, looks good - let it rip.否咋调用do_brk进行内存分配 */
	if (do_brk(oldbrk, newbrk-oldbrk, &uf) < 0)
		goto out;


/* 设置新的堆顶 */
set_brk:
	mm->brk = brk;
......
	return brk;
out:
	retval = mm->brk;
	return retval

static int do_brk(unsigned long addr, unsigned long len, struct list_head *uf)
{
	return do_brk_flags(addr, len, 0, uf);
}


static int do_brk_flags(unsigned long addr, unsigned long request, unsigned long flags, struct list_head *uf)
{
	struct mm_struct *mm = current->mm;
	struct vm_area_struct *vma, *prev;
	unsigned long len;
	struct rb_node **rb_link, *rb_parent;
	pgoff_t pgoff = addr >> PAGE_SHIFT;
	int error;


	len = PAGE_ALIGN(request);
......
	find_vma_links(mm, addr, addr + len, &prev, &rb_link,
			      &rb_parent);
......
	vma = vma_merge(mm, prev, addr, addr + len, flags,
			NULL, NULL, pgoff, NULL, NULL_VM_UFFD_CTX);
	if (vma)
		goto out;
......
	vma = kmem_cache_zalloc(vm_area_cachep, GFP_KERNEL);
	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);
out:
	perf_event_mmap(vma);
	mm->total_vm += len >> PAGE_SHIFT;
	mm->data_vm += len >> PAGE_SHIFT;
	if (flags & VM_LOCKED)
		mm->locked_vm += (len >> PAGE_SHIFT);
	vma->vm_flags |= VM_SOFTDIRTY;
	return 0;

find_vma_links找到将来的vm_area_struct节点在红黑树的位置,找到它的父节点、前序节点。接下来就是调用vma_merge,看新节点是否能够和现有树中的节点合并。如果地址是连着的,能够合并,不需要创建新的vm_area_struct。否则创建新的vm_area_struct,添加到链表中,也加到红黑树中

三、内核态的布局

32位的内核态的布局

img

32位的内核空间就1G,占据绝大部分的896G,称为直接映射区

所谓直接映射区,就是和物理内存的映射关系非常简单,它直接映射物理的内存的前896M,就是内存地址减去3G,得到物理地址

内核中有两个宏

#define __va(x)			((void *)((unsigned long)(x)+PAGE_OFFSET))
#define __pa(x)		__phys_addr((unsigned long)(x))
#define __phys_addr(x)		__phys_addr_nodebug(x)
#define __phys_addr_nodebug(x)	((x) - PAGE_OFFSET)

即使这里映射非常简单,但是依然需要建立页表

在涉及内核栈的分配,内核的进程管理会讲内核栈放到3G-3G+896M地址中,当然存放在物理内存的前896M中,相应的页表也会被创建

896M这个被定义为高端内存,高端内存是物理内存的概念,是内核管理模块看待物理内存的概念

内核空间剩余的空间可以分为以下几类

  • VMALLOC_START和VMALLOC_END之间称为内核的动态映射区,内核可以也可以通过vmlloac申请内存,这块区域可以映射物理内存中任何的位置
  • PKMAP_BASE到FIXADDR_START的空间称为持久内核映射。使用alloc_pages()在物理内存的高端内存分配物理页,可以使用kmap将其映射到此区域
  • FIXADDR_START到FIXADDR_TOP的空间称为固定映射区,主要用于满足特殊需求

64位的地址分布如下

img

下面两幅图总结

img

img

发布了107 篇原创文章 · 获赞 197 · 访问量 8万+

猜你喜欢

转载自blog.csdn.net/weixin_42462202/article/details/102511182