4进程虚拟内存

4进程虚拟内存

在这里插入图片描述
在这里插入图片描述

进程虚拟地址空间

进程的虚拟地址空间范围为 0 - (TASK_SIZE-1),之后为内核地址空间

在这里插入图片描述

进程内存空间的布局

虚拟地址空间中包含了若干区域。其分布方式是特定于体系结构的,但所有方法都有下列共同成分:

  • text段:对应ELF文件中text段,存放程序的代码部分。
  • data段:对应ELF文件的data段,存放初始化后的全局变量、初始化后的局部静态变量。
  • bss段:对应ELF文件的bss段,存放未初始化的全局变量、未初始化的局部静态变量。
  • Heap:堆,存放动态分配的内存,比如malloc函数、new关键字等申请的内存,向地址增大的方向增长,可读可写可执行
  • Memory Maping段:存放mmap映射内容以及动态链接库内容。
  • Stack:栈,存放局部变量,函数参数,当前状态,函数调用信息等,向地址减小的方向增长,可读可写可执行
  • 命令行参数和环境变量段
  • 内核空间:属于操作系统的空间,进程不能直接方案这段地址,只能通过系统调用跟内核交互。
//include/linux/mm_types.h
struct mm_struct {/*内存描述符*/
    ...
    unsigned long (*get_unmapped_area) (struct file *filp,
    unsigned long addr, unsigned long len,
    unsigned long pgoff, unsigned long flags);
    ...
    unsigned long mmap_base;		/* base of mmap area *///虚拟地址空间中用于内存映射的起始地址,通常设置为TASK_UNMAPPED_BASE,每个体系结构都需要定义。几乎所有的情况下,其值都是TASK_SIZE/3
	unsigned long task_size;		/* size of task vm space *///存储了对应进程的地址空间长度,32位系统该值通常是 TASK_SIZE
    ...
    //start_code, end_code:可执行代码占用的虚拟地址空间区域,其开始和结束,在ELF二进制文件映射到地址空间中之后,这些区域的长度不再改变
	//start_data, end_data:包含已初始化数据的区域,其开始和结束,在ELF二进制文件映射到地址空间中之后,这些区域的长度不再改变
	unsigned long start_code, end_code, start_data, end_data;
	//start_brk, brk:堆的起始地址和结束地址,尽管堆的起始地址在进程生命周期中是不变的,但堆的长度会发生变化,因而brk的值也会变
	unsigned long start_brk, brk, start_stack;
	//arg_start, arg_end:参数列表的位置,两个区域都位于栈中最高的区域
	//env_start, env_end:环境变量的位置,两个区域都位于栈中最高的区域
	unsigned long arg_start, arg_end, env_start, env_end;
    ...
}

各个体系结构可以通过几个配置选项影响虚拟地址空间的布局:

  1. 如果体系结构想要在不同 mmap 区域布局之间作出选择,则需要设置 HAVE_ARCH_PICK_MMAP_LAYOUT ,并提供arch_pick_mmap_layout函数
  2. 在创建新的内存映射时,除非用户指定了具体的地址,否则内核需要找到一个适当的位置。如果体系结构自身想要选择合适的位置,则必须设置预处理器符号 HAVE_ARCH_UNMAPPED_AREA ,并相应地定义 arch_get_unmapped_area 函数。
  3. 在寻找新的内存映射低端内存位置时,通常从较低的内存位置开始,逐渐向较高的内存地址搜索。内核提供了默认的函数 arch_get_unmapped_area_topdown 用于搜索,但如果某个体系结构想要提供专门的实现,则需要设置预处理器符号 HAVE_ARCH_UNMAPPED_AREA_TOPDOWN 。
  4. 通常,栈自顶向下增长。具有不同处理方式的体系结构需要设置配置选项 CONFIG_STACK_GROWSUP 设置内核栈向上增长

task_struct->flags 设了 PF_RANDOMIZE 标志,则内核不会为栈和内存映射的起点选择固定位置,而是在每次新进程启动时随机改变这些值的设置

虚拟地址空间经典布局:

在这里插入图片描述

text段如何映射到虚拟地址空间中由ELF标准确定。每个体系结构都指定了一个特定的起始地址:IA-32系统起始于0x08048000,在text段的起始地址与最低的可用地址之间有大约128 MiB的间距,用于捕获NULL指针。其他体系结构也有类似的缺口:UltraSparc计算机使用0x100000000作为text段的起始点,而AMD64使用0x0000000000400000。堆紧接着text段开始,向上增长
栈起始于STACK_TOP,如果设置了PF_RANDOMIZE,则起始点会减少一个小的随机量。每个体系结构都必须定义STACK_TOP,大多数都设置为TASK_SIZE,即用户地址空间中最高的可用地址。进程的参数列表和环境变量都是栈的初始数据
用于内存映射的区域(MMAP)起始于mm_struct->mmap_base,通常设置为 TASK_UNMAPPED_BASE,每个体系结构都需要定义。几乎所有的情况下,其值都是TASK_SIZE/3(1G=0x4000000)。要注意,如果使用内核的默认配置,则mmap区域的起始点不是随机的

虚拟内存空间经典布局限制了堆最大为1G,超过会出问题,因此

内核版本2.6.7引入新的虚拟地址空间布局:
在这里插入图片描述

