Linux------进程地址空间

目录

一、进程地址空间

二、地址空间本质

三、什么是区域划分 

四、为什么要有地址空间

1.让进程以统一的视角看到内存

2.进程访问内存的安全检查

3.将进程管理与内存管理进行解耦


一、进程地址空间

在我们学习C/C++的时候,一定经常听到数据存放在堆区、栈区、常量区、全局区等等概念。今天我们来详细了解一下这些是怎么回事。

我们下面这张图是32位系统最多能表示的范围,00000000到FFFFFFFF,数据都存放在该区域里,我们在该区域里进行位置的划分。其实这并不是真实的内存,而是进程地址空间

我们可以写一段代码来验证一下地址是否是这样分布的。

#include<stdio.h>
#include<stdlib.h>                                                       
int un_gval;
int init_gval = 100;int main()
{printf("代码区:%p\n", main);const char* str = "hello linux";printf("字符常量区:%p\n", str);printf("已初始化全局数据区:%p\n", &init_gval);printf("未初始化全局数据区:%p\n", &un_gval);char* heap1 = (char*)malloc(100);printf("堆区1:%p\n", heap1);printf("栈区:%p\n", &heap1);return 0;
}

运行一下可以看到打印出来的地址逐渐变大。

 刚好给之前的图对比起来,数据就是按照这个位置来存放的。

 我们多创建几个变量,看看堆和栈的生长方向是往哪边的。

#include<stdio.h>
#include<stdlib.h>                                                       
int un_gval;
int init_gval = 100;int main()
{printf("代码区:%p\n", main);const char* str = "hello linux";printf("字符常量区:%p\n", str);printf("已初始化全局数据区:%p\n", &init_gval);printf("未初始化全局数据区:%p\n", &un_gval);char* heap1 = (char*)malloc(100);char* heap2 = (char*)malloc(100);char* heap3 = (char*)malloc(100);char* heap4 = (char*)malloc(100);printf("堆区1:%p\n", heap1);printf("堆区2:%p\n", heap2);printf("堆区3:%p\n", heap3);printf("堆区4:%p\n", heap4);printf("栈区1:%p\n", &heap1);printf("栈区2:%p\n", &heap2);printf("栈区3:%p\n", &heap3);printf("栈区4:%p\n", &heap4);return 0;
}

打印结果如下,根据之前的图可以看到,堆栈相向而生,栈往地址变小的地方生长,堆往地址变大的方向增长

虽然栈内的定义的变量地址逐渐减小,但是如果我们将目光放细微一点,比如一个数组,在数组内部,索引大的地方比索引小的地方地址要大。这也是为什么我们变量要使用++。

如下代码,按照我们之前的分析,站内定义的变量地址逐渐减小,arr2的地址肯定比arr1小,但是在数组中,索引9的地址要比索引0地址大。栈全局地址变小,局部地址变大

这个程序进程地址空间图栈的部分如下所示,开辟空间的起始地址是在低地址处,会根据你开辟的大小,给你预留好位置,内部索引地址逐渐变大。

同理,结构体的地址分布也是类似, 全局地址变小,局部地址变大。都是以起始地址+偏移量进行访问的。

静态变量也是存放在全局区的,具体存放在已初始化全局数据区,因为编译器将静态变量认为了全局变量,因此函数调用了该变量,函数结束时静态变量并不会被释放。

二、地址空间本质

基于地址空间,重新理解地址。

我们使用代码来举例,如下代码定义了一个全局变量,fork一份子进程,让子进程修改一下全局变量的值,观察父子进程打印出来的值以及值地址变化情况。

