【Linux:程序地址空间--原来操作系统也喜欢画大饼】

简介: 【Linux:程序地址空间--原来操作系统也喜欢画大饼】

1 代码感受

在正式讲程序地址空间前我们先来看一段简单的代码来分析分析:

  1 #include<iostream>
  2 #include<unistd.h>
  3 using namespace std;
  4 
  5 int g_val=100;
  6 
  7 int main()
  8 {
  9   pid_t id=fork();
 10   if(id==0)
 11   {
 12     //child
 13    while(true)
 14    {
 15     cout<<"我是一个子进程,我的pid是:"<<getpid()<<",我的ppid是:"<<getppid()<<",g_val:"<<g_val<<";&g_val:"<<&g_val<<endl;
 16     g_val=200;
 17     sleep(1);
 18    }
 19   }
 20   else
 21   {
 22     //parent                                                                                                                                               
 23    while(true)
 24    {
 25     cout<<"我是一个父进程,我的pid是:"<<getpid()<<",我的ppid是:"<<getppid()<<",g_val:"<<g_val<<";&g_val:"<<&g_val<<endl;
 26     sleep(1);
 27    }
 28   }
 29   return 0;
 30 }
 31 

大家可以自己先分析一下结果。

我们来运行一下结果:

e25da545516a4a77880fd2de7d6859f1.png

大家看前面几行可能就会立马发现问题:我们定义的g_val是全局变量,当子进程修改g_val的值时我们发现父进程的g_val是不受影响的,那么说明父子进程所用的g_val并不是同一个变量(这个很好理解,之前的我们说过父子进程是相互独立的,互相不受干扰的),但是问题出现在最后一列,我们惊奇的发现居然父子进程的g_val变量的地址居然是相同的,前面不是说父子进程的g_val不是同一个变量吗?这里为啥打印出来的地址会是相同的呢?

这里就说明我们打印出来的地址并不是真正的物理地址,我们语言层面打印出的地址叫做虚拟地址或者线性地址。我们在用C/C++语言所看到的地址,全部都是虚拟地址!而物理地址,用户一概看不到,由OS统一管理 。OS必须负责将虚拟地址转化成物理地址 。

2 进程地址空间

首先我们来讲一个故事:从前有一个企业家很有钱,他的家产大概有一亿美金左右的样子。他有4个私生子,并且这四个私生子互相并不知道对方的存在。第一个私生子是个学霸,在国内顶尖学校上学,这个富豪便对他说,你要好好读书,将来我这一亿美金全部都是你的;第二个私生子是一个三线演员,富豪便对他说,我帮你打开你红的渠道,你不要辜负了我对你的期望,好好努力,将来我这一亿美金都是你的;第3个私生子是个女儿,当的是小学老师,富豪便对他说,你也不用太过努力工作,我就你这一个女儿,等我老了这一亿美金就是你的了;第四个私生子是一个初中的小混混,富豪对他说,你只要好好听我的话,这一亿美金就是你的了。

富豪给每个私生子都做出了承诺要将一亿美金给他们,但是实际富豪并没有那么多的钱给每个私生子一亿美金,而这一亿美金就是富豪给私生子们画的一张大饼,但是它的私生子们却信以为真。

9a1aef095c9d4c5191bf7684c15f321d.png

那这个故事与我们讲的知识有什么关系呢?其实操作系统就是那个富豪,私生子们就是一个一个的进程,而那一亿美金就是进程地址空间。

PS:我们在生活中要尽量少画饼。

3f638bd9dac24fdc9b39dd3431e4deff.png

操作系统给进程画了一张大饼,操作系统的资源是有限的,所以他就得要好好的把这张饼给管理起来,不让这些进程乱来,而如何管理呢?

那就要先描述,再组织,Linux中用的是一种叫做mm_struct的内核数据结构来管理的。

我们来用一张图带大家来看看程序地址空间:

a924bbcad38043cfb4ef8e280e2e07f2.png

这张图相信大家多多少少也不会陌生,在C语言的学习中我们也见到了很多次。

那么程序地址空间如何编码的呢?(32位的平台下虚拟地址空间大概是4GB)

ps:下面图每个小空格代表着一个字节。

e97d98d3ef804ac39d54e0eae16388d7.png

所以从这里我们也不难看出为啥虚拟地址也叫做线性地址。那么我们究竟是如何管理虚拟地址空间的每个区的呢?