其想法在于使用固定值限制栈的最大长度。由于栈是有界的,因此安置内存映射的区域可以在栈末端的下方立即开始。与经典方法相反,该区域现在是自顶向下扩展。由于堆仍然位于虚拟地址空间中较低的区域并向上增长,因此mmap区域和堆可以相向增长,直至耗尽虚拟地址空间中剩余的区域。为确保栈与mmap区域不发生冲突,两者之间设置了一个安全隙

建立进程内存空间的布局

  • do_execve
    • search_binary_handler
      • load_elf_binary

在使用 load_elf_binary 装载一个ELF二进制文件时,将创建进程的地址空间,而exec系统调用刚好使用了该函数

//fs/binfmt_elf.c
static int load_elf_binary(struct linux_binprm *bprm, struct pt_regs *regs)

关注建立虚拟内存区域所需的各个步骤:
在这里插入图片描述

用户可以通过 /proc/sys/kernel/randomize_va_space 停用地址空间随机化机制

//arch/x86/mm/mmap_32.c
#define MIN_GAP (128*1024*1024)
#define MAX_GAP (TASK_SIZE/6*5)
//IA32选择应用程序内存布局(经典布局)
void arch_pick_mmap_layout(struct mm_struct *mm)

#define MIN_GAP (128*1024*1024)
#define MAX_GAP (TASK_SIZE/6*5)
//计算内存映射基地值(mmap基地址)
static inline unsigned long mmap_base(struct mm_struct *mm)

/proc/sys/kernel/legacy_va_layout 可以设置使用旧的内存布局

//mm/mmap.c
#ifndef HAVE_ARCH_UNMAPPED_AREA_TOPDOWN
//如果mmap区域自顶向下扩展,那么分配新区域的函数是arch_get_unmapped_area_topdown,其处理逻辑与 arch_get_unmapped_area 类似。当然,搜索的方向相反(新布局)
unsigned long
arch_get_unmapped_area_topdown(struct file *filp, const unsigned long addr0,
			  const unsigned long len, const unsigned long pgoff,
			  const unsigned long flags)

AMD64系统上对虚拟地址空间总是使用经典布局,因此无需区分各种选项

内存映射的原理

由于所有用户进程总的虚拟地址空间比可用的物理内存大得多,因此只有最常用的部分才与物理页帧关联。这不是问题,因为大多数程序只占用实际可用内存的一小部分。我们考察一下通过文本编辑器操作文件的情况。通常用户只关注文件结尾处,因此尽管整个文件都映射到内存中,实际上只使用了几页来存储文件末尾的数据。至于文件开始处的数据,内核只需要在地址空间保存相关信息,如数据在磁盘上的位置,以及需要数据时如何读取。
text段的情形类似,始终需要的只是其中一部分。继续考虑文本编辑器的例子,那么就只需要加载与主要编辑功能相关的代码。其他部分,如帮助系统或所有程序通用的Web和电子邮件客户端程序,只会在用户明确要求时才加载
内核必须提供数据结构,以建立虚拟地址空间的区域和相关数据所在位置之间的关联。例如,在映射文本文件时,映射的虚似内存区必须关联到文件系统在硬盘上存储文件内容的区域,如下:

在这里插入图片描述

内核利用address_space数据结构,提供一组方法从存储设备读取数据。例如,从文件系统读取。因此address_space形成了一个辅助层,将映射的数据表示为连续的线性区域,提供给内存管理子系统
按需分配和填充页称之为按需调页法(demand paging)。它基于处理器和内核之间的交互,使用的各种数据结构如下:
在这里插入图片描述

  1. 进程试图访问用户地址空间中的一个内存地址,但使用页表无法确定物理地址(物理内存中没有关联页)。
  2. 处理器接下来触发一个缺页异常,发送到内核。
  3. 内核会检查负责缺页区域的进程地址空间数据结构,找到适当的后备存储器,或者确认该访问实际上是不正确的。
  4. 分配物理内存页,并从后备存储器读取所需数据填充。
  5. 借助于页表将物理内存页并入到用户进程的地址空间,应用程序恢复执行。

进程虚拟内存相关数据结构 mm_struct

mm_struct下列成员用于管理用户进程在虚拟地址空间中的所有内存区域

//include/linux/mm_types.h 
struct mm_struct { 
    struct vm_area_struct * mmap; /* 虚拟内存区域列表 */ 
    struct rb_root mm_rb; 
    struct vm_area_struct * mmap_cache; /* 上一次find_vma的结果 */ 
... 
}

mm_struct中vm_area_struct的两种排序,树和链表

每个区域都通过一个vm_area_struct实例描述,进程的各区域按两种方法排序

  1. 在一个单链表上(开始于mm_struct->mmap)。
  2. 在一个红黑树中,根结点位于mm_rb。

用户虚拟地址空间中的每个区域由开始和结束地址描述。现存的区域按起始地址以递增次序被归入链表中。扫描链表找到与特定地址关联的区域,在有大量区域时是非常低效的操作(数据密集型的应用程序就是这样)。因此vm_area_struct的各个实例还通过红黑树管理,可以显著加快扫描速度。
增加新区域时,内核首先搜索红黑树,找到刚好在新区域之前的区域。因此,内核可以向树和线性链表添加新的区域,而无需扫描链表
在这里插入图片描述

