热门标签 | HotTags
当前位置:  开发笔记 > 编程语言 > 正文

【Linux】深度理解进程地址空间

测试环境Linuxkernel2.6.3232位平台文章目录1.程序地址空间回顾2.地址空间的“领地意识”3.地址空间是物理内存吗?4.虚拟地址空间5.为什么要存在地

测试环境

Linux kernel 2.6.32
32位平台


文章目录

  • 1.程序地址空间回顾
  • 2. 地址空间的“领地意识”
  • 3. 地址空间是物理内存吗?
  • 4. 虚拟地址空间
  • 5. 为什么要存在地址空间?
    • 好处1:保护内存
    • 好处2:将空间连续化处理
  • 6. 地址空间的本质


1.程序地址空间回顾

相信大家在学习C/C++或者其它语言的时候,一定见到过一幅类似于这样空间布局图:
在这里插入图片描述
语言阶段我们应该是将这幅图称之为“程序地址空间分布图”,不过这幅图可能和你在学习语言阶段所见到的图有些许的差别。大家以前见到的图应该不是完整的,那是因为如果涉及系统知识可能会不太好讲解。

首先我想先写一段代码验证一下这幅图结构布局,顺便带大家回顾一下以前所学的知识:

#include
#include int g_val = 10;
int g_unval;int main(int argc, char* argv[], char* env[])
{printf("code addr: %p\n", main); // <&#61;&#61;&#61; 正文代码区printf("g addr: %p\n", &g_val); // <&#61;&#61;&#61; 初始化数据区printf("g uninit addr: %p\n", &g_unval); // <&#61;&#61;&#61; 未初始化数据区char* mem &#61; (char*)malloc(10);printf("heap addr: %p\n", mem); // <&#61;&#61;&#61; 堆区printf("stack addr: %p\n", &mem); // <&#61;&#61;&#61; 栈区printf("opt addr: %p\n", argv[0]); // <&#61;&#61;&#61; 命令行参数printf("opt addr: %p\n", argv[argc - 1]);printf("env addr: %p\n", env[0]); // <&#61;&#61;&#61; 环境变量return 0;
}

这是一段C语言代码&#xff0c;代码实现的功能是在程序地址分布图的每个区域定义变量&#xff0c;然后通过打印这些变量的地址&#xff0c;观察这些变量的地址是不是如图中所示&#xff0c;从正文代码区到命令行参数区由低到高增长&#xff08;由于共享区里存放的是动态库和共享内存&#xff0c;这些地址不方便打印&#xff0c;所以不包括共享区&#xff09;。

运行结果&#xff1a;
在这里插入图片描述
我们发现的确如图中一样&#xff0c;这些区域的地址是由低到高增长的。

接下来我想在这里纠正一个概念&#xff0c;之前我们一直叫的“程序地址空间”严格来说并不准确&#xff0c;准确的叫法应该是“进程地址空间”&#xff0c;因为程序加载进内存后就变成了一个进程&#xff0c;所以地址空间应该站在进程的角度去分析。

程序地址空间我们就先回顾到这里&#xff0c;接下来我会从系统的角度出发&#xff0c;带大家更深刻的认识地址空间。

2. 地址空间的“领地意识”

一开始我们先来对地址空间有一个浅度的认识&#xff0c;这里我来提一个问题&#xff1a;从地址空间分布图中我们看到&#xff0c;进程地址空间划分了许多区域。那么我想问&#xff0c;这块空间的区域是不是每时每刻都在被进程使用&#xff1f;

为了回答这个问题&#xff0c;我为大家举一个“森林之王”老虎的例子。

我们应该知道&#xff0c;动物世界中像老虎这样的霸主一般情况下都有强烈的“领地意识” 。一只老虎一定会拥有一块领地&#xff0c;这块领地属于该老虎的活动范围&#xff0c;供自己栖息生活。
在这里插入图片描述
上图我为大家简化出了一块老虎的领地&#xff0c;试问该领地内的区域是随时都被老虎占据的吗&#xff1f;