#include<stdio.h>
#include<stdlib.h>
#include<sys/types.h>
#include<unistd.h>int g_val = 100;int main()
{pid_t id = fork();if(id==0){//childint 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("子进程的g_val变为了200\n");}cnt--;}}else{//fatherwhile(1){printf("father, Pid: %d,Ppid: %d,g_val: %d,&g_val: %p\n",getpid(),getppid(),g_val,&g_val);sleep(1);}}
}

打印出来看看结果,这有点颠覆我们的认知,同一地址,竟然能存放两个值,写时拷贝不应该地址不相同吗。 

至此,我们能得出一个结论:我们C/C++看到的地址,绝对不是物理地址。 其实我们平时用到的地址,都是虚拟地址

我们还知道,基于冯洛伊曼体系结构,进程的变量与数据,最终一定要在内存里每一个进程运行之后,都会有一个进程地址空间的存在。通过页表映射结构(kv模型),虚拟地址映射到物理地址,通过查找页表,就可以找到数据真实的存放位置了。这样才能保证,同一的进程地址空间的地址,有着不同的值。

具体结构如下,父进程task_struct里有字段能指向属于他自己的进程地址空间,进程地址空间里的虚拟地址,通过页表映射能找到物理地址,这样一来就能找到数据真实存放地址了。

父进程fork后创建子进程,子进程也有自己的task_struct,自己的进程地址空间,自己的页表结构,因此我们看到g_val变量在父进程的地址是0x60105c,子进程也是一样的值,一开始父子进程都页表映射都指向的是同一块物理地址,但是当子进程g_val发生变化后,页表的key不变,依然是0x60105c,但value会发生变化,这是写时拷贝,操作系统会在物理内存中新开辟一段空间,将新的值放入进去,同时将该地址写到子进程的页表里,这才完成了流程。 

有了这一块知识,现在我们也能理解fork之后,返回的 id 为何可以有两个值。

为了方便理解,我们再讲一个小故事。

        有一个大富翁,他拥有十亿美元,男人有钱就变坏,他也不例外,他的理想是一片森林而不是一颗树木,根本不打算结婚。彩旗飘飘的他,生下了4个私生子,这4个私生子互相不知道对方的存在,认为只有自己是他的儿子/女儿。他死后,自己一人能继承富翁的所有财产。平时私生子找大富翁要钱,要得很多大富翁肯定不会给,我都没死呢?你要这么多钱,我还用啥?但是金额不大的情况下,大富翁还是十分慷慨,说给就给。就这样一直生活下去。

在这个故事中,大富翁就是操作系统,十亿美元就是内存,私生子们就是各个进程。大富翁给每一个私生子都花了一张大饼,我的钱都是你的,这一张大饼就是进程地址空间。私生子(进程)每个人都以为自己有十个亿(内存),但是他们每个人都不可能要十个亿(内存)。

每一个进程都要有地址空间,地址空间也要被操作系统管理起来,管理就要用到之前我们在冯诺依曼体系结构中提到的 先描述,再组织 。因此,进程地址空间本质就是一个内核的数据结构对象,就是一个结构体!

三、什么是区域划分 

在进程地址空间中,我们进行了很多划分,将数据划分到对应的区中,再用页表映射到物理内存上,这样方便我们更好管理。生活中也存在区域划分的情况,比如我们上学时期同桌给划分的三八线,超过线就要被惩罚。

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

例如

struct mm_struct
{long code_start;long code_end;long data_start;long data_end;long heap_start;long heap_end;long stack_start;long stack_end;//........等等
}

使用long整形将区域的起始地址和结束地址存放起来,进程就可以将数据放到对于区域的地址范围中。这样就完成了区域划分。

我们打开linux2.6的源码也可以看到一些。 

我们之前提到过堆栈相向而生,这样就能让堆栈的区域可以灵活调整,只需要修改一下区域的start和end变量即可。

四、为什么要有地址空间

1.让进程以统一的视角看到内存

任意一个进程,可以通过地址空间+页表将乱序的内存数据,变成有序,分门别类的的规划好

如果没有地址空间,将来我们有程序加载时,肯定先加载代码,放到内存中某个位置,我们后续继续运行程序,会不断生成新的数据,该数据不一定放在代码加载地址的下面,因为该区域可能存在其他进程的数据。这样数据就是无序的。

有了进程地址空间,进程只知道数据在进程地址空间的某个规定的区域内就可以了,有页表的存在,不需要关心具体在物理内存的那个地方。这样就将无序转为了有序。

2.进程访问内存的安全检查

我们要对进程进行约束,防止进程对物理内存的一些不安全的行为。

比如代码段或者常量区是只读的,通过页表进行虚拟地址向物理地址的转化,同时页表中还有访问权限字段,该字段有r(读)、rw(读写)等等权限来进行进程访问内存的安全检查,如果不加以控制,进程对代码段随意修改,或者对常量区的数据修改,就会在页表处被拦住,不让你继续处理。

还有防止你对非法地址的访问,因为页表中根本没有非法地址的映射。只让你访问已经定义或开辟了内存的内容。

3.将进程管理与内存管理进行解耦

进程管理好理解,将进程从阻塞变为运行,加载进程的task_struct数据等等操作都是在进行进程管理。下面举个进程管理的实际应用的例子:

进程进行各种转化(虚拟到物理),各种访问(内存),一定是这个进程正在运行。(进程没在CPU上运行,根本就不会去访问内存)。每一个进程肯定是在CPU上运行的,CPU内存在一个叫做CR3的寄存器,他存放了当前进程页表的地址(物理地址),CR3也是进程的上下文,当进程切换的时候,进程的task_struct一定会保存CR3中的内容,再退出,而另一个进程运行前, 也一定要将CR3的填上自己task_struct中的数据,也就是说进程切换还要将进程地址空间和页表也做切换。这些本质上都是task_struct里面的字段

 而内存管理,我们讲个故事

        比如我们玩一些比较大的游戏,比如英雄联盟、CF或者其他3A大作,这种游戏小则10多个G,大则50G,一般电脑的内存是装不下的,但是这并不妨碍我们能运行这些游戏。

        当一个游戏很大的时候,操作系统并不会将游戏全部加载到内存里,他只需要加载游戏中的某一部分,比如你先登录的时候,他只加载登录这一部分,再比如吃鸡这种多人游戏,他会通过判断地图的远近和对手的距离,只加载你附近的建筑和对手,很远的东西就不考虑(这也是为何吃鸡人少的地方不卡,人一多就开始卡起来)。

        在我们学习进程状态的时候在一个状态叫做挂起状态,当操作系统内存资源严重不足,当前进程正在运行或者阻塞,他的代码和数据在内存中仍要占用空间,现在的该进程的某部分内容并不会被调度,操作系统就会将这些代码和数据置换出去。页表中还有一个字段用来表明虚拟地址是否分配有物理地址,里面是否有内容

        如果当前进程从11变成了00字段,就代表代码已经没有分配了,内容已经被置换出去了,该空间就被释放了,就可以给别人使用的,如果查页表时,发现很多映射字段都为00,我们就可以认为当前进程是挂起的。

        有了挂起,就可以让游戏的一部分申请进入内存,如果不需要了,就将他字段修改为00,这样就可以让进程边加载边执行。

        如果进程地址空间中代码字段为00,当前没有分配且无内容,但现在我们又要运行了,操作系统就会将你的访问请求先暂停,让内存加载这部分内容,页表重新填写映射的物理地址,字段修改为11,最后取消暂停,让你访问。这个工作我们称之为缺页中断

        其实我们这一套操作,叫做内存管理,进程并不知道我们详细做了什么。这样就完成了进程管理与内存管理的解耦

因为有了进程地址空间的存在,让不同的进程经过页表, 映射到物理内存的不同处,从而支持进程独立性。因为每个进程都有自己的进程地址空间与页表。

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

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

相关文章

4、正则表达式、本地存储

一、正则表达式 1、定义 用事先定义好的一些特定字符&#xff0c;这样的字符组合&#xff0c;组合成一个“规则字符串” 2、正则的组成 特殊字符 字母、数字、下划线、中文、特殊字符… 元字符&#xff08;常用&#xff09; 1、\d 匹配至少有一个数字 var reg /\d/ /…

SpringBoot整合rabbitmq-直连交换机队列(二)

说明&#xff1a;本文章主要是Direct定向/直连类型交换机的使用&#xff0c;它的大致流程是将一个队列绑定到一个直连交换机上&#xff0c;并赋予一个路由键 routingkey&#xff0c;当一个消息携带着路由值为routingkey&#xff0c;这个消息通过生产者发送给交换机时&#xff0…

【冲击蓝桥篇】动态规划(下):你还在怕动态规划!?进来!答题模板+思路解析+真题实战

&#x1f389;&#x1f389;欢迎光临&#x1f389;&#x1f389; &#x1f3c5;我是苏泽&#xff0c;一位对技术充满热情的探索者和分享者。&#x1f680;&#x1f680; &#x1f31f;特别推荐给大家我的最新专栏《数据结构与算法&#xff1a;初学者入门指南》&#x1f4d8;&am…

Python中检查一个数字是否是科技数的完整指南

目录 前言 什么是科技数&#xff1f; 如何判断一个数字是否是科技数&#xff1f; 分割数字并计算平方 Python实现科技数检测的示例代码 科技数的应用场景 1. 数字游戏 2. 数据处理 3. 算法优化 4. 数据结构设计 总结 前言 科技数&#xff08;Tech Number&#xff09;是一…

(二十三)Flask之高频面试点

目录&#xff1a; 每篇前言&#xff1a;Q1&#xff1a;为什么把request和session放在一起&#xff1f;Q2&#xff1a;Local对象的作用&#xff1f;Q3:&#xff1a;LocalStack对象的作用&#xff1f;Q4&#xff1a;一个运行中的Flask应用程序分别包括几个Local/LocalStack&#…

若依前后端分离版开源项目学习

前言&#xff1a;vscode中vue代码没有高亮显示&#xff0c;可以下载vetur插件解决&#xff0c;ctrl点击无法跳转函数定义问题&#xff0c;可以下载vue-helper插件解决&#xff1b;idea中ctrl点击函数即可跳转函数定义。 一、登录 1.生成验证码 基本思路&#xff1a; 后端生…

vue a-table 实现指定字段相同数据合并行

vue a-table 实现相同数据合并行 实现效果代码实现cloums数据格式数据源格式合并代码 实现效果 代码实现 cloums数据格式 const getColumns function () {return [{title: "分类",dataIndex: "checked",width: "150px",customRender: (text, …

JMeter--9.录制脚本

录制步骤 1.新建线程组&#xff1a;测试计划->线程->线程组 测试计划下&#xff0c;至少要有1个线程组&#xff0c;因为在录制器中需要选择【目标控制器】 2. 新建录制器&#xff1a;测试计划->非测试原件->HTTP(S)测试脚本记录器&#xff08;HTTP代理服务器&…

Ansible自动化运维(四)jinja2 模板、Roles角色详解

&#x1f468;‍&#x1f393;博主简介 &#x1f3c5;云计算领域优质创作者   &#x1f3c5;华为云开发者社区专家博主   &#x1f3c5;阿里云开发者社区专家博主 &#x1f48a;交流社区&#xff1a;运维交流社区 欢迎大家的加入&#xff01; &#x1f40b; 希望大家多多支…

Springboot+vue的考务报名平台(有报告)。Javaee项目,springboot vue前后端分离项目。

演示视频&#xff1a; Springbootvue的考务报名平台&#xff08;有报告&#xff09;。Javaee项目&#xff0c;springboot vue前后端分离项目。 项目介绍&#xff1a; 本文设计了一个基于Springbootvue的前后端分离的考务报名平台&#xff0c;采用M&#xff08;model&#xff0…

vue2后台管理系统demo,包含增删查改、模糊搜索、分页

因一直敲小程序&#xff0c;vue不熟练&#xff0c;自己练手项目&#xff0c;就包含增删查改以及模糊搜索分页 一、页面简单但功能齐全 二、数据是mock模拟 三、启动步骤 1、 json-server --watch data.json 启动mock数据 2、npm i 下载依赖 3、npm run serve 四、github地址…

ETH网络中的账户

ETH网络中的账户 Externally owned accounts (EOA) - 外部账户 由用户控制&#xff0c;我们导入助记词创建的账户就属于此类账户。 Contract accounts (smart contracts) - 合约账户 合约账户由以太坊虚拟机执行的代码控制。它也被称为智能合约。合约帐户有相关的代码和数据存…

Redis的高性能之道

前言&#xff1a;做码农这么多年&#xff0c;我也读过很多开源软件或者框架的源码&#xff0c;在我看来&#xff0c;Redis是我看过写得最优美、最像一件艺术品的软件&#xff0c;正如Redis之父自己说的那样&#xff0c;他宁愿以一个糟糕的艺术家身份而不是一名好程序员被别人记…

探索AI视频模型的无限可能:OpenAI的Sora引领创新浪潮

文章目录 &#x1f4d1;前言一、技术解析二、应用场景三、未来展望四、伦理与创意五、用户体验与互动&#x1f324;️总结 &#x1f4d1;前言 随着人工智能技术的蓬勃发展&#xff0c;AI视频模型正逐渐成为科技领域的新宠。在这个变革的浪潮中&#xff0c;OpenAI推出的首个AI视…

算法沉淀——动态规划之回文串问题(上)(leetcode真题剖析)

算法沉淀——动态规划之回文串问题 01.回文子串02.最长回文子串03.分割回文串 IV04.分割回文串 II05.最长回文子序列06.让字符串成为回文串的最少插入次数 01.回文子串 题目链接&#xff1a;https://leetcode.cn/problems/palindromic-substrings/ 给你一个字符串 s &#xf…

雾锁王国服务器官方配置要求说明

雾锁王国/Enshrouded服务器CPU内存配置如何选择&#xff1f;阿里云服务器网aliyunfuwuqi.com建议选择8核32G配置&#xff0c;支持4人玩家畅玩&#xff0c;自带10M公网带宽&#xff0c;1个月90元&#xff0c;3个月271元&#xff0c;幻兽帕鲁服务器申请页面 https://t.aliyun.com…

【机器人最短路径规划问题(栅格地图)】基于蚁群算法求解

基于蚁群算法求解机器人最短路径规划问题的仿真结果 仿真结果 收敛曲线变化趋势 蚁群算法求解最优解的机器人运动路径 各代蚂蚁求解机器人最短路径的运动轨迹

Java+SpringBoot+Vue:瑜伽馆管理的黄金组合

✍✍计算机毕业编程指导师 ⭐⭐个人介绍&#xff1a;自己非常喜欢研究技术问题&#xff01;专业做Java、Python、微信小程序、安卓、大数据、爬虫、Golang、大屏等实战项目。 ⛽⛽实战项目&#xff1a;有源码或者技术上的问题欢迎在评论区一起讨论交流&#xff01; ⚡⚡ Java、…

java工程师面试突击中华石杉,开发人员必学

如何高效的学习MyBatis源码呢&#xff1f; 市面上真正适合学习的MyBatis资料太少&#xff0c;有的书或资料虽然讲得比较深入&#xff0c;但是语言晦涩难懂&#xff0c;大多数人看完这些书基本都是从入门到放弃。学透MyBatis源码难道就真的就没有一种适合大多数同学的方法吗&am…