虚拟内存区域的表示

每个区域表示为vm_area_struct的一个实例

//include/linux/mm_types.h 
//简称VMA,也被称为进程地址空间或进程线性区
struct vm_area_struct { 
    struct mm_struct * vm_mm; /* 所属地址空间。 */ 
    unsigned long vm_start; /* vm_mm内的起始地址。 */ 
    unsigned long vm_end; /* 在vm_mm内结束地址之后的第一个字节的地址。 */ 
    /* 各进程的虚拟内存区域链表,按地址排序 */ 
    struct vm_area_struct *vm_next; 
    pgprot_t vm_page_prot; /* 该虚拟内存区域的访问权限。 */ 
    unsigned long vm_flags; /* 标志,如下列出。 */ 
    struct rb_node vm_rb; 
    /* 
    对于有地址空间和后备存储器的区域来说, 
    shared连接到address_space->i_mmap优先树,
    或连接到悬挂在优先树结点之外、类似的一组虚拟内存区域的链表,
    或连接到address_space->i_mmap_nonlinear链表中的虚拟内存区域。 */ 
    union { 
        struct { 
        struct list_head list; 
            void *parent; /* 与prio_tree_node的parent成员在内存中位于同一位置 */ 
            struct vm_area_struct *head;
        } vm_set; 
        struct raw_prio_tree_node prio_tree_node; 
    } shared; 
    /* 
    *在文件的某一页经过写时复制之后,文件的MAP_PRIVATE虚拟内存区域可能同时在i_mmap树和
    *anon_vma链表中。MAP_SHARED虚拟内存区域只能在i_mmap树中。
    *匿名的MAP_PRIVATE、栈或brk虚拟内存区域(file指针为NULL)只能处于anon_vma链表中。
    */ 
    struct list_head anon_vma_node; /* 对该成员的访问通过anon_vma->lock串行化 */ 
    struct anon_vma *anon_vma; /* 对该成员的访问通过page_table_lock串行化 */ 
    /* 用于处理该结构的各个函数指针。 */ 
    struct vm_operations_struct * vm_ops; 
    /* 后备存储器的有关信息: */ 
    unsigned long vm_pgoff; /* (vm_file内)的偏移量,单位是PAGE_SIZE,不是PAGE_CACHE_SIZE */ 
    struct file * vm_file; /* 映射到的文件(可能是NULL)。 */ 
    void * vm_private_data; /* vm_pte(即共享内存) */ 
};

页缓存address_space中的优先查找树,文件结构的页缓存(file->address_space)与 task_struct->mm_struct->vm_area_struct之间建立的关系

优先查找树(priority search tree)用于建立文件中的一个区域与该区域映射到的所有虚拟地址空间之间的关联。

  1. 附加的数据结构

    每个打开文件(和每个块设备,因为这些也可以通过设备文件进行内存映射)都表示为struct file的一个实例。该结构包含了一个指向地址空间对象struct address_space的指针。该对象是优先查找树(prio tree)的基础,而文件区间与其映射到的地址空间之间的关联即通过优先树建立

    //include/linux/fs.h
    //页高速缓存核心结构
    struct address_space {
        struct inode *host; /* owner: inode, block_device */ 
        ... 
        struct prio_tree_root i_mmap; /*radix优先搜索树根节点,子节点为struct vm_area_struct->share->prio_tree_node*/
        struct list_head i_mmap_nonlinear;/*地址空间(虚拟内存区域)中非线性内存区(VM_NONLINEAR)的链表头,元素为struct vm_area_struct->shared->vm_set->list*/
        ... 
    }
    
    struct file {
        ... 
        struct address_space *f_mapping; 
        ... 
    }
    

    每个文件和块设备都表示为struct inode的一实例。struct file是通过open系统调用打开的文件的抽象,与此相反,inode表示文件系统自身中的对象

    //include/linux/fs.h
    struct inode {
        ... 
        struct address_space *i_mapping; 
        ... 
    }
    

    下文只讨论文件区间的映射,但实际上也可以映射不同的东西。例如,直接映射裸(raw)块设备上的区间,而不通过文件系统迂回。在打开文件时,内核将file->f_mapping设置到inode->i_mapping。这使得多个进程可以访问同一个文件,而不会直接干扰到其他进程:inode是一个特定于文件的数据结构,而file则是特定于给定进程的

    在这里插入图片描述

    地址空间是优先树的基本要素,而优先树包含了所有相关的vm_area_struct实例,描述了与inode关联的文件区间到一些虚拟地址空间的映射。由于每个struct vm_area_struct 的实例都包含了一个指向所属进程的mm_struct的指针,关联就已经建立起来了!要注意,vm_area_struct还可以通过以i_mmap_nonlinear为表头的双链表与一个地址空间关联。这是非线性映射(nonlinear mapping)所需要的,我现在暂时忽略该内容。我们将在4.7.3节再讲解非线性映射。

    一个给定的struct vm_area_struct 实例,可以包含在两个数据结构中。一个建立进程虚拟地址空间中的区域与潜在的文件数据之间的关联,一个用于查找映射了给定文件区间的所有地址空间

  2. 优先树的表示

    优先树用来管理表示给定文件中特定区间的所有vm_area_struct实例。这要求该数据结构不仅能够处理重叠,还要能处理相同的文件区间。如图4-8所示:两个进程将一个文件的[7, 12]区域映射到其虚拟地址空间中,而第3个进程映射了区间[10, 30].

    在这里插入图片描述

    各个优先树结点表示为一个raw_prio_tree_node实例,该实例直接包含在各个vm_area_struct实例中。该实例与一个vm_set实例在同一个联合中。这可以将一个vm_set(进而vm_area_struct)的链表与一个优先树结点关联起来。如下图:

    在这里插入图片描述

    向虚拟内存区域插入优先树结点操作如下:

    1. vm_area_struct->share->prio_tree_node 树结点用于加入树根,vm_area_struct->vm_set->parent 与 prio_tree_node结构的最后一个成员相同,该成员在 vm_set 中不使用,内核可以使用parent != NULL,来检查当前的vm_area_struct实例是否已经在树中.vmset的head成员与prio_tree_node不重叠,因此二者尽管在同一个联合之中,也可以同时使用.因此内核使用vm_set.head指向属于一个共享映射的vm_area_struct实例列表中的第一个实例。
    2. 如果上述共享映射的链表包含了一个vm_area_struct,则vm_set.list用作表头,链表包含所有涉及的虚拟内存区域

    后面将详细讨论内核如何插入新区域

