Linux_理解进程地址空间和页表

news2025/1/11 17:06:49

目录

1、进程地址空间示意图 

2、验证进程地址空间的结构

3、验证进程地址空间是虚拟地址 

4、页表-虚拟地址与物理地址

5、什么是进程地址空间

6、进程地址空间和页表的存在意义

6.1 原因一(效率性)

6.2 原因二(安全性)

6.3 原因三(解耦)

7、页表的使用

8、页表的权限 

9、页表的缺页中断 

10、页表的好处 

结语


前言:

        在Linux下,每一个进程都有属于自己的进程地址空间,进程地址空间又叫虚拟内存、虚拟地址空间,从“虚拟二字”可以判断进程地址空间并不是真实的物理空间,他只是物理空间的一个映射表,具体是通过页表作为媒介来建立他们之间的映射关系,所以我们在程序中定义的一系列变量,这些变量的地址都只是该进程的进程地址空间上的地址数,并不是真实的物理地址数。

        注意:本文物理空间和物理内存指的是一个概念。

1、进程地址空间示意图 

        我们所说的代码段(正文代码,包括字符常量区)、数据段(已初始化数据区)、BSS段(未初始化数据区)、堆区、共享区、栈区实际上都是Linux下的进程地址空间中的概念,物理内存上根本不存在上面这些划分,所以可以得出进程地址空间是在对物理内存进行管理。

        进程地址空间一般分为两个部分:用户空间、内核空间,用户空间就是上面所说的堆、栈区域,而内核空间拥有比用户空间更高的权限级别,他主要是系统内部进行进程管理、内存管理、设备驱动、文件系统、网络系统等相关工作,对外只会暴露接口给到程序员使用。进程地址空间示意图如下:

2、验证进程地址空间的结构

        从上图可以发现,进程地址空间的地址数是从下往上增大的,即在32位平台下,最低处代码段的地址是0x0000 0000,而最高处内核空间的地址是0xffff ffff,因此可以通过代码打印在不同区域所出创建的各种变量的地址,来观察他们的地址数就能验证进程地址空间的结构组织。

        代码如下: 

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

int g_val_1;
int g_val_2 = 100;

int main(int argc, char *argv[], char *env[])
{
    printf("main函数地址: %p\n", main);
    const char *str = "hello world";
    printf("代码段 %p\n", str);
    printf("数据段 %p\n", &g_val_2);
    printf("BSS段  %p\n", &g_val_1);

    char *mem = (char*)malloc(100);
    char *mem1 = (char*)malloc(100);
    char *mem2 = (char*)malloc(100);
    printf("堆区: %p\n", mem);
    printf("堆区: %p\n", mem1);
    printf("堆区: %p\n", mem2);
    printf("栈区: %p\n", &str);
    printf("栈区: %p\n", &mem);

    static int a = 0;
    static int a1;
    int b;
    int c;
    printf("数据段: %p\n", &a);
    printf("BSS段: %p\n", &a1);
    printf("栈区: %p\n", &b);
    printf("栈区: %p\n", &c);

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

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

    return 0;
}

        运行结果:

        从上图的测试结果可以发现,地址数的大小确实按照了进程地址空间的排布来打印,但是这里有必要说明一点:BSS段的地址数大部分场景下是比数据段的地址数要高的,但是BSS段的变量地址不一定就比数据段的变量地址要高,具体根据程序实现和操作系统加载机制有关。

3、验证进程地址空间是虚拟地址 

        若要验证进程地址空间里的地址数是虚拟地址,则需要用fork函数创建子进程来完成,具体思路:定义一个全局变量g_val,目的是让父子进程都能看到,然后在子进程中对g_val进行修改,发现父进程里看到的g_val还是原值,但是父子进程看到g_val的地址却还是一样的。即现象是:同一个地址下看到不同的值。 

        测试代码如下:

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

int g_val = 100;

