【Linux】基础:进程地址空间

news2025/4/6 22:58:14

【Linux】基础:进程地址空间

摘要:本文首先通过复习关于C语言内存空间的知识来做实验提出问题,从而引入进程的地址空间。需要理解的是进程地址空间的组织形式与其表示意义,在需要理解如何完成进程地址空间的划分以及关键对应物理内存的思想,掌握虚拟的概念。最后通过解释设计原因,帮助读者更深入理解进程地址空间。


文章目录

  • 【Linux】基础:进程地址空间
    • 一. 背景介绍
      • 1.1 C语言地址空间
      • 1.2 问题提出
    • 二. 进程地址空间概述
    • 三. 进程地址空间划分方式
    • 四. 虚拟地址与物理内存
    • 五. 进程地址空间设计原因
    • 六. 总结

一. 背景介绍

1.1 C语言地址空间

在C语言学习过程中,可能曾经了解过C语言的程序地址空间分布,对于32位的机器,内存空间大小为4GB,从低地址到高地址,分别划分区域为:正文代码、初始化数据、未初始化数据、堆、共享区、栈区与命令行参数环境变量几个部分。其中需要注意的是栈的地址是从高地址向低地址使用的,而堆区相反

现在,通过进一步的实验来验证该图的空间分布,在此简单书写各区域的代码,并对其取地址并打印,进行比较,实验如下:

proc:proc.c
	gcc -o $@ $^ -std=c99
.PHONY:clean
	rm -f proc
#include<stdio.h>
#include<string.h>
#include<stdlib.h>
#include<unistd.h>
int gal_unval;
int gal_val = 100;

int main(){
	const char *s = "hello world";
	printf("Address of main: %p\n", main);
	printf("Address of const String: %p\n",s);
	printf("Address of int: %p\n",&gal_val);
	printf("Address of unint: %p\n",&gal_unval);
	char *heap = (char*)malloc(sizeof(char));

	printf("Address of heap: %p\n",heap);

	int a = 10;
	int b = 20;
	printf("Address of Stack_s: %p\n",&s);
	printf("Address of Stack_a: %p\n",&a);
	printf("Address of Stack_b: %p\n",&b);
	return 0;
}
[lht@VM-12-7-centos Blogs_processAddress]$ ./proc 
Address of main: 0x4005d6
Address of const String: 0x400758
Address of int: 0x60102c
Address of unint: 0x601034
Address of heap: 0x1cc06b0
Address of Stack_s: 0x7ffe402086a0
Address of Stack_a: 0x7ffe4020869c
Address of Stack_b: 0x7ffe40208698

可以发现,从打印出来的结果是由小到大排序,符合上图的区域分布。

再进行实验验证,栈的地址是从高地址向低地址使用的,而堆区相反,实验如下:

#include<stdio.h>
#include<string.h>
#include<stdlib.h>
#include<unistd.h>

int main(){
	char *heap = (char*)malloc(sizeof(char));
	char *heap1 = (char*)malloc(sizeof(char));
	char *heap2 = (char*)malloc(sizeof(char));
	char *heap3 = (char*)malloc(sizeof(char));
	printf("Address of heap: %p\n",heap);
	printf("Address of heap1: %p\n",heap1);
	printf("Address of heap2: %p\n",heap2);
	printf("Address of heap3: %p\n",heap3);

	int a = 10;
	int b = 20;
	int c = 30;
	printf("Address of Stack_a: %p\n",&a);
	printf("Address of Stack_b: %p\n",&b);
	printf("Address of Stack_b: %p\n",&c);

	return 0;
}
[lht@VM-12-7-centos Blogs_processAddress]$ ./proc 
Address of heap: 0x8472a0
Address of heap1: 0x8472c0
Address of heap2: 0x8472e0
Address of heap3: 0x847300
Address of Stack_a: 0x7fff4fbb45fc
Address of Stack_b: 0x7fff4fbb45f8
Address of Stack_b: 0x7fff4fbb45f4

实验输出地址排序符合预期结果。

1.2 问题提出

再进行一次实验,来查看父子进程的数据对于C语言地址空间的分布情况,实验代码与输出结果如下:

