首页 > 系统相关 >Linux系统 —— 进程系列 - 程序地址空间:虚拟地址空间

Linux系统 —— 进程系列 - 程序地址空间:虚拟地址空间

时间:2024-12-17 18:55:51浏览次数:11  
标签:struct mm 虚拟地址 vm 进程 内存 Linux 空间

接前文:

  

Linux系统 —— 进程系列 - 进程优先级与进程切换-CSDN博客icon-default.png?t=O83Ahttps://blog.csdn.net/hedhjd/article/details/144404639?spm=1001.2014.3001.5502

目录

前言

1. 虚拟地址空间和进程地址空间

1.1 什么是虚拟地址空间?

 结论

1.2 虚拟地址空间的结构体里有哪些属性(如何实现)

1.3 虚拟内存管理

2. 为什么要有虚拟地址空间

问题一:安全风险

问题二:地址不确定

问题三:效率低下


前言

我们都知道,我们的内存空间划分为几个区域,我们在调试代码窗口的时候, 看到的地址其实并不是真实内存的地址,而是虚拟地址空间

  

虚拟地址空间并不是物理内存,两者之间有一点关系,但是不多

  

一个进程有一个虚拟地址空间,我们前面学习的时候只知道创建一个进程就需要有一个对应的task_struct来描述对应的进程,而每一个task_struct都要对应一个虚拟地址空间

在我们的操作系统里面,一个进程会构建一个页表,我们页表左侧存储的是我们的虚拟地址,右侧存储的是物理地址

  

页表是用来做虚拟地址到物理地址映射的:所有的数据包括代码本身全部都有地址,所以每一个元素对应的地址都是由每一个虚拟地址加载到物理内存然后经过页面映射找到物理内存

我们都知道,进程有两个进程一个是父进程,另一个是子进程,而我们上面的就是父进程,我们下面这张图片就是子进程,子进程所有数据都是从父进程哪里拷贝下来的(包括:task_struct,页表,数据 ... )

   

而一旦拷贝就意味着在我们的子进程初始化全局数据区里面也会同样存在着全局变量,也叫做g_val的虚拟地址,而这种页表关系就类似于哈希表,当我们拿到了它的虚拟地址再找到了它对应的物理地址,页表就会把父进程的页表拷贝到子进程的页表里面,这种概率就是发生了简单的浅拷贝,其实就相当于它们都指向了同一个物理内存,所以父进程和子进程的代码和数据都是共享的

这个时候我们就有一个问题:子进程要对变量进行修改怎么办?

  

答案是:当子进程要对变量进行修改时,这个时候我们的OS就会介入,OS直接给我们开辟一段新的地址空间,然后把我们之前的内容拷贝进新开辟的空间里,那么这个时候我们就会得到一个新的物理地址,然后OS再重新填写页表,构建新的映射关系

  

那么这个时候,我们对应的同一个虚拟地址并没有变化,但是它的物理内存已经指向了一个新的物理起始空间,这种机制我们称之为:写实拷贝


1. 虚拟地址空间和进程地址空间

1.1 什么是虚拟地址空间?

举个例子:比如,在国外有一个大富翁,非常有钱,有100个亿的资产,但是大富翁的私生活非常混乱,有四个私生子,有一天,这个大富翁找到了他的第一个私生子说:“儿子,只要你好好学习,爸爸的这100个亿就都是你的”,第一个私生子就非常高兴,从此就好好学习,过了一段时间,这个大富翁又找到了他的第二个私生子,也开始给第二个私生子画饼......

  

这里的大富翁就是我们的操作系统OS,100个亿就是物理内存,私生子就是进程,画饼就是虚拟地址空间

这个是我们就有一个问题了:OS要把进程管理起来,那么要不要把饼也管理起来呢?答案是:要        那么我们应该怎么管理饼呢?答案就是:先描述,再组织

  

我们的虚拟地址空间(画饼)本质上其实就是一个数据结构,叫做:mm_struct

  

  

总结:虚拟地址空间其实就是一个在内核当中,在操作系统内部给进程创建结构体对象,这就叫做虚拟地址空间

我们来段代码感受⼀下

#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
int g_val = 0;
int main()
{
	pid_t id = fork();
	if (id < 0) {
		perror("fork");
		return 0;
	}
	else if (id == 0) { //child
		printf("child[%d]: %d : %p\n", getpid(), g_val, &g_val);
	}
	else { //parent
		printf("parent[%d]: %d : %p\n", getpid(), g_val, &g_val);
	}
	sleep(1);
	return 0;
}

 输出:

//与环境相关,观察现象即可
parent[2995]: 0 : 0x80497d8
child[2996] : 0 : 0x80497d8

