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

 更新时间:2025年08月20日 09:39:33   作者:驱动探索者  
Linux进程地址空间通过VMA和mm_struct管理,包含代码段、数据段、堆栈、mmap等区域,利用页表实现隔离,处理缺页异常,支持匿名与文件映射,系统调用如brk/mmap用于内存分配与管理

进程地址空间

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

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

内存区域

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

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

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

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

  • 代码段映射:可执行文件中包含只读并可执行的程序头,如代码段和 init 段等
  • 数据段映射:可执行文件中包含可读/可写的程序头,如数据段和未初始化数据段等
  • 用户进程栈:通常位于用户空间的最高地址,从上往下延伸。它包含栈帧,里面包含了局部变量和函数调用参数等
  • mmap 映射区域:位于用户进程栈下面,主要用于 mmap 系统调用
  • 堆映射区域:malloc() 函数分配的进程虚拟地址就是这段区域

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

mm_struct 数据结构

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

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

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

  • mmap:进程里所有的 VMA 形成一个单链表,这是该链表的头
  • mmrb:VMA 红黑树的根节点
  • get_unmapped_area:用于判断虚拟内存空间是否有足够的空间,返回一段没有映射过的空间的起始地址,这个函数会使用具体的处理器架构的实现
  • mmap_base:指向 mmap 空间的起始地址。在 32 位处理器中,mmap 空间的起始地是 0x4000 0000
  • pgd:指向进程的 PGD(一级页表)
  • mm_users:记录正在使用该进程地址空间的进程数目,如果两个线程共享该地址空间那么 mm_users 的值等于 2
  • mm_count:mm_struct 结构体的主引用计数
  • mmap_sem:保护 VMA 的一个读写信号量
  • mmlist:所有的 mm_struct 数据结构都连接到一个双向链表中,该链表的头是 init_mm 内存描述符,它是 init 进程的地址空间
  • start_code,end_code:代码段的起始地址和结束地址
  • start_data,end_data:数据段的起始地址和结束地址
  • start_brk:堆空间的起始地址
  • brk:表示当前堆中的 VMA 的结束地址
  • total_vm:已经使用的进程地址空间总和

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

VMA 数据结构

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

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

  • vm_startvm_end:指定 VMA 在进程地址空间的起始地址和结束地址
  • vm_nextvm_prev:进程的 VMA 都连接成一个链表
  • vmrb:VMA 作为一个节点加入红黑树,每个进程的 mm_struct 数据结构中都有一棵红黑树 mm->mm_rb
  • vm_mm:指向该 VMA 所属进程的 mm_struct 数据结构
  • vm_page_prot:VMA 的访问权限
  • vm_flags:描述该 VMA 的一组标志位
  • anon_vma_chainanon_vma:用于管理反向映射(Reverse Mapping,RMAP)
  • vm_ops:指向许多方法的集合,这些方法用于在 VMA 中执行各种操作,通常用于文件映射
  • vm_pgoff:指定文件映射的偏移量,这个变量的单位不是字节,而是页面的大小件映射。(PAGE SIZE)。对于匿名页面来说,它的值可以是 0 或者 vm_addr/PAGE_SIZE
  • vm_file:指向 file 的实例,描述一个被映射的文件

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

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

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

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

  • task_struct 结结构中有一个 mm 成员指向进程的内存管理描述符 mm_struct 数据结构
  • 可以通过 mm_struct 数据结构中的 mmap 成员来遍历所有的 VMA
  • 也可以通过 mm_struct 数据结构中的 mm_rb 成员来遍历和查找 VMA
  • mm_struct 数据结构的 pgd 成员指向进程的页表,每个进程都有一份独立的页表
  • 当 CPU 第一次访问虚拟地址空间时会触发缺页异常。在缺页异常处理中,分配物理页面,利用分配的物理页面来创建页表项并且填充页表,完成虚拟地址到物理地址的映射关系的建立

