当前位置: 首页 > news >正文

【Linux系统】进程地址空间

我们有提到过内存分布,可是我们对他并不理解!这次我们再来回顾一下,可以先对其进行各区域分布验证:

代码语言:javascript

AI代码解释

#include <stdio.h> #include <unistd.h> #include <stdlib.h> int g_unval; int g_val = 100; int main(int argc, char *argv[], char *env[]) { const char *str = "helloworld"; printf("code addr: %p\n", main); printf("init global addr: %p\n", &g_val); printf("uninit global addr: %p\n", &g_unval); static int test = 10; char *heap_mem = (char*)malloc(10); char *heap_mem1 = (char*)malloc(10); char *heap_mem2 = (char*)malloc(10); char *heap_mem3 = (char*)malloc(10); printf("heap addr: %p\n", heap_mem); //heap_mem(0), &heap_mem(1) printf("heap addr: %p\n", heap_mem1); //heap_mem(0), &heap_mem(1) printf("heap addr: %p\n", heap_mem2); //heap_mem(0), &heap_mem(1) printf("heap addr: %p\n", heap_mem3); //heap_mem(0), &heap_mem(1) printf("test static addr: %p\n", &test); //heap_mem(0), &heap_mem(1) printf("stack addr: %p\n", &heap_mem); //heap_mem(0), &heap_mem(1) printf("stack addr: %p\n", &heap_mem1); //heap_mem(0), &heap_mem(1) printf("stack addr: %p\n", &heap_mem2); //heap_mem(0), &heap_mem(1) printf("stack addr: %p\n", &heap_mem3); //heap_mem(0), &heap_mem(1) printf("read only string addr: %p\n", str); for(int i = 0 ;i < argc; i++) { printf("argv[%d]: %p\n", i, argv[i]); } for(int i = 0; env[i]; i++) { printf("env[%d]: %p\n", i, env[i]); } return 0; }

运行结果;

代码语言:javascript

AI代码解释

ltx@hcss-ecs-d90d:~/lesson5$ touch code.c ltx@hcss-ecs-d90d:~/lesson5$ vim code.c ltx@hcss-ecs-d90d:~/lesson5$ touch Makefile ltx@hcss-ecs-d90d:~/lesson5$ vim Makefile ltx@hcss-ecs-d90d:~/lesson5$ make gcc -o code code.c ltx@hcss-ecs-d90d:~/lesson5$ ./code code addr: 0x55fbceb76189 init global addr: 0x55fbceb79010 uninit global addr: 0x55fbceb7901c heap addr: 0x55fbd04566b0 heap addr: 0x55fbd04566d0 heap addr: 0x55fbd04566f0 heap addr: 0x55fbd0456710 test static addr: 0x55fbceb79014 stack addr: 0x7fff7f9936c0 stack addr: 0x7fff7f9936c8 stack addr: 0x7fff7f9936d0 stack addr: 0x7fff7f9936d8 read only string addr: 0x55fbceb77004 argv[0]: 0x7fff7f99473b env[0]: 0x7fff7f994742 env[1]: 0x7fff7f994752 env[2]: 0x7fff7f994760 env[3]: 0x7fff7f99477a env[4]: 0x7fff7f994790 env[5]: 0x7fff7f99479c env[6]: 0x7fff7f9947b1 env[7]: 0x7fff7f9947c0 env[8]: 0x7fff7f9947cf env[9]: 0x7fff7f9947e0 env[10]: 0x7fff7f994dcf env[11]: 0x7fff7f994e04 env[12]: 0x7fff7f994e26 env[13]: 0x7fff7f994e3d env[14]: 0x7fff7f994e48 env[15]: 0x7fff7f994e68 env[16]: 0x7fff7f994e71 env[17]: 0x7fff7f994e88 env[18]: 0x7fff7f994e90 env[19]: 0x7fff7f994ea3 env[20]: 0x7fff7f994ec2 env[21]: 0x7fff7f994ee5 env[22]: 0x7fff7f994f26 env[23]: 0x7fff7f994f8e env[24]: 0x7fff7f994fc4 env[25]: 0x7fff7f994fd7 env[26]: 0x7fff7f994fe0

通过结果可以看到,地址时依次增大的


2. 虚拟地址

来段代码感受一下

代码语言:javascript

AI代码解释

#include <stdio.h> #include <unistd.h> #include <stdlib.h> int gval = 0; int main() { pid_t id = fork(); if(id == 0) { while(1) { printf("子: gval: %d, &gval: %p, pid: %d, ppid: %d\n", gval, &gval, getpid(), getppid()); sleep(1); gval++; } } else { while(1) { printf("父: gval: %d, &gval: %p, pid: %d, ppid: %d\n", gval, &gval, getpid(), getppid()); sleep(1); } } return 0; }

