计算机系统漫游
Hello程序的生命周期
- Hello Program
hello.c
#include <stdio.h>
int main()
{
printf("hello, world\n");
return 0;
}
- 编译
生成可执行文件hello
linux> gcc -o hello hello.c
为什么要理解编译系统的运行?
- Optimizing program performance (优化程序性能)
- Understanding link-time errors (理解链接时出现的错误)
- Avoiding security holes (避免安全漏洞)
- 运行
目前,hello程序已经放入了磁盘。在linux系统上运行可执行程序。
打开shell程序,在shell程序中输入文件名。
linux> ./hello
注:shell是一个命令解释程序,它输出一个提示符 > 来等待一个命令行的输入,然后执行这个命令。如果命令行的第一个单词不是内置的 shell 命令,那么 shell 就会假设这是一个可执行文件的名字,对这个文件进行加载并运行。
在这个例子中,shell加载并且运行 hello 程序,屏幕上显示 hello, world内容。hello程序运行结束并退出。shell 继续等待下一个命令的输入。
在介绍 hello 程序运行之前,先看一下计算机系统的硬件组成。
中央处理单元(Central Processing Unit, CPU),也称处理器。
PC(program count),实质上是一个大小为一个字的存储区域。(一个字是多大呢?对于32位的机器,一个字是4个字节。对于64位的机器,一个字就是8个字节)。里面存放的是某一条指令的地址。处理器在不断地执行PC指向的指令,然后更新PC,使其指向下一条要执行的指令。(这个下一条指令与刚刚执行的指令不一定是相邻的)。
寄存器文件,它就是CPU内部的一个存储设备。寄存器文件是由一些单字长的寄存器构成,每个寄存器都有自己唯一的名字。寄存器可以理解为一个临时存放数据的空间。
ALU(Arithmatic/logic Unit)。
主存,也称为内存。处理器在执行程序时,内存主要存放程序指令以及数据。从物理上将,内存是由随机动态存储器芯片组成。从逻辑上讲,内存可以看成一个从零开始的大数组。每个字节都有相应的地址。
总线,内存和处理器之间通过总线来进行数据传递。它负责将信息从一个部件传递到另外一个部件,通常总线被设计成传送固定长度的字节块,也就是字(word)。至于这个字到底是多少字节,各个系统中是不一样的。
输入/输出设备。
接下来,看看hello程序执行时,到底发生了什么。
首先,通过键盘输入"./hello"的字符串,shell 程序会将输入的字符逐一读入寄存器,处理器会把 hello 这个字符放入内存中。输入完成后,按下回车键时,shell 程序就知道我们已经完成了命令的输入。然后执行一系列指令来加载可执行文件 hello ,这些指令将 hello 中的数据和代码从磁盘复制到内存,数据就是我们要显示输出的"hello, world\n"。这个复制的过程将利用 DMA (Direct Memory Access) 技术,即数据可以不经过处理器,从磁盘直接到达内存。当可执行文件 hello 中的代码和数据被加载到内存中,处理器就开始执行 main 函数中的代码。CPU 会将"hello, world\n"这个字符串从内存复制到寄存器文件,然后再从寄存器文件复制到显示设备。最终 hello, world 显示在屏幕上。
存储
从 hello 程序执行的过程来看,系统即使执行如此简单的程序,数据信息仍旧需要在磁盘、内存、处理器以及 IO 设备之间进行搬运。数据从一个地方搬运到另外一个地方需要花费时间,系统设计人员的一个主要任务就是缩短信息搬运所花费的时间。
通常情况下,大容量的存储设备的存取速度要比小容量的慢。运行速度更快的设备的价格相对于低俗设备要更贵。
整个计算机系统的信息存储可以用一个层次结构来表示。从这个层次结构来看,从上到下,设备的访问速度越来越慢,容量越来越大,每字节的造价也越来越便宜。这个层次结构的主要思想是,上一层存储设备是下一层存储设备的高速缓存。程序员理解计算机中的高速缓存,可以利用存储器的层次结构板来提升程序的性能。
操作系统
无论是 shell 程序还是 hello 程序都没有直接访问键盘、显示器、磁盘这些硬件设备。真正操纵硬件的是操作系统,其可以看成是程序和硬件之间的中间层,所有应用程序对硬件的操作必须通过操作系统来完成。
目的:
- 防止硬件被失控的应用程序滥用;
- 提供统一的机制来控制这些复杂的底层硬件。
进程
进程切换
假设示例场景中只有两个并发的进程,shell 进程和 hello 进程。
最开始,只有 shell 进程在运行,即 shell 在等待命令行的输入,当我们通过 shell 进程加载 hello 进程时,shell 进程通过系统调用来执行我们的请求。系统调用会将控制权从 shell 进程传递给操作系统,操作系统保存 shell 进程的上下文,然后创建一个 hello 进程及其上下文,将控制权交给新的 hello 进程。Hello 进程执行完之后,操作系统会恢复 shell 进程的上下文,并将控制权交给 shell 进程。
注:上下文,操作系统会跟踪进程运行中所需要的所有状态信息。例如,当前PC和寄存器的值,以及内存中的内容等等。
线程
现代操作系统中,一个进程实际上由多个线程组成。每个线程都运行在进程的上下文中,共享代码和数据。
虚拟内存
它为每个进程提供了一个假象,就是每个进程都在独自占用整个内存空间。每个进程看到的内存都是一样的,称之为虚拟地址空间。
文件
网络
使用 ssh 运行 hello 程序
阿姆达尔定律
当我们对系统的某一部分进行加速时,被加速部分的重要性和加速程度是影响整个系统性能的关键因素。
在加速前,假设一个应用程序的执行所需要的全部时间用来表示。为了方便描述,我们可以笼统的将这个程序分为两部分。一部分是不可加速的,另一部分是可加速的。其中可以加速的部分执行花费的时间为,不可加速部分的执行时间为。程序经过优化后,可加速部分性能提升比例为K。那么经过加速后,这个可加速部分所花费的时间就是。即:
当时,。当k趋于无穷大时,。
因此,我们需要把系统的性能提高2倍或者更多,只有通过优化大部分的组件才能获得。
如何获得更高的计算能力呢?可以通过三种途径。
- Thread-Level Concurrency (线程级并发)
- Instruction-Level Parallelism (指令级并行)
- Single-Instruction Multiple-Data Parallelism (单指令多数据并行)
Multi-core Processor Organization
超线程(hyperthreading)
超线程,也称为同时多线程。如果每个CPU核心可以执行两个线程,那么四个核心就可以并行执行8个线程。在CPU内部,像程序计数器和寄存器文件这样的硬件部件有多个备份,而像浮点运算部件这样的硬件还是只有一份,常规单线程处理器在做线程切换时,大概需要20000个时钟周期。而超线程处理器可以在单周期的基础上决定执行哪一个线程,这样一来,CPU可以更好地利用它的处理资源,当一个线程因为读取数据而进入等待状态时,CPU可以去执行另外一个线程,其中线程之间的切换只需要极少的时间代价。
指令级并行
现代处理器可以同时执行多个指令的属性称为指令级并行,每条指令从开始到结束大概需要20个时钟周期或者更多,但是处理器采用了非常多的技巧可以同时处理多达100条指令。因此,近几年的处理器可以保持每个周期2~4条指令的执行速率。
单指令多数据
现代处理器拥有特殊的硬件部件,允许一条指令产生多个并行的操作,这种方式称为单指令多数据。SIMD的指令多是为了提高处理视频、以及声音这类数据的执行速度。比较新的Intel以及AMD的处理器都是支持SIMD指令加速。