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

Linux下 进程(六)(程序地址空间)

欢迎来到我的频道 【点击跳转专栏】
码云链接 【点此转跳】

Linux进程相关的所有内容(可以直接点击转跳):
一:Linux进程概念相关:

  1. 【Linux下 进程(一)(冯诺依曼体系、操作系统、进程基本概念与基本操作)】
  2. 【 Linux下 进程(二)(进程状态、僵尸进程和孤儿进程)】
  3. 【Linux下 进程(三)(进程的优先级)】
  4. 【Linux下 进程(四)(进程的组织、进程的切换和进程O(1)调度算法)】
  5. 【Linux下 进程(五)(命令⾏参数和环境变量)】
  6. 【Linux下 进程(六)(程序地址空间)】

二:Linux进程控制相关:

  1. 【Linux下 进程控制(一) —— 进程的创建、终止和等待】
  2. 【Linux下 进程控制(二) —— 进程程序替换】
  3. 【Linux下 进程控制(三) —— ⾃主Shell命令⾏解释器】

文章目录

  • 1. C语言程序地址空间回顾
  • 2. 地址空间的引出
  • 3. 虚拟地址空间
    • 3.1 轮廓解释上面g_val不同问题
    • 3.2 什么是虚拟地址空间
    • 3.3 理解虚拟地址空间中的区域划分(怎么分出栈、堆等区域的)
    • 3.4 怎么理解全局变量,static变量生命周期是全局
  • 4. 页表
    • 4.1 页表的内容
  • 5. 虚拟地址空间存在的意义
  • 6. 其它问题(懒加载、堆区虚拟地址的管理)
    • 6.1 懒加载
    • 6.2 堆区虚拟地址的管理

1. C语言程序地址空间回顾

C语⾔的时候,⽼师给⼤家画过这样的空间布局图:


通过代码的方式在不同的区域创建变量然后来取地址获取再进行比较来证明这个结论的正确性:

#include<stdio.h>#include<unistd.h>#include<stdlib.h>intg_unval;intg_val=100;intmain(intargc,char*argv[],char*env[]){constchar*str="helloworld";printf("code addr: %p\n",main);printf("init global addr: %p\n",&g_val);printf("uninit global addr: %p\n",&g_unval);staticinttest=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(inti=0;i<argc;i++){printf("argv[%d]: %p\n",i,argv[i]);}for(inti=0;env[i];i++){printf("env[%d]: %p\n",i,env[i]);}return0;}

