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)
    {
        //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("子进程的g_val变为了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);
        }
    }
}

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

至此,我们能得出一个结论:我们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,最后取消暂停,让你访问。这个工作我们称之为缺页中断

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

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

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

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

相关文章

Linux shell:补充命令的使用

目录 一.导读 二.正文 三.结语 一.导读 上一篇介绍了脚本的简单概念以及使用&#xff0c;现在补充一些命令。 二.正文 目前处于全局目录&#xff0c;通过mkdir创建名我为day01的文件。 通过cd命令day01 切换至day01文件当中。 使用vim文本编辑器文件名&#xff08;firstdir&…

Dledger部署RocketMQ高可用集群(9节点集群)

文章目录 &#x1f50a;博主介绍&#x1f964;本文内容规划集群准备工作节点0配置&#xff08;ip地址为192.168.80.101的机器&#xff09;节点1配置&#xff08;ip地址为192.168.80.102的机器&#xff09;节点2配置&#xff08;ip地址为192.168.80.103的机器&#xff09;在所有…

基于springboot实现鞋类商品购物商城系统项目【项目源码+论文说明】计算机毕业设计

基于springboot实现鞋类商品购物商城系统演示 摘要 时代的变化速度实在超出人类的所料&#xff0c;21世纪&#xff0c;计算机已经发展到各行各业&#xff0c;各个地区&#xff0c;它的载体媒介-计算机&#xff0c;大众称之为的电脑&#xff0c;是一种特高速的科学仪器&#xf…

05|Mysql锁分类

