通过跟踪hello程序的生命周期来开始对系统的学习——从它被程序员创建开始,到在系统上运行,输出简单的消息,然后终止。
1.1 信息就是位+上下文
注意:每个文本行都是以一个看不见的换行符'\n'来结束的。
hello.c程序是以字节序列的方式储存在文件中的。每个字节都有一个整数值,对应于某些字符。例如第一个字节的整数值是35,它对应的就是字符'#'。注意:每个文本行都是以一个看不见的换行符'\n'来结束的。
像hello.c这样只有ASCII字符构成的文件称为文本文件,那么其他文件都称为二进制文件。
1.2 程序被其他程序翻译成不同的格式
为了在系统上运行hello.c程序,每条C语句都必须被其他程序转化为一系列的低级机器语言指令。然后这些指令按照一种称为可执行目标程序的格式打包,并以二进制磁盘文件的形式存放起来。目标程序也称为可执行目标文件。
在Linux系统,从源文件到目标文件的转化,是由编译器驱动程序完成的:
linux> gcc -o hello hello.c
这个翻译过程分为四个阶段完成,执行这四个阶段的程序(预处理器、编译器、汇编器、链接器)一起构成了编译系统。
- 预处理阶段:
将头文件插入程序文本中
- 编译阶段:
2-7行都以一种文本格式描述了一条低级机器语言指令。
- 汇编阶段:
将hello.s翻译成机器语言指令。hello.o是一个二进制文件,它包含的17个字节是函数main的指令编码。如果在文本编辑器打开hello.o,将看到一堆乱码。
- 链接阶段:
注意:hello程序中调用了printf函数,它是每个C编译器都提供的标准C库中的一个函数。printf函数存在一个名为printf.o的单独的预编译好了的目标文件中,而这个文件必须以某种方式合并到我们的hello.o程序中。连接器ld就负责处理这种合并。结果就得到hello文件,它是一个可执行目标文件(可执行文件),可以被加载到内存中,由系统执行。
1.3 了解编译系统如何工作是大有益处的
🐷
1.4 处理器读并解释储存在内存中的指令
此刻,hello.c源程序已经被编译系统翻译成了可执行目标文件hello,存放在磁盘。要写Linux系统运行该可执行文件,我们要将它的文件名输入到称为shell的应用程序中:
linux> ./hello
hello,world
linux>
shell是一个命令行解释器,它输出一个提示符,等待输入一个命令行,然后执行这个命令。
1.4.1 系统的硬件组成
- 主存:是一个临时存储设备,处理器执行程序时,用来存放程序和程序处理的数据
物理上:主存是由一组DRAM芯片组成的
逻辑上:线性字节数组,每个字节都有唯一的地址(数组索引),这些地址从0开始
-
处理器:解释(或执行)存储在主存中指令的引擎。
核心:大小为一个字的存储设备(或寄存器),称为程序计数器(PC)。任何时刻,PC都指向主存中的某条机器语言指令(即PC中含有该条指令的地址)。
从系统通电,处理器一直在不断执行PC指向的指令,再更新PC,使其指向下一条指令(这条指令并不一和在内存中刚刚执行的指令相邻)。
指令指示的操作围绕主存、寄存器文件和算数/逻辑单元(ALU)进行。寄存器文件是一个小的存储设备,由一些单个字长的寄存器组成,每个寄存器都有唯一的名字。ALU计算新的数据和地址值。
CPU在指令的要求下可能执行的操作:
-
加载
从主存复制一个字节或一个字到寄存器,以覆盖寄存器原来内容
-
存储
从寄存器复制一个字节或者一个字到主存的某个位置,以覆盖这个位置上原来的内容
-
操作
把两个寄存器的内容复制到ALU,ALU对这两个字做算术运算,并将结果存放到一个寄存器中,以覆盖该寄存器中原来的内容
-
跳转 从指令本身抽取一个字,并将这个字复制到PC中,以覆盖PC中原来的值
1.4.2 运行hello程序
- 初始时,shell程序执行它的指令,等待我们输入一个命令。当我们在键盘上输入字符串"./hello"后,shell程序将字符逐一读入寄存器,再把它存放到内存中。
- 当我们在键盘上敲回车键时,shell程序知道我们结束了命令输入。然后shell执行一系列指令来加载可执行的hello文件,这些指令将hello目标文件中的代码和数据从磁盘复制到主存。数据包括最终会被输出的字符串"hello,world\n"。
利用直接存储器存取(MDA)技术,磁盘中数据可以不通过处理器而直接从磁盘到达主存。
- 一旦目标文件hello中的代码和数据被加载到了主存,处理器就开始执行hello程序的main程序中的机器语言指令。这些指令将"hello,world\n"字符串中的字节从主存复制到寄存器,再次寄存器文件复制到显示设备,最终显示在屏幕上。
1.5 高速缓存至关重要
根据示例:系统花费了大量的事件把信息从一个地方挪到另一个地方。从程序员的角度来看,这些挪动(即复制)就是开销,减慢了程序“真正”的工作。
处理器从主存中读取数据要比从磁盘驱动器上读取快1000万倍,处理器从寄存器文件中读数据比从主存中读取几乎要快100倍。近年来,处理器与主存之间的差距还在持续增大。加快处理器的运行速度比加快主存的运行速度要容易和便宜的多。
针对处理器与主存之间的差异,高速缓存存储器应运而生(cache memory,简称cache或高速缓存),作为暂时的集结区域,存放处理器近期可能会需要的信息。L1高速缓存的访问速度几乎和访问寄存器文件一样快。
- L1、L2高速缓存是用SRAM的硬件技术实现
- 主存是用DRAM的硬件技术实现
原理:利用了高速缓存的局部性原理,即程序具有访问局部区域里的数据和代码的趋势。通过让高速缓存里存放可能经常访问的数据,大部分的内存操作都能在快速的高速缓存中完成。
意识到高速缓存存储器存在的应用程序员能够利用高速缓存将程序的性能提高一个数量级。
1.6 存储设备形成层级结构
存储器层次结构的主要思想是:上一层的存储器作为第一层存储器的高速缓存。
程序员同样可以利用对整个存储器层次结构的理解来提高程序性能。
1.7 操作系统管理硬件
回到hello程序的例子。当shell加载和运行hello程序时,以及hello程序输出自己的消息时,shello和hello程序都没有直接访问键盘、显示器、磁盘或者主存。中间的媒介是,操作系统提供的服务。
可以把操作系统看成是应用程序和硬件之间插入的一层软件。所有应用程序对硬件的操作尝试都必须通过操作系统。
操作系统有两个基本功能:
- 防止硬件被失控的应用程序滥用;
- 向应用程序提供简单一致的机制来控制复杂而又通常大不相同的低级硬件设备。
操作系统通过几个基本的抽象概念(进程、虚拟内存和文件)来实现这两个功能。
- 文件:对IO设备抽象表示
- 虚拟内存:对主存和磁盘IO设备的抽象表示
- 进程:对处理器、主存、IO设备的抽象表示
1.7.1 进程
进程:操作系统对一个正在运行的程序的一种抽象。
并发运行:一个进程的指令和另一个进程的指令是交错执行的。
无论是单核还是多核系统,一个CPU看上去都像是在并发地执行多个进程,这是通过处理器在进程间切换来实现的。操作系统实现这种交错执行的机制称为上下文切换。
下面讨论只包含一个CPU的单处理器系统:
操作系统保持跟踪进程运行的所有状态信息。这种状态,也就是上下文,包括许多信息:比如PC和寄存器文件的当前值、以及主存的内容。在任何一个时刻,单处理器系统都只能执行一个进程的代码。当当操作系统决定要把控制权从当前进程转移到某个新进程时,就会进行上下文切换,即保存当前进程的上下文、恢复新进程的上下文,然后将控制权传递到新进程。新进程就会从它上次停止的地方开始。
两个并发进程 shell进程和hello进程的示例:
如图,从一个进程到另一个进程的转换是由操作系统内核管理的,内核就是操作系统代码常驻主存的部分。 当应用程序需要操作系统的某些操作时,比如读写文件,它就执行一条特殊的系统调用指令,然后将控制权传递给内核。然后内核执行完被请求的操作后,将控制权返回给相应的应用层程序。
注意:内核不是一个独立的进程。相反,它是系统管理全部进程所用代码和数据结构的集合。
实现进程这个抽象概念需要低级硬件和操作系统软件之间的紧密合作。
1.7.2 线程
尽管通常我们认为一个进程只有单一的控制流,但现代OS中,一个进程实际上可以由多个称为线程的执行单元组成,每个线程都运行在进程的上下文中,并共享同样的代码和全局数据。
当有多处理器可用的时候,多线程也是一种使得程序可以运行的更快的方法。
1.7.3 虚拟内存
虚拟内存是一个抽象概念,为每个进程提供了一个假象,即每个进程都在独立使用主存。每个进程看到的内存都是一致的,称为虚拟地址空间。
如图为Linux进程的虚拟地址空间:
地址空间最上面的区域是保留给OS中的代码和数据的,这对所有进程来说都是一样的。 每个进程看到的虚拟地址空间由大量准确定义的区构成,每个区都有专门的功能。
注意:图中的地址是从下往上增大的。
- 程序代码和数据区:示例中的可执行文件hello
- 堆区:代码和数据区在进程一开始运行时就被指定了大小,于此不同,当调用malloc和free这样的C标准库函数时,堆可以在运行的时候动态扩展与收缩。
- 共享库:存放像C标准库和数据库这样的共享库的代码和数据的区域。
- 栈区:编译器用它来实现函数调用。和堆一样,用户栈在程序执行期间可以动态扩展与收缩。特别地,每次调用一个函数,栈就会增长;从一个函数返回,栈就会收缩。
- 内核虚拟内存区:为内核保留。不允许应用程序独写这个区域的内容或者直接调用内核代码定义的函数。好玩的是😇,这些应用程序必须调用内核才能执行这些操作。
虚拟内存的基本思想:把一个进程虚拟内存的内容存储在磁盘上,然后用主存作为磁盘的高速缓存。
1.7.4 文件
文件就是字节序列,仅此而已。每个IO设备包括磁盘、键盘、显示器、甚至网络都可以看成是文件。系统中所有的输入输出都是通过使用一小组称为Unix I/O 的系统函数调用读写文件来实现的。
文件向应用程序提供了一个统一的视图,来看待系统中可能含有的所有各式各样的IO设备。
1.8 系统之间利用网络通信
从一个单独的系统来看,网络可视为一个IO设备。
一种客户端和服务器之间的交互类型:
1.9 重要主题
1.9.1 Amdahl定律
主要思想:当我们对系统某个部分加速时,其对系统整体性能的影响取决于该部分的重要性和加速程度。
公式:
Amdahl定律描述了改善任何过程的一般原则。
1.9.2 并发和并行
并发:指一个同时具有多个活动的系统。
并行:指的是用并发来使一个系统运行的更快。并行可以在计算机系统的多个抽象层次上运用。
按系统层次结构中由高到低的顺序:
-
线程级并发:
构建在进程这个抽象之上,我们能够设计出同时有多个程序执行的系统,这就导致了并发。 而使用线程,我们甚至能够在一个进程中执行多个控制流。
传统意义上,并发执行只是模拟出来的,是通过一台计算机在它正在执行的进程间快速切换来实现的。这便是单处理器系统。
当构建一个由单操作系统内核控制的多处理器组成的系统时,我们就得到了一个多处理器系统。
-
多核处理器: 将多个CPU(称为“核”)集成到一个集成电路芯片上。
-
超线程: 有时称为同时多线程,是一项允许一个CPU执行多个控制流的技术。举例来说:Intel Core i7处理器可以让每个核执行两个线程,所以一个4核的系统实际上可以并行地执行8个线程。
多处理器的使用可以从两方面提高系统性能:
-
减少了执行多个任务时模拟并发的需要。
-
可以使应用程序运行得更快,当然,这必须要求程序是以多线程方式来书写的,这些线程可以并行地高效执行。
-
-
指令级并行
在较低的抽象层次上,现代处理器可以同时执行多条指令的属性称为指令级并行。
最近的处理器可以保持每个时钟周期2-4条指令的执行速率。其实每条指令从开始到结束需要长的多的时间,大约20个或更多周期,但是处理器使用了非常多的技巧来同时处理多达100条指令。
流水线的使用:在流水线中,将执行一条指令所需要的活动划分成不同的步骤,将处理器的硬件组织成一系列的阶段,每个阶段执行一个步骤。而这些阶段可以并行地操作,用来处理不同指令的部分。
-
单指令、多数据并行
在最低层次上,许多现代处理器拥有特殊的硬件,允许一条指令产生多个可以并行执行的操作,这种方式称为单指令、多数据,即SIMD并行。
1.9.3 计算机系统中抽象的重要性
抽象的使用是计算机科学中最为重要的概念之一。
不同的编程语言提供不同形式和等级的抽象支持:例如java类的声明和C语言的函数原型。
而计算机系统中同样有抽象:
- 处理器中的指令集架构:提供了对实际处理器硬件的抽象。底层的硬件远比抽象描述的要复杂精细,它并行地执行多条指令,但又总是与那个简单有序的模型保持一致。
- 文件:是IO设备的抽象
- 虚拟内存:是程序存储器的抽象
- 进程:是对一个正在运行的程序的抽象
- 虚拟机:提供对整个计算机的抽象
2.0 一些问题
- 对1.9.2中并发和并行的定义存在疑惑。
感觉对并发的定义明明是并行的定义。。。😵
贴一个可能更加清晰的链接: [blog.csdn.net/qq_42009498…]