初识处理器体系结构——首本基于 Linux 4.X 新书《奔跑吧Linux内核》大放送

2017-09-13 08:30:00 开源中国

开源中国联合人民邮电出版社异步社区给粉丝送福利啦!!这次可是大干货——《奔跑吧Linux内核》技术书籍。试读第一章内容《处理器体系结构》,9 月 15 日前在评论区写下你的见解,我们将在评论用户中抽出五位幸运用户送出精美图书一本!


本书以一份Linux内核奔跑卷开篇,详细分析了Linux内核设计与四大模块的源代码实现,包括内存管理、进程管理、并发与同步、中断管理。本书的特色在于以实际问题为导向来分析源代码的实现,在每章节前会列出相应的思考题,引导读者带着问题去思考和阅读源代码。此外,本书中还介绍了ARM32/ARM64体系结构中的常见问题,利用QEMU搭建简易的内核调试环境。本书内容主要基于Linux 4.0内核,同时对Linux 4.0到Linux 4.10的发展情况也有所介绍。

第1章 处理器体系结构


Linux 4.x内核已经支持几十种的处理器体系结构,目前市面上最流行的两种体系结构是x86和ARM。x86体系结构以Intel公司的PC和服务器市场为主导,ARM体系结构则是以ARM公司为主导的芯片公司占领了移动手持设备等市场。本书重点讲述Linux内核的设计与实现,但是离开了处理器体系结构,就犹如空中楼阁,毕竟操作系统只是为处理器服务的一种软件而已。目前大部分的Linux内核书籍都是基于x86架构的,但是国内还是有相当多的开发者采用ARM处理器来进行开发产品,比如手机、IoT设备、嵌入式设备等。因此本书基于ARM体系结构来讲述Linux内核的设计与实现。

关于ARM体系结构,ARM公司的官方文档已经有很多详细资料,其中描述ARMv7-A和ARMv8-A架构的手册包括:

  • <ARM Architecture Reference Manual, ARMv7-A and ARMv7-R edition>

  • <ARM Architecture Reference Manual, ARMv8, for ARMv8-A architecture profile>

另外还有一本非常棒的官方资料,讲述ARM Coxtex系统处理器编程技巧:

  • <ARM Coxtex-A Series Programmer’s Guide, version 4.0>

  • <ARM Coxtex-A Series Programmer’s Guide for ARMv8-A, version 1.0>

读者可以从ARM官方网站中下载到上述4本资料[1]。本书的重点集中在Linux内核本身,不会用过多的篇幅来介绍ARM体系结构的细节,因此本章以快问快答的方式来介绍一些ARM体系结构相关的问题。


1、请简述精简指令集RISC和复杂指令集CISC的区别


20世纪70年代,IBM的John Cocke研究发现,处理器提供的大量指令集和复杂寻址方式并不会被编译器生成的代码用到:20%的简单指令经常被用到,占程序总指令数的80%,而指令集里其余80%的复杂指令很少被用到,只占程序总指令数的20%。基于这种思想,将指令集和处理器进行重新设计,在新的设计中只保留了常用的简单指令,这样处理器不需要浪费太多的晶体管去做那些很复杂又很少使用的复杂指令。通常,简单指令大部分时间都能在一个cycle内完成,基于这种思想的指令集叫作RISC(Reduced Instruction Set Computer)指令集,以前的指令集叫作CISC(Complex Instruction Set Computer)指令集。

IBM和加州大学伯克利分校的David Patterson以及斯坦福大学的John Hennessy是RISC研究的先驱。Power处理器来自IBM,ARM/SPARC处理器受到伯克利RISC的影响,MIPS来自斯坦福。当下还在使用的最出名的CISC指令集是Intel/AMD的x86指令集。

RISC处理器通过更合理的微架构在性能上超越了当时传统的CISC处理器,在最初的较量中,Intel处理器败下阵来,服务器市场的处理器大部分被RISC阵营占据。Intel的David Papworth和他的同事一起设计了Pentium Pro处理器,x86指令集被解码成类似RISC指令的微操作指令(micro-operations,简称uops),以后执行的过程采用RISC内核的方式。CISC这个古老的架构通过巧妙的设计,又一次焕发生机,Intel的x86处理器的性能逐渐超过同期的RISC处理器,抢占了服务器市场,导致其他的处理器厂商只能向低功耗或者嵌入式方向发展。