int main()
{
   pid_t id = fork();
   if(id == 0)
   {
       int cnt = 3;
       // 子进程
       while(1)
       {
            //观察g_val的值和地址
           printf("i am child, pid : %d, ppid : %d, g_val: \
            %d, &g_val: %p\n", getpid(), getppid(), g_val, &g_val);
           sleep(1);

           if(cnt) cnt--;
           else {
               g_val=200;
               printf("子进程change g_val : 100->200\n");
               cnt--;
           }
       }
   }
   else
   {
       // 父进程
       while(1)
       {
           //观察g_val的值和地址
           printf("i am parent, pid : %d, ppid : %d, g_val: \
            %d, &g_val: %p\n", getpid(), getppid(), g_val, &g_val);
           sleep(1);
       }
   }
   return 0;
}

        运行结果:

        通过进程相关概念可以得知,当父子进程修改同一份资源时,会发生写时拷贝,即修改资源的进程会拷贝一份资源到该进程的空间地址中进行修改,这样一来修改的资源不会影响另一个进程所看到的资源,保证了进程的独立性。

        但是从测试结果可以发现,虽然发生了写时拷贝,但是他们的地址却是一样的,如果进程地址空间的地址数就是真实物理内存的地址数,那么这里所看到的g_val的地址肯定是不一样的,因为同一个地址只能记录一个值,因此可以推断进程地址空间上的地址数是虚拟的(即我们平常打印出来的地址数都是虚拟的)。他们和物理内存的关系图如下:

4、页表-虚拟地址与物理地址

        从上图可以发现虚拟地址和物理地址不一定是一一对应的,即虚拟地址的具体值在物理地址上可能指向的是另一个地址,那么要完成这个转换动作就必须有一个转换器,这个转换器就是页表,页表中记录了虚拟地址和物理地址的对应关系,其实发生写时拷贝的时候,虚拟地址在页表中对应的物理地址就已经是另一个地址数了,只是虚拟地址不关心物理地址的变化,并且程序员也无需关心,因为虚拟地址到物理地址的转换是由操作系统自动完成,操作系统只需要保证程序员访问虚拟地址时可以拿到正确的值即可。

        页表结构图:

        小结:每一个进程都会有一个进程地址空间的蓝图,即进程的进程地址空间是独立的,子进程会将父进程的PCB和进程地址空间和页表深拷贝一份给自己使用(除了一些子进程自带的特性字段不拷贝,比如子进程自己的pid,其他的内容都会拷贝一份),这也就解释了父子进程代码共享,所以创建一个进程的消耗是很大的,因为需要维护进程本身的结构体PCB还要维护地址空间、页表等。   

        有了上述的认知,就能理解fork创建子进程的细节了,因为fork返回值是一个写入,所以会发生写时拷贝,则子进程会在物理空间上开辟一块新空间存放子进程的id(此时子进程的页表的物理地址会被更改,但是虚拟地址不变),然后父进程和子进程通过查找自己的虚拟地址映射到不同的物理地址处,所以用于保存fork返回值的变量就会显示两个值。

5、什么是进程地址空间

         我们知道物理内存中的地址是由32根地址总线经过不同的排列组合得来的(32位平台下),所以在32位平台下,物理内存的大小是2^32 = 4GB,而进程地址空间是物理内存的映射,即进程地址空间也是有自己的大小,但是他的大小不可能和物理内存一样大,前面虽说进程地址空间的大小是4GB,其实只是一个地址范围而不是真正的大小,记录一个范围只需要两个int类型的变量即可,例子示意图如下:

        所以进程地址空间的堆、栈这些部分,其实只是用两个变量来维护的,这样一来只需多个变量就可以描述进程地址空间的结构了,所以进程地址空间本质是一个描述线性内存可视范围的结构体,该结构体内的成员变量的含义就是用来划分不同的区域板块。


        比如可以用以下结构体来描述进程地址空间(进程PCB结构体中有一个指向地址空间的指针): 

        每个进程的进程地址空间的范围都是一样的,所以对于每个进程而言,仿佛都可以申请到3G的物理内存空间,但是实际上并不如此,因为一个进程不可能用掉3G的物理空间,当物理内存快被消耗殆尽了,则系统肯定会发出警告,所以进程地址空间的结构体就像是操作系统给进程画的一个大饼,因为该结构体让进程以为物理空间内有很多的空间,但实际上可能没剩下多少空间了,但是进程只要向系统申请空间则物理空间就会分给该进程,若物理空间不足则申请失败。 

