【Linux】进程虚拟地址空间

简介: 进程虚拟地址空间

1. 引入

在C/C++中,多次画过这幅程序地址空间布局图 ——

<img src=" title="">

那么这是内存吗?事实上它根本就不是内存!!!是不是颠覆了世界观?!那它是什么呢?

我们先来看一段程序。定义了一个全局变量,在3s时,父或子进程更改数据——

<img src=" title="">
我们惊奇的发现,同一个地址,居然打出了不同的变量 ——

<img src=" title="">

众所周知,在fork创建子进程时,父子默认情况共享数据,修改时,为了维护进程独立性,发生写时拷贝,这是能够理解的。但地址怎么能没有变化呢?

如果C/C++中打印出来的是物理内存的地址,这种现象绝对不可能存在!这说明,我们在语言中所使用的地址,绝对不是物理地址,而是虚拟地址

2. 进程地址空间

所以之前所说的“程序地址空间”是不准确的,准确的说是“进程地址空间”,那么什么是进程地址空间呢?

2.1 what?

每个进程都有一个地址空间,操作系统为每一个进程画了一个大饼,它们都认为自己在独占物理内存。 (至于为什么画大饼,暂时理解为便于统一规划,后文详谈)

系统中存在大量进程,需要管理地址空间,那么就需要先描述、再组织。

地址空间本质上在内核中是一个数据类型 ,可以定义具体的进程地址空间变量——

struct mm_struct
{
    //进程地址空间
}

<img src=" title="">

那么我们是如何用struct结构体进行区域划分的?各个区域又是如何与物理内存建立关联的?

2.2 how?

⛄️ 虚拟地址空间 & 分页

我们将实体物理内存抽象出一把尺子,上面的刻度相当于虚拟地址(地址空间进行区域划分时,对应的线性位置虚拟地址)

<img src=" title="">

每个进程都认为自己拥有4GB,都认为空间的划分是按照4GB来划分的。虽然这里只有start和end,但这是一个区间概念,每个进程都认为mm_struct代表的是从0x00000000到0xffffffff整个内存。

:heart: 那么如何将虚拟地址和物理地址建立映射关系呢?通过查页表(页表+MMU硬件设备)

<img src=" title="">

2.3 why?

:heart: 1. 通过添加一层软件层,完成有效的对进程操作内存的风险管理(权限管理),本质是为了保护物理内存各个进程的数据安全

类似于过年的压岁钱妈妈帮你收着,等你要用的时候,再来问我要,防止你乱花钱。对应到这里,中间层是有利于操作系统管理的,不是不给你,而是管控你的做法是否合适;如果没有中间层(OS),能直接访问物理地址,可能发生非法越界访问。

在语言层面上,我们知道,字符串存在字符常量区不能修改 ——

char* str = "more than words";
*str = 'b'; //不能修改

本质上是因为,这里str指针就是虚拟地址,*解引用进行写入时,访问虚拟地址,要进行虚拟地址和物理地址的转化,然而OS只给你读r权限,直接把你的进程崩溃掉。

<img src=" title="">

:heart: 2. 将内存申请和内存使用在时间上解耦。通过虚拟地址空间,来屏蔽底层申请内存的过程,达到进程读写内存操作和OS进行内存管理进行软件层面上的分离。

比如我们在堆上申请一大块空间,但是我们可能暂时不会全部使用甚至暂时不用(有了空间,从来没有读写),在OS角度,这部分空间本来是可以给别人立马用的,却被闲置着。于是,OS在当你真的要使用时,再把空间开辟出来,建立映射关系,这叫做基于缺页中断进行物理内存申请。

再比如假如物理内存已经100%占满了,而你还要,那么OS执行内存管理算法,把某些进程闲置的空间置换到磁盘上,这样进程照样可以申请到内存。而这些都是我们用户在应用层根本感受不到,换句话说OS做的内存操作是透明的。

:heart: 3. 站在CPU和应用层的角度,进程统一使用4GB的空间,且每个空间区域的相对位置是比较确定的。

比如CPU寻找不同进程代码的第一行,如果直接访问物理内存,CPU会比较凌乱。有了虚拟地址空间,CPU能以统一的视角看待物理内存,不同的进程再通过的各自的页表,映射到不同的物理内存。同时,程序的代码和数据可以加载到内存的任意位置,大大减少内存管理的负担。

进一步谈进程和程序有什么区别?进程要包括描述进程的PCB、进程虚拟地址空间、页表、代码和数据。

3. 再次理解

3.1 又回到最初的起点

:yellow_heart: 我们再次回到文章开头的问题,为什么相同的地址会打印出不同的值?

众所周知,子进程的创建是以父进程为模板的 ——
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-B6WUeykz-1648474228302)(C:\Users\13136\AppData\Roaming\Typora\typora-user-images\image-20220328194135912.png)]

为了维护进程的独立性,子进程在更改时发生写时拷贝,即为子进程重新开辟一段物理空间,把值拷贝过来,再重新建立虚拟地址到物理地址的映射关系 ——
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-lapxZbAc-1648474228304)(C:\Users\13136\AppData\Roaming\Typora\typora-user-images\image-20220328195553972.png)]

所以打印的是一样的虚拟地址,而不同的值,是因为在物理内存上本来就是不同的变量。