#include<stdio.h>
#include<string.h>
#include<stdlib.h>
#include<unistd.h>
int gal_val = 1;
int main(){
	if(fork() == 0){
		//child
		int nums = 5;
		while(nums){
			printf("I am child ,nums = %d,gal_val = %d,gal_val_address = %p\n", nums ,gal_val,&gal_val);
			nums--;
			if(nums == 2){
				printf("====================== 在此处修改了gal_val的值为100 ======================\n");
				gal_val = 100;
			}
			sleep(1);
		}
	}
	else{
		//parent
		while(1){
			printf("I am father ,gal_val = %d,gal_val_address = %p\n",gal_val,&gal_val);
			sleep(1);
		}
	}
}
[lht@VM-12-7-centos Blogs_processAddress]$ ./proc 
I am father ,gal_val = 1,gal_val_address = 0x60103c
I am child ,nums = 5,gal_val = 1,gal_val_address = 0x60103c
I am father ,gal_val = 1,gal_val_address = 0x60103c
I am child ,nums = 4,gal_val = 1,gal_val_address = 0x60103c
I am father ,gal_val = 1,gal_val_address = 0x60103c
I am child ,nums = 3,gal_val = 1,gal_val_address = 0x60103c
=========================== 在此处修改了gal_val的值为100 ===========================
I am father ,gal_val = 1,gal_val_address = 0x60103c
I am child ,nums = 2,gal_val = 100,gal_val_address = 0x60103c
I am father ,gal_val = 1,gal_val_address = 0x60103c
I am child ,nums = 1,gal_val = 100,gal_val_address = 0x60103c
I am father ,gal_val = 1,gal_val_address = 0x60103c

从实验结果可以发现,好似在最开始时,父进程与子进程公用一块地址空间,在后来,我们对程序的变量进行了修改,会发生非常熟悉的写时拷贝,预期结果是在内存中开辟了一个块新的地址空间储存了这个子进程数据,可是实验结果出现了预期以外的现象,在这里的值改了,但是地址空间却还是同一块地址空间,这是否非常奇怪

在此对该实验现象进行介绍,其实在这里的地址空间并不是物理的地址空间,而是虚拟的地址空间,是逻辑上的地址空间,因此进程的地址空间是虚拟的。在此实验中提出了进程的虚拟空间这一个概念,本文将会介绍,如何通过虚拟空间来实现进程地址空间的管理,如何与物理内存交互起来,最后会对该实验结果进行重新的解释。当然本文只是对进程地址空间的概述,因此不会探讨的太深刻。

二. 进程地址空间概述

在进程概念中提到过,对于操作系统的管理方式是对管理对象进行描述再进行组织,对于地址空间的管理也是使用了同样的思路。对于每个进程来说,创建时会同时会创建一个进程控制块,进程控制块包含了进程的所有属性,其中也有管理进程地址空间的数据结构,在Linux中这个数据结构名为mm_struct

对于mm_struct,是内核中的一个数据结构类型,是具体进程的地址空间变量,其表示的大小就是内存的大小,通过这个数据结构,进程可以认为自己拥有了物理内存的实际空间,可是这是不现实的,在操作系统中有如此之多的进程,怎么可能每个进程都是独占物理内存,因此这个独占是虚拟的。而能设置如此的虚拟空间,是因为有着内存调度算法的作用,可以通过其来提高效率,提高空间利用率,本文对此不会赘述,将在后文具体描述。

对于mm_struct与PCB和物理内存,在此关系进行一个简单的图示:

三. 进程地址空间划分方式

对于进程地址空间的划分是通过偏移量来划分的,类比与刻度尺,在通过每个区域的头尾刻度进行划分。也就是在地址空间上,对应的地址空间以线性的视角看待,地址空间上进行区域划分时,分别对应线性位置的头尾来划分区域,但对应的线性位置是分配划分的虚拟地址。

划分的地址空间是按照物理内存大小进行划分给的,对于32位地址的内存,就是4GB的内存。也就是说每个进程都会认为自己拥有4GB的内存空间,而对于操作系统而言,这4GB是虚拟的,如果是物理空间也是不可能做到的。用C语言进行简单的描述如下:

struct mm_struct{
	.......

	unsigned int code_start;
	unsigned int end_start;

	unsigned int init_data_start;
	unsigned int init_data_end;

	unsigned int uninit_data_start;
	unsigned int uninit_data_end;

	.......

	unsigned int stack_start;
	unsigned int stack_end;
	
	.......
}

四. 虚拟地址与物理内存

根据上面的分析,虚拟地址就是进程控制块在创建时同时也创建的进程地址空间,在Linux上为数据结构结构体mm_struct,虚拟地址也可以称为线性地址,其的表示的大小与内存的大小一致,从0x00000000至0xFFFFFFFF,犹如拷贝一样,但其空间是虚拟的不存在的