运行结果:

代码语言:javascript

AI代码解释

ltx@hcss-ecs-d90d:~/lesson5$ ./code 父: gval: 0, &gval: 0x55ab2eaf6014, pid: 878687, ppid: 878659 子: gval: 0, &gval: 0x55ab2eaf6014, pid: 878688, ppid: 878687 父: gval: 0, &gval: 0x55ab2eaf6014, pid: 878687, ppid: 878659 子: gval: 1, &gval: 0x55ab2eaf6014, pid: 878688, ppid: 878687 子: gval: 2, &gval: 0x55ab2eaf6014, pid: 878688, ppid: 878687 父: gval: 0, &gval: 0x55ab2eaf6014, pid: 878687, ppid: 878659 父: gval: 0, &gval: 0x55ab2eaf6014, pid: 878687, ppid: 878659 子: gval: 3, &gval: 0x55ab2eaf6014, pid: 878688, ppid: 878687 子: gval: 4, &gval: 0x55ab2eaf6014, pid: 878688, ppid: 878687 父: gval: 0, &gval: 0x55ab2eaf6014, pid: 878687, ppid: 878659 子: gval: 5, &gval: 0x55ab2eaf6014, pid: 878688, ppid: 878687 父: gval: 0, &gval: 0x55ab2eaf6014, pid: 878687, ppid: 878659 子: gval: 6, &gval: 0x55ab2eaf6014, pid: 878688, ppid: 878687 父: gval: 0, &gval: 0x55ab2eaf6014, pid: 878687, ppid: 878659 子: gval: 7, &gval: 0x55ab2eaf6014, pid: 878688, ppid: 878687 父: gval: 0, &gval: 0x55ab2eaf6014, pid: 878687, ppid: 878659 子: gval: 8, &gval: 0x55ab2eaf6014, pid: 878688, ppid: 878687 父: gval: 0, &gval: 0x55ab2eaf6014, pid: 878687, ppid: 878659 ^C
  • 父子进程共享代码段,但拥有独立的数据段(包括全局变量gval)。初始时,子进程是父进程的副本,因此gval初始值都为0,地址也相同。
  • 运行结果显示:
    • 父子进程输出的地址一致(例如,&gval: 0x55ab2eaf6014),但gval的值不同:父进程始终为0,子进程从0开始递增。
  • 这证明变量gval在父子进程中是独立的副本:修改子进程的gval不影响父进程,反之亦然。

为什么地址相同但值不同?

  • 在C/C++中,输出的地址是虚拟地址(virtual address),不是物理地址(physical address)。虚拟地址是进程视角的地址,由OS统一管理,每个进程都有自己的虚拟地址空间。
  • 当fork()创建子进程时,OS为子进程复制父进程的虚拟地址映射,因此初始地址相同。但物理地址可能不同,因为OS将虚拟地址映射到不同的物理内存位置。

3. 进程地址空间

所以之前说‘程序的地址空间’是不准确的,准确的应该说成 进程地址空间 ,那该如何理解呢?看

图:

分页&虚拟地址空间

虚拟地址到物理地址的转换

  • 当进程访问一个虚拟地址(如0x55ab2eaf6014),MMU拦截该访问,查询页表找到对应的物理地址。父子进程可能有相同的虚拟地址,但页表映射到不同的物理地址,因此变量值独立。
  • 在上面代码中,gval的虚拟地址相同,但物理地址不同:子进程的gval存储在另一个物理位置,导致值变化。(gval改变引起写时拷贝,在这之前子进程的虚拟地址空间和页表都是拷贝父进程的)

物理地址,用户一概看不到,由OS统一管理,OS必须负责将虚拟地址转化成物理地址:OS通过MMU硬件组件实现转换。MMU使用页表(page table)映射虚拟地址到物理地址,用户程序无法直接访问物理地址

一、再谈写时拷贝(COW)