VMA 的属性

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

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

  • VM_READ: 可读属性
  • VM_WRITE: 可写属性
  • VM_EXEC: 可执行
  • VM_SHARED: 允许被多个进程共享
  • VM_MAYREAD: 允许设置 VM_READ 属性
  • VM_MAYWRITE: 允许设置 VM WRITE 属性
  • VM_MAYEXEC: 允许设置 VM EXEC 属性
  • VM_MAYSHARE: 允许设置 VM SHARED 属性
  • VM_GROWSDOWN: 该 VMA 允许向低地址增长
  • VM_UFFD_MISSING: 表示该 VMA 适用于用户态的缺页异常处理
  • VM_PFNMAP: 表示使用纯正的 PFN,不需要使用内核的 page 数据结构来管理物理页面
  • VM_DENYWRITE: 表示不允许写入
  • VM_UFFD_WP: 用于页面的写保护跟踪
  • VM_LOCKED: 表示该 VMA 的内存会立刻分配物理内存,并且页面被锁定,不会被交换到交换分区
  • VM_IO: 表示 I/0 内存映射
  • VM_SEQ_READ: 表示应用程序会顺序读该 VMA 的内容
  • VM_RAND_READ: 表示应用程序会随机读该 VMA 的内容
  • VM_DONTCOPY: 表示在创建分支时不要复制该 VMA
  • VM_DONTEXPAND: 通过 mremapo 系统调用禁止 VMA 扩展
  • VM_ACCOUNT: 在创建 IPC 以共享 VMA 时,检测是否有足够的空闲内存用于映射
  • VM_HUGETLB: 用于巨页的映射
  • VM_SYNC: 表示同步的缺页异常
  • VM_ARCH_1: 与架构相关的标志位
  • VM_WIPEONFORK: 表示不会从父进程相应的 VMA 中复制页表到子进程的 VMA 中
  • VM_DONTDUMP: 表示该 VMA 不包含到核心转储文件中
  • VM_SOFTDIRTY: 软件模拟实现的脏位。用于一些特殊的架构,需要打开 CONFIG_MEM_SOFT_DIRTY
  • VM_MIXEDMAP: 表示混合使用了纯 PFN 以及 page 数据结构的页面,如使用 vm_insert_page() 函数插入 VMA
  • VM_HUGEPAGE: 表示在 madvise 系统调用中使用 MADV_HUGEPAGE 标志位来标记该 VMA
  • VM_NOHUGEPAGE: 表示在 madvise 系统调用中使用 MADV_NOHUGEPAGE 标志位来标记该 VMA
  • VM_MERGEABLE: 表示该 VMA 是可以合并的,用于 KSM 机制
  • VM_SPECIAL: 表示该 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)来看,它究竟包含哪些页表项的标志位。

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

  • PTE_TYPE_PAGE:表示这是一个基于页面的页表项,即设置页表项的 Bit[1:O]
  • PTE_AF:设置访问位
  • PTE_SHARED:设置内存共享属性
  • MT_NORMAL:设置内存属性为 normal
  • PTE_USER:设置 AP 访问位,允许通过用户权限访问该内存
  • PTE_NG:设置该内存对应的 TLB 只属于该进程
  • PTE_PXN:表示该内存不能在特权模式下执行
  • PTE_UXN:表示该内存不能在用户模式下执行
  • PTE_RDONLY:表示只读属性

内核如何管理内存

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 函数的参数如下。

  • addr:用于指定映射到进程地址空间的起始地址,为了提高应用程序的可移植性,一般设置为 NULL,让内核来分配一个合适的地址
  • length:表示映射到进程地址空间的大小
  • prot:用于设置内存映射区域的读写属性等
  • flags:用于设置内存映射的属性,如共享映射、私有映射等
  • fd:表示这是一个文件映射,fd 是打开的文件的句柄
  • offset:在文件映射时,表示文件的偏移量。prot 参数通常表示映射页面的读写权限,有如下参数组合
  • PROT_EXEC:表示映射的页面是可以执行的
  • PROT_READ:表示映射的页面是可以读取的
  • PROT_WRITE:表示映射的页面是可以写入的
  • PROT_NONE:表示映射的页面是不可访问的

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

  • MAP_SHARED:创建一个共享映射的区域。多个进程可以通过共享映射方式来映射一个文件,这样其他进程也可以看到映射内容的改变,修改后的内容会同步到磁盘文件中
  • MAP_PRIVATE:创建一个私有的写时复制的映射。多个进程可以通过私有映射的方式来映射一个文件,这样其他进程不会看到映射内容的改变,修改后的内容也不会同步到磁盘文件中
  • MAP_ANONYMOUS:创建一个匿名映射,即没有关联到文件的映射
  • MAP_FIXED:使用参数 addr 创建映射,如果在内核中无法映射指定的地址,那么 mmap 会返回失败,参数 addr 要求按页对齐。如果 addr 和 length 指定的进程地址空间和已有的 VMA 重叠,那么内核会调用 do_munmapO 函数把这段重叠区域销毁,然后重新映射新的内容
  • MAP_POPULATE:对于文件映射来说,会提前预读文件内容到映射区域,该特性只支持私用映射

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

  • 匿名映射:没有映射对应的相关文件,匿名映射的内存区域的内容会初始化为 0
  • 文件映射:映射和实际文件相关联,通常把文件内容映射到进程地址空间,这样应用程序就可以像操作进程地址空间一样读写文件