对虚拟内存区域的操作

在建立或删除映射时,内核还负责在管理这些数据结构时进行优化:

在这里插入图片描述

  • 如果一个新区域紧接着现存区域前后直接添加,内核将涉及的数据结构合并为一个。当然,前提是涉及的所有区域的访问权限相同,而且是从同一后备存储器映射的连续数据
  • 如果在区域的开始或结束处进行删除,则必须据此截断现存的数据结构
  • 如果删除两个区域之间的一个区域,那么一方面需要减小现存数据结构的长度,另一方面需要为形成的新区域创建一个新的数据结构

通过虚拟地址查找所属的虚拟内存区域 find_vma

通过虚拟地址,find_vma可以查找用户地址空间中结束地址在给定地址之后的第一个区域,即满足addr < vm_area_struct->vm_end条件的第一个区域。该函数的参数不仅包括虚拟地址(addr),还包括一个指向mm_struct实例的指针,后者指定了扫描哪个进程的地址空间

//mm/mmap.c
//根据虚拟地址找到用户地址空间中包含该地址的虚拟内存区域
struct vm_area_struct * find_vma(struct mm_struct * mm, unsigned long addr)

//include/linux/mm.h
//用于确认边界为start_addr和end_addr的区间是否完全包含在一个现存的虚拟内存区域内部
static inline struct vm_area_struct * find_vma_intersection(struct mm_struct * mm, unsigned long start_addr, unsigned long end_addr)

虚拟内存区域合并 vma_merge

//将一个新虚拟内存区域与周边虚拟内存区域合并
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)

