三、虚拟地址空间管理

Linux内存管理 | 三、虚拟地址空间管理 #

上一节,我们主要了解了虚拟内存空间的布局情况,趁热打铁,我们直接从源代码的视角,来看一下Linux内核是如何管理虚拟内存空间的。

废话不多说,直接开始!

1、用户态空间管理 #

读完上一节我们知道,用户态的布局情况如下:

image-20231005160139650

我们运行的可执行程序,被加载进内存后,会作为一个进程存在,这个进程Linux内核会将其抽象成一个结构体。没错,它就是task_struct

1.1 task_struct结构体 #

task_struct结构体是进程的抽象,进程所涉及到的内容非常多,下面只列举出一些重要的数据结构,方面理解。

// include/linux/sched.h
struct task_struct {
    ...
    pid_t				pid;		//	进程PID
    pid_t				tgid;		//	线程PID
    struct files_struct	*files;		//  进程打开的文件信息
    struct mm_struct	*mm;		//	进程虚拟内存空间的内存描述符
    ...
}

如上,进程抽象为task_struct结构体,通过mm_struct结构体来管理虚拟内存空间。

1.2 mm_struct结构体 #

每个进程都有唯一的 mm_struct 结构体,也就是前边提到的每个进程的虚拟地址空间都是独立,互不干扰的。

mm_struct的结构体如下:

//	include/linux/mm_types.h
struct mm_struct {
    ...
    struct {
        ...
        
        unsigned long task_size;	/* size of task vm space */
    	...
        
        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;        
        ...
        
        struct vm_area_struct *mmap;		/* list of VMAs */
        struct rb_root mm_rb;
        ...
        
        
    }__randomize_layout;
    ...
}

 

1.3 内核态和用户态的划分 #

mm_struct里面定义的task_size变量,就是用来划分虚拟内存的用户空间和内核空间的。

unsigned long task_size;

task_size也就是两者的分界线,下面我们看下task_size是如何被赋值的。

 

当我们执行一个新的进程的时候,Linux内核会执行load_elf_binaryAPI接口,进而调用setup_new_exec函数来实现新进程的创建。

setup_new_exec函数中,会执行

current->mm->task_size = 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)
......

这里我们只需要知道TASK_SIZE默认值3为PAGE_OFFSET,并且默认为0xC0000000为分界线的,即用户空间3GB,内核空间1GB;当然这个可以由我们动态配置,可以配置PAGE_OFFSET0x80000000,即用户空间和内核空间均为2GB,取决于我们的应用场合,当你看到与我们讲解不同时,也不用大惊小怪。

以上,表达的概念很简单,如下图:

image.png

 

1.4 位置信息描述 #

我们知道用户态内存空间分为几个区域:代码段、数据段、BSS段、堆、文件映射和匿名映射区、栈等几个部分,同样在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;
  • total_vm:总映射页面的数目。(这么大的虚拟内存空间,不可能全部映射到真实的物理内存,都是按需映射的,这里表示当前映射的页面总数目)

由于物理内存比较小,当内存吃紧的时候,就会发生换入换出的操作,即将暂时不用的页面换出到硬盘上,有的页面比较重要,不能换出。

  • locked_vm:被锁定不能换出的页面
  • pinned_vm :不能换出、也不能移动的页面
  • data_vm:存放数据页的页的数目
  • exec_vm:存放可执行文件的页的数目
  • stack_vm:存放堆栈信息页的数目
  • start_codeend_code:表示可执行代码开始和结束的位置
  • start_dataend_data:表示已初始化数据的开始位置和结束位置
  • start_brkbrk:堆的起始地址,结束地址
  • start_stack:是栈的起始位置,在 RBP 寄存器中存储,栈的结束位置也就是栈顶指针,在 RSP 寄存器中存储。在栈中内存地址的增长方向也是由高地址向低地址增长。
  • arg_startarg_end:参数列表的起始位置和结束位置
  • env_startenv_end:环境变量的起始位置和结束位置

 

整体的布局情况如下

image.png

 

1.5 区域属性描述 #

尽管已经有了一些变量来描述每一个段的信息,但是Linux内核在mm_struct结构体里面,还有一个专门的数据结构vm_area_struct来管理每个区域的属性。

