【Linux】Linux进程地址空间

1.程序地址空间分配回顾

在前⾯C语⾔以及C部分介绍过⼆者的内存分配如下图所示:

全局变量区和未初始化全局变量区也被称为数据区,数据区中除了有全局变 量,还有静态变量和常量

使⽤下⾯的代码演示不同的内容所处的地址:

#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
 
// 初始化的全局变量
int global = 0;
// 未初始化的全局变量
int value;
 
int main() {
    // 环境变量
    char* envi = getenv("PWD");
    
    // 堆区
    int* ptr = (int*)malloc(sizeof(int)*10);
    // 栈区
    int a = 0;
    int b = 0;
    int c = 0;
    
    printf("%p\n", &global);
    printf("%p\n", &value);
    printf("%p\n", ptr);
    // 代码区
    printf("%p\n", main); // 函数名即函数地址
    printf("%p\n", envi);
    printf("%p\n", &a);
    printf("%p\n", &b);
    printf("%p\n", &c);
    
    return 0;
}
 
输出结果:
0x601048
0x60104c
0x7e1010
0x4005cd
0x7ffc99757f43
0x7ffc9975694c
0x7ffc99756948
0x7ffc99756944

实际上,所谓的程序地址空间就是进程地址空间,进程地址空间是如何产⽣的就是下 ⾯需要探讨的问题

2.进程地址空间

2.1.虚拟地址

前⾯提到,⽗进程和⼦进程会共享代码和数据,尤其是两个进程不进⾏数据修改时, 数据不会产⽣两份,那么这样理解就可以直观地认为当⼦进程修改了数据,对应的变 量内存地址就会发⽣改变,但是改变的是不是程序读取到的地址,看下⾯例⼦的演示 结果:

#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
 
// 初始化的全局变量
int global = 0;
// 未初始化的全局变量
// int value;
 
int main() {
    printf("父进程, pid = %d, &global = %p\n", getpid(), &global);
    
    pid_t p = fork();
    
    if(p == 0) {
    // 子进程
    while(1) {
            printf("子进程, pid = %d, &global = %p\n", getpid(), &global);
            // 子进程修改
            global++;
            sleep(1);
            }
        } else {
            while(1) {
            
            }
    }
    
    return 0;
}
 
输出结果:
父进程, pid = 12969, &global = 0x601050
子进程, pid = 12970, &global = 0x601050

可以看到,尽管⼦进程修改了与⽗进程共享的代码中的变量,⼦进程读取到的变量的 地址与⽗进程读取到的变量的地址是完全相同的。

实际上,在C语⾔程序中使⽤ & 获取到的地址是⼀个虚拟地址(也称线性地址),对 应虚拟地址的就是物理地址。

在上⾯提到的「⼦进程改变代码中的数据,对应的内存地址会发⽣改变」,本质是因 为此处的内存地址指的是物理地址,⽽不是虚拟地址

2.2.地址空间

地址空间,可以理解为是操作系统为每⼀个进程开辟的⼀块运⾏空间,示意图如下:

为了保证所有的进程都有⼀个完整的地址空间,操作系统为每个进程提供⼀个独⽴的 虚拟地址空间。例如,如果操作系统⽀持3GB的虚拟地址空间,每个进程都会认为⾃ ⼰拥有独⽴的3GB空间,不会受到其他进程占⽤内存的影响。这⼀过程暂且可以理解 为操作系统为每⼀个进程“画的⼀张⼤饼”,⽽这个“饼”即为进程地址空间

2.3.区域划分

进程地址空间中存在着多个区域,每⼀个区域有着⾃⼰的作⽤。每⼀块区域的划分实 际上是根据区域的起始值和终⽌值进⾏决定,示意图如下:

为了⽅便管理,在Linux中,操作系统在底层的 task_struct 内部存在着⼀个结构 体指针,该结构体指针的类型是 mm_struct 结构体,该结构体中存在着⼀些变量⽤ 于存储指定区域的起始值和终⽌值。这些值本质也是地址值,但是这个地址并不是实 际的物理内存的地址,⽽是通过映射后的虚拟地址,源码如下:

struct mm_struct {
    // ...
    unsigned long start_code, end_code, start_data, end_data;
    unsigned long start_brk, brk, start_stack;
    unsigned long arg_start, arg_end, env_start, env_end;
    unsigned long rss, total_vm, locked_vm;
    // ...
};