插入虚拟内存区域 insert_vm_struct {#insert}

在这里插入图片描述

//用于插入新虚拟内存区域的标准函数
int insert_vm_struct(struct mm_struct * mm, struct vm_area_struct * vma)

获取虚拟内存区域 get_unmapped_area

//mm/mmap.c
//获取虚拟地址空间中满足条件的未映射区域
unsigned long
get_unmapped_area(struct file *file, unsigned long addr, unsigned long len,
		unsigned long pgoff, unsigned long flags)
  • get_unmapped_area
    • current->mm->get_unmapped_area(为 arch_get_unmapped_area 或 arch_get_unmapped_area_topdown)

vm_area_struct 与 address_space 的关系

文件的内存映射可以认为是两个不同的地址空间之间的映射,以简化(系统)程序员的工作。一个地址空间是用户进程的虚拟地址空间,另一个是文件系统所在的地址空间
在内核创建一个映射时,必须建立两个地址空间之间的关联,以支持二者以读写请求的形式通信。 vm_operations_struct 结构即用于完成该工作。它提供了一个操作,来读取已经映射到虚拟地址空间、但其内容尚未进入物理内存的页

//include/linux/mm.h
//在内核创建一个映射时,必须建立两个地址空间之间的关联,以支持二者以读写请求的形式通信。vm_operations_struct结构即用于完成该工作
struct vm_operations_struct {
	void (*open)(struct vm_area_struct * area);//创建区域时调用,通常不使用,设置为NULL指针
	void (*close)(struct vm_area_struct * area);//删除区域时调用,通常不使用,设置为NULL指针
	int (*fault)(struct vm_area_struct *vma, struct vm_fault *vmf);//如果地址空间中的某个虚拟内存页不在物理内存中,自动触发的缺页异常处理程序会调用该函数,将对应的数据读取到一个映射在用户地址空间的物理内存页中,调用了 struct address_space->a_ops->readpage 函数从磁盘读页
	struct page *(*nopage)(struct vm_area_struct *area,
			unsigned long address, int *type);//是内核原来用于响应缺页异常的方法,不如fault那么灵活。出于兼容性的考虑,该成员仍然保留,但不应该用于新的代码
	unsigned long (*nopfn)(struct vm_area_struct *area,
			unsigned long address);

	/* notification that a previously read-only page is about to become
	 * writable, if an error is returned it will cause a SIGBUS */
	int (*page_mkwrite)(struct vm_area_struct *vma, struct page *page);
};

//include/linux/mm_types.h 
//简称VMA,也被称为进程地址空间或进程线性区
struct vm_area_struct {
    /* 用于处理该结构的各个函数指针。 */ 
    struct vm_operations_struct * vm_ops; 
};

但该操作不了解映射类型或其性质的相关信息。由于存在许多种类的文件映射(不同类型文件系统上的普通文件、设备文件等),因此需要更多的信息。实际上,内核需要更详细地说明数据源所在的地址空间。
上文简要提到的 address_space 结构,即为该目的定义,包含了有关映射的附加信息。每个文件映射都有一个相关的address_space实例。

//include/linux/fs.h
/*对所有者页进行操作的函数集合*/
struct address_space_operations {
	int (*writepage)(struct page *page, struct writeback_control *wbc);//写操作(将一页从内存写到磁盘)
	int (*readpage)(struct file *, struct page *);//读一页操作(从磁盘读到页)
	void (*sync_page)(struct page *);

	/* Write back some dirty pages from this mapping. */
	int (*writepages)(struct address_space *, struct writeback_control *);//把指定数量的脏页写回磁盘

	/* Set a page dirty.  Return true if this dirtied it */
	int (*set_page_dirty)(struct page *page);//把页设为脏页,即已与磁盘中内容不同

	int (*readpages)(struct file *filp, struct address_space *mapping,
			struct list_head *pages, unsigned nr_pages);//从磁盘读页的链表

	/*
	 * ext3 requires that a successful prepare_write() call be followed
	 * by a commit_write() call - they must be balanced
	 */
	int (*prepare_write)(struct file *, struct page *, unsigned, unsigned);//为写操作做准备(由磁盘文件系统使用)
	int (*commit_write)(struct file *, struct page *, unsigned, unsigned);//完成写操作(由磁盘文件系统使用)

	int (*write_begin)(struct file *, struct address_space *mapping,
				loff_t pos, unsigned len, unsigned flags,
				struct page **pagep, void **fsdata);
	int (*write_end)(struct file *, struct address_space *mapping,
				loff_t pos, unsigned len, unsigned copied,
				struct page *page, void *fsdata);

	/* Unfortunately this kludge is needed for FIBMAP. Don't use it */
	sector_t (*bmap)(struct address_space *, sector_t);//从文件块索引中获取逻辑块号
	void (*invalidatepage) (struct page *, unsigned long);//使所有者的页无效(截断文件时使用)
	int (*releasepage) (struct page *, gfp_t);//由日志文件系统使用以准备释放页
	ssize_t (*direct_IO)(int, struct kiocb *, const struct iovec *iov,
			loff_t offset, unsigned long nr_segs);//页的直接I/O传输(绕过页高速缓存)
	struct page* (*get_xip_page)(struct address_space *, sector_t,
			int);
	/* migrate the contents of a page to the specified target */
	int (*migratepage) (struct address_space *,
			struct page *, struct page *);//重新定位一页,即将一页的内容移动到另外一页
	int (*launder_page) (struct page *);
};

//地址空间,用于管理文件(struct inode)映射到内存的页面(struct page)的结构体,因该叫物理页缓存结构体,页高速缓存核心数据结构
struct address_space {
    const struct address_space_operations *a_ops;	/*对所有者页进行操作的函数集合*//* methods */
}

vm_area_struct 与 address_space 的关系:
缺页异常调用 vm_area_struct->vm_ops->fault
fault 为 filemap_fault函数,最后调用 file->f_mapping->a_ops->readpage 从磁盘读取一页
在这里插入图片描述

内存映射

讨论在建立映射时内核和应用程序之间的交互.C标准库提供了mmap函数建立映射,内核提供了两个系统调用mmap和mmap2。某些体系结构实现了两个版本,例如IA-64和Sparc(64),其他的只实现了第1个(AMD64)或第2个(IA-32)。两个函数的参数相同。

//arch/x86/kernel/sys_i386_32.c
asmlinkage long sys_mmap(2)(unsigned long addr, unsigned long len,
			  unsigned long prot, unsigned long flags,
			  unsigned long fd, unsigned long pgoff)

这两个调用都会在用户虚拟地址空间中的pos位置,建立一个长度为len的映射,其访问权限通过prot定义。flags是一个标志集,用于设置一些参数。相关的文件通过其文件描述符fd标识。
mmap和mmap2之间的差别在于偏移量的语义( pgoff )。在这两个调用中,它都表示映射在文件中开始的位置。对于mmap,位置的单位是字节,而mmap2使用的单位则是页(PAGE_SIZE)。因此即使文件比可用地址空间大,也可以映射文件的一部分。
可使用 munmap 系统调用删除映射

创建映射

用户空间mmap和mmap2标志说明:

  • MAP_FIXED:指定除了给定地址之外,不能将其他地址用于映射。如果没有设置该标志,内核可以在受阻时随意改变目标地址。例如,在目标地址已经存在一个映射的情况(否则,现存的映射将被覆盖)。
  • MAP_SHARED:如果一个对象(通常是文件)在几个进程之间共享时,则必须使用MAP_SHARED。
  • MAP_PRIVATE:创建一个与数据源分离的私有映射,对映射区域的写入操作不影响文件中的数据。
  • MAP_ANONYMOUS:创建与任何数据源都不相关的匿名映射,fd和off参数被忽略。此类映射可用于为应用程序分配类似malloc所用的内存
//mm/mmap.c
//被mmap或mmap2调用,mmap->sys_mmap->do_mmap->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)
  • sys_mmap2
    • do_mmap_pgoff
      在这里插入图片描述

删除映射

用户层 munmap 触发系统调用 sys_munmap, sys_munmap调用 do_munmap
在这里插入图片描述

非线性映射

普通的映射将文件中一个连续的部分映射到虚拟内存中一个同样连续的部分。如果需要将文件的不同部分以不同顺序映射到虚拟内存的连续区域中,通常必须使用几个映射,从消耗的资源来看,代价比较昂贵(特别是需要分配的vm_area_struct数量)。实现同样效果的一个更简单的方法是使用非线性映射,该特性在内核版本2.5开发期间引入。内核提供了一个独立的系统调用,专门用于该目的 sys_remap_file_pages,实现该特性无需移动内存中的数据,而是通过操作进程的页表实现的

//mm/fremap.c
asmlinkage long sys_remap_file_pages(unsigned long start, unsigned long size,
	unsigned long prot, unsigned long pgoff, unsigned long flags)
  1. 非线性映射的 vm_area_struct 实例在链表头 struct address_spacei_mmap_nonlinear 中,链表元素为 vm_area_struct.shared.vm_set.list
  2. 所述区域对应的页表项用一些特殊的项填充。这些页表项看起来像是对应于不存在的页,但其中包含附加信息,将其标识为非线性映射的页表项。在访问此类页表项描述的页时,会产生一个缺页异常,并读入正确的页

三个函数用于非线性映射

  1. pgoff_to_pte将文件偏移量编码为页号,并将其编码为一种可以存储在页表中的格式
  2. pte_to_pgoff可以解码页表中存储的编码过的文件偏移量
  3. pte_file(pte)检查给定的页表项是否用于表示非线性映射。特别地,该函数使得在缺页异常发生时,能够区分非线性映射和普通换出页的页表项

预处理器常数PTE_FILE_MAX_BITS表示页表项中有多少个比特位可用于存储文件偏移量

//include/asm-x86/pgtable_64.h
#define pte_to_pgoff(pte) ((pte_val(pte) & PHYSICAL_PAGE_MASK) >> PAGE_SHIFT)
#define pgoff_to_pte(off) ((pte_t) { ((off) << PAGE_SHIFT) | _PAGE_FILE })
#define PTE_FILE_MAX_BITS __PHYSICAL_MASK_SHIFT
-------------------------
#define PTE_FILE_MAX_BITS 61
#define pte_to_pgoff(pte) ((pte_val(pte) << 1) >> 3)
#define pgoff_to_pte(off) ((pte_t) { ((off) << 2) | _PAGE_FILE })

交换标识符长64位。比特位0必须为零,因为该页不在内存中。比特位1代表_PAGE_FILE,表明该项属于一个非线性映射,不是交换标识符。最后一个比特位,即比特位63,专用于_PAGE_PROTNONE标志位。因此,这给非线性映射中页偏移量的表示,留出了61位
在这里插入图片描述

在这里插入图片描述

反向映射(通过页找到使用该页的使用者)

前面建立了虚拟地址和物理地址之间的联系(修改页表),以及进程的一个内存区域与虚拟内存页地址间的联系,还缺一个联系,是物理内存页与进程间的联系。页换出swap out时需要该关联。这需要反向映射,在缺页异常中讲具体实现

用于反向映射的数据结构

//include/linux/mm_types.h
struct page {
    ....
    /* 用于反向映射,表示在页表中有多少项指向该页,如果页由一个进程映射,该计数器值为0,未映射的页,其值为-1 */
    atomic_t _mapcount;
    union {
        struct {
            ...
            /* 指定页帧所在的地址空间,pgoff_t index表示页帧在映射内部的偏移量
            * 如果mapping最低位被设为1(PAGE_MAPPING_ANON置位)(因为address_space是sizeof(long)对齐,所以最低位总是0),
            * 则mapping不是struct address_space结构体,
            * 而是struct anon_vma结构体,该结构对实现匿名页的逆向映射很重要
            * 内核可用anon_vma = (struct anon_vma *) (mapping -PAGE_MAPPING_ANON)恢复指针
            * */
            struct address_space *mapping;
        }
        ...
    }
    ...
};

还有两个数据结构用于找到页的使用者:

  1. 优先查找树中嵌入了属于非匿名映射的每个区域
  2. 指向内存中同一页的匿名区域的链表

这两个结构在 vm_area_struct 中,即shared联合以及anon_vma_node和anon_vma:

//include/linux/mm_types.h 
struct vm_area_struct {
    ...
    /*
    *对于有地址空间和后备存储器的区域来说,shared连接到address_space->i_mmap优先树,
    *或连接到悬挂在优先树结点之外、类似的一组虚拟内存区域的链表,
    *或连接到address_space->i_mmap_nonlinear链表中的虚拟内存区域。
    */
    union {
        struct {
            struct list_head list;
            void *parent; /* 与prio_tree_node的parent成员在内存中位于同一位置 */
            struct vm_area_struct *head;
        } vm_set;
        ...
    } shared;
    /*
    *在文件的某一页经过写时复制之后,文件的MAP_PRIVATE虚拟内存区域可能同时在i_mmap
    *树和anon_vma链表中。MAP_SHARED虚拟内存区域只能在i_mmap树中。匿名的
    *MAP_PRIVATE、栈或brk虚拟内存区域(file指针为NULL)只能处于anon_vma链表中。
    */
    struct list_head anon_vma_node; //链表元素,链表头为 struct vm_area_struct->anon_vma->head,指向相同页的映射都保存在一个双链表上,anon_vma_node充当链表元素.有若干此类链表,具体的数目取决于共享物理内存页的映射集合的数目
    struct anon_vma *anon_vma; //anon_vma成员用于匿名映射链表,是一个指向与各链表关联的管理结构的指针,该管理结构由一个表头和相关的锁组成,反向映射时 struct page->mapping被设置为该结构,page->mapping = (struct address_space *) (vm_area_struct->anon_vma+1)
    ...
}

内核在实现反向映射时采用的技巧是,不直接保存页和相关的使用者之间的关联,而只保存页和页所在区域之间的关联。包含该页的所有其他区域(进而所有的使用者)都可以通过刚才提到的数据结构找到。该方法又名基于对象的反向映射(object-based reverse mapping),因为没有存储页和使用者之间的直接关联。相反,在两者之间插入了另一个对象(该页所在的区域)。

建立反向映射的两种方式(RMAP)

有两种方式建立反向映射,匿名页和文件页的反向映射

  1. 匿名页
    将匿名页插入到逆向映射数据结构中有两种方法:

    1. 对新的匿名页调用page_add_new_anon_rmap
    2. 已经有引用计数的页,则使用page_add_anon_rmap

    两个函数的差别是前者将映射计数器page->_mapcount设置为0,后者+1.两个函数最后都调用 __page_set_anon_rmap

    __page_set_anon_rmap 设置 struct page->mapping = (struct address_space *) (vm_area_struct->anon_vma+1);

  2. 文件页
    page_add_file_rmap

计算反向映射使用者数

  • page_referenced 计算页使用者数目
    • page_referenced_anon 计算了匿名页的使用者数目
      • page_referenced_one
    • page_referenced_file 计算了文件页的使用者数目
      • page_referenced_one

堆的管理

堆是进程中用于动态分配变量和数据的内存区域,应用层malloc触发内核brk系统调用,负责扩展/收缩堆.新近的malloc实现(诸如GNU标准库提供的)使用了一种组合方法,使用brk和匿名映射。该方法提供了更好的性能,而且在分配较大的内存区时具有某些优点。
堆是一个连续的内存区域,在扩展时自下至上增长

//include/linux/mm_types.h
struct mm_struct {/*内存描述符*/
    //start_brk, brk:堆的起始地址和结束地址,尽管堆的起始地址在进程生命周期中是不变的,但堆的长度会发生变化,因而brk的值也会变
	unsigned long start_brk, brk, start_stack;
}

在这里插入图片描述

//mm/mmap.c
asmlinkage unsigned long sys_brk(unsigned long brk)

缺页异常的处理{#misspage}

触发正常的缺页异常,CPU 访问的虚拟内存页面类型是:

  1. 未分配页面。
  2. 已分配未映射页面。
  3. 已映射,但是由于内存紧张的原因,该虚拟内存页映射的物理内存页被置换到磁盘上了。

以上三种虚拟内存页有一个共同的特征就是它们背后的物理内存页均不在内存中,要么是没有映射,要么是被置换到磁盘上。当 CPU 访问这些虚拟内存页面的时候,就会产生缺页中断,随后进入内核态为其分配物理内存页面,填充物理内存页面中的内容,最后在页表中建立映射关系。之后的内存访问均是在用户态中进行。

在实际需要某个虚拟内存区域的数据之前,虚拟和物理内存之间的关联不会建立。如果进程访问的虚拟地址空间部分尚未与页帧关联,处理器自动地引发一个缺页异常,内核必须处理此异常。这是内存管理中最重要、最复杂的方面之一,因为必须考虑到无数的细节
在这里插入图片描述

//arch/x86/kernel/entry_32.S
//缺页异常入口
KPROBE_ENTRY(page_fault)
	RING0_EC_FRAME
	pushl $do_page_fault
    ...

//arch/x86/mm/fault_32.c
//该函数需要传递两个参数:发生异常时使用中的寄存器集合,提供异常原因信息的错误代码(long error_code)。目前error_code只使用了前5个比特位(0、1、2、3、4),
/*
比特位	  置位(1)						 未置位(0)
  0       缺页 							保护异常(没有足够的访问权限)
  1 	  读访问 						写访问
  2 	  核心态 						用户状态
  3 	  表示检测到使用了保留位
  4 	  表示缺页异常是在取指令时出现的
*/
fastcall void __kprobes do_page_fault(struct pt_regs *regs,
				      unsigned long error_code)

在这里插入图片描述

  • do_page_fault 缺页异常处理,根据异常地址找到所在虚拟内存区域
    • handle_mm_fault 用户空间缺页异常的校正(即,读取所需数据),根据虚拟地址找到页目录项
    • fixup_exception 搜索异常表
      • handle_pte_fault 分析缺页异常的原因,根据页目录项,虚拟地址,虚拟内存域等从磁盘读取数据到内存中
        • do_linear_fault 按需分配页
          • __do_fault
        • do_anonymous_page 没有关联到文件作为后备存储器的页,需要调用do_anonymous_page进行映射
          • page_add_new_anon_rmap 建立反向映射,page->mapping=(vma->anon_vma+1)
        • do_nonlinear_fault 非线性映射缺页异常处理
          • __do_fault
        • do_wp_page 写时复制

用户空间缺页异常的校正

//mm/memory.c
//用户空间缺页异常的校正(即,读取所需数据)
int handle_mm_fault(struct mm_struct *mm, struct vm_area_struct *vma,
		unsigned long address, int write_access)

//分析缺页异常的原因
static inline int handle_pte_fault(struct mm_struct *mm,
		struct vm_area_struct *vma, unsigned long address,
		pte_t *pte, pmd_t *pmd, int write_access)

文件页

//mm/memory.c
//按需分配页的工作委托给do_linear_fault
static int do_linear_fault(struct mm_struct *mm, struct vm_area_struct *vma,
		unsigned long address, pte_t *page_table, pmd_t *pmd,
		int write_access, pte_t orig_pte)

在这里插入图片描述

匿名页

对匿名页调用 do_anonymous_page ,除了无需向页读入数据之外,该过程几乎与文件页的数据没什么不同。在highmem内存域建立一个新页,并清空其内容。接下来将页加入到进程的页表,并更新高速缓存或者MMU

//mm/memory.c
static int do_anonymous_page(struct mm_struct *mm, struct vm_area_struct *vma,
		unsigned long address, pte_t *page_table, pmd_t *pmd,
		int write_access)

写时复制

//mm/memory.c
//写时复制
static int do_wp_page(struct mm_struct *mm, struct vm_area_struct *vma,
		unsigned long address, pte_t *page_table, pmd_t *pmd,
		spinlock_t *ptl, pte_t orig_pte)

在这里插入图片描述

获取非线性映射

//mm/memory.c
//非线性映射的缺页处理
static int do_nonlinear_fault(struct mm_struct *mm, struct vm_area_struct *vma,
		unsigned long address, pte_t *page_table, pmd_t *pmd,
		int write_access, pte_t orig_pte)
  • do_nonlinear_fault
    • __do_fault

内核缺页异常

缺页异常触发条件:

  1. 真正错误:访问了错误地址.开发版本偶尔会出现,稳定版本不会出现
  2. 真正错误:内核通过用户空间传递的系统调用参数,访问了无效地址
  3. 访问使用vmalloc分配的区域,触发缺页异常

每次发生缺页异常时,将输出异常的原因和当前执行代码的地址。这使得内核可以编译一个列表,列出所有可能执行未授权内存访问操作的危险代码块。这个“异常表”在链接内核映像时创建,在二进制文件中位于__start_exception_table__end_exception_table之间。每个表项都对应于一个struct exception_table实例

//include/asm-x86/uaccess_32.h
struct exception_table_entry
{
	/*insn指定了内核预期在虚拟地址空间中发生异常的位置。fixup指定了发生异常时执行恢复到哪个代码地址*/
	unsigned long insn, fixup;
};

//arch/x86/mm/extable_32.c
//用于搜索异常表
int fixup_exception(struct pt_regs *regs)

在内核和用户空间之间复制数据

在这里插入图片描述

在这里插入图片描述

源自用户空间的指针必须用关键字__user标记

总结

进程中有个内存管理结构体 task_struct->mm_struct ,内存管理结构体中存着进程使用的多个虚拟内存区域结构体 vm_area_struct ,用单链表和红黑树两种方式管理,红黑树用于加速搜索

两种映射方式:

  1. 普通的映射将文件中一个连续的部分映射到虚拟内存中一个同样连续的部分,各个虚拟内存区域结构体在文件结构体中用优先查找树管理(address_space->i_mmap)
  2. 如果需要将文件的不同部分以不同顺序映射到虚拟内存的连续区域中,使用非线性映射,用链表管理(address_space->i_mmap_nonlinear)

页缓存使用的结构为 address_space ,以优先树结构存着 vm_area_struct,以基数树结构存着页面 page
一个 address_space与一个偏移量能够确定一个page cache 或swap cache中的一个页面
page cache是与文件映射对应的,而swap cache是与匿名页对应的。如果一个内存页面不是文件映射,则在换入换出的时候加入到swap cache,如果是文件映射,则不需要交换缓冲。

3种正常的缺页异常:

  1. 未分配页面。
  2. 已分配未映射页面。
  3. 已映射,但是由于内存紧张的原因,该虚拟内存页映射的物理内存页被置换到磁盘上了。

错误的缺页异常:访问了无效地址

红黑树性质:
性质1. 结点是红色或黑色。
性质2. 根结点是黑色。
性质3. 所有叶子都是黑色。(叶子是NIL结点)
性质4. 每个红色结点的两个子结点都是黑色。(从每个叶子到根的所有路径上不能有两个连续的红色结点)
性质5. 从任一结点到其每个叶子的所有路径都包含相同数目的黑色结点。

=========================================

涉及的命令和配置:
CONFIG_STACK_GROWSUP 设置内核栈向上增长

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值
>