Linux4.9、进程地址空间

简介: Linux4.9、进程地址空间

个人主页:Lei宝啊

愿所有美好如期而遇


我们先看一张图

你可能只是见过这个图,或者听过,但是验证过吗?或者说真正了解过这个图吗?我们通过代码来验证一下。(代码贴在后面)

#include <stdlib.h>
    3 
    4 int g_val;
    5 int g_lav = 1;
    6 
    7 int main(int argc,char* argv[],char* env[])
    8 {
    9 
   10     printf("main: %p\n",main);
   11     printf("g_val:%p\n",&g_val);
   12     printf("g_lav:%p\n",&g_lav);
   13     printf("--------------------\n");
   14     int *heap1 = (int*)malloc(4);
   15     int *heap2 = (int*)malloc(4);
   16     int *heap3 = (int*)malloc(4);
   17     int *heap4 = (int*)malloc(4);
   18     printf("heap1: %p\n",heap1);
   19     printf("heap2: %p\n",heap2);
   20     printf("heap3: %p\n",heap3);
   21     printf("heap4: %p\n",heap4);
   22     printf("--------------------\n");
   23     printf("stack1: %p\n",&heap1);
   24     printf("stack2: %p\n",&heap2);
   25     printf("stack3: %p\n",&heap3);
   26     printf("stack4: %p\n",&heap4);
   27     printf("--------------------\n");
   28     printf("&argv[0]:%p\n",argv);
   29     printf("&argv[1]:%p\n",argv+1);
   30     printf("&env[0]:%p\n",env);
   31     printf("&env[1]:%p\n",env+1);
   32     printf("------\n");
   33     printf("argv[0]:%p\n",argv[0]);
   34     printf("argv[1]:%p\n",argv[1]);
   35     printf("env[0]:%p\n",env[0]);
   36     printf("env[1]:%p\n",env[1]);                                                                                                                    
   37 
   38 
   39     return 0;
   40 }

通过我们的验证,我们发现确实如同我们的图上一般。

接下来我们再来看一段代码。

关于下图我们觉得很正常,但是我们再将代码稍作修改

我们让子进程对全局变量g_val进行修改,子进程先跑完,所以子进程先修改,之后父进程再读取。

我们发现父子进程中输出的全局变量地址相同,但是值不一样,我们可以得出以下结论

  • 变量内容不一样,所以父子进程输出的变量绝对不是同一个变量。
  • 地址值相同,但是,我们可以肯定的是,该地址绝对不是物理地址,因为他们的值不一样。
  • 在Linux下,这种地址叫做虚拟地址或者说是线性地址。
  • 我们用C/C++写的代码所看到的地址,都是虚拟地址,物理地址,用户统一看不到,由操作系统统一管理。

也就是说操作系统必须将虚拟地址转换为物理地址。

我们说这么多东西想说明什么呢?就是想说明其实我们一开始看到的那张图就是进程地址空间,我们先看图说结论,再分开详细讲解:

我们的进程地址空间也是task_struct里的一个结构体,初始化数据会在进程地址空间申请空间,并返回虚拟地址,当我们向这个变量做写入时,如果他的虚拟地址及操作合法,那么操作系统就会先停止写入,在物理地址空间申请内存,然后在页表中构建虚拟地址到物理地址的映射,之后再做写入。

那这和我们的问题又有什么关联?我们接着看图和解释:

子进程会继承拷贝父进程task_struct里的大部分内容,其中就包括父进程的进程地址空间,同时子进程也会有一个页表,同样会将父进程的页表内容拷贝一份,所以我们才说他们的数据是共享的,但是我们说进程是相互独立的,如果父子进程要对变量内容做修改,一定不能影响到其他进程,所以就会有写时拷贝,这里当子进程做写入,操作系统会在物理地址空间再申请一块空间,将原来空间的数据做拷贝,然后在页表修改映射关系,物理地址就和父进程不一样了,但是虚拟地址不做修改。

所以我们也就能解释为什么fork返回两个值,却使用一个变量接收,因为变量名在编译后其实就变成了地址,而我们能够看到的地址全部都是虚拟地址,所以其实fork返回的两个值他们最终的物理地址是不同的。

那么问题来了,什么是进程地址空间?

我们说每一个进程都有自己的进程地址空间,在32位机器下,进程地址空间可以被操作系统分配到的空间大小范围是[0 , 4G],我们之前也提到过,他其实是task_struct里的一个数据结构,我们看图:

也就是说,进程地址空间划分了区域的范围,并没有保存数据的能力,申请的空间如果在合法的区域范围内,操作系统就会将进程地址空间上的虚拟地址转化到物理内存中,于是我们也就有了页表。

第二个问题,为什么要有进程地址空间+页表?

根据上图,我们其实可以知道,虚拟地址可以映射到物理内存上,那么数据和代码从磁盘上加载进内存时虚拟地址是可以映射他们的物理地址的,也就是说,申请的物理地址其实不需要连续,我们的虚拟地址是可以保证连续的,所以,也就将物理地址从无序变有序,让进程以统一的视角去看待内存。

再一个,进程管理和内存管理的关系其实就不是很紧密了,当进程需要虚拟地址映射到物理地址时,操作系统去内存中申请就好,进程也不会管他是否连续,进程只需要知道他申请好内存了,然后在页表中映射,接着访问内存,进程管理只需要将进程管理好,内存管理只需要去申请内存,实现了两者间很好的解耦合。

还有一个原因就是内存的安全问题,当进程的操作不合法,例如非法访问或者申请的虚拟地址不合法,那么在页表那里操作系统就会拦截,严重点的进程直接就被操作系统kill掉,而我们的内存不受任何影响,只有合法的操作才会影响到内存,这就保证了内存的安全问题,所以进程地址空间加上页表是保护内存安全的重要手段。

最后,我们解释几个问题

前面我们有提过缺页中断,我们再来理一遍思路,当我们的进程有一个新的变量,那么就会去进程虚拟空间上申请内存,但是不是说你申请,操作系统立刻就去内存上申请,操作系统要保证效率和资源使用率,他并不清楚用户定义的变量是否要使用,或者什么时候使用,如果你定义我就开空间,那么效率显然不高,所以当我们真正要给这个变量写入数据时,而且操作合法,由于没有申请内存,那么操作系统此时会中断写入操作,先去内存中申请物理空间,然后在页表上构建映射关系,然后恢复写入操作,我们将上述流程称为缺页中断。

所以我们的new和malloc也是同理,不是说你申请空间,操作系统立刻就会去内存上申请,而是在使用时才去申请,这样就保证了内存的使用率,不会让内存申请了而不去使用。

我们最后要提及的就是写时拷贝,页表其实有很多选项,只不过我们只列出来两个而已,这里我们再说一个,就是权限,只读,只写,还是读写,或者可执行,父进程在创建子进程后,会将数据段的数据权限全部改为只读,而代码段我们本身就是只读,不解释。

那么为什么要改为只读呢?这样在写入数据时,因为没有写权限,是不是会报错?其实这是故意设计成这样的,就是因为通过这样的冲突让操作系统知道出现这个问题时就需要进行写时拷贝了,然后操作系统进行解决。那么为什么申请了空间还要拷贝?我既然要有一块新的空间,旧的数据我为什么还要?如果说我要对旧的数据进行自增或者说位运算呢?没有旧的数据恐怕不行,所以要在开辟新空间后还要拷贝旧的数据。

至此,我们对进程地址空间的讲解就结束了。

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