因为变量的地址都是在这些指定的进程地址空间区域中,所以也可以解释为什么程序 获取到的是虚拟地址⽽⾮物理地址

解释:

3.分页与数据独立

在上⾯探讨了虚拟地址和物理地址,操作系统为了管理这两个地址之间的映射,从⽽ 保证虚拟地址能够正确访问到实际的物理地址对应的内容,在创建⼀个进程PCB时会 同时创建⼀个⻚表。

在执⾏前⾯的代码的过程中,⼦进程未创建之前,⽗进程拥有⾃⼰的⻚表和进程地址 空间,此时全局变量 global 会由操作系统在物理地址上开辟⼀块空间,并将映射后 的虚拟地址填充到⻚表中被进程获取,如下图所示:

当创建⼦进程之后,⼦进程会与⽗进程共享代码和数据,此时意味着⼦进程拷⻉⽗进 程中的 task_struct 和⻚表,对 task_struct 中的内容进⾏针对性地修改,在⼦进 程没有改变复制过来的数据之前,由于⻚表内容相同,所以⼦进程⻚表中的映射与⽗ 进程完全相同,示意图如下:

当⼦进程对 global 变量进⾏修改时,操作系统就会在物理内存中开辟⼀块新的空 间,将共享的 global 数据拷⻉到该空间,这个拷⻉的过程也被称为写时拷⻉。物理 内存虽然开辟了⼀块新的空间,实际上对于⼦进程的⻚表来说,还是通过同样的虚拟 地址映射物理地址:

上⾯的过程也就可以解释为什么最开始的代码中,⼦进程和⽗进程获取到的变量值不 同,但是地址却相同,也就更加验证了⼀个变量中不可能存在两个不同的值

4.页表基本介绍

在Linux中,⻚表不仅仅有虚拟地址和物理地址的映射,还有物理地址的属性 RWX 以 及是否有数据的标识 isExists ,所以更加细致的⻚表应该如下所示:

RWX 属性:代表虚拟地址对应的物理地址是否具有读(R)、写(W)和执⾏权限 (X)。

前⾯提到,每⼀个进程地址空间区域都由指定的起始值和终⽌值进⾏划分,⽽这些区 域有的是可以写,有的不可以写只能读,但是对于物理内存来说,绝⼤部分的空间都 是可以写的,所以对于限制指定的物理地址是否可以写⼊就是通过 RWX 属性进⾏控制

例如,前⾯学习到的栈区和堆区,在程序代码运⾏时,可以在栈区和堆区申请空间并 进⾏写⼊,但是对于字符串常量等具有常性的值就不可以进⾏随意修改和写⼊。⽽之 所以在语⾔层⾯⽆法检测到这种问题,原因就是⻚表并不是在编译期间就创建的,⽽ 是在程序运⾏开始由操作系统创建,但是为了语⾔层⾯更加容易看出这种错误,就可 以把不想被修改的内容或者本身不可以修改的内容修饰为 const ,从⽽让编译器可 以在编译器检查出错误

此刻也便可以解释为什么程序会有野指针的概念,在语⾔层⾯,野指针表示指针中的 地址对应的空间已经被释放并归还给操作系统进⾏管理,此时不可以通过这个指针访 问对应的地址。在此时的系统层⾯,之所以可以限制野指针写⼊就是通过 RWX 属性, 因为虚拟地址对应的物理地址具有的属性已经是R

isExists 属性:代表虚拟地址对应的物理地址是否存在有效数据。在操作系统加载 进程时,会创建对应的进程地址空间和⻚表,在⻚表中将虚拟地址和物理地址进⾏映 射。然⽽,这个过程中某些进程的代码可能特别多,导致正⽂代码区占⽤的空间变得 很⼤,⽽实际使⽤时,可能有很⼤⼀部分的代码⻓时间不会被执⾏,造成资源浪费。 为了解决这个问题,操作系统通常采⽤按需加载(惰性加载,Lazy Loading)策 略。操作系统会先加载⼀部分内容,在运⾏过程中,通过检查⻚表中的有效位 (Valid Bit)来判断虚拟地址访问的物理地址是否已经加载。如果没有加载,则 触发缺⻚中断(Page Fault),操作系统会从磁盘加载相应的数据到内存中,再继 续执⾏。这⼀过程也适⽤于交换分区(Swap Partition),决定何时将数据换⼊ 或换出内存

