探秘 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】!

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

vue videojs使用canvas截取视频画面

前言 刚开始做的时候太多坑&#xff0c;导致一直报错&#xff1a; Uncaught (in promise) TypeError: Failed to execute ‘drawImage’ on ‘CanvasRenderingContext2D’: The provided value is not of type ‘(CSSImageValue or HTMLCanvasElement or HTMLImageElement or H…

防火墙旁挂组网双机热备负载均衡

一&#xff0c;二层交换网络&#xff1a; 使用MSTPVRRP组网形式 VLAN 2--->SW3为主,SW4 作为备份 VLAN 3--->SW4为主,SW3 作为备份 MSTP 设计 --->SW3 、 4 、 5 运行 实例 1 &#xff1a; VLAN 2 实例 2 &#xff1a; VLAN 3 SW3 是实例 1 的主根&#xff0c;实…

记忆化搜索与动态规划:原理、实现与比较

记忆化搜索和动态规划是解决优化问题的两种重要方法&#xff0c;尤其在处理具有重叠子问题和最优子结构性质的问题时非常有效。 目录 1. 记忆化搜索&#xff08;Memoization&#xff09; 定义&#xff1a; 实现步骤&#xff1a; 示例代码&#xff08;斐波那契数列&#xff0…

《几何原本》命题I.9

《几何原本》命题I.9 一个角可以切分成两个相等的角。 设 ∠ E A F \angle EAF ∠EAF 为给定角 在 A E , A F AE,AF AE,AF 上任取两点 B , C B,C B,C 使得 A B A C ABAC ABAC 连结 B C BC BC 在 A A A 下方作等边三角形 B C D BCD BCD 则 A B A C , B D C D , A D…

docker-compose安装anythingLLM

1、anythingLLM的docker-compose文件 version: 3.8 services:anythingllm:image: mintplexlabs/anythingllm:latestcontainer_name: anythingllmports:- "23001:3001"cap_add:- SYS_ADMINenvironment:# Adjust for your environment- STORAGE_DIR/app/server/storage…

DeepSeek 助力 Vue3 开发:打造丝滑的表格(Table)示例2: 分页和排序

前言:哈喽,大家好,今天给大家分享一篇文章!并提供具体代码帮助大家深入理解,彻底掌握!创作不易,如果能帮助到大家或者给大家一些灵感和启发,欢迎收藏+关注哦 💕 目录 DeepSeek 助力 Vue3 开发:打造丝滑的表格(Table)示例2: 分页和排序📚前言📚页面效果📚指令…

SQL命令详解之多表查询(连接查询)

目录 1 简介 2 内连接查询 2.1 内连接语法 2.2 内连接练习 3 外连接查询 3.1 外连接语法 3.2 外连接练习 4 总结 1 简介 连接的本质就是把各个表中的记录都取出来依次匹配的组合加入结果集并返回给用户。我们把 t1 和 t2 两个表连接起来的过程如下图所示&#xff1a; …

二、QT和驱动模块实现智能家居-----问题汇总1

1、文件地址改变后必须在QT下更改地址 2、指定了QT内Kits下的Sysroot头文件地址&#xff0c;但是还是找不到头文件&#xff1a; 3、提示无法执行QT程序&#xff1a;先干掉之前的QT程序 ps //查看程序PIDkill -9 PID 4、无法执行QT程序 1&#xff09;未设置环境变量 …

CentOS7安装Mysql5.7(ARM64架构)

1.第一步&#xff1a;下载 arm 版本离线 mysql 5.7 安装包 arm 版本离线 mysql 5.7 安装包 2.第二步&#xff1a;查询并卸载 CentOS 自带的数据库 Mariadb 找到数据库 mariadb&#xff0c;如果有会给出一个结果&#xff0c;结果是 mariadb 名称 rpm -qa | grep mariadb 如果…

AI数字人口播源码开发全解析

——源码即未来&#xff1a;揭秘千亿级市场的技术底层逻辑 一、为什么源码开发是数字人赛道的“核武器”&#xff1f; 2025年全球AI数字人市场规模预计突破6402.7亿元&#xff0c;而源码开发能力正成为企业竞争的核心壁垒。与标准化SaaS工具相比&#xff0c;源码开发赋予三大…

Versal - XRT(CPP) 2024.1

