【Linux】:程序地址空间

朋友们、伙计们,我们又见面了,本期来给大家解读一下有关Linux程序地址空间的相关知识点,如果看完之后对你有一定的启发,那么请留下你的三连,祝大家心想事成!

C 语 言 专 栏:C语言:从入门到精通

数据结构专栏:数据结构

个  人  主  页 :stackY、

C + + 专 栏   :C++

Linux 专 栏  :Linux

​ 

目录

1. 程序地址空间分布 

2. 基于地址空间,重新理解地址

3. 进程地址空间

3.1 地址空间和区域划分

3.2 为什么要有地址空间? 

4. 基于地址空间进行扩展

4.1 每一个进程都有页表

4.2 缺页中断 

4.3 进程的独立性

5. 写时拷贝


1. 程序地址空间分布 

在C语言阶段就了解过这个图,那么本章来配合代码深入了解一下:

#include <stdio.h>
#include <stdlib.h>

int un_gval;
int init_gval = 100;

int main(int argc, char *argv[], char *env[])
{
    printf("code addr: %p\n", main);                     // 代码区

    const char *str = "HelloLinux!";
    printf("read only char addr: %p\n", str);            // 字符常量区

    printf("init global value addr: %p\n", &init_gval);  // 已初始化全局数据区
    printf("uninit global value addr: %p\n", &un_gval);  // 未初始化全局数据区

    char* heap = (char*)malloc(100);
    printf("heap addr: %p\n", heap);                     // 堆区

    printf("stack addr: %p\n", &str);                    // 栈区

    int i = 0;
    for(i = 0; argv[i]; i++)
    {
        printf("argv[%d]: %p\n",i, argv[i]);             // 命令行参数
    }

    for(i = 0; env[i]; i++)
    {
        printf("env[%d]: %p\n",i, env[i]);               // 环境变量
    }
    return 0;
}

​ 使用代码将对应区域的地址打印出来可以发现于图片完全一致。

① 在程序地址空间中的堆区是向上增长的,栈区是向下增长的,通常也叫做堆栈相对而生。

② 我们定义的任何类型(栈区中)都是整体向下开辟,使用时局部向上使用。

③ 在栈中定义的int类型变量是4个字节,我们要访问时,需要通过它的起始地址再配合它的类型大小进行访问,变量类型大小就相当于起始地址的偏移量,访问的形式就是起始地址 + 偏移量。

④ static修饰局部变量本质上就是将局部变量的地址放到了全局区(全局变量)。

2. 基于地址空间,重新理解地址

在之前的进程创建与进程fork本质章节中遗留了一个问题:如何理解同一个变量会有两个不同的指?

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>

int g_val = 100;

int main()
{
   pid_t id = fork();
   if(id == 0)
   {
       //child
       int cnt = 5;
       while(1)
       {
           printf("child, Pid: %d, Ppid: %d, g_val: %d, &g_val=%p\n", getpid(), getppid(), g_val, &g_val);
           sleep(1);
           if(cnt == 0)
           {
               g_val=200;
               printf("child change g_val: 100->200\n");
           }
           cnt--;
       }
   }
   else
   {
       //father
       while(1)
       {
           printf("father, Pid: %d, Ppid: %d, g_val: %d, &g_val=%p\n", getpid(), getppid(), g_val, &g_val);
           sleep(1);
       }
   }
   return 0;
}

可以看到具有相同的地址同一个变量居然会有两个值,那么这也就证明了我们C/C++中观察到的地址并不是物理地址,我们平时用到的地址都是虚拟地址/线性地址

3. 进程地址空间

前面提到的虚拟地址也叫做进程的地址空间,它属于进程PCB中的一个字段,每一个进程在运行之后,都会有一个进程地址空间。

现在就来一步一步解释为什么同一个地址的变量会有两种值:

① 我们定义的全局变量g_val在已初始化全局数据区,里面保存的是该变量的起始地址,进程地址空间不存储数据,它是虚拟地址,那么就要有需要有真正存储数据的地址--物理地址

② 数据存储在物理地址中,需要通过一种类似于hash的映射关系由虚拟到物理的转化,这种方式在这里叫做--页表,通过页表可以完成由虚拟地址映射到物理地址。

③ 父进程创建子进程的时候需要以自己的PCB为模版来构建子进程的PCB,所以父进程中的全局变量g_val的虚拟地址在子进程的进程地址空间中也会有,同样的,子进程的页表也需要按照父进程为模版构建,所以虚拟到物理的转化关系也有了。

④ 此时,子进程的虚拟地址到物理地址的转化之后也指向了同一块物理地址,当检测到子进程要修改这个变量时,OS会先以写时拷贝的方式在物理地址中重新找一块空间,拷贝原来的数据到新的空间,并将子进程页表中的映射关系随之改变,然后就可以随意的修改变量。

