CSAPP之一:程序生命周期漫谈

来源:互联网 发布:json时间格式化 编辑:程序博客网 时间:2024/06/11 11:31

以下内容是基于《深入理解计算机系统》的读书笔记(第一章 计算机系统漫游),另有PPT版(有宏请放心启用,本博截图都来自于此),欢迎下载交流。

  这一章通过跟踪hello程序的生命周期来开始对系统的学习——从它被程序员创建,到在系统上运行,输出消息,然后终止。先留几个问题:

  • 编写的程序经过了怎样的过程才得以运行?
  • 程序是如何存储的?
  • 编译程序有哪些步骤?
  • 程序运行是什么样的过程?计算机的各硬件都是什么角色?
  • 操作系统扮演了什么样的角色?

1. 程序表示 信息就是“位+上下文”

首先隆重请出本章主人公——hello程序,它是以源码的形式出场的:

1    #include <stdio.h>23    int main()4    {5        printf(“hello, world\n”);6    }

那么它保存到文件的时候是什么样子的呢:

这里写图片描述

如果你手头上有一个高级文本编辑器,比如notepad++,就可以将这段文本转换为十六进制的格式。

源程序实际上是位(bit)序列,每个位要么是0,要么是1。8个位组成1个字节(byte)。每个字节可以表示一个整数,该整数对应ASCII标准的一个字符,从而构成一个“文本文件”。

空格和换行符分别用整数32和10表示,十六进制表示就是20和0A。
  
ASCII码表
  
系统中所有的信息——包括磁盘文件、存储器中的程序、存储器中存放的用户数据以及网络上传送的数据,都是由一串位表示的。区分不同数据对象的唯一方法是我们读到的这些数据对象时的上下文。

比如:位序列00100011,既可以表示数字35,也可以表示字符 ’#’,具体看所处的上下文如何解释它。


2. 编译程序 源代码需要被“翻译”成计算机能够执行的指令格式

进入Linux,打开Terminal,活动活动手指头吧:

这里写图片描述
  
话说, gcc –o hello hello.c这句话到底做了什么?

这里写图片描述
   

  1. 预处理阶段
    • 预处理器根据已字符#开头的命令,修改原始的C程序。
    • hello.c的第一行#include <stdio.h>告诉预处理器读取系统头文件stdio.h的内容,并把它直接插入到程序文本中,形成.i结尾的文件。
  2. 编译阶段
    • 编译器将文本文件hello.i编译成文本文件hello.s,它包含一个汇编语言程序,每条语句都以一种标准的文本格式确切地描述了一条低级机器语言指令。
    • 汇编语言为不同高级语言的不同编译器提供了通用的输出语言,例如C编译器和Fortran编译器产生的都是同样的汇编语言。
  3. 汇编阶段
    • 汇编器将hello.s翻译成机器语言指令,把这些指令打包成可重定位目标程序,并保存在目标文件hello.o中。
  4. 链接阶段
    • hello程序调用了printf函数,printf函数存在于一个名为printf.o的与编译好了的目标文件中,链接器负责将这个文件以某种形式合并到hello.o程序中,最终形成可执行目标文件,从而可以被加载到内存中由系统执行。

为什么要了解编译系统是如何工作的?
1. 优化程序性能
  一个switch语句是否总是比一系列的if-then-else语句高效的多?
  一个函数调用的开销多大?
  while循环比for循环更有效吗?
  指针引用比数组引用更有效吗?
  为什么只是简单地重新排列一下一个算数表达式中的括号就能让函数运行地更快?
  ……
2. 理解链接时出现的错误
  链接器报告它无法解析一个引用是什么意思?
  静态变量和全局变量的区别是什么?
  在命令行上排列库的顺序有什么影响?
  为什么有些链接错误到运行时才出现?
  ……
3. 避免安全漏洞
  缓冲区溢出错误是如何出现的?
  ……


3. 程序执行处理器读取并解释存储在存储器中的指令

编译好的程序运行一下吧:

这里写图片描述

这个过程“hello”君都经历了什么?

1. 输入字符串“./hello”后,shell将字符逐一读入寄存器,再存放到存储器中。

PPT里是有动画的,这里没办法了
  
2. 敲入回车时,shell执行一系列指令来加载可执行文件hello,将代码复制到主存。

利用直接存储器存取(DMA)技术,数据可以不通过CPU而直接从磁盘到内存
  