我们发现,输出出来的变量值和地址是⼀模⼀样的,很好理解呀,因为⼦进程按照⽗进程为模版,父子并没有对变量进⾏进⾏任何修改。可是将代码稍加改动:

#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
int g_val = 0;
int main()
{
	pid_t id = fork();
	if (id < 0) {
		perror("fork");
		return 0;
	}
	else if (id == 0) { //child,⼦进程肯定先跑完,也就是⼦进程先修改,完成之后,⽗进程再
		读取
			g_val = 100;
		printf("child[%d]: %d : %p\n", getpid(), g_val, &g_val);
	}
	else { //parent
		sleep(3);
		printf("parent[%d]: %d : %p\n", getpid(), g_val, &g_val);
	}
	sleep(1);
	return 0;
}

输出结果:

//与环境相关,观察现象即可
child[3046]: 100 : 0x80497e8
parent[3045] : 0 : 0x80497e8

 结论

   

我们发现,父子进程,输出地址是⼀致的,但是变量内容不⼀样!能得出如下结论:

1. 变量内容不⼀样,所以⽗⼦进程输出的变量绝对不是同⼀个变量

    
2. 但地址值是⼀样的,说明,该地址绝对不是物理地址

    
3. 在Linux地址下,这种地址叫做 虚拟地址

   
4. 我们在⽤C/C++语⾔所看到的地址,全部都是虚拟地址!物理地址,⽤⼾⼀概看不到,由OS统⼀管理

       
OS必须负责将 虚拟地址 转化成 物理地址 


1.2 虚拟地址空间的结构体里有哪些属性(如何实现)

我们有一个问题:什么叫做区域划分呢?

   

举个例子:我们正在学校上课,但是有一天我们和同桌吵架了,这个时候我们的同桌画了一条38线,并警告我们不许越界,不然就打我们

  

这个时候,这个38线其实就是我们的区域划分

   

  

结合上图,我们的区域划分在只需要定好区域的开始和结束就可以了,而我们想要使用对应的空间就可以以刻度来访问,所以刻度就是地址,刻度是线性且连续的,地址其实就是整数0~100的数字,所以我们可以所以整数Int类型来保存

   

所以,我们虚拟地址空间的结构体里就包含这些属性:

  

  

结构体里的属性都是每个区域的开始和结束

  

mm_struct:

展开:

 


1.3 虚拟内存管理

  

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

struct task_struct
{
	
	struct mm_struct
		/** mm; 对于普通的⽤⼾进程来说该字段指向他的
	虚拟地址空间的⽤⼾空间部分,对于内核线程来说这部分为NULL*/
		struct mm_struct
		* active_mm; 
	/*该字段是内核线程使⽤的。当该进程是内核线程时,它的mm字段为NULL,
	表⽰没有内存地址空间,可也并不是真正的没有,这是因为所
	有进程关于内核的映射都是⼀样的,内核线程可以使⽤任意进程的地址空间*/

}

可以说,mm_struct结构是对整个⽤⼾空间的描述。每⼀个进程都会有⾃⼰独⽴mm_struct,这样每⼀个进程都会有⾃⼰独⽴的地址空间才能互不⼲扰。先来看看由task_struct到mm_struct,进程的地址空间的分布情况

定位mm_struct⽂件所在位置和task_struct所在路径是⼀样的,不过他们所在⽂件是不⼀样的,mm_struct所在的⽂件是mm_types.h

struct mm_struct
{

	struct vm_area_struct* mmap;
	/* 指向虚拟区间(VMA)链表 */

	struct rb_root mm_rb;
	/* red_black树 */

	unsigned long task_size;
	/*具有该结构体的进程的虚拟地址空间的⼤⼩*/

	// 代码段、数据段、堆栈段、参数段及环境段的起始和结束地址。
	unsigned long start_code, end_code, start_data, end_data;
	unsigned long start_brk, brk, start_stack;
	unsigned long arg_start, arg_end, env_start, env_end;
}

那既然每⼀个进程都会有⾃⼰独⽴的mm_struct,操作系统肯定是要将这么多进程的mm_struct组织起来的!虚拟空间的组织⽅式有两种:

    
                                1. 当虚拟区较少时采取单链表,由mmap指针指向这个链表

     
                                2. 当虚拟区间多时采取红⿊树进⾏管理,由mm_rb指向这棵树

linux内核使⽤ vm_area_struct 结构来表⽰⼀个独⽴的虚拟内存区域(VMA),由于每个不同质的虚拟内存区域功能和内部机制都不同,因此⼀个进程使⽤多个vm_area_struct结构来分别表⽰不同类型的虚拟内存区域。上⾯提到的两种组织⽅式使⽤的就是vm_area_struct结构来连接各个VMA,⽅便进程快速访问

