第一章:计算机系统漫游

// hello.c
#include 
int main()
{
    printf("hello, world\n");
}

学过C语言的人都知道,上面这小段程序便是你初次与C语言打招呼的用语。我们将通过跟踪它的生命周期(从被你创建,到在系统上运行,输出简单的消息,最后终止)来对计算机系统进行学习。

01程序的创建

程序员通过编辑器创建hello源程序(源文件),并保存为文本文件hello.c。源程序中每个字符都由一个字节(8 bits)表示,每个字节对应一个ASCII码值(整数),比如字符'a'对应的ASCII码值为97,因此源程序是以字节序列(本质上也就是0,1序列)的方式储存在文件中。

其实,计算机系统中的所有信息都是由一串比特(bit)表示的,因为计算机用高、低电平分别表示1、0。因此,我们只能根据数据的上下文来区分不同的数据对象。在不同的上下文中,同样的字节序列可能表示一个整数、浮点数、字符串或机器指令。信息 = bits + 上下文

hello.c 程序创建完成后,为了能够在系统上运行,每条C语句还必须转化成机器能够读懂的低级机器语言指令,进而得到能够在系统上运行的可执行目标程序hello

image.png

02编译系统

gcc编译器驱动程序将源文件hello.c 翻译为可执行目标文件hello,该翻译过程是分四个阶段完成的,如下图所示。


编译系统

注意,汇编语言是人可读可编辑的低级语言,也是能够直接控制计算机硬件的语言,汇编语言与机器语言之间是一对一的关系,每条汇编指令对应一条机器指令,e.g.
mov ax,bx 汇编指令 将寄存器bx的内容送到ax中
1000100111011000 机器指令

03运行可执行文件

为了在Unix系统上运行可执行文件,我们将它输入shell应用程序中:
unix> ./hello
hello, world
unix>
等待你输入可执行程序的文件名后,shell会加载和执行该程序,屏幕上显示hello, world,然后程序运行结束并退出,shell继续等待下一个命令输入。

接下来我们详细分析hello程序运行过程中发生了什么?

  • 首先shell程序等待我们输入命令,当在键盘上输入字符串./hello后,shell程序就逐一读取字符到寄存器,再把hello这个字符串放入内存。
    第1步
  • 当我们敲下回车键时,shell知道我们已经结束了输入,然后执行一系列指令将hello目标文件中的代码和数据从磁盘拷贝到内存,从而加载hello文件。数据就是最终要输出的字符串"hello, world\n"。该拷贝过程利用DMA(Direct Memory Access)技术,数据可以不经过处理器,直接到达内存。


    第2步
  • 一旦hello目标文件中的代码和数据加载到了内存,处理器就开始执行hello程序的主程序中的机器指令。这些指令将“hello, world\n”中的字节从内存拷贝到寄存器,再从寄存器拷贝到显示设备,最终显示在屏幕上。
    第3步

    从hello程序的执行过程中可以发现,系统花了大量时间把数据信息从一个地方挪到另一个地方,从而减慢了程序的实际工作。因此系统设计者的一个主要目标就是使这些拷贝操作尽可能快

04高速缓存

对于处理器而言,从寄存器读取数据要比从内存读取差不多快100倍,针对两者之间的差异,系统设计者引入了更小更快的存储设备——高速缓存(cache momories),它们被用来作为暂时的集结区域,存放处理器在不就的将来可能会需要的信息。

整个计算机系统的信息存储可以用一个层次结构表示,通常来说,存储容量越小,速度越快,价格越高。上一层存储器是下一层存储器的高速缓存。


存储器层次模型

05操作系统

无论是shell还是hello程序,都没有直接访问I/O设备或者内存,因为它们依靠操作系统提供服务。所有应用程序对硬件的操作都必须通过操作系统来完成。


image.png

这样设计的目的在于:

  1. 防止硬件被失控的应用程序滥用
  2. 操作系统为应用程序提供统一的机制来空着复杂不同的底层硬件
    操作系统通过几个基本的抽象概念(进程、虚拟内存和文件)来实现这两个功能。
操作系统为应用程序提供的假象——进程

hello程序在系统上运行时,操作系统会提供一种假象,好像系统上只有这个程序在运行,程序看上去独占地使用处理器、主存和I/O设备。这些假设是通过进程来实现的。
在一个系统上可以同时运行多个进程,而每个进程都好像在独占地使用硬件,我们称之为并发运行,实际上是一个进程的指令和另一个进程的指令交错执行,操作系统实现这种交错执行的机制称为上下文切换
操作系统保存进程运行所需的所有状态信息,就称为上下文,比如程序计数器PC,寄存器的当前值,主存的内容。
注意,在任何一个时刻,单处理器系统都只能执行一个进程的代码。

进程的上下文切换

最开始,只有shell进程在运行,等待命令行的输入。当我们让它运行hello程序后,shell进程通过系统调用执行我们的请求,系统调用将控制权从shell进程传递给操作系统。操作系统保存shell进程的上下文,创建一个新的hello进程及其上下文,然后将控制权传给新的hello进程。在hello进程终止后,操作系统恢复shell进程的上下文,并将控制权传回给它,它会继续等待下一命令行的输入。

进程之间的切换是通过操作系统内核(kernel)管理的。内核是操作系统代码中常驻主存的部分,是系统管理全部进程所用代码和数据结构的集合。
当应用程序需要操作系统的某些操作,比如读写文件时,它就是执行一条特殊的系统调用指令,将控制权传给内核,然后内核执行被请求的操作并返回应用程序。

在现代系统中,一个进程实际上可以由多个线程组成,每个线程都运行在进程的上下文中,共享代码和全局数据。

操作系统为进程提供的假象——虚拟内存

操作系统为每个进程提供了一种假象,好像它们都在独占地使用主存,每个进程看到的内存都是一致的,称之为虚拟地址空间

Linux的虚拟地址空间

如图所示为Linux的虚拟地址空间,地址从下往上逐渐增大,每个区都有专门的功能。

  • 程序代码和数据。对于所有进程,代码都是从同一固定地址开始的,紧接着是C全局变量对应的数据区。代码和数据区都是从可执行目标文件中加载进来的。
  • 运行时堆。程序代码和数据在进程一开始运行时就被指定了大小,而堆可以在运行时动态地扩展和收缩,比如运行时调用malloc申请的内存空间就在堆中。
  • 共享库。存放C语言中的标准库和数学库这样的共享库的代码和数据,比如printf函数。
  • 用户栈。实现函数调用,每调用一个函数,栈就会增长,每次从函数返回时,栈就会收缩。栈和对一样,在程序执行时可以动态地扩展和收缩。值得注意的是,栈的增长方向是从高地址低地址
  • 内核虚拟内存。内核是操作系统总是驻留在内存中的部分。应用程序不允许读写这个区域的数据,也不能直接调用内核代码定义的函数。
文件——对I/O设备的抽象

文件只不过是字节序列,所以所有的I/O设备,比如键盘,磁盘,显示器,甚至网络,都可以看成文件。系统中的所有输入输出都是通过读写文件来完成的。

06系统之间通过网络进行通信

现代系统之间通过网络连接在一起,从一个单独的系统来看,网络可别视为一个I/O设备,如图所示,当系统从主存拷贝一串字符到网络适配器时,数据流经过网络到达另一条机器,相似地,系统也可以读取从其他机器发送来的数据,并将数据拷贝自己的主存。


网络也是一种I/O设备

参考:
Datawhale 开源 408 计划——《深入理解计算机系统》
什么是汇编语言

你可能感兴趣的:(第一章:计算机系统漫游)