进程虚拟内存

管理用户虚拟地址空间的方法比内核地址空间复杂一些:

  1. 每个应用程序都有自己的地址空间,与所有其他应用程序分开;
  2. 地址空间巨大,只有很少的段可用于各个用户空间进程,这些段之间有一定的距离,内核需要有效的管理这些分配的段;
  3. 内核无法信任用户程序,必须做检查;
  4. 地址空间只有极少的一部分和物理页帧直接关联,不经常使用的部分只有在必要时与页帧关联。
进程由不同长度的段组成用于不同的目的:当前运行代码的二进制代码段、程序使用的动态库的代码、全局变量和动态产生的数据的堆、用于保存局部变量和实现函数调用的栈、环境变量和命令行参数、文件内容。系统中的每个进程都用一个mm_struct结构来保存内存管理信息:
struct mm_struct {
struct vm_area_struct * mmap;
struct rb_root mm_rb;
struct vm_area_struct * mmap_cache;
unsigned
long (*get_unmapped_area) (struct file *filp, unsigned long addr, unsigned long len, unsigned long pgoff, unsigned long flags);
void (*unmap_area) (struct mm_struct *mm, unsigned long addr);
unsigned
long mmap_base;
unsigned
long task_size;
unsigned
long cached_hole_size;
unsigned
long free_area_cache;
pgd_t
* pgd;
atomic_t mm_users;
atomic_t mm_count;
int map_count;
struct rw_semaphore mmap_sem;
spinlock_t page_table_lock;
struct list_head mmlist;
mm_counter_t _file_rss;
mm_counter_t _anon_rss;
unsigned
long hiwater_rss;
unsigned
long hiwater_vm;
unsigned
long total_vm, locked_vm, shared_vm, exec_vm;
unsigned
long stack_vm, reserved_vm, def_flags, nr_ptes;
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;
unsigned
long saved_auxv[AT_VECTOR_SIZE];
cpumask_t cpu_vm_mask;
mm_context_t context;
struct list_head delayed_drop;
unsigned
int faultstamp;
unsigned
int token_priority;
unsigned
int last_interval;
unsigned
long flags;
int core_waiters;
struct completion *core_startup_done, core_done;
rwlock_t ioctx_list_lock;
struct kioctx *ioctx_list;
#ifdef CONFIG_CGROUP_MEM_RES_CTLR
struct mem_cgroup *mem_cgroup;
#endif
};

    其实想想用户进程空间中有什么样的数据就需要什么样的数据来管理。用于内存映射的区域起始于mm_struct->mmap_base,一般值为TASK_SIZE/3。虚拟地址空间从0到0xC0000000,每个用户空间都有3GB可用,TASK_UNMAPPED_BASE起始于0x40000000,这意味着堆只有1GB空间可使用,继续增长则会进入mmap区域。新的布局中用固定值限制栈的最大长度,因此在安置内存映射的区域可以在栈末端的下方开始就可以了,因此mmap区域和堆可以相对扩展,直至耗尽虚拟地址空间中剩余的区域,为确保栈与mmap区域不发生冲突,两者之间设置一个安全隙。

    在使用load_elf_binary装载一个二进制文件时,将创建进程的地址空间,选择布局的工作由arch_pick_mmap_layout完成。可以根据栈的最大长度来计算栈最低可能的位置作为mmap区域的起始点。但内核至少跨越128MB的空间。如果要求使用地址空间随机话机制,上述位置会减去一个随机的偏移量,最大为1MB。下面来看内存映射的原理:文件数据在硬盘上时不连续的,而是分布在若干小的区域,内核利用address_space数据结构提供一种方法从后备存储器读取数据,因为address_space形成一个辅助层,将映射的数据表示为连续的线性区域,提供给内存管理子系统。从后备存储器通过address_space到虚拟地址空间,然后通过页表从虚拟地址空间到物理页帧。进程试图访问用户空间中的一个内存地址,但使用页表无法确定物理地址,处理器接下来触发一个缺页异常发送到内核,然后内核检查负责缺页区域的进程地址空间数据结构,找到适合的后背存储器,分配物理页帧并从后背存储器读取所需数据填充。借助于页表将物理内存页并入到用户进程的地址空间,应用程序恢复执行。

