上一节,我们主要了解了虚拟内存空间的布局情况,趁热打铁,我们直接从源代码的视角,来看一下Linux内核是如何管理虚拟内存空间的。
废话不多说,直接开始!
1、用户态空间管理读完上一节我们知道,用户态的布局情况如下:
image-20231005160139650我们运行的可执行程序,被加载进内存后,会作为一个进程存在,这个进程Linux内核会将其抽象成一个结构体。没错,它就是task_struct。
1.1 task_struct结构体task_struct结构体是进程的抽象,进程所涉及到的内容非常多,下面只列举出一些重要的数据结构,方面理解。
// include/linux/sched.h如上,进程抽象为task_struct结构体,通过mm_struct结构体来管理虚拟内存空间。
1.2 mm_struct结构体每个进程都有唯一的 mm_struct 结构体,也就是前边提到的每个进程的虚拟地址空间都是独立,互不干扰的。
mm_struct的结构体如下:
// include/linux/mm_types.h1.3 内核态和用户态的划分
mm_struct里面定义的task_size变量,就是用来划分虚拟内存的用户空间和内核空间的。
unsigned long task_size;task_size也就是两者的分界线,下面我们看下task_size是如何被赋值的。
当我们执行一个新的进程的时候,Linux内核会执行load_elf_binary的API接口,进而调用setup_new_exec函数来实现新进程的创建。
在setup_new_exec函数中,会执行
current->mm->task_size = TASK_SIZE;这个TASK_SIZE就是我们设置的内核空间地址和用户空间地址的分界线,由我们自定义配置。
这里我们只需要知道TASK_SIZE默认值3为PAGE_OFFSET,并且默认为0xC0000000为分界线的,即用户空间3GB,内核空间1GB;当然这个可以由我们动态配置,可以配置PAGE_OFFSET为0x80000000,即用户空间和内核空间均为2GB,取决于我们的应用场合,当你看到与我们讲解不同时,也不用大惊小怪。
image.png以上,表达的概念很简单,如下图:
1.4 位置信息描述
我们知道用户态内存空间分为几个区域:代码段、数据段、BSS段、堆、文件映射和匿名映射区、栈等几个部分,同样在mm_struct中,定义了这些区域的统计信息和位置。
unsigned long mmap_base; /* base of mmap area */由于物理内存比较小,当内存吃紧的时候,就会发生换入换出的操作,即将暂时不用的页面换出到硬盘上,有的页面比较重要,不能换出。
整体的布局情况如下:
image.png1.5 区域属性描述
尽管已经有了一些变量来描述每一个段的信息,但是Linux内核在mm_struct结构体里面,还有一个专门的数据结构vm_area_struct来管理每个区域的属性。
struct vm_area_struct *mmap; /* list of VMAs */mmap:为一个单链表,将所有的区域串联起来
mm_rb:为一个红黑树,方便查找和修改内存区域。
下面看一下vm_area_struct数据结构:
struct vm_area_struct {vm_start、vm_end:为该区域在用户空间的起始和结束地址
vm_next、vm_prev:将该区域添加到链表上,便于管理。
vm_rb:将这个区域放到红黑树上
vm_ops:对该区域可以进行的内存操作
anon_vma:匿名映射
vm_file:文件映射
用户态空间的每个区域都由该结构体来管理,最终形成下面的这个结构:
image-20231008184824770
2、内核态空间管理顺便介绍一下 我的圈子:高级工程师聚集地,期待大家的加入。
上面,我们从源码角度了解了用户态空间管理,下面我们看内核态空间管理。
回顾一下,我们内核态的布局情况是怎么样的呢,还记得吗?
image-20231005155942462我们要知道:
内核态空间管理并不像用户态那样使用结构体来统一管理,而是直接使用宏来定义每个区域的分界线,
2.1 分界线定义 /*下面我们以x86架构来分析内核态空间的管理
TASK_SIZE:内核态空间与用户态空间的分界线
PAGE_OFFSET:该宏表示内核镜像起始的虚拟地址。
CONFIG_PAGE_OFFSET:这个宏定义的值,根据实际情况自行设定,默认为0XC0000000,可以设置为0X80000000等。
以上,TASK_SIZE就被定义为0XC0000000作为用户态空间和内核态空间的分界线,将4G虚拟内存分配为3G/1G结构。
image-20231010072937276 2.2 直接映射区定义直接映射区是定义在PAGE_OFFSET和high_memory之间的区域。
image-20231010073949813 2.3 安全保护区定义顺便说明以下,TASK_SIZE和PAGE_OFFSET在不同架构下是不同的,在ARM架构下,两者并不相等,本文以X86架构为例
系统会在high_memory和VMALLOC_START之间预留8M的安全保护区,防止访问越界。
VMALLOC_OFFSET表示的是内核动态映射区的偏移,也就是所谓的安全保护区。
可以很清楚的看到VMALLOC_OFFSET定义了8M的空间,VMALLOC_START在high_memory基础上,偏移了VMALLOC_OFFSET 8M空间大小作为安全保护区,以防越界访问。
image-20231010074810831 2.3 动态映射区定义VMALLOC_START和VMALLOC_END之间称为内核动态映射区。
和用户态进程使用 malloc 申请内存一样,在这块动态映射区内核是使用 vmalloc 进行内存分配。
PKMAP_BASE:是永久映射区的起始地址。
VMALLOC_END:在永久映射区的起始地址下,偏移2个PAGE_SIZE作为安全保护区。
image-20231010075717944 2.4 永久映射区定义PKMAP_BASE 到 FIXADDR_START 的空间称为永久内核映射,在内核的这段虚拟地址空间中允许建立与物理高端内存的长期映射关系。
比如内核通过 alloc_pages() 函数在物理内存的高端内存中申请获取到的物理内存页,这些物理内存页可以通过调用 kmap 映射到永久映射区中。
FIXADDR_START到FIXADDR_TOP的空间称为固定映射区,主要用于满足特殊的需求。
固定映射区中的虚拟地址,可以自由映射到物理内存的高端地址空间上,特点是其映射的虚拟地址是不变的,物理地址是可以改变的。
image-20231011195006927 2.6 临时映射区定义最后FIXADDR_TOP到0xFFFFFFFF之间的区域称为临时映射区。
它主要用来做什么呢,网上举的一个例子,大家参考以下。
假设用户态的进程要映射一个文件到内存中,先要映射用户态进程空间的一段虚拟地址到物理内存,然后将文件内容写入这个物理内存供用户态进程访问。
给用户态进程分配物理内存页可以通过 alloc_pages(),分配完毕后,按说将用户态进程虚拟地址和物理内存的映射关系放在用户态进程的页表中,就完事大吉了。这个时候,用户态进程可以通过用户态的虚拟地址,也即 0 至 3G 的部分,经过页表映射后访问物理内存,并不需要内核态的虚拟地址里面也划出一块来,映射到这个物理内存页。
但是如果要把文件内容写入物理内存,这件事情要内核来干了,这就只好通过 kmap_atomic 做一个临时映射,写入物理内存完毕后,再 kunmap_atomic 来解映射即可。
image-20231011195939077以上,就是内核态空间的布局以及管理。
3、总结该篇文章,主要从源码角度来了解用户态空间和内核态空间是如何管理的,挪用大佬的一个图片,结合上面所讲的,相信很快就能茅塞顿开。
image-20231011200638903作者: _嵌入式艺术_, 来源:面包板社区
链接: https://mbb.eet-china.com/blog/uid-me-4040659.html
版权声明:本文为博主原创,未经本人允许,禁止转载!
文章评论(0条评论)
登录后参与讨论