『 Linux 』进程地址空间存在的意义

简介: 『 Linux 』进程地址空间存在的意义



前言🦕

在文章『 Linux 』进程地址空间概念中提到了进程地址空间的部分概念;

这部分概念主要围绕进程地址空间到底是什么;

在实际中,进程地址空间是一个进程的数据结构,这个数据结构的作用是模拟出虚拟地址;

当一个进程需要访问物理内存时必须经过进程地址空间获取其虚拟地址,通过页表找到页表中所映射的物理地址,才能对需要的物理地址中的数据进行操作;

这样的操作流程一定程度上保证了进程间与物理内存的安全性;


🦖 防止进程对物理内存的非法(危险)访问

在进程当中,每个进程都会有对应的PCB结构体(进程控制块),进程控制块与进程的进程地址空间产生对应关系;

当一个进程需要去访问对应的物理地址时将要从进程地址空间获取对应的虚拟地址,通过该虚拟地址以一种映射关系映射到对应的物理地址当中;

这个映射关系是通过一种名为页表的数据结构进行的;

页表以key/value的模型使得cpu获取到虚拟地址时能通过映射关系找到对应的物理地址;

而实际当中,页表不仅仅可以做到映射关系,页表还能做到权限查询;

当进程创建之后将会初始化进程间对应的一些数据结构,这些数据结构包括进程控制块,进程地址空间,页表等;

而在初始化页表的阶段,不仅会给页表初始化对应的虚拟地址(不一定会直接申请内存并产生映射关系),还会根据虚拟地址对应代码初始化对应的权限(页表中的页表项,不作过多说明);

使得一个进程在对物理内存进行非法访问的时候能使该进程因内权限不足不予访问;

如果进程在通过页表映射关系对物理内存的访问非法访问时则会触发页表的权限查询;

以一个例子为例,存在这样一段代码:

int main()
{
    const char *str="hello world";
    *str = 'H';
    return 0;
}

在实际中这段代码将在编译过程中的语义分析报错而导致编译失败;

假设这段代码编译未报错且生成了对应的可执行程序;

这段代码中使用*str对字符常量区进行修改;

但字符常量区的代码的权限为只读权限,即该区域内的代码不能被进行写入操作;

当进程需要对该物理内存以写入的方式进行访问时将会触发页表的权限查询行为;

内存管理单元(MMU)会根据页表中的映射关系讲虚拟地址转化为物理地址,并在转换的过程中进行权限检查;

如果权限不符合访问要求,MMU将会触发异常,这个异常会被传递给OS(操作系统),OS在接收到异常过后会根据异常的类型进行处理;


在OS的层面中,本质上的物理内存是不具备物理访问权限限制的,意思是物理内存本身是可以被进行任意的读写操作的;


而若是直接对物理内存进行读和写的操作时可能会出现进程间的误操作导致不能保证物理内存的安全性;


而在拥有进程地址空间(包括页表)时,在这套机制下,内存管理单元(MMU)将对页表的权限进行查询,使得若是某个进程非法对物理内存进行读写操作时能对该操作进行有效拦截;

可以使用mprotect()函数对该场景进行模拟:

#include <stdio.h>
#include <stdlib.h>
#include <sys/mman.h>
#include <string.h>
int main() {
    const char* str = "hello world";
    // 将str所在的内存页标记为只读
    size_t page_size = sysconf(_SC_PAGESIZE);
    void* page_start = (void*)((uintptr_t)str & -page_size);
    if (mprotect(page_start, page_size, PROT_READ) != 0) {
        perror("mprotect");
        return 1;
    }
    // 尝试对只读内存区域进行写入操作
    char* writable_str = (char*)str; // 将const char*转换为char*,这是非法操作
    *writable_str = 'H'; // 这里会触发内存保护异常
    return 0;
}

在这个演示中,使用mprotect()函数将str所在的内存页标记为只读,然后尝试对只读内存区域进行写入操作;


当进程尝试对只读内存区域进行非法写入时,会触发内存保护异常,从而导致程序的终止或异常处理;


总而言之,进程地址空间的存在以及内存管理单元(MMU)对页表的权限查询机制保护了物理内存中的所有的合法数据(包括进程及与内核相关的有效数据);


🦖 进程管理模块与内存管理模块的解耦合