目录 1.简介 2. XRT 2.1 XRT vs OpenCL 2.2 Takeways 2.3 XRT C APIs 2.4 Device and XCLBIN 2.5 Buffers 2.5.1 Buffer 创建 2.5.1.1 普通 Buffer 2.5.1.2 特殊 Buffer 2.5.1.3 用户指针 Buffer 2.5.2 Data Transfer 2.5.2.1 read/write API 2.5.2.2 map API 2…

GPPT: Graph Pre-training and Prompt Tuning to Generalize Graph Neural Networks

GPPT: Graph Pre-training and Prompt Tuning to Generalize Graph Neural Networks KDD22 推荐指数&#xff1a;#paper/⭐⭐#​ 动机 本文探讨了图神经网络&#xff08;GNN&#xff09;在迁移学习中“预训练-微调”框架的局限性及改进方向。现有方法通过预训练&#xff08…

微信小程序上如何使用图形验证码

1、php服务器生成图片验证码的代码片段如下&#xff1a; 注意红框部分的代码&#xff0c;生成的是ArrayBuffer类型的二进制图片 2、显示验证码 显示验证码&#xff0c;不要直接image组件加上src显示&#xff0c;那样拿不到cookie&#xff0c;没有办法做图形验证码的验证&…

MAX232数据手册:搭建电平转换桥梁,助力串口稳定通信

在现代电子设备的通信领域&#xff0c;串口通信因其简单可靠而被广泛应用。MAX232 芯片作为串口通信中的关键角色&#xff0c;发挥着不可或缺的作用。下面&#xff0c;我们将依据提供的资料&#xff0c;深入解读 MAX232 芯片的各项特性、参数以及应用要点。 一、引脚说明 MAX2…

HTTP 与 HTTPS 协议:从基础到安全强化

引言 互联网的消息是如何传递的&#xff1f; 是在路由器上不断进行跳转 IP的目的是在寻址 HTTP 协议&#xff1a;互联网的基石 定义 HTTP&#xff08;英文&#xff1a;HyperText Transfer Protocol&#xff0c;缩写&#xff1a;HTTP&#xff09;&#xff0c;即超文本传输协…

记录linux安装mysql后链接不上的解决方法

首先确保是否安装成功 systemctl status mysql 如果没有安装的话&#xff0c;执行命令安装 sudo apt install mysql-server 安装完成后&#xff0c;执行第一步检测是否成功。 通常初始是没有密码的&#xff0c;直接登陆 sudo mysql -u root 登录后执行以下命令修改密码&…

精讲坐标轴系统(Axis)

续前文&#xff1a; 保姆级matplotlib教程&#xff1a;详细目录 保姆级seaborn教程&#xff1a;详细目录 seaborn和matplotlib怎么选&#xff0c;还是两个都要学&#xff1f; 详解Python matplotlib深度美化&#xff08;第一期&#xff09; 详解Python matplotlib深度美化&…

OSPF路由ISIS路由与路由学习对比(‌OSPF vs ISIS Routing Learning Comparison)

OSPF路由ISIS路由与路由学习对比 1.OSPF 路由学习规律 OSPF使用链路状态数据库&#xff08;Link State Database&#xff09;来存储网络拓扑信息。每个OSPF路由器通过交换链路状态更新&#xff08;Link State Updates&#xff09;来了解整个网络的拓扑&#xff0c;并根据收到…

【基于Mesh组网的UWB技术讨论】

基于Mesh组网的UWB技术讨论 Mesh 组网无线Mesh与无线中继的区别 基于Mesh拓扑的UWB技术可行性星型拓扑 / Mesh拓扑的UWB技术比较 Mesh 组网 Mesh(网格)是一种无中心、自组织的高度业务协同的网络。通常分为无线Mesh和有线Mesh&#xff0c;但在实际应用场景&#xff0c;有线Mes…

拼电商客户管理系统

内容来自&#xff1a;尚硅谷 难度&#xff1a;easy 目 标 l 模拟实现一个基于文本界面的 《 拼电商客户管理系统 》 l 进一步掌握编程技巧和调试技巧&#xff0c;熟悉面向对象编程 l 主要涉及以下知识点&#xff1a; 类结构的使用&#xff1a;属性、方法及构造器 对象的创建与…