可是面对那么多的进程,操作系统是如何给每个进程画饼,告诉他们自身独占内存呢,实际上,操作系统可以管理页表与MMU使虚拟内存与物理内存相互对应。**MMU是一种硬件,功能相当于查页表,全称为内存管理单元,集成在CPU中。页表是操作系统给进程维护的表 ,可以理解为映射表或者哈希表,其作用是将虚拟地址与物理地址对应,还包括了虚拟地址访问物理地址的权限。**如此设计简单来说是为了方便管理内存,合法使用物理内存。在此只是对页表的内容进行简单的提示,不做过多赘述,未来会进行更详细的说明。简易图示如下:

五. 进程地址空间设计原因

进程地址空间通过虚拟空间这种方式的设计原因主要有三点:

  • 通过添加一层软件层,完成有效的对进程操作内存够进行风险管理(权限管理),本质目的是为了保护物理内存以及各个进程的数据安全;
  • 将内存申请和内存使用在时间上划分清楚,通过虚拟地址空间,来屏蔽申请内存的过程,达到进程读写内存够和OS进行内存管理操作进行软件上面的分离;
  • 站在CPU的角度和应用层的角度,进程统一可以看作统一使用4GB空间,而且每个空间区域的相对位置是比较确定的;

原因一:通过添加一层软件层,完成有效的对进程操作内存够进行风险管理(权限管理),本质目的是为了保护物理内存以及各个进程的数据安全。

对于原因一,在上述分析中,我们知道进程地址空间是虚拟空间,不与实际的物理空间进行直接交互,需要通过查页表才能完成交互。试想如果每个进程都可以直接访问实际物理地址空间,那么难以方式越界访问,非法访问,错误访问等问题,对内存安全与各个进程安全是非常严重的威胁,因此通过添加一个软件层,可以通过操作系统的介入,对进程访问内存进行监测,防止非法行为的发生。

原因二:将内存申请和内存使用在时间上划分清楚,通过虚拟地址空间,来屏蔽申请内存的过程,达到进程读写内存和OS进行内存管理操作进行软件上面的分离;

在进程申请地址空间时,实际上是只是申请了,而实际物理空间并不一定为之开辟,因为进程申请物理空间后,不一定就对其使用,所谓的使用就是对其进行读写。当进程没有使用申请的空间时,但又分配了实际的物理空间给该进程,就意味着原本这些空间可以给其他进程使用的,而现在却被闲置了,这样就会造成空间上的浪费。

因此操作系统可以通过虚拟内存的方式,将内存申请与使用在时间上划分,通过内存管理算法,在申请空间后需要使用,进行缺页中断,进行物理内存申请,达到进程读写内存和OS进行内存管理操作进行软件上面的分离。由这种方式来进行地址空间管理,对于进程而言OS做的内存申请的动作是透明的。

原因三:站在CPU的角度和应用层的角度,进程统一可以看作统一使用4GB空间,而且每个空间区域的相对位置是比较确定的;

对于CPU与应用层,将进程地址空间的虚拟内存看作物理内存直接使用,直接划分区域,这种较为确定区域的划分,便于CPU访问,而物理内存中的数据和代码是可以加载到任何位置的,只需通过操作系统的页表管理,就可以完成对虚拟内存物理内存的对应,减少物理内存管理的负担。

总结:如此设计最终可以达成一致的目标:每个进程都认为自己是独占系统资源的。

六. 总结

通过上述说明,引入了虚拟内存的概念,清楚C语言的地址空间并不是物理空间,而且对于每个程序来说应该是进程的地址空间。之所以出现开始的问题现象,是因为对于父子进程而言,都有着属于自己的进程地址空间,在Linux中,用结构体mm_struct表示,而子进程是以父进程为模板的,当有其他数据生成时才会发生写时拷贝,而发生写时拷贝时,虚拟内存申请空间,但在物理内存中才可以体现数据的差异,示意简图如下,可以发现虚拟地址一致但是在页表指向后,物理内存是不一样的。

这样的设计还可以减少内存的浪费,比如面对常量字符串时,如果字符串内容相同,可以共同指向同一块物理内存空间,代码示例与结果如下:

#include<string.h>
#include<stdlib.h>
#include<unistd.h>