结合前⾯的两个属性,操作系统就实现先告诉进程⾃⼰已经开辟好了空间,这个空间 的地址由多个虚拟地址组成,但是实际上可能物理地址并没有全部与指定的虚拟地址 对应,当程序运⾏到指定的部分再进⾏开辟映射,这个操作就可以实现将内存空间利 ⽤率最⼤化

5.缺页中断与写时拷贝

前⾯提到,当⼦进程修改了⽗进程的共享变量就会发⽣写时拷⻉,实际上,在这之前 需要进⾏⼀系列的操作检测

当⽗进程开始运⾏,⼦进程还未被创建之前,⽗进程的代码区为只读,但是数据区是 默认为读写的,当⼦进程创建后,代码区依旧是只读,但是⼆者的数据区均变为只 读,⼦进程从创建的位置开始执⾏代码,因为⼦进程会拷⻉ task_struct 中有关程 序计数器部分的内容,此时⽗⼦进程都只是读取对应代码区和数据区的数据,实现数 据和代码共享

注意::!

此处之所以⼦进程不额外直接拷⻉⽗进程的数据,尽管将来可能要对数据进⾏ 修改,是因为可能⽗进程的数据很⼤,但是⼦进程需要修改的数据很⼩,如果在创建 ⼦进程时就将数据拷⻉,此时就会导致空间的浪费

当⼦进程准备修改数据时,因为数据区已经变为只读,⼦进程想要修改就会修改失 败,此时就会触发缺⻚中断错误。但是因为缺⻚中断错误发⽣的原因不只有⼀种,还 有可能是野指针写⼊等情况,所以此时系统需要检测是否是误操作。当系统检测到数 据区本身是读写的,但是现在被设置为只读时,就会认为需要发⽣写时拷⻉

判定完需要进⾏写时拷⻉时,系统就会在物理内存中申请空间,将原始数据拷⻉到新 空间,并对⽗进程和⼦进程的数据区修改为只读,⽗进程和⼦进程代码继续执⾏

这个过程中涉及到原始数据拷⻉⽽不是简单开辟空间使⽤等待⼦进程使⽤新数 据进⾏覆盖是因为可能⼦进程对数据的修改是基于原数据的,例如变量⾃增⾏为等

上述过程简略示意图如下:

⼦进程修改数据::

6.进程地址空间结构初始化时机

任何⼀个结构体在创建时⼀定要进⾏初始化,⽽进程地址空间也是由⼀个结构进⾏描 述,这个结构的初始化由操作系统在可执⾏程序加载到内存时完成,可执⾏程序加载 到内存变成进程时,操作系统可以获取到部分区域的起始值和终⽌值,例如正⽂代码 区、数据区(包括全局变量区、常量区、静态变量区)和命令⾏与环境变量区。所以 这就可以解释为什么静态变量、全局变量和常量⼀直持续到进程结束

但是这其中有两个不同的区域:栈区和堆区,栈区在函数创建时会开辟对应的栈帧, 堆区在申请时会在已有的堆区上额外开辟需要的空间,所以这两个区域在程序刚加载 到内存时是不存在的

7.总结

之所以需要存在⻚表和进程地址空间有以下三个原因:

1. 保护内存:如果让进程直接访问物理内存,会导致在物理层⾯的野指针情况, 并且这个情况在物理层⾯并不容易被发现和阻⽌

2. 存在⻚表可以达到进程管理和内存管理耦合度降低:因为⻚表主要作⽤的是虚 拟地址和物理地址之间的映射,操作系统在创建进程时初始化对应的虚拟地址 即可,但是虚拟地址是否有对应的物理地址可以不⽤关⼼,只要没有被使⽤。 ⽽对于内存管理,操作系统只需要考虑在需要的时候将物理地址加载到⻚表, 此时对应的虚拟地址有映射就可以正常执⾏,在不需要的时候,将物理地址设 置为只读或者给其他进程使⽤

3. 让进程以统⼀的视⻆看待物理内存(⽆序变有序):因为操作系统会为每⼀个 进程开辟⼀个独⽴的⻚表和进程地址空间,让每⼀个进程都认为⾃⼰拥有操作 系统分配的全部内存空间,并且在进程访问地址空间时,实际上这个地址空间 的地址是⼀个虚拟地址,这个虚拟地址可以由操作系统⾃主决定为连续地址, 此时就可以不⽤考虑物理地址是否需要连续,因为进程只能获取到虚拟地址, 只要虚拟地址有物理地址映射并且拥有指定的权限,就不会出现问题,从⽽实 现让「让⽆序的物理内存地址变为有序的地址」

