计算机系统漫游
程序
程序的生命周期从一个源程序(源文件)开始,即程序员利用编辑器创建并保存的文本文件,如文件名为hello.c的c语言程序。源程序是由0和1组成的位序列,8个位被组织成一组,称为字节。每个字节表示程序中的某个文本字符。这种形式能够被人读懂。
基本思想:系统中的所有的信息-包括磁盘文件、存储器中的程序、存储器中存放的用户数据以及网络上传送的数据,都是由一串位表示的。区分不同数据对象的唯一方法是我们读到这些数据对象时的上下文。
为了在系统上运行hello.c程序,每条c语句都必须被其他程序转化为一系列的低级机器语言指令(如汇编语言)。然后,这些指令按照可执行目标程序的格式打好包,以二进制磁盘文件的形式存放起来,其称为可执行目标程序。
从hello.c到hello.exe的翻译过程可分为四个阶段完成。执行这四个阶段的程序(预处理器、编译器、汇编器和链接器)一起构成了编译系统。
程序由操作系统执行。Unix系统中通过shell应用程序及相关命令加载执行。
硬件设备
总线:贯穿整个系统的一组电子管道,其携带信息字节并负责在各个部件间传递。通常总线被设计成传送定常的字节块,也就是字。
I/O设备:系统与外部世界的联系通道。鼠标、键盘、显示器、磁盘驱动器等。每个I/O设备通过一个控制器或适配器与I/O总线相连。控制器是至于I/O设备本身的或者系统的主板上的芯片组,而适配器是一块插在主板插槽上的卡。其区别主要在于封装的方式不一样。
主存:临时存储设备,在处理器执行程序时,用来存放程序和程序处理的数据。
中央处理单元:解释或执行存储在主存中指令的引擎。处理器的核心是一个字长的存储设备(寄存器),称为程序计数器。在任何时刻,程序计数器PC都指向主存中的某条机器语言指令(即含有该指令的地址)。
处理器从程序计数器指向的存储器处读取指令,解释指令中的位,执行该指令指示的简单操作,然后更新程序计数器,使其指向下一条指令,而这一条指令并不一定与存储器中刚刚执行的指令相邻。
操作系统
所有应用程序对硬件的操作都必须通过操作系统。可以把操作系统看成是应用程序和硬件之间插入的一层软件。
操作系统基本功能:
1) 防止硬件被失控的应用程序滥用
2)向应用程序提供简单一致的机制来控制复杂而又通常大相径庭的低级硬件设备。
其通过进程、虚拟存储器、文件等抽象概念来实现这两个功能。
文件是对I/O设备的抽象,虚拟存储器是对主存(内存)和磁盘I/O设备的抽象,进程则是对处理器、贮存和I/O设备的抽象。
进程。进程是操作系统对一个正在运行的程序的一种抽象。在一个系统上可以同时运行多个进程,而每个进程都好像在独占地使用硬件。传统系统在一个时刻只能执行一个程序,而多核处理器能同时执行多个程序。无论是单核还是多核系统,一个cpu通过在进程间切换,可以并发的执行多个进程。
操作系统保持跟踪进程运行所需的所有状态信息,即上下文,包括程序计数器和寄存器文件的当前值,主存内容等。在任何一个时刻,单处理器系统都只能执行一个进程的代码。当操作系统决定要把控制权从当前进程转移到某个新进程时,就会进行上下文切换。
实现进程这个抽象概念需要低级硬件和操作系统软件之间的紧密合作。
线程。一个进程实际上可以由多个称为线程的执行单元组成,每个线程都运行在进程的上下文中,并共享同样的代码和全局数据。由于网络服务器对并行处理的需求,线程成为非常重要的编程模型,因为多线程之间比多进程之间更容易共享数据;当多处理器可用的时候,多线程也是一种使程序可以更快运行的方法。
虚拟存储器。虚拟存储器这个抽象为每个进程提供了一个假象,即每个进程都在独占地使用主存。每个进程看到的是一致的存储器,称为为虚拟地址空间。
文件。文件就是字节序列。每个I/O设备,磁盘、键盘、显示器、网络等都可以视为文件。文件这个抽象向应用程序提供了一个统一的视角来看待系统中可能出现的各种I/O设备。
并发、并行和抽象
并发concurrency是一个通用的概念:指同时具有多个活动的系统。
并行parallelism:指用并发使一个系统运行得更快。并行可以在计算机系统的多个抽象层次上运用。
1. 线程级并发
构建进程这个抽象,我们能够设计出同时执行多个程序的系统,这就导致了并发。使用线程,我们能够在一个进程中执行多个控制流。
单处理器系统中,并发执行只是模拟出来的,是通过使一台计算机在他正在执行的进程间快速切换的方式实现的。
当构建一个由单操作系统内核控制的多处理器组成的系统时,就得到一个多处理器系统。多处理器是将多个cpu集成到一个集成电路芯片上。超线程(同时多线程)处理器是一项允许一个cpu执行多个控制流的技术。如常规的处理器需要大约20000个时钟周期做不同的线程切换,而超线程处理器可以在单个周期的基础上进行切换。这使得cpu可以更好的利用其处理资源。
多处理器的使用可以从两个方面提高系统性能。首先,它减少了在执行多个任务时模拟并发的需要。其次,它可以使应用程序运行的更快。当然,这必须要求程序是以多线程方式来书写的,这些线程可以并行地高效执行。
2. 指令级并行
在较低的抽象层次上,现代处理器可以同时执行多条指令的属性指令级并行。基本思想是将一条指令所需要的活动划分为不同的步骤,将处理器的硬件组织成一系列的阶段,每个阶段执行一个步骤。这些阶段可以并行的操作,用来处理不同的指令的不同部分。其通过硬件设计,能够达到接近于一个时钟周期一条的指令的执行速率。甚至更快,称为超标量处理器。
3. 单指令、多数据并行
在最低的层次上,许多现代处理器拥有特殊的硬件,允许一条指令产生多个并行执行的操作,这种方式称为单指令、多数据SIMD并行。如专门处理向量计算的处理器。
抽象。抽象是计算机科学中最为重要的概念之一。如一组函数的API等,类的封装,接口。
在处理器中,指令集结构提供了对实际处理器硬件的抽象。使用这个抽象,机器代码程序表现得好像它是运行在一个一次只执行一条指令处理器上。实际上,底层的硬件比抽象描述的要复杂精细的多,它并行地执行多条指令,但又总是与那个简单有序的模型保持一致。
操作系统中,文件是对I/O的抽象;虚拟存储器是对程序存储器的抽象;进程是对一个正在运行的程序的抽象。
而虚拟机,他提供了对整个计算机的抽象。
计算机系统中的一个重大主题就是提供不同层次的抽象表示,而隐藏实际实现的复杂性。类比计算机网络。
信息的表示和处理
信息的存储
大多数计算机使用8位的块,或者字节,作为最小的可寻址的存储器单位,而不是在存储器中访问单独的位。机器级程序将存储器是为一个非常大的字节数组,称为虚拟存储器。存储器的每个字节都由一个唯一的数字来标识,称为地址。所有可能的地址集合称为虚拟地址空间。
虚拟地址空间只是一个展现给机器级程序的概念性映像。实际的实现是将随机访问存储器ram、磁盘存储器、特殊硬件和操作系统软件结合起来,为程序提供一个看上去统一的字节数组。
优化程序性能
消除循环的低效
减少过程的调用
等。。
存储器的层次结构
我们可以建立一个简单的计算机系统模型。Cpu执行指令,而存储器系统为cpu存放指令和数据。在这个模型中,存储器系统是一个线性的字节数组,cpu能够在常数时间内访问每个存储器位置。这是个有效的模型,但其没有反映系统实际工作的方式。
实际上,存储器是一个具有不同容量、成本和访问时间的存储设备的层次结构。高层次存储器速度快,成本高;低层次速度慢,成本低。整体效果是一个大的存储器池,其成本与层次结构底层最便宜的存储设备相当,但是却以接近于层次结构顶部存储设备的高速率向程序提供数据。
如果你的程序需要的数据在cpu的寄存器中,那么在0个cpu周期就能访问到,如果在高速缓存中需要1-30个周期,如果在主存中需要50-200个周期,如果在磁盘上需要约几千万个周期。
思想:如果你了解系统是如何将数据在存储器层次结构中上上下下移动的,那么你就可以编写你的应用程序,使得它们的数据项存储在层次结构中较高的地方,在那里cpu能更快地访问到它们。即程序的局部性locality。
Cpu使用一种称为存储器映射I/O的技术向I/O设备发出命令。在使用存储器映射I/O的系统中,地址空间中有一块地址是为与I/O设备通信保留的。每个这样的地址称为一个I/O端口。当一个设备连接到总线时,她与一个或多个端口相关联(或它被映射到一个或多个端口)。(Note:每一个I/O设备都有一个处理器)
局部性思想: 局部性通常有两种不同的形式即时间局部性和空间局部性。在一个具有良好的时间局部性的程序中,被引用过一次的存储器位置很可能在不远的将来再被多次引用。在一个具有良好空间局部性程序中,如果一个存储器位置被引用了一次,那么程序很可能在不愿的将来引用附近的一个存储器位置。局部性原理在应用程序的设计中扮演着重要的角色。如web浏览器将最近被引用的文档放在本地磁盘上,利用的就是时间局部性。
评价一个程序的局部性原则:
1.重复引用同一个变量的程序具有良好的时间局部性
2.对于具有步长为k的引用模式的程序,步长越小,空间局部性越好。
3.对于取指令来说,循环有好的时间和空间局部性。循环体越小,循环迭代次数越多,局部性越好。
存储器层次结构的本质是每一层存储设备都是较低一层的缓存。
在系统上运行程序
链接
链接是将各种代码和数据部分收集起来并组合成为一个单一文件的过程,这个文件可被加载(或被拷贝)到存储器并执行。链接可以执行于编译时(源代码被翻译成机器代码时);也可以执行于加载时(程序被加载器加载到存储器并执行时);还可以执行与运行时,由应用程序来执行。
链接是由叫做连接器linker的程序自动执行的。链接器使得分离编译成为可能。我们不需要将一个大型的应用程序组织为一个巨大的源文件,而是可以把它分解为更小、更好管理的模块,可以独立的修改和编译这些模块。
理解链接器将帮助你构造大型程序;将帮助你避免一些编程错误;将帮助你理解语言的作用域规则如何实现;有助于理解和使用共享库等。
目标文件三种形式:
1. 可重定位目标文件
2. 可执行目标文件
3. 共享目标文件。可以在加载或者运行时被动态地加载到存储器并链接。
编译器和汇编器生成可重定位目标文件(包括共享目标文件)。链接器生成可执行目标文件。一个目标模块就是一个字节序列,一个目标文件就是一个存放在磁盘文件中的目标模块。
异常控制流
从给处理器加电开始,直到断电为止,程序计数器假设一个值的序列a0,a1,..,an-1。其中,每个ak是某个相应的指令Ik的地址。每次从ak到ak+1的过渡称为控制转移。这样的控制转移序列叫做处理器的控制流。Flow of control, control flow。(程序计数器值(指令地址)序列称为控制流。)
最简单的一种控制流是一个“平滑的”序列,其中每个Ik与Ik+1在存储器中都是相邻的。这种平滑的突变,即Ik与Ik+1不相邻,是由诸如跳转、调用和返回等程序指令造成的。这使得程序能够对由程序变量表示的(捕获的)内部程序状态中的变化做出反应。
此外,系统也必须能够对系统状态的变化做出反应,这些系统状态不是被内部程序变量捕获的,而且也不一定要和程序的执行相关。
如一个硬件定时器定期产生信号,这个事件必须得到处理;包达到网络适配器后,必须存放在存储器中;程序向磁盘请求数据,然后休眠,直到被通知数据已就绪;当子进程终止时,创造这些子进程的父进程必须得到通知。
现代系统通过使控制流发生突变来对这些情况做出反应。我们把这些突变称为异常控制流(execptional control flow)。异常控制流发生在计算机系统的各个层次。
在硬件层,硬件检测到的事件会触发控制突然转移到异常处理程序。
在操作系统层,内核通过上下文切换将控制从一个用户进程转移到另一个用户进程。
在应用层,一个进程可以发送信号到另一个进程,而接收者会将控制突然转移到他的一个信号处理程序。一个程序可以通过回避通常的栈规则,并执行到其他函数中任意位置的非本地跳转来对错误做出反应。
异常
异常处理
异常就是控制流中的突变,用来响应处理器状态中的某些变化。在处理器中状态被编码为不同的位和信号。状态变化称为事件。当处理器检测到有事件发生时,它就会通过一张叫做异常表的跳转表,进行一个间接过程调用,到一个专门设计用来处理这类事件的操作系统子程序(异常处理程序)。
异常有硬件异常和软件异常。
系统中可能的每种类型的异常都分配了一个唯一的非负整数的异常号。其中一些号码是由处理器的设计者分配的,其他号码是由操作系统内核的设计者分配的。前者包括被0除、缺页、存储器访问违例、断点以及算数溢出。后者包括系统调用和来自外部I/O设备的信号。
在系统启动时,操作系统分配和初始化一张称为异常表的跳转表(数组),其中条目k包含异常k的处理程序地址。
当系统在执行某个程序时,处理器检测到发生了一个事件,并且确定了相应的异常号k。随后,处理器触发异常,执行间接过程调用,通过异常表的条目k转到相应的处理程序。
异常号是到异常表中的索引,异常表的起始地址放在一个叫做异常表基址寄存器的特殊cpu寄存器中。
异常分类
异常可以分为四类:中断interrupt、陷阱trap、故障fault、终止abort。
中断。中断是异步发生的,是来自处理器外部的I/O设备的信号的结果。硬件中断不是由任何一条专门的指令造成的,从这个意义上来说它是异步的。硬件中断的异常处理程序通常称为中断处理程序。
陷阱和系统调用。陷阱是有意的异常,是执行一条指令的结果。陷阱处理程序将控制返回到下一条指令。陷阱最重要的用途是在用户程序和内核之间提供一个像过程一样的接口,叫做系统调用。
用户程序经常需要向内核请求服务,比如都一个文件read,创建一个新的进程fork,加载一个新的程序execve,或者终止当前进程exit。为了允许对这些内核服务的受控的访问,处理器提供了类似“syscall n”指令,当用户程序想要请求服务n时,可以执行这条指令。执行syscall 指令会导致一个到异常处理程序的陷阱,这个处理程序对参数解码,并调用适当的内核程序。
从程序员的角度看,系统调用和普通的函数调用时一样的。但是,普通函数运行在用户模式中,用户模式限制了函数可以执行的指令类型,而且它们只能访问与调用函数相同的栈。系统调用运行在内核模式中,内核模式允许系统调用执行指令,并访问定义在内核中的栈。
故障。故障由错误情况引起,其可能能够被故障处理程序修正。如果处理程序能够修正这个错误的情况,它就将控制返回到引起故障的指令,从而重新执行它;否则,处理程序返回到内核中的abort例程,以终止引起故障的应用程序。一个典型的故障时缺页异常,当指令引用一个虚拟地址,而与该地址相对应的物理页面不存在存储器中,因此必须从磁盘中取出时,就会发生故障。
终止。终止是不可恢复的致命错误造成的结果。处理程序将控制返回至abort例程,终止这个应用程序。
每个系统调用都有一个唯一的整数号n,对应于一个到内核中跳转表的偏移量。
C语言用syscall(n)函数可以直接调用任何系统调用。但这需要你记得每个系统调用的整数号,比较繁琐。标准c库提供了一组方便的包装函数。这些包装函数将参数打包到一起,以适当的系统调用号陷入内核,然后将系统调用的返回状态传递回调用函数。这样你只要知道函数名即可。这样的包装函数称为系统级函数。
进程
当我们在系统上运行一个程序时,会得到一个假象-我们的程序是系统中当前运行着的唯一的程序。我们的程序好像是独占地使用处理器和存储器。处理器好像无间断地一条接一条地执行程序中的指令。程序中的代码和数据好像是系统存储器重唯一的对象。
这些假象都是通过进程的概念提供给我们的。
进程是一个执行中的程序的实例。系统中的每个程序都是运行在某个进程的上下文中的。上下文包括存放在存储器中的程序代码和数据,它的栈、通用目的寄存器内容、程序计数器、环境变量以及打开文件描述符的集合。
每次用户通过向shell输入一个可执行目标文件的名字,并运行一个程序时,shell就会创建一个新的进程,然后在这个新的进程的上下文中运行这个可执行目标文件。应用程序也能够创建新的进程。
进程提供给应用程序两个关键抽象:
1.一个独立的逻辑控制流,其提供一个假象,好像我们的程序独占地使用处理器
2.一个私有的地址空间,其提供一个假象,好像我们的程序独占地使用存储器系统
逻辑控制流
逻辑控制流即程序计数器值的序列,简称逻辑流。如下图,处理器的一个物理控制流分成了三个逻辑流,每个进程一个。显然,实际上进程是轮流使用处理器的。
一个逻辑流的执行在时间上与另一个流重叠,称为并发流,这俩个流被称为并发地运行。多个流并发地执行的一般现象称为并发。一个进程和其他进程轮流运行的概念称为多任务。一个进程执行它的控制流的一部分的每一时间段叫做时间片。多任务也叫时间分片。
并发的思想与流运行的处理器核数或者计算机数无关。如果两个流在时间上重叠,那么就是并发的。并行流是并发流的一个真子集。如果两个流并发地运行在不同的处理器核或者计算机上,那么我们称他们为并行流,它们并行地运行和执行。
私有地址空间
进程也为每一个程序提供一种假象,好像它独占地使用系统地址空间。一台有n位地址的机器,地址空间是2^n个可能地址的集合,0,1,…,2^n-1。一个进程为每个程序提供它自己的私有地址空间。一般而言,和这个空间中某个地址相关联的那个存储器字节是不能被其他进程读或写的,所以说,这个地址空间是私有的。
地址空间的组织结构。地址空间底部是保留给用户程序的,包括通常的文本、数据、堆和栈段。地址空间顶部是留给内核的。这个部分包含内核在代表进程执行指令时(比如,当应用程序执行一个系统调用时)使用的代码、数据和栈。
进程控制
可以通过系统级函数(这里C程序)调用来控制进程。
获取进程ID:每个进程都有一个唯一的帧数进程ID(PID),pid_t getpid(void)
创建和终止进程:我们可以认为进程总处于三种状态之一
n 运行。进程要么在cpu上执行,要么在等待被执行且最终会被内核调度
n 停止。进程的执行被挂起,且不会被调度。当收到SIGSTOP、SIGTSTP、SIDTTIN或者SIGTTOU信号时,进程就停止,并且保持停止到它收到一个SIGCONT信号,在这个时刻,进程再次开始运行。(信号是软件中断的形式)
n 终止。进程永远停止。三种原因:1)收到信号的行为是终止程序2)从主程序返回3)调用exit函数。Exit函数以退出状态如exit(0)来终止进程。另一种设置退出状态的方法是从主程序main返回一个整数值。
信号
一个信号就是一条小消息,它通知进程系统中发生了一个某种类型的事件。这是一种更高层的软件形式的异常,它允许进程中断其他进程。
每种信号类型都对应于某种系统事件。低层的硬件异常是由内核异常处理程序处理的,正常情况下,对用户进程而言是不可见的。信号提供了一种机制,通知用户进程发生了这些异常。
比如,如果一个进程试图除以0,那么内核就发送给它一个SIGFPE信号(序号8)。再如,当前进程在前台运行时,你键入ctrl-c,那么讷河就会发送一个SIGINT信号(序号2)给这个前台程序;一个进程可以通过向另一个进程发送一个SIGKILL信号(序号9)强制终止它;当一个子进程终止或者停止时,内核会发送一个SIGCHILD信号(序号17)给父进程。
传送一个信号到目的进程是由两个不同步骤组成的:
n 发送信号。内核通过更新目的进程上下文中的某个状态,发送一个信号给目的进程。发送信号原因:1)内核检测到一个系统事件,比如被0除错误2)一个进程调用了kill函数,显式地要求内核发送一个信号给目的进程。一个进程可以发送信号给它自己。
n 接收信号。当目的进程被内核强迫以某种方式对信号的发送做出反应时,目的进程就接收了信号。进程可以忽略这个信号,终止或者通过执行一个称为信号处理程序的用户层函数捕获这个信号。
发送信号
Unix系统提供了大量向进程发送信号的机制。所有这些机制都是基于进程组process group这个概念。
I/O编程
文件描述符
文件描述符在形式上是一个非负整数。实际上,它是一个索引值,指向内核为每一个进程所维护的该进程打开文件的记录表。当程序打开一个现有文件或者创建一个新文件时,内核向进程返回一个文件描述符。在程序设计中,一些涉及底层的程序编写往往会围绕着文件描述符展开。但是文件描述符这一概念往往只适用于UNIX、Linux这样的操作系统。
对于ANSI C规范中定义的标准库的文件I/O操作。ANSI C规范给出了一个解决方法,就是使用FILE结构体的指针。事实上,UNIX/Linux平台上的FILE结构体的实现中往往都是封装了文件描述符变量在其中。
在UNIX/Linux平台上,对于控制台(Console)的标准输入,标准输出,标准错误输出也对应了三个文件描述符。它们分别是0,1,2。在实际编程中,如果要操作这三个文件描述符时,建议使用<unistd.h>头文件中定义的三个宏来表示: STDIN_FILENO, STDOUT_FILENO以及STDERR_FILENO。
文件描述符与文件指针的区别
文件描述符:在linux系统中打开文件就会获得文件描述符,它是个很小的正整数。每个进程在PCB(Process Control Block)中保存着一份文件描述符表,文件描述符就是这个表的索引,每个表项都有一个指向已打开文件的指针。
文件指针:C语言中使用文件指针做为I/O的句柄。文件指针指向进程用户区中的一个被称为FILE结构的数据结构。FILE结构包括一个缓冲区和一个文件描述符。而文件描述符是文件描述符表的一个索引,因此从某种意义上说文件指针就是句柄的句柄(在Windows系统上,文件描述符被称作文件句柄)。
转载自原文链接, 如需删除请联系管理员。
原文链接:深入理解计算机系统-笔记,转载请注明来源!