显然不是的&#xff0c;老虎对该领地的使用应该是这样的。当老虎想要休息的时候就去占据休息区&#xff0c;想要吃饭的时候就去占据进食区&#xff0c;想锻炼身体的时候就去占据锻炼区。这些区域仅仅是属于老虎活动范围&#xff0c;但这并不意味着老虎会一直占据它们&#xff0c;老虎只会在特定的情况下去占据特定的区域。

同理&#xff0c;回到系统中&#xff0c;进程就像这只老虎&#xff0c;进程的地址空间就像这块老虎的领地。进程地址空间的作用仅仅是为进程衡量了一块空间&#xff0c;这块空间属于进程的使用范围&#xff0c;但并不意味着进程占据了该地址空间的所有部分。

这是我们应该对进程地址空间的第一层理解。

3. 地址空间是物理内存吗&#xff1f;

我们来看下面这段代码&#xff1a;

#include
#include int g_val &#61; 100;int main()
{pid_t id &#61; fork();if(id &#61;&#61; 0){//childprintf("g_val: %d , g_val addr: %p, child\n", g_val, &g_val);sleep(1);}else if(id > 0){//fathersleep(1);printf("g_val: %d , g_val addr: %p, father\n", g_val, &g_val);sleep(1);}else{//errorprintf("error\n");}return 0;
}

这段代码的作用是&#xff0c;创建子进程&#xff0c;然后父进程和子进程都打印一个全局变量g_val的值和地址。
运行结果&#xff1a;
在这里插入图片描述
我们看到父进程和子进程打印出来的值和地址是完全相同的。

好&#xff0c;这也不难理解。这个全局变量g_val是定义在地址空间初始化区的一块内存中&#xff0c;两个进程指向同一块空间&#xff0c;打印出来的变量内容和地址肯定是一样的了。

下面我来对这段代码稍作修改&#xff1a;

#include
#include int g_val &#61; 100;int main()
{pid_t id &#61; fork();if(id &#61;&#61; 0){//childg_val &#61; 1000;printf("g_val: %d , g_val addr: %p, child\n", g_val, &g_val);sleep(1);}else if(id > 0){//fathersleep(1);printf("g_val: %d , g_val addr: %p, father\n", g_val, &g_val);sleep(1);}else{//errorprintf("error\n");}return 0;
}

我在子进程的分流中&#xff0c;将全局变量g_val的值改成了1000。我们先来推测一下&#xff0c;当我再次运行这段代码之后&#xff0c;会发生什么&#xff1f;是不是父进程和子进程打印出来的值都变成了1000&#xff0c;来看运行结果&#xff1a;
在这里插入图片描述
我们惊讶的发现&#xff0c;当子进程修改全局变量之后&#xff0c;子进程所打印出来的全局变量值发生了改变&#xff0c;而父进程中全局变量的值并未发生改变&#xff0c;但是这两个进程所打印出来的全局变量的地址确实一样的&#xff01;&#xff01;

非常奇怪&#xff0c;现在我来问一个问题&#xff0c;进程地址空间它是物理内存吗&#xff1f;

我们先来假设进程地址空间是物理内存。

如果地址空间是物理内存&#xff0c;那么g_val这个全局变量一定存放在内存的某一块空间中&#xff0c;而这块空间的地址肯定是唯一确定的。也就意味着子进程和父进程打印这个变量时&#xff0c;打印的同一块空间的值。根据上述代码中发生的情况&#xff0c;两个进程打印变量的地址是一样的&#xff0c;说明是同一块空间&#xff0c;但值却不一样。那么试想一下&#xff0c;内存中同一块空间的值&#xff0c;有没有可能在同一时刻被不同进程读取&#xff0c;表现出不同的值&#xff1f;

不可能&#xff01;&#xff01;&#xff01;举一个很简单例子&#xff0c;就像同一间教室&#xff0c;你和你的舍友同时走进教室&#xff0c;你看到的是张三老师在上课&#xff0c;而你的舍友看到的是李四老师在上课&#xff0c;这种情况可能发生吗&#xff1f;根本就是不可能时间。

因此我们推翻上面地址空间是物理内存的假设&#xff0c;得出结论&#xff1a;进程地址空间一定不是物理内存。