进程中的每个虚拟区域表示为一个vm_struct_area的一个实例,这个应该是数据段、代码段这些吧,该结构如下:

struct vm_area_struct {
struct mm_struct * vm_mm;
unsigned
long vm_start;
unsigned
long vm_end;
struct vm_area_struct *vm_next;
pgprot_t vm_page_prot;
unsigned
long vm_flags;
struct rb_node vm_rb;
union {
struct {
struct list_head list;
void *parent;
struct vm_area_struct *head;
} vm_set;
struct raw_prio_tree_node prio_tree_node;
} shared;
struct list_head anon_vma_node;
struct anon_vma *anon_vma;
struct vm_operations_struct * vm_ops;
unsigned
long vm_pgoff;
struct file * vm_file;
void * vm_private_data;
unsigned
long vm_truncate_count;
#ifndef CONFIG_MMU
atomic_t vm_usage;
#endif
#ifdef CONFIG_NUMA
struct mempolicy *vm_policy;
#endif
};

其中需要注意看的一个是“shared”,对于有地址空间和后背存储器的区域来说,shared连接到address_space->i_mmap优先树,或者链接到悬挂在优先树节点之外、类似的一组虚拟内存区域的链表中,或连接到address_space->i_mmap_nonlinear链表中的虚拟内存区域。

下面看一下和vma相关的函数:

  1. struct vm_area_struct * find_vma(struct mm_struct * mm, unsigned long addr)函数用来查找地址空间中结束地址在给定地址之后的第一个区域,其中的具体的操作很容易理解,具体的做法中用mmap_cache来加速;
  2. static inline struct vm_area_struct * find_vma_intersection(struct mm_struct * mm, unsigned long start_addr, unsigned long end_addr)函数用来确认边界为start_addr和end_addr的区间是否完全包含一个现存的区域内部,先用函数(1)找到然后判断就可以了;
  3. struct vm_area_struct *vma_merge(struct mm_struct *mm, struct vm_area_struct *prev, unsigned long addr, unsigned long end, unsigned long vm_flags, struct anon_vma *anon_vma, struct file *file, pgoff_t pgoff, struct mempolicy *policy)函数在可能的情况下讲一个新区域与周边区域合并;
  4. int insert_vm_struct(struct mm_struct * mm, struct vm_area_struct * vma)函数用于插入新区域,其实也就是收集信息然后将vma结构插入到相应的结构中;
  5. 将vma插入对应数据结构通过函数static void __vma_link(struct mm_struct *mm, struct vm_area_struct *vma, struct vm_area_struct *prev, struct rb_node **rb_link, struct rb_node *rb_parent)来实现;
  6. 在向数据结构插入新的内存区域之前,内核必须确认虚拟地址空间中是否有足够的空闲空间可用于给定长度的区域,这些由函数unsigned long get_unmapped_area(struct file *file, unsigned long addr, unsigned long len, unsigned long pgoff, unsigned long flags)来完成;
    文件的内存映射可以认为是两个不同地址空间之间的映射,以简化程序员的工作。一个地址空间是用户进程的虚拟地址空间,另一个是文件系统所在的地址空间。每一个文件映射都有相关的address_space实例。

    下面就开始看内核是怎样映射到vma的这个过程:Linux内核中有两个系统调用sys_mmap和sys_mmap2,两个函数的区别仅在于off参数的意义。它们两个最终会调用到函数do_mmap_pgoff,这是一个体系结构无关的函数(貌似我现在只对体系结构无关的函数感兴趣,或者是对体系相关函数的畏惧)。下面就来看这个函数具体的实现:

unsigned long do_mmap_pgoff(struct file * file, unsigned long addr, unsigned long len, unsigned long prot, unsigned long flags, unsigned long pgoff)