⑤ 当子进程修改完变量的值之后,我们再查看时就会发现同一个地址(虚拟地址)的变量会有两个值。

3.1 地址空间和区域划分

先来了解一下空间的概念(以32位机器为例),在之前C语言的指针阶段就提到,计算机只认识二进制,那么二进制的0或1表示的就是有无的意思,那么在计算机里面的0或1表示的就是是否有电频,32位机器中存在会有32根地址总线,每一根地址线表示的情况都会两种,所以32根地址线一共会有2^32种情况,我们访问数据是以byte为单位,所以它的总大小换算一下就是2^32byte = 4GB大小的空间。 

地址空间 

假设一个OS的内存一共有4GB的空间大小,在我们运行程序的时候,OS会管理许多的进程,那么进程被调度是需要内存空间的,所以呢,OS就会虚拟的给每一个进程分配OS仅有的4GB的内存空间,那么在OS管理下的所有的进程都会认为自己将来会有4GB的内存空间,简单的说就是OS给每一个进程画了一张饼,那么这张饼就叫做虚拟地址空间(地址空间)

区域划分

通过一个小故事来理解区域划分:

在某小学,小胖和小花是同桌,共同使用一个长度位100cm的桌子,由于小胖的不注意卫生,遭到了小花的嫌弃,所以呢,小花就提出不再共同使用这张桌子,而是在桌子的中间画一条线,他两每一个用一半,这条线也被我们亲切的称为38线,所以画38线的本质就是对空间进行区域划分

区域调整

还是小胖和小花的这个例子,再画完38线之后呢,小胖和小花愉快的度过了一段时间,但是还是因为小胖的不自觉,经常把自己的垃圾放在小花的那一块,这就让小花很不能忍受,再加上小花实力在小胖之上,所以直接将小胖的区域再次压缩,从之前的五五分直接变成了四六分,对小胖的区域压缩的行为就叫做区域调整。

代码简述

对小胖和小花的这个行为使用计算机语言简单的描述就是:

地址空间也要被管理! 

在OS中会有许多的进程,每一个进程都有对应的地址空间,在系统中,一定要对地址空间做管理,防止地址空间的混淆。根据管理的本质:先描述,再组织。

在Linux中,这个进程/虚拟地址空间的东西叫做:struct mm_struct:

它是进程PCB中的一个字段,在PCB中是通过struct mm_struct *mm指向的一个结构化字段。

得出的结论:地址空间最终是一个内核的数据结构对象!就是一个内核结构体,所以我们看到的地址叫做虚拟地址。

3.2 为什么要有地址空间? 

1. 地址空间固定的存储结构,可以让进程以统一的视角看待内存,所以任意一个进程,可以通过地址空间 + 页表将乱序的内存数据变成有序并分门别类的规划好。

在我们的计算机中存在许多的程序,那么当程序要运行就要被加载到内存中,OS就要在内存中给进程分配空间,此时的进程的代码和数据会在内存中杂乱的分布,没有顺序,这使得PCB在寻找自己的代码和数据时非常麻烦,地址空间恰好解决了这一点。

2. 地址空间配合页表可以很好的进行进程访问的内存安全检查。  

在页表中还存在一个字段,它表示的是访问权限的字段,有的是只读,有的是只写,有的是读写,就比如常量字符串只允许读,不允许修改。地址空间就起到了一个控制检查的作用。

3. 将进程管理和内存管理解耦 

由PCB到虚拟地址的提取以及保存的这一过程是属于进程管理的,从内存到物理地址的提取与保存这一过程是属于内存管理的,两者互不影响! 

4. 基于地址空间进行扩展

4.1 每一个进程都有页表

在CPU内部有一个寄存器叫做:CR3寄存器,它主要是保存当前进程的页表地址。

在之前的进程切换章节我们了解到,进程要被CPU调度,进程在CPU内运行形成的临时数据叫做进程的硬件上下文,那么页表由虚拟到物理的转化也是属于数据,那么CP3寄存器的数据也叫做该进程的硬件上下文,当进程切换的时候,会将进程的硬件上下文数据从寄存器剥离下来,保存在自己的PCB中,那么每一个进程都要这么做,所以每一个进程都有自己独立的页表。

4.2 缺页中断 

页表中的虚拟地址可能有很多,但是物理地址可能还没有分配好,所以再继续访问的时候发现物理地址没有分配好,此时OS就会暂停访问,然后在物理地址中开辟空间,并且修改页表,然后继续执行访问,这个操作叫做缺页中断。

页表中还存在一个字段,它表示的是该地址是否分配或者是否有内容。 

4.3 进程的独立性

虚拟地址有很多个,有可能相同,也有可能不同,多个进程通过页表由虚拟地址映射到同一块内存,这些个虚拟地址很可能相同,也有可能不同,通过各自的页表的映射关系之后,所映射的物理地址是完全不一样的,所以即使两个相同虚拟地址的进程,其中一个挂掉了,也不会影响另外一个。

