Linux

关注公众号 jb51net

关闭
首页 > 网站技巧 > 服务器 > Linux > Linux进程地址空间

Linux进程地址空间的使用及说明

作者:驱动探索者

Linux进程地址空间通过VMA和mm_struct管理,包含代码段、数据段、堆栈、mmap等区域,利用页表实现隔离,处理缺页异常,支持匿名与文件映射,系统调用如brk/mmap用于内存分配与管理

进程地址空间

进程地址空间在内核中使用 vm_area_struct 数据结构来描述,简称 VMA,表示进程地址空间或进程线性区。

由于这些地址空间属于各个用户进程,因此在用户进程的 mm_struct 数据结构中有相应的成员,用于对这些 VMA 进行管理。

内存区域

进程地址空间(process address space)是指进程可寻址的虚拟地址空间,进程可以通过内核的内存管理机制动态地添加和删除内存区域,这些内存区域在 Linux 内核采用 VMA 数据结构来抽象描述。

每个内存区域具有相关的权限,如可读、可写或者可执行权限。若一个进程访问了不在有效范围的内存区域,或者非法访问了内存区域,或者以不正确的方式访问了内存区域,那么处理器会报告缺页异常。

在 Linux 内核的缺页异常处理中会处理这些情况,严重的会报告“SegmentFault'”并终止该进程。

内存区域主要包含内容如下:

每个进程都有一套页表,这样每个进程地址空间就是相互隔离的。即使两个进程地址空间的虚拟地址是相同的,但是经过两套不同页表的转换之后,它们也会对应不同的物理地址。

mm_struct 数据结构

Linux 内核需要管理每个进程所有的内存区域以及它们对应的页表映射,所以必须抽象出一个数据结构,这就是 mm_struct 数据结构。

进程控制块(Process Control Block,PCB)数据结构 task_struct 中有一个指针 mm,该指针指向这个 mm_struct 数据结构。mm_struct 数据结构定义在 include/linux/mm_types.h 文件中,下面是它的主要成员。

mm_struct 数据结构中主要成员的含义如下:

从进程的角度来观察内存管理,可以沿着 mm_struct 数据结构进行延伸和思考,如下图所示。

VMA 数据结构

VMA(vm_area_struct)数据结构定义在 mm_types.h 文件中,其主要成员如下。

VMA 数据结构中各个成员的含义如下:

mm_struct 数据结构是描述进程内存管理的核心数据结构,该数据结构提供了管理 VMA 所需要的信息,每个 VMA 都要连接到 mm_struct 中的链表和红黑树,以方便查找。

VMA 按照起始地址以递增的方式插入 mm_struct->mmp 链表中。

当进程拥有大量的 VMA 时,扫描链表和查找特定的 VMA 是非常低效的操作,如在云计算的机器中,所以内核中通常需要红黑树来协助,以便提高查找速度。

站在进程的角度来看,我们可以从进程控制块。task_struct 数据结构里顺藤摸瓜找到该进程所有的 VMA,如图所示。

VMA 的属性

作为一个进程地址空间的区间,VMA 是有属性的,如可读/可写、共享等属性。

vm_flags 成员描述这些属性,描述了该 VMA 的全部页面信息,包括如何映射页面、访问每个页面的权限等信息,VMA 属性的标志位如下所示。

VMA 属性的标志位可以任意组合,但是最终要落实到硬件机制上,即页表项的属性中。VMA 属性到页表属性的转换如下图所示。vm_area_struct 数据结构中有两个成员和属性相关:一个是 vm_flags 成员,用于描述 VMA 的属性;另外一个是 vm_page_prot 成员,用于将 VMA 属性标志位转换成与处理器相关的页表项的属性,它和具体架构相关。

在创建一个新的 VMA 时使用 vm_get_page_prot() 函数可以把 vm_flags 标志位转化成具页表项的硬件标志位。

这个转化过程得益于内核预先定义了一个内存属性数组 protection_map[], 我们只需要根据 vm_flag 标志位来查询这个数组即可,在这个场景下,通过查询 protection_map[] 数组可以获得页表属性。

protection_map[] 数组的每个成员代表一个属性的组合,如__P000 表示无效的 PTE 属性,__P001 表示只读属性,__P1O0 表示可执行属性(PAGE_EXECONLY)等。

下面以只读属性(PAGE_READONLY)来看,它究竟包含哪些页表项的标志位。

把上述的宏全部展开,我们可以得到如下页表项的标志位。

内核如何管理内存