其中参数prot可以是PROT_EXEC、PROT_READ、PROT_WRITE、PROT_NONE值的组合值。下面开始看函数的过程,首先是检查传进来的参数,其中包括判断“是不是可执行的?”,如果flags中设置了“MAP_FIXED”就不能将其他地址用于映射(只能用给定的地址)。

addr = get_unmapped_area(file, addr, len, pgoff, flags);

这样就在虚拟地址空间中找到了一个区域用于映射,应用程序可以对映射指定固定的地址、建议一个地址或由内核选择地址。

vm_flags = calc_vm_prot_bits(prot) | calc_vm_flag_bits(flags) | mm->def_flags | VM_MAYREAD | VM_MAYWRITE | VM_MAYEXEC;

将系统调用中指定的标志和访问权限常数合并在一起在后续操作中容易处理一点。

return mmap_region(file, addr, len, flags, vm_flags, pgoff, accountable);

检查完标志之后就把工作交给mmap_region来处理,其实do_mmap_pgoff函数主要还是和标志这些东西打交道,下面来看真正的实现过程:

unsigned long mmap_region(struct file *file, unsigned long addr, unsigned long len, unsigned long flags, unsigned int vm_flags, unsigned long pgoff, int accountable)

这个就是这个函数的定义了,

vma = find_vma_prepare(mm, addr, &prev, &rb_link, &rb_parent);

先找到前一个和后一个vma的实例,

        if (vma && vma->vm_start < addr + len) {
if (do_munmap(mm, addr, len))
return -ENOMEM;
goto munmap_back;
}

如果在指定的映射位置已经存在一个映射则通过do_munmap删除它,

if (!may_expand_vm(mm, len >> PAGE_SHIFT)) return -ENOMEM;

检查有没有加上这个区域有没有违反limit,其实也就是看有没有足够的空间来存放这个啦(从对进程资源限制的角度来看),

if (security_vm_enough_memory(charged)) return -ENOMEM;

会调用vm_enough_memory来选择是否分配操作所需的内存,

if (!file && !(vm_flags & VM_SHARED) && vma_merge(mm, prev, addr, addr + len, vm_flags,NULL, NULL, pgoff, NULL))
goto out;

判断是否可以仅仅通过扩展一个匿名映射来达到目的,

vma = kmem_cache_zalloc(vm_area_cachep, GFP_KERNEL);
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;

如果不行就分配一个vma实例并插入到对应的结构中、

error = file->f_op->mmap(file, vma);

用文件的函数创建映射,在这个过程中变量“addr”可能发生变化,是因为几个不同的驱动程序可能同时操作(表示不懂)?

return addr;

返回地址,在这个过程中进行了统计、广泛地安全性和合理性检查,这个函数就看到这里了。看完了创建映射的过程,解除映射的过程也就很容易理解了。在上面的过程中将文件中一个连续的部分映射到虚拟内存中同样连续的部分。在需要将文件的不同部分以不同的顺序映射到虚拟内存中需要几个vma,这样消耗太大了,一种替代方法是使用非线性映射,内核为此提供了一个独立的系统调用sys_remap_file_pages。

    该系统调用允许重排映射中的页,使得内存与文件中的顺序不再等价。实现该特性无需移动内存中的数据,而是通过操作进程的页表实现的。在换出非线性映射时内核必须确保再次换入时任然要保持原来的偏移量,因此把所需的信息存储在换出页的页表项中,再次换入时必须参考相应的信息。所有简历的非线性映射的vma保存在一个链表中,表头是address_space中的i_mmap_nonlinear成员。所述区域对应的页表项使用一些特殊的项填充,这些页表项看起来像是对应于不存在的页,但其中包含附加信息,将其标识为非线性映射的页表项。在访问此类页表项描述的页时会产生一个缺页异常并读入正确的页。

    内核利用上面的数据结构可以简历虚拟地址空间和物理地址之间的联系(通过页表),以及进城的一个内存区域与其虚拟内存页地址之间的关联。仍然缺失的一个联系是物理内存页和该页所属进程之间的联系。在换出页时更新所涉及的进程。在映射一页时,它关联到一个进程,但不一定处于使用中。对页的引用次数表明页使用的活跃程度。这里用到的数据结构就是page:

struct page {
unsigned
long flags;
atomic_t _count;
union {
atomic_t _mapcount;
unsigned
int inuse;
};
union {
struct {
unsigned
long private;
struct address_space *mapping;
};
#if NR_CPUS >= CONFIG_SPLIT_PTLOCK_CPUS
spinlock_t ptl;
#endif
struct kmem_cache *slab;
struct page *first_page;
};
union {
pgoff_t index;
void *freelist;
};
struct list_head lru;
#if defined(WANT_PAGE_VIRTUAL)
void *virtual;
#endif
#ifdef CONFIG_CGROUP_MEM_RES_CTLR
unsigned
long page_cgroup;
#endif
};

那么如何从给定的page结构找到所有映射了该物理内存页的位置?这还需要其他两个数据结构发挥作用,一是优先查找树嵌入属于非匿名映射的每个区域,二是指向内存中同一页匿名区域的链表。内核在实现逆向映射时采用的技巧是不直接保存页和相关使用者之间的联系,而是保存页和页所在区域之间的关联。包含该页的所有其他区域都可以通过刚才提到的数据结构找到。

    将匿名页插入到匿名映射数据结构中有两种方法,对新的匿名页必须调用page_add_new_anon_rmap,已经有引用计数的页则使用page_add_anon_rmap。最后都是由__page_set_anon_rmap来实现:

static void __page_set_anon_rmap(struct page *page, struct vm_area_struct *vma, unsigned long address)
{
struct anon_vma *anon_vma = vma->anon_vma;
BUG_ON(
!anon_vma);
anon_vma
= (void *) anon_vma + PAGE_MAPPING_ANON;
page
->mapping = (struct address_space *) anon_vma;
page
->index = linear_page_index(vma, address);
__inc_zone_page_state(page, NR_ANON_PAGES);
}

如何使用匿名映射?显然这个在释放内存页的时候需要,所以还是在以后详细地介绍这个。

    下面看如何管理堆,堆是进程中用于动态分配变量和数据的区域,堆的管理对应用程序员不是直接可见的。malloc与内核之间的经典接口是brk系统调用,下面开始详细地看这个函数:

asmlinkage unsigned long sys_brk(unsigned long brk)

参数brk指定了堆在虚拟地址空间中新的结束地址,

rlim = current->signal->rlim[RLIMIT_DATA].rlim_cur;
if (rlim < RLIM_INFINITY && (brk - mm->start_brk) + (mm->end_data - mm->start_data) > rlim)
goto out;

检查资源限制,

newbrk = PAGE_ALIGN(brk);
oldbrk
= PAGE_ALIGN(mm->brk);

将brk对齐到页,判断是否要增加brk的值,

if (brk <= mm->brk) {
if (!do_munmap(mm, newbrk, oldbrk-newbrk))
goto set_brk;
goto out;
}

如果要收缩brk就要调用do_munmap函数,

if (find_vma_intersection(mm, oldbrk, newbrk+PAGE_SIZE))
goto out;

如果堆要扩大,内核首先必须检查新的长度是否超出进程的堆的最大长度限制,然后并检查是否扩大的堆与现存的映射重叠。

if (do_brk(oldbrk, newbrk-oldbrk) != oldbrk)
goto out;

实际的扩展工作由do_brk来做,

unsigned long do_brk(unsigned long addr, unsigned long len)

而这个函数是do_mmap_pgoff简化版本没什么新东西,它在用户地址空间中创建一个匿名映射,但省去了一些安全检查和用于提高代码性能的对特殊情况的处理。

----------------------------

个人理解,欢迎拍砖。

原文地址:https://www.cnblogs.com/ggzwtj/p/2138527.html