通过页表,让进程映射到不同的物理内存,从而体现了进程具有独立性! 

5. 写时拷贝

在前面说到过当子进程写入的时候,OS会发生写时拷贝,重新开辟一块空间给子进程,那么这个写时拷贝中间还存在许多细节:

1. 当父进程形成子进程的时候,子进程开始写入,那么OS会在何时发生写时拷贝?或者说是在某一时机发生写时拷贝?

当父进程创建子进程的时候,首先将自己的页表读写权限改为只读,然后再创建子进程,但是这个过程用户并不知道,当用户进行写入时,会因为页表转化的权限问题而出错,此时,操作系统就会介入,从而触发重新申请内存的拷贝内容的策略机制,这个就叫做写时拷贝。

2. 反正都是要写入,只重新开辟空间就好了,为什么要拷贝原来的内容呢?

我们写入的操作不一定要把原始数据全部修改,如果不拷贝原始数据,然后写入操作,会导致原始数据的丢失以及不完整。

朋友们、伙计们,美好的时光总是短暂的,我们本期的的分享就到此结束,欲知后事如何,请听下回分解~,最后看完别忘了留下你们弥足珍贵的三连喔,感谢大家的支持!     

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

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

相关文章

css使用伪元素after或者before的时候想要给after设置z-index无效

css使用伪元素after或者before的时候想要给after或者before设置一个层级关系&#xff0c;使该伪类写入的样式在box的下面&#xff0c;发现给box设置z-index无效&#xff0c; 需要找到父级元素&#xff0c;在父级元素上设置z-index值并且将伪类设置z-index:-1

【Whisper】WhisperX: Time-Accurate Speech Transcription of Long-Form Audio

Abstract Whisper 的跨语言语音识别取得了很好的结果&#xff0c;但是对应的时间戳往往不准确&#xff0c;而且单词级别的时间戳也不能做到开箱即用(out-of-the-box). 此外&#xff0c;他们在处理长音频时通过缓冲转录

Spark快速大数据分析PDF下载读书分享推荐

《Spark 快速大数据分析》是一本为 Spark 初学者准备的书&#xff0c;它没有过多深入实现细节&#xff0c;而是更多关注上层用户的具体用法。不过&#xff0c;本书绝不仅仅限于 Spark 的用法&#xff0c;它对 Spark 的核心概念和基本原理也有较为全面的介绍&#xff0c;让读者能…

Elasticsearch:Runtime fields - 运行时字段(一)

目录 使用运行时字段带来的好处 激励 折衷 映射运行时字段 定义运行时字段而不使用脚本 忽略运行时字段上的脚本错误 更新和删除运行时字段 在搜索请求中定义运行时字段 创建使用其他运行时字段的运行时字段 运行时字段&#xff08;runtime fields&#xff09;是在查询…

golang结合neo4j实现权限功能设计

neo4j 是非关系型数据库之图形数据库&#xff0c;这里不再赘述。 传统关系数据库基于rbac实现权限, user ---- role ------permission,加上中间表共5张表。 如果再添上部门的概念&#xff1a;用户属于部门&#xff0c;部门拥有 角色&#xff0c;则又多了一层&#xff1a; user-…

WPF UI 界面布局 魔术棒 文字笔记识别 技能提升 布局功能扩展与自定义 继承Panel的对象,测量与排列 系列七

应用开发第一步 功能分类&#xff1a;页面上的功能区域划分。。。。需求分析 业务逻辑 数据流 功能模块 UI/UX 编码 测试 发布 功能开发与布局 不用显式的方式设定元素的尺寸 不使用屏幕坐标来指定位置 Grid 功能最强大&#xff0c;布局最灵活的容器…

代码提交错分支了怎么办?

你有么有遇到过正在开发的代码&#xff0c;提交到生产环境的分支去&#xff0c;遇到这种情况怎么办&#xff1f; 问题重现&#xff1a; 这段注释// AAAAAAAAAAA 本来应该写在dev分支的&#xff0c;现在提交并push到master分支了 现在第一步&#xff0c;撤回提交 第二步&…

第五届机械工程与智能制造国际学术会议(MEIM 2024,7月26-28)

第五届机械工程与智能制造国际学术会议(MEIM 2024) 计划2024年7月26-28日在中国辽宁锦州隆重举行。本次会议由辽宁理工学院主办。 会议主要围绕机械工程与智能制造等研究领域展开讨论&#xff0c;旨在为从事机械工程与智能制造研究的专家学者、程技术人员、技术研发人员提供一个…

Midjourney 如何使用参考图像来提升图像的准确性和相似度?

