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

news/2025/2/20 16:13:29/

 实验:

  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.ppmy.cn/news/1573334.html

相关文章

postgresql FSM页面怎么组织

PostgreSQL 的空闲空间映射&#xff08;Free Space Map&#xff0c;FSM&#xff09;是用于跟踪堆和索引关系中可用空间的数据结构。FSM 的设计目标是快速定位到有足够空间容纳新元组的页面&#xff0c;或者决定是否需要扩展新的页面。 FSM 页面的组织方式 存储结构 每个堆和索…

什么是AI Agent、Chat、RAG、MoE

什么是AI Agent、Chat、RAG、MoE 目录 什么是AI Agent、Chat、RAG、MoE定义与原理功能特点应用场景AI Agent有哪些关键组成部分感知模块决策模块知识模块行动模块学习模块AI Agent、Chat、RAG、MoE是人工智能领域中不同的概念和技术,它们在功能、原理和应用等方面存在一些区别…

matlab齿轮传动

实现齿轮啮合分析&#xff0c;齿轮传动非线性分析&#xff0c;对扭转振动方程组进行求解&#xff0c;可得到齿轮扭转角随时间变化相关参数 列表 齿轮传动非线性分析&#xff0c;对扭转振动方程组进行求解&#xff0c;可得到齿轮扭转角随时间变化相关参数/niu_gou_yuan_Rg.m , …

人工智能(AI)在癌症休眠研究及精准肿瘤学中的应用|顶刊速递·25-02-18

小罗碎碎念 推文速览 癌症休眠是导致癌症复发转移的关键因素&#xff0c;当前治疗策略对其效果不佳&#xff0c;因此深入探究癌症休眠机制并开发针对性疗法至关重要。 文章首先阐述癌症休眠的基本概念&#xff0c;包括肿瘤块休眠和细胞休眠两种类型&#xff0c;详细介绍癌细胞…

WebSocket 小白快速入门(2025)

目录 一、websocket 背景和特性 二、websocket 和 ajax区别是什么 &#xff1f; 三、传统方案和 websocket 方案对比 服务端推送web方案 1.短轮询 2.长轮询 3.Websocket长连接 四、websocket 代码实现方案 1.tomcat实现websocket 2.netty实现websocket 为啥选netty…

基于单片机ht7038 demo

单片机与ht7038 demo&#xff0c;三相电能表&#xff0c;电量数据包括电流电压功能&#xff0c;采用免校准方法 列表 ht7038模块/CORE/core_cm3.c , 17273 ht7038模块/CORE/core_cm3.h , 85714 ht7038模块/CORE/startup_stm32f10x_hd.s , 15503 ht7038模块/CORE/startup_stm32…

K8s:kubernetes.io~csi 目录介绍

目录标题 **1. 进入 CSI 相关目录****2. PVC 相关目录操作****3. 挂载点相关操作****4. CSI PVC 的使用流程****5. 总结** 在 Kubernetes&#xff08;K8s&#xff09;中&#xff0c;容器存储接口&#xff08;CSI&#xff09; 是一种标准&#xff0c;用于将存储系统暴露给 K8s 中…

Natural Language Processing NLP

NLP 清晰版本查看 Sentence segmentation (split)Tokenisation (split)Named entity recognition (combine) 概念主要內容典型方法Distributional Semantics&#xff08;分佈式語義&#xff09;&#xff08;分銷語義&#xff08;分佈式語義&#xff09;單詞的語義來自於它的…