Linux进程学习【进程地址】

简介: 对于 `C/C++` 来说,程序中的内存包括这几部分:`栈区`、`堆区`、`静态区` 等,其中各个部分功能都不相同,比如函数的栈帧位于 `栈区`,动态申请的空间位于 `堆区`,全局变量和常量位于 `静态区` ,区域划分的意义是为了更好的使用和管理空间,那么 `真实物理空间` 也是如此划分吗?`多进程运行` 时,又是如何区分空间的呢?`写时拷贝` 机制原理是什么?本文将对这些问题进行解答

✨个人主页: Yohifo
🎉所属专栏: Linux学习之旅
🎊每篇一句: 图片来源
🎃操作环境: CentOS 7.6 阿里云远程服务器

  • Perseverance is not a long race; it is many short races one after another.

    • 毅力不是一场漫长的比赛;是许多短跑一个接一个。

    31feb00d655343e18e58ffd74efceeba.jpg

📘前言

对于 C/C++ 来说,程序中的内存包括这几部分:栈区堆区静态区 等,其中各个部分功能都不相同,比如函数的栈帧位于 栈区,动态申请的空间位于 堆区,全局变量和常量位于 静态区 ,区域划分的意义是为了更好的使用和管理空间,那么 真实物理空间 也是如此划分吗?多进程运行 时,又是如何区分空间的呢?写时拷贝 机制原理是什么?本文将对这些问题进行解答

内存条:真实的物理空间,用来存储各种数据

R-C.jpg


📘正文

📖问题引入

地址是唯一的,对地址进程编号的目的是为了不冲突

这是个耳熟能详的概念,在 C语言 学习阶段,我们可以通过对变量 & 取地址的方式,查看当前变量存储空间的首地址信息

#include <stdio.h>

int main()
{
  const char* ps = "这是一个常量字符串";
  printf("字符串地址:%p\n", ps);  //%p 专门用来打印地址信息
  return 0;
}

结果.png

利用前面学习的 fork 函数创建子进程,使得子进程和父进程共同使用一个变量

#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <stdlib.h>

int main()
{
  int val = 10;
  pid_t id = fork();
  if(id == 0)
  {
      val *= 2;    //刻意改变共享值
    printf("我是子进程,pid:%d ppid:%d 共享值:%d 共享值地址:%p\n", getpid(), getppid(), val, &val);
    exit(0);
  }

  waitpid(id, 0, 0);

  printf("我是父进程,pid:%d ppid:%d 共享值:%d 共享值地址:%p\n", getpid(), getppid(), val, &val);
  return 0;
}

结果2.png

对于同一块空间,读取到了不同的值,是不可能出现这种情况的

因为真实地址都是 唯一 的,分析:

  • 不同的空间出现同名的情况
  • 父子进程使用的真实物理空间并非同一块空间!

原因:

  • 当子进程尝试修改共享值时,发生 写时拷贝 机制
  • 语言层面的程序空间地址不是真实物理地址
  • 一般将此地址称为 虚拟地址线性地址

结论: 语言层面的地址都是虚拟地址,用户无法看到真实的物理地址,由 OS 统一管理


📖虚拟空间划分

一般用户的认知中,C/C++ 程序内存分布如下图所示,直接表示内存中的各个部分
虚拟空间.png


📖真实空间分布

但实际上的空间分布是这样的:
真实分布.png

如果有多个进程(真实地址空间只有一份),此时情况是这样的:
多进程.png

🖋️代码实现

在实现虚拟地址空间时,是用结构体 mm_struct 实现的

task_struct 一样,mm_struct 中也包含了很多成员,比如不同区域的边界值

//简单展示其中的成员信息
mm_struct
{
    //代码区域划分
    unsigned long code_start;
    unsigned long code_end;

    //堆区域划分
    unsigned long heap_start;
    unsigned long heap_end;

    //栈区域划分
    unsigned long stack_start;
    unsigned long stack_end;

    //还有很多其他信息
    ……
}

每个进程都会有这样一个 mm_struct,其中的区域划分就是虚拟地址空间

通过对边界值的调整,可以做到不同区域的增长,如堆区、栈区扩大

mm_struct 中的信息配合 页表+MMU 在对应的真实空间中使内存(程序寻址)

🖋️问题反思

此时可以理解为什么会发生同一块空间能读取到不同值的现象了

  • 父子进程有着各自的 mm_struct,其成员起始值一致
  • 对于同一个变量,如果未改写,则两者的虚拟地址通过 页表 + MMU 转换后指向同一块空间
  • 发生改写行为,此时会在真实空间中再开辟一块空间,拷贝变量值,让其中一个进程的虚拟地址空间映射改变,这种行为称为 写时拷贝

