CSI-I:计算机系统漫游-由hello程序所联想到的问题

来源:互联网 发布:生产流程管理优化 编辑:程序博客网 时间:2024/05/05 09:58

前言

         这章内容是对整本书内容的概括和描述,你可以通过本篇文章快速的了解本书所涉及的内容,了解学习书中所列内容的必要性。之后你就会明白学习本书其实是非常有趣的历程,你会知道计算机是怎么工作,自己的写的程序是如何工作的,以及对如何对程序进行优化,同时对进程、线程、虚拟存储器也会有更加清楚的认识。

    其次,我要说明的是每章内容并没有必要的联系,所以你完全可以随心所欲地阅读自己感兴趣的领域。好了,言归正传,开始我们的漫游吧!

由helloworld联想的问题

  首先我写个程序

 #include<stdio.h>

 int main()

 {

      Printf(“hello,world!”);

 }

这是大家刚学编程时都会写的一段代码,现在我们就从这个简单的程序说起。根据这个程序请容许我提出几个问题:

1.这个程序编译成可执行程序需要哪些步骤,各步骤都做了哪些事?

2.这个程序从加载执行到完成到底在计算机中经历了什么?

3.这个程序在执行过程中的进程状态是怎么样的? 

第一个问题,相信很多人会很轻松的给出回答,请看下图

             

这个过程需要四个主要的步骤,我做一个简单的总结:

1) 预处理阶段.预处理器根据以字符#开头的命令,定义宏等修改原C程序,得到另一个C程序 ,通常以.i作为拓展名hello.i。

2) 编译阶段。编译器将文本文件hello.i编译成文本文件hello.s,即汇编语言程序。这里的汇编程序是对我们写的C程序的低层翻译。

3) 汇编阶段。汇编器将hello.s翻译成机器语言指令,把这些指令打包成一个叫做可重定位目标程序的格式,并将结果保存在hello.o中,hello文件是一个二进制文件,而不再是字符。

4) 链接阶段。在程序中使用的printf函数,并不是我们写的。printf存在于一个单独编译好的printf.o文件中,而这个文件必须合并到我们的hello.o中,连接器就负责处理这种合并。结果就得到hello程序

 

第二个问题,如果很清楚计算机的组织结构,相信你也能很快的给出答案。在我们用键盘上敲下”./hello”后,外壳程序(shell,dos)执行将它逐一读取到寄存器中,再放到存储器中。当我们敲下回车后,外壳程序知道我们命令输入完毕,然后外壳程序执行一系列指令来加载可执行的hello文件,将hello目标文件中的指令和数据复制到内存中,其中就用到了DMA的存取技术,一旦目标文件中的指令和数据加载到内存,处理器就开始执行hello程序main中的机器语言指令,这些指令将”hello,world!”字符复制到寄存器,再从寄存器复制到显示设备,最终显示在屏幕上。下面给出几幅图来直观地展示该过程。

                                                       


                                                                 


                                                                 

   不过,我们从上面的过程中可以看到系统在执行一个程序时,花费了大量的时间把数据从一个地方移动到另一个地方。Hello程序开始存放在磁盘上,随后别加载到内存,然后再到处理器执行。从我们编程的角度来说,这些复制就是时间的开销,减缓了真正的工作。因此,系统设计者的一个主要目标就是使这些复制操作更快完成。

   我们都知道从内存中读取数据要比从磁盘中读取数据快上很多,而从寄存器中读取要比从内存快很多。而且处理器与内存之间的速度差异越来越大,针对这些差异,系统设计者们采用了一种叫做高速缓存存储器的存储设备,作为暂时的数据存放,这些数据可能是处理器近期需要访问的。而高速缓存正是利用了程序的局部性原理,即一段时间内处理器访问的只是一个程序片段的局部数据和代码。意识到这一点很重要,因为应用程序员可以利用高速缓存将他们的程序性能提高一个数量级。

                  