接下来问题又来了&#xff0c;你说地址空间不是内存&#xff0c;但我变量的值的确是存到了某一个地方&#xff0c;并且这个地方还有对应的地址&#xff0c;这些又怎么来解释呢&#xff1f;

这里要告诉大家的是&#xff0c;进程地址空间实际是一块虚拟出来空间&#xff0c;下面我就来为大家介绍虚拟地址空间。

4. 虚拟地址空间

在这里插入图片描述

如上图所示&#xff0c;我们看到进程地址空间实际上是进程和物理内存之间的一层虚拟层&#xff0c;进程地址空间是物理内存的一种虚拟化表示&#xff0c;最终一定要以某种方式转换到物理内存&#xff0c;进程看到内存空间是被虚拟空间解释的。

拿上面的代码举例&#xff0c;全局变量g_val看起来被保存在虚拟地址空间的初始化区&#xff0c;实际上是虚拟地址空间通过某种方式把变量的内容映射到了物理内存当中。

我们还看到&#xff0c;地址空间实际上是一个进程特有的。那么也就意味着有多少个进程&#xff0c;就有多少个进程地址空间。

所以在上述代码中&#xff0c;父子进程看到的都只是各自的虚拟地址空间&#xff0c;因此他们看到的全局变量的地址仅仅是各自虚拟空间中全局变量的地址。但这个地址绝对不是物理地址&#xff0c;操作系统最终会把虚拟地址转化为物理地址&#xff0c;但是父子进程访问的数据&#xff0c;最后绝对会被保存到不同的物理内存中。

接下来我再来回答一个问题&#xff0c;为什么这两个进程的地址空间是不相同的&#xff0c;但是变量保存的地址确实一样的&#xff1f;

这是因为父进程在创建子进程的时候&#xff0c;父进程的地址空间会给子进程拷贝一份&#xff0c;所以子进程拿到了一份和父进程类似的地址空间&#xff0c;并且在同一个位置看到了一个全局变量g_val。

到这里&#xff0c;我们就可以自己将上述代码中的内存变化来捋一遍了。

首先创建父进程&#xff0c;父进程开辟一块虚拟地址空间&#xff0c;在虚拟空间的初始化区创建一个全局变量g_val&#xff0c;操作系统会将这个虚拟地址映射到物理内存的某个区域。

接下来子进程创建&#xff0c;拷贝父进程的地址空间&#xff0c;于是子进程在虚拟空间相同位置看到了一个全局变量g_val&#xff0c;。此时注意&#xff0c;由于数据并未发生变化&#xff0c;所以子进程和父进程的代码还是共享的&#xff0c;因此子进程的g_val和父进程映射的是同一个位置。

接下来子进程修改g_val的值&#xff0c;我们知道进程之间是相互独立的&#xff0c;而这个独立首先就要体现在数据独立上。一开始子进程的值和父进程相同&#xff0c;相互之间还可以共享物理内存。如果子进程的值发生变化&#xff0c;操作系统就会在内存中重新为子进程的g_val开辟一块空间。但是这里仅仅是虚拟地址映射的物理内存发生变化&#xff0c;而虚拟地址并未发生变化。

这也就不难理解为什么父子进程地址相同&#xff0c;但是值不相同。因为虚拟地址相同&#xff0c;但是映射到物理内存的地址却是不一样的。

5. 为什么要存在地址空间&#xff1f;

讲到这里&#xff0c;相信大家有对于地址空间已经有了初步的认识。不过我想有人可能还是会疑惑&#xff0c;为什么要有地址空间的存在&#xff1f;进程直接和物理内存交互难道不好吗&#xff1f;

接下来我就来向大家讲述地址空间的两大好处。

好处1&#xff1a;保护内存

我们在学习语言阶段肯定发生过数组越界访问&#xff0c;指针越界访问的情况。这个时候程序一定会报错或者直接崩溃掉&#xff0c;以避免我们非法访问内存&#xff0c;这其中就是地址空间在起作用。

我们要知道&#xff0c;物理内存是没有辨别越界能力的。所以如果进程直接访问内存&#xff0c;当发生越界的时候就不会出现报错&#xff0c;这样你就有可能写坏其它空间的内容。

下面我再来讲地址空间是如何来保护内存。

前面我一直在说物理内存和地址空间直接存在一层映射关系&#xff0c;这层关系被保留的地方我们一般将它称之为页表

在这里插入图片描述
上图所示为一个简易的页表模型&#xff0c;比方说你的进程申请了一个数组&#xff0c;这个时候你会获得一个虚拟地址的范围&#xff0c;这时页表就会保存你的虚拟地址范围和映射到内存中的物理地址范围。同时页表还会保存数据的一些相关信息&#xff0c;比方说“可读可写”、“只可读不可写”。

这样当你通过地址访问数组的时候&#xff0c;操作系统会就会在页表中找有没有该虚拟地址和物理地址的对应关系。如果地址正常&#xff0c;页表中一定会有映射关系存在&#xff0c;这样就可以正常访问数组。如果发生越界情况&#xff0c;操作系统在页表中找不到该虚拟地址和物理地址的对应关系&#xff0c;说明非法访问&#xff0c;然后报错。

还有如果当你试图更改一个不可写的数据时&#xff0c;操作系统会判断当前数据是否可写&#xff0c;如果不可写就会报错。比方说&#xff1a;字符串、常量等等…

这就是地址空间的第一个作用&#xff0c;保护内存。

好处2&#xff1a;将空间连续化处理

如果进程直接存放进物理内存中&#xff0c;数据就可能会出现离散现象。
在这里插入图片描述
上图为数据保存的一个模拟过程&#xff0c;红色代表当前进程内存&#xff0c;黑色代表其它被占用的内存。当你申请空间的时候总会出现内存块不足的情况&#xff0c;这时你就必须重新找一片区域开辟内存。这样就会导致内存数据离散化&#xff0c;访问起来特别不方便。

那么地址空间如何来解决这个问题呢&#xff1f;
我们知道地址空间是只属于进程自己的&#xff0c;因此即使虚拟地址映射的物理内存不连续&#xff0c;但我一定可以保证我的虚拟地址是连续的。而我在实际访问的时候也只关心虚拟地址&#xff0c;只要虚拟地址连续&#xff0c;我访问起来就方便。至于怎样通过虚拟地址去访问不连续的物理地址&#xff0c;这是操作系统为我们解决的事情&#xff0c;就不用我们再费心了。

因此我们总结出地址空间的第二个好处&#xff1a;将空间连续化处理

为了方便大家理解&#xff0c;这里我再为大家举一个生活中的例子。

你去银行存钱&#xff0c;是不是直接把你的钱放到银行的金库里面去&#xff1f;当然不是。如果每个人都直接把钱存到银行的金库中&#xff0c;而金库只有存钱的功能&#xff0c;这样如果有些坏人拿走别人的钱怎么办&#xff1f;因此银行金库和用户之间会存在一个柜台&#xff0c;用户只需把钱交给柜台服务人物&#xff0c;然后服务人员帮你存钱&#xff0c;这样是不是可以保护金库的安全。

还有一点&#xff0c;比方说你每个月往银行存3000元&#xff0c;现在你要取出10000元出来&#xff0c;你会发现你零零散散存进去的钱&#xff0c;可以被整取出来。这就是银行柜台的好处&#xff0c;可以保证用户零存整取。

银行这一系列操作和我们的操作系统十分类似&#xff0c;银行金库就像是物理内存&#xff0c;而银行柜台就像是虚拟地址空间&#xff0c;用户就像一个进程&#xff0c;希望大家可以好好感受一下。

6. 地址空间的本质

现在我想再从另外一个角度带大家认识一下地址空间。前面我曾经说过&#xff0c;每个进程都有一个地址空间。也就是说100个进程&#xff0c;就有100个地址空间&#xff0c;请问这些地址空间需不需要被管理起来&#xff1f;

当然需要被管理。请问&#xff0c;怎么管理&#xff1f;

六个字&#xff1a;“先描述&#xff0c;再组织”。请问怎么描述一个地址空间&#xff1f;