Linux 进程在内核中作为进程描述符 task_struct 的实例实现。task_struct 中的 mm 字段指向内存描述符 mm_struct ,它是程序内存的执行内容。它存储了如上所示的内存段的开始和结束、进程使用的物理内存页数(rss 代表 Resident Set Size)、使用的虚拟地址空间 以及其他信息。

每个虚拟内存区域(VMA)是一个连续的虚拟地址范围;这些区域永远不会重叠。vm_area_struct 的实例完整地描述了一个内存区域,包括其起始和结束地址、用于确定访问权限和行为的标志,以及用于指定该区域映射的文件(如果有)的 vm_file 字段。不映射文件的 VMA 是匿名的。除了内存映射段之外,上面的每个内存段(例如,堆、堆栈)对应于单个 VMA。这不是必需的,尽管这在 x86 机器中很常见。VMA 不关心它们位于哪个段。 程序的 VMA 都以链表形式存储在其内存描述符中 mmap 字段,按起始虚拟地址排序,并作为以 mm_rb 字段为根的红黑树 。红黑树允许内核快速搜索覆盖给定虚拟地址的内存区域。当您读取文件/proc/pid_of_process/maps 时,内核只是遍历进程的 VMA 链接列表并打印每一个 VMA。

VMA 的大小必须是页面大小的倍数。处理器查阅页表以将虚拟地址转换为物理内存地址。每个进程都有自己的一组页表;每当发生进程切换时,用户空间的页表也会切换。Linux 在内存描述符的 pgd 字段中存储指向进程页表的指针。页表中的每个虚拟页都对应一个页表项 (PTE),在常规 x86 分页中,它是一个简单的 4 字节记录,如下所示:

malloc函数

malloc() 函数是 C 标准库封装的一个核心函数,C 标准库做一些处理后会调用 Linux 的系线调用接口 brk 向系统申请内存。

brk 系统调用

brk 系统调用主要实现在 mm/mmap.c 文件中。

详细流程这里不一一列出来了,下面用一张图概括 brk 的流程,如下:

malloc流程

假设不考虑 libc 的因素,malloc() 分配 100 字节,那么内核会分配多少字节呢?处理器的 MMU 的最小处理单元是页面,所以内核分配内存、建立虚拟地址和物理地址映射关系都以页面为单位,PAGE_ALIGN(addr)宏让地址按页面大小对齐。

下图所示为 malloc() 函数的实现流程。

mmap函数

mmap/munmap 函数是用户空间中常用的系统调用函数,无论是在用户程序中分配内存、读写大文件、链接动态库文件,还是多进程间共享内存,都可以看到 mmp/munmap() 函数的身影。mmp/munmap 函数的声明如下。

mmap/munmap 函数的参数如下。

flags 参数是一个很重要的参数,可以设置为以下值。

通过参数 fd 可以看出 mmap 映射是否和文件相关联,因此在 Linux 内核中,映射可以分成匿名映射和文件映射。

私有匿名映射

当使用参数 fd=-1 且 flags = MAP_ANONYMOUS|MAP_PRIVATE 时,创建的 mmap 映射是私有匿名映射。

私有匿名映射常见的用途是在 glbc 分配大内存块时,如果需要分配的内存大 MMAP_THREASHOLD(128KB),glibc 会默认使用 mmap 代替 brk 来分配内存。

共享匿名映射

当使用参数 fd=-1 且 flags = MAP_ANONYMOUS | MAP_SHARED 时,创建的 mmap 映射是共享匿名映射。

共享匿名映射让相关进程共享一块内存区域,通常用于父、子进程之间的通信创建共享匿名映射有如下两种方式。

上述两种方式最终都调用 shmem 模块来创建共享匿名映射。

私有文件映射

创建文件射时如果 flags 设置为 MAPP_PRIVATE,就会创建私有文件映射。

私有文件映射常用的场景是加载动态共享库。

共享文件映射

创建文件映射时,如果 flags 设置为 MAP_SHARED,就会创建共享文件映射。

如果 prot 参数指定了 PROT_WRITE,那么打开文件时需要指定 O_RDWR 标志位。共享文件映射通常有 mmap 如下两个常用的场景。

总结

mmap 机制在 Linux 内核中实现的代码框架和 brk 机制非常类似,其中有很多关于 VMA 的操作。

mmap 机制和缺页中断机制结合在一起会变得复杂很多。

mmap 机制在 Linux 内核中的实现流程如图所示。

以上为个人经验,希望能给大家一个参考,也希望大家多多支持脚本之家。

您可能感兴趣的文章:
阅读全文