3. 处理器开始执行hello程序的main主方法中的机器语言指令,这些指令将“hello, world!\n”字符串中的字节从主存复制到寄存器文件,再从寄存器文件中复制到显示设备。


4. 存储层次 存储设备形成层次结构

高速缓存至关重要。意识到高速缓存存在的程序员甚至可以利用高速缓存将他们程序的性能提高一个数量级。

这里写图片描述
  
根据机械原理,较大的存储设备要比较小的存储设备运行得慢,而快速设备的造价远高于低速设备:

  • 论大小:磁盘比主存大1000倍;论速度:CPU读取一个字的时间开销磁盘比主存大1000万倍。

CPU中的高速缓存:

  • L1高速缓存的容量约数十kB(字节),访问速度与寄存器文件一样快。
  • L2高速缓存的容量约几百kB~几MB,访问时间比L1约慢5倍,比内存快约5~10倍,L2通过一条特殊的总线连接到处理器。
  • L1、L2甚至L3是用一种叫做静态随机访问存储器(SRAM)的硬件技术实现的。

计算机把大量的时间用于不同存储设备间的数据复制,因此将各存储设备划分为不同的层次,各层次的数据相应速度和容量大小不同,将数据根据数据量和执行需求置于不同的层次,从而大幅减少时间开销。
  
这里写图片描述


5. 硬件管理 程序执行过程中操作系统是如何管理硬件的

  操作系统可以看成是应用程序和硬件之间插入的一层软件,提供进程虚拟存储器文件等抽象概念。
  
这里写图片描述

操作系统有两个基本功能:

  • 防止硬件被失控的应用程序滥用;
  • 向应用程序提供简单一致的机制来控制复杂而又通常大相径庭的低级硬件设备。

当Shell加载和运行hello程序,以及hello程序输出自己的消息的时候,Shell和hello都没有直接访问硬件设备,而是依靠操作系统提供的服务。

1. 进程

在你边用Eclipse开发,边听歌的时候,操作系统会提供一种假象,好像Eclipse和听歌软件都是同时独立使用硬件一样。再打开office、记事本、炒个股,电脑真像三头六臂一样。

但是!~~~对于CPU的一个独立的核心来说,所有的程序都是流水处理的,然后通过不停的切换让你有了上边的错觉。就像小吃摊老板同时做6份炒饼一个道理。

每个正在运行的程序都抽象为一个进程。同时做6份炒饼的概念叫做并发,过程就像下图:

这里写图片描述

  操作系统保持跟踪进程运行所需的所有状态信息,这些状态信息就是“上下文” (上下文的英文是Context,鉴于实在没有合适的中文来表述这个词,建议直接记住英文,只可意会不可言传,见几次就知道啥意思了)。
  还是说那做炒饼的师傅,同时做了6份炒饼,有的刚放上油等炝锅、有的已经9成熟马上可以出锅、有的正在焖、有的该放盐了……他每次从这个锅到另一个锅的时候,需要先记下这个锅的进度,然后到另一个锅想一想之前该做哪一步了,然后继续……这就是进程切换,进度其实就是上下文。计算机中进程的切换是一样道理。

2. 虚拟存储器

虚拟存储器是一个抽象的概念,它让每个进程都觉得是独占地使用主存,每个进程看到的是一致的虚拟地址空间,如图(地址是从下往上增大的):

虚拟存储器

1. 程序代码和数据

对所有的进程来说,代码是从同一个固定地址开始的,紧接着是和C全局变量相对应的数据位置。代码和数据区是直接按照可执行目标文件的内容初始化的。代码和数据区是在进程一开始运行时就被规定了大小的。

起始位置:0x08048000(32位) / 0x00400000(64位)

2. 堆

当调用malloc和free这样的函数时,堆可以在运行时动态地扩展和收缩。
在学习到如何管理虚拟存储器时候会详细研究堆。

3. 共享库

大约在地址空间的中间位置是一块用来存放像C标准库和数学库这样共享库的代码和数据的区域。共享库的概念很强大也很难懂。

在第7章介绍动态链接时会详细研究。

4. 栈

位于用户虚拟地址空间的顶部,通常用来实现函数调用。在程序执行期间,栈的大小是动态变化的,一般调用一个函数时,栈就会增长,从一个函数返回时,栈就会收缩。

在第3章介绍编译器是如何使用栈的。

