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

news2024/11/19 17:26:14

目录

一、进程地址空间

二、地址空间本质

三、什么是区域划分 

四、为什么要有地址空间

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,最后取消暂停,让你访问。这个工作我们称之为缺页中断

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

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

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

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

相关文章

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

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

TCP/IP-常用网络协议自定义结构体

1、TCP/IP模型&#xff1a; 2、TCP/IP- 各层级网络协议&#xff08;从下往上&#xff09;&#xff1a; 1&#xff09;数据链路层&#xff1a; ARP: 地址解析协议&#xff0c;用IP地址获取MAC地址的协议&#xff0c;通过ip的地址获取mac地 …

Vue项目 快速上手(如何新建Vue项目,启动Vue项目,Vue的生命周期,Vue的常用指令)

目录 一.什么Vue框架 二.如何新建一个Vue项目 1.使用命令行新建Vue项目 2.使用图形化界面新建Vue项目 三.Vue项目的启动 启动Vue项目 1.通过VScode提供的图形化界面启动Vue项目 2.通过命令行的方式启动Vue项目 四.Vue项目的基础使用 常用指令 v-bind 和 v-model v…

学生党福音!趁着拍拍开学季活动买平板啦!

谁还在买5年前的平板啊&#xff1f; 当然是我&#xff01; 虽然手里有台ipad&#xff0c;但ios系统限制多&#xff0c;不方便&#xff0c;一直想再要一台安卓平板。 去年观望了好久小米平板5pro&#xff0c;想着如果8256G配置价格在1500以下就入手&#xff0c;结果一直不掉价…

4_怎么看原理图之协议类接口之SPI笔记

SPI&#xff08;Serial Peripheral Interface&#xff09;是一种同步串行通信协议&#xff0c;通常用于在芯片之间传输数据。SPI协议使用四根线进行通信&#xff1a;主设备发送数据&#xff08;MOSI&#xff09;&#xff0c;从设备发送数据&#xff08;MISO&#xff09;&#x…

苹果ios群控软件开发常用源代码分享!

在移动软件开发领域&#xff0c;苹果设备由于其封闭性和安全性受到了广大开发者的青睐&#xff0c;然而&#xff0c;这也为开发者带来了一些挑战&#xff0c;特别是在进行群控软件开发时。 群控软件是指可以同时控制多台设备的软件&#xff0c;这在自动化测试、批量操作等场景…

01 MySQL之连接

1. 连接 1.0 基础认知 多表(主表)和一表(从表的区别): 多表一般是主表&#xff0c;一般存储主要数据&#xff0c;每个字段都可能存在重复值&#xff0c;没有主键&#xff0c;无法根据某个字段定位到准确的记录&#xff1b; 一表一般是从表&#xff0c;一般存储辅助数据&…

『Java安全』编译jdk

文章目录 一、源码下载二、环境依赖配置[^1]三、依赖检查及构建编译配置四、编译jdk五、编译完成完 一、源码下载 以OpenJDK为例&#xff1a; jdk&#xff1c;10访问OpenJDK Mercurial Repositories jdk≥10访问子目录jdk/jdk12: log (openjdk.org) 二、环境依赖配置1 i7-…

神经网络结构搜索(NAS)

华为诺亚AI系统工程实验室主任刘文志解读如何使用AutoML预测基站流量 - 知乎讲师介绍&#xff1a;刘文志&#xff08;花名风辰&#xff09;&#xff0c;华为诺亚AI系统工程实验室主任&#xff0c;异构并行计算专家&#xff0c;毕业于中国科学院研究生院&#xff0c;闻名于并行计…

鸿蒙应用程序包安装和卸载流程

开发者 开发者可以通过调试命令进行应用的安装和卸载&#xff0c;可参考多HAP的调试流程。 图1 应用程序包安装和卸载流程&#xff08;开发者&#xff09; 多HAP的开发调试与发布部署流程 多HAP的开发调试与发布部署流程如下图所示。 图1 多HAP的开发调试与发布部署流程 …

全网最详细的接口自动化测试框架实战(Pytest+Allure+Excel)

