『 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位机器下);

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


目录
打赏
0
0
0
0
2
分享
相关文章
【Linux进程概念】—— 操作系统中的“生命体”,计算机里的“多线程”
在计算机系统的底层架构中,操作系统肩负着资源管理与任务调度的重任。当我们启动各类应用程序时,其背后复杂的运作机制便悄然展开。程序,作为静态的指令集合,如何在系统中实现动态执行?本文带你一探究竟!
【Linux进程概念】—— 操作系统中的“生命体”,计算机里的“多线程”
深入理解Linux操作系统的进程管理
本文旨在探讨Linux操作系统中的进程管理机制,包括进程的创建、执行、调度和终止等环节。通过对Linux内核中相关模块的分析,揭示其高效的进程管理策略,为开发者提供优化程序性能和资源利用率的参考。
132 1
【Linux】进程IO|系统调用|open|write|文件描述符fd|封装|理解一切皆文件
本文详细介绍了Linux中的进程IO与系统调用,包括 `open`、`write`、`read`和 `close`函数及其用法,解释了文件描述符(fd)的概念,并深入探讨了Linux中的“一切皆文件”思想。这种设计极大地简化了系统编程,使得处理不同类型的IO设备变得更加一致和简单。通过本文的学习,您应该能够更好地理解和应用Linux中的进程IO操作,提高系统编程的效率和能力。
72 34
|
7天前
|
Linux:守护进程(进程组、会话和守护进程)
守护进程在 Linux 系统中扮演着重要角色,通过后台执行关键任务和服务,确保系统的稳定运行。理解进程组和会话的概念,是正确创建和管理守护进程的基础。使用现代的 `systemd` 或传统的 `init.d` 方法,可以有效地管理守护进程,提升系统的可靠性和可维护性。希望本文能帮助读者深入理解并掌握 Linux 守护进程的相关知识。
26 7
|
6天前
|
Linux 进程前台后台切换与作业控制
进程前台/后台切换及作业控制简介: 在 Shell 中,启动的程序默认为前台进程,会占用终端直到执行完毕。例如,执行 `./shella.sh` 时,终端会被占用。为避免不便,可将命令放到后台运行,如 `./shella.sh &`,此时终端命令行立即返回,可继续输入其他命令。 常用作业控制命令: - `fg %1`:将后台作业切换到前台。 - `Ctrl + Z`:暂停前台作业并放到后台。 - `bg %1`:让暂停的后台作业继续执行。 - `kill %1`:终止后台作业。 优先级调整:
29 5
Linux 进程管理基础
Linux 进程是操作系统中运行程序的实例,彼此隔离以确保安全性和稳定性。常用命令查看和管理进程:`ps` 显示当前终端会话相关进程;`ps aux` 和 `ps -ef` 显示所有进程信息;`ps -u username` 查看特定用户进程;`ps -e | grep &lt;进程名&gt;` 查找特定进程;`ps -p &lt;PID&gt;` 查看指定 PID 的进程详情。终止进程可用 `kill &lt;PID&gt;` 或 `pkill &lt;进程名&gt;`,强制终止加 `-9` 选项。
19 3
c++ linux通过实现独立进程之间的通信和传递字符串 demo
的进程间通信机制,适用于父子进程之间的数据传输。希望本文能帮助您更好地理解和应用Linux管道,提升开发效率。 在实际开发中,除了管道,还可以根据具体需求选择消息队列、共享内存、套接字等其他进程间通信方
63 16
Linux:进程间通信(共享内存详细讲解以及小项目使用和相关指令、消息队列、信号量)
通过上述讲解和代码示例,您可以理解和实现Linux系统中的进程间通信机制,包括共享内存、消息队列和信号量。这些机制在实际开发中非常重要,能够提高系统的并发处理能力和数据通信效率。希望本文能为您的学习和开发提供实用的指导和帮助。
172 20
嵌入式Linux系统编程 — 5.3 times、clock函数获取进程时间
在嵌入式Linux系统编程中,`times`和 `clock`函数是获取进程时间的两个重要工具。`times`函数提供了更详细的进程和子进程时间信息,而 `clock`函数则提供了更简单的处理器时间获取方法。根据具体需求选择合适的函数,可以更有效地进行性能分析和资源管理。通过本文的介绍,希望能帮助您更好地理解和使用这两个函数,提高嵌入式系统编程的效率和效果。
135 13
南大通用GBase 8a MPP Cluster Linux端SQL进程监控工具
南大通用GBase 8a MPP Cluster Linux端SQL进程监控工具
AI助理

你好,我是AI助理

可以解答问题、推荐解决方案等