刚开始,父子进程共同使用同一块空间
真相.png

当子进程修改共享值后

写时拷贝.png


📖进程地址空间

下面来好好谈谈 进程地址空间 (虚拟地址)

🖋️虚拟地址

在早期程序中,是没有虚拟地址空间的,对于数据的写入和读取,是直接在物理地址上进行的,程序与物理空间直接打交道,存在以下问题:

  • 假设存在野指针问题,此时可能直接对物理内存造成越界读写
  • 程序运行时,每次都需要大小为 4GB 的内存使用,当进程过多时,资源分配就会很紧张,引起进程阻塞,导致执行效率下降
  • 动态申请内存后,需要依次释放,影响整体效率

野指针行为.png

为了解决各种问题,大佬们提出了 虚拟地址空间 这个概念,有了 虚拟空间 后,当进程创建时,系统会为其分配属于自己的 虚拟空间需要使用内存时,通过 寻址 的方式,使用物理地址上的空间即可

  • 多个进程互不影响,动态使用,做到 效率资源 双赢
  • 发生越界行为时,寻址 机制会检测出是否发生越界行为,如果发生了,能在其对物理地址造成影响前进行拦截
  • 因为每个进程都有属于自己的空间,OS 在管理进程时,能够以统一的视角进行管理,效率很高

光有 虚拟地址空间 是不够的,还需要一套完整的 ''翻译'' 机制进行程序寻址,如 Linux 中的 页表 + MMU

🖋️页表+MMU

页表 本质上就是一张表,操作系统 会为每个 进程 分配一个 页表,该 页表 使用 物理地址 存储。当 进程 使用类似 malloc 等需要 映射代码或数据 的操作时,操作系统 会在随后马上 修改页表 以加入新的 物理内存。当 进程 完成退出时,内核会将相关的页表项删除掉,以便分配给新的 进程
原话出处: ARM体系架构——MMU

系统底层机制的研究是非常生涩的,这里简言之就是 页表 记录信息,通过 MMU 机制进行寻址使用内存,假设目标空间为只读区域(比如数据段、代码段),在进行空间开辟时,会打上只读权限标签。后续对这块进行写入操作时,会直接拒绝

对于这种机制感兴趣的同学可以点击下面这几篇文章查看详细内容:
Linux的虚拟内存详解(MMU、页表结构)
ARM体系架构——MMU
逻辑地址、页表、MMU等

🖋️写时拷贝

Linux 中存在一个很有意思的机制:写时拷贝
这是一种 赌bo 行为,OS 此时就赌你不会对数据进行修改,这样就可以 使多个 进程 在访问同一个数据时,指向同一块空间,当发生改写行为时,再新开辟空间进行读写

这种行为对于内置类型来说感知还不是很强,但如果是自定义类型的话,写时拷贝 行为可以在某些场景下减少 拷贝构造 函数的调用次数(尤其是 深拷贝),尽可能提高效率

可以通过一个简单的例子来证明此现象

//计算 string 类的大小
#include <iostream>
#include <string>
using namespace std;


int main()
{
    string s;
    cout << sizeof(s) << endl;
    return 0;
}

对比.png

原因:

  • g++ 中的 string 对象创建后,它就赌你不会直接改写,所以实际对象为一个指针类型(64位环境下为8字节),当发生改写行为时,触发 写时拷贝 机制,再进行其他操作

🖋️内存申请

值得注意的是,在进行动态内存申请时,OS 也并非直接去申请好内存,而是先判断是否有足够的内存,如果有,就在 页表 中记录相应信息(这种行为叫做 缺页中断),当程序实际使用到这块空间时,OS 才会去申请内存给程序使用

OS是一个讲究人,不允许任何空间浪费或低效率行为

假设没有 缺页中断 机制,给程序分配空间后,程序又不用,此时空间属于闲置状态,这是不被 OS 认可的低效浪费行为

缺页中断.png

图片来源:3.2.2 OS之请求分页管理方式(请求页表、缺页中断机构、地址变换机构)


📖虚拟地址空间存在的意义

总结一下,虚拟内存+页表+MMU 这种管理方式的好处:

  • 防止地址随意访问,保护物理内存与其他进程(权限设置)
  • 进程管理内存管理 进行 解耦,方便 OS 进行更高效的管理
  • 可以让进程以统一的视角看待自己的代码和数据