私有匿名映射

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

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

共享匿名映射

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

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

  • 使 fd=-1 且 flags = MAP_ANONYMOUS | MAP_SHARED。在这种情况下,do_mmap_pgoffO->mmap_region() 函数最终会调用 shmem_zero_setup():来打开一个特殊的“/dev/zero”设备文件
  • 直接打开“/dev/zero”设备文件,然后使用这个文件句柄来创建 mmap

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

私有文件映射

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

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

共享文件映射

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

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

  • 读写文件。把文件内容映射到进程地址空间,同时对映射的内容做了修改,内核的回写(writeback)机制最终会把修改的内容同步到磁盘中
  • 进程间通信。进程之间的进程地址空间相互隔离,一个进程不能访问另外一个进程的地址空间。如果多个进程同时映射到一个文件,就实现了多进程间的共享内存通信。如果一个进程对映射内容做了修改,那么另外的进程是可以看到的

总结

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

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

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

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

相关文章

  • 浅谈Linux环境变量文件介绍

    浅谈Linux环境变量文件介绍

    这篇文章主要介绍了浅谈Linux环境变量文件介绍,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧
    2020-04-04
  • Linux 中不输入密码运行 sudo 命令的方法

    Linux 中不输入密码运行 sudo 命令的方法

    sudo命令允许受信任的用户作为另一个用户运行程序,默认情况下是root用户。这篇文章主要介绍了Linux 中不输入密码运行 sudo 命令的方法,需要的朋友可以参考下
    2019-07-07
  • ubuntu如何开启ssh远程登录

    ubuntu如何开启ssh远程登录

    在Ubuntu上开启SSH远程登录的步骤如下:检查SSH安装情况、启动SSH服务、设置开机启动、检查SSH状态、配置防火墙、并使用SSH客户端进行远程登录
    2025-03-03
  • CentOS 7如何快速开放端口

    CentOS 7如何快速开放端口

    这篇文章主要为大家详细介绍了CentOS 7如何快速开放端口,如何使用firewalld开放Linux端口,具有一定的参考价值,感兴趣的小伙伴们可以参考一下
    2017-01-01
  • Linux unlink函数和删除文件的操作方法

    Linux unlink函数和删除文件的操作方法

    这篇文章主要介绍了Linux unlink函数和删除文件的操作方法,本文给大家介绍的非常详细,具有一定的参考借鉴价值,需要的朋友可以参考下
    2020-02-02
  • linux开机启动nodemanager步骤

    linux开机启动nodemanager步骤

    这篇文章主要介绍了linux开机启动nodemanager的步骤,大家参考使用吧
    2014-01-01
  • Linux 常用的文件基本指令

    Linux 常用的文件基本指令

    这篇文章主要介绍了Linux的常用的文件指令,文章中详细列出了常用指令,感兴趣的小伙伴可以参考一下
    2023-04-04
  • Linux修改pip和conda缓存路径的几种方法

    Linux修改pip和conda缓存路径的几种方法

    在 Python 生态中,pip 和 conda 是两种常见的软件包管理工具,它们在安装、更新和卸载软件包时都会使用缓存来提高效率,适当地修改它们的缓存路径,不仅可以优化存储管理,还可以在存储空间有限,本文将详细介绍如何修改 pip 和 conda 的缓存路径
    2025-03-03
  • linux中高并发socket最大连接数的优化详解

    linux中高并发socket最大连接数的优化详解

    这篇文章主要给大家介绍了关于linux中高并发socket最大连接数优化的相关资料,文中介绍的很详细,相信对大家具有一定的参考价值,需要的朋友们下面来一起学习学习吧。
    2017-02-02
  • 详解如何备份及恢复 Linux 文件权限

    详解如何备份及恢复 Linux 文件权限

    这篇文章主要介绍了详解如何备份及恢复 Linux 文件权限,小编觉得挺不错的,现在分享给大家,也给大家做个参考。一起跟随小编过来看看吧
    2017-03-03

最新评论