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://xiahunao.cn/news/2815584.html

如若内容造成侵权/违法违规/事实不符,请联系瞎胡闹网进行投诉反馈,一经查实,立即删除!

相关文章

java多线程并发实战,java高并发场景面试题

阶段一&#xff1a;筑基 Java基础掌握不牢&#xff0c;对于一个开发人员来说无疑是非常致命的。学习任何一个技术知识无疑不是从基础开始&#xff1b;在面试的时候&#xff0c;面试官无疑不是从基础开始拷问。 内容包括&#xff1a;Java概述、Java基本语法、Java 执行控制流程、…

代码随想录算法训练营第26天—回溯算法06 | ● *332.重新安排行程 ● *51. N皇后 ● *37. 解数独 ● 总结

*332.重新安排行程 https://programmercarl.com/0332.%E9%87%8D%E6%96%B0%E5%AE%89%E6%8E%92%E8%A1%8C%E7%A8%8B.html 考点 图论里的深度优先搜索&#xff08;本题使用回溯来解决&#xff09;这是一道hard题&#xff0c;一刷先放过去&#xff0c;二刷有精力再做 我的思路 无思…

Hybird App开发,纯血鸿蒙系统快速兼容救星

2024年1月18日的开发者&#xff08;HDC&#xff09;大会上&#xff0c;就官宣了“纯血鸿蒙”操作系统即将于2024年3季度正式投产。与此同时&#xff0c;支付宝、京东、小红书、微博、高德地图、中国移动等在内的超百个头部应用都启动了鸿蒙原生应用开发&#xff0c;鸿蒙开发者日…

多域名ov ssl证书1200元

SSL证书是一种特殊的数字证书产品&#xff0c;它是维护互联网信息安全的重要手段之一&#xff0c;部署到服务器之后可以保护网站信息传输安全。因此&#xff0c;随着互联网的发展&#xff0c;SSL证书也随之越来越受到众多开发者的重视。SSL证书的数字证书产品多种多样&#xff…

手写mybatis插件之分页查询