6、进程地址空间和页表的存在意义

6.1 原因一(效率性)

         因为若没有进程地址空间,则进程直接在物理内存上进行数据的存放,此时如果进程的状态变成挂起状态,为了节省物理内存则该进程原本存放在物理内存的数据可能就要被移到磁盘中,下一次该进程进入内存时要重新摆放数据至物理内存中,并且还要重新修改PCB的内容了,太麻烦了效率又低。

        有了进程地址空间后就无需关心进程在物理空间内的数据摆放的位置了,因为数据在物理内存中的存放顺序我们不关心,系统会帮我们建立页表和物理内存的映射关心,我们只要按照页表的虚拟地址进行寻址即可,所以可以把进程地址空间看成是进程和物理内存之间的桥梁、转换器。

        以统一视角来看待内存,做到了一致性,让进程对内存的分配和控制更加方便了。 

6.2 原因二(安全性)

        进程如果直接访问物理内存,会有可能更改其他用户的内容,而如果进程先访问虚拟地址空间和页表,若发生了修改其他用户内容的情况,则虚拟地址空间和页表会直接反馈并拒绝这个动作,达到保护物理内存的效果。 

        并且对于代码的结构也做了明确的功能划分,比如代码段的数据不可更改,保护了代码。

6.3 原因三(解耦)

        将进程管理模块和内存管理模块进行了解耦,具体示意图如下:

7、页表的使用

        从上文得知,若要使用进程地址空间,则在PCB结构体中的pmm指针就能找到进程地址空间,但是该如何找到并使用页表呢? 

        使用页表的示意图如下:

        总结而来就是进程PCB中间接的包含了找到页表的方法。 

8、页表的权限 

        页表实际上还有一列用于显示权限,示意图如下:

         页表会记录虚拟地址对应的物理地址是否为文字常量区,若为文字常量区而进程还要修改该地址的内容,则页表直接会报错并且终止这个进程,这也是为什么代码段的数据不可被修改,原因就是所有的访问都要通过页表这个媒介,页表会判断虚拟地址然后对权限做出相关改变。

9、页表的缺页中断 

        当进程被挂起时就表示缺页中断,该进程的代码会被从内存移至磁盘,页表中还有一列是专门记录代码是否还存放在内存中,因为若把进程的所有代码都从磁盘加载至内存中,有些代码还没使用到就会浪费内存的资源,因此进程的调度遵循着“分批加载-惰性加载” ,而页表的缺页中断就是为了让cpu知道目前哪些代码已经被加载进内存中哪些代码还在磁盘上。

        页表中用于记录当前是否为缺页中断的标识符示意图如下:

         进程的挂起实际上就是页表中断,他的底层是将进程的代码都拿走放到磁盘中,然后页表中的物理内存地址也清空,并且把内容标志位为0,这时候就是进程挂起了。挂起结束时就会根据内再将磁盘中的代码重新拿到内存中,然后把内容标志位从0置为1,表示缺页中断结束,并且页表中的物理内存地址填上加载后新的地址,这个过程虚拟地址是不需要改变的。

10、页表的好处 

        当把可执行程序加载到内存时,可以不考虑在内存的摆放顺序,因为有页表的存在,我们只需要关心页表中的虚拟地址就能判断出哪些数据只能读哪些地址只能写了,而且必须要用统一的视角看待内存,因为只有用统一的视角看待内存才能让内存的无序摆放对于进程来说是有序的。

结语

        以上就是关于进程地址空间一级页表的讲解,理解进程地址空间和页表是理解进程管理的重要一环,他属于进程管理中较为细节的一部分。

        最后希望本文可以给你带来更多的收获,如果本文对你起到了帮助,希望可以动动小指头帮忙点赞👍+关注😎+收藏👌!如果有遗漏或者有误的地方欢迎大家在评论区补充,谢谢大家!!  

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

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

相关文章

【CT】LeetCode手撕—236. 二叉树的最近公共祖先

目录 题目1- 思路2- 实现⭐236. 二叉树的最近公共祖先——题解思路 3- ACM实现 题目 原题连接&#xff1a;236. 二叉树的最近公共祖先 1- 思路 模式识别 模式1&#xff1a;二叉树最近公共祖先 ——> 递归 判断 递归思路&#xff0c;分情况判断&#xff1a; 1.参数及返…

【IEEE独立出版、有确定的ISBN号】第三届能源与电力系统国际学术会议 (ICEEPS 2024)

第三届能源与电力系统国际学术会议 (ICEEPS 2024) 2024 3rd International Conference on Energy and Electrical Power Systems 连续2届会后4-5个月EI检索&#xff0c;检索稳定&#xff01; 成功申请IEEE出版&#xff01; 特邀院士、Fellow 报告&#xff01; 一、大会信息 …

nexus配置问题

错误信息&#xff1a; npm ERR! code E401 npm ERR! Unable to authenticate, need: BASIC realm"Sonatype Nexus Repository Manager"解决办法一&#xff1a; npm login --registryhttp://192.168.52.128:8081/repository/npm-repo 输入 用户名 密码 邮箱完成后会…

用Python执行JavaScript代码,这些方法你不可不知!

目录 1、PyExecJS:轻量级桥梁 🌉 1.1 安装与配置 1.2 执行JS代码示例 1.3 案例:数据交互与转换 1.4 错误处理与性能考量 2、Node.js子进程 🌀 2.1 准备工作:安装Node.js 2.1 利用subprocess模块 2.2 实时数据交换技巧 2.3 实战:跨语言API调用 3、Selenium驱…

【vue scrollTo 数据无限滚动 】

vue数据无限滚动 参考来源 Vue3 实现消息无限滚动的新思路 —— 林三心不学挖掘机 vue3代码 <template><div class"scroll-container" ref"scrollRef"><div v-for"(item, index) in list" :key"index" style"hei…

基于Sringboot+Vue的校园招聘系统【原创】【开源】

浏览器&#xff1a;Chrome或360浏览器 系统环境配置 前置条件&#xff1a;系统已经安装了Mysql5.7、Mysql工具&#xff08;Navicat&#xff09;、JDK1.8、Maven3.6.1、vue3.0以下开发环境、 Intellij Idea、 Chrome或360浏览器 1、导入数据库 2、编译前端代码vue 编译&…

帮企商城10合一万能DIY分销商城小程序源码系统 带源代码包+搭建部署教程

系统概述 这是一款集多种功能于一体的源码系统&#xff0c;旨在为用户提供一站式的商城解决方案。它不仅支持小程序端&#xff0c;还能与其他平台无缝对接&#xff0c;满足不同用户的需求。 代码示例 系统特色功能一览 1.万能 DIY 功能&#xff1a;用户可以根据自己的需求和创…

视频云沉浸式音视频技术能力探索与建设

概述 随着传输技术、显示技术与算力的持续提升&#xff0c;用户对于音视频体验的需求在提高&#xff0c;各家设备厂商也在探索和推出对应的技术与产品。打造空间感的空间视频与空间音频是其中最为关键的2项技术&#xff0c;bilibili视频云在这两项技术领域也进行了相关代探索与…

足底筋膜炎的症状

足底筋膜炎是足底的肌腱或者筋膜发生无菌性炎症所致&#xff0c;其症状主要包括&#xff1a; 1、疼痛&#xff1a;这是足底筋膜炎最常见和突出的症状。疼痛通常出现在足跟或足底近足跟处&#xff0c;有时压痛较剧烈且持续存在。晨起时或长时间不活动后&#xff0c;疼痛感觉尤为…

计算机系统基础(一)

1. 引入——从源程序到可执行文件 了解高级语言编写的代码在后台是如何被编译并运行的 首先我们会编写一段代码&#xff0c;例如 #include<stdio.h>int main(){printf("hello world!\n");return 0; } 并把它命名为hello.c文件 预处理阶段 接下来通过命令…

数据结构--单链表(图文)

单链表的概念 在单链表中&#xff0c;每个元素&#xff08;称为节点&#xff09;包含两部分&#xff1a;一部分是存储数据的数据域&#xff0c;另一部分是存储下一个节点地址的指针域。这里的“单”指的是每个节点只有一个指向下一个节点的指针。 节点&#xff1a;链表中的基…

网络协议安全:TCP/IP协议栈的安全问题和解决方案

「作者简介」:北京冬奥会网络安全中国代表队,CSDN Top100,就职奇安信多年,以实战工作为基础对安全知识体系进行总结与归纳,著作适用于快速入门的 《网络安全自学教程》,内容涵盖Web安全、系统安全等12个知识域的一百多个知识点,持续更新。 这一章节我们需要知道TCP/IP每…

2024最新最全【网络安全/渗透测试】面试题汇总

思路流程 信息收集漏洞挖掘漏洞利用&权限提升清除测试数据&输出报告复测 问题深信服一面:SQL注入防护为什么参数化查询可以防止sql注入SQL头注入点盲注是什么&#xff1f;怎么盲注&#xff1f;宽字节注入产生原理以及根本原因 产生原理在哪里编码根本原因解决办法sql里…

Kafka中的时间轮算法

1. Kafka与时间轮&#xff1a; Kafka的定时器底层使用时间轮算法。Kafka时间轮是层次时间轮&#xff0c;并且支持时间轮复用。 优点&#xff1a; 高效的插入操作&#xff1a; 时间轮底层数据结构&#xff08;桶&#xff09;&#xff0c;使用双向链表的设计使得插入操作的时间…

掌握JavaScript ES6精髓:探索函数和对象的高级扩展与实用技巧

序言 JavaScript&#xff0c;作为前端开发中不可或缺的语言&#xff0c;已经发展到了ECMAScript 2015&#xff08;简称ES6&#xff09;以及后续的版本。ES6带来了诸多语法上的改进和创新&#xff0c;使得代码更加简洁、优雅&#xff0c;同时也提供了更多的编程模式和实用技巧。…

Python-面向对象编程(超详细易懂)

面向对象编程&#xff08;oop&#xff09; 面向对象是Python最重要的特性&#xff0c;在Python中一切数据类型都是面向对象的。 面向对象的编程思想&#xff1a;按照真实世界客观事物的自然规律进行分析&#xff0c;客观世界中存在什么样的实体&#xff0c;构建的软件系统就存在…

Docker搭建yolov8并训练自己的数据集(包会)

本文通过docker的方式搭建yolov8运行环境&#xff0c;并成功训练了自己的数据集&#xff08;化学仪器数据集&#xff09;。 1、安装docker 在一台通往的服务器上安装docker&#xff0c;建议参考我之前的文章&#xff1a;Docker基础学习-CSDN博客 2、创建yolov8镜像 首先编写…

openresty(Nginx) 301重定向域名 http访问强制使用https

1 访问http 2 修改配置访问 server {listen 80;server_name example.cn;return 301 https://$server_name$request_uri;access_log /data/logs/czgzzfjgsup_access.log access;error_log /data/logs/czgzzfjg_error.log error;#location / {root /usr/local/open…

【Linux进程】Linux下的---七大进程状态(什么是进程状态?Linux下有哪些进程状态?)

目录 一、前言 二、什么是进程状态&#xff1f; 三、操作系统(OS)下的 --- 进程状态 &#x1f525;运行状态&#x1f525; &#x1f525;阻塞状态&#x1f525; &#x1f525;挂起状态&#x1f525; 四、Linux下的7种进程状态 &#x1f525;运行状态 -- R&#x1f525;…

内存优化技巧:让数据处理更高效

Pandas无疑是我们数据分析时一个不可或缺的工具&#xff0c;它以其强大的数据处理能力、灵活的数据结构以及易于上手的API赢得了广大数据分析师和机器学习工程师的喜爱。 然而&#xff0c;随着数据量的不断增长&#xff0c;如何高效、合理地管理内存&#xff0c;确保Pandas Da…