psv-fuyang
计算机原理:
	我们的操作系统或者我们的计算机体系大致分这样几个层次,最底层的是硬件,硬件本身在工业设计上可能由于不同厂商所成产的各种硬件产品有可能是不兼容的,所以后来就出现了所谓的工业标准,各种硬件兼容了,能够很简单的拼凑起来了,当然这也是我们平时能够接触到的计算机的攒机时如何实现的,或者是之所以能够得以实现的一个基础,不过要注意的是硬件级别上它们所实现的电路设计是类似于使用电子电路这种方式加上所谓工业生产向晶体管我们知道大规模集成电路等等柔和起来之后,基于逻辑电路数字电路所设计出来的一个芯片,一个计算芯片,这心芯片彼此之间或者这些部件彼此之间,可以互相协调的,但是由于是在硬件级别完成的具体实现所以通常情况下它们比较丑陋,就比如说我们看到的一栋房子被装点得非常漂亮,但是实际上你打开这个房子之后你会发现里面无非就是一块块的砖瓦实现的,这样大家能理解了吧,那我们所能看到的硬件就类似于你所看到的砖瓦是未加任何修饰的基本的底层零件,所以这个设备或者这个基本硬件所提供给用户能够调用的接口,是极其丑陋的,我们要想能够利用这些硬件上工作起来是非常繁琐和不便的,所以在此基础之上我们期望对它做一下装饰,让用户体验更好让用户使用起来更为便捷,于是就出现了一种软件,能够将底层硬件的这些接口给它抽象为比较美观比较直观的比较易于调用的第二层接口,这就是我们的操作系统!
	经过操作系统封装后这些底层的硬件的各种功能被封装成所谓的系统调用,不过我们一而再再而三的强调这些系统调用虽然比起来硬件接口要美观多了但依然非常底层,之所以制作成这么底层,主要的目的之一就包括为了尽可能少的减少系统调用的个数,各位因明白,操作系统所提供的接口是一个通用的兼容接口,我们想利用它完成任何特定工作还必须利用这些接口,去开发应用程序才可以,而开发应用程序其实就是调用这些接口,因此如果你的接口量非常大比如说数千个甚至上万个,可以想象一下任何一个程序员或者有哪位程序员能够做到,灵活的使用者近万个系统调用接口,去做到程序开发这是极其困难的,所以此时少而精可能才是最关键最重要的,所以我们说调用接口依然非常简单非常底层,但比起来硬件接口已经直观已经有效已经易于调用的多了,不过它的这种仍然不是特别易于调用特性,是的我们的程序员在研发时不得不自己手动拼凑完成更复杂的功能,再说一遍它虽然数量很少,但过于底层的特性导致用户再拼凑一个完整功能时不得不把大量工作手头去进行,举个简单例子其实我之前说过很多次了,比如我们要吃一个馒头或大米饭,那早些年,我们有硬件时,很可能说我们就不用去种植,有人已经把大米或麦子出售,我们拿来直接调用就行,但是各位知道,拿来麦子之后我们没法直接去做馒头,所以就有了所谓的第二个层次,这个层次能够把麦子直接磨成精粉,面粉,我们拿到面粉之后就能直接吃馒头了,但是各位明白,现在,又不是现成的馒头,所以我们还得再次做尽可能的封装,比如,把面发酵好,把馒头做好,可能就没有蒸熟而已.我们买回来直接蒸熟就行了.蒸熟之后就可以直接吃了,所以这就是所谓的底层与否的概念,因此,越高级就越接近于你的最终目的,那通常情况下,越高级也就越抽象,越底层也就越具体,越底层也就越接近硬件的真实面目,所以这中间为了尽可能接近用户的最终目的,他需要被多次进行抽象,那于是乎,在最上面这层上或者在较上层上我们需要去开发的是各应用程序,但是我们说过这些应用程序彼此之间,可能都需要很多共同的功能,比方说我们说过的Excel word 它们都需要打印功能,于是这个打印机制,去调用打印机完成打印机制,如果每个软件都需要,自我独立实现的话,程序员的工作量加大不说,我们的整个系统本身,软件本身的体积变得非常庞大,所以为了避免这种情况,就把这些公共功能部分抽取出来做成了库,这些库表现出来的接口我们通常把它叫做API ,叫应用编程接口,但是各位要注意的是,站在这个角度来讲,通常而言,我们对这两的层次有两种独特的称呼,不管他有没有库,他们只要是被开发成应用程序的不是作为操作系统本身的一部分而存在的,那么我们就把它称作用户空间的程序,也就把他称为运行于用户态.所需要在内核空间中运行的程序,我们可以认为这就是操作系统,我们把它称为内核态,也叫核心态,也叫内核空间.在众多应用程序当中,我们说过每个应用程序要想真正运行起来,它最终是通过向内核发起系统调用来完成的,或者有一部分逻辑,不需要内核的参与,有我们的应用程序就能完成,哪一本分需要内核参与,哪一部分不需要内核参与呢?比如,我们举个例子,我们要计算1+1=2,这个需要在内核态中执行还是在用户态中执行?,我们说过内核负责完成任何具体工作么?没有,不负责,很显然我们计算1+1这是一种用户实际的特定的需要,他不需要在内核空间中运行的,所以这些指令就像我们所写的很多shell脚本一样,如果你只是在shell脚本中计算1+1=?这种情况,他通常是不会发起系统调用的,因为他不需要内核执行任何特权操作,只需要把1+1这种请求交给CPU来运行就行了.所谓用户态和内核态无非就是我们要大家知道计算机的运行无非就是运行指令的,那么有些指令是特权级别的,有些指令是非特权级别的,我们说过x86系列的CPU架构大概分成了四个层次,四个环由内而外,被称为环0环1环2环3,我说过环0是特权级的指令,环3是用户级的指令,一般来讲,特权级的指令是指那些像操作硬件控制总线等等,这些才是特权级的操作.我们要计算1+1=2并没有做出特权级别的操作,因此它运行在用户态即可,所以如果他不需要发起系统调用的话,就意味着我们的这个程序直接在CPU上就能运行,那一旦发起系统调用呢?说白了,调用就是调用执行别的函数,或者别的过程,所以如果他调用的是一个内核功能,而不是用户空间中的程序的话,这意味着,CPU接下来要转为在特权模式下执行,所以这就叫模式切换.而在特权模式下执行的一定是内核中的代码,所以说这其实意味着当用户执行一个程序时,走着走着发现自己需要做一个特权操作,但程序自身没这个能力,怎么办呢,转而向内核发申请,让内核来帮忙完成,这些底下所谓的特权操作,内核发现这个用户是有权限请求这些操作,于是它自己负责在特权级别下把这几个指令运行一下,并把指令的运行结果返回给刚才的这个应用程序,而这个程序将继续向后执行,所以有一部分操作是没办法自己完成的,因为自己没有权限在CPU上运行这些特权指令,所以要向内核发调用,所以就涉及到了模式转换,从用户模式到内核模式,从内核模式再返回给用户模式,各位知道,内核模式只是为了支撑用户模式完成某些用户模式所需要的操作的,而一个用户所需的特定操作,通常情况下一定是某个应用程序在用户模式所需要的执行的指令,所完成的.所以我们说过操作系统是否能够产生生产力,通常要看他是不是在用户空间运行了大量的或者占据了大量的时间,一个程序开发的不是特别好的话,你会发现内核空间的运行模式运行时间占据了百分之六七十,而用户空间运行的指令只占据了两三成,这样通常意味着,他可能CPU的大量时间没有花费到去产生生产力上.因为我们说过内核模式本身并不负责产生生产力.
	我们说了半天就是想告诉各位,一个程序的执行最终需要在内核的协调下,有可能需要在模式之间来回切换,有可能是他不断地需要运行用户空间代码加内核空间代码共同执行完成,所谓一个程序的执行一定是内核调度它到CPU上去执行的,那于是结果就是,我们哪些程序能执行,哪些程序不能执行,为什么某些程序能够运行起来,或者是我们这样举个例子,我们在外存上,在我们硬盘上存放有30个应用程序难道说一开机就得让30个应用程序都得运行起来么,那我怎么能让她运行起来,无非就是这样几种需要,有些程序是操作系统运行过程当中,为了完成基本功能就得运行的,我们就让他在后台自动运行,这叫守护进程.但有些是用户需要时才运行的,那用户需要时怎么能够通知内核把这个程序运行起来呢?说白了我们得有一个方式让用户能够跟内核相交互,告诉内核把程序运行起来,所以这需要一个特殊的应用程序—shell(人机接口).所以用户通过这么个shell接口能够跟我们的操作系统打交道,能够去发起一个指令的执行,这个程序说白了就是能够把程序的运行请求提交给内核,进而内核给他开放其运行所需要的有赖于的基本条件,从而程序执行起来了,因此任何一个没有shell的操作系统它能跑起来能完成生产作用么?为什么不能呢?只要我们把它要运行的程序默认定义为开机就能运行,是不是就OK了.但是我们想要他做一些特定的操作将无从入手.所以希望大家能够明白shell的基本功用,好了,这是我们讲到的操作系统的基本功能.操作系统发展到今天经过了好几个步骤,经历了好几个时段,像我们的计算机,到今天大致经历了四个时代,第一点主要是真空管计算机,那个时候所谓的输入输出通常都是穿孔字带,0就是穿非0就是不穿,或者穿孔卡片都行,那时对计算机的操作时极其不便的,一个计算机的使用可能需要一个团队,数十个人协调才能工作,这个时代大概是1945-1955这十年,计算机刚刚诞生的最初时期,到了第二代以后,晶体管技术出现了,所以计算机的体积被大规模的缩小,而且耗电量也大规模缩小.同时计算能力也提高了很多.这是就进入了晶体管时代,而且这时在程序上,已经出现了所谓的批处理系统,这时的主机通常个头都比较大,我们通常把它称作Mainframe主机,也就是后来经常提到的大型主机.不过大家需要注意的是那时的大型主机未必有现在的手机性能好,这个时期是接下来10年1955-1965.这个时候已经出现高级语言了,向Fortran语言这时已经诞生了,非常古老的编程语言.第三代大概又是经过了10年左右,甚至可能有一段更长的时间,大概10-15年的时间,这时已经发展出来了集成电路芯片,而且出现了多道程序设计的概念,所谓多道程序设计你可以理解为他比早期的批处理运行起来要复杂的多的一种运行程序,简单来讲,所谓批处理,假如说我们一共有3个任务需要完成,由程序员编写好程序之后,由所谓的计算机处理人员,把它制作成放在所谓的磁带机,真正的一些作业一些指令程序每一个作业开头大概是一大堆的$符开始, 架构1开始了而后有一堆指令,最后有一个特殊的标记位,可能又是一大堆的$符,第一个作业结束了.而后开始第二个作业,所谓批处理在在某一时刻,我们计算机上运行了一个程序,一定是这样子的,只不过这多个程序我们可以运行多个程序,着多个程序只不过是串行执行的,非并行,那所谓的集成电路时代的多道程序设计,就出现了早期计算机的雏形,简单来讲引入了一个所谓的叫监控的程序,这个监控程序负责在我们的硬件上同时运行多个程序,这多个程序为了能够彼此之间能够互相隔离,开始出现了所谓的进程级的概念,就不在是程序了,我们说过很多次进程通常是运行的一个程序的父本,他有生命周期从执行第一个指令开始,从程序入门开始到运行结束终止,这整个程序从运行起到执行结束,当然也包括中间的调度操作,全有中间这么一个叫做监控程序的程序来负责实现,这个时候我们把它叫做多道程序设计的时期,这个时候的一个著名系统被叫做分时系统,也正是分时这个技术的出现,使得多道程序设计变成了现实,把CPU的运算分成了时间片,第三代的大概时间是1965到1980年这十五年的时间,而从1980年前后PC机的出现开始标志着计算机的发展进入了第四代,这个时候主要是大规模集成电路LSI,在每平方厘米的芯片上可以集成数千个晶体管,一直到今天可能都不止数千个了,制作工艺已经达到22纳米可能甚至更小,所以每一个晶体管本身被做得人都肉眼是看不见的,在特殊的设备下通过特殊的工艺特殊的材料彼此间进行隔离,而在彼此间进行连接,这个时期在PC机上出现了第一个操作系统被称为叫CP/M,简单来讲它叫做微型计算机的控制程序,所谓叫做control program for microcomputer,简写为CP/M,这个时期大概就是1974年左右,注意还没有发展到真正的个人计算机时代,77年的时候所谓的叫做dataed research数据研究所他们重写了CP/M使得CP/M可以运行在早期Interl的8080芯片上,而Z-BOX的Z80以及其他CPU的芯片上也能跑CP/M,后来来自IBM的人员跟比尔盖茨他们彼此之间的故事我们此前已经讲过,20世纪60年代带斯坦福研究院有一个家伙供职于施乐公司的park实验室研发出来了另外一种所谓叫图形用户界面,但是施乐公司的park实验室对此产品没有敏感的捕捉到商机,后来的故事就是乔布斯独霸天下,这种操作性的发展历史,到今天为止我们操作系统的基本功能体系还是比较简单的,一般而言我们的计算机有五大基本部件,分别是运算器控制器被独立在CPU上,在CPU当中有一个独特的芯片叫MMU(内存控制单元),除了CPU之外五大基本部件还有存储器(memory),接着还有各种IO设备(比方说最常用最基本的显示设备(VG),键盘(KB),还用更重要的硬盘控制器(HDC),当然硬盘不是直接连接到主机上的而是通过硬盘控制器或适配器), VG+KB+HDC这三组其实他们都是被称作IO设备的设备,这些部件之间如何进行交互,需要一个线将其连起来,这就跟我们需要将多台电脑组成一个网络链大家为了彼此通讯需要一个线连起来,那么我们需要通过什么方式把这三个IO设备连起来呢?可以使用星型方式,找一个中心控制节点,大家都连接到这个中心控制节点,由它来完成数据交换,还有一种方式,我们说过计算机网络早期发展的第一个方式是总线方式,只有一根线,在我们的主机内部如果使用所谓星型控制方式将会变得非常麻烦,为什么? 每一种硬件设备的独立控制都要CPU进行,所以CPU需要发出N根线或者为每个设备都要实现构建数根连接线,这将会让整个计算机体积变得非常复杂,所以在计算机内部它们彼此的连接是靠一个线路连接起来的,我们通常把它叫做总线,所以这些设备都连接到同一个总线上,因此彼此之间就可以交互了,既然是总线在某一时刻一旦一个设备被使用就想我们此前讲过的计算机一样,其他设备就没法再使用了,所以CPU如果跟内存进行交互时,而这种方式进行连接的话,此时CPU要去控制VGA就没法进行了,因为总线只用来CPU跟内存之间的数据交互,但好在这个总线的工作频率通常非常高,它的数据交换能力非常强,所以不管怎么讲在早期的计算机的CPU运行性能和总线频率上,总线不是系统性能瓶颈所在,那MMU是做什么的?MMU被称为叫做内存控制单元,为什么要有内存控制单元,其实这种新型的设备才具有的一个组件,尤其是在X86的系统上常用的一个组件,在阿么系统上有没有还很难说,有的有,有的没有,了解就行. 所以这里不是特别交代就是以X86系列PC架构的为根本进行说明,MMU其实叫做内存管理单元,叫memory management unit,它出现的主要目的是为了实现让内存分页memory page, 这几大部件中CPU是最核心的,这是计算机的大脑,它的主要工作就是从内存中去除指令并运行它,在每个CPU的基本时钟周期当中,它要取出指令,解码指令,确定其类型和操作数,然后才能执行的,所以这个执行过程大概分了三步,取址-解码-执行, 再说一遍CPU的主要功能就是从内存中装载指令,必要时还要装载数据,从内存中取到指令以后,去解码指令,跟着指令转化成CPU上可以真正运行的指令,必须要完成解码,完成解码之后再开始执行,那因此CPU对这方面的工作,为了能够变得更为简便,它通常有三个组件分别完成每一个步骤上的功能,第一个叫取址单元,用来专门负责从内存中取指令的,取到的指令被交给第二个模块, CPU中的第二个组件叫做解码单元,解码完之后就可以执行了,所以第三步叫做执行单元,事实上我们在设计CPU时这三个功能可以不用分开,由一个芯片来完成,一个芯片既负责取址任务有负责解码还负责执行,但这样一来任何一条指令的执行都至少需要三个时钟周期,第一个时钟滴答把指令取过来,各位应该知道通常情况内存运行比较慢,这个能不是一个周期就能完成的操作,我们只是说这有三个步骤,所以第一个步骤取出来指令,每一个CPU内部都有一些用来保存关键变量和临时数据的存储器,这些存储器通常被称为CPU的寄存器,CPU取来指令以后要放入解码,要找个位置把它们放起来,如果输某一个程序大概有十条指令,现在我们取出第一条执行了,现在去取第二条,CPU怎么知道接下来要取的是第二条,它里面还有一个指令计数器,每取完一个指令之后,这个计数器,指令寄存器当中就可以把这个指针指向下一个指令在内存中的位置,所以我们取出来第二条指令的时候它马上就指向了第三条指令,叫程序计数器和堆栈指针程序状态字等等,实际上这些整个操作完成,在CPU内部需要多个所谓的控制芯片/存储芯片共同参与,尤其是存储芯片,这些存储芯片只是临时使用的,它跟CPU的工作周期频率是一样快的速度,跟CPU自身的工作频率是在一个同一个时钟周期下,那因此它的性能是非常好的,在CPU内部总线上完成数据通讯,这些设备通常都被成为CPU的寄存器,一般而言我们操作系统必须清楚明白了解每一个寄存器,尤其是在时间多路复用就是我们刚才所说的CPU要被多个程序所共享的使用中,操作系统经常会终止/挂起一个当前正在运行的某一个进程,而后转而执行其他进程,这叫上下文切换,每一次将一个程序终止/挂起的时候,操作系统必须把这些寄存器中的每一种状态给它保存下来,为什么? 因为寄存器保存这些程执行了第多少条指令了,你要把它清除了去装载其他的进程,所以此时它使用的已经是另外进程的数据了,当在重新还原回去的时候,那些此前终止时刻运行的程序的所有状态信息都没了,为了避免这种情况我们必须把那些程序运行当中这些寄存器当中包含的它的各种状态字,它的指令指针计数器等等都必须保存下来,这个过程称作保存线程,我们重新报线程装载回来时,就意味着它必须把保存的线程中的那些数据给重新再覆盖恢复到这些寄存器当中去,那这个过程就叫做上下文切换,这个过程一定是有成本的,一定会有开销的,保存线程需要时间,恢复线程也需要时间,保存线程到什么地方去了? 肯定是另外一个存储空间---内存, 内存当中为每一个要运行的进程都有一个叫做任务结构(task struct)的内存结构,这个task struct就明确记录了当前进程的各种状态信息包括他运行线程进程的ID号/父进程号 所使用的内存空间等等, 当然还有一种空间用来保存线程的,这个数据结构是由内核使用的,所以内核需要将进程调度出来时不让它执行,让它挂起的时候就会将这些状态字中的信息给它保存到这个数据结构中去. 当下次需要执行时,就把这些数据读出来,在恢复到CPU的各寄存器中去,所以这就叫上下文切换, 如果prefork的模型的http进程如果有一千个并发连接进来了,每一个用户请求就是一个进程,每一个用户的响应应该进程快的时候,于是这些进程就要快速切换,以实现为每一个用户处理请求的,所以切换过程中有大量的时间就浪费在切换中去了,所以后来我们讲到了Nginx不用切换了,nginx一个进程能够响应多个用户请求,这个进程就始终能够在这个CPU上执行,不用切换,所以省去了大量用于上下文切换的时间和开销,不过要完成这种操作我们就必须要实现将某一进程给它绑定在CPU上用来实现CPU的亲缘性,叫CPU affiliated,所以跟CPU相关的或者跟进程相关的调优,跟进程相关的操作优化主要目标之一就是实现进程的cpu亲缘性.
	处理器执行方式是取址.解码.执行,这三个步骤,所以每一个指令要想执行完成,就要等这三个步骤,那如果说我们有10条指令,就意味着有30个步骤,性能很差,那为了保证这个执行过程非常流畅,每一个分别有一个组件来实现,取址单元取完址之后要交给解码单元,这时取址单元闲着的,这时执行单元也是闲着的,那么我们能不能通过流水线,多级流水线,取址单元取完第一个指令,马上取第二个指令,所以与此同时,它取第一个指令,并交给解码单元执行的时候,取址单元在解码单元解码的时候又取第二个指令,当它把第二条指令取来时,解码单元已经将第一条解码完成,将结果交给执行单元,此时解码单元空闲了,所以与此同时,不断重复,这样一来五个周期之内完成三个操作,比我们想象的更快.因为是三个单元多以是三级流水,所以当以进行下一循环时,他又从取址单元开始了,而执行单元有时会慢,那因此,后来在实现相关功能时,为了实现更高级的性能,在CPU内部取址单元可能不止一个,解码单元也可能不止一个,而执行单元亦是不止一个,所以很多个指令就可以并行执行了.早期CPU发展中有一个摩尔定律,我哦们的计算机同样大小的芯片,晶体管的数量每18个月会翻一番,摩尔定律保持了30年,到今天为止,CPU频没有超过4GHz,现在的工业制造工业很难突破4GHz,或者是尽管能突破也很难像之前那样迅猛发展,我们如何保证计算机的处理能力每18月翻一番,于是就出现了所谓的多核心,不但有多核心还有超线程,超线程也就是多线程,多线程在根本上来讲是在run一颗CPU能够在两个不同的线程之间进行切换,它只不过在CPU内部引入一个独特的寄存器类似于寄存器这样的设备来完成,所以CPU在某一时刻一个CPU核心在某一时刻能够运行两个线程,所以叫超线程模式,常见的i3CPU有所谓的双核双线程,它表现的就是4个核心,就是类似于一个CPU在同一时刻可以同时执行两个线程,我们知道CPU在某一时段按道理来讲我们大脑在某一时刻只能想一件事,但是现在它的确能想两件事,使得一个芯片看上去就像两个芯片一样,所以这就是超线程的意义. 引入超线程技术,事实上物理核心数像i3只有两个核心,但逻辑核心却是四个,我们在windows的任务管理器中看到的也是四个. CPU到今天已经发展成多核心模型,一个物理CPU内部可能存在多个核心,这每个核心就像单独的CPU一样,使得他们彼此之间或者是一颗CPU在性能的提升上由于它可以同时执行多个程序使得其性能也一致保持进化状态,但CPU的工作频率已经很难提升了,早期的CPU就是靠工作频率来提升的,在早期加入在1秒钟内能完成3000次的取址解码执行,后来频率的提升,大家知道一个操作只需要在一个时间周期内完成,所以就需要一个更快的震荡功能来完成其所谓的工作频率的 所以早期的所谓工作频率指的就是时钟在一秒钟内所能完成操作的个数,叫时钟周期数,而CPU每一个操作要完成至少要依赖一个时间周期,早期可以把一秒钟平均划分成3万份,或平均划分成3千份,大家知道3GHz是3x1024x1024x1024个时间周期, 假如早期1秒钟能划分成3万份,所以它就只能执3万次操作,后来制造工艺的提升加入了更多的芯片,它能够把一秒钟平均划分成30万份了,它在1秒钟内执行30万次操作,这个性能就提升了,早期就是靠这种方式来提升CPU性能. 后来发现频率替补上去了,而对性能上又需要继续提升,无非还是提升在同样的时间内执行的指令个数,所以后来一个芯片不成就加入两个芯片,两个芯片不成就加入4个芯片,就这样去做, 所以就实现了所谓的并行执行力, 但是说了多次一般而言一个进程在执行时他通常只能用到一个CPU,无论有多少个核心,在某一时刻一个进程在运行时只能使用一个核心,那这个时候怎么表现出来CPU让操作系统跑得更快呢? 系统上所需要运行的进程可能不止一个,CPU的核心多了那就意味着某一个核心在某一个时刻可以运行一个进程,那么4个核心同时就可以运行4个进程,所以一共有400个进程,平均下来100次, 一个进程执行1秒,一共需要100秒,平均100秒就可以轮一遍. 所以只有一个核心的话就需要平均400秒轮一遍. 所以核心数越多 提升性能就是靠这种方式来实现的,但是还要想到一种特殊场景,假如众多进程当中很有可能处于繁忙状态的并非是所有的,如100个进程中96个长时间处于睡眠状态,有2个偶尔需要运行一下,只有2个进程非常繁忙, 比如我是一个web服务器,web服务器更多的别的进程中可能不需要工作,就这两个web进程需要频繁的去运行,占据着大量的CPU使用时间, 假如web服务器只有一个进程,而一共有4个核心,其他进程都很闲,那么这个CUP的这4个核心是没有得到充分利用的,因为一个进程再忙它也只能使用一个核心,所以这个时候我们要想能够一个非常繁忙的服务器上或者一个服务器上的某一个进程单独繁忙时能够尽可能最大化发挥主机性能 ---  并行编程: 让你的一个进程自己能够跑在多个CPU上!  如何实现让一个进程跑在多个CPU上呢? 要分析我们要运行的这个进程或任务本身大概需要运行哪些指令和程序,把这些指令和程序把它划分成多个不同的指令单元或叫执行流, 把本来应该串行执行流做成并行执行流,有些执行流之间可能是互不相干的,所以做成并行的,多个并行执行流,每个执行流做成一个独立的线程,多个线程调度到多个CPU上去执行,所以使得一个任务400条指令,做成4个执行流来执行,平均下来一个线程只需要执行100条指令,所以使得单线程或单进程的性能提升了!! 这种编程机制就叫做并行编程. 那并行编程有什么问题? 或者是我们为了使用多颗CPU有什么问题? 我们不做并发编程的话,CPU核心数就算有点多,事实上这背后所需要解决的问题并不复杂,因为某一进城在某一时段就像以前一样也是单核CPU无论物理CPU有多个, 多个CPU由内核调度多个进程分别同时执行,儿进程与进程之间互不相干,现在不是这样了,一旦并行编程,进程间很相关了,你把一个作业化成多个流程,有可能第一个线程运行到一半的时候需要第二个线程的结果,那它不得不等在这个地方,所以它的理想状况是四个指令,四个执行流水,四个线程同时开始同时结束,但大多数情况并不是如此,如果一个线程正在操作一个文件,那么第二个线程任然使用这个文件,而线程是共享进程所打开的文件标识符,共享进程的所有资源的,所以一旦第一个线程占据了,那么第二个线程只有处于等待状态,因为资源会发生争用,发生资源争用的地方我们都把它称作临界区,所以在这个层面下程序员就必须要精心解决这个问题,否则很有可能在一个进程内部各线程之间彼此协调不流畅,导致多线程不但不会提升性能反而会降低性能,就是这样一个原因. 不过在我们的网路运行模型当中,以web服务器为例,第一个用户的请求和第二个用户的请求它们彼此之间不相干, 所以在web服务器内部生成n个线程,每个线程想应用起来对线程之间影响并不大,尤其是在读操作比较多的时候,大家对网路访问都是get方法, 所以一个进程打开一个文件之后第一个线程在使用的时候,第二个线程也照样可以读,第三个线程也可以读,所以它们争用并不是特别多,所以这个模型尤其适合所谓的并发应用. 这就是CPU多核心对于我们现在应用程序彼此之间所存在的一些关系,以及现在所谓的并行编程指的是什么的概念. 包括要明白的是我们一旦一个程序生成了多个线程那么这些线程每一个都是一个独立的执行单元这样子的进程本来就很轻量级,所以linux并非真正意义上的线程操作系统, 所以每一个线程在Linux内核看来仍然是一个进程, 调度时它任然是进程级别的, 我们说过在调度时多核心就有问题出现.当一个进程被内核调度到第一颗CPU核心上执行时,后来它的执行时间耗完了,内核把它挂起,下次再调回来执行时有可能并不在原来的那颗CPU上,这会带来一个很严重的结果----缓存难以命中,  还有内核为了保证这些CPU核心是被负载均衡使用的,它的确是找到那个CPU核心比较闲就把这个进程调度到其上执行, 一般而言是这个样子, 即便默认不是如此。他也在不停的每一秒钟要rebalance一次,进内核在管理cpu时,每一颗CPU都有自己的执行队列。每一颗CPU都有自己的进程队列。我们一共有400个进程。所以每一颗CPU理想进程状态。每一个核心分了一百个。他自己管理两个队列。一个是等待运行的进程队列。运行完以后就放在这个所谓的。过期队列。所以cpu从这个执行队列中。一次取一个运行。当时间耗完之后,就放在过期队列之中。再取第二个在运行。运行完了,时间耗尽了,没有退出,再放到过期队列当中。当下一次,这个运行队列当中的所有进程都调动了一个遍之后。把过去对面当中很多进程还没有运行完毕的。把两个队列再次调个,原来的过期队列又成了运行队列。原来的运行队列又成了过期队列。为什么要把他们调个个儿?不调个的话还得复制呢.如果你只是把第一个给你当运行队列。第二个队列当过期队列。运行队列运行第一个进程运行结束。放到第二个,队列里面。接下来他俩都没有退出。这时候又重第一个进程开始调度。如果第一个是运行队列,第二个是过期队列。他还在把,这个进程从过期队列复制到运行队列当中。然后再去取。这样很麻烦。如果把这两个队列一换个儿上面运行一结束。上面这个成为过期队列。下面这个成为运行队列。于是重头取出陈运行结束,放入第二个过期队列。运行完毕,一调个儿,又回来了,每一个CPU都有它自己的队列。所以在某一时刻。能把整个系统上所需要的,400个进程平均一下,每一个cpu上一百个。但有些进程的运行时间长,有些进程的运行时间短。过了两秒钟以后。很有可能一个核心,90%的进程都在。而第四个核心可能只有3%了。因为他大多数都已经运行完了。那么它的结构是怎么样呢?是不是结构就是第一个核心,非常繁忙。第四个核心就很空闲。所以为了避免这种问题,该怎么办呢?内核rebalance,重新均衡。所以他把第一个进程中,多余的进程拿到第四个核心的运行队列里边。 所以得重新均衡一下。均衡的目的是为了让cpu,负载更均衡。 运行性能更好,运行速度更快。但结果是,缓存没法命中。因为数据在CPU核心上其实是有缓存的。所以我说事实上,我们将来,实现所谓的cpu的优化,为什么要做进程绑定呢? 这是一个原因。把一个进程绑定在一个核心上。提高其缓存命中率的。好,这是CPU的基本结构。为了让系统性能更好。所以一颗cpu内部提供了多个核心。各位有没有想过?如果一颗cpu还是忙不过来呢。在非常繁忙的服务器上。那多来一个CPU就好了。 多cpu的结构,假如说一个主机上有四个CPU。每一个CPU里面有四个核心。这就是16核。这种机制我们通常把它称作SMP对称多处理器。那么我们在实现CPU管理时,需要明白。我们主板上要想使用,四个CPU。你是不是得有四个cpu插槽才可以?如果你只有一个插槽,想要装四个CPU也装不上去。而这些CPU的插槽,我们把它称作CPU的socket。socket就是插槽的意思。每一个插槽可以插一颗cpu。而每一个CPU有四个核心。那很显然,我们这个主板最多支持16核,我们知道cpu只是一个计算控制单元。里面有寄存器。只是暂存一些数据的。真正的指令或程序是存放在内存中的,所以cpu要通过总线要跟所谓的叫内存的这么一个存储器进行交互。他们两之间可以直接连起来。现在的计算机体系结构,我们内存的频率也越来越高了。虽然仍然没有接近于cpu的工作频率。现在内存的生产工艺也越来越好,而且主流的频率已经达到1600,但是很快就会被取代掉所以内存频率也在不断的提高。早期内存频率跟CPU怎么结合起来的?我们知道我们的主机上为了使用一种总线连接更多的设备,他实际上也是有一个中间的设备控制器的。设备控制器把所谓的总线汇总起来以后连入cpu。有一个高速的接口芯片叫北桥。还有一个低速的叫南桥,这些桥我们都把它称作IO桥,只不过北桥通常被称作pci桥,南桥被称作i槽桥, 早期通常是这样称呼的。但是计算机发展到今天可能已经不再是这种结构了。内存的工作频率已经非常的高了。那么在从北桥接入的话,北桥可能会变得非常的繁忙,有些体系接口中,尤其是在服务器级别上,内存可能已经使用专门的内存总线直接连接到cpu上。而显卡等其他的io设备仍然通过所谓的北桥南桥进行连接。而且各位知道,南桥上面的这些,慢速设备可能存在的已经不多了。打印机键盘鼠标。而网卡现在已经接近于千兆PCI-e了,所以这些都有可能直接到北桥上去了。VGA总线更是如此尤其是在个人或者图形处理工作站上。图形显示芯片,所需要的数据带宽有可能更强大,他们有可能是直接到北桥上去的。而在服务器上,通常连显示器都没有。显示芯片都没有。给你个穿线接口,连上去偶尔,调试一下即可。所以在服务器上,VGA通常不是重要芯片。你是cpu跟内存之间是要实现,彼此数据交互的。但不管怎么样讲,cpu一定是比内存要快的。至少到现在为止,大多数的程序当中,cpu是比内存要快的。按道理来讲如果cpu在他自己的使用周期内,每一个周期取一个指令,这种性能会变得非常的理想,但很遗憾的是,我们的指令都是在内存中存放的,CPU内部的存储器非常小,不是用来存放指令的,只是用来存那些值或者指令执行过程中的状态或者暂存一些临时的数据,空间很小,所以不得不使用外部存储设备,这就是我们所谓的叫存储器。存储器空间足够大,造价也便宜,但性能很差,我们没办法让它跟cpu在同一个时钟周期下工作。于是乎,cpu可能大多数情况下是这么工作的,发出一个曲取址请求,等一圈没响应,再等一圈也没有响应,为什么呢?因为在一秒钟之内,CPU可以转一圈儿内存,可能只能转半圈。所以这就意味着我们cpu可能需要在原地转19圈,才可能等够内存转一圈回来数据。所以严重不协调,可以想象一下,在这种场景中,按照这种方式工作,我们CPU的大量时钟周期,都是浪费的。所以为了避免这种情况,为了避免二者衔接,应该怎么做呢。加入中间层都能解决问题。因为我们知道我们的程序是有所谓的局部性原理。这是经过后来所证实的,我们任何程序在运行的时候到了满足局部性这样一种特性,这种局部性满足两个维度的局部性。第一是空间局部性,第二是时间局部性.那所谓空间局部性是指是什么?我们说过程序是由指令加数据组成的,所以空间局部性指的是一个数据被访问到之后任何一个离这个数据很近的数据也可能随后被访问到,这叫做空间上的局部性。那什么叫时间局部性呢?一般而言当一个指令执行完毕之后,很快会被再次访问到,一个数据也是这样的道理,一个刚刚被访问到的数据,可能会被再次很快的访问到。好,这是所谓的,程序局部性原理。这是局部性原理的存在,所以使得无论是在空间或者时间的角度来考虑,一般而言,我们都需要对其数据做缓存。各位知道我们内存出现的主要目的是拿空间换时间,还是拿时间换空间啦呢?很明显是拿时间换空间。在没有缓存的情况下,我们引入了内存,内存的主要目的不是有更大的空间吗,所以这就是损失了时间,换取更大的空间。而加上缓存的目的主要是什么?拿一些空间换取更多的时间。所以无非就是在这两个维度之间进行切换。那由此我们的计算机的存储体系大概就分为这样几个层次:首先,最慢的设备在早期是磁带,而现在我们比较慢的设备是机械式硬盘;接着比机械式硬盘更快的是固态硬盘;再向上就是内存了,现在内存动辄几十个GB,所以现在的内存就相当于以前的硬盘了,而现在的硬盘就相当于以前的磁带了;接着再向上就是高速缓存,视频的缓存现在又分为三级,三级缓存,二级缓存,一级缓存,最后,一级缓存上边儿就是寄存器。所以这就是cpu能够使用的所有空间的完整层次。很显然,我们基于所谓的局部性原理数据很可能真是存放在机械式硬盘上面的,那为了使数据的访问更快,根据程序的二八法则, 机械硬盘上20%的数据存放在固态硬盘上,固态硬盘上20%数据存放在内存中,20%在存放三级缓存中,20%存放在二级缓存中,20%存放在一级缓存中,只将自己的状态字临时放在寄存器当中。寄存器几乎能够以接近于CPU的时钟周期在工作。所以她俩是在同一个时钟周期内完成交互的。我们非常小,寄存器也非常小,于是乎依赖于一级缓存能够存放更多的数据,但一级缓存比寄存器的速度就要慢多了,假如说寄存器的时钟周期再一次完成一个操作是1纳秒的话,那么一级缓存大概需要2纳秒,所以一级缓存是非常接近于cpu的使用周期的,那二级缓存就有可能是4纳秒,三级缓存就是8纳秒,内存就是10纳秒,固态硬盘就是1000纳秒,机械硬盘就是10毫秒左右.所以说我们的大量数据如果都放在机械硬盘上,不断需要通过硬盘去读取数据的话,那么你的整个系统性能就可想而知,会非常的低。这就是为什么我们之前讲的缓存服务器使用内存和缓存,其性能就会有大幅提升的原因。但一级缓存,二级缓存,三级缓存,没办法在程序级别上直接操作的,因为一级缓存通常是cpu的物理核心专用的,每一颗物理核心都有自己专用的一级缓存。二级缓存通常现在也是专用的。三级缓存是各CPU物理核心所共享的。所以cpu的架构是,每个核心都有自己的一级缓存和二级缓存,这样意味着每个CPU核心都不会跟其他的核心产生资源争夺,假如说我们现在有四个核心,这四个核心可能要共享三级缓存,所以三级缓存可能很大,他有可能会提升性能,但并不意味着性能会很高。一个cpu的系统性能不光要看他的时钟周期,更重要的是要看他的一级缓存和二级缓存的大小,通常同样的时钟频率,一级缓存128k,一颗cpu可能卖200块,而一级缓存256k,一颗cpu卖350块到400块,一级缓存512k,一颗cpu可能就要卖到1200块。因为缓存的造价非常高,所以我们看cpu的系统性能时,不要只顾着看主频,更重要的是看一二级缓存的大小。如果说四个核心共享三级缓存会出现什么问题?资源争用! 所以一旦有任何地方存在以共享为目的的设备,那我们就不得不手动去解决资源争用的问题.如果在硬件级别为了保证避免资源争用,必须有额外的机制对他们进行指挥交通。大家知道,我们指挥四个和指挥4000个肯定依赖的消耗是不一样的,还是那句话,并行或单方向的四个车道和单方向的八个车道,说需要的管理和消耗可能不止相差一倍。我们之前说过的,向上扩展,四个核心共享三级缓存,四颗cpu共享内存。在所谓的对称多处理器架构下SNMP架构下,如果你有多颗物理cpu,每个cpu有多个核心我们先不管他,那这多个cpu将会使用同一段内存空间。每颗cpu都能访问内存空间的地址从手部到尾部,以实现其定位访问任意数据,结果是什么呢?如果第一颗CPU存放的数据,而第二颗CPU把它给占据了,结果就麻烦大了,因此我们必须在一种机制上去协调他们之间的资源争用问题。那怎么协调呢。所以我们说了很多次向上扩展性能总有那么一个时刻开始下降。我也是从你集成的芯片数量在某一单位时间内要摩尔定律的下降问题,还是内部资源争用协调问题,都会导致。随着你的芯片在某一时刻内,多一个芯片,它的性能可能是上升的,但它一定有一个临界点,再多芯片性能不但不会上升,反而会下降。这就是我们所提到的多个物理核心所共享三级缓存,或者多个cpu共享物理内存的问题。那各位想象一下,我们为了避免资源争吵用该怎么做?虽然这些物理核心可能会资源争用,但它有一个好处,每个核心都有自己的一级缓存和二级缓存,所以他们对三级缓存的争用近况就小得多了,各位可以想明白,如果没有一级缓存和二级缓存,大家如果都是直接共享的一个缓存。那结果是每一个CPU的数据都直接放在这一个缓存中,没有自己本地的存储空间,频繁需要对同一段共享内存设备进行操作的话,资源征用现象将会非常严重。那同样的,如果我们是多颗cpu呢?CPU使用外部的内存是必然的,我们不考虑缓存的情况下,使用内存是必然的。而一个非常繁忙的服务器有多颗cpu,那除了CPU不考虑缓存的情况下对内存的占用情况仍然是非常普遍的现象。只要征用就得有仲裁机制,比如说我们的每一颗cpu为了访问内存,他们中间有一个共享的内存仲裁访问控制器,所以任何一颗CPU就不能直接访问内存了,而是先找这个控制器,看自己能不能够获得这个控制器的使用权,如果别的CPU都没有再使用的,于是自己就获得这个控制器,于是自己就可以使用这个内存。而且控制器还要告诉这个cpu哪个进程在哪个地址空间上,你应该能够使用哪些内存,那些内存空间是已经被分割出去的。这种方式在一定程度上固然可以缓解征用的可能性,不过效率比较差。那怎样才能提高效率呢?现在在很多的物理CPU上,都引入了所谓的叫NUMA结构(非一致性内存访问),什么叫非一致性内存访问呢?每一颗CPU都有自己的专用内存,你看cpu都有专用的外部内存空间,可以把它想象成自己独有的一个三级缓存一样。所以每一颗CPU都在自己独立的内存空间中操作数据,如果这个数据没有,它会基于缓存机制到共享内存中去读取数据,取过来之后,仍然是在自己的内存中进行操作,这叫NUMA体系。NUMA体制的引入会有什么样的好处呢? 征用的可能性大大的缩减了。但是也带来了另外一个问题,比如说我把第一个进程运行时调度到了第一颗cpu上,这个进程的数据在第一颗CPU的内存当中,尤其是他所修改的数据也在第一段内存当中,基于重新负载的结果,这个进程下一次被调度到了第二颗cpu上去了,在第二套CPU上是没有他的数据的,就只能通过其他的CPU内存中复制过来一份,这个复制过程是需要时间的,所以重新均衡reblance仍然会导致性能降低,那怎么办呢?只能进行进程绑定,进程的CPU亲缘性绑定。所以我们在很多服务器架构下,有了多颗物理CPU时,对于非常繁忙的进程做专CPU专用,在NUMA机制上通常做这种进程绑定的需求因为迫切。好,继续回过来说对一级缓存,二级缓存和三级缓存的使用。各位在想一个问题,CPU在访问数据时,CPU所能够操作的数据,通常只能是寄存器当中的数据,因为cpu只能操作自己的本地存储器。如果本地存储器没有数据,他要从外边去取,取回来之后仍然放在本地寄存器中来操作。各位可以想象,如果一个指令修改了某一个数据,那么我们可以认为它是在寄存器当中修改的,但是最终这个数据要存放在内存上。由操作系统再负责从内存中再同步到磁盘上。我都不考虑吃饭的问题,因为在CPU看来,一般是外部的io设备,我们整个计算机的核心无非就是运算器控制器存储器这三个部件。CPU一定能够抽出时间是在寄存器当中,计算机当中没有数据,他去哪儿找啊。你加了缓存的,这个缓存不是旁路功能模式,而是类似于代理模式的。所以当计算器没有数据时,CPU就会尝试先到一级缓存把数据加到寄存器中做操作,那如果一级缓存没有呢?找二级缓存.二级缓存如果还没有呢?找三级缓存。同样的如果三级缓存还没有,找内存。内存中如果还没有,那就要从磁盘中进行装载,这个过程我们先不考虑。如果说内存中有,那于是乎这些数据从内存复制到三级缓存一份。很显然三级缓存如果数据空间不够用了,就要请你出去一份,给他腾空间,然后他再换乘到二级缓存当中,再缓存到一级缓存,最后到寄存器中使用。寄存器修改完成,那接下来他的修改数据要保存到一级缓存,我们做过真正修改操作数据最终真正要存放到内存当中(不考虑磁盘的情况),所以站在这个角度来考虑,你说如果他直接把数据放在一级缓存当中,万一系统断电了或者是放在一级缓存当中其他cpu要想找回这些数据能看到吗?看不到,一般而言,我们可能要多核心多cpu。我这个进程被调度到其他cpu上该怎么办呢?进程切换时,寄存器会被清空,会被重新修改,大缓存并不会。只要有空间缓存,是不会被置换出去的不会被清理出去的。结果呢?结果是一旦被调度到其他CPU上,这个程序就被访问不到了。为了避免这种情况。大家知道内存通常都是共享的,所以一个进程在某一个CPU上做的操作如果直接存放到内存当中,那么即便是被调度到其他cpu上,事实上,这个CPU从内存中取数据取过来之后,照样能够操作。所以处理数据一定要写到内存当中去的,而且是尽快要写到内存中去。那事实上怎么去写呢。为了保证这个数据能够尽快到达内存,CPU在写缓存的时候有两种策略。第一种叫通写,第二种叫回写,通写叫write through, 回写叫write back. write back性能更好,因为我只要写到一级缓存,它就会告诉cpu完成了,此时cpu就可以去做其他事情了。通写呢?——CPU必须要等待这个数据写到一级缓存二级缓存三级缓存,写到内存当中,然后cpu才知道写完成了,因为这整个操作过程是需要cpu来控制的,所以cpu是没法去做其他事情的,只有这个写作操作完成了,cpu才可能去做其他的事情。这就是换成当中所谓的通写和回写机制,哪个性能好?——回写.  哪个更靠谱?——通写.   具体采用哪一种呢?按需而定。事实上将来我们去学到硬盘的缓存时,大家知道,我们的硬盘设备都有自己的内部缓存,。事实上,这时候硬盘的写入操作也是遵循所谓通写和回写的。只要有缓存,想象一下我们的数据从内存最终要同步到硬盘上,不然系统一断电,数据就丢失了。必须要写到永久存储设备当中去。很显然这个行操作是我们的内核后台线程定期的,或者按照某种策略周期性的,将内存中的数据给它同步到硬盘当中去的。怎么同步呢?这个请求先发给硬盘的缓存,然后再写到硬盘的真正存储空间当中去。你说此事是通写还是回写呢?一样的道理,通写就意味着内核控制的内存, 只要把数据内存挪到硬盘的缓存中,回写就OK啦,就返回告诉你写完成了。然后硬盘控制器自己控制着把这些缓存中的数据在同步到硬盘当中去,这叫做回写。那什么是通写呢?内核控制的内存中的数据必须要先写到缓存中,再写到磁盘上,才会返回写入成功。哪一种性能好----回写.  哪一种数据可靠性更高更靠谱———通写,但童鞋的性能更差,所以采用通写模式,你就何不去面对降低性能的问题.而使用回写机制,你不得不,忍受系统断电时,数据有可能会丢失的问题。硬盘自己的缓存空间跟内存差不多,断电数据就会丢失。好了,大家理解了缓存的通写和回写机制,大家还是要注意的是CPU内部的缓存还有所谓的几路关联的问题。比方说八路关联(eight way associate) 完全关联(full associates)等等这些概念确实是对cpu的性能有一定的影响,但对我们调优相关性并不大。因此有兴趣的同学可以去了解一下什么叫cpu的n路关联问题(n-way associate)。 通常完全关联性能好,但完全关联造价高。内存不是永久性设备,内存中的数据很显然是通过基于IO通过其他的设备获取而来的。从某种意义上来讲,对cpu来讲,内存虽然是一个专用的存储设备,但是其数据都来自于外部io设备,所以计算机的五大基本部件还要包含io设备,那io接到我们总线上面,CPU怎么跟IO交互,IO中的数据又怎么装载到内存中去?你是我们需要了解的问题。

分类:

技术点:

相关文章: