热门标签 | 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;最后希望这篇文章能够为大家带来帮助。


推荐阅读
  • Codeforces Round #566 (Div. 2) A~F个人题解
    Dashboard-CodeforcesRound#566(Div.2)-CodeforcesA.FillingShapes题意:给你一个的表格,你 ... [详细]
  • C++实现经典排序算法
    本文详细介绍了七种经典的排序算法及其性能分析。每种算法的平均、最坏和最好情况的时间复杂度、辅助空间需求以及稳定性都被列出,帮助读者全面了解这些排序方法的特点。 ... [详细]
  • UNP 第9章:主机名与地址转换
    本章探讨了用于在主机名和数值地址之间进行转换的函数,如gethostbyname和gethostbyaddr。此外,还介绍了getservbyname和getservbyport函数,用于在服务器名和端口号之间进行转换。 ... [详细]
  • 题目Link题目学习link1题目学习link2题目学习link3%%%受益匪浅!-----&# ... [详细]
  • 本文详细探讨了KMP算法中next数组的构建及其应用,重点分析了未改良和改良后的next数组在字符串匹配中的作用。通过具体实例和代码实现,帮助读者更好地理解KMP算法的核心原理。 ... [详细]
  • 本题探讨了一种字符串变换方法,旨在判断两个给定的字符串是否可以通过特定的字母替换和位置交换操作相互转换。核心在于找到这些变换中的不变量,从而确定转换的可能性。 ... [详细]
  • 本文介绍如何使用Objective-C结合dispatch库进行并发编程,以提高素数计数任务的效率。通过对比纯C代码与引入并发机制后的代码,展示dispatch库的强大功能。 ... [详细]
  • 火星商店问题:线段树分治与持久化Trie树的应用
    本题涉及编号为1至n的火星商店,每个商店有一个永久商品价值v。操作包括每天在指定商店增加一个新商品,以及查询某段时间内某些商店中所有商品(含永久商品)与给定密码值的最大异或结果。通过线段树分治和持久化Trie树来高效解决此问题。 ... [详细]
  • 扫描线三巨头 hdu1928hdu 1255  hdu 1542 [POJ 1151]
    学习链接:http:blog.csdn.netlwt36articledetails48908031学习扫描线主要学习的是一种扫描的思想,后期可以求解很 ... [详细]
  • 本文探讨了 C++ 中普通数组和标准库类型 vector 的初始化方法。普通数组具有固定长度,而 vector 是一种可扩展的容器,允许动态调整大小。文章详细介绍了不同初始化方式及其应用场景,并提供了代码示例以加深理解。 ... [详细]
  • 本实验主要探讨了二叉排序树(BST)的基本操作,包括创建、查找和删除节点。通过具体实例和代码实现,详细介绍了如何使用递归和非递归方法进行关键字查找,并展示了删除特定节点后的树结构变化。 ... [详细]
  • 本文详细介绍了C语言中链表的两种动态创建方法——头插法和尾插法,包括具体的实现代码和运行示例。通过这些内容,读者可以更好地理解和掌握链表的基本操作。 ... [详细]
  • 本文详细探讨了VxWorks操作系统中双向链表和环形缓冲区的实现原理及使用方法,通过具体示例代码加深理解。 ... [详细]
  • 本题涉及一棵由N个节点组成的树(共有N-1条边),初始时所有节点均为白色。题目要求处理两种操作:一是改变某个节点的颜色(从白变黑或从黑变白);二是查询从根节点到指定节点路径上的第一个黑色节点,若无则输出-1。 ... [详细]
  • 本题通过将每个矩形视为一个节点,根据其相对位置构建拓扑图,并利用深度优先搜索(DFS)或状态压缩动态规划(DP)求解最小涂色次数。本文详细解析了该问题的建模思路与算法实现。 ... [详细]
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社区 版权所有