Linux之进程地址空间

简介: Linux之进程地址空间

前言

内存区域划分:

在学习C/C++时我们都有接触过内存区域划分这个概念,也知道它表示的是程序加载到内存中不同的数据所分布的不同的区域,但是我们并不清楚它是什么东西,在哪里存储着,为什么要有它,它又是怎样实现的。今天我们就来解决这些疑惑。


一、是什么

进程地址空间是什么?

1.例子

我们先来看这样一个现象:

1 #include<stdio.h>
  2 #include<unistd.h>
  3 #include<stdlib.h>
  4 int main()
  5 {
  6         pid_t pid = fork();
  7         int i = 10;
  8         if(pid < 0)//函调用失败
  9         {
 10                 perror("fork");
 11                 exit(1);
 12         }
 13         else if(pid == 0)//子进程
 14         {
 15                 int cnt = 0;
 16                 while(1)
 17                 {
 18                         printf("子进程,pid = %d, ppid = %d, i = %d, &i = %p\n",getpid(),getppid(),i,&i);
 19                         if(cnt == 5)
 20                         {
 21                                 i = 5;
 22                         printf("子进程已更改全局变量……\n");
 23                         }
 24                         cnt++;
 25                         sleep(1);
 26                 }
 27         }
 28         else
 29         {
 30                 //父进程
 31                 while(1)
 32                 {
 33                         printf("父进程,pid = %d, ppid = %d, i = %d, &i = %p\n",getpid(),getppid(),i,&i);
 34                         sleep(2);
 35                 }
 36         }
 37         return 0;
 38 }

我们发现在同一个地址空间的i变量,父进程和子进程访问时获得的值却不是相同的,这是什么原因呢?

首先,我们可以理解,父子进程的值不同是因为进程间具有独立性,但是这里的i的地址居然是相同的!!!我们可以先排除该地址是在物理磁盘上的地址的可能性,因为物理磁盘的同一个地址只能存唯一确定的一个值。

因此,这个地址只能是虚拟地址(线性地址)。在Linux中,特殊情况,我们将这种地址也成为逻辑地址。

2.感性的理解虚拟地址空间

从前有一个大富翁,他有10亿美元的资产。他有三个私生子,这三个私生子都不知道对方的存在,而大富翁也给他们画饼,说自己就他这一个儿子,自己走后,这10个亿都归他所有,因此他们都认为大富翁的10亿美元是被他们独占。当然大富翁也知道,这三个儿子虽然想着所有的钱都是他的,但是不可能一次性向大富翁要所有的钱,每次最多几十万、几百万的向大富翁要,所以大富翁画的饼从来没有被拆穿过。

我们的操作系统就相当于大富翁,而进程则相当于他的私生子,因为进程之间是相互独立的,因此每个进程都认为自己可以独占操作系统的所有资源,当然进程也不会一次性向申请操作系统申请操作系统的所有资源,一次只会申请一小部分资源。

为了给进程画饼(让进程认为自己独占操作系统资源),操作系统为每个进程都创建了独立的地址空间,地址空间的内容通过页表映射到物理内存中这样每个进程都能独立的运行。

3.现象的具体解释

父进程和子进程都有自己独立的进程地址空间,也有独立的页表结构。子进程由父进程创建,因此子进程的进程地址空间是拷贝父进程的进程地址空间。刚开始父子进程并未对进程地址空间做修改,因此i值在一开始指向同一个物理内存。

后来,子进程修改了i的值,操作系统通过页表映射发现i的值是两个进程共享的,操作系统为了保持进程的独立性,当子进程或者父进程任何一方尝试对共享的数据做写入,操作系统就会在物理内存上重新开辟一块新的内存空间拷贝原来的数据,然后修改映射关系,使其指向新的物理地址,再进行写入操作。整个修改的过程中,这些工作与父子进程的虚拟地址没有关系,只有底层经过页表映射到了新的物理地址,因此我们观察到的虚拟地址是相同的,但是内容却不同。

4.写时拷贝