&#x1f345; 视频学习&#xff1a;文末有免费的配套视频可观看 &#x1f345; 关注公众号【互联网杂货铺】&#xff0c;回复 1 &#xff0c;免费获取软件测试全套资料&#xff0c;资料在手&#xff0c;涨薪更快 1. Allure 简介 Allure 框架是一个灵活的、轻量级的、支持多语…

Flink动态分区裁剪

1 原理 1.1 静态分区裁剪与动态分区裁剪 静态分区裁剪的原理跟谓词下推是一致的&#xff0c;只是适用的是分区表&#xff0c;通过将where条件中的分区条件下推到数据源达到减少分区扫描的目的   动态分区裁剪应用于Join场景&#xff0c;这种场景下&#xff0c;分区条件在joi…

【服务发现--ingress】

1、ingress介绍 Ingress 提供从集群外部到集群内服务的 HTTP 和 HTTPS 路由。 流量路由由 Ingress 资源所定义的规则来控制。 Ingress 是对集群中服务的外部访问进行管理的 API 对象&#xff0c;典型的访问方式是 HTTP。 Ingress 可以提供负载均衡、SSL 终结和基于名称的虚拟…

Nginx网络服务六-----IP透传、调度算法和负载均衡

1.实现反向代理客户端 IP 透传 就是在日志里面加上一个变量 Module ngx_http_proxy_module [rootcentos8 ~]# cat /apps/nginx/conf/conf.d/pc.conf server { listen 80; server_name www.kgc.org; location / { index index.html index.php; root /data/nginx/html/p…

德人合科技 | 天锐绿盾终端安全管理系统

德人合科技提到的“天锐绿盾终端安全管理系统”是一款专业的信息安全防泄密软件。这款软件基于核心驱动层&#xff0c;为企业提供信息化防泄密一体化方案。 www.drhchina.com 其主要特点包括&#xff1a; 数据防泄密管理&#xff1a;天锐绿盾终端安全管理系统能够确保数据在创…

element el-date-picker 日期组件置灰指定日期范围、禁止日期范围日期选择

JS如何将当前日期或指定日期转时间戳_javascript技巧_脚本之家 小于指定日期前的日期置灰 比如这里 指定日期是 2024-02-20 10:48:15 disabledDate(time) time是一个函数提供的时间用于比较 他是一个时间戳↓ 理解为我们想要置灰的时间 time.getTime() < timeStamps- 1 *…

通过一个栗子来看看创建和运行servlet

通过前面一篇文章的介绍&#xff08;搞着搞着对web project是不是有点迷糊&#xff1f;先关注几个问题-CSDN博客&#xff09;大家对于servlet作为中间层接收请求和反馈响应有了概念上的认知&#xff0c;接下来通过一个栗子来加深一下理解&#xff0c;还是基于前面的jsp项目&…

VBA技术资料MF123:程序运行过程中手动切换工作表

我给VBA的定义&#xff1a;VBA是个人小型自动化处理的有效工具。利用好了&#xff0c;可以大大提高自己的工作效率&#xff0c;而且可以提高数据的准确度。“VBA语言専攻”提供的教程一共九套&#xff0c;分为初级、中级、高级三大部分&#xff0c;教程是对VBA的系统讲解&#…

线程池(ThreadPoolExecutor,as_completed)和scrapy框架初步构建——学习笔记

用法1&#xff1a;map函数 with ThreadPoolExecutor() as pool: results pool.map(craw,utls)for result in results:print(result) 1.Scrapy框架&#xff1a; 五大结构&#xff1a;引擎&#xff0c;下载器&#xff0c;爬虫&#xff0c;调度器&#xff0c;管道&#x…

Opencv实战(3)详解霍夫变换

霍夫变换 Opencv实战系列指路前文&#xff1a; Opencv(1)读取与图像操作 Opencv(2)绘图与图像操作 文章目录 霍夫变换1.霍夫线变换1.1 原理1.2 HoughLines() 2.霍夫圆变换2.1 原理2.2 HoughCircles() 最基本的霍夫变换是从黑白图像中检测直线(线段) 霍夫变换(Hough Transform…