之前说的,父子进程的代码一般是共享的,也就是通过映射到同一段物理空间实现的。

之前说的,所有的只读数据一般可以只有一份,本质不是在语言上,而是在系统上,这样操作系统的维护成本是最低的,不同的虚拟地址映射到相同的物理地址上 ——

// 打印str和p的地址相同 
char* str = "more than words";
char* p = "more than words";

3.2 验证进程地址空间

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

int g_unval;
int g_val = 100;
  
int main(int argc,char* argv[],char* env[])
{
    const char* str = "more than words";
    printf("code addr:%p\n",main);
    printf("string rdonly addr:%p\n",str);
    printf("init addr:%p\n",&g_val);
    printf("uninit addr:%p\n",&g_unval);

    char* heap1 = (char*)malloc(10);
    char* heap2 = (char*)malloc(10);                                                                                                               
    char* heap3 = (char*)malloc(10);    
    char* heap4 = (char*)malloc(10);    
    printf("heap addr:%p\n",heap1);    
    printf("heap addr:%p\n",heap2);    
    printf("heap addr:%p\n",heap3);    
    printf("heap addr:%p\n",heap4);    

    int a = 10;                    
    int b = 20;                    
    printf("stack addr:%p\n",&a);    
    printf("stack addr:%p\n",&b);    

    for(int i = 0; argv[i]; i++)    
    {                              
      printf("argv[%d]:%p\n", i, argv[i]);    
    }
    
      for(int i = 0; env[i]; i++)
    {
      printf("env[%d]:%p\n", i, env[i]);
    }
    return 0;
  }

可以看到,地址空间变化完全吻合。其中堆向上生长,栈向下生长,堆栈相对而生,且空间很大 ——
<img src=" title="">
相同的程序,每次运行,main函数的地址都一样 ——

<img src=" title="">

系统传的参数,在栈的上面,再完善一下 ——
<img src=" title="">
持续更新@边通书

相关文章
|
1月前
|
资源调度 Linux 调度
Linux c/c++之进程基础
这篇文章主要介绍了Linux下C/C++进程的基本概念、组成、模式、运行和状态,以及如何使用系统调用创建和管理进程。
37 0
|
3月前
|
网络协议 Linux
Linux查看端口监听情况,以及Linux查看某个端口对应的进程号和程序
Linux查看端口监听情况,以及Linux查看某个端口对应的进程号和程序
659 2
|
19天前
|
缓存 监控 Linux
linux进程管理万字详解!!!
本文档介绍了Linux系统中进程管理、系统负载监控、内存监控和磁盘监控的基本概念和常用命令。主要内容包括: 1. **进程管理**: - **进程介绍**:程序与进程的关系、进程的生命周期、查看进程号和父进程号的方法。 - **进程监控命令**:`ps`、`pstree`、`pidof`、`top`、`htop`、`lsof`等命令的使用方法和案例。 - **进程管理命令**:控制信号、`kill`、`pkill`、`killall`、前台和后台运行、`screen`、`nohup`等命令的使用方法和案例。
53 4
linux进程管理万字详解!!!
|
9天前
|
存储 运维 监控
深入Linux基础:文件系统与进程管理详解
深入Linux基础:文件系统与进程管理详解
48 8
|
7天前
|
Linux
如何在 Linux 系统中查看进程占用的内存?
如何在 Linux 系统中查看进程占用的内存?
|
18天前
|
算法 Linux 定位技术
Linux内核中的进程调度算法解析####
【10月更文挑战第29天】 本文深入剖析了Linux操作系统的心脏——内核中至关重要的组成部分之一,即进程调度机制。不同于传统的摘要概述,我们将通过一段引人入胜的故事线来揭开进程调度算法的神秘面纱,展现其背后的精妙设计与复杂逻辑,让读者仿佛跟随一位虚拟的“进程侦探”,一步步探索Linux如何高效、公平地管理众多进程,确保系统资源的最优分配与利用。 ####
52 4
|
19天前
|
缓存 负载均衡 算法
Linux内核中的进程调度算法解析####
本文深入探讨了Linux操作系统核心组件之一——进程调度器,着重分析了其采用的CFS(完全公平调度器)算法。不同于传统摘要对研究背景、方法、结果和结论的概述,本文摘要将直接揭示CFS算法的核心优势及其在现代多核处理器环境下如何实现高效、公平的资源分配,同时简要提及该算法如何优化系统响应时间和吞吐量,为读者快速构建对Linux进程调度机制的认知框架。 ####
|
21天前
|
消息中间件 存储 Linux
|
27天前
|
运维 Linux
Linux查找占用的端口,并杀死进程的简单方法
通过上述步骤和命令,您能够迅速识别并根据实际情况管理Linux系统中占用特定端口的进程。为了获得更全面的服务器管理技巧和解决方案,提供了丰富的资源和专业服务,是您提升运维技能的理想选择。
34 1
|
1月前
|
算法 Linux 调度
深入理解Linux操作系统的进程管理
【10月更文挑战第9天】本文将深入浅出地介绍Linux系统中的进程管理机制,包括进程的概念、状态、调度以及如何在Linux环境下进行进程控制。我们将通过直观的语言和生动的比喻,让读者轻松掌握这一核心概念。文章不仅适合初学者构建基础,也能帮助有经验的用户加深对进程管理的理解。
25 1