物理地址在开辟时,⼀般也会尽量是连续开辟,保证CPU在缓冲中的数据命中率

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/895696.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

Element-ui官方示例(Popover 弹出框)

Element-ui官方示例&#xff08;Popover 弹出框&#xff09;&#xff0c;好用的弹出框。 使用 vue-cli3 我们为新版的 vue-cli 准备了相应的​Element 插件​&#xff0c;你可以用它们快速地搭建一个基于 Element 的项目。 使用 Starter Kit 我们提供了通用的项目模版&#…

深入探讨C++多线程性能优化

深入探讨C多线程性能优化 在现代软件开发中&#xff0c;多线程编程已成为提升应用程序性能和响应速度的关键技术之一。尤其在C领域&#xff0c;多线程编程不仅能充分利用多核处理器的优势&#xff0c;还能显著提高计算密集型任务的效率。然而&#xff0c;多线程编程也带来了诸…

Redis应用高频面试题

Redis 作为一个高性能的分布式缓存系统,广泛应用于后端开发中,因此在后端研发面试中,关于 Redis 的问题十分常见。 本文整理了30个常见的 Redis 面试题目,涵盖了 Redis 的源码、数据结构、原理、集群模式等方面的知识,并附上简要的回答,帮助大家更好地准备相关的面试。 …

【Windows】【DevOps】Windows Server 2022 采用WinSW将一个控制台应用程序作为服务启动(方便)

下载WinSW 项目地址&#xff1a; GitHub - winsw/winsw: A wrapper executable that can run any executable as a Windows service, in a permissive license. 下载地址&#xff1a; https://github.com/winsw/winsw/releases/download/v2.12.0/WinSW-x64.exe 参考配置模…

深度学习 之 模型部署 使用Flask和PyTorch构建图像分类Web服务

引言 随着深度学习的发展&#xff0c;图像分类已成为一项基础的技术&#xff0c;被广泛应用于各种场景之中。本文将介绍如何使用Flask框架和PyTorch库来构建一个简单的图像分类Web服务。通过这个服务&#xff0c;用户可以通过HTTP POST请求上传花朵图片&#xff0c;然后由后端…

Nginx(Linux):服务器版本升级和新增模块

目录 1、概述2、使用Nginx服务信号完成Nginx升级2.1 备份当前版本的Nginx2.2 向服务器导入新的Nginx2.3 向服务器导入新的Nginx2.4 停止老版本Nginx 3、使用Nginx安装目录的make命令完成升级3.1 备份当前版本的Nginx3.2 向服务器导入新的Nginx3.3 执行更新命令 1、概述 如果想…

E41.【C语言】练习:斐波那契函数的空间复杂度的计算及函数调用分析