struct vm_area_struct {
	unsigned long vm_start; //虚存区起始
	unsigned long vm_end;
	//虚存区结束
	struct vm_area_struct* vm_next, * vm_prev;
	//前后指针
	struct rb_node vm_rb;
	//红⿊树中的位置
	unsigned long rb_subtree_gap;
	struct mm_struct* vm_mm;
	//所属的 mm_struct
	pgprot_t vm_page_prot;
	unsigned long vm_flags;
	//标志位
	struct {
		struct rb_node rb;
		unsigned long rb_subtree_last;
	} shared;
	struct list_head anon_vma_chain;
	struct anon_vma* anon_vma;
	const struct vm_operations_struct* vm_ops; //vma对应的实际操作
	unsigned long vm_pgoff;
	//⽂件映射偏移量
	struct file* vm_file;
	//映射的⽂件
	void* vm_private_data;
	//私有数据
	atomic_long_t swap_readahead_info;
#ifndef CONFIG_MMU
	struct vm_region* vm_region;
	/* NOMMU mapping region */
#endif
#ifdef CONFIG_NUMA
	struct mempolicy* vm_policy;
	/* NUMA policy for the VMA */
#endif
	struct vm_userfaultfd_ctx vm_userfaultfd_ctx;
} __randomize_layout;

化为图片就是:


2. 为什么要有虚拟地址空间

也可以说是:如果程序直接可以操作物理内存会造成什么问题?

在早期的计算机中,要运⾏⼀个程序,会把这些程序全都装⼊内存,程序都是直接运⾏在内存上的,也就是说程序中访问的内存地址都是实际的物理内存地址。当计算机同时运⾏多个程序时,必须保证这些程序⽤到的内存总量要⼩于计算机实际物理内存的⼤⼩

     
那当程序同时运⾏多个程序时,操作系统是如何为这些程序分配内存的呢?例如某台计算机总的内存⼤⼩是128M,现在同时运⾏两个程序A和B,A需占⽤内存10M,B需占⽤内存110。计算机在给程序分配内存时会采取这样的⽅法:先将内存中的前10M分配给程序A,接着再从内存中剩余的118M中划分出110M分配给程序B

这种分配⽅法可以保证程序A和程序B都能运⾏,但是这种简单的内存分配策略问题很多

问题一:安全风险

    
每个进程都可以访问任意的内存空间,这也就意味着任意⼀个进程都能够去读写系统相关内
存区域,如果是⼀个⽊⻢病毒,那么他就能随意的修改内存空间,让设备直接瘫痪

问题二:地址不确定

    
众所周知,编译完成后的程序是存放在硬盘上的,当运⾏的时候,需要将程序搬到内存当中
去运⾏,如果直接使⽤物理地址的话,我们⽆法确定内存现在使⽤到哪⾥了,也就是说拷⻉
的实际内存地址每⼀次运⾏都是不确定的,⽐如:第⼀次执⾏a.out时候,内存当中⼀个进程
都没有运⾏,所以搬移到内存地址是0x00000000,但是第⼆次的时候,内存已经有10个进程在运⾏了,那执⾏a.out的时候,内存地址就不⼀定了

问题三:效率低下


如果直接使⽤物理内存的话,⼀个进程就是作为⼀个整体(内存块)操作的,如果出现物理
内存不够⽤的时候,我们⼀般的办法是将不常⽤的进程拷⻉到磁盘的交换分区中,好腾出内
存,但是如果是物理地址的话,就需要将整个进程⼀起拷⾛,这样,在内存和磁盘之间拷⻉
时间太⻓,效率较低

那么,我们有没有解决方法呢?答案是有的

1. 地址空间和⻚表是OS创建并维护的!是不是也就意味着,凡是想使⽤地址空间和⻚表进⾏映射,也⼀定要在OS的监管之下来进⾏访问!!也顺便保护了物理内存中的所有的合法数据
,包括各个进程以及内核的相关有效数据

   

2. 因为有地址空间的存在和⻚表的映射的存在,我们的物理内存中可以对未来的数据进⾏任意位置的加载!物理内存的分配 和 进程的管理就可以做到没有关系,进程管理模块和内存管理模块就完成了解耦合
    
因为有地址空间的存在,所以我们在C、C++语⾔上new, malloc空间的时候,其实是在地址
空间上申请的,物理内存可以甚⾄⼀个字节都不给你。⽽当你真正进⾏对物理地址空间访问
的时候,才执⾏内存的相关管理算法,帮你申请内存,构建⻚表映射关系(延迟分配),这
是由操作系统⾃动完成,⽤⼾包括进程完全0感知

   