虚拟存储器的运作需要硬件和操作系统软件之间精密复杂的交互,包括对处理器生成的每个地址的硬件翻译。其基本思想是把一个进程虚拟存储器的内容存储在磁盘上,然后用主存作为磁盘的高速缓存。

3. 文件

文件就是字节序列,仅此而已。

每个I/O设备,包括磁盘、键盘、显示器,甚至网络,都可以视为文件。系统中所有的输入输出都是通过使用一小组称为Unix I/O的系统函数调用读写文件来实现的。

它向应用程序提供了一个统一的视角,来看待系统中各式各样的I/O设备,因此文件这个简单而精致的概念其内涵是非常丰富的。


6. 并发与并行

我们对于计算机的需求无非两点:做得更多,跑的更快。一个系统如果能够处理多个活动或请求,这种能力就叫并发,并发通常是“看起来”或“感觉上”同时处理,如果是更加严格意义上的“同时”,这种效果就是并行。

1. 线程级并发

多处理器/多核:面对现代系统的程序员,通常是针对多处理器系统进行开发,多处理器系统就是我们常说的“N核”的系统,每个核有自己的L1和L2高速缓存,并共享更高层次的存储,如图。

这里写图片描述

超线程:这是一项允许一个CPU执行多个控制流的技术。超线程技术就是利用特殊的硬件指令,把两个逻辑内核模拟成两个物理芯片,让单个处理器都能使用线程级并行计算,从而兼容多线程操作系统和软件,提高处理器的性能。

举个栗子:当一个逻辑处理器在执行浮点运算(使用处理器的浮点运算单元)时,另一个逻辑处理器可以执行加法运算(使用处理器的整数运算单元)。这样做,无疑大大提高了处理器内部处理单元的利用率和相应的数据、指令处吞吐能力。

2. 指令级并行

现代处理器可以同时执行多条指令的属性就是“指令级并行”。

先举个栗子:流水线的概念您应该了解吧,制造业和服务业中经常见到,比如洗车这件事,通常要经过喷水、打皂、擦洗、上蜡、烘干这几个步骤,对于一辆车来说,各个步骤是有先后顺序的,而对于不同的待洗车来说,互相是无关的,因此,假设客流比较大,洗车店完全可以同时对不同的车进行不同步骤的服务,从而同时清洗5辆车,这样就能大大提高效率。

这里写图片描述

再来看处理器的指令级并行,类似地,其实每条指令从开始到结束需要大约20个或更长的周期,但是处理器可以使用非常多的聪明技巧来同时处理多达100条指令。

1  Load  C1 ← 23(R2) 2  Add   R3 ← R3+13  FPAdd C4 ← C4+C3
1  Add   R3 ← R3+1 2  Add   R4 ← R3+R2 3  Store R0 ← R4

前三条指令由于互相之间没有关系,因此并行度为3,也就是可以并行执行;
后三条指令由于依次依赖前一条指令产生的结果,因此并行度为1,也就是必须依次执行。

处理器可以挖掘出并行度较高的指令,并行执行,从而提高吞吐率。

3. 单指令、多数据并行

许多现代处理器拥有特殊的硬件,允许一条指令产生多个可以并行执行的操作,这种方式成为单指令、多数据并行(SIMD)

说白了,就是一箭多雕、一石多鸟。

比如,现在的处理器一般都具有并行地对4对单精度浮点数(float)做加法的指令。

这里写图片描述


7. 总结

计算机系统是由硬件和软件组成的,它们共同协作以运行应用程序。

计算机内部的信息被表示为一组一组的位,它们根据上下文有不同的解释。

程序被编译系统翻译成可执行文件,加载到内存后就可以供处理器解释和执行。

计算机把大量的时间用于不同存储设备间的数据复制,因此将各存储设备划分为不同的层次,各层次的数据相应速度和容量大小不同,将数据根据数据量和执行需求置于不同的层次,从而大幅减少时间开销。

操作系统内核是应用程序和硬件之间的媒介,它提供三个基本的抽象:1)文件是对I/O设备的抽象;2)虚拟存储器是对主存和磁盘的抽象;3)进程是对处理器、主存和I/O设备的抽象。

现代计算机体系中,并发和并行相关技术将发挥越来越大的作用,能够基于此进行程序设计和优化的程序员将掌握更多主动。


get-set

以上内容是基于《深入理解计算机系统》的读书笔记(第一章 计算机系统漫游),另有PPT版(有宏请放心启用,本博截图都来自于此),欢迎下载交流。

0 0
原创粉丝点击