写时拷贝是一种内存管理技术,用于在进程创建子进程时(例如通过fork()系统调用),避免立即复制父进程的内存页。具体步骤如下:

  1. 父进程和子进程共享内存页:在fork()调用后,父进程和子进程共享相同的内存页。这意味着它们的虚拟地址空间中的某些区域(如已初始化数据区)指向相同的物理内存页。
  2. 页表项设置为只读:为了实现写时拷贝,这些共享的内存页在页表中被标记为只读。这意味着如果任何一个进程尝试写入这些共享页,将会触发一个页错误(page fault)。
  3. 页错误处理:当发生页错误时,内核会检查错误的原因。如果是因为写操作导致的页错误,内核会执行以下操作:
    • 复制页:内核会为写操作的进程(父进程或子进程)分配一个新的物理内存页,并将共享页的内容复制到新页中。
    • 更新页表:内核会更新写操作进程的页表,使其指向新的物理内存页,并将该页标记为可写。
    • 继续执行:进程继续执行,此时它对新页的写操作不会影响另一个进程。

二、如何理解虚拟地址空间

虚拟地址空间的概念

虚拟地址空间就像是一个虚拟的“地图”,程序中的每个变量、函数等都位于这个虚拟地图上的某个位置。这个“地图”对每个进程来说都是独立的,就像每个家庭都有自己的独立地址簿一样。

示例:

想象一下,一个城市中有许多图书馆。每个图书馆都有自己的书架编号系统,这些编号就像是虚拟地址。读者(程序)只需要根据书架编号(虚拟地址)就能找到书(数据)。不同图书馆的书架编号(虚拟地址)可能相同,但它们指向的是不同的书(不同的物理内存位置)。就好像在两个不同的图书馆中,都有编号为“123”的书架,但这两个书架上的书是完全不同的。图书馆管理员(操作系统)会将这些虚拟的书架编号(虚拟地址)映射到实际存放书籍的仓库位置(物理内存地址)。这样,读者(程序)不需要知道书籍实际存放的仓库位置,只需要知道图书馆内的书架编号(虚拟地址)就能访问书籍(数据)。

三、如何理解区域划分

区域划分的概念

区域划分是将虚拟地址空间按功能分割成不同区间(如代码区、堆区、栈区),每个区域通过起始地址(start)和结束地址(end)标记边界,由内核数据结构(如Linux的mm_struct)管理。区域可动态调整:扩大时修改end指针(如malloc申请堆空间),缩小时反向操作。

示例:

同桌两人共用一张100cm的课桌(虚拟地址空间)。他们在桌上画"三八线"划分区域:

  • 小胖的区域:start=0cm, end=50cm
  • 小美的区域:start=50cm, end=100cm这就像操作系统的mm_struct结构体记录了每个区域的边界。

当小美想扩大地盘时,她将三八线移到30cm处,此时:

  • 小胖的区域变为:start=0cm, end=30cm(缩小)
  • 小美的区域变为:start=30cm, end=100cm(扩大) 这对应malloc扩大堆空间时,内核修改堆区的end值。

而小胖在50cm内放书包(堆区存变量)、小美在70cm处放水杯(栈区存局部变量),就像进程在不同虚拟区域存取数据。


4. 浅谈虚拟内存管理

由于刚开始学习,所以本篇文章只站在进程的角度去看待虚拟内存。

描述linux下进程的地址空间的所有的信息的结构体是 mm_struct (内存描述符)。每个进程只有一个mm_struct结构,在每个进程的 task_struct 结构中,有一个指向该进程的mm_struct结构体指 针。

1.mm_struct的核心地位

作用mm_struct是描述整个进程用户空间虚拟地址空间的核心结构体,定义在include/linux/mm_types.h中 。

独立性:每个进程拥有独立的mm_struct,确保进程地址空间隔离 。

与进程关联:在进程描述符task_struct中,通过指针mm指向该进程的mm_struct

代码语言:javascript

AI代码解释

struct task_struct { struct mm_struct *mm; // 用户进程的地址空间描述符 struct mm_struct *active_mm; // 内核线程借用前一个进程的地址空间 };
  • 用户进程mm指向自身的mm_struct内核线程:对于内核线程来说,mm字段通常为NULL。内核线程没有独立的用户空间地址,但它们可以使用任意进程的地址空间。此时,内核线程会使用active_mm字段来指向一个有效的地址空间。

可以说,mm_struct 结构是对整个用户空间的完整描述。在Linux内核中,每个进程都会拥有自己独立的mm_struct结构体实例,这个结构体包含了该进程所有内存管理相关的信息。正是由于这种独立性,才保证了每个进程都能拥有专属的虚拟地址空间,实现进程间的内存隔离。

从进程控制块(task_struct)到内存描述符(mm_struct)的关联关系如下:

  1. 在task_struct结构中,有一个名为"mm"的指针成员,它直接指向当前进程的内存描述符(mm_struct)
  2. 当进程创建时(fork系统调用),内核会为子进程分配一个新的mm_struct结构体实例
  3. 这个新的mm_struct会继承或复制父进程的地址空间布局,但实际物理内存会被标记为写时复制(COW)

进程的地址空间的分布情况:

2.mm_struct关键成员解析

(1) 虚拟内存区域管理

  • mmap:指向一个单链表 ——vm_area_struct链表的头部,链表中的每个节点都是一个vm_area_struct结构体,表示一个虚拟内存区域(VMA)。当进程的虚拟内存区域较少时,使用这种单链表的方式来组织。
  • mm_rb:指向红黑树根节点,树中的每个节点也是一个vm_area_struct结构体。当进程的虚拟内存区域较多时,使用红黑树来组织,以便更高效地进行查找、插入和删除操作。
  • 双结构协同:链表与红黑树同步维护,链表用于顺序遍历,红黑树用于快速查找 。
  • mmap_cache:缓存最近访问的 VMA,加速局部性访问 。

(2) 地址空间范围定义

task_size:用户空间大小(如 32 位系统为 3GB)。

分段地址边界

代码语言:javascript

AI代码解释

unsigned long start_code, end_code; // 代码段 unsigned long start_data, end_data; // 数据段 unsigned long start_brk, brk; // 堆(brk 动态扩展) unsigned long start_stack; // 栈起始地址 unsigned long arg_start, arg_end; // 命令行参数 unsigned long env_start, env_end; // 环境变量

这些字段明确划分用户空间各区域 。

(3) 页表与计数

  • pgd:指向进程页全局目录(Page Global Directory),管理虚拟到物理地址转换 。

  • mm_users:共享该地址空间的进程数(如线程共享)。
  • mm_countmm_struct主引用计数,为 0 时释放结构体 。
  • map_count:当前 VMA 的数量 。

(4) 同步与保护

  • mmap_sem:读写信号量,保护 VMA 修改操作(如mmap系统调用)。
  • page_table_lock:自旋锁,保护页表和 RSS(常驻内存集)统计 。

代码语言:javascript

AI代码解释

struct mm_struct { /*...*/ struct vm_area_struct *mmap; /* 指向虚拟区间(VMA)链表 */ struct rb_root mm_rb; /* red_black树 */ unsigned long task_size; /*具有该结构体的进程的虚拟地址空间的⼤⼩*/ /*...*/ // 代码段、数据段、堆栈段、参数段及环境段的起始和结束地址。 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; /*...*/ }

3. 虚拟内存区域(VMA):vm_area_struct

vm_area_struct(简称 VMA)是 Linux 内核中描述进程虚拟地址空间中连续内存区域的核心数据结构。每个 VMA 代表一个具有相同属性(如访问权限、映射类型)的独立内存区间,例如代码段、堆、栈或内存映射文件。

VMA 的核心作用

  • 内存区域抽象将进程的虚拟地址空间划分为逻辑独立的区间(如代码段只读、堆可读写),每个区间对应一个 VMA。例如:
    • 代码段(.text):VM_READ | VM_EXEC
    • 数据段(.data):VM_READ | VM_WRITE
    • 共享库映射:VM_READ | VM_SHARED
  • 缺页异常处理基础当进程访问未分配物理页的虚拟地址时,内核通过 VMA 判断:
    1. 该地址是否属于有效 VMA(vm_start≤ 地址 <vm_end)。
    2. 访问权限是否匹配(通过vm_flags校验)。
    3. 执行相应操作(如分配物理页、加载文件内容)。(证据 1/9)
  • 支持高级内存操作通过vm_ops函数表实现按需分配(Demand Paging)、写时复制(Copy-on-Write)等机制。

关键字段包括:

代码语言:javascript

AI代码解释

struct vm_area_struct { unsigned long vm_start; //虚存区起始 unsigned long vm_end; //虚存区结束 struct vm_area_struct *vm_next, *vm_prev; //前后指针 struct rb_node vm_rb; //红⿊树中的位置 unsigned long rb_subtree_gap; struct mm_struct *vm_mm; //所属的 mm_struct pgprot_t vm_page_prot; unsigned long vm_flags; //标志位 struct { struct rb_node rb; unsigned long rb_subtree_last; } shared; struct list_head anon_vma_chain; struct anon_vma *anon_vma; const struct vm_operations_struct *vm_ops; //vma对应的实际操作 unsigned long vm_pgoff; //⽂件映射偏移量 struct file * vm_file; //映射的⽂件 void * vm_private_data; //私有数据 atomic_long_t swap_readahead_info; #ifndef CONFIG_MMU struct vm_region *vm_region; /* NOMMU mapping region */ #endif #ifdef CONFIG_NUMA struct mempolicy *vm_policy; /* NUMA policy for the VMA */ #endif struct vm_userfaultfd_ctx vm_userfaultfd_ctx; } __randomize_layout;
  • vm_start、vm_end:分别表示该虚拟内存区域的起始和结束地址。
  • vm_next、vm_prev:用于将多个vm_area_struct结构体连接成单链表。
  • vm_rb:用于将vm_area_struct结构体插入到红黑树中。
  • vm_mm:指向该虚拟内存区域所属的mm_struct结构体。
  • vm_page_prot、vm_flags:分别表示该虚拟内存区域的页面保护属性和标志位,用于控制对该区域的访问权限。
  • vm_ops:指向一个包含该虚拟内存区域操作函数的结构体,用于实现对该区域的特殊操作(如映射文件、共享内存等)。
  • vm_pgoff:表示文件映射的偏移量,当该虚拟内存区域映射到一个文件时,这个字段表示文件中的起始位置。
  • vm_file:指向该虚拟内存区域映射的文件对象(如果有的话)。
  • 作用:将进程地址空间划分为不同属性的区域(如代码段只读、堆可读写)。
  • 动态管理:通过vm_ops实现按需分配物理页(Demand Paging)。
http://www.jsqmd.com/news/454453/

相关文章:

  • Linux网络编程:应用层自定义协议与序列化
  • 2026年外贸建站公司实力大盘点:口碑、技术、信用TOP级企业全解析 - 品牌推荐大师1
  • 年度总结:我的技术成长与反思
  • 【Linux系统】命令行参数和环境变量
  • 核“芯”动力,重构无人机通信边界——LR1121IMLTRT 多频段LoRa收发器
  • Java项目中策略模式的使用方法:从零开始掌握可扩展业务逻辑设计
  • 互联网大厂Java小白面试:从基础到进阶的技术问答细节
  • 2026年快速温变试验箱优质供应商盘点:哪家能耗更低? - 品牌推荐大师
  • 2026年波纹金属软管厂商评价排行,目前评价好的波纹金属软管厂商选哪家,波纹补偿器/阀用波纹管,波纹金属软管品牌推荐 - 品牌推荐师
  • 零碳园区商业模式创新的政策支持对企业有哪些影响?
  • Linux服务器崩溃急救指南:实战演练常见故障排查
  • 互联网大厂Java面试:Spring Boot微服务与Redis缓存应用场景分析
  • Flutter 三方库 clean_feature_gen 的鸿蒙化适配指南 - 掌握整洁架构自动化生成技术、助力大中型项目构建高内聚、低耦合且极速迭代的功能模块体系
  • Java Web 榆林特色旅游网站系统源码-SpringBoot2+Vue3+MyBatis-Plus+MySQL8.0【含文档】
  • 柴油发电机3D模型图纸 Solidworks设计
  • 2026热收缩膜包装机优质厂商推荐榜 - 优质品牌商家
  • Spring的下载与配置
  • 2026年天津国际高中择校全指南:优质名校盘点与升学规划策略 - 品牌2026
  • 2026年3月深圳家庭影院、客厅影院音响、定制影院音响、家庭影院KTV音响、家庭影音解决方案、客厅影K套装音响服务商综合选购推荐报告 - 2026年企业推荐榜
  • 立体库SolidWorks三维
  • 得帆云iPaaS如何以“可控”破解AI应用落地难题
  • 2026年NMN、NAD+硬核领跑品牌,NMN什么品牌最好?NMN十大靓牌认证 - 速递信息
  • Flutter 三方库 olx_test_runner 的鸿蒙化适配指南 - 打造工业级的自动化测试流水线、助力鸿蒙应用交付质量跃升
  • 基于Java+SSM+Django影院管理系统(源码+LW+调试文档+讲解等)/影院管理软件/影院管理系统功能/影院管理系统优势/影院排期系统/影院售票系统/影院订票系统/影院会员管理系统
  • 【毕业设计】SpringBoot+Vue+MySQL 智能停车计费系统平台源码+数据库+论文+部署文档
  • 万里通积分卡如何快速回收?线上平台实用指南大揭秘! - 团团收购物卡回收
  • 深海服务器:高压环境代码容错设计的技术实践与测试验证
  • 为什么 PDF 编辑这么难?
  • 神经符号AI实战:解决大模型幻觉
  • ​2026年适配新零售行业的商旅平台排名Top 7与商旅平台选型解析 - 资讯焦点