1. 锁分类 1.1根据性能 乐观锁 ● 版本号 ● 读多场景 ● 第二次循环需要读取到最新的数据统计 示例 while{ // 1.调用方法获取当前版本号 getCurrentBalanceAndVersion(accountId); // 2.Java运算newBalance Balance 500; updateAccountBalance(account…

LVS+Keepalived高可用群集

一、Keepalived简介 Keepalived 软件起初是专为LVS负载均衡软件设计的&#xff0c;用来管理并监控LVS集群系统中各个服务节点的状态&#xff0c;后来又加入了可以实现高可用的VRRP功能。因此&#xff0c;Keepalived除了能够管理LVS软件外&#xff0c;还可以作为其他服务&…

低代码平台与MES:智能制造的新篇章

随着工业4.0和智能制造的兴起&#xff0c;企业对于生产过程的数字化、智能化需求日益迫切。传统的MES系统实施周期长、成本高&#xff0c;成为许多企业数字化转型的瓶颈。而低代码开发平台的出现为这一问题提供了新的解决思路。 一、万界星空科技低代码平台的优势&#xff1a; …

【项目管理】CMMI-质量保证过程

质量保证过程&#xff08;PQA)&#xff1a;通过质量保证活动&#xff0c;确保过程与产品满足过程、规程及相应的要求&#xff0c;确保问题得到关注与解决&#xff0c;使工作人员和管理者能够客观地了解过程与相关的工作产品。QA工程师应实施质量保证策划活动&#xff0c;客观地…

Leetcode—64. 最小路径和【中等】

2024每日刷题&#xff08;116&#xff09; Leetcode—64. 最小路径和 实现代码 class Solution { public:int minPathSum(vector<vector<int>>& grid) {int m grid.size();int n grid[0].size();vector<vector<int>> dp(m 1, vector<int&g…

代理ip并发是不是越大越好?为何对代理IP做出并发请求限制?

随着网络技术的不断发展&#xff0c;代理IP已成为许多人在上网时保护隐私和突破地域限制的重要手段。然而&#xff0c;在选择和使用代理IP时&#xff0c;有一个问题常常被人们提及&#xff1a;代理IP的并发数是不是越大越好&#xff1f;为何对代理IP做出并发请求限制&#xff1…

C++ 高频考点

1. C/C内存有哪几种类型&#xff1f; C中&#xff0c;内存分为5个区&#xff1a;堆(malloc)、栈(如局部变量、函数参数)、程序代码区&#xff08;存放二进制代码&#xff09;、全局/静态存储区&#xff08;全局变量、static变量&#xff09;和常量存储区&#xff08;常量&…

言语理解与表达-郭熙-01

1、听课小贴上 2、 言语理解与表达-题型分类&#xff08;章就是类&#xff09; 国考&#xff1a;40道题 时间建议&#xff1a;30分钟 目标正确率&#xff1a;80% 3、课程安排 4、第一章-片段阅读 看问题---> 带着问题读文段----> 确定选项 4.1 中心理解题 关联词&…

QT摄像头采集

主界面为显示框&#xff0c;两个下拉框&#xff0c;一个是所有相机&#xff0c;一个是相机支持的分辨率 系统根据UI界面自动生成的部分不再描述&#xff0c;以下为其他部分源码 widget.h #include <QWidget> #include <QMouseEvent> class QCamera; class QCamer…

osi模型,tcp/ip模型(名字由来+各层介绍+中间设备介绍)

目录 网络协议如何分层 引入 osi模型 tcp/ip模型 引入 命名由来 介绍 物理层 数据链路层 网络层 传输层 应用层 中间设备 网络协议如何分层 引入 我们已经知道了网络协议是层状结构,接下来就来了解了解下网络协议如何分层 常见的网络协议分层模型是OSI模型 和 …

Sentinel实战(待完善)

目录 服务雪崩 什么是服务雪崩 服务不可用原因 解决方案 技术选型对比 Sentinel 介绍 优点 核心概念 资源 规则 代码实战 API实现 SentinelResource注解埋点实现 服务雪崩 什么是服务雪崩 在服务调用链路中, 服务提供者不可用, 导致服务调用者不可用, 间接让上上游…

C++——类的6个默认成员函数

目录 类中的6个默认成员函数 构造函数 构造函数的特点 初始化列表 隐式类型转换 析构函数 拷贝构造函数 赋值重载 运算符重载 赋值重载 取地址重载 类中的6个默认成员函数 类中的6个默认成员函数根据不同的作用可以分为&#xff1a; 初始化和使用后清理&#xff1a;…

【vue】什么是虚拟Dom,怎么实现虚拟DOM,虚拟DOM一定更快吗

什么是虚拟Dom 虚拟 DOM 基于虚拟节点 VNode&#xff0c;VNode 本质上是一个对象&#xff0c;VDOM 就是VNode 组成的 废话&#xff0c;js 中所有的东西都是对象 虚拟DOM 为什么快&#xff0c;做了哪些优化 批量更新 多个DOM合并更新减少浏览器的重排和重绘局部更新 通过新VDO…

基于springboot+vue的保险信息网站(源码+论文)

文章目录 文章目录 前言 一、功能设计 二、功能实现 2.1 前台功能设计 2.1.1 网站首页 2.1.2 保险产品列表 2.1.3 留言板界面 2.1.4 产品查询界面 2.2后台管理设计 2.2.1 系统用户登录管理 三、库表设计 四、论文 前言 随着互联网的不断发展&#xff0c;现在人们获…

使用空闲电脑免费搭建一个私人的网盘

如果你也有一台空闲电脑&#xff0c;可以使用它来搭建一个私人的网盘。 这里使用的是飞梦云网盘&#xff1b; 服务端&#xff1a;下载 服务器文件使用hash校验进行储存&#xff0c;实现重复上传的文件秒传功能。 Fuse4Ui&#xff08;虚拟分区工具&#xff09;&#xff1a;下…

手撕Java集合之简易版Deque(LinkedList)

在目前&#xff0c;许多互联网公司的面试已经要求能手撕集合源码&#xff0c;集合源码本身算是源码里比较简单的一部分&#xff0c;但是要在面试极短的10来分钟内快速写出一个简易版的源码还是比较麻烦的&#xff0c;很容易出现各种小问题。所以在平时就要注重这方面的联系。 以…

Java 小项目开发日记 01(注册接口的开发)

Java 小项目开发日记 01&#xff08;注册接口的开发&#xff09; 1.项目需求 完成注册接口 2.项目目录 3. 配置文件&#xff08;pom.xml&#xff09; <project xmlns"http://maven.apache.org/POM/4.0.0" xmlns:xsi"http://www.w3.org/2001/XMLSchema-insta…