计算机系统是由硬件和系统软件组成的。
我们通过跟踪hello程序的生命周期来开始对系统的学习:
#include<stdio.h> int main() { printf("hello, world\n"); return 0; }
大部分计算机系统都使用ASCII标准来表示文本字符
只由ASCII字符构成的文件称为文本文件,所有其他文件都称为二进制文件
系统中所有的信息——包括磁盘文件、内存中的程序、内存中存放的用户数据及网络上传送的数据,都是由一串比特表示的
(预处理器、编译器、汇编器、链接器)一起构成了编译系统(compilation system)
gcc -E hello.c -o hello.i
预处理阶段,预处理器(cpp)根据以字符 # 开头的命令,修改原始 C 程序;
第一行中的 #include <stdio.h> 命令告诉预处理器读取系统头文件 stdio.h 的内容;并把它直接插入程序文本中
gcc -S hello.i -o hello.s
编译阶段,编译器(ccl)将 .i 文件翻译成文本文件 .s,它包含一个汇编语言程序
as hello.s -o hello.o 或(gcc -c hello.s -o hello.o)
汇编阶段,汇编器(as)将 .s 翻译成机器语言指令,把这些指令打包成一种叫做 可重定位目标程序的格式,保存在 .o 文件中,它是二进制文件,打开后看到一堆乱码
gcc hello.o -o hello
链接阶段,hello程序调用了 printf 函数,它是每个 C 编译器都提供的标准 C 库中的一个函数,printf 函数存在于一个名为 printf.o 的单独的预编译好了的目标文件中
结果得到 hello 文件,它是一个可执行目标文件,可以被加载到内存中,由系统执行
l 了解编译系统如何工作的益处
优化程序性能
理解链接时出现的错误
避免安全漏洞
l 系统硬件的组成
1. 总线
贯穿整个系统的是一组电子管道,称作 总线,它携带信息字节并负责在各个部件间传递。
通常总线被设计成传送定长的字节块,也就是 字(word) 。字中的字节数(即字长)是一个基本的系统参数,各个系统都不尽相同。现在大多数机器字长要么是 4 个字节(32位),要么是 8 个字节(64位)
2. I/O 设备
I/O (输入/输出) 设备是系统与外部世界的联系通道。如用户输入的键盘和鼠标,作为用户输出的显示器,以及用于长期存储数据和程序的磁盘驱动器(简单说就是磁盘)。
最开始,可执行程序 hello 就存放在磁盘上
每个 I/O 设备都通过一个控制器或适配器与 I/O 总线相连。控制器和适配器之间的区别主要在于它们的封装方式
控制器是 I/O 设备本身或者系统的主板上的芯片组。
适配器是一块插在主板插槽上的卡
3. 主存
主存是一个临时存储设备,在处理器执行程序时,用来存放程序和程序处理的数据
4. 处理器
中央处理单元(CPU),简称 处理器,是解释(或执行)存储在主存中指令的引擎。处理器的核心是一个大小为一个字的存储设备(或寄存器),称为程序计数器(PC)。
在任何时刻,PC 都指向主存中的某条机器语言指令(即含有该条指令的地址)
CPU在指令的要求下可能会执行这些操作:
l 加载:从主存复制一个字节或者一个字到寄存器,以覆盖寄存器原来的内容
l 存储:从寄存器复制一个字节或一个字到主存的某个位置,以覆盖这个位置上原来的内容
l 操作:把两个寄存器的内容复制到 ALU,ALU对这两个字做算术运算,并将结果存放到一个寄存器中,以覆盖该寄存器中原来的内容
l 跳转:从指令本身抽取一个字,并将这个字复制到程序计数器(PC)中,以覆盖PC 中原来的值
当我们在键盘上输入字符串 “./hello” 后,shell程序将字符逐一读入寄存器,再把它存放到内存中。当我们在键盘上敲回车键时,shell程序就知道我们已经结束了命令的输入。
然后shell执行一系列指令来加载可执行的 hello 文件,这些指令将 hello 目标文件中的代码和数据从磁盘复制到主存。利用直接存储器存取(DMA)技术,数据可以不通过处理器而直接从磁盘到达主存
被加载到主存后,处理器就开始执行 hello 程序的 main 程序中的机器语言指令。这些指令将 “hello, world\n” 字符串中的字节从主存复制到寄存器文件,再从寄存器文件中复制到显示设备,最终显示在屏幕上
系统花费了大量时间把信息从一个地方挪到另一个地方,系统设计者采用了更小更快的存储设备,称为 高速缓存存储器,存放处理器近期可能会需要的信息
当shell加载和运行hello程序时,以及hello程序输出自己消息时,shell和hello程序都没有直接访问键盘、显示器、磁盘或者主存。取而代之的是,它们依靠操作系统提供的服务
可以把操作系统看成是应用程序和硬件之间插入的一层软件
操作系统有两个基本功能:
2. 防止硬件被失控的应用程序滥用
3. 向应用程序提供简单一致的机制来控制复杂而又通常大不相同的低级硬件设备
操作系统通过几个基本的抽象概念来实现这两个功能
4. 文件是对 I/O 设备的抽象表示
5. 虚拟内存是对主存和磁盘I/O 设备的抽象表示
6. 进程则是对处理器、主存和 I/O 设备的抽象表示
像hello这样的程序在运行时,操作系统会提供一种假象,就好像系统上只有这个程序在运行,这些假象是通过进程的概念来实现的
进程是操作系统对一个正在运行的程序的一种抽象。在一个系统上可以同时运行多个进程。并发运行,则是说一个进程的指令和另一个进程的指令是交错执行的
上面示例中有两个并发的进程:shell进程和hello进程
从一个进程到另一个进程的转换由操作系统内核管理。内核是操作系统代码常驻主存部分
当操作系统决定要把控制权从当前进程转移到某个新进程时,就会进行上下文切换。即保存当前进程的上下文,恢复进程的上下文,然后将控制器传递到新进程
内核不是一个独立的进程。它是系统管理全部进程所用代码和数据结构的集合
线程
一个进程实际上可以由多个称为线程的执行单元组成,每个线程都运行在进程的上下文中,并共享同样的代码和全局数据
虚拟内存
虚拟内存是一个抽象概念,每个进程看到的内存都是一致的,称为虚拟地址空间;地址空间最上面的区域是保留给操作系统中的代码和数据,底部区域存放用户进程定义的代码和数据
图中的地址是从下往上增大的
l 程序和代码:对所有进程来说,代码是从同一固定地址开始,紧接着是和C全局变量相对应的数据位置。代码区和数据区是直接按照可执行目标文件的内容初始化的
l 堆:代码和数据区一开始运行时就被指定了大小,当调用像malloc和free这样的C标准库函数时,堆可以在运行时动态的扩展和收缩
l 共享库:大约在地址空间的中间部分是一块用来存放像C标准库和数学库这样的共享库的代码和数据的区域
l 栈:位于用户虚拟地址空间顶部的是用户栈,编译器用它来实现函数调用
l 内核虚拟内存:地址空间顶部的区域是为内核保留的,不允许应用程序读写这个区域的内容或者直接调用内核代码定义的函数
系统之间利用网络通信
从一个单独的系统来看,网络可视为一个I/O设备。如图所示。
我们可以使用telnet应用在一个远程主机上运行hello程序
并发和并行
并发是一个通用的概念,指一个同时具有多个活动的系统
并行指的是用并发来使一个系统运行得更快
构建在进程这个抽象之上,就能够设计出同时有多个程序执行的系统了,这就导致了并发。在以前即使处理器必须在多个任务间切换,大多数实际的计算也都是由一个处理器来完成的。这种配置称为单处理器系统
当构建一个由单操作系统内核控制的多处理器组成的系统时,就得到了一个 多处理系统。多处理系统就是将多个CPU(称为“核”)集成到一个集成电路芯片上
超线程,有时称为同时多线程,是一项允许一个CPU执行多个控制流的技术
在较低的抽象层次上,现代处理器可以同时执行多条指令的属性称为 指令级并行。如果处理器可以达到比一个周期一条指令更快的执行速率,就称为超标量处理器。大多数现代处理器都支持超标量操作
最后,在增加一个新的抽象:虚拟机,它提供对整个计算机的抽象,包括操作系统、处理器和程序