其实,高速缓存只是计算机存储系统结构的一部分,每个计算机系统中的存储设备都被组织成了一个存储器层次结构,如下图所示,在这个层次结构中,从上至下,设备访问速度越来越慢,容量越来越大。

                    

    第三个问题,这里涉及到计算机系统中一个很重要的概念–进程。你可以把进程看做是程序的一个容器,容器里包括了程序执行所必须的资源,如处理器,主存和I/O设备,以及程序本身的数据。每执行一个程序,系统就为程序创建一个这样的容器,并赋予不同的编号,而每个程序都觉得自己在独自地享用系统资源:-,直到程序执行完毕退出后,系统才把该容器的资源收回。可是,如果有多个程序同时都在运行,那作为系统是如何管理这些容器?这时候系统会跟踪和保存它们的状态信息,即上下文信息。有了这些上下文信息后,系统就可以在容器间灵活地进行切换。

    在hello程序执行过程中,就有两个并发的进程:外壳进程和hello进程。起初只有外壳进程在运行,等待输入。当我们让它执行hello程序时,外壳通过一个专门的函数,即系统调用来执行我们的请求,系统调用会将控制权传递给系统,系统保存外壳进程的上下文,然后创建hello进程,然后将控制权转给hello 进程,hello进程执行完后,系统恢复外壳进程的上下文,并将控制权转给它,外壳系统继续执行等待输入。

                      

    其实在上面的场景中,我们知道进程是一个容器,但真正的执行单元并不是这个容器,而是另一个很重要的执行单元,即线程,线程存在于该容器,并可以有多个,他们共享容器中所有的资源和数据,所以在它们之间进行切换变得非常容易,所以显得线程比进程高效了很多。

虚拟存储器

     在第三个问题中,我们都知道了进程在执行中都自认为在独占着系统资源(处理器,内存等),而这只是系统提供的一个假象。每个进程看到的都是一致的存储器,称为虚拟地址空间。我们先看下它的结构,图中地址是从下往上增大.

                        

虚拟地址空间是由几个区构成的,下面我逐一介绍:

1) 程序代码和数据.对于所有的进程来说,代码是从同一固定的地址开始的,紧接着

是和C全局变量对应的数据位置。代码和数据区是直接按照可执行文件的数据初始化的。

2) 堆.代码和数据区后紧随着的是运行时堆。代码和数据区是在进程以开始是就被规定了大小,与此不同,当调用如malloc ,free这些C函数是,堆会动态地扩展和收缩。

3) 共享库。从字面意思上理解就是多个进程对该存储映射区域的共享,更直观的来说,比如多个进程使用同一个C函数,为了节省系统资源,这多个进程都会到共享存储区来加载这个函数,而不是每个进程都拷贝一份。

4) 用户栈。编译器用它来实现函数调用,调用的时候栈空间会增长,函数返回后栈空间会收缩。后面我们会详细介绍编译器是如何使用栈空间的。

5) 内核虚拟存储器。这部分区域是用来存放系统内核的,系统内核会在系统启动后常驻该区域,并且应用程序不能访问该区域的数据。


   可是,我们到底该怎么理解虚拟存储器?系统是如何使用虚拟存储器的呢?

   虚拟存储器的确是一个很抽象的概念,因为它并不是事实存在的东西。而是一种基于swapping技术,由外存和内存构成的存储机制。Swapping即存储交换技术,通过该技术能够实现外存和内存的数据交换,从而大大提高内存利用率。

    我们都知道对于32位的系统有2^32(4G)的寻址空间,而这个寻址空间只是逻辑上的,跟计算机的物理内存要区分开。那么程序进程是怎么执行的呢?其实这里就会用到虚拟存储技术,进程在一开始加载的时候,系统将虚拟存储地址空间划分成和物理内存同样大小的页面,这时会加载一部分页面到物理内存开始执行,等到需要某些页面的时候再去加载,其实这也是得益于程序的局部性原理,因为没必要加载所有的数据到内存中,指令一般都是顺序执行的。与此同时,在这个过程中会有一个地址翻译的硬件对逻辑地址到物理地址进行映射。

同样的,对于多个进程来说,执行的过程也是如此,系统通过合适的页面置换算法来保证多个进程共享的使用内存资源。

    Ok,关于虚拟存储器的内容我先讲这些,至于虚拟存储器的详细内容我会在后面的篇幅中再进行介绍。

    可见,这个hello程序的确涉及到这本书中的很多内容,但还有一小部分内容并没有涉及到,比如信息的表示和处理,程序机器级表示,以及程序优化、网络编程、并发编程等。当然,以上的所介绍的内容只是对本书部分内容的简单介绍,个人认为,本书和操作系统有一定的联系,但并不偏向操作系统,只是从程序的角度来讲解系统而不是纯粹的理论。

0 0