yml文件 server:port: 8081mybatis:mapper-locations: classpath:mapper/*.xmlconfig-location: classpath:mybatis-config.xmlspring:datasource:password: 1234username: rootdriver-class-name: com.mysql.jdbc.Driverurl: jdbc:mysql://localhost:3306/mybatis?useUnicod…

react-组件基础

1.目标 能够使用函数创建组件 能够使用class创建组件 能够给React元素绑定事件 能够使用state和setState() 能够处理事件中的this指向问题 能够使用受控组件方式处理表单 2.目录 React组件介绍 React组件的两种创建方式 React事件处理 有状态组件和无状态组件 组件中的state…

4G工牌室内外定位系统

4G工牌室内外定位系统是一种高效、精准的定位技术&#xff0c;它利用4G通信网络和GPS卫星定位系统&#xff0c;实现了对人员和物品的实时跟踪和定位。该系统广泛应用于企业管理、安全监控、智能交通等领域&#xff0c;为企业提供了更加高效、便捷的管理方式。 在室内环境中&am…

链表(C语言版)超详细讲解

链表 链表基础 一、链表的概念 定义&#xff1a; 链表是一种物理存储上非连续&#xff0c;数据元素的逻辑顺序通过链表中的指针链接次序&#xff0c;实现的一种线性存储结构。二、链表的构成 构成&#xff1a;链表由一个个结点组成&#xff0c;每个结点包含两个部分&#xff1…

全网最详细的Jmeter接口自动化测试

前面我们复习了jmeter 的非图形化界面运行我们的测试接口。 大家可以翻看往期jmeter的文章。 具体来说就是&#xff1a;jmeter -n -t ****.jmx -l ****.jtl -e -o **** (*号代表路径&#xff09; 生成了测试报告。 但是这个非图形化运行有个缺点&#xff0c;就是只能运…

蓝牙资产标签信标

随着科技的不断进步&#xff0c;蓝牙技术的应用已经深入到我们的日常生活中。其中&#xff0c;蓝牙资产标签作为一种新型的资产管理方式&#xff0c;正逐渐受到广泛欢迎。蓝牙资产标签是一种基于蓝牙技术的小型电子标签&#xff0c;可以粘贴在各种资产上&#xff0c;通过手机或…

代码随想录算法训练营第27天—贪心算法01 | ● 理论基础 ● 455.分发饼干 ● 376. 摆动序列 ● 53. 最大子序和

理论基础 https://programmercarl.com/%E8%B4%AA%E5%BF%83%E7%AE%97%E6%B3%95%E7%90%86%E8%AE%BA%E5%9F%BA%E7%A1%80.html 贪心算法的本质&#xff1a;由局部最优推到全局最优贪心算法的套路&#xff1a;无固定套路 455.分发饼干 https://programmercarl.com/0455.%E5%88%8…

(白盒测试)简单循环测试

简单循环测试 1.为什么要引入简单循环测试&#xff1f; 用来测试代码中的循环结构是否能正常执行 是否会少执行一次&#xff1f;多执行一次&#xff1f; 通过循环测试就可以得知 2.什么是简单循环&#xff1f; 没有嵌套的循环⇒简单循环 比如 单层的for循环 单层的while循…

【Qt】鼠标拖拽修改控件尺寸---八个方位修改

前提 在开发一个类似qdesiger的项目中 使用QGraphicsProxyWidget将Qt基础控件作为item放在场景视图中显示和编辑 创建自定义类继承QGraphicsProxyWidget&#xff0c;管理控件 成员变量 有控件的xywh等&#xff0c;其中x、y坐标存储是基于最底层widgetitem的 坐标系 x轴以右为正…

anaconda指定目录创建环境无效/环境无法创建到指定位置

已经设置目录到D盘 创建环境时还是分配到C盘 可能是指定位置没有开启读写权限&#xff0c;如我在这里安装到了anaconda文件夹&#xff0c;则打开该文件夹的属性->安全->编辑 allusers下的权限全都打勾

【DAY05 软考中级备考笔记】线性表,栈和队列,串数组矩阵和广义表

线性表&#xff0c;栈和队列&#xff0c;串数组矩阵和广义表 2月28日 – 天气&#xff1a;阴转晴 时隔好几天没有学习了&#xff0c;今天补上。明天发工资&#xff0c;开心&#x1f604; 1. 线性表 1.1 线性表的结构 首先线性表的结构分为物理结构和逻辑结构 物理结构按照实…

动态规划之使用最小花费爬楼梯【LeetCode】

动态规划之使用最小花费爬楼梯 LCR 088. 使用最小花费爬楼梯解法1解法2 LCR 088. 使用最小花费爬楼梯 LCR 088. 使用最小花费爬楼梯 解法1 状态表示&#xff08;这是最重要的&#xff09;&#xff1a;dp[i]表示以第i级台阶为楼层顶部&#xff0c;到达第i层台阶的最低花费。 状…

LeetCode_Java_移除链表元素(题目+思路+代码)

203.移除链表元素 给你一个链表的头节点 head 和一个整数 val &#xff0c;请你删除链表中所有满足 Node.val val 的节点&#xff0c;并返回 新的头节点 。 示例 1&#xff1a; 输入&#xff1a;head [1,2,6,3,4,5,6], val 6 输出&#xff1a;[1,2,3,4,5]思路&#xff1a;…

idea打包报错,clean、package报错

一、idea在打包时&#xff0c;点击clean或package报错如下&#xff1a; Error running ie [clean]: No valid Maven installation found. Either set the home directory in the configuration dialog or set the M2_HOME environment variable on your system. 示例图&#xf…

从0开始python学习-53.python中flask创建简单接口

目录 1. 创建一个简单的请求,没有写方法时默认为get 2. 创建一个get请求 3. 创建一个post请求&#xff0c;默认可以使用params和表单传参 4. 带有参数的post请求 1. 创建一个简单的请求,没有写方法时默认为get from flask import Flask, request# 初始化一个flask的对象 ap…

Python入门学习:if语句与条件控制--and、or、in、not in详解与实践

Python入门学习&#xff1a;if语句与条件控制–and、or、in、not in详解与实践 &#x1f308; 个人主页&#xff1a;高斯小哥 &#x1f525; 高质量专栏&#xff1a;Matplotlib之旅&#xff1a;零基础精通数据可视化、Python基础【高质量合集】、PyTorch零基础入门教程&#x1…