同时 我们发现:

  1. 堆区向地址增大的方向增长。
  2. 栈区向地址减小的方向增长,但是定义的变量(如连续的整形数组),是从低地址向高地址开始访问。

  1. 堆、栈相对而生!!!
  2. 而静态变量和全局变量会被定义在全局区(也叫初始化、未初始化数据区,同时未初始化数据区地址比初始化数据地址高

  1. 代码在全局区之下的代码区(注意:字符串常量区也在代码区位置)


2. 地址空间的引出

我们通过上述案例创建了一个子进程,并且在子进程中

#include<stdio.h>#include<unistd.h>intg_val=100;intmain(){printf("g_val: %d, &g_val: %p\n",g_val,&g_val);pid_t id=fork();if(id==0){while(1){printf("我是子进程, pid: %d, ppid: %d, g_val: %d, &g_val: %p\n",getpid(),getppid(),g_val,&g_val);sleep(1);g_val++;}}else{while(1){printf("我是父进程, pid: %d, ppid: %d, g_val: %d, &g_val: %p\n",getpid(),getppid(),g_val,&g_val);sleep(1);}}}


我们发现 同一个地址居然g_val的值不一样! 如果变量的地址是一个物理地址,是绝对不可能出现这种情况的,因此我们的变量地址必然不是物理地址
那么这个地址是什么呢? 我们平时使用的地址全都不是物理地址,而是虚拟地址!!

3. 虚拟地址空间

3.1 轮廓解释上面g_val不同问题

  1. 我们的代码数据存储在磁盘中,运行时加载到内存,其中g_val对应与物理内存的地址假设是0x12345678,内容则是100
  2. 而系统此时就会为我们创建一个虚拟地址空间,与进程的PCB关联联系,虚拟地址会为g_val再次开辟一个地址假设是0x601054
  3. 而操作系统在创建进程的时候,会为每一个进程构建一个页表,而页表则负责建立虚拟地址和物理地址间的映射!
  4. 我们用户所得到的地址统统都是虚拟地址物理地址我们用户是看不到的,被OS隐藏起来了,这样可以变相保护物理地址中的数据!
  5. 后来我们对代码进行fork创建了一个子进程,子进程会以父进程为模版进行创建,众所周知进程=内核数据结构+代码和数据,父进程有关的虚拟地址和页表信息也自然会给子进程拷贝一份。

我们由上述内容得到以下结论:

  • 结论一:虚拟地址空间和页表,每一个进程各自有一套。
  • 结论二:众所周知,为了节约空间,fork以后父子进程共享一份数据代码,而这些能够做到原因就是因为子进程拷贝了父进程的页表信息,类似于浅拷贝!

进程之间是有独立性的!那么该如何保证进程的独立性呢?

  • 结论三:代码只读的,父子不会影响!但OS规定:父子中,任何一个进程,尝试对共享的变量进行修改,不能直接修改,而要发生“写时拷贝”,类似深拷贝!

上述g_val不同的原因就是因为,当g_val++的时候,物理内存重新开辟了一个新的空间,把原本的g_val拷贝过来后修改了它的值,而重新开辟的新内存空间,我们假设是0x11223344,然后修改子进程中的页标信息虚拟地址不变,但是映射的内存地址改成了0x11223344

3.2 什么是虚拟地址空间

打个比方:一个大富翁有10个私生子,10个私生子互相不知道各自存在,大富翁对每个私生子都画饼说:“我有10亿财产,以后都给你!”。每个私生子都很开心,但是大富翁做法本质就是画饼!10亿不可能给私生子,但是每个私生子在需要钱的时候,大富翁肯定都会给个一点!而私生子也不会直接要10个亿,因为请求不合理,但是他们每个人都自以为未来10亿都是属于自己的,但实际要钱的时候只会一点一点要。
但是饼画多了,饼是不是也要管理?大富翁要清楚每个私生子的诉求,比如给私生子1一千万,那么私生子1最后只能继承九亿九千万,再跟私生子1吹是10亿不就出事了。

这里大富翁对应就是OS大富翁实际的10亿资产就是物理内存大小,而画的饼就是虚拟地址空间

虚拟地址空间(Virtual Address Space)是现代计算机操作系统中一项核心的内存管理机制。它为每一个运行的程序(进程)提供了一个关键的“幻觉”:让程序以为自己独占了一大块连续的内存,而无需关心真实的物理内存是如何分布和使用的。

Linux中,任何的管理都离不开先描述,再组织,那么虚拟地址空间绝对就是个struct结构体!

而在Linux内核中,该结构体是客观存在的,叫mm_struct

每个进程只有⼀个mm_struct结构,在每个进程的task_struct结构中,有⼀个指向该进程的mm_struct结构体指针

3.3 理解虚拟地址空间中的区域划分(怎么分出栈、堆等区域的)

举个例子:沸羊羊(张三)和小花在一张桌子上,一共100cm,小花很嫌弃张三,小花画了一条线,一人50cm。
而这么做的本质:区域划分

小花的行为用C语言表示就是:

而区域的划分,实际只要一个地址的开始和结束即可划分了吗!
而区域的变大或者变小不就是修改对应结构体内部的start和end即可!


在实际内核中,表示虚拟地址空间的范围实际就是用unsigned long来表示的!

由 task_struct 到mm_struct ,进程的地址空间的分布情况:

3.4 怎么理解全局变量,static变量生命周期是全局

这些数据都在进程的初始化和未初始化数据区,变量会随着栈区开辟和释放;而初始化和未初始化数据区一直存在,只要进程活着,地址空间就在,地址空间在,全局数据区就在,全局变量就一直存在!!

4. 页表

4.1 页表的内容

页表具体内容:虚拟地址、物理地址、读写权限、标志位(对应的代码和数据是否被加载到内存中)

读写权限:为什么C语言中常量字符串无法修改,本质就是对应值的读写权限是r,当你想修改时,就会被操作系统拦截,该非法请求就不会被发送到物理内存,让你的进程无法写入。

标志位:判断进程的代码和数据是否被加载到内存中(具体可以通过0、1表示),因为进程对应的代码和数据是有可能处于挂起状态

ps: 什么是挂起状态可以参考 我前面写的内容 https://blog.csdn.net/Fcy648/article/details/157724383#t4

5. 虚拟地址空间存在的意义

  1. 控制进程的行为,拦截非法行为(非法行为通过虚拟地址空间和页表将其拦截),达到保护物理内存的目的。
  2. 代码数据在物理内存上是随意加载,但是有了有序虚拟地址空间和页表的映射,就可以将进程的内存布局,无序变有序。
  3. 进程管理和内存管理进行 解耦!(二者互不影响!例子参考下面的懒加载

6. 其它问题(懒加载、堆区虚拟地址的管理)

6.1 懒加载

问: 在Linux中,创建一个进程,是先创建内核数据结构,然后再加载代码和数据,还是反过来?

在实际中,如果我创建了一个进程,但是不着急使用,就可以先不急着加载数据,等需要用的时候再加载进程,这个就叫懒加载,本质就是用 效率来换取空间的充分利用!
我们在C语言中malloc了空间,我们不一定会立马使用,其本质是在虚拟地址空间中的堆区申请空间,等你真正要使用的时候再给你动态申请物理内存(进程管理和内存管理的解耦!),这个过程叫缺页中断引起内存的二次申请


分⻚&虚拟地址空间:

6.2 堆区虚拟地址的管理

栈区是线性增长和释放的,只需加减就可以移动了;但是堆区往往是离散的(比如你申请 ABC三块空间,释放了B,此时AC就是离散的空间了),离散的空间该该如何管理呢?

在进程的struct_task中有一个struct mm_struct *mm指针指向对应的mm_struct(虚拟地址空间的结构体)。

同时每个mm_struct都有一个struct vm_area_struct *mmap指向以vm_area_struct(虚拟内存区域)为节点的链表

vm_area_struct结构体长这样,其中的vm_startvm_end分别表示一段虚拟内存空间的开始和结束:

vm_area_struct为节点构成链表来管理内存布局,哪怕堆区不连续也能管理,mmap就是指向链表的头节点的指针!对堆区的具体管理布局如下:

所以,在Linux中就是依靠vm_area_struct结构来表示一个独立的虚拟内存区域(VMA),每一个VMA都包含一个独立的内存区域(start和end),用它来更细力度的来指明一个又一个区域,然后以链表形式串在一起,哪怕是不连续的堆区也能精确管理!

最后,附上完整的内存划分图:

http://www.jsqmd.com/news/709124/

相关文章:

  • 从Circularity-Cursor项目解析Windows光标主题的设计、实现与自定义
  • 推荐2026大负载减速机轴承:哪款更耐用? - 品牌2025
  • Python PDF文本提取终极指南:pdftotext技术深度解析
  • G-Helper全面升级:华硕笔记本轻量化控制的终极指南
  • 2026年昆明银行变更与工商变更全流程避坑指南 - 企业名录优选推荐
  • 抖音视频下载终极指南:免费批量下载高清无水印视频的完整解决方案
  • 3分钟掌握APK安装器:Windows上运行安卓应用的终极方案
  • 开源模型商用合规指南:SenseVoice-Small ONNX本地部署与数据隐私保护
  • 终极AI瞄准助手:用YOLOv8/YOLOv10技术实现智能游戏瞄准
  • 祛黑头泥膜哪种好?普通人亲测好用的清洁泥膜分享 - 全网最美
  • 超自动化巡检:让合规与审计变得轻松简单
  • IT运维必备:用PowerShell脚本批量管理公司电脑的BitLocker状态(含manage-bde命令实战)
  • 保姆级教程:在Ubuntu18.04 ROS Melodic下,用Kinova Mico和RealSense D435i搞定手眼标定(附常见rviz界面问题解决)
  • 2026年Q2安徽母线槽十大品牌权威推荐:专业测评最新发布 - 安互工业信息
  • 零初始化低秩适配器优化视觉Transformer模型
  • 2026年5月卡地亚官方售后服务升级预告:全国维修网点地址更新・服务热线400-1063365正式启用 - 速递信息
  • mattpocock/skills:TypeScript 大神把自己的 .claude 目录开源了,这意味着什么?
  • nli-MiniLM2-L6-H768实战案例:客服对话一致性校验系统搭建
  • STM32的ADC到底有多快?用逻辑分析仪实测F103的采样率与转换时间,附CubeMX配置技巧
  • 2026减速机轴承厂家推荐?看人形关节核心部件怎么选 - 品牌2025
  • 揭秘:国际金价受什么影响最大?衢州本地黄金回收实战指南 - 福正美黄金回收
  • 保姆级教程:从零在国产飞腾服务器(麒麟V10)上搭建Java Web生产环境(Nginx+Tomcat+MySQL)
  • Jasmine漫画浏览器完整指南:3步实现全平台漫画阅读自由
  • 2026关节模组轴承厂家哪家好?选型经验分享 - 品牌2025
  • 2026年昆明代理记账与工商变更全生命周期财税服务深度横评:云南本土企业的合规避坑指南 - 企业名录优选推荐
  • 2026年昆明代理记账与工商变更一站式服务深度评测指南 - 企业名录优选推荐
  • 终极静音方案:5步掌握FanControl免费风扇控制软件
  • 【量产级】嵌入式安全固件生成工具 | AES128 一次一密 | 工厂烧录 + OTA 双输出 | 跨平台可直接商用
  • 2026年苏州有名的水下切割焊接品牌企业哪家好,快来看看 - 工业品网
  • 2026年展馆设计施工风向标:从数字化叙事到沉浸式体验的五大实力派盘点 - 深度智识库