我们可以用下面这种方式来描述管理:

struct mm_struct
{
long code _start;
long code _end;
long init _start;
long init _end;
…………
long brk _start;
long brk _end;
long stack _start;
long stack _end;
}

而_start和_end限定的区域就是叫做虚拟地址(线性地址)

那么问题来了,既然上面我们讲了那么多虚拟地址,真正的物理地址又在哪里呢?

我们画一个图方便大家理解:

e64638147a7848758501b952699ca895.png

通过这张图大家并不难发现,我们在语言层面上的地址是地址空间的虚拟地址,而虚拟地址要与物理地址建立映射,就需要一张页表(页表的工作原理我们将放到后面来讲)。

我们在学习C语言时大家在书上看到这样的一句代码:const char* str="hello world";

这时书上会告诉大家这句str指向的内容是只读的,不可修改的,但是这时为什么呢?这时我们就可以自己来分析分析:str指向的内容是在常量字符区,当常量字符区通过页表与物理地址建立映射时在页表中就将该数据设置为只读,当我们后续有修改操作时就会直接报错。

有了上面的基础我们就可以来解释解释为啥开头我们的g_val是同一个地址,但是指向的内容却不相同的问题了:

92f01c7b1a0c4375ade6943e81edd659.png

当不修改数据时就不会发生写时拷贝,父子进程指向的是同一块物理空间(为了节约资源);当要修改数据时就会发生写时拷贝,父子进程指向的是不同的物理空间,但是虚拟地址空间是相等的。

我们再来回答为啥fork会有两个返回值的问题就很容易了,就是因为父子进程的返回值是不同的,所以肯定会发生写时拷贝将不同的返回值用相同的虚拟地址来进行返回,虽然虚拟地址是相同的,但是他们通过页表建立映射的关系却是不一样的。

到目前为止,程序地址空间的基本内容已经ok,接下来给出一些扩展。

3 扩展

首先引出一个问题:假如没有程序地址空间,OS是如何工作的?

我们知道如果没有了地址空间,那么cpu将直接跟物理地址打交道,这样做的后果是什么?

我们不难知道假如cpu直接跟物理地址打交道的话那么当我们从cpu中读到非法地址时那就坏了,通过非法地址将我们程序中其他变量的值给修改了那不就扯淡了吗。所以我们要通过一层屏障来保护数据,而这一层保护就是通过程序地址空间来进行的,当我们访问的数据非法时通过页表的映射就会拒绝你的非法操作。

所以我们得出了程序地址空间的第一个好处:防止地址随意访问,保护物理内存和其他进程。

在向大家提出一个小问题:当我们在堆上new空间时OS是立马就把空间给你,还是等你需要的时候再给你?

这个问题大家应该都能够答对,与我们想得一样,OS会在我们需要该空间的时候再去在堆上申请。

22966145fc094f10b485ea8f19602bad.png

而页表暂时没有与物理内存建立映射关系称作页表中断,当我们需要空间的时候再与·物理内存建立映射。大家从这张图看出来没有,当我们通过页表建立映射时将进程管理与内存管理给解耦合了。我进程管理不需要关心你是怎样在内存上申请空间的,内存管理也不需要关心进程是如何管理起来的,这样下来维护成本就会变得更低,维护效率会更加高效一些。

所以我们得出了程序地址空间的第二个好处:将进程管理与内存管理进行解耦合。

再提出一个问题:程序在被编译的时候没有被加载到内存,那么程序内有没有地址呢?

答案是有的。源代码再被编译的时候就是按照虚拟地址空间的方式将对应的代码和数据进行编制,编译器也会遵守虚拟地址的规则。

当我们把程序加载到内存,程序里保存的地址(虚拟地址,并不是程序本身在内存中的物理地址)就会被cpu读取,cpu通过虚拟地址找到对应的虚拟地址空间,然后虚拟地址空间又通过页表映射到物理内存中,这样就将程序的整个运转给联系起来了。

所以我们得出了程序地址空间的第三个好处:可以让进程以统一的视角看待自己的代码和数据。