父子进程中的任意一方试图对共享数据进行写入,操作系统就会先将原数据进行拷贝,然后改变要写入一方的页表映射,使它映射到新的物理内存中,然后再让进程进行写入的技术称为写时拷贝

二、为什么

为什么存在进程地址空间?

  1. 保证了数据的安全性
    如果进程出现越界非法访问、非法写入,页表会对进程进行拦截。直接对物理内存进行访问,对于账号信息等数据是不安全的(可能出现:意外损坏数据或者恶意读取用户信息等问题)。
  2. 方便进程之间的数据代码的解耦,保证了进程的独立性
    一个进程对数据的修改不会对另一个进程造成影响,保证了进程的独立性。
  3. 让进程以统一的视角看待进程的代码和数据所在的各个区域,同时方便了编译器以统一视角编译代码
    可执行程序再被编译器编译的时候代码和数据再内存中已经有虚拟地址(在磁盘上的这种地址称为逻辑地址),也就是说操作系统和编译器都是遵守地址空间这一理论的。
    在程序被加载到内存成为进程后,每个变量/函数都具备了物理地址。因此,我们现在有两套地址,一套是用于表示物理内存中代码和数据的物理地址;另一套是用于程序内部函数之间进行跳转的虚拟地址。
    加载完毕后,代码的各个区域的地址,操作系统和编译器都已经知道了。进程被调度时,CPU拿到虚拟地址,经过地址空间的页表的映射,就能查到物理地址,通过物理地址访问到代码,然后执行。
    CPU -> 虚拟地址 -> 页表 -> 物理地址 -> 执行。
    也就是说明,CPU运行的整个过程中,CPU都没有见到物理地址,而是用虚拟地址运行程序。

对于磁盘内编译过的可执行程序中的地址不叫虚拟地址,而是叫做逻辑地址。当然对于Linux而言,虚拟地址、线性地址、逻辑地址都是一样的。

三、怎么办

  1. 操作系统要为每一个进程分配地址空间,那么操作系统是否要管理这些地址空间呢?当然是要管理的。
  2. 那么,操作系统怎么管理进程的地址空间
    说到管理,那就是管理数据,管理的方法是先描述,再组织
    首先,进程本身就是需要被管理的,操作系统管理它的方式是将进程的信息存入结构体PCB(即,task_struct)中,再用链式结构将每一个进程的PCB对象组织起来。
    而地址空间也是需要用内核数据结构mm_struct进行管理,OS会为每一个进程创建一个mm_struct(结构体)对象,进行管理。该结构体对象保存在它所对应进程的PCB中。(PCB中的一个属性mm_struct)
  3. 区域划分和调整
    地址空间有很多区域:栈区、堆区、数据段、代码段等,那么进程地址空间是如何进行区域划分的呢?
    举个简单的小栗子:

    上小学的小蓝和小粉是同桌,小粉并不想和小蓝一起玩,因此将桌子上用一条“三八线”划分为了两个区域,左边属于小蓝,右边属于小粉两人约定不能过线(即,不能非法访问别人的区域)。
    虚拟地址空间是连续的,因此将地址空间划分为不同区域的方法与上面例子的做法类似,我们用一个区域的起始地址start和终止地址end来调整和维护这一块区域。
struct mm_struct
{
  uint32_t code_start,code_end;
  uint32_t data_start,data_end;
  uint32_t heap_start,heap_end;
  uint32_t stack_start,stack_end;
}

所谓的区域调整,本质就是修改对应区域的start和end的值。

补充说明:

对于区域划分,进程地址空间的划分实际上是这样的:

0-3G是用户空间,命令行参数和环境变量是在用户空间,这也是为什么我们可以在main函数通过第三个参数env获取环境变量。3-4G是内核空间。


总结

以上就是今天要讲的内容,本文介绍了进程地址空间的相关概念。本文作者目前也是正在学习Linux相关的知识,如果文章中的内容有错误或者不严谨的部分,欢迎大家在评论区指出,也欢迎大家在评论区提问、交流。

最后,如果本篇文章对你有所启发的话,希望可以多多支持作者,谢谢大家!

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