zoukankan      html  css  js  c++  java
  • 应用程序的装载与运行

    现在有一台计算机,它的CPU 是X86,CPU有如下功能:

    1. CPU可以从内存中读取汇编指令并执行

    2. CPU可以根据提供给它的内存地址,去地址指向的内存空间取得在那里的指令执行

    内存地址:

    内存,正如它名字所言,是存储器,用来存储数据和指令;为了方便CPU找到特定的数据或者指令,内存被按字节标注了地址。为了方便理解,我们可以将内存想象为一个宾馆,每个房间都有一个门牌号码,根据这个门牌号码就可以很容易地找到对应的房间。

    我们知道,没有软件的计算机就是一堆废铁,为了让计算机对我们有用,我们需要在其上运行软件。我们已经知道CPU的功能(或者说限制),怎么才能让这个计算机运行软件呢?

    我们首先需要做的就是将应用程序从磁盘中拿出放在内存中,我们把这个过程称为“加载”。那怎么放呢?我们知道一个应用程序包含很多数据和指令,在内存中怎么安排它们呢?

    容易想到的最简单的方式就是,从内存首地址开始,逐个地安排应用程序的指令和数据。打个比方,我们将应用程序看做一个旅行团,其中的指令和数据就是团员。当第一个旅行团到达的时候,我们从 1号房间开始,将旅行团成员逐个安排到相应的房间;在前一个旅行团离开之前,宾馆不再接待其他旅行团,直到这个旅行团离开,才可以接待下一个旅行团。

    这正是早期操作系统的实现方式。

    我们很容易就可以看出这种方式的弊端:

    1. 浪费资源。明明宾馆(内存)还有很多空房间,却不能接待更多旅行团。

    2. 浪费服务能力。不管旅行团成员是1个还是100个,都是这么多服务人员(CPU)。

    为了避免这些浪费,更好地利用资源和服务能力。产生了多任务操作系统,可以同时运行多个任务,相当于宾馆现在可以同时接待多个旅行团了。

    现在又遇到新的问题:如何有效地安排多个旅行团的住宿?

    假设,我们的宾馆有128个房间,旅行团A有10个团员,旅行团B有100个成员,旅行团C有20个成员,按照先后顺寻,旅行团A到达后,将1到10号房间分配给它;旅行团B到达后,就将11到110号房间分配给它;现在剩下18个空房间了。这时如果旅行团C到达了,房间就不够住了,怎么办呢?只能先将其他的旅行团赶出去。我们看到如果将旅行团A赶出去,会空出10个房间,加上原来空余的18个房间,足够容纳旅行团C,但是,旅行团C提出抗议:我们要住在相邻的房间,不能把我们分开!(程序运行所需的空间是连续的)这样,赶出旅行团A的方案不可行,只能将旅行团B赶出,将如此庞大的一个旅行团赶出,必然会遇到很大的困难,需要做的工作也很多,这是第一个问题。

    第二个问题是,同时接待多个旅行团,免不了会遇到一些旅行团行为不端,会溜到别的旅行团房间窃取财物(恶意程序访问其他程序的内存数据),或者有些成员不小心走错房间,误入其他旅行团的房间,引起一些不必要的麻烦(bug导出访问其他程序的内存地址)。

    第三个问题,旅行团成员之间会有很多的交流,如果同一个旅行团多次住宿同一个宾馆,但所住的房间每次都不一样,在交流的时候就需要每次都重新查找每个成员的房间号,比较麻烦。

    怎么解决这些问题呢?

    作为服务的提供者,宾馆的经营者,我们一定要想客户所想,急客户所急,我们要设身处地地想一下,如果我们自己是一个旅行团,我们期望的宾馆是什么样的?

    如果每次来住的时候,整个宾馆只住我一个旅行团该多好啊。不用担心其他恶意的旅行团窃取财物;不用担心不小心走错房间误入我们房间的其他旅行团成员;每次都能住进同一个房间,团员交流的时候不必要重新查找每个成员的房间号

    当然,在满足客户需求的前提下,怎么能最大程度地利用我的宾馆资源,也是我们很关心的问题,比如说,旅行团C来了后,能不能不用将B全部赶出去?

    怎么办?我们来一起解决这些问题。

    如何防止恶意旅行团或者不小心走错房间的其他旅行团?

    能不能将每个旅行团隔离开?比如说,旅行团A安排在一层;旅行团B安排在2层;层与层之间没有通道相连。这样任何一个旅行团都没有机会接触到其他旅行团。

    那么针对内存怎么实现这样的隔离呢?

    我们可以设置一个联络人,然后规定任何人要联系其他人,都必须经过联络人。比如说,张三要联系1003房间的李四,就告诉联络人,联络人根据张三给的地址去把李四找来。

    现代的CPU就是这样设置了一个联络人(MMU – Memory Management Unit),它是CPU的一个内置硬件单元,因此,任何应用程序都无法绕过这个联络人,直接与特定地址的内存单元联系。

    因为有了这个联络人,实际上,李四不必非得在1003房间了,比如根据实际情况,我们可能将李四安排在2002房间,只要联络人当其他人要找1003房间的时候,实际需要去2002房间找到李四就行了。这样,联络人可以建立一个通讯录:

    对外公布的房间号

    实际房间号

    姓名

    1003

    2002

    李四

    1004

    2013

    王五

    1005

    3121

    赵六

         

    这样一来,旅行团可以给每个成员编一个固定的号码,任何时候,都可以通过这个固定的号码找到他,而不管他每次住在哪个房间。

    同时,联络人只负责联系本团成员,任何时候,你给联络人的地址,联络人都会认为是本团成员的地址,也只会在本团成员中找,而不会找到别的旅行团房间去。

    同样,因为有了联络人,旅行团不必在意宾馆到底有多少房间,只要按照自己的需要给每个成员一个编号就可以了。比如,宾馆可能有100个房间,但旅行团成员可能有150人,我们依然可以给150人每人一个编号,而不用管宾馆到底有多少房间(当然,实际住宿的时候会有些麻烦,但不影响编号)。这样,我们就有了一个计算机中很重要的概念:虚拟地址空间。简单地理解,虚拟地址空间就是旅行团所能容纳的成员数。相当于,我们组织一个旅行团的时候,宣布这个旅行团可以带1000个成员,然后我们就创建了1000个编号。实际上可能大部分编号都没用,但只是一个编号也没什么浪费。在这个范围内,领队可以给每个成员一个编号,也可以通过编号找到每个成员并与之联系。相应的,我们说32位CPU能支持的最大虚拟地址空间是4G,就是说,CPU最多可以找到4G的地址,并操作这些地址所标识的内容。

    这样,我们就算解决了应用程序,或者说旅行团,之间的隔离问题。

    接下来的问题是,如何在满足客户的前提下,最大化宾馆经营方的利益,即资源(CPU,内存)利用率最大化。

    我们前面提到,旅行团C的到来,导致B,最大的旅行团,不得不被暂时赶出去,尽管C只有20人,却导致了一个100人团队的重新安置,怎么能避免这种浪费呢?

    我们能不能跟旅行团商量一下,让他们不要一次性都住进来呢?比如,旅行团B来的时候,我们只安排其中10人,或者20人先入住,其他人视情况再做安排,这样,C来的时候,可能需要重新安置的就只有20人,而不是100人了。我们跟各个旅行团商量后,惊喜地发现,旅行团的成员没必要一次性都住进来(同样,一个应用程序没必要一次性都加载进内存),因为旅行团的成员不是同时活动,因此,我们可以只安排活动的那部分成员入住,当其他成员需要活动时,可能以前活动的部分已经不再活动了,就可以把以前入住的成员先挪出去,让后来的成员入住。同时,我们发现,往往是编号靠近的成员一起活动(程序的局部性),比如1-10号成员一起活动,11-20号成员一起活动,这样,我们就按照每10个成员一组来分组安排住宿,下一组成员需要的时候,就把上一组成员整体挪出去重新安置。

    这种机制在计算机的内存管理中称为“分页”。在Intel CPU中将每页划分为 4K或者4M,而32位的CPU可以管理4G的地址空间,按照4K每页的话,即1048576页。

    现在我们会问,CPU是怎么知道该安排哪个组住进来呢?我们引入一个新的概念—页错误。虽然,每个组内部的联系会更紧密一些,但组之间也不是完全没有联系,当前一个组要联系后一个组的时候,可能后一个组还没有被安排住进宾馆,这个时候,CPU就会报告找不到这个组,我们把这种情况称为页错误。操作系统得知发生页错误后,就从磁盘上找到相应的页,并把它读出来,放入内存。

    映射

    因为不必考虑宾馆的实际房间号,我们给每个旅行团的成员编号都可以从1号开始。但1号成员,或者说第一页(第一组)的实际房间号都是多少,是入住后才能知道的,一旦入住,就唯一确定了每个成员编号与真实房间号的对应关系,这称为虚拟地址空间到物理地址空间的映射。除了这个映射,还有一个是从磁盘到虚拟地址空间的映射。我们可以将处于磁盘上的应用程序看做尚未组团出发的旅行团,这个时候,每个成员还没有被旅行团编号,但是每个成员都有自己的身份证号码,一旦组团出发(应用程序启动),就回给每个成员一个旅行团自己的编号,这个编号跟身份证号码也可以一一对应起来,这样就构成了从磁盘到虚拟地址空间的映射。

    以上的整个过程就是应用程序的加载过程。我们下面总结一下:

    1. 创建一个独立的虚拟地址空间

    创建1000个编号,并将编号与宾馆房间号对应起来

    2. 建立虚拟地址空间和可执行文件的映射

    将编号和身份证号码对应起来

    运行

    应用程序加载到内存后,就可以运行了。操作系统此时知道这个的入口函数所在的内存地址,就将指令寄存器(有的也叫程序计数器)的值设置为这个内存地址。CPU读取这个寄存器的值,从中取得这个地址,然后通过这个地址找到入口函数对应的汇编指令,执行。CPU在取指令的同时,会将这个指令的下一条指令的地址放在指令寄存器(EIP)中,执行完当前指令后,CPU就会再次从EIP中得到下一条指令,如此往复,知道程序结束运行。

    打个比方,旅行团的活动必须由领队发起,因此,活动运营方必须知道领队的房间号码,于是一开始,就将领队的房间号码由一个调度人员(EIP)保存,在通知领队活动的同时,活动运营方告知下一个团员准备活动(将其房间号码告知调度人员),这个调度人员每次只保存下一个活动人员的房间号。

    这就是一个应用程序的运行过程。

    参考:

    程序员的自我修养链接、装载和库

    深入理解计算机系统

  • 相关阅读:
    【精品软件】小蔡电脑助手 V2.0
    C++ 数据结构与算法(二)线性表之单链表
    C++ 数据结构与算法:冒泡排序及改进算法
    C++ 数据结构与算法(四)线性表之循环链表
    const int *p、int*const p、和 int const *p 详解
    C++ 指针和引用的区别
    C++ 数据结构与算法(三)线性表之双向链表
    C++ 数据结构与算法(一)线性表之顺序表
    C++数据结构与算法:选择排序
    VC++ 获取文件夹大小
  • 原文地址:https://www.cnblogs.com/lbsong/p/3134902.html
Copyright © 2011-2022 走看看