目录 1.题目 2.解 Fib嵌套函数调用细则的分析 调用堆栈分析 之后的具体内容见视频 附:一张核心图 附:一张堆栈图 注意 1.题目 求下列代码的时间复杂度 long long f(size_t n) {if(n < 3)return 1;return f(n-1) f(n-2); } 2.解 显然是递归算法(递归讲解见35.【…

推荐一款多显示器管理工具:DisplayMagician

DisplayMagician是一款开源工具&#xff0c;专为Windows用户设计&#xff0c;能够通过一个快捷方式轻松自动配置屏幕和声音。它特别适合游戏玩家和应用程序用户&#xff0c;可以实现屏幕配置、声音设备切换以及启动额外程序等功能&#xff0c;最后在游戏或应用程序关闭时&#…

实现vlan间的通信

方法一&#xff1a;单臂路由 概述 单臂路由是一种网络配置&#xff0c;它允许在路由器的一个物理接口上通过配置多个子接口来处理不同VLAN的流量&#xff0c;从而实现VLAN间的通信。 原理 路由器重新封装MAC地址&#xff0c;转换Vlan标签 基础模型 1、配置交换机的链…

Vxe UI vue vxe-table grid 如何滚动、定位到指定行或列

Vxe UI vue vxe-table vxe-grid 在表格中有时候需要对数据会列进行操作。可以会定位到某一行或某一列&#xff0c;vxe-table 中提供了丰富的函数式 API&#xff0c;可以轻松对行与列进行各种的灵活的操作。 定位到指定行与列 通过调用 scrollColumn(columnOrField) 方法&…

阿里云云盘在卸载时关联到PHP进程,如何在不影响PHP进程情况下卸载磁盘

1.问题&#xff1a; 在使用umount /dev/vdc1 卸载磁盘时&#xff0c;提示如下&#xff0c;导致无法在Linux系统下卸载磁盘 umount /dev/vdc1 umount: /var/www/html/*/eshop/IFile3: target is busy.(In some cases useful info about processes that usethe device is found…

WPF -- LiveCharts的使用和源码

LiveCharts 是一个开源的 .NET 图表库&#xff0c;特别适用于 WPF、WinForms 和其他 .NET 平台。它提供了丰富的图表类型和功能&#xff0c;使开发者能够轻松地在应用程序中创建动态和交互式图表。下面我将使用WPF平台创建一个测试实例。 一、LiveCharts的安装和使用 1.安装N…

网盘直链下载神器NDM

工具介绍 ​Neat Download Manager分享一款网盘不限速神器,安装步骤稍微有一点繁琐,但实际体验下载速度飞快,个人实际体验还是非常不错的 NDM是一款免费且强大的下载工具。可以帮助你下载各种文件&#xff0c;还能够在多任务下载中保持出色的速度及其稳定性 通过网盘分享的文…

五年三次冲刺IPO失败,企业业绩成长性恐不足,三年分红约1.5亿元

中超股份终止原因如下&#xff1a;首先&#xff0c;报告期&#xff0c;中超股份营收和净利润增幅出现下降趋势&#xff0c;公司业绩规模成长性恐不足。其次&#xff0c;公司货币资金较为紧张情况下&#xff0c;仍在报告期内连续三年分红&#xff0c;累计1.46亿元&#xff0c;募…

Java爬虫:获取直播带货数据的实战指南

在当今数字化时代&#xff0c;直播带货已成为电商领域的新热点&#xff0c;通过直播平台展示商品并进行销售&#xff0c;有效促进了产品的曝光和销售量的提升。然而&#xff0c;如何在直播带货过程中进行数据分析和评估效果&#xff0c;成为了摆在商家面前的一个重要问题。本文…

边缘计算技术的优势与挑战

如今&#xff0c;随着5G快速无线网络的到来&#xff0c;将计算存储和物联网&#xff08;IoT&#xff09;分析的部署放在靠近数据产生的地方&#xff0c;使得边缘计算成为可能。 物联网设备和新应用的扩展需要实时计算能力。5G无线正在考虑边缘系统&#xff0c;以快速跟踪支持实…

016集——c# 实现CAD类库 与窗体的交互(CAD—C#二次开发入门)

第一步&#xff1a;搭建CAD类库dll开发环境。 第二步&#xff1a;添加窗体 第三步&#xff1a;添加控件 第四步&#xff1a;双击控件&#xff0c;在控件点击方法内输入代码 第五步&#xff1a;在主程序内实例化新建的form类&#xff0c;并弹窗form窗体 第六步&#xff1a;CAD命…

1.2.3 TCP IP模型

TCP/IP模型&#xff08;接网叔用&#xff09; 网络接口层 网络层 传输层 应用层 理念&#xff1a;如果某些应用需要“数据格式转换”“会话管理功能”&#xff0c;就交给应用层的特定协议去实现 tip&#xff1a;数据 局部正确不等于全局正确 但是&#xff0c;数据的 全局正…

Codeforces Round 770 (Div. 2)

比赛链接&#xff1a;Dashboard - Codeforces Round 770 (Div. 2) - Codeforces A. Reverse and Concatenate 题意&#xff1a; 思路&#xff1a; 假设 s "abba" 经过1次操作后 -> "abbaabba" s "abcd" 经过一次操作后 -> "abcd…

EditPlus的安装软件包

解压并粘贴到C:\Program Files (x86)中 点击激活密匙,并一直同意 确认并选择默认的位置: 关闭并重新激活密匙 就好了 无需添加快捷方式: 只需要选择任意文件 并选择该应用打开一次即可 通过百度网盘分享的文件&#xff1a;EditPlus_5.0.611.zip 链接&#xff1a;https://pa…