探秘 Linux 系统编程:进程地址空间的奇妙世界

亲爱的读者朋友们😃,此文开启知识盛宴与思想碰撞🎉。

快来参与讨论💬,点赞👍、收藏⭐、分享📤,共创活力社区。

        在 Linux 系统编程的领域里,进程地址空间可是个相当重要的概念🤔。它就像是一个神秘的魔法盒子,藏着许多有趣又关键的知识。今天,就让我们一起打开这个盒子,看看里面都有什么奇妙的东西吧🧐!


目录

一、C 语言内存管理基础:内存的 “小秘密”

1. 内存区域大揭秘

2. 栈区和堆区的 “小脾气”

3. 静态变量的 “特殊待遇”

二、fork 遗留问题:变量的 “神奇分身术”

三、进程地址空间:进程的 “专属小世界”

1. 什么是地址空间

2. 地址空间的区域划分

3. 进程地址空间的奥秘

四、页表:进程地址空间的 “大管家”

1. 写时拷贝、缺页中断和惰性加载

2. 进程地址空间的切换

3. 进程创建的过程

4. 进程的独立性

五、为什么要有进程地址空间:进程的 “保护罩” 和 “便利贴”

六、命令行参数和环境变量的 “藏身之处”


一、C 语言内存管理基础:内存的 “小秘密”

在 C 语言的内存管理世界中,有一个有趣的现象😉。如果一个指针指向了常量字符串,这个字符串存放在常量区,是只读的,不能被修改。要是强行修改,程序就会崩溃哦😱,就像下面这段代码:

#include <stdio.h>
#include <stdlib.h>int main() {char *str = "hello bit";*str = 'H';printf("xxx=%s\n", getenv("xxx"));return 0;
}

1. 内存区域大揭秘

        C 语言中的线性地址是有区域划分的,从高地址到低地址分别是栈区、堆区、未初始化全局变量区、全局数据区(初始化全局变量在这)、字符常量区和代码区🎯。

怎么验证这个划分是不是正确的呢?我们可以通过代码在不同区域创建变量,然后获取它们的地址并比较,就像这样:

#include <stdio.h>
#include <stdlib.h>int g_val_1;
int g_val_2 = 100;int main() {printf("code addr: %p\n", main);const char *str = "hello bit";printf("read only string addr: %p\n", str);printf("init global value addr: %p\n", &g_val_2);printf("uninit global value addr: %p\n", &g_val_1);char *mem = (char*)malloc(100);printf("heap addr: %p\n", mem);printf("stack addr: %p\n", &str);
}

运行这段代码,会得到不同区域变量的地址,从而验证内存区域的划分🧐。

运行结果如下👇

验证成功!

2. 栈区和堆区的 “小脾气”

栈区和堆区就像两个性格相反的小伙伴😜。栈区的地址是逐渐变低的(栈区向地址减少方向增长),我们可以通过在栈区定义多个变量,观察它们地址的变化来验证:

int main() {printf("code addr: %p\n", main);const char *str = "hello bit";printf("read only string addr: %p\n", str);printf("init global value addr: %p\n", &g_val_2);printf("uninit global value addr: %p\n", &g_val_1);char *mem = (char*)malloc(100);printf("heap addr: %p\n", mem);printf("stack addr: %p\n", &str);printf("stack addr: %p\n", &mem);int a;int b;int c;printf("stack addr: %p\n", &a);printf("stack addr: %p\n", &b);printf("stack addr: %p\n", &c);
}

 运行结果如下👇

堆区的地址则是逐渐变高的(堆区向地址增大方向增长),同样可以用代码来验证:

int main() {printf("code addr: %p\n", main);const char *str = "hello bit";printf("read only string addr: %p\n", str);printf("init global value addr: %p\n", &g_val_2);printf("uninit global value addr: %p\n", &g_val_1);char *mem = (char*)malloc(100);char *mem1 = (char*)malloc(100);char *mem2 = (char*)malloc(100);printf("heap addr: %p\n", mem);printf("heap addr: %p\n", mem1);printf("heap addr: %p\n", mem2);printf("stack addr: %p\n", &str);printf("stack addr: %p\n", &mem);int a;int b;int c;printf("stack addr: %p\n", &a);printf("stack addr: %p\n", &b);printf("stack addr: %p\n", &c);
}