用一个结构体来描述。所以说地址空间的本质是就是一个结构体struct

这里可能说的大家有些懵了&#xff0c;用结构体描述一块地址空间&#xff0c;这怎么能做到。

我先来为大家举一个简单的例子&#xff1a;
在一所小学中有一个小男孩和小女孩是同桌关系&#xff0c;这个小男孩平时不怎么讲卫生&#xff0c;每天看起来脏脏的&#xff0c;臭臭的。而同桌的小女孩特别爱干净&#xff0c;小女孩就很讨厌这个小男孩。于是有一天小女孩做了这样一件事&#xff0c;拿笔在桌子上画了一条线&#xff0c;就是我们俗称的“三八线”。小女孩对小男孩说&#xff0c;你不要越过这条线&#xff0c;敢过来我就打你。

现在我的问题来了&#xff0c;画三八线的本质是在干什么&#xff1f;

本质就是划分区域

那么我们能不能先试着用结构体表示出小男孩和小女孩的区域。

既然是区域&#xff0c;那就一定要有范围。比方说小男孩的范围是[1, 40],小女孩的范围是[40, 100].于是我们可以这样表示。

struct area
{int start;int end
}&#xff1b;struct area b_a &#61; {1, 40};
struct area g_a &#61; {40, 100};

然后在回到我们的地址空间&#xff0c;我们发现地址空间实际上也是在划分区域&#xff0c;堆区一块区域&#xff0c;栈区一块区域…因此我们可以这样来定义地址空间的结构&#xff1a;

struct mm_struct
{unsigned long code_start;unsigned long code_end;unsigned long init_data_start;unsigned long init_data_end;unsigned long uninit_data_start;unsigned long uninit_data_end;unsigned long heap_start;unsigned long heap_end;.........
};

所以申请空间的本质是&#xff1a;向内存所要空间得到物理地址&#xff0c;然后在特定的区域申请没有被使用的虚拟地址&#xff0c;建立映射关系&#xff0c;再返回虚拟地址即可。

本篇文章到这里就全部结束了&#xff0c;虽然我已经尽可能的为大家去剖析地址空间了&#xff0c;但肯定有些地方还是没有讲到&#xff0c;因为系统部分涉及的知识面实在是太广了&#xff0c;不易全部展开。当然&#xff0c;我相信如果大家能看完本篇文章一定会有所收益的。对于本篇文章如果有问题的话可以私信我&#xff0c;最后希望这篇文章能够为大家带来帮助。