RISC和CISC都是时代的产物,RISC在很多思想上更为先进。Intel的CSIC指令集也凭借向前兼容这一利器,打败所有的RISC厂商,包括DEC、SUN、Motorola和IBM,一统PC和服务器领域。不过最近在手机移动业务方面,以ARM为首的厂商占得先机。


2.请简述数值0x12345678在大小端字节序处理器的存储器中的存储方式。


在计算机系统中是以字节为单位的,每个地址单元都对应着一个字节,一个字节为8个比特位。但在32位处理器中,C语言中除了8比特的char类型之外,还有16比特的short型,32bit的int型。另外,对于位数大于8位的处理器,例如16位或者32位的处理器,由于寄存器宽度大于一个字节,那么必然存在着如何安排多个字节的问题,因此导致了大端存储模式(Big-endian)和小端存储模式(Little-endian)。例如一个16比特的short型变量X,在内存中的地址为0x0010,X的值为0x1122,那么0x11为高字节,0x22为低字节。对于大端模式,就将0x11放在低地址中;0x22放在高地址中。小端模式则刚好相反。很多的ARM处理器默认使用小端模式,有些ARM处理器还可以由硬件来选择是大端模式还是小端模式。Cortex-A系列的处理器可以通过软件来配置大小端模式。大小端模式是在处理器Load/Store 访问内存时用于描述寄存器的字节顺序和内存中的字节顺序之间的关系。

大端模式:指数据的高字节保存在内存的低地址中,而数据的低字节保存在内存的高地址中。例如:

在大端模式下,前32位应该这样读:12 34 56 78。

因此,大端模式下地址的增长顺序与值的增长顺序相同。

小端模式:指数据的高字节保存在内存的高地址中,而数据的低字节保存在内存的低地址中。例如:

在小端模式下,前32位应该这样读:12 34 56 78。

因此,小端模式下地址的增长顺序与值的增长顺序相反。

如何检查处理器是大端模式还是小端模式?联合体Union的存放顺序是所有成员都从低地址开始存放的,利用该特性可以轻松获取CPU对内存采用大端模式还是小端模式读写。

如果输出结果是true,则是小端模式,否则是大端模式。


3.请简述在你所熟悉的处理器(比如双核Cortex-A9)中一条存储读写指令的执行全过程。


经典处理器架构的流水线是五级流水线:取指、译码、发射、执行和写回。

现代处理器在设计上都采用了超标量体系结构(Superscalar Architecture)和乱序执行(out-of-order)技术,极大地提高了处理器计算能力。超标量技术能够在一个时钟周期内执行多个指令,实现指令级的并行,有效提高了ILP(Instruction Level Parallelism)指令级的并行效率,同时也增加了整个cache和memory层次结构的实现难度。

一条存储读写指令的执行全过程很难用一句话来回答。在一个支持超标量和乱序执行技术的处理器当中,一条存储读写指令的执行过程被分解为若干步骤。指令首先进入流水线(pipeline)的前端(Front-End),包括预取(fetch)和译码(decode),经过分发(dispatch)和调度(scheduler)后进入执行单元,最后提交执行结果。所有的指令采用顺序方式(In-Order)通过前端,并采用乱序的方式(Out-of-Order,OOO)进行发射,然后乱序执行,最后用顺序方式提交结果,并将最终结果更新到LSQ(Load-Store Queue)部件。LSQ部件是指令流水线的一个执行部件,可以理解为存储子系统的最高层,其上接收来自CPU的存储器指令,其下连接着存储器子系统。其主要功能是将来自CPU的存储器请求发送到存储器子系统,并处理其下存储器子系统的应答数据和消息。

