『 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操作系统中的进程管理机制,包括进程的创建、执行、调度和终止等环节。通过对Linux内核中相关模块的分析,揭示其高效的进程管理策略,为开发者提供优化程序性能和资源利用率的参考。
126 1
|
2天前
|
存储 Linux API
【Linux进程概念】—— 操作系统中的“生命体”,计算机里的“多线程”
在计算机系统的底层架构中,操作系统肩负着资源管理与任务调度的重任。当我们启动各类应用程序时,其背后复杂的运作机制便悄然展开。程序,作为静态的指令集合,如何在系统中实现动态执行?本文带你一探究竟!
【Linux进程概念】—— 操作系统中的“生命体”,计算机里的“多线程”
|
14天前
|
存储 网络协议 Linux
【Linux】进程IO|系统调用|open|write|文件描述符fd|封装|理解一切皆文件
本文详细介绍了Linux中的进程IO与系统调用,包括 `open`、`write`、`read`和 `close`函数及其用法,解释了文件描述符(fd)的概念,并深入探讨了Linux中的“一切皆文件”思想。这种设计极大地简化了系统编程,使得处理不同类型的IO设备变得更加一致和简单。通过本文的学习,您应该能够更好地理解和应用Linux中的进程IO操作,提高系统编程的效率和能力。
66 34
|
18天前
|
消息中间件 Linux C++
c++ linux通过实现独立进程之间的通信和传递字符串 demo
的进程间通信机制,适用于父子进程之间的数据传输。希望本文能帮助您更好地理解和应用Linux管道,提升开发效率。 在实际开发中,除了管道,还可以根据具体需求选择消息队列、共享内存、套接字等其他进程间通信方
55 16
|
1月前
|
消息中间件 Linux
Linux:进程间通信(共享内存详细讲解以及小项目使用和相关指令、消息队列、信号量)
通过上述讲解和代码示例,您可以理解和实现Linux系统中的进程间通信机制,包括共享内存、消息队列和信号量。这些机制在实际开发中非常重要,能够提高系统的并发处理能力和数据通信效率。希望本文能为您的学习和开发提供实用的指导和帮助。
149 20
|
2月前
|
存储 监控 Linux
嵌入式Linux系统编程 — 5.3 times、clock函数获取进程时间
在嵌入式Linux系统编程中,`times`和 `clock`函数是获取进程时间的两个重要工具。`times`函数提供了更详细的进程和子进程时间信息,而 `clock`函数则提供了更简单的处理器时间获取方法。根据具体需求选择合适的函数,可以更有效地进行性能分析和资源管理。通过本文的介绍,希望能帮助您更好地理解和使用这两个函数,提高嵌入式系统编程的效率和效果。
126 13
|
2月前
|
SQL 运维 监控
南大通用GBase 8a MPP Cluster Linux端SQL进程监控工具
南大通用GBase 8a MPP Cluster Linux端SQL进程监控工具
|
2月前
|
运维 监控 Linux
Linux操作系统的守护进程与服务管理深度剖析####
本文作为一篇技术性文章,旨在深入探讨Linux操作系统中守护进程与服务管理的机制、工具及实践策略。不同于传统的摘要概述,本文将以“守护进程的生命周期”为核心线索,串联起Linux服务管理的各个方面,从守护进程的定义与特性出发,逐步深入到Systemd的工作原理、服务单元文件编写、服务状态管理以及故障排查技巧,为读者呈现一幅Linux服务管理的全景图。 ####
|
3月前
|
缓存 算法 Linux
Linux内核的心脏:深入理解进程调度器
本文探讨了Linux操作系统中至关重要的组成部分——进程调度器。通过分析其工作原理、调度算法以及在不同场景下的表现,揭示它是如何高效管理CPU资源,确保系统响应性和公平性的。本文旨在为读者提供一个清晰的视图,了解在多任务环境下,Linux是如何智能地分配处理器时间给各个进程的。
|
3月前
|
网络协议 Linux 虚拟化
如何在 Linux 系统中查看进程的详细信息?
如何在 Linux 系统中查看进程的详细信息?
374 1

热门文章

最新文章