📘总结

以上就是本篇关于 Linux进程学习【进程地址】的全部内容了,我们从一个有趣的小问题切入,见识到了 虚拟地址空间物理地址空间 的奇妙关系,在种种机制的加持之下,OS 对进程的管理变得更加得心应手,系统也因此得以高效运行

如果你觉得本文写的还不错的话,期待留下一个小小的赞👍,你的支持是我分享的最大动力!

如果本文有不足或错误的地方,随时欢迎指出,我会在第一时间改正


1658489422102.jpg.jpg

相关文章推荐


Linux进程学习【环境变量】

Linux进程学习【进程状态】

Linux进程学习【基本认知】


===============


Linux工具学习之【gdb】

Linux工具学习之【git】

Linux工具学习之【gcc/g++】

Linux工具学习之【vim】

承蒙厚爱,感谢支持.gif

目录
相关文章
|
10天前
|
存储 安全 Linux
|
11天前
|
缓存 监控 Linux
linux进程管理万字详解!!!
本文档介绍了Linux系统中进程管理、系统负载监控、内存监控和磁盘监控的基本概念和常用命令。主要内容包括: 1. **进程管理**: - **进程介绍**:程序与进程的关系、进程的生命周期、查看进程号和父进程号的方法。 - **进程监控命令**:`ps`、`pstree`、`pidof`、`top`、`htop`、`lsof`等命令的使用方法和案例。 - **进程管理命令**:控制信号、`kill`、`pkill`、`killall`、前台和后台运行、`screen`、`nohup`等命令的使用方法和案例。
39 4
linux进程管理万字详解!!!
|
13天前
|
Linux Shell 数据安全/隐私保护
|
2天前
|
存储 运维 监控
深入Linux基础:文件系统与进程管理详解
深入Linux基础:文件系统与进程管理详解
33 8
|
11天前
|
算法 Linux 定位技术
Linux内核中的进程调度算法解析####
【10月更文挑战第29天】 本文深入剖析了Linux操作系统的心脏——内核中至关重要的组成部分之一,即进程调度机制。不同于传统的摘要概述,我们将通过一段引人入胜的故事线来揭开进程调度算法的神秘面纱,展现其背后的精妙设计与复杂逻辑,让读者仿佛跟随一位虚拟的“进程侦探”,一步步探索Linux如何高效、公平地管理众多进程,确保系统资源的最优分配与利用。 ####
39 4
|
12天前
|
缓存 负载均衡 算法
Linux内核中的进程调度算法解析####
本文深入探讨了Linux操作系统核心组件之一——进程调度器,着重分析了其采用的CFS(完全公平调度器)算法。不同于传统摘要对研究背景、方法、结果和结论的概述,本文摘要将直接揭示CFS算法的核心优势及其在现代多核处理器环境下如何实现高效、公平的资源分配,同时简要提及该算法如何优化系统响应时间和吞吐量,为读者快速构建对Linux进程调度机制的认知框架。 ####
|
13天前
|
消息中间件 存储 Linux
|
4月前
|
运维 关系型数据库 MySQL
掌握taskset:优化你的Linux进程,提升系统性能
在多核处理器成为现代计算标准的今天,运维人员和性能调优人员面临着如何有效利用这些处理能力的挑战。优化进程运行的位置不仅可以提高性能,还能更好地管理和分配系统资源。 其中,taskset命令是一个强大的工具,它允许管理员将进程绑定到特定的CPU核心,减少上下文切换的开销,从而提升整体效率。
掌握taskset:优化你的Linux进程,提升系统性能
|
4月前
|
弹性计算 Linux 区块链
Linux系统CPU异常占用(minerd 、tplink等挖矿进程)
Linux系统CPU异常占用(minerd 、tplink等挖矿进程)
160 4
Linux系统CPU异常占用(minerd 、tplink等挖矿进程)
|
3月前
|
算法 Linux 调度
探索进程调度:Linux内核中的完全公平调度器
【8月更文挑战第2天】在操作系统的心脏——内核中,进程调度算法扮演着至关重要的角色。本文将深入探讨Linux内核中的完全公平调度器(Completely Fair Scheduler, CFS),一个旨在提供公平时间分配给所有进程的调度器。我们将通过代码示例,理解CFS如何管理运行队列、选择下一个运行进程以及如何对实时负载进行响应。文章将揭示CFS的设计哲学,并展示其如何在现代多任务计算环境中实现高效的资源分配。