struct vm_area_struct *mmap;		/* list of VMAs */
struct rb_root mm_rb;

mmap:为一个单链表,将所有的区域串联起来

mm_rb:为一个红黑树,方便查找和修改内存区域。

 

下面看一下vm_area_struct数据结构:

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_startvm_end:为该区域在用户空间的起始和结束地址

  • vm_nextvm_prev:将该区域添加到链表上,便于管理。

  • vm_rb:将这个区域放到红黑树上

  • vm_ops:对该区域可以进行的内存操作

  • anon_vma:匿名映射

  • vm_file:文件映射

 

用户态空间的每个区域都由该结构体来管理,最终形成下面的这个结构:

image-20231008184824770

 

顺便介绍一下 我的圈子:高级工程师聚集地,期待大家的加入。

2、内核态空间管理 #

上面,我们从源码角度了解了用户态空间管理,下面我们看内核态空间管理。

回顾一下,我们内核态的布局情况是怎么样的呢,还记得吗?

image-20231005155942462

我们要知道:

  1. 内核态的虚拟空间和任何一个进程都没有关系,所有的进程看到的内核态虚拟空间都是一样的。
  2. 在内核态,我们直接操作的依旧是虚拟地址,而非物理地址
  3. 不同CPU结构下,内核态空间的布局格式是不变的,但是大小会有所调整,比如ARMX86的大小空间有所不同。

 

内核态空间管理并不像用户态那样使用结构体来统一管理,而是直接使用宏来定义每个区域的分界线,

下面我们以x86架构来分析内核态空间的管理

2.1 分界线定义 #

/*
 * User space process size: 3GB (default).
 */
#define TASK_SIZE		PAGE_OFFSET

/* PAGE_OFFSET - the virtual address of the start of the kernel image */
#define PAGE_OFFSET		((unsigned long)__PAGE_OFFSET)

#define __PAGE_OFFSET		__PAGE_OFFSET_BASE

#define __PAGE_OFFSET_BASE	_AC(CONFIG_PAGE_OFFSET, UL)

config PAGE_OFFSET
    hex
    default 0xB0000000 if VMSPLIT_3G_OPT
    default 0x80000000 if VMSPLIT_2G
    default 0x78000000 if VMSPLIT_2G_OPT
    default 0x40000000 if VMSPLIT_1G
    default 0xC0000000
    depends on X86_32

TASK_SIZE:内核态空间与用户态空间的分界线

PAGE_OFFSET:该宏表示内核镜像起始的虚拟地址。

CONFIG_PAGE_OFFSET:这个宏定义的值,根据实际情况自行设定,默认为0XC0000000,可以设置为0X80000000等。

以上,TASK_SIZE就被定义为0XC0000000作为用户态空间和内核态空间的分界线,将4G虚拟内存分配为3G/1G结构。

image-20231010072937276

2.2 直接映射区定义 #

直接映射区是定义在PAGE_OFFSEThigh_memory之间的区域。

  • PAGE_OFFSET:表示内核镜像的起始地址,上文已经说明。
  • high_memory也是表示的就是896M这个值,表示高端内存的分界线。

顺便说明以下,TASK_SIZEPAGE_OFFSET在不同架构下是不同的,在ARM架构下,两者并不相等,本文以X86架构为例

image-20231010073949813

2.3 安全保护区定义 #

系统会在high_memoryVMALLOC_START之间预留8M的安全保护区,防止访问越界。

VMALLOC_OFFSET表示的是内核动态映射区的偏移,也就是所谓的安全保护区。

#define VMALLOC_START		(((unsigned long)high_memory + VMALLOC_OFFSET) & ~(VMALLOC_OFFSET-1))

#define VMALLOC_OFFSET		(8*1024*1024)

可以很清楚的看到VMALLOC_OFFSET定义了8M的空间,VMALLOC_STARThigh_memory基础上,偏移了VMALLOC_OFFSET 8M空间大小作为安全保护区,以防越界访问。

image-20231010074810831

2.3 动态映射区定义 #

VMALLOC_STARTVMALLOC_END之间称为内核动态映射区。

和用户态进程使用 malloc 申请内存一样,在这块动态映射区内核是使用 vmalloc 进行内存分配。

#define VMALLOC_START		(((unsigned long)high_memory + VMALLOC_OFFSET) & ~(VMALLOC_OFFSET-1))

#ifdef CONFIG_HIGHMEM
# define VMALLOC_END	(PKMAP_BASE - 2 * PAGE_SIZE)
#else
# define VMALLOC_END	(LDT_BASE_ADDR - 2 * PAGE_SIZE)
#endif

PKMAP_BASE:是永久映射区的起始地址。

VMALLOC_END:在永久映射区的起始地址下,偏移2个PAGE_SIZE作为安全保护区。

image-20231010075717944

2.4 永久映射区定义 #

PKMAP_BASEFIXADDR_START 的空间称为永久内核映射,在内核的这段虚拟地址空间中允许建立与物理高端内存的长期映射关系。

比如内核通过 alloc_pages() 函数在物理内存的高端内存中申请获取到的物理内存页,这些物理内存页可以通过调用 kmap 映射到永久映射区中。

#define PKMAP_BASE		\
    ((LDT_BASE_ADDR - PAGE_SIZE) & PMD_MASK)
    
#define LDT_BASE_ADDR		\
    ((CPU_ENTRY_AREA_BASE - PAGE_SIZE) & PMD_MASK)

#define CPU_ENTRY_AREA_BASE						\
    ((FIXADDR_TOT_START - PAGE_SIZE * (CPU_ENTRY_AREA_PAGES + 1))   \
     & PMD_MASK)

#define FIXADDR_TOT_START	(FIXADDR_TOP - FIXADDR_TOT_SIZE)

#define FIXADDR_TOP	((unsigned long)__FIXADDR_TOP)

#define FIXADDR_TOT_SIZE	(__end_of_fixed_addresses << PAGE_SHIFT)

unsigned long __FIXADDR_TOP = 0xfffff000;

#define PMD_MASK	(~(PMD_SIZE - 1))

#define PAGE_SHIFT		12
#define PAGE_SIZE		(_AC(1,UL) << PAGE_SHIFT)

#define CPU_ENTRY_AREA_PAGES	(NR_CPUS * 40)

#define FIXADDR_START		(FIXADDR_TOP - FIXADDR_SIZE)
  • PKMAP_BASE:是永久映射区的起始地址,它经过一系列的计算得到,具体可以看上面的宏定义,我们大概了解就行了,不同体系结构的定义位置还不一样。
  • FIXADDR_START:是固定映射区的起始地址,也是永久映射区的结束地址。

image-20231011194447450

2.5 固定映射区定义 #

FIXADDR_STARTFIXADDR_TOP的空间称为固定映射区,主要用于满足特殊的需求。

#define FIXADDR_TOP	((unsigned long)__FIXADDR_TOP)

unsigned long __FIXADDR_TOP = 0xfffff000;

固定映射区中的虚拟地址,可以自由映射到物理内存的高端地址空间上,特点是其映射的虚拟地址是不变的,物理地址是可以改变的。

image-20231011195006927

2.6 临时映射区定义 #

最后FIXADDR_TOP0xFFFFFFFF之间的区域称为临时映射区。

它主要用来做什么呢,网上举的一个例子,大家参考以下。

假设用户态的进程要映射一个文件到内存中,先要映射用户态进程空间的一段虚拟地址到物理内存,然后将文件内容写入这个物理内存供用户态进程访问。

给用户态进程分配物理内存页可以通过 alloc_pages(),分配完毕后,按说将用户态进程虚拟地址和物理内存的映射关系放在用户态进程的页表中,就完事大吉了。这个时候,用户态进程可以通过用户态的虚拟地址,也即 0 至 3G 的部分,经过页表映射后访问物理内存,并不需要内核态的虚拟地址里面也划出一块来,映射到这个物理内存页。

但是如果要把文件内容写入物理内存,这件事情要内核来干了,这就只好通过 kmap_atomic 做一个临时映射,写入物理内存完毕后,再 kunmap_atomic 来解映射即可。

image-20231011195939077

以上,就是内核态空间的布局以及管理。

3、总结 #

该篇文章,主要从源码角度来了解用户态空间和内核态空间是如何管理的,挪用大佬的一个图片,结合上面所讲的,相信很快就能茅塞顿开。

image-20231011200638903

欢迎关注【嵌入式艺术】,董哥原创!
img