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; /*sizeoftaskvmspace*/ ... unsigned long mmap_base; /*baseofmmaparea*/ unsigned long total_vm; /*Totalpagesmapped*/ unsigned long locked_vm; /*PagesthathavePG_mlockedset*/ unsigned long pinned_vm; /*Refcountpermanentlyincreased*/ 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 ; /*listofVMAs*/ struct rb_root mm_rb ; ... }__randomize_layout; ... } 1.3 内核态和用户态的划分 mm_struct里面定义的task_size变量,就是用来划分虚拟内存的用户空间和内核空间的。 unsigned long task_size; task_size也就是两者的分界线,下面我们看下task_size是如何被赋值的。 当我们执行一个新的进程的时候,Linux内核会执行load_elf_binary的API接口,进而调用setup_new_exec函数来实现新进程的创建。 在setup_new_exec函数中,会执行 task_size=TASK_SIZE; 这个TASK_SIZE就是我们设置的内核空间地址和用户空间地址的分界线,由我们自定义配置。 # ifdef CONFIG_X86_32 /* *Userspaceprocesssize:3GB(default). */ # define TASK_SIZEPAGE_OFFSET # define TASK_SIZE_MAXTASK_SIZE /* configPAGE_OFFSET hex default0xC0000000 dependsonX86_32 */ # else /* *Userspaceprocesssize.47bitsminusoneguardpage. */ # 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_OFFSET为0x80000000,即用户空间和内核空间均为2GB,取决于我们的应用场合,当你看到与我们讲解不同时,也不用大惊小怪。 以上,表达的概念很简单,如下图: image.png 1.4 位置信息描述 我们知道用户态内存空间分为几个区域:代码段、数据段、BSS段、堆、文件映射和匿名映射区、栈等几个部分,同样在mm_struct中,定义了这些区域的统计信息和位置。 unsigned long mmap_base; /*baseofmmaparea*/ unsigned long total_vm; /*Totalpagesmapped*/ unsigned long locked_vm; /*PagesthathavePG_mlockedset*/ unsigned long pinned_vm; /*Refcountpermanentlyincreased*/ 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_code、end_code:表示可执行代码开始和结束的位置 start_data、end_data:表示已初始化数据的开始位置和结束位置 start_brk、brk:堆的起始地址,结束地址 start_stack:是栈的起始位置,在 RBP 寄存器中存储,栈的结束位置也就是栈顶指针,在 RSP 寄存器中存储。在栈中内存地址的增长方向也是由高地址向低地址增长。 arg_start、arg_end:参数列表的起始位置和结束位置 env_start、env_end:环境变量的起始位置和结束位置 整体的布局情况如下 : image.png 1.5 区域属性描述 尽管已经有了一些变量来描述每一个段的信息,但是Linux内核在mm_struct结构体里面,还有一个专门的数据结构vm_area_struct来管理每个区域的属性。 struct vm_area_struct * mmap ; /*listofVMAs*/ struct rb_root mm_rb ; mmap:为一个单链表,将所有的区域串联起来 mm_rb:为一个红黑树,方便查找和修改内存区域。 下面看一下vm_area_struct数据结构: struct vm_area_struct { /*ThefirstcachelinehastheinfoforVMAtreewalking.*/ unsigned long vm_start; /*Ourstartaddresswithinvm_mm.*/ unsigned long vm_end; /*Thefirstbyteafterourendaddresswithinvm_mm.*/ /*linkedlistofVMareaspertask,sortedbyaddress*/ struct vm_area_struct * vm_next ,* vm_prev ; struct rb_node vm_rb ; struct mm_struct * vm_mm ; /*Theaddressspacewebelongto.*/ struct list_head anon_vma_chain ; /*Serializedbymmap_sem& *page_table_lock*/ struct anon_vma * anon_vma ; /*Serializedbypage_table_lock*/ /*Functionpointerstodealwiththisstruct.*/ const struct vm_operations_struct * vm_ops ; struct file * vm_file ; /*Filewemapto(canbeNULL).*/ void *vm_private_data; /*wasvm_pte(sharedmem)*/ }__randomize_layout; vm_start、vm_end:为该区域在用户空间的起始和结束地址 vm_next、vm_prev:将该区域添加到链表上,便于管理。 vm_rb:将这个区域放到红黑树上 vm_ops:对该区域可以进行的内存操作 anon_vma:匿名映射 vm_file:文件映射 用户态空间的每个区域都由该结构体来管理,最终形成下面的这个结构: image-20231008184824770 顺便介绍一下 我的圈子: 高级工程师聚集地 ,期待大家的加入。 2、内核态空间管理 上面,我们从源码角度了解了用户态空间管理,下面我们看内核态空间管理。 回顾一下,我们内核态的布局情况是怎么样的呢,还记得吗? image-20231005155942462 我们要知道: 内核态的虚拟空间和任何一个进程都没有关系,所有的进程看到的内核态虚拟空间都是一样的。 在内核态,我们直接操作的依旧是虚拟地址,而非物理地址 不同CPU结构下,内核态空间的布局格式是不变的,但是大小会有所调整,比如ARM和X86的大小空间有所不同。 内核态空间管理并不像用户态那样使用结构体来统一管理,而是直接使用宏来定义每个区域的分界线, 下面我们以x86架构来分析内核态空间的管理 2.1 分界线定义 /* *Userspaceprocesssize:3GB(default). */ # define TASK_SIZEPAGE_OFFSET /*PAGE_OFFSET-thevirtualaddressofthestartofthekernelimage*/ # define PAGE_OFFSET((unsignedlong)__PAGE_OFFSET) # define __PAGE_OFFSET__PAGE_OFFSET_BASE # define __PAGE_OFFSET_BASE_AC(CONFIG_PAGE_OFFSET,UL) configPAGE_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 dependsonX86_32 TASK_SIZE:内核态空间与用户态空间的分界线 PAGE_OFFSET:该宏表示内核镜像起始的虚拟地址。 CONFIG_PAGE_OFFSET:这个宏定义的值,根据实际情况自行设定,默认为0XC0000000,可以设置为0X80000000等。 以上,TASK_SIZE就被定义为0XC0000000作为用户态空间和内核态空间的分界线,将4G虚拟内存分配为3G/1G结构。 image-20231010072937276 2.2 直接映射区定义 直接映射区是定义在PAGE_OFFSET和high_memory之间的区域。 PAGE_OFFSET:表示内核镜像的起始地址,上文已经说明。 high_memory也是表示的就是896M这个值,表示高端内存的分界线。 顺便说明以下,TASK_SIZE和PAGE_OFFSET在不同架构下是不同的,在ARM架构下,两者并不相等,本文以X86架构为例 image-20231010073949813 2.3 安全保护区定义 系统会在high_memory和VMALLOC_START之间预留8M的安全保护区,防止访问越界。 VMALLOC_OFFSET表示的是内核动态映射区的偏移,也就是所谓的安全保护区。 # define VMALLOC_START(((unsignedlong)high_memory+VMALLOC_OFFSET)&~(VMALLOC_OFFSET-1)) # define VMALLOC_OFFSET(8*1024*1024) 可以很清楚的看到VMALLOC_OFFSET定义了8M的空间,VMALLOC_START在high_memory基础上,偏移了VMALLOC_OFFSET 8M空间大小作为安全保护区,以防越界访问。 image-20231010074810831 2.3 动态映射区定义 VMALLOC_START和VMALLOC_END之间称为内核动态映射区。 和用户态进程使用 malloc 申请内存一样,在这块动态映射区内核是使用 vmalloc 进行内存分配。 # define VMALLOC_START(((unsignedlong)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_BASE 到 FIXADDR_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((unsignedlong)__FIXADDR_TOP) # define FIXADDR_TOT_SIZE(__end_of_fixed_addresses<