很多程序员对乱序执行的理解有误差。对于一串给定的指令序列,为了提高效率,处理器会找出非真正数据依赖和地址依赖的指令,让它们并行执行。但是在提交执行结果时,是按照指令次序的。总的来说,顺序提交指令,乱序执行,最后顺序提交结果。例如有两条没有数据依赖的数据指令,后面那条指令的读数据先被返回,它的结果也不能先写回到最终寄存器,而是必须等到前一条指令完成之后才可以。

对于读指令,当处理器在等待数据从缓存或者内存返回时,它处于什么状态呢?是等在那不动,还是继续执行别的指令?对于乱序执行的处理器,可以执行后面的指令;对于顺序执行的处理器,会使流水线停顿,直到读取的数据返回。

如图1.1所示,在x86微处理器经典架构中,存储指令从L1指令cache中读取指令,L1指令cache会做指令加载、指令预取、指令预解码,以及分支预测。然后进入Fetch & Decode单元,会把指令解码成macro-ops微操作指令,然后由Dispatch部件分发到Integer Unit或者FloatPoint Unit。Integer Unit由Integer Scheduler和Execution Unit组成,Execution Unit包含算术逻辑单元(arithmetic-logic unit,ALU)和地址生成单元(address generation unit,AGU),在ALU计算完成之后进入AGU,计算有效地址完毕后,将结果发送到LSQ部件。LSQ部件首先根据处理器系统要求的内存一致性(memory consistency)模型确定访问时序,另外LSQ还需要处理存储器指令间的依赖关系,最后LSQ需要准备L1 cache使用的地址,包括有效地址的计算和虚实地址转换,将地址发送到L1 Data Cache中。

图1.1 x86微处理器经典架构图

如图1.2所示,在ARM Cortex-A9处理器中,存储指令首先通过主存储器或者L2 cache加载到L1指令cache中。在指令预取阶段(instruction prefetch stage),主要是做指令预取和分支预测,然后指令通过Instruction Queue队列被送到解码器进行指令的解码工作。解码器(decode)支持两路解码,可以同时解码两条指令。在寄存器重名阶段(Register rename stage)会做寄存器重命名,避免机器指令不必要的顺序化操作,提高处理器的指令级并行能力。在指令分发阶段(Dispatch stage),这里支持4路猜测发射和乱序执行(Out-of-Order Multi-Issue with Speculation),然后在执行单元(ALU/MUL/FPU/NEON)中乱序执行。存储指令会计算有效地址并发射到内存系统中的LSU部件(Load Store Unit),最终LSU部件会去访问L1数据cache。在ARM中,只有cacheable的内存地址才需要访问cache。

图1.2 Cortex-A9结构框图

在多处理器环境下,还需要考虑Cache的一致性问题。L1和L2 Cache控制器需要保证cache的一致性,在Cortex-A9中cache的一致性是由MESI协议来实现的。Cortex-A9处理器内置了L1 Cache模块,由SCU(Snoop Control Unit)单元来实现Cache的一致性管理。L2 Cache需要外接芯片(例如PL310)。在最糟糕情况下需要访问主存储器,并将数据重新传递给LSQ,完成一次存储器读写的全过程。