3. 因为⻚表的映射的存在,程序在物理内存中理论上就可以任意位置加载。它可以将地址空间上的虚拟地址和物理地址进⾏映射,在进程视⻆所有的内存分布都可以是有序的


进程概念系列就先到此为止啦~

 

标签:struct,mm,虚拟地址,vm,进程,内存,Linux,空间
From: https://blog.csdn.net/hedhjd/article/details/144448894

相关文章

  • Linux应急响应流程
    备份用户信息文件cat/etc/passwd>passwd.txt;cat/etc/shadow>shadow.txt;备份当前网络信息netstat-anp>netstat_anp.txt; 备份历史命令cp~/.bash_historyhistory.txt;history>history.txt1;备份用户登录的信息w>users.txt; 备份当前进程信息psaux>ps......
  • Linux文件属性 --- 文件.目录、软硬链接、字符设备文件
    目录 七种文件类型1、普通文件和目录 2、链接文件 2.1硬链接 2.2软链接  3、字符设备文件 七种文件类型 Linux的文件属性中一共有以下七种类型 :符号类型含义解释-普通文件纯文本文件(ASCII)和二进制文件(binary)d目录类似于Windows的文件夹l符号链接文件ln–......
  • Linux环境下安装MapReduce(以Hadoop MapReduce为例)的详细步骤
    一、前提条件操作系统准备确保你有一个合适的Linux发行版,如Ubuntu、CentOS等。以CentOS为例,系统应该是比较新的版本,并且已经完成了基本的系统更新。安装好Java运行环境(JDK),因为Hadoop是基于Java开发的。你可以通过以下命令检查Java是否安装:java-version。如果没有安装,在CentO......
  • Linux系统下安装分布式数据库HBase的详细步骤
    一、前提条件Java环境安装HBase是基于Java开发的,所以需要先安装JavaDevelopmentKit(JDK)。可以从Oracle官方网站(https://www.oracle.com/java/technologies/javase-downloads.html)下载适合你系统的JDK版本。安装完成后,需要配置Java环境变量。例如,在Ubuntu系统中,编辑/etc/p......
  • Linux系统下安装Hive的详细步骤
    一、前提条件确保已经安装了Java运行环境(JDK)检查Java是否安装:在终端中输入java-version。如果已经安装,会显示Java的版本信息。如果没有安装,可以从Oracle官方网站下载适合您系统的JDK版本进行安装。安装并配置好Hadoop集群Hive依赖于Hadoop,因为它的数据存储主要基于Hadoo......
  • 在Linux系统下安装Solr的详细步骤
    一、安装Java环境(Solr是基于Java开发的,需要Java运行环境)检查系统是否已安装Java打开终端,输入命令java-version。如果已经安装,会显示Java的版本信息。如果没有安装,需要进行安装。安装OpenJDK(以Ubuntu为例)运行命令sudoapt-yupdate更新软件包列表。安装OpenJDK11(Solr......
  • Windows和Linux系统下安装Oracle数据库的详细步骤
    在Windows系统下安装Oracle数据库的一般步骤:一、系统要求检查硬件要求:确保服务器或计算机有足够的内存。对于小型测试环境,建议至少2GB内存;生产环境可能需要更多,如16GB或更高,这取决于数据库的负载和预期用途。足够的磁盘空间。Oracle软件本身可能需要数GB的空间,并且还需要为......
  • Linux系统中安装HDFS(Hadoop分布式文件系统)的详细步骤
    一、前提条件安装好Linux操作系统(如Ubuntu、CentOS等)。确保系统已经安装了Java运行环境(JDK),因为Hadoop是基于Java开发的。可以通过在终端输入java-version来检查是否安装了JDK。如果没有安装,需要先安装适合您系统的JDK版本,并配置好环境变量。二、下载Hadoop访问Hadoop官方......
  • Linux系统下安装Yarn(以Hadoop Yarn为例)的详细步骤
    一、前提条件安装JavaYarn是基于Java开发的,需要先安装JavaDevelopmentKit(JDK)。你可以从Oracle官方网站(https://www.oracle.com/java/technologies/javase-jdk11-downloads.html)下载适合你系统的JDK版本。安装完成后,设置JAVA_HOME环境变量。例如,在bash环境下,将以下内容添......
  • imx6ull RTC-S35390A时钟 LINUX增加驱动
    CPU平台:imx6ull软件平台:qt+linux4.1.15驱动部分:在驱动编写中,对S35390A的地址填写为0x30+指令,实际只需要用到0x30、0x31、0x32。(i2c-imx.c中发送和接收时,设备地址,有一个左移一位)1.i2c设备树中增加:rtc:rtc-s35390a@60{ compatible="s35390a"; reg=<0x30>;};compa......