目录
相关文章
|
7月前
|
存储 Linux API
【Linux进程概念】—— 操作系统中的“生命体”,计算机里的“多线程”
在计算机系统的底层架构中,操作系统肩负着资源管理与任务调度的重任。当我们启动各类应用程序时,其背后复杂的运作机制便悄然展开。程序,作为静态的指令集合,如何在系统中实现动态执行?本文带你一探究竟!
【Linux进程概念】—— 操作系统中的“生命体”,计算机里的“多线程”
|
6月前
|
存储 Linux iOS开发
【Linux】冯诺依曼体系与操作系统理解
本文深入浅出地讲解了计算机体系的两大核心概念:冯诺依曼体系结构与操作系统。冯诺依曼体系作为现代计算机的基础架构,通过中央处理器、存储器和输入输出设备协同工作,解决了硬件性能瓶颈问题。操作系统则是连接硬件与用户的桥梁,管理软硬件资源,提供运行环境。文章还详细解析了操作系统的分类、意义及管理方式,并重点阐述了系统调用的作用,为学习Linux系统编程打下坚实基础。适合希望深入了解计算机原理和技术内幕的读者。
147 1
|
2月前
|
监控 Linux 开发者
理解Linux操作系统内核中物理设备驱动(phy driver)的功能。
综合来看,物理设备驱动在Linux系统中的作用是至关重要的,它通过与硬件设备的紧密配合,为上层应用提供稳定可靠的通信基础设施。开发一款优秀的物理设备驱动需要开发者具备深厚的硬件知识、熟练的编程技能以及对Linux内核架构的深入理解,以确保驱动程序能在不同的硬件平台和网络条件下都能提供最优的性能。
103 0
|
4月前
|
Java 关系型数据库 MySQL
在Linux操作系统上设置JDK、Tomcat、MySQL以及J2EE后端接口的部署步骤
让我们总结一下,给你的Linux操作系统装备上最强的军队,需要先后装备好JDK的弓箭,布置好Tomcat的阵地,再把MySQL的物资原料准备好,最后部署好J2EE攻城车,那就准备好进军吧,你的Linux军团,无人可挡!
111 18
|
4月前
|
开发框架 关系型数据库 Java
Linux操作系统中JDK、Tomcat、MySQL的完整安装流程以及J2EE后端接口的部署
然后Tomcat会自动将其解压成一个名为ROOT的文件夹。重启Tomcat,让新“植物”适应新环境。访问http://localhost:8080/yourproject看到你的项目页面,说明“植物”种植成功。
120 10
|
9月前
|
算法 Linux 调度
深入理解Linux操作系统的进程管理
本文旨在探讨Linux操作系统中的进程管理机制,包括进程的创建、执行、调度和终止等环节。通过对Linux内核中相关模块的分析,揭示其高效的进程管理策略,为开发者提供优化程序性能和资源利用率的参考。
255 1
|
7月前
|
Linux
Linux 操作系统
在 Linux 中,UID(用户 ID)是标识用户身份的重要概念。UID 唯一标识每个用户,通过 UID 可区分不同用户类别:UID 0 为超级用户,1-999 为系统用户,1000 及以上为普通用户。因此,正确选项为:UID 标识用户、可区分用户类别、普通用户 UID 大于 1000。
|
7月前
|
NoSQL Unix Linux
Linux 操作系统的诞生与发展历程
步探索与准备: 1991年初,林纳斯·托瓦兹开始在一台386sx兼容微机上学习minix操作系统。通过学习,他逐渐不能满足于minix系统的现有性能,并开始酝酿开发一个新的免费操作系统。
209 8
Linux 操作系统的诞生与发展历程
|
7月前
|
运维 自然语言处理 Ubuntu
OS Copilot-操作系统智能助手-Linux新手小白的福音
OS Copilot 是阿里云推出的一款操作系统智能助手,专为Linux新手设计,支持自然语言问答、辅助命令执行和系统运维调优等功能。通过简单的命令行操作,用户可以快速获取所需信息并执行任务,极大提升了Linux系统的使用效率。安装步骤简单,只需在阿里云服务器上运行几条命令即可完成部署。使用过程中,OS Copilot不仅能帮助查找命令,还能处理文件和复杂场景,显著节省了查找资料的时间。体验中发现,部分输出格式和偶尔出现的英文提示有待优化,但整体非常实用,特别适合Linux初学者。
365 10
|
8月前
|
缓存 安全 Linux
Linux系统查看操作系统版本信息、CPU信息、模块信息
在Linux系统中,常用命令可帮助用户查看操作系统版本、CPU信息和模块信息
1292 23