这里涉及计算机体系结构中的众多术语,比较晦涩难懂,现在对部分术语做简单解释。

  • 超标量体系结构(Superscalar Architecture):早期的单发射结构微处理器的流水线设计目标是做到每个周期能平均执行一条指令,但这一目标不能满足处理器性能增长的要求,为了提高处理器的性能,要求处理器具有每个周期能发射执行多条指令的能力。因此超标量体系结构是描述一种微处理器设计理念,它能够在一个时钟周期执行多个指令。

  • 乱序执行(Out-of-order Execution):指CPU采用了允许将多条指令不按程序规定的顺序分开发送给各相应电路单元处理的技术,避免处理器在计算对象不可获取时的等待,从而导致流水线停顿。

  • 寄存器重命名(Register Rename):现代处理器的一种技术,用来避免机器指令或者微操作的不必要的顺序化执行,从而提高处理器的指令级并行的能力。它在乱序执行的流水线中有两个作用,一是消除指令之间的寄存器读后写相关(Write-after-Read,WAR)和写后写相关(Write-after-Write,WAW);二是当指令执行发生例外或者转移指令猜测错误而取消后面的指令时,可用来保证现场的精确。其思路为当一条指令写一个结果寄存器时不直接写到这个结果寄存器,而是先写到一个中间寄存器过渡,当这条指令提交时再写到结果寄存器中。

  • 分支预测(Branch Predictor):当处理一个分支指令时,有可能会产生跳转,从而打断流水线指令的处理,因为处理器无法确定该指令的下一条指令,直到分支指令执行完毕。流水线越长,处理器等待时间便越长,分支预测技术就是为了解决这一问题而出现的。因此,分支预测是处理器在程序分支指令执行前预测其结果的一种机制。在ARM中,使用全局分支预测器,该预测器由转移目标缓冲器(Branch Target Buffer,BTB)、全局历史缓冲器(Global History Buffer,GHB)、MicroBTB,以及Return Stack组成。

  • 指令译码器(Instruction Decode):指令由操作码和地址码组成。操作码表示要执行的操作性质,即执行什么操作;地址码是操作码执行时的操作对象的地址。计算机执行一条指定的指令时,必须首先分析这条指令的操作码是什么,以决定操作的性质和方法,然后才能控制计算机其他各部件协同完成指令表达的功能,这个分析工作由译码器来完成。例如,Cortex-A57可以支持3路译码器,即同时执行3条指令译码,而Cortex-A9处理器只能同时译码2条指令。

  • 调度单元(Dispatch):调度器负责把指令或微操作指令派发到相应的执行单元去执行,例如,Cortex-A9处理器的调度器单元有4个接口和执行单元连接,因此每个周期可以同时派发4条指令。

  • ALU算术逻辑单元:ALU是处理器的执行单元,主要是进行算术运算,逻辑运算和关系运算的部件。

  • LSQ/LSU部件(Load Store Queue/Unit):LSQ部件是指令流水线的一个执行部件,其主要功能是将来自CPU的存储器请求发送到存储器子系统,并处理其下存储器子系统的应答数据和消息。


本章还将叙述几个问题:

4.请简述内存屏障(memory barrier)产生的原因。

5.ARM有几条memory barrier的指令?分别有什么区别?

6.请简述cache的工作方式。

7.cache的映射方式有full-associative(全关联)、direct-mapping(直接映射)和set-associative(组相联)3种方式,请简述它们之间的区别。为什么现代的处理器都使用组相联的cache映射方式?

8.在一个32KB的4路组相联的cache中,其中cache line为32Byte,请画出这个cache的cache line、way和set的示意图。

9.ARM9处理器的Data Cache组织方式使用的VIVT,即虚拟Index虚拟Tag,而在Cortex-A7处理器中使用PIPT,即物理Index物理Tag,请简述PIPT比VIVT有什么优势?

10.请画出在二级页表架构中虚拟地址到物理地址查询页表的过程。

11.在多核处理器中,cache的一致性是如何实现的?请简述MESI协议的含义。

12.cache在Linux内核中有哪些应用?

13.请简述ARM big.LITTLE架构,包括总线连接和cache管理等。

14.cache coherency和memory consistency有什么区别?

15.请简述cache的write back有哪些策略。

16.请简述cache line的替换策略。

17.多进程间频繁切换对TLB有什么影响?现代的处理器是如何面对这个问题的?

18.请简述NUMA架构的特点。

19.ARM从Cortex系列开始性能有了质的飞越,比如Cortex-A8/A15/A53/A72,请说说Cortex系列在芯片设计方面做了哪些重大改进?

想了解更多关于Linux内核的知识吗?赶紧在评论区写下你的见解吧,可能获奖的就是你哦!



推荐阅读

CSDN CODE 项目整体迁移到码云平台的公告

可能是国内最火的开源项目 —— PHP 篇

实用即王道,超好用的 Linux 文件管理器推荐

Web 开发者需要知道的 12 个终端命令

国产开源项目又被抄袭?主角还是 00 后创业者