推荐阅读
  • Linux环境变量函数getenv、putenv、setenv和unsetenv详解
    本文详细解释了Linux中的环境变量函数getenv、putenv、setenv和unsetenv的用法和功能。通过使用这些函数,可以获取、设置和删除环境变量的值。同时给出了相应的函数原型、参数说明和返回值。通过示例代码演示了如何使用getenv函数获取环境变量的值,并打印出来。 ... [详细]
  • c语言\n不换行,c语言printf不换行
    本文目录一览:1、C语言不换行输入2、c语言的 ... [详细]
  • 本文详细介绍了Linux中进程控制块PCBtask_struct结构体的结构和作用,包括进程状态、进程号、待处理信号、进程地址空间、调度标志、锁深度、基本时间片、调度策略以及内存管理信息等方面的内容。阅读本文可以更加深入地了解Linux进程管理的原理和机制。 ... [详细]
  • 本文介绍了解决二叉树层序创建问题的方法。通过使用队列结构体和二叉树结构体,实现了入队和出队操作,并提供了判断队列是否为空的函数。详细介绍了解决该问题的步骤和流程。 ... [详细]
  • 本文讨论了clone的fork与pthread_create创建线程的不同之处。进程是一个指令执行流及其执行环境,其执行环境是一个系统资源的集合。在调用系统调用fork创建一个进程时,子进程只是完全复制父进程的资源,这样得到的子进程独立于父进程,具有良好的并发性。但是二者之间的通讯需要通过专门的通讯机制,另外通过fork创建子进程系统开销很大。因此,在某些情况下,使用clone或pthread_create创建线程可能更加高效。 ... [详细]
  • VScode格式化文档换行或不换行的设置方法
    本文介绍了在VScode中设置格式化文档换行或不换行的方法,包括使用插件和修改settings.json文件的内容。详细步骤为:找到settings.json文件,将其中的代码替换为指定的代码。 ... [详细]
  • 本文讨论了使用差分约束系统求解House Man跳跃问题的思路与方法。给定一组不同高度,要求从最低点跳跃到最高点,每次跳跃的距离不超过D,并且不能改变给定的顺序。通过建立差分约束系统,将问题转化为图的建立和查询距离的问题。文章详细介绍了建立约束条件的方法,并使用SPFA算法判环并输出结果。同时还讨论了建边方向和跳跃顺序的关系。 ... [详细]
  • 本文介绍了一种划分和计数油田地块的方法。根据给定的条件,通过遍历和DFS算法,将符合条件的地块标记为不符合条件的地块,并进行计数。同时,还介绍了如何判断点是否在给定范围内的方法。 ... [详细]
  • 本文介绍了为什么要使用多进程处理TCP服务端,多进程的好处包括可靠性高和处理大量数据时速度快。然而,多进程不能共享进程空间,因此有一些变量不能共享。文章还提供了使用多进程实现TCP服务端的代码,并对代码进行了详细注释。 ... [详细]
  • 本文介绍了C函数ispunct()的用法及示例代码。ispunct()函数用于检查传递的字符是否是标点符号,如果是标点符号则返回非零值,否则返回零。示例代码演示了如何使用ispunct()函数来判断字符是否为标点符号。 ... [详细]
  • CF:3D City Model(小思维)问题解析和代码实现
    本文通过解析CF:3D City Model问题,介绍了问题的背景和要求,并给出了相应的代码实现。该问题涉及到在一个矩形的网格上建造城市的情景,每个网格单元可以作为建筑的基础,建筑由多个立方体叠加而成。文章详细讲解了问题的解决思路,并给出了相应的代码实现供读者参考。 ... [详细]
  • 本文介绍了PE文件结构中的导出表的解析方法,包括获取区段头表、遍历查找所在的区段等步骤。通过该方法可以准确地解析PE文件中的导出表信息。 ... [详细]
  • 成功安装Sabayon Linux在thinkpad X60上的经验分享
    本文分享了作者在国庆期间在thinkpad X60上成功安装Sabayon Linux的经验。通过修改CHOST和执行emerge命令,作者顺利完成了安装过程。Sabayon Linux是一个基于Gentoo Linux的发行版,可以将电脑快速转变为一个功能强大的系统。除了作为一个live DVD使用外,Sabayon Linux还可以被安装在硬盘上,方便用户使用。 ... [详细]
  • 本文介绍了南邮ctf-web的writeup,包括签到题和md5 collision。在CTF比赛和渗透测试中,可以通过查看源代码、代码注释、页面隐藏元素、超链接和HTTP响应头部来寻找flag或提示信息。利用PHP弱类型,可以发现md5('QNKCDZO')='0e830400451993494058024219903391'和md5('240610708')='0e462097431906509019562988736854'。 ... [详细]
  • Android源码深入理解JNI技术的概述和应用
    本文介绍了Android源码中的JNI技术,包括概述和应用。JNI是Java Native Interface的缩写,是一种技术,可以实现Java程序调用Native语言写的函数,以及Native程序调用Java层的函数。在Android平台上,JNI充当了连接Java世界和Native世界的桥梁。本文通过分析Android源码中的相关文件和位置,深入探讨了JNI技术在Android开发中的重要性和应用场景。 ... [详细]
author-avatar
mobiledu2502903757
这个家伙很懒,什么也没留下!
PHP1.CN | 中国最专业的PHP中文社区 | DevBox开发工具箱 | json解析格式化 |PHP资讯 | PHP教程 | 数据库技术 | 服务器技术 | 前端开发技术 | PHP框架 | 开发工具 | 在线工具
Copyright © 1998 - 2020 PHP1.CN. All Rights Reserved | 京公网安备 11010802041100号 | 京ICP备19059560号-4 | PHP1.CN 第一PHP社区 版权所有