int main(){
	const char* str1 = "hello world\n";
	const char* str2 = "hello world\n";

	printf("str1 Address:%p\n",str1);
	printf("str2 Address:%p\n",str2);

	return 0;
}
[lht@VM-12-7-centos Blogs_processAddress]$ ./proc 
str1 Address:0x400688
str2 Address:0x400688

补充:

  1. 代码将会放到:Linux_Review: Linux博客与代码 (gitee.com) ,欢迎查看!
  2. 欢迎各位点赞、评论、收藏与关注,大家的支持是我更新的动力,我会继续不断地分享更多的知识!

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

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

C++12 ---对象于对象的关系

一、对象于对象的关系 在一个系统中&#xff0c;一个对象可能与不同的对象相关&#xff0c;以下是不同的关系。 依赖(Dependency) (使用一个) 关联(Association) (使用一个) 聚合(Aggregation) (有一个) 组合(Composition ) (有一个&#xff0c;"用..来实现") …

从Matlab实例学习遗传算法

文章目录前言问题背景遗传算法Matlab实例代码附录君主方案遗传算法解决旅行商问题前言 本文旨在使用智能优化算法及其MATLAB实例&#xff08;第2版&#xff09; 一书中的例子&#xff0c;来透彻理解遗传算法的本质。 问题背景 目标&#xff1a; 求解最大化函数 f(x)x10sin⁡…

2023最新SSM计算机毕业设计选题大全(附源码+LW)之java星光之夜香水网站的设计与开发bfmcr

大学计算机专业毕业的&#xff0c;实际上到了毕业的时候&#xff0c;基本属于会与不会之间。说会&#xff0c;是因为学了整套的理论和方法&#xff0c;就是所谓的科班出身。说不会&#xff0c;是因为实践能力极差。 不会的问题&#xff0c;集中体现在毕设的时候&#xff0c;系…

CTFSHOW菜狗杯 web

文章目录web签到web2 c0me_t0_s1gn我的眼里只有$抽老婆一言既出驷马难追TapTapTapWebshell化零为整无一幸免传说之下&#xff08;雾&#xff09;算力超群算力升级2遍地飘零茶歇区小舔田&#xff1f;LSB探姬Is_Not_Obfuscate龙珠NFTweb签到 eval($_REQUEST[$_GET[$_POST[$_COOK…

Ubuntu22.04虚拟机配置双网

文章目录Ubuntu22.04虚拟机配置双网一、 虚拟机网络1、 简介1.1 概述1.2 四种网络2、 配置双网2.1 NAT2.2 主机模式3、 添加到虚拟机二、 ubuntu设置Ubuntu22.04虚拟机配置双网 一、 虚拟机网络 1、 简介 1.1 概述 近期在使用VirtualBox的时候遇到这样的场景&#xff0c;我…

Docker(五)—— 镜像原理、容器快照commit

一、如何得到镜像 1&#xff09;从远程仓库下载 2&#xff09;朋友/同事拷贝给你 3&#xff09;自己制作DockerFile 二、联合文件系统 Docker的镜像是由一层层的文件系统组成&#xff0c;这种层级的文件系统叫做联合文件系统UnionFS。 三、Docker镜像加载原理 1. bootfs:…

第十四届蓝桥杯校内模拟赛第一期——Python

第十四届蓝桥杯校内模拟赛第一期——Python 文章目录第十四届蓝桥杯校内模拟赛第一期——Python1.二进制位数问题描述参考答案扩展2. 晨跑问题描述参考答案扩展3. 调和级数问题描述参考答案4. 山谷问题描述参考答案5. 最小矩阵问题描述参考答案6. 核酸日期问题描述输入格式输出…

redux与react-redux的学习笔记之react-redux

redux与react-redux前言一、redux和react-redux是啥&#xff1f;二、redux使用步骤1.引入库2.原理图原理图释义actions1&#xff09;含义2&#xff09;demostore.js1&#xff09;含义2&#xff09;demoreducer.js1&#xff09;含义2&#xff09;demoCount.jsx1&#xff09;含义…

2022年,我们为什么要学习C++?

“C已死” 大学时代&#xff0c;我就听过这样的说法——差不多十多年前的事儿了。那时候至少在美国&#xff0c;Java已经成了各公司的主流语言。程序员也许都很熟悉Joel Spolsky在2005年12月对JavaSchools发起的批驳。此外&#xff0c;作为微软应对Java的手段&#xff0c;2000…

Ubuntu环境配置(instant-ngp)

综合环境配置 这篇文章的综合配置我是在恒源云上配的&#xff0c;自己穷买不起机子&#xff0c;就只能租咯&#xff0c;这家价格还行&#xff0c;而且可以装VNC&#xff0c;非推广&#xff0c;只是感觉方便&#xff0c;请大家结合自身实际情况 数据上传 这里有几种方法&…

【免费】多种方法手把手教你如何将自己做的网页做成网络链接(直接访问)

目录 前言 ​一、github&#xff08;最常用的&#xff09; 二、七牛云&#xff08;推荐小白使用&#xff0c;简单粗暴&#xff09; 三、NATAPP 四、codepen&#xff08;建议学网页的人群使用&#xff09; 彩蛋 前言 http://t.csdn.cn/VaiP1我之前发的爱心代码&#xff0c;…

电脑突然开机无反应,怎么办

电脑常见故障之三开机无响应&#xff08;上&#xff09; 经常使用电脑的朋友应该会碰到这种情况&#xff0c;开机时按下电源按钮后&#xff0c;电脑无响应&#xff0c;显示器黑屏不亮。 除去那些傻瓜式的故障原因&#xff0c;如显示器、主机电源没插好&#xff1b;显示器与主…

手写小程序摇树优化工具(一)——依赖分析介绍

道可道&#xff0c;非常道&#xff1b;名可名&#xff0c;非常名&#xff1b;玄之又玄&#xff0c;众妙之门。 现在国内好像没有什么针对小程序代码的摇树优化方案&#xff0c;出现了很多超包的问题无法解决&#xff0c;本教程就手把手教大家如何编写一个完整的微信小程序摇树优…

深度学习零基础学习之路——第四章 UNet-Family中Unet、Unet++和Unet3+的简介

Python深度学习入门 第一章 Python深度学习入门之环境软件配置 第二章 Python深度学习入门之数据处理Dataset的使用 第三章 数据可视化TensorBoard和TochVision的使用 第四章 UNet-Family中Unet、Unet和Unet3的简介 Unet-Family的学习Python深度学习入门前言一、FCN全卷积网络…

【JavaSE】面向对象三大特性之多态

文章目录多态的概念向上转型重写之动态绑定与重载之静态绑定重写与重载的区别重写的注意事项总结不安全的向下转型多态的优点和注意事项优点缺点和注意事项多态的概念 多态可以理解为一个对象在某些时刻可以代表不同的对象&#xff0c;指的是对象的多种形态。所以在某些时刻&a…

CSDN第九次竞赛题解与总结

CSDN第九次竞赛题解与总结前言T1小艺读书题意分析T2鬼画符门之宗门大比题意分析代码别的方法T3硬币划分题意分析状态转移方程初始值代码T4饿龙咆哮-逃离城堡题意分析坑点代码写在最后前言 2022/11/12 我有幸参加了csdn第九次竞赛&#xff0c;终于拿了次满分&#xff0c;进了次…

Vuex④(多组件共享数据、Vuex模块化+namespace)

文章目录多组件共享数据代码实现Vuex模块化总结多组件共享数据 我们现在想实现这种情况&#xff1a; Person组件的总人数就是Person中列表的长度 br上的是Count组件&#xff0c;br下的是Person组件。 我们通过vuex中的state实现一些数据的多组件共享&#xff1a; 代码实现 …

第二章STP应用配置

目录 一 生成树 二 生成树算法 三 STP是什么 四 BPDU是什么 五 BPDU的概念 六 生成树基本配置 一 生成树 生成树算法的网桥协议STP(Spanning Tree Protocol) 它通过生成生成树保证一个已知的网桥在网络拓扑中沿一个环动态工作。网桥与其他网桥交换BPDU消息来监测环路&#xf…

使用 hugo oss 搭建个人博客网站

系列文章目录 文章目录系列文章目录前言一、下载hugo二、oss三、域名四、创建博客上传五、发布&#xff0c;上传文章前言 本文主要详解如何用最低的成本搭建个人博客网站 原本我是直接用的github搭建的博客网站&#xff0c;因为免费&#xff0c;但由于github访问很不稳定&…

Python——正则表达式的应用

文章目录前言正则表达式方法re.search方法group方法re.match方法re.findall方法re.finditer方法re.split方法re.sub方法正则表达式的应用前言 提示&#xff1a;这里可以添加本文要记录的大概内容&#xff1a; 正则表达式是字符串处理的有力工具和技术。 使用正在表达式的目的…