初识Linux(9):程序地址空间

 实验:

  1 #include <stdio.h>2 #include <sys/types.h>3 #include <unistd.h>4 #include <string.h>5 6 int g_val = 100;7 8 int main()9 {10   printf("我是一个进程: pid:%d,ppid:%d\n",getpid(),getppid());11   pid_t id = fork();12   if(id<0)13   {14     perror("fork");15     return 0;16   }else if(id==0){17     //child process18     while(1)19     {20       printf("child process: pid:%d,ppid:%d,g_val:%d,add_of_v:%p\n",getpid(),getppid(),g_val++,&g_val);21       sleep(1);22     }23   }else{24     //parent process25     while(1){26       printf("parent process: pid:%d,ppid:%d,g_val:%d,add_of_v:%p\n",getpid(),getppid(),g_val,&g_val);27       sleep(1);                                                                                                                                                                                              28     }29   }30 31 32   return 0;33 }

实验结果: 

父子进程之间按理来说应该是写时拷贝,但是此时改变子进程的g_val的值,按理来说是拷贝到其他空间,但是打印出来的地址竟然是一样的。


1. 理解地址空间

 一个抽象例子:一个富豪有十亿美金,他有十个儿子,儿子们之间互相不知道、不认识,都认为自己可以继承所有财产,富豪给每个儿子都画饼,告诉他们自己可以继承十亿美金,不过鉴于他们年龄不同,目前从事的事业不同,这个先给200美金,那个先给3万,另一个先给10块.........

操作系统给每个进程都“画饼”,让每个进程就算现在只分300MB,但还是认为自己会有四个G的空间

每个进程都认为有2^32个地址空间给自己继承


 2. 理解内存划分

在学习指针的时候,我们有如下认知(总大小为4GB):

                            

由”画饼“的思想结合上图可得:每个程序还需要把自己的空间分划出具体结构。

struct mm_struct是task_struct中的一个指针。

linux下进程的地址空间的所有的信息的结构体是 mm_struct (内存描述符)
每个进程只有⼀个mm_struct结构,在每个进程的task_struct结构中,有⼀个指向该结构的指针。

mm_struct用于描述每个进程在每个虚拟区域的地址划分,逻辑结构如上图,真实存储如下图:

tip:

virtual memory就是vm的缩写

地址的本质是数字,所以用unsigned long来描述即可。

                    

区域划分的本质:只要知道了开始和结束的位置即可,这中间的空间我们都可以随意使用。


3. 初识页表 (Page Table)

简单来说,页表就是一个进程中虚拟地址和真实物理地址的映射

                                                        

利用页表回答:fork返回值为什么能让id的数值不一样?

"对于最基础的单级页表,每个进程一个页表,父子进程使用的是不同的页表,所以id的数值不一样"

                        

大概感受一下页表带来的作用:

页表的设计带来了虚拟内存,而虚拟内存存在的原因(大概有个感觉):
1. 地址空间隔离
虚拟内存可以将每个进程的内存空间隔离开来,确保一个进程不能直接访问或修改另一个进程的内存。
2. 扩展可用内存
现代计算机的物理内存有限,而程序的内存需求可能远超过物理内存的大小。虚拟内存允许操作系统将不常用的部分数据存储到磁盘上(例如硬盘的交换空间或交换文件中),而将常用的数据留在内存中。这样,操作系统能够:在物理内存不足时,利用磁盘空间(虽然访问速度慢),扩展可用内存的总量。通过分页机制或分段机制,高效地利用物理内存,确保程序仍然能够运行。


回到最开始的实验代码,我们继续了解页表:

这是一个父进程的task_struct所指向的mm_struct

                                       

g_val这样的全局变量是放在数据区的,假设红框就是他的虚拟地址

                  

红框对应的地址一定存在页表中,映射着其真实地址。

这时我们调用了fork函数:

创建子进程时,本来是“浅拷贝”父进程的模板

子进程的PCB(我们前面学习过,先建立PCB才会导入代码)不仅需要开出空间,还需要有初始化的内容,就像c++的内容一样

当然,拷贝肯定不是全拷,比如时间片/上下文信息/PID等内容就不会拷贝。

提示:

进程=代码和数据+PCB

其中,继承的时候,代码是只读的,存储于代码区。

页表当中有很多的标记位:

第一种:记录rwx的标记位

这样就能解释为什么下述代码的编译器不会报错,但是运行起来有问题:

                                                

                    

这种问题编译器拦不住,因为hello bit是存在于常量区(代码区)的,这一块在页表的映射中有(注意,是常量区,不是数据区)

标记为“不能w”,所以在*str='H'时发生权限错误,并且操作系统会认为一个进程去"w"一个不应该被写的空间是一定有错的,所以会直接杀掉这次进程(也就是这次程序运行)。因此这种错误是不能被编译器检查出来的,只是运行的时候被直接杀掉,语法上为了解决这个问题,就提出了const关键字

                                    

char* 的str本来就是不可修改的(操作系统层面),为什么还要加一个const?

基于上述解释,就能理解了:有const修饰,这下再进行*str='H'的操作,

就会在编译器层面上报错。

第二种:isexists标记位(is exists)

程序内容不一定随时都加载到了内存当中,可能会出现挂起的情况。

例如:

一个100个G的大程序要从磁盘加载到内存运行(比如玩黑神话悟空),内存本身的大小可能都没有100个G,必须分批将这个程序加载进来,比如先加载前2g,此时剩下的98G在页表中对应的isexists就写成false,虽然虚拟和真实有映射关系,但是这段内容并没有真正加载进捏内存。这2g的内容(2g对应的isexists是true)跑完之后,假设暂时不需要这部分内容,将其的isexists从true标记为false。

另一个例子:你的一个程序在等待scanf时被操作系统变成阻塞状态了,内容被交换到磁盘中去,这个时候的页表就要将对应的数据的isexists改为0

因此,每次映射不一定都是能映射到的,对应的数据和代码可能没有加载到内存中去。

页表能帮助我们把没有加载到内存中的数据给换入到内存中去。

页表可以用来分批分页加载

因此,一个进程不是一加载就全部拷到内存上,而是分批加载的


4. mm_struct

mm_struct 是 Linux 内核中用于描述进程虚拟地址空间的核心数据结构(页表就包含在里面)。它包含了进程的内存管理信息,是进程虚拟内存的抽象表示。

mm_struct也是结构体,是结构体就需要初始化。

比如程序A是一个3A大作,程序B是你写的hello world,正文部分(代码区)大小区别非常大,总不能每个mm_struct的代码区都初始化设置的一样大吧?

代码和数据空间是提前规划好的 

有一个命令叫readelf,即read ELF

可执行与链接格式(Executable and Linkable Format,简称ELF

readelf后面接一个elf格式的文件(注意,此时没有执行这个elf)

                  

               描述的都是各个区域的大小。

                比如以下这个是read only data的空间大小。

                     

结论:编译的时候每个区域的大小已经记录下来了。从这个进程的各个区域的大小属性处得到数据,用于初始化该进程的mm_struct

所以,操作系统需要有能力去读懂编译器在编译的时候很多属性,并且编译器和操作系统是有关联的!

虚拟空间中每一部分的大小都是在编译的时候就知道了。

 进程的代码和数据加载进来,对应的页表isexitst就置1,没加载就置0。

而像堆,栈这些区域,都是操作系统在程序运行起来之后再创建的。

                                       

整个进程的虚拟空间是由:磁盘中拷贝+操作系统自行动态创建的

自行创建:

堆区:每次malloc或者new的时候扩展一点虚拟内存,而不是立即在物理内存中找空间。

操作系统在“欺骗”进程。只要真正要使用这个空间时,操作系统才会去开空间。

栈区:用了才扩展出来栈区

【虚拟栈内存:】
每个进程都会有一个虚拟的栈区,这个栈区是进程的虚拟地址空间的一部分。mm_struct 中会保存该栈区的虚拟内存的大小和位置。
【物理栈内存:】
只有当进程使用栈空间时,操作系统才会真正为栈区分配物理内存。这意味着栈区的虚拟内存大小可能远大于进程实际使用的物理内存。
进程栈区的实际物理内存是动态分配的,操作系统可能会通过 页面分配将虚拟栈内存映射到物理内存中,只有当栈区被实际访问时(比如函数调用时,栈帧被推入栈中),操作系统才会分配物理页面并映射。 


5. 虚拟地址的作用

1.野指针等安全问题

为什么C/CPP语言访问一个野指针的时候会使得程序崩溃呢

因为虚拟地址在映射时的地址不对或者权限不对!这样就能保护内存

如果没有虚拟地址,一个进程在就是在直接访问内存的物理空间地址,意味着每一个进程都能访问整个空间地址,如果这是一个木马病毒,一个病毒就能使得整个内存瘫痪。

大概意思就是:“你买零食的时候你妈拿着钱”

2. 使得进程管理和内存管理在系统层面解耦合

让进程管理,比如malloc的时候,不再需要多考虑内存是否足够大等问题。

内存管理也只需要负责把空间给你就行,不需要管你是拿来作什么的。

因为有地址空间的存在,所以我们在C、C++语⾔上new, malloc空间的时候,其实是在地址
空间上申请的,物理内存可以甚⾄⼀个字节都不给你。⽽当你真正进⾏对物理地址空间访问
的时候,才执⾏内存的相关管理算法,帮你申请内存,构建⻚表映射关系(延迟分配),这
是由操作系统⾃动完成,⽤⼾包括进程完全0感知!!

 3.让进程以统一视角看待内存

物理内存可以认为不存在读写权限等问题,只要空间给了就行。

并且物理内存是延迟、惰性加载

其他作用:继承环境变量

环境变量的继承和全局变量的继承是一样的道理, 环境变量和命令行参数是在栈之上单独的虚拟地址,在页表上有自己单独的映射。子进程在继承时,拷贝PCB,拷贝 页表等等,自然就继承下去了

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

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

相关文章

RadASM环境,win32汇编入门教程之六

;运行效果 ;RadASM环境&#xff0c;win32汇编入门教程之六 ;在上一个教程里面&#xff0c;我们学习了如何定义数据&#xff0c;那么在这一章节里面&#xff0c;我们来学习一下&#xff0c;再说明怎么把这些数据显示出来 ;下列就是显示出这些数据的示例程序&#xff0c;可以直接…

Flutter

项目文件目录结构介绍 注&#xff1a;创建 Flutter 项目名称不要包含特殊字符&#xff0c;不要使用驼峰标识 // TODO 开发中运行一个 Flutter 三种启动方式&#xff1a; Run 冷启动从零开始启动Hot Reload 热重载执行 build 方法Hot Restart 热重启重新运行整个 APP 代码分析…

【基础架构篇十一】《DeepSeek日志体系:ELK+Prometheus监控方案》

各位被日志淹没的工程师们,是否经历过这些抓狂时刻?——凌晨三点被报警短信吵醒,打开系统却看到: 日志文件以每秒100MB的速度疯狂膨胀关键报错信息在10TB日志里玩捉迷藏监控图表像心电图一样上蹿下跳服务器硬盘在报警声中发出垂死呻吟今天我们不聊什么基础的日志收集,直接…

JavaEE -JDBC池化思想 与 IDEA导包

1.JDBC概述 1.JDBC 的概述 * Java DataBase Connectivity Java数据库的连接。 * 目的使用 Java 的代码来操作数据库 * 需要使用 JDBC &#xff08; Java 数据库的连接&#xff09;规范来操作数据。 2.JDBC 的规范 * JDBC是一套接口规范 * JDBC的实现类都是由各个数据库的…

Pycharm打开的jupyter notebook无法在pycharm中关闭怎么解决

首先你可以先看一下你的pycharm的jupyter界面的输出&#xff1a; 可以看到第一行有个启动命令 找到这个–port的端口号&#xff0c;现在我们可以走下面的步骤&#xff0c;假设你找到的是–port47187 &#xff1a; 步骤 1&#xff1a;定位占用端口的进程&#xff08;Linux/Mac…

电磁铁的磁芯材质

电磁铁的磁芯通常采用软铁材质&#xff0c;因其具有高磁导率和低矫顽力&#xff0c;使得电磁铁能够在通电时迅速产生强磁场&#xff0c;断电后磁场又能迅速消失。 一、电磁铁与磁芯材质 电磁铁是一种利用电流产生磁场的装置。其核心部件——磁芯&#xff0c;对电磁铁的性能有着…

网络安全等级保护测评(等保测评):全面指南与准备要点

等保测评&#xff0c;全称为“网络安全等级保护测评”&#xff0c;是根据《网络安全法》及《网络安全等级保护条例》等法律法规&#xff0c;对信息系统进行安全等级划分&#xff0c;并依据不同等级的安全保护要求&#xff0c;采用科学方法和技术手段&#xff0c;全面评估信息系…

24蓝桥省赛B-数字接龙

#include<bits/stdc.h> using namespace std; const int N13; int mp[N][N],flag,n,k; bool vis[N][N]; int f[N][N][N][N];//存储路径,用于判断是否斜着走,是本题剪枝的难点 vector<int>ans; vector<int>res; int dx[]{-1,-1,0,1,1,1,0,-1}; int dy[]{0,1,1…

基于豆瓣2025电影数据可视化分析系统的设计与实现

✔️本项目旨在通过对豆瓣电影数据进行综合分析与可视化展示&#xff0c;构建一个基于Python的大数据可视化系统。通过数据爬取收集、清洗、分析豆瓣电影数据&#xff0c;我们提供了一个全面的电影信息平台&#xff0c;为用户提供深入了解电影产业趋势、影片评价与演员表现的工…

React实现自动滚动表格

在 React 中实现一个自动滚动的表格&#xff0c;可以通过 CSS 动画和 JavaScript 定时器来实现。以下是一个完整的示例代码&#xff0c;包含示例数据和自动滚动功能。 实现思路&#xff1a; ** 自动滚动&#xff1a;** 使用 setInterval 实现表格的自动滚动。 手动滚动&…

2024年GESP09月认证Scratch一级试卷

2024年GESP09月认证Scratch一级试卷分数&#xff1a;100 题数&#xff1a;17 一、单选题(共10题&#xff0c;每题3分&#xff0c;共30分) 01020304050607080910AACBCABCDD 1、据有关资料&#xff0c;山东大学于1972年研制成功DJL-1计算机&#xff0c;并于1973年投入运行&…

Qt常用控件之按钮QPushButton

按钮QPushButton QPushButton 在 Qt 中用于表示一个按钮控件&#xff0c;它继承自抽象 QAbstractButton 类。 QPushButton属性 属性说明text按钮中的文本。icon按钮中的图标。iconSize按钮中图标的大小。shortCut按钮对应的快捷键。autoRepeat按钮是否会重复触发&#xff08…

【PHP】php+mysql 活动信息管理系统(源码+论文+数据库+数据库文件)【独一无二】

&#x1f449;博__主&#x1f448;&#xff1a;米码收割机 &#x1f449;技__能&#x1f448;&#xff1a;C/Python语言 &#x1f449;专__注&#x1f448;&#xff1a;专注主流机器人、人工智能等相关领域的开发、测试技术。 【PHP】php 活动信息管理系统&#xff08;源码论文…

搭建一个 Spring Boot 项目,解决jdk与springboot版本不匹配

搭建一个 Spring Boot 项目 方式一&#xff1a;使用 Spring Initializr Spring Initializr 是一个基于 Web 的工具&#xff0c;用于快速生成 Spring Boot 项目的基础结构。 访问 Spring Initializr 网站&#xff1a;https://start.spring.io/配置项目信息&#xff1a; …

基于SpringBoot的小区运动中心预约管理系统

作者&#xff1a;计算机学姐 开发技术&#xff1a;SpringBoot、SSM、Vue、MySQL、JSP、ElementUI、Python、小程序等&#xff0c;“文末源码”。 专栏推荐&#xff1a;前后端分离项目源码、SpringBoot项目源码、Vue项目源码、SSM项目源码、微信小程序源码 精品专栏&#xff1a;…

WPF快速创建DeepSeek本地自己的客户端-基础思路版本

开发工具&#xff1a;VS 2015 开发环境&#xff1a;.Net 4.0 使用技术&#xff1a;WPF 本篇文章内容&#xff1a; 本地部署DeepSeek以后一般使用网页工具&#xff08;如Chatbox&#xff09;或者DOS窗口与其对话。本篇文章使用WPF创建一个基础版的对话工具。 一、搭建本地DeepS…

【怎么使用Redis实现一个延时队列?】

怎么使用Redis实现一个延时队列? 详细说明Java代码示例解释注意事项使用Redis实现延时队列通常通过有序集合(Sorted Set)来实现,利用Redis的ZSET类型及其相关命令可以很方便地实现这一功能。 有序集合中的每个元素都有一个分数(score),我们可以利用这个分数来存储消息需…

Blackbox.AI:高效智能的生产力工具新选择

前言 在当今数字化时代&#xff0c;一款高效、智能且功能全面的工具对于开发者、设计师以及全栈工程师来说至关重要。Blackbox.AI凭借其独特的产品特点&#xff0c;在众多生产力工具中脱颖而出&#xff0c;成为了我近期测评的焦点。以下是我对Blackbox.AI的详细测评&#xff0…

第2章 信息技术发展(一)

2.1 信息技术及其发展 2.1.1 计算机软硬件 计算机硬件(Computer Hardware)是指计算机系统中由电子、机械和光电元件等组成的各种物理装置的总称。 计算机软件 (Computer Software)是指计算机系统中的程序及其文档&#xff0c;程序是计算任务的处理对象和处理规则的描述; 文档…

AI工作流

AI 工作流 是什么&#xff1f; AI 工作流 是一种利用人工智能技术设计的一系列任务或步骤序列&#xff0c;用于完成特定目标的过程。它将一系列AI相关的操作整合在一起&#xff0c;形成一个高效的、结构化的流程&#xff0c;从而实现预定的目标。 AI 工作流 的组成部分 目标定…