耦合顾名思义就是关联性:

在开发的过程中一般都要求程序的模块间尽量的低耦合高类聚;

一个程序模块间的耦合度越低,其维护成本也低;

在历史中的进程中并不存在进程地址空间,使得一个进程在访问物理地址的时候需要采用直接访问的方式(地址+偏移量);

若是采用直接对物理内存进行访问的方式对地址进行读写操作,则可能出现某些进程恶意修改其他进程的上下文内容或是其他有效代码与合法数据,将危及其他进程;

且若是采用这种方式对物理内存进行访问的话其进程管理与内存管理将是一种强耦合的关系;

对于这种强耦合的关系其维护成本必定高于弱耦合;

而进程地址空间的出现可以有效的将进程对内存的访问分为两个模块:

  • 内存管理模块
  • 进程管理模块

对于进程管理模块而言,操作系统将初始化对应的进程控制块(PCB结构体)与其内部的数据结构,这些数据结构包括进程中的进程地址空间(mm_struct),在对进程地址空间进行初始化时将对页表内部对应的虚拟地址进行初始化;

当一个进程初始化结束时(并未进入调度队列运行)时,其虚拟地址是已经通过磁盘内的虚拟地址(逻辑地址)进行同步的初始化;

但实际上其物理内存并未真正给予该进程对应的物理内存空间,只不过当该进程使用CPU资源时OS将根据进程的代码为进程合理分配物理内存;

OS为了使进程的物理地址更加具有安全性,将会采用一种ASLR的内存分布随机化的技术使得进程页表中虚拟地址所映射的物理地址进行随机分布;

即一个进程的虚拟地址在页表中所映射的物理地址在物理内存中是可以随机分布的,并不会以在语言层面的内存概念那样以栈区,堆区,正文代码区等等进行内存分布;

这也更加的能够使得进程管理模块内存管理模块进行解耦合;

当然在对一个进程进行初始化(包括进程地址空间)时并不一定在会将物理内存中开辟物理空间;

在语言层面当中(以c/C++为例),当使用new或者是malloc对内存申请空间时,对于上层的这个内存申请并不是实质的物理内存;

为了避免物理内存被申请时并不马上被使用所造成的空间浪费,上层在申请内存空间时本质上是在进程地址空间中申请的,当上层对进程地址空间进行内存申请时,OS并不会马上在页表中进行映射(开辟物理内存空间);

只有当真正需要对物理内存进行访问时OS才会执行内存的相关管理算法(包括内存申请与构建页表的映射关系)后再对该物理空间进行访问;

OS作为进程与各项资源的管理者,是可以随时对物理内存进行访问的,而在用户和进程的视角当中,并不会感知OS对应的执行内存相关管理算法等操作;

OS将采用一种缺页中断的技术判断页表中的虚拟地址是否有映射对应的物理地址(开辟物理空间);

总而言之,因为进程地址空间以及页表的存在,可以使得整体以内存管理模块进程管理模块两个模块进行解耦合;

在进程管理模块当中OS只需要根据磁盘中的虚拟地址(逻辑地址)对进程地址空间与页表进行初始化;

而在内存管理模块当中,OS更可以不对该进程立马分配物理地址,而是根据延迟分配的方式提高整机的效率;


🦖 实现进程间的独立性

根据上文可知,OS在通过页表中的虚拟地址映射给物理地址时(开辟空间)所采用的方式为ASLR的随内存分布随机化的方式,导致了真正在物理内存当中其物理地址的分化是无序的;

而进程地址空间和页表的存在尤其是页表的映射关系使得在进程的视角中可以使得内存有序化;

同时从上文得知,在对一个进程申请内存时其物理空间并不会马上被申请(延迟分配的策略);

由于内存管理模块与进程管理模块的解耦合,在多个进程对物理内存进行访问时OS将使用一定的内存管理使得在进程的视角当中每个进程都能够独立拥有一整块内存空间,以此实现进程间的独立性;

由此可知进程间的独立性可以依靠进程地址空间与页表共同完成;

总而言之由于进程地址空间的存在,在进程的视角当中每个进程都可以拥有有序的4GB内存(32位机器下);

操作系统将通过页表映射到不同的物理地址从而实现进程的独立性;


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