运行结果如下👇 

3. 静态变量的 “特殊待遇”

        静态变量也很特别哦😎,它被定义在全局区,但只在作用域里使用。第一次使用时初始化,之后它的生命周期就不随着函数的调用和释放而变化啦,就像有自己的 “小天地” 一样。

 用代码来验证:

说明static 修饰的局部变量,编译的时候已经被编译到全局数据区! 


二、fork 遗留问题:变量的 “神奇分身术”

        在使用fork函数时,会遇到一个有趣的问题🤯:为什么一个变量可以同时等于 0 又大于 0 呢?看下面这个实验代码:

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>int main() {pid_t id = fork();if (id == 0) {// 子进程int cnt = 5;while (1) {printf("i am child, pid: %d,ppid: %d, g_val: %d, &g_val: %p\n", getpid(), getppid(), g_val, &g_val);sleep(1);if (cnt) cnt--;else {g_val = 200;printf("子进程change g_val:100->200\n");}}} else {// 父进程while (1) {printf("i am parent, pid : %d, ppid : %d, g_val: %d, &g_val: %p\n", getpid(), getppid(), g_val, &g_val);sleep(1);}}
}

        运行这段代码,会发现同一个地址竟然读到了不同的内容😱!这就说明,我们平时在 C/C++ 里使用的地址不是物理地址,而是虚拟地址🧐。用户是看不到物理地址的,操作系统会负责把虚拟地址转化成物理地址,就像有个 “翻译官” 在中间帮忙一样。


三、进程地址空间:进程的 “专属小世界”

为了理解上面这些现象,我们要引入一个新的概念 —— 进程地址空间😃。

1. 什么是地址空间

        在 32 位机器中,有 32 位的地址和数据总线。每根地址总线可以是 0 或 1(其实计算机识别的是高低电平,1 代表高电平,0 代表低电平),32 根地址总线就有种组合方式,对应的地址范围就是,这个范围就是地址空间啦🎯,就像一个超级大的 “地址仓库”。

 

 

2. 地址空间的区域划分

地址空间是有区域划分的,就好比一张 100cm 长的桌子,小胖和小美坐在上面,为了避免小胖骚扰小美,在中间画了三八线,每人各占 50cm 的空间😏。用结构体来表示就是这样:

struct area {int start;int end;
};
struct destop_area {struct area xiaopang;struct area xiaohua;
};
struct destop_area line_area = {{1, 50}, {51, 100}};
 

        如果想改变区域大小,修改结构体里的startend就行啦。而且在地址空间的范围内,每一个最小单位都有地址,这些地址都可以被使用哦😎。

3. 进程地址空间的奥秘

        进程地址空间本质上是一个描述进程可视化范围的地址空间,里面有各种区域划分,它是一个内核数据结构,和 PCB(进程控制块)一样,都需要被操作系统管理,也就是 “先描述再组织”🤗。每个进程都有自己的进程地址空间,PCB 里有个指针指向这块空间。在 32 位系统中,默认划分的区域大小是 4GB,这里面详细划分了代码区、只读数据区、初始化数据区等多个区域,每个区域都有自己的 “职责”。

 


四、页表:进程地址空间的 “大管家”

在现代操作系统中,页表起着至关重要的作用,它就像一个 “大管家”,管理着进程地址空间的各种事务😎。

1. 写时拷贝、缺页中断和惰性加载

  • 页表里记录了虚拟地址、物理地址、读写权限、标志位(用于判断代码和数据是否被加载到内存中)等信息📋。
  • 读写权限可以防止非法操作,比如你想修改常量字符区的内容,操作系统就会通过页表检查拦截这个非法请求,保护物理内存的安全🛡️。
  • 标志位能帮助我们判断进程的代码和数据有没有被加载到内存中。因为进程的代码和数据可能处于挂起状态,还没被加载进来。
  • 惰性加载就是 “按需加载”,操作系统不会一下子把大文件全部加载到内存,而是需要多少就加载多少,是不是很聪明呢😜?

  • 缺页中断是指当执行进程时,如果发现标志位显示当前代码和数据没有加载,就会暂时中断这个进程,等代码和数据加载进来后,再恢复原来的状态继续运行。有人可能会问,为什么不一次全部加载进去呢?这是因为文件可能很大,一次加载不仅占用大量内存,而且你也不是一下子就把所有内容都用上呀,所以缺页中断能更合理地使用内存呢🧐。
  • 写时拷贝也很有趣,数据区的数据本来是可写的,但一开始权限被设置成只读(这时父子进程共享数据)。一旦父子进程有一方想修改数据,发现是只读的,系统不会报错,而是会开辟一块新的物理内存,修改页表的映射,实现数据的分离,就像给每个进程都准备了一份属于自己的数据 “拷贝” 一样😃。

 

2. 进程地址空间的切换

        进程 PCB 结构体里有指向进程地址空间的指针,进程切换就意味着进程地址空间也被切换啦。而页表会被存储在 CPU 的 cr3 寄存器中,这属于进程的上下文信息,在进程切换的时候会跟着进程 “走”,之后还能恢复过来,保证进程再次运行时一切正常🤗。

3. 进程创建的过程

        进程创建时,会优先加载 PCB 结构体和对应的进程地址空间结构体,而它的代码和数据可能不会马上被加载进来,就像先搭建好 “房子框架”,里面的 “家具”(代码和数据)之后再慢慢摆放一样😉。

4. 进程的独立性

进程具有独立性,主要体现在以下几个方面:

  • 在内核数据结构上是独立的,每个进程都有自己专属的 “小账本”(内核数据结构)。
  • 物理内存中加载的代码和数据,通过页表映射不同的物理地址,让父子进程 “互不干扰”。即使虚拟地址看起来一样,但通过页表映射到不同的物理地址,就实现了解耦。这样一来,一旦某个进程出现异常,不会影响其他进程,各自释放各自的资源就行啦😎。
  • 通过页表的虚拟地址映射物理地址,进程可以随便取地址,甚至是乱序的。但对于进程来说,看到的是一个线性的地址,就好像所有地址都是按顺序排列的,是不是很神奇呢🧐?

五、为什么要有进程地址空间:进程的 “保护罩” 和 “便利贴”

有了进程地址空间,好处可多啦😃!

  • 让进程以统一的视角看待内存,进程不需要关心数据具体放在物理内存的什么位置,也不用担心会影响别人的数据,这些复杂的工作都交给操作系统去完成,进程只要 “安心使用” 就行啦,就像有个贴心的小助手帮你打理一切琐事一样🤗。
  • 增加虚拟地址空间,在访问内存时多了一个转换过程。在这个过程中,操作系统可以对寻址进行审查,如果发现异常访问,直接拦截,不让请求到达物理内存,这就像给物理内存穿上了一层坚固的 “保护罩”🛡️,保护它不被非法访问。
  • 地址空间和页表的存在,将进程管理模块和内存模块解耦合。进程不用关心申请物理内存的哪一块、优先加载可执行程序的哪一部分、页表填写到什么地方等问题,这些都由 Linux 的内存模块负责管理,进程只要专注于自己的 “本职工作” 就好啦😎。
  • 变量名在定义的时候其实就已经被转化成虚拟地址了,我们使用a&a,本质上是为了区分获取变量的值还是地址,是不是有一种恍然大悟的感觉呢🧐?
  • 以前我们学习的 C 内存管理,本质上就是进程地址空间,而内存管理的具体细节是由 Linux 完成的。我们在写上层语言代码时,不需要关心这些细节,直接通过线性地址使用内存就行啦,就像用 “便利贴” 一样方便😜。

六、命令行参数和环境变量的 “藏身之处”

        命令行参数和环境变量存放在栈的上面,是一个独立的空间🧐。子进程能够继承父进程的环境变量,是因为子进程启动时,父进程已经把环境变量信息加载进去了。它们也是地址空间的一部分,有页表帮助建立虚拟地址和物理地址的映射。子进程创建时,会复制父进程虚拟地址空间中环境变量的相关参数映射,所以即使传参数,子进程也能获取到父进程的环境变量信息啦😎。


怎么样,进程地址空间是不是很有趣呢😃?希望通过这篇文章,你能对它有更深入的了解🧐! 

我将持续更新Linux的高质量内容,欢迎关注我👉【A charmer】!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.rhkb.cn/news/27882.html

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

2025-03-04 学习记录--C/C++-PTA 习题5-5 使用函数统计指定数字的个数

合抱之木&#xff0c;生于毫末&#xff1b;九层之台&#xff0c;起于累土&#xff1b;千里之行&#xff0c;始于足下。&#x1f4aa;&#x1f3fb; 一、题目描述 ⭐️ 二、代码&#xff08;C语言&#xff09;⭐️ #include <stdio.h>int CountDigit( int number, int di…

25年第四本【认知觉醒】

《认知觉醒》&#xff1a;一场与大脑的深度谈判 在信息爆炸的焦虑时代&#xff0c;我们像被抛入湍流的溺水者&#xff0c;拼命抓取各种自我提升的浮木&#xff0c;却在知识的漩涡中越陷越深。这不是一本简单的成功学指南&#xff0c;而是一场关于人类认知系统的深度对话&#…

汽车视频智能包装创作解决方案,让旅途记忆一键升级为影视级大片

在智能汽车时代&#xff0c;行车记录已不再是简单的影像留存&#xff0c;而是承载情感与创意的载体。美摄科技依托20余年视音频领域技术积累&#xff0c;推出汽车视频智能包装创作解决方案&#xff0c;以AI驱动影像处理与艺术创作&#xff0c;重新定义车载视频体验&#xff0c;…

DeepSeek 智慧城市应用:交通流量预测(918)

**摘要&#xff1a;**本文探讨了利用 DeepSeek 技术框架解决城市交通流量预测问题的方法&#xff0c;主要内容包括基于时空图卷积网络&#xff08;ST - GCN&#xff09;的预测模型、多传感器数据融合策略以及实时推理 API 服务的搭建&#xff0c;旨在为智慧城市的交通管理提供高…

如何在随机振动分析中包括缓冲器

总结 在随机振动分析中&#xff0c;准确模拟系统的动态行为对于预测其在随机激励下的响应至关重要。在这种情况下&#xff0c;分立阻尼器&#xff08;如减振器&#xff09;是必不可少的组件&#xff0c;因为它有助于模拟实际系统中的能量耗散机制。通过将离散阻尼器集成到模型…

python3.13安装教程【2025】python3.13超详细图文教程(包含安装包)

文章目录 前言一、python3.13安装包下载二、Python 3.13安装步骤三、Python3.13验证 前言 本教程将为你详细介绍 Python 3.13 python3.13安装教程&#xff0c;帮助你顺利搭建起 Python 3.13 开发环境&#xff0c;快速投身于 Python 编程的精彩实践中。 一、python3.13安装包下…

Transformer 代码剖析6 - 位置编码 (pytorch实现)

一、位置编码的数学原理与设计思想 1.1 核心公式解析 位置编码采用正弦余弦交替编码方案&#xff1a; P E ( p o s , 2 i ) sin ⁡ ( p o s 1000 0 2 i / d m o d e l ) P E ( p o s , 2 i 1 ) cos ⁡ ( p o s 1000 0 2 i / d m o d e l ) PE_{(pos,2i)} \sin\left(\fra…

CF 452A.Eevee(Java实现)

题目分析 输入一个数字-长度&#xff0c;输入一个字符串。判断这个字符串是具体的哪一个单词 思路分析 首先给了长度&#xff0c;那我先判断长度相同的单词&#xff0c;然后再一一对比&#xff0c;如果都能通过&#xff0c;那就输出这个单词 代码 import java.util.*;public …

【监控】使用Prometheus+Grafana搭建服务器运维监控面板(含带BearerToken的Exporter配置)

【监控】使用PrometheusGrafana搭建服务器运维监控面板&#xff08;含带BearerToken的Exporter配置&#xff09; 文章目录 1、Grafana 数据可视化面板2、Prometheus - 收集和存储指标数据3、Exporter - 采集和上报指标数据 1、Grafana 数据可视化面板 Grafana 是一个开源的可视…

ADC采集模块与MCU内置ADC性能对比

2.5V基准电压源&#xff1a; 1. 精度更高&#xff0c;误差更小 ADR03B 具有 0.1% 或更小的初始精度&#xff0c;而 电阻分压方式的误差主要来自电阻的容差&#xff08;通常 1% 或 0.5%&#xff09;。长期稳定性更好&#xff0c;分压电阻容易受到温度、老化的影响&#xff0c;长…

UDP协议(20250303)

1. UDP UDP:用户数据报协议&#xff08;User Datagram Protocol&#xff09;&#xff0c;传输层协议之一&#xff08;UDP&#xff0c;TCP&#xff09; 2. 特性 发送数据时不需要建立链接&#xff0c;节省资源开销不安全不可靠的协议 //一般用在实时性比较高…

基于https虚拟主机配置

一、https介绍 http 明文&#xff0c;80/tcp https 密文&#xff0c;443/tcp 二、安全性保障 1、数据安全性 数据加密 2、数据完整性 3、验证身份的真实性、有效性 三、数据安全性 手段&#xff1a;加密 发送方加密数据&#xff0c;接收方解密数据 对称加密算法 加密、解密数据…

机器学习(五)

一&#xff0c;多类&#xff08;Multiclass&#xff09; 多类是指输出不止有两个输出标签&#xff0c;想要对多个种类进行分类。 Softmax回归算法&#xff1a; Softmax回归算法是Logistic回归在多类问题上的推广&#xff0c;和线性回归一样&#xff0c;将输入的特征与权重进行…

概率论基础概念

前言 本文隶属于专栏《机器学习数学通关指南》&#xff0c;该专栏为笔者原创&#xff0c;引用请注明来源&#xff0c;不足和错误之处请在评论区帮忙指出&#xff0c;谢谢&#xff01; 本专栏目录结构和参考文献请见《机器学习数学通关指南》 正文 &#x1f3b2; 1. 随机事件 …

动漫短剧开发公司,短剧小程序搭建快速上线

在当今快节奏的生活里&#xff0c;人们的娱乐方式愈发多元&#xff0c;而动漫短剧作为新兴娱乐形式&#xff0c;正以独特魅力迅速崛起&#xff0c;成为娱乐市场的耀眼新星。近年来&#xff0c;动漫短剧市场呈爆发式增长&#xff0c;吸引众多创作者与观众目光。 从市场规模来看…

MySQL零基础教程15—简单的表连接(join)

在学习子查询的时候&#xff0c;我们已经感受到了&#xff0c;在一个语句中&#xff0c;通过访问不同表的数据最终获取我们想要的结果这种操作方式&#xff0c;实际上在mysql中&#xff0c;还有更加有趣的一个功能&#xff0c;就是表连接&#xff0c;同样是在查询数据的时候连接…

【AVRCP】深入剖析 AVRCP 命令体系:从单元到特定命令的全面解读

在蓝牙音频 / 视频远程控制规范&#xff08;AVRCP&#xff09;中&#xff0c;丰富的命令体系是实现设备间高效交互的关键。这些命令涵盖了单元命令、通用单元与子单元命令、特定命令等多个层面&#xff0c; 一、支持的单元命令 1.1 单元命令概述 AVRCP中支持的单元命令在设备…

物业管理系统源码 物业小程序源码

物业管理系统源码 物业小程序源码 一、基础信息管理 1. 房产信息管理 记录楼栋、单元、房间的详细信息&#xff08;面积、户型、产权等&#xff09;。 管理业主/租户的档案&#xff0c;包括联系方式、合同信息等。 2. 公共资源管理 管理停车场、电梯、绿化带、公…

专题二最大连续1的个数|||

1.题目 题目分析&#xff1a; 给一个数字k&#xff0c;可以把数组里的0改成1&#xff0c;但是只能改k次&#xff0c;然后该变得到的数组能找到最长的子串且都是1。 2.算法原理 这里不用真的把0变成1&#xff0c;因为改了比较麻烦&#xff0c;下次用就要改回成1&#xff0c;这…

【计算机网络入门】初学计算机网络(十一)重要

目录 1. CIDR无分类编址 1.1 CIDR的子网划分 1.1.1 定长子网划分 1.1.2 变长子网划分 2. 路由聚合 2.1 最长前缀匹配原则 3. 网络地址转换NAT 3.1 端口号 3.2 IP地址不够用&#xff1f; 3.3 公网IP和内网IP 3.4 NAT作用 4. ARP协议 4.1 如何利用IP地址找到MAC地址…