🧙🏼图像提示 🧙🏼‍♂️ 您可以使用图像作为提示的一部分来影响作业的构图、样式和颜色。图像提示可以单独使用,也可以与文本提示一起使用 - 尝试组合具有不同样式的图像以获得最令人兴奋的结果。 🛠️实际图像提示操作步骤 点击加号按钮,双击上传文件,把小黄猫…

SwiftUI 6.0(iOS 18.0)滚动视图新增的滚动阶段(Scroll Phase)监听功能趣谈

何曾几时&#xff0c;在 SwiftUI 开发中的秃头小码农们迫切需要一种能够读取当前滚动状态的方法。 在过去&#xff0c;他们往往需要借助于 UIKit 的神秘力量。不过这一切在 SwiftUI 6.0 中已成“沧海桑田”。 在本篇博文中&#xff0c;您将学到如下内容&#xff1a; 1. Scroll…

Anubi WebKey开启去中心化数字革命的新纪元

随着技术的飞速发展&#xff0c;Web3正在重新定义未来互联网的架构&#xff0c;标志着从集中式控制向去中心化自主的历史性转变。在这场全球性的技术演变中&#xff0c;Anubi WebKey不仅仅是一款前沿的智能设备&#xff0c;它代表的是一种划时代的技术革命&#xff0c;一个重塑…

24.【C语言】getchar putchar的使用

1.基本作用 用户输入字符&#xff0c;getchar()获取字符&#xff08;含\n:即键入的Enter&#xff09;&#xff08;字符本质上是以ASCII值或EOF&#xff08;-1&#xff09;存储的&#xff09;&#xff08;与scanf有区别&#xff09; putchar() 打印字符&#xff08;把得到的A…

图像畸变矫正与透视变换

图像畸变矫正与透视变换 Halcon自动生成的圆形棋盘格Halcon透视变换 Halcon自动生成的圆形棋盘格 示例程序&#xff1a; *生成棋圆形棋盘格 行 列 直径 直径/距离比值 gen_caltab (12, 9, 0.002, 0.5, caltab_12X9.descr, caltab.ps) *生成相机参数 焦距 畸变系数 X解析度 Y解…

计算云服务1

前言 一直以来&#xff0c;计算资源都是整个企业业务系统发展所需的大动脉&#xff0c;没有计算资源&#xff0c;企业业务就无法正常运行。在云计算的时代里&#xff0c;计算服务也是云服务中的第一大类服务&#xff0c;计算资源的重要性由此可见。本章&#xff0c;我们将带领…

【数据结构】常见四类排序算法

1. 插入排序 1.1基本思想&#xff1a; 直接插入排序是一种简单的插入排序法&#xff0c;其基本思想是&#xff1a;把待排序的记录按其关键码值的大小逐个插入到一个已经排好序的有序序列中&#xff0c;直到所有的记录插入完为止&#xff0c;得到一个新的有序序列 。实际中我们…

HTML5+JavaScript单词游戏

HTML5 JavaScript单词游戏 数据字典格式&#xff1a;每行一个 单词 &#xff0c;单词和解释用空格分隔&#xff0c;如 a art.一(个)&#xff1b;每一(个) ability n.能力&#xff1b;能耐&#xff0c;本领 able a.有能力的&#xff1b;出色的 baby n.婴儿&#xff1b;孩子…

NET程序开发可能会用到的一些资料文档

NET程序开发使用的一些资料文件&#xff0c;NET高级调试&#xff0c;NET关键技术深入解析&#xff0c;WPF专业编程指南&#xff0c;程序员求职攻略&#xff0c;WPF编程宝典等。 下载链接&#xff1a;https://download.csdn.net/download/qq_43307934/89518582

Python入门 2024/7/6

目录 数据容器入门 列表的定义语法 基本语法 嵌套列表 ​编辑 列表的下表索引 ​编辑 列表的常用操作 列表的常见方法 查找元素的下标 修改下标索引的值 插入元素 追加元素 追加一批元素 删除元素 删除某元素在列表中的第一个匹配项 清空列表内容 统计元素在…

【Unity URP】通过代码动态添加URP渲染通道RendererFeature

URP的渲染通道RendererFeature可以很方便的实现一些渲染问题,比如渲染顺序问题,遮挡后的材质替换等等。 那么我们如何通过代码来动态添加和修改呢? 首先我们需要获取到当前的URP配置文件,在对配置文件进行添加 1.通过反射获取当前UniversalRendererData 我们通过Graphic…

Linux:文件系统与日志分析

一、block与inode 1.1、概述 文件是存储在硬盘上的&#xff0c;硬盘的最小存储单位叫做“扇区”(sector)&#xff0c;每个扇区存储512字节。 一般连续八个扇区组成一个"块”(block)&#xff0c;一个块是4K大小&#xff0c;是文件存取的最小单位。 文件数据包括实际数据…