夏虫的博客

却道天凉好个秋

一、什么是指令集?

业界泰斗、ACM 和 IEEE 两院院士、2017 年图灵奖得主、 RISC-V 基金会的董事会副主席 David Patterson 给了非常通俗易懂的定义:软件通过一个专业词汇上称为指令集的东西和硬件讲话。换句话说,指令集是软硬件之间沟通的桥梁。

指令集ISA架构是同一个概念,实现一个指令集的CPU叫微架构

例如:ARMv,这是一个指令集,也是一个架构版本。ARM 的 Cortex-A72 处理器,就是一个实现了ARMv8 指令集的具体 CPU,是一个微架构。ARM 的 Cortex-A72 是一个支持 ARMv8 指令集的 CPU IP,就是一堆 RTL 代码。瑞芯微的 RK3399 芯片就是一个集成了2个 Cortex- A72 之后的芯片,是一个物理体,这个芯片支持 ARMv8 指令集。凡是支持 ARMv8 的软件,都可以在 RK3399 上运行。

指令集

fs.py配置文件

fs.py文件是可以运行FS模式模拟的通用配置文件,但由于其编写的时间很早,部分指令集的新功能没有更新。

如不支持RISCV

一、导入库

fs.py自己的路径configs/example/fs.py是知道的,它用addToPath(“…/…/”),使得可以直接导入confis/中的模块,然后引用了configs里自带的一些python文件。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
# [fs.py]
import argparse
import sys
# 由于在之前的配置文件中将src/python/文件夹也加入了库搜索路径,所以可以import m5
import m5
from m5.defines import buildEnv
from m5.objects import *
from m5.util import addToPath, fatal, warn
from m5.util.fdthelper import *

addToPath('../')
# confis/ 中含有ruby/和common/
from ruby import Ruby

from common.FSConfig import *
from common.SysPaths import *
from common.Benchmarks import *
from common import Simulation
from common import CacheConfig
from common import CpuConfig
from common import MemConfig
from common import ObjectList
from common.Caches import *
from common import Options

二、命令行参数

1
2
3
4
5
6
7
8
9
10
11
# [fs.py]
# Add args
parser = argparse.ArgumentParser()
Options.addCommonOptions(parser) # 添加常用参数。如CPU类型、CPU时钟频率等
Options.addFSOptions(parser) # 添加FS模拟参数。如使用的内核、镜像文件等

# Add the ruby specific and protocol specific args
if '--ruby' in sys.argv:
Ruby.define_options(parser)

args = parser.parse_args()

第2行通过标准库argparse的ArgumentParser()方法初始化了parser类

第3、4行通过configs/common/Options.py模块中的两个方法中添加相应命令行选项。

第7行检查是否配置了ruby。如果配置了,则在第8行,通过confis/ruby/Ruby.py模块中的define_options()方法配置ruby相关的参数,其中,network相关参数也会配置。

最后,通过标准库argparse的解析函数对parser进行参数解析,得到了args,所有参数都存储在args中。

args 是一个包含所有命令行参数值的命名空间对象。每个命令行参数都成为这个对象的一个属性,可以通过 args.参数名 的形式访问。
在 gem5 的上下文中,这意味着 args 包含了模拟器运行所需的所有配置信息,如是否启用 Ruby 模拟、文件系统的配置等。

以上代码定义了argparse解析器并添加了命令行参数。然后它解析了这些参数,并基于这些参数来设置CPU类和内存类。

三、确定CPU和Memory类型

1
2
3
4
5
# system under test can be any CPU
(TestCPUClass, test_mem_mode, FutureClass) = Simulation.setCPUClass(args)

# Match the memories with the CPUs, based on the options for the test system
TestMemClass = Simulation.setMemClass(args)

四、配置benchmark

Benchmarks为一个字典,在configs/common/Benchmark.py中定义

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
if args.benchmark:
try:
bm = Benchmarks[args.benchmark]
except KeyError:
print("Error benchmark %s has not been defined." % args.benchmark)
print("Valid benchmarks are: %s" % DefinedBenchmarks)
sys.exit(1)
else:
if args.dual:
bm = [SysConfig(disks=args.disk_image, rootdev=args.root_device,
mem=args.mem_size, os_type=args.os_type),
SysConfig(disks=args.disk_image, rootdev=args.root_device,
mem=args.mem_size, os_type=args.os_type)]
else:
bm = [SysConfig(disks=args.disk_image, rootdev=args.root_device,
mem=args.mem_size, os_type=args.os_type)]

五、root

在gem5中,root是整个模拟环境的最顶层容器,它通常包含了模拟的计算机系统(如处理器、内存、总线等)以及与这些组件连接的所有设备和系统的配置。这样的结构设计允许gem5能够方便地访问和管理模拟的不同部分。

本质上,fs.py只是运行了最后一行的代码:

1
2
# [fs.py]
Simulation.run(args, root, test_sys, FutureClass)

其中,args是所有参数,root是仿真的硬件,test_sys是模拟的系统

如果模拟的是一个双系统(dual系统),那么root会通过调用makeDualRoot()函数被创建,这意味着会有两个系统(test_sys和drive_sys)并行运行在模拟中。
如果启用了分布式模拟(dist模式),则通过调用makeDistRoot()函数创建root,此时root代表的系统会作为分布式模拟中的一个节点。
如果模拟的是单个系统,那么root会简单地通过Root(full_system=True, system=test_sys)创建,其中test_sys是通过build_test_system()函数构建的测试系统

之后,它构建测试系统,可能还会构建驱动系统,最后创建一个root根对象来启动模拟。

在gem5中,root是整个模拟环境的最顶层容器,它通常包含了模拟的计算机系统(如处理器、内存、总线等)以及与这些组件连接的所有设备和系统的配置。

六、构建系统

构造系统是配置文件的主要工作,因此通过函数单独实现。

通过代码中的build_test_system()函数构造测试系统;如果双系统,还会通过build_drive_system()构建驱动系统。

并分别通过Root()、makeDualRoot()、makeDistRoot()构建单系统、双系统、分布式系统的root。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
np = args.num_cpus

test_sys = build_test_system(np)

if len(bm) == 2:
drive_sys = build_drive_system(np)
root = makeDualRoot(True, test_sys, drive_sys, args.etherdump)
elif len(bm) == 1 and args.dist:
# This system is part of a dist-gem5 simulation
root = makeDistRoot(test_sys,
args.dist_rank,
args.dist_size,
args.dist_server_name,
args.dist_server_port,
args.dist_sync_repeat,
args.dist_sync_start,
args.ethernet_linkspeed,
args.ethernet_linkdelay,
args.etherdump);
elif len(bm) == 1:
root = Root(full_system=True, system=test_sys)
else:
print("Error I don't know how to create more than 2 systems.")
sys.exit(1)

构造测试系统

不同的ISA不同,以X86为例:

第3行~第30行:首先通过makeLinuxX86System,创建一个基础的test_sys。

第32行~第62行:然后再指定一些细节,如cpu等。

第64行~第134行:如果使用ruby,test_sys有更多细节。

第136行~第148行:如果使用KVM加速,则设置KVM。

最后返回测试系统test_sys。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
def build_test_system(np):
cmdline = cmd_line_template()
if buildEnv['TARGET_ISA'] == "mips":
test_sys = makeLinuxMipsSystem(test_mem_mode, bm[0], cmdline=cmdline)
elif buildEnv['TARGET_ISA'] == "sparc":
test_sys = makeSparcSystem(test_mem_mode, bm[0], cmdline=cmdline)
elif buildEnv['TARGET_ISA'] == "riscv":
test_sys = makeBareMetalRiscvSystem(test_mem_mode, bm[0],
cmdline=cmdline)
elif buildEnv['TARGET_ISA'] == "x86":
test_sys = makeLinuxX86System(test_mem_mode, np, bm[0], args.ruby,
cmdline=cmdline)
elif buildEnv['TARGET_ISA'] == "arm":
test_sys = makeArmSystem(
test_mem_mode,
args.machine_type,
np,
bm[0],
args.dtb_filename,
bare_metal=args.bare_metal,
cmdline=cmdline,
external_memory=args.external_memory_system,
ruby=args.ruby,
vio_9p=args.vio_9p,
bootloader=args.bootloader,
)
if args.enable_context_switch_stats_dump:
test_sys.enable_context_switch_stats_dump = True
else:
fatal("Incapable of building %s full system!", buildEnv['TARGET_ISA'])

# Set the cache line size for the entire system
test_sys.cache_line_size = args.cacheline_size

# Create a top-level voltage domain
test_sys.voltage_domain = VoltageDomain(voltage = args.sys_voltage)

# Create a source clock for the system and set the clock period
test_sys.clk_domain = SrcClockDomain(clock = args.sys_clock,
voltage_domain = test_sys.voltage_domain)

# Create a CPU voltage domain
test_sys.cpu_voltage_domain = VoltageDomain()

# Create a source clock for the CPUs and set the clock period
test_sys.cpu_clk_domain = SrcClockDomain(clock = args.cpu_clock,
voltage_domain =
test_sys.cpu_voltage_domain)

if buildEnv['TARGET_ISA'] == 'riscv':
test_sys.workload.bootloader = args.kernel
elif args.kernel is not None:
test_sys.workload.object_file = binary(args.kernel)

if args.script is not None:
test_sys.readfile = args.script

test_sys.init_param = args.init_param

# For now, assign all the CPUs to the same clock domain
test_sys.cpu = [TestCPUClass(clk_domain=test_sys.cpu_clk_domain, cpu_id=i)
for i in range(np)]

if args.ruby:
bootmem = getattr(test_sys, '_bootmem', None)
Ruby.create_system(args, True, test_sys, test_sys.iobus,
test_sys._dma_ports, bootmem)

# Create a seperate clock domain for Ruby
test_sys.ruby.clk_domain = SrcClockDomain(clock = args.ruby_clock,
voltage_domain = test_sys.voltage_domain)

# Connect the ruby io port to the PIO bus,
# assuming that there is just one such port.
test_sys.iobus.mem_side_ports = test_sys.ruby._io_port.in_ports

for (i, cpu) in enumerate(test_sys.cpu):
#
# Tie the cpu ports to the correct ruby system ports
#
cpu.clk_domain = test_sys.cpu_clk_domain
cpu.createThreads()
cpu.createInterruptController()

test_sys.ruby._cpu_ports[i].connectCpuPorts(cpu)

else:
if args.caches or args.l2cache:
# By default the IOCache runs at the system clock
test_sys.iocache = IOCache(addr_ranges = test_sys.mem_ranges)
test_sys.iocache.cpu_side = test_sys.iobus.mem_side_ports
test_sys.iocache.mem_side = test_sys.membus.cpu_side_ports
elif not args.external_memory_system:
test_sys.iobridge = Bridge(delay='50ns', ranges = test_sys.mem_ranges)
test_sys.iobridge.cpu_side_port = test_sys.iobus.mem_side_ports
test_sys.iobridge.mem_side_port = test_sys.membus.cpu_side_ports

# Sanity check
if args.simpoint_profile:
if not ObjectList.is_noncaching_cpu(TestCPUClass):
fatal("SimPoint generation should be done with atomic cpu")
if np > 1:
fatal("SimPoint generation not supported with more than one CPUs")

for i in range(np):
if args.simpoint_profile:
test_sys.cpu[i].addSimPointProbe(args.simpoint_interval)
if args.checker:
test_sys.cpu[i].addCheckerCpu()
if not ObjectList.is_kvm_cpu(TestCPUClass):
if args.bp_type:
bpClass = ObjectList.bp_list.get(args.bp_type)
test_sys.cpu[i].branchPred = bpClass()
if args.indirect_bp_type:
IndirectBPClass = ObjectList.indirect_bp_list.get(
args.indirect_bp_type)
test_sys.cpu[i].branchPred.indirectBranchPred = \
IndirectBPClass()
test_sys.cpu[i].createThreads()

# If elastic tracing is enabled when not restoring from checkpoint and
# when not fast forwarding using the atomic cpu, then check that the
# TestCPUClass is DerivO3CPU or inherits from DerivO3CPU. If the check
# passes then attach the elastic trace probe.
# If restoring from checkpoint or fast forwarding, the code that does this for
# FutureCPUClass is in the Simulation module. If the check passes then the
# elastic trace probe is attached to the switch CPUs.
if args.elastic_trace_en and args.checkpoint_restore == None and \
not args.fast_forward:
CpuConfig.config_etrace(TestCPUClass, test_sys.cpu, args)

CacheConfig.config_cache(args, test_sys)

MemConfig.config_mem(args, test_sys)

if ObjectList.is_kvm_cpu(TestCPUClass) or \
ObjectList.is_kvm_cpu(FutureClass):
# Assign KVM CPUs to their own event queues / threads. This
# has to be done after creating caches and other child objects
# since these mustn't inherit the CPU event queue.
for i,cpu in enumerate(test_sys.cpu):
# Child objects usually inherit the parent's event
# queue. Override that and use the same event queue for
# all devices.
for obj in cpu.descendants():
obj.eventq_index = 0
cpu.eventq_index = i + 1
test_sys.kvm_vm = KvmVM()

return test_sys

构造驱动系统

与测试系统的构造大致一致,先构建基本系统,然后再指定一些细节。

最后返回驱动系统drive_sys

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
def build_drive_system(np):
# driver system CPU is always simple, so is the memory
# Note this is an assignment of a class, not an instance.
DriveCPUClass = AtomicSimpleCPU
drive_mem_mode = 'atomic'
DriveMemClass = SimpleMemory

cmdline = cmd_line_template()
if buildEnv['TARGET_ISA'] == 'mips':
drive_sys = makeLinuxMipsSystem(drive_mem_mode, bm[1], cmdline=cmdline)
elif buildEnv['TARGET_ISA'] == 'sparc':
drive_sys = makeSparcSystem(drive_mem_mode, bm[1], cmdline=cmdline)
elif buildEnv['TARGET_ISA'] == 'x86':
drive_sys = makeLinuxX86System(drive_mem_mode, np, bm[1],
cmdline=cmdline)
elif buildEnv['TARGET_ISA'] == 'arm':
drive_sys = makeArmSystem(drive_mem_mode, args.machine_type, np,
bm[1], args.dtb_filename, cmdline=cmdline)

# Create a top-level voltage domain
drive_sys.voltage_domain = VoltageDomain(voltage = args.sys_voltage)

# Create a source clock for the system and set the clock period
drive_sys.clk_domain = SrcClockDomain(clock = args.sys_clock,
voltage_domain = drive_sys.voltage_domain)

# Create a CPU voltage domain
drive_sys.cpu_voltage_domain = VoltageDomain()

# Create a source clock for the CPUs and set the clock period
drive_sys.cpu_clk_domain = SrcClockDomain(clock = args.cpu_clock,
voltage_domain =
drive_sys.cpu_voltage_domain)

drive_sys.cpu = DriveCPUClass(clk_domain=drive_sys.cpu_clk_domain,
cpu_id=0)
drive_sys.cpu.createThreads()
drive_sys.cpu.createInterruptController()
drive_sys.cpu.connectBus(drive_sys.membus)
if args.kernel is not None:
drive_sys.workload.object_file = binary(args.kernel)

if ObjectList.is_kvm_cpu(DriveCPUClass):
drive_sys.kvm_vm = KvmVM()

drive_sys.iobridge = Bridge(delay='50ns',
ranges = drive_sys.mem_ranges)
drive_sys.iobridge.cpu_side_port = drive_sys.iobus.mem_side_ports
drive_sys.iobridge.mem_side_port = drive_sys.membus.cpu_side_ports

# Create the appropriate memory controllers and connect them to the
# memory bus
drive_sys.mem_ctrls = [DriveMemClass(range = r)
for r in drive_sys.mem_ranges]
for i in range(len(drive_sys.mem_ctrls)):
drive_sys.mem_ctrls[i].port = drive_sys.membus.mem_side_ports

drive_sys.init_param = args.init_param

return drive_sys

se.py配置文件

fs_linux.py配置文件

拉丁文缩写

i.e. 也就是说

id est(“that is” , “in other words”。进一步解释用,意为:也就是)的缩写。目的是用来进一步解释前面所说的观点(不像后文的e.g.那样引入实例来形象化),意思是“那就是说,换句话说”。

例句1:Each of these items are actionable, i.e. you can actually do them.

e.g. 例如

e.g.exempli gratia(”for example; for instance;such as”。举例用,意为:例如)的缩写,其目的用若干例子来让前面说法更具体,更易感知。

例句1: I like sports, e.g., football.

etc. 等等

etc.et cetera(“and so forth; and the others; and other things; and the rest; and so on”。举例用,意为:等等)的缩写。它放在列表的最后,表示前面的例子还没列举完,最后加个词“等等”。

例句1: I need to go to the store and buy some pie, milk, cheese, etc.

例句1:I like to eat boardwalk food, i.e., funnel cake and french fries.
例句2:I like to eat boardwalk food, e.g., funnel cake and french fries.

例句1表示只有 funnel cake and french fries这两种boardwalk食物,而且这两种我都喜欢。例句2表示我喜欢boardwalk食物,比如 funnel cake and french fries;但是诸如snow cones and corn dogs等其他类型,我也可能喜欢。

viz. 即

viz.videlicet( “namely”, “towit”, “precisely”, “that is to say”。进一步解释用,意为:即)的缩写,与e.g.不同,viz位于同位列表之前,要把它前面单词所包含的项目全部列出。(不常用,渐渐被i.e.取代)

例句1:The school offers two modules in Teaching English as a Foreign Language, viz. Principles and Methods of Language Teaching and Applied Linguistics.(该校提供两个模块用于英语作为外语的教学,即语言教学的原理方法和应用语言学。)
例句2: In this paper, a new TDNN architecture with two input variable, viz. wave form and its phase difference, is developed to reduce the grain noise.(本文提出了一种新的TDNN结构用于降低粗晶材料结构噪声,该结构具有波形及其相位差组成的双变量输入。)

et al. 等其他人

et al.et alia(”and others; and co-workers”。在引用文献作者时用,意为:等其他人)的缩写。它几乎都是在列文献作者时使用,即把主要作者列出后,其它作者全放在et al. 里面。

人的场合用et al,而无生命的场合用etc.(et cetera)。

例句1: These results agree with the ones published by Pelon et al. (2002).
例句2: Clegg et al. (1995) explain that in the electronics industry linear-programming models can be used to analyse the viability of the recovered parts in remanufacturing.(克莱格等人(1995)解释说,电子行业的线性规划模型可以用来分析在再制造过程中回收零部件的可行性。)

片上网络(NoC)

单芯片多处理器系统(Chip Multiprocessor,CMP)通过在单款芯片上集成多个处理器和以挖掘线程级并行和任务级并行。

然而,传统的总线和交叉开关等片上互联结构的可扩展性较差,它们只能满足较少量的计算核的通信需求。因此,人们将“报文交换”的思想引入片上互联结构中,提出了“片上网络”(Network on Chip,NoC)的概念。

结构

网络的每个节点包含一个处理器核和一个路由器。处理器核通过网络接口将报文注入网络,或者从网络接收报文,路由器通过物理链路在网络节点间转发报文。

优点

与传统的总线和交叉开关等片上互联结构相比,片上网络具有以下特点和优势:

  1. 更高的可扩展性和可重用性。当增加处理器核数时,只需要在片上网络中增加相应数量的路由器和网络接口,不需要重新设计整个网络。片上网络的开销与网络节点数目成线性关系,易于扩展。新增路由器与现有路由器的结构基本相同,可以复用路由器设计。
  2. 通过路由器将芯片长连线切分成多条短连线,从而可以控制通信延迟和功耗。
  3. 采用分布式控制策略,比采用集中式控制策略的总线支持更高的事务并发性。
  4. 由于片上网络时相互独立的,它可以采用全局异步局部同步(Global Asynchronous Local Synchronous,GALS)的时钟策略,将网络划分成多个细粒度时钟域,独立控制每个时钟域的电压和频率,灵活管理功耗。

片上网络与片外网络的差异

片上网络与片外网络都采用了“报文交换”的思想,但有许多差异:

  1. 片上网络互联的是单个芯片上的多个处理器;而片外网络是用于超级计算机和集群系统的。
  2. 链路资源差异。现代集成电路的多层互联金属层为片上网络提供了丰富的连线资源,允许相邻路由器间的链路带宽达到数百位;然而对于片外网络,受限于芯片引脚数目限制,片外网络的链路带宽仅为数十位。
  3. 延迟构成差异。片上网络相邻路由器之间距离较短,一般可以在一个时钟周期内完成链路传输;片外网络相邻路由器之间距离较大,链路传输需要多个时钟周期。然而较小的片上网络链路延迟增加了路由器延迟对新能的影响。
  4. 串行总线Vs并行总线。片上网络主要采用并行总线,但由于片外并行总线的串扰难以控制,片外网路大多采用串行总线。
  5. 面积和功耗资源。片上网络与处理器核竞争芯片有限的面积和功耗资源,与采用独立芯片实现的片外网络路由器相比,片上网络路由器面临着更为严格的面积和功耗限制。

Ruby

Ruby是一个详细的内存子系统的模拟模型。可以通过各种替换策略、一致性协议实现、互连网络、DMA和内存控制器、发起内存请求和处理响应的各种定序器来建模包含(inclusive)/独占(exclusive)缓存层次结构。这些模型是模块化的、灵活的和高度可配置的。

Ruby有以下三个特点:

  • 关注点分离。将内存系统的各个模型模块化,例如,一致性协议规范与替换策略和缓存索引映射是分开的,网络拓扑结构与实现也是分开指定的。
  • 丰富的可配置性。几乎所有影响内存层次结构的功能和时序都可以控制。
  • 快速的原型设计。使用一种高级规范语言SLICC来指定各种控制器的功能。

SLICC + 一致性协议

SLICC:Specification Language for Implementing Cache Coherence,是一种特定领域的语言,用于指定缓存一致性协议。

缓存一致性协议以状态机的方式工作,而SLICC用于指定状态机的行为。SLICC文件以“.sm”结尾,它们是状态机文件。每个文件描述状态、某些事件从开始状态到结束状态的转换,以及在转换过程中要采取的操作。

每个一致性协议都由多个SLICC状态机文件组成。这些文件是用SLCC编译器编译的,该编译器是用Python编写的,也是gem5源代码的一部分。SLIC编译器获取状态机文件输出一组C++文件,这些文件与gem5的所有其他文件一起编译。这些文件包括SimObject声明文件以及SimObjects和其他C++对象的实现文件。

实现一致性协议的步骤

以下是对官网教程gem5: Introduction to Ruby的内容整理,教程的目标是实现MSI协议,协议的具体内容可以在《A Primer on Memory Consistency and Cache Coherence》书的第8.2节找到 (pages 141-149)

1. 注册状态机

MSI协议是通过SLICC语言编写的状态机文件实现的,这些状态机文件是用SLCC编译器编译的,会通过scons与gem5其他文件一起编译,因此需要为SCons创建一个文件,以便知道要编译什么。这里我们创建一个Sconsopts文件,而不是Sconscript文件,这是因为Sconsopts会在Sconscript之前执行,而我们也需要在编译gem5之前编译状态机文件。

1
2
3
4
5
Import('*')
# 注册协议名‘MSI’,Scons将假定一个名为MSI.slicc的文件
main.Append(ALL_PROTOCOLS=['MSI'])
# 告诉SCons在当前目录中查找要传递给SLCC编译器的文件。
main.Append(PROTOCOL_DIRS=[Dir('.')])

2.编写状态机文件

编写状态机文件是实现一致性协议的最主要工作,状态机文件通常包含以下几个部分:

  • Parameters

    These are the parameters for the SimObject that will be generated from the SLICC code.

  • Declaring required structures and functions

    This section declares the states, events, and many other required structures for the state machine.

  • In port code blocks

    Contain code that looks at incoming messages from the (in_port) message buffers and determines what events to trigger.

  • Actions

    These are simple one-effect code blocks (e.g., send a message) that are executed when going through a transition.

  • Transitions

    Specify actions to execute given a starting state and an event and the final state. This is the meat of the state machine definition.

Over the next few sections we will go over how to write each of these components of the protocol.

2.1 状态机声明

创建一个名为MSI-call.sm的文件,并按以下格式声明状态机

1
2
3
4
5
machine(MachineType:L1Cache, "MSI cache")
: <parameters>
{
<All state machine code>
}

MachineType:L1Cache将状态机命名为L1Cache,SLICC 将使用该名称为我们生成许多不同的对象。例如,一旦编译了这个文件,就会有一个新的 SimObject: L1Cache_Controller 作为缓存控制器。这个声明中还包括对这个状态机的描述: “ MSI cache”。

2.2 状态机参数声明

状态机参数的声明在冒号(:)之后

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
machine(MachineType:L1Cache, "MSI cache")
: Sequencer *sequencer;
CacheMemory *cacheMemory;
bool send_evictions;
# “To” buffer
MessageBuffer * requestToDir, network="To", virtual_network="0", vnet_type="request";
MessageBuffer * responseToDirOrSibling, network="To", virtual_network="2", vnet_type="response";
# “From” buffer
MessageBuffer * forwardFromDir, network="From", virtual_network="1", vnet_type="forward";
MessageBuffer * responseFromDirOrSibling, network="From", virtual_network="2", vnet_type="response";
# “To” buffer
MessageBuffer * mandatoryQueue;
{

}
  • 排序器(Sequencer)

    Sequencer是一个带有从端口的gem5 MemObject,因此它可以接受来自其他对象的内存请求。Swquencer接受来自CPU(或其他主端口)的请求,并将gem5数据包转换为RubyRequest。最后,RubyRequest被推送到状态机的强制队列(commandoryQueue)中。

  • 缓存数据(Cache Memory)

    用于保存缓存数据(即缓存条目)的内容。

  • 消息缓冲区(MessageBuffer)

    消息缓冲区是状态机和Ruby网络之间的接口。通过消息缓冲区发送和接收消息。因此,对于我们协议中的每个虚拟通道,我们都需要一个单独的消息缓冲区。

    虚拟网络的作用是防止死锁。MSI协议需要三个不同的虚拟网络。在该协议中,最高优先级是响应Response(虚拟网络2),其次是转发的请求Forwarded Requests(虚拟网络1),然后请求Requests具有最低优先级(虚拟网络0)。

    代码中有两个”To” buffer,两个”From” buffer和一个Special buffer。其中,”To” buffer类似于gem5中的主端口,是此控制器用来向系统中的其他控制器发送消息的消息缓冲区;”From” buffer类似于gem5中的从端口,是此控制器用来接收系统中其他控制器发送的消息的消息缓冲区;对于Special buffer,Sequencer使用此消息缓冲区将gem5数据包转换为Ruby请求。与其他消息缓冲区不同,commandoryQueue不连接到Ruby网络,并且,此消息缓冲区的名称是硬编码的,必须为“commandoryQueue”

    两个”To” buffer一个用于低优先级请求,另一个用于高优先级响应。优先级基于其他控制器查看消息缓冲区的顺序。类似地,两个”From” buffer使该缓存可以通过两种不同的方式接收消息,要么作为来自目录的转发请求,要么作为对该控制器发出的请求的响应。响应的优先级高于转发的请求。

如前所述,状态机文件在经过SLICC编译后会成为Simobject文件,以上代码编译后产生的文件为L1Cache_Controller.py,内容如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
from m5.params import *
from m5.SimObject import SimObject
from Controller import RubyController

class L1Cache_Controller(RubyController):
type = 'L1Cache_Controller'
cxx_header = 'mem/protocol/L1Cache_Controller.hh'
sequencer = Param.RubySequencer("")
cacheMemory = Param.RubyCache("")
send_evictions = Param.Bool("")
requestToDir = Param.MessageBuffer("")
responseToDirOrSibling = Param.MessageBuffer("")
forwardFromDir = Param.MessageBuffer("")
responseFromDirOrSibling = Param.MessageBuffer("")
mandatoryQueue = Param.MessageBuffer("")

对这个文件,不能做任何修改!

2.3 状态声明

通过state_declaration声明状态机的所有稳定和瞬态。对瞬态的命名遵循Sorin等人的命名约定。例如,瞬态“IM_AD”对应于在等待确认(A)和数据(D)时从无效(I)移动到已修改(M)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
state_declaration(State, desc="Cache states") {
I, AccessPermission:Invalid,
desc="Not present/Invalid";

// States moving out of I
IS_D, AccessPermission:Invalid,
desc="Invalid, moving to S, waiting for data";
IM_AD, AccessPermission:Invalid,
desc="Invalid, moving to M, waiting for acks and data";
IM_A, AccessPermission:Busy,
desc="Invalid, moving to M, waiting for acks";

S, AccessPermission:Read_Only,
desc="Shared. Read-only, other caches may have the block";

// States moving out of S
SM_AD, AccessPermission:Read_Only,
desc="Shared, moving to M, waiting for acks and 'data'";
SM_A, AccessPermission:Read_Only,
desc="Shared, moving to M, waiting for acks";

M, AccessPermission:Read_Write,
desc="Modified. Read & write permissions. Owner of block";

// States moving to Invalid
MI_A, AccessPermission:Busy,
desc="Was modified, moving to I, waiting for put ack";
SI_A, AccessPermission:Busy,
desc="Was shared, moving to I, waiting for put ack";
II_A, AccessPermission:Invalid,
desc="Sent valid data before receiving put ack. "Waiting for put ack.";
}

每个状态都有一个相关的访问权限AccessPermission,包括无效(Invalid)、不存在(NotPresent)、忙(Busy)、只读(Read_Only)、读写(Read_Write),访问权限用于对缓存进行功能访问functional accesses。对于功能访问,将检查所有缓存,看它们是否有具有匹配地址的相应块。

2.4 事件声明

声明由该缓存控制器的传入消息触发的所有事件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
enumeration(Event, desc="Cache events") {
// From the processor/sequencer/mandatory queue
Load, desc="Load from processor";
Store, desc="Store from processor";

// Internal event (only triggered from processor requests)
Replacement, desc="Triggered when block is chosen as victim";

// Forwarded request from other cache via dir on the forward network
FwdGetS, desc="Directory sent us a request to satisfy GetS. We must have the block in M to respond to this.";
FwdGetM, desc="Directory sent us a request to satisfy GetM. We must have the block in M to respond to this.";
Inv, desc="Invalidate from the directory.";
PutAck, desc="Response from directory after we issue a put. This must be on the fwd network to avoid deadlock.";

// Responses from directory
DataDirNoAcks, desc="Data from directory (acks = 0)";
DataDirAcks, desc="Data from directory (acks > 0)";

// Responses from other caches
DataOwner, desc="Data from owner";
InvAck, desc="Invalidation ack from other cache after Inv";

// Special event to simplify implementation
LastInvAck, desc="Triggered after the last ack is received";
}

状态和事件可以参考表8.3的缓存控制器转换表

image-20231204190803198

2.5 用户定义结构体

下面定义的是在这个控制器的其他地方用到的结构体

Entry

这是存储在CacheMemory中的结构。它只需要包含数据和状态

1
2
3
4
structure(Entry, desc="Cache entry", interface="AbstractCacheEntry") {
State CacheState, desc="cache state";
DataBlock DataBlk, desc="Data in the block";
}

TBE

TBE是“事务缓冲区条目”。这存储了瞬态期间所需的信息。这就像MSHR。它在该协议中起MSHR的作用,但该条目也被分配用于其他用途。在该协议中,它将存储状态(通常需要)、数据(通常也需要)以及该块当前正在等待的ack数。

1
2
3
4
5
structure(TBE, desc="Entry for transient requests") {
State TBEState, desc="State of block";
DataBlock DataBlk, desc="Data for the block. Needed for MI_A";
int AcksOutstanding, default=0, desc="Number of acks left to receive.";
}

TBETable

还需要一个存放所有TBE的地方。其中external="yes"表明这是一个外部定义的类;它是在SLICC之外的C++中定义的。因此,我们需要声明我们将要使用它,并声明我们将对其调用的任何函数。您可以在src/mem/ruby/structures/TBETable.hh中找到TBETable的代码。

1
2
3
4
5
6
structure(TBETable, external="yes") {
TBE lookup(Addr);
void allocate(Addr);
void deallocate(Addr);
bool isPresent(Addr);
}

外部函数声明

如果我们要在文件的其余部分中使用AbstractController中的任何函数,都需要声明它们。

1
2
3
4
5
Tick clockEdge();
void set_cache_entry(AbstractCacheEntry a);
void unset_cache_entry();
void set_tbe(TBE b);
void unset_tbe();

样板代码

下面这一组样板工具代码很少在不同的协议之间发生变化。在AbstractController中,我们必须实现一组纯虚拟的函数。

  • getState

Given a TBE, cache entry, and address return the state of the block. This is called on the block to decide which transition to execute when an event is triggered. Usually, you return the state in the TBE or cache entry, whichever is valid.

给定IBE,entry和地址,返回地址所在缓存块的状态

  • setState

Given a TBE, cache entry, and address make sure the state is set correctly on the block. This is called at the end of the transition to set the final state on the block.

  • getAccessPermission

Get the access permission of a block. This is used during functional access to decide whether or not to functionally access the block. It is similar to getState, get the information from the TBE if valid, cache entry, if valid, or the block is not present.

  • setAccessPermission

Like getAccessPermission, but sets the permission.

  • functionalRead

Functionally read the data. It is possible the TBE has more up-to-date information, so check that first. Note: testAndRead/Write defined in src/mem/ruby/slicc_interface/Util.hh

  • functionalWrite

Functionally write the data. Similarly, you may need to update the data in both the TBE and the cache entry.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
State getState(TBE tbe, Entry cache_entry, Addr addr) {
// The TBE state will override the state in cache memory, if valid
if (is_valid(tbe)) { return tbe.TBEState; }
// Next, if the cache entry is valid, it holds the state
else if (is_valid(cache_entry)) { return cache_entry.CacheState; }
// If the block isn't present, then it's state must be I.
else { return State:I; }
}

void setState(TBE tbe, Entry cache_entry, Addr addr, State state) {
if (is_valid(tbe)) { tbe.TBEState := state; }
if (is_valid(cache_entry)) { cache_entry.CacheState := state; }
}

AccessPermission getAccessPermission(Addr addr) {
TBE tbe := TBEs[addr];
if(is_valid(tbe)) {
return L1Cache_State_to_permission(tbe.TBEState);
}
Entry cache_entry := getCacheEntry(addr);
if(is_valid(cache_entry)) {
return L1Cache_State_to_permission(cache_entry.CacheState);
}
return AccessPermission:NotPresent;
}

void setAccessPermission(Entry cache_entry, Addr addr, State state) {
if (is_valid(cache_entry)) {
cache_entry.changePermission(L1Cache_State_to_permission(state));
}
}

void functionalRead(Addr addr, Packet *pkt) {
TBE tbe := TBEs[addr];
if(is_valid(tbe)) {
testAndRead(addr, tbe.DataBlk, pkt);
} else {
testAndRead(addr, getCacheEntry(addr).DataBlk, pkt);
}
}

int functionalWrite(Addr addr, Packet *pkt) {
int num_functional_writes := 0;
TBE tbe := TBEs[addr];
if(is_valid(tbe)) {
num_functional_writes := num_functional_writes +
testAndWrite(addr, tbe.DataBlk, pkt);
return num_functional_writes;
}

以下是对这些状态和转换的高层次描述:

img

2.6 输入输出端口

参考链接🔗

gem5模拟器入门(四) - 知乎 (zhihu.com)

EM5官方教程全流程: part 3 RUBY-CSDN博客

对称式共享存储器系统支持共享数据和私有数据的缓存。

  • 私有数据被单个处理器使用。

    好处:把一个私有数据从存储器缓存到Cache之后,对该数据的访问就可以在Cache中进行,因此减少了平均访存时间和对存储器带宽的要求。

    同时因为没有其他处理器使用这些数据,程序的行为与单处理器系统相同。

  • 共享数据被多个处理器所使用。

    通过读写共享数据完成处理器之间的通信。

    好处:共享数据装载到Cache中时,会在多个Cache中形成副本,这样1. 一方面会减少访问时间并降低对存储器带宽的要求,2. 还可以减少多个处理器同时读共享数据时所产生的冲突。

    但是,共享数据进入Cache也产生了一个新问题——Cache的一致性问题。

一、什么是多处理器的Cache一致性?

存储器一致性定义一

一个模糊且简单的定义:如果在一个存储器系统中读取任何一个数据项的返回结果总是最近写入的数值,那么就可以认为该存储器具有一致性。(定义一)

这个定义包含了两个方面:一方面是一致(coherence),它定义了读操作可以返回什么样的数值【what】;另一方面是连贯(consistency),它定义了写入的数值什么时候才能被读操作返回【when】。

一致定义了对同一个存储器地址进行的读写操作行为;连贯定义了关于访问其他存储器地址的读写操作。

存储器一致性定义二

如果一个存储器系统满足以下条件,那么认为该存储器系统是一致的:(定义二)

  1. 处理器P对地址X的写操作后面紧跟着处理器P对X的读操作,而且在这次读操作和写操作之间没有其他处理器对X进行写操作,这时读操作总是返回P写入的数值。

    这个性质保证了程序的顺序,即使在单处理器中也要保证这个性质。

  2. 在其他处理器对X的写操作后,处理器P对X执行读操作,这两个操作之间有足够的间隔并且没有其他处理器对X进行写操作,这是,读操作返回的是写入的数值。

    这个性质给出了一致性的概念,如果一个处理器对某个数据执行读操作时,总是的读入旧的数据,那么该存储器是非一致的。

  3. 对同一地址的写操作是串行执行的;也就是说,任何两个处理器对同一地址的两个写操作在所有处理器看来都有相同的顺序。例如,对同一地址先后写入数值1和数值2,处理器绝不会从该地址中先读出2再读出1。

这个性质称为写串行化,保证同一地址所写的顺序对任何处理器来说都是相同的。

在三个条件之外,还又两个假设:

假设:

  1. 直到所有处理器都看到了写操作之后一个写操作才算完成,并且后续的写操作才能开始。
  2. 处理器不会因为其他存储操作而改变写操作的顺序。

这两个假设意味着如果处理器向地址A写入后又向地址B写入,所有能看到B中新值的处理器必须也能看到A的新值。

定义一和定义二的联系

定义二的三个条件已经体现了定义一的一致方面【what】,定义一的连贯方面,即什么时候才能获得写进去的值【when】,则体现在条件2的读写操作之间有足够的时间间隔上面。

通常不可能要求在一个处理器写入X的数值后,其他处理器就能即刻在X上读出这一值。因此,如果一个处理器对X进行写后,很短时间内另一处理器对X进行读,那么无法保证该读操作能返回写入的数值,因为这一刻写入的数据是怎知可能还没被处理器发送出去。

总的来说,缓存一致性机制需要解决的问题就是 2 点:

  • 特性 1 - 写传播(Write Propagation): 每个 CPU 核心的写入操作,需要传播到其他 CPU 核心;
  • 特性 2 - 写事务串行化(Transaction Serialization): 各个 CPU 核心所有写入操作的顺序,在所有 CPU 核心看起来是一致。

如果没有写串行化,举个例子:假如 CPU 有 4 个核心,Core 1 将共享数据修改为 1000,随后 Core 2 将共享数据修改为 2000。在写传播下,“修改为 1000” 和 “修改为 2000” 两个事务会同步到 Core 3 和 Core 4。但是,如果没有事务串行化,不同核心收到的事务顺序可能是不同的,最终数据还是不一致。

二、实现一致性的方案

在一致的多处理机中,Cache提供了共享数据的迁移和复制功能。

共享数据的迁移是把远程的共享数据项备份放在本处理器局部的Cache中使用,从而降低了对远程共享数据的访问延迟。

共享数据的复制是把多个处理器需要同时读取的共享数据项的备份放在各自的局部Cache中使用。

对多个处理器维护一致性的协议称为Cache一致性协议(Cache-coherent Protocal)

目录协议与监听协议

(1)监听(Snooping)——每个Cache除了包含物理存储器中块的数据备份之外,也保存着每个块的共享状态信息。Cache通常连在共享存储器的总线上,各个Cache控制器通过监听总线来判断他们是否有总线上请求的数据块。【每个数据块状态是分散在各个Cache中的】

对于监听协议,常用于集中式共享存储器体系结构,因为可以利用已有的物理连接(总线)来进行广播,但因为广播带来的带宽压力大,所以监听协议的可扩展性较差。

(2)目录(Directory)——物理存储器中共享数据块的状态及相关信息均被保存集中地在一个称为目录的地方。【每个数据块状态是集中在目录中的】

对于目录协议,常用于分布式共享存储器体系结构,通过目录避免了广播操作,减小了带宽负担,可扩展性好。

实现Cache一致性协议的关键是跟踪共享数据块的状态。目前有两类协议,采用了不同的共享数据状态跟踪技术:

写作废协议与写更新协议

可通过两种方法来维持一致性要求:

(1)写作废协议(Write Invalidate)——在一个处理器写某个数据项之前保证它对该数据项有唯一的访问权,唯一的访问权保证了在进行写后不存在其他可读或可写的备份,因为别的备份都作废了。又叫写无效。

(2)写更新协议(Write Update)——当一个处理器写某数据项时。通过广播使其他Cache中所有对应的该数据项备份进行更新。又叫写广播。

由于写更新协议必须将所有写操作广播给共享Cache,需要更大的带宽,所以大多处理器都选择写作废协议。

三、监听协议

由于监听协议需要广播,对总线带宽的需求大,因此仅适用于小规模的多处理机,即集中式共享存储器系统。

监听协议的关键是利用总线或其他广播媒介进行作废操作。当某个处理器进行写数据时,必须先获得总线的控制权,然后将要作废的数据块的地址放在总线上。其他处理器一致监听总线,他们检测地址所对应的数据是否在它们的Cache中,若在,则作废相应数据块。

当写Cache未命中时,除了作废其他处理器上相应的Cache数据块以外,还要从存储器取出该数据块。

  • 对于写直达Cache,因为所有写的数据同时被写回主存,则从主存中总可以取到最新的数据值。

  • 对于写回Cache,因为最新之可能在某个处理器的Cache中,也可能在主存中,所以得到数据的最新值会困难一些。在写回Cache失效时可使用相同的监听机制:1. 当请求处理器的Cache发生写失效后,广播该数据块的地址。2. 其他处理器都监听放在总线上的地址,如果某个处理器发现它含有被请求数据块的一个已经修改过的备份(即修改过了该数据块但还未写回内存),它九江这个数据块送给发出读请求的处理器,并停止其对主存的访问请求。

相比于写直达Cache,写回Cache实现一致性有两个缺点:

  1. 实现复杂度显然更高
  2. 写回Cache从处理器Cache中重新找回数据块的时间通常比写直达Cache从共享存储器中找回数据块的时间长

但写回Cache所需的存储器带宽较低的特点使得其在多处理机实现上很受欢迎。

写回Cache条件下的监听协议实现

利用总线实现写传播和写事务串行化:

  • 写传播 - 总线嗅探: 总线除了能在一个主模块和一个从模块之间传输数据,还支持一个主模块对多个从模块写入数据,这种操作就是广播。要实现写传播,其实就是将所有的读写操作广播到所有 CPU 核心,而其它 CPU 核心时刻监听总线上的广播,再修改本地的数据;
  • 事务串行化 - 总线仲裁: 总线的独占性要求同一时刻最多只有一个主模块占用总线,天然地会将所有核心对内存的读写操作串行化。如果多个核心同时发起总线事务,此时总线仲裁单元会对竞争做出仲裁,未获胜的事务只能等待获胜的事务处理完成后才能执行。

使用的辅助结构

  • 有效位

    在写回Cache的数据块中,有效位被用于指示该块是否有效。利用有效位,可以使作废的处理很简单,只需要将该位置为无效即可。

  • 状态位

    为了分辨某个数据块所处的状态,还要给每个块增加一个特殊的状态位。

  • 降低冲突

    因为每次总线任务均要检查Cache的地址位,这可能与CPU对Cache的访问冲突,可通过以下两种技术之一降低冲突:

    1. 复制标志位:将Cache的标志位复制一份,一份正常用于Cache访问,另一份用于监听,两个任务可以并行对标志位进行读。但代价就是修改标志位的时候也必须同时修改两份,同时如果两个任务修改标志位冲突,则非抢先者将被挂起。
    2. 多级包含Cache:采用多级Cache,通常为两级,靠近CPU的第一级Cache是较远的第二级Cache的一个子集。于是,监听可针对第二级Cache进行,而处理器的大多数访问针对第一季Cache,极大地避免了冲突。但是,如果监听命中第二级Cache,它必须垄断对各级Cache的访问,更新块状态并可能写回数据,这通常要挂起处理器对Cache的访问。

    可采用将第二级Cache中的标志位复制,会更有效地减少CPU和监听之间的冲突。

    判断数据块是否共享,可以帮助写操作判断是否需要发送作废操作——当对共享数据块进行写操作时,Cache会在总线上发送一个作废操作,并把该块标记为专有(非共享);当对专有数据块进行写操作时,由于只有该Cache有数据块的唯一副本,所以不用发送作废操作到总线上了。这样避免了发送作废操作,可以节省时间和带宽。

一致性协议本质上是一个状态机,具有若干状态。不同的协议具有不同的状态组合和转换策略。

对于数据块状态的描述,可以用以下四个特征来编码:

  • 有效性(Validity):有效的块含有数据的最新值,可以被读。
  • 肮脏性(Dirtiness):如果一个Cache中的块与在内存中对应的块是不同的,那么意味着Cache中的块是被修改过的、最新的、有效的,还未被写入内存,而内存中的对应块是旧的、无效的。因为Cache中的块被修改了,所以称其为“脏”的。
  • 独占性(Exclusivity):如果缓存块是系统中该块的唯一私有缓存副本,则该缓存块是独占的。(它也可能出现在内存中,但不可能出现在其他Cache中)。
  • 所有权(Ownership):如果缓存控制器(或内存控制器)负责响应对该块的一致性请求,那么它就是该块的所有者。

稳态

一致性协议常见稳定状态:

  • 共享状态(S)是指块是同于主存的,该块可能还在其他Cache中,也可能仅在该Cache中
  • 独占状态(E)是指块是同于主存的,仅在该Cache中
  • 修改状态(M)是指块是不同与主存的,即在Cache中已经被更新;修改状态暗示了此时的块已经被独占,即仅在该Cache中
  • 无效状态(I)是指块里的数据是旧的,已经失效了

NB:

“同于主存”是指在Cache中的块与在内存中对应的块是相同的。有时也称是“干净”的

“不同于主存”是指在Cache中的块与在内存中对应的块是不同的,内存中的块是旧的、无效的。有时也称是“脏”的

M (Modified):该块是有效的、排他的、拥有的,并且可能是脏的。 该块可以被读取或写入。 缓存具有块的唯一有效副本,缓存必须响应对块的请求,并且 LLC/内存中的块副本可能是陈旧的。

S (Shared):该块有效但不排他,不脏,不拥有。缓存具有块的只读副本。其他缓存可能具有该块的有效只读副本。

I (Invalid):块无效。缓存要么不包含块,要么包含可能无法读取或写入的陈旧副本。在本入门书中,我们不区分这两种情况,尽管有时前一种情况可以表示为“不存在”状态。

O (Owned):该块是有效的,拥有的,并且可能是脏的,但不是独占的。 缓存具有块的只读副本,并且必须响应对该块的请求。 其他缓存可能具有该块的只读副本,但它们不是所有者。LLC/内存中的块副本可能已过时。

E (Exclusive):该块是有效的、排他的和干净的。缓存具有块的只读副本。没有其他缓存拥有该块的有效副本,并且 LLC/内存中的块副本是最新的。在本入门书中,我们认为当区块处于独占状态时,它是拥有的,尽管在某些协议中独占状态不被视为所有权状态。当我们在后面的章节中介绍 MESI 监听和目录协议时,我们将讨论是否把独占块视为所有者的问题。

监听一致性协议一般通过每个节点的有效状态控制器来实现,控制器对每个来自处理器总线的请求做出响应,然后改变响应Cache块的状态

MSI协议

MSI协议是最基础的监听协议实现。有无效共享修改三种状态

MESI协议

MESI是最经典的监听协议。有无效共享独占修改三种状态

相比于MSI协议,MESI协议增加了独占(E)状态,是一种投机性优化:如果一个CPU想修改一个处于S状态的缓存行,总线事务需要将所有该缓存行的copy变成invalid状态,而修改E状态的缓存不需要使用总线事务。

演示MESI协议网页VivioJS MESI (tcd.ie)

监听协议和集中式共享存储器体系结构的局限性

随着多处理器中处理器数目的增加,或者处理器对存储器带宽的增加,系统的任何集中式资源都会成为“瓶颈”

例如在基于总线的多处理器中,总线必须同时支持由于Cache导致的存储器通信和一致性通信。如果是只有一个物理存储器的集中式共享存储器体系结构,总线的带宽(bandwidth)负担会极大。

为了增加处理器和存储器之间的通信带宽,设计者使用多种总线以及各种互联网络,如交叉开关和小型点对点网络。在该设计中,存储器系统可以被配置称多个物理组,有效增加带宽。这正是集中式共享存储器和分布式共享存储器的结合

由上例可知,使用监听Cache一致性协议可以不要求使用集中式总线这样的很容易造成带宽瓶颈的通信方式,但仍然要求完成广播。由于一致性通信量与处理器速度没有关系,这种一致性通信限制了处理器的扩展与速度

四、目录协议

对于监听协议,在处理每个Cache缺失时,都需要和所有的Cache进行通信,造成带宽瓶颈,限制了处理器的扩展。

每个目录负责跟踪共享本地存储器的Cache,存储器的每一块在目录中对应有一项

每个目录项主要有“状态”和“位向量”两种成分。状态描述该目录所对应的存储块的当前情况;位向量有处理器数量的位数,其每一位对应于一个处理器的局部Cache,用于指出该Cache中有无该存储块的备份。


本文是对《计算机体系结构——量化研究方法(第四版)》和《计算机体系结构(第二版)》的相关章节的整理

参考链接🔗

cache之多核一致性(一) - 总线上没有秘密 - 知乎 (zhihu.com)

一、并行体系结构的Flynn分类法

1. 单指令流,单数据流(SISD)

单处理器,即传统的、串行的冯·诺伊曼计算机

2. 单指令流,多数据流(SIMD)

同一条指令被多个使用不同数据流的多处理器执行,实现数据级并行

例如:向量计算机、阵列计算机

3. 多指令流,单数据流(MISD)

奇葩没有研究价值

4. 多指令流,多数据流(MIMD)

每个处理器取自己的指令并对自己的数据进行操作,实现线程级并行

例如:集群、片内多处理器(多核)

  • 其中,MIMD计算机更加受到学术和商业上的关注,这是因为以下两点:

    • 灵活性强:MIMD可以实现线程级并行机制,因此

      (1)支持数据级并行:既可以作为单用户多处理器为单一应用程序提供高性能(为一个高负载程序提供高性能)

      (2)支持任务级并行:也可以作为同时运行多个任务的多道程序多处理器系统使用(同时运行多个程序)

      (3)甚至可以支持数据级和任务级两种并行的应用。

    • 性价比高:能充分利用现有微处理器的性价比优势,多核芯片可以通过复制的方式,有效降低单处理器内核的设计成本。

二、MIMD分类

根据处理器间通信机制(传递数据所用的方法),MIMD还可以分为多处理器系统多计算机系统

多处理器系统

多处理器系统是共享内存的计算机。多个处理器通过逻辑上共享的地址空间进行通信,换言之,CPU上运行的操作系统可以直接通过LOAD指令或者STORE指令访问其他远程CPU的内存字。

多计算机系统

多计算机系统是消息传递的计算机,多个处理器在逻辑上没有共享的地址空间,而是有多个私有的地址空间组成,这些地址空间在逻辑上是独立分散的,换言之,CPU只能通过显示发送消息并等待响应的方式与其他远程CPU进行通信。

多处理器和多计算机两种体系结构的差别

多处理器 多计算机
CPU间通信方式 共享内存(隐式地,通过load、store指令) 消息传递(显式地,通过发送、接收消息的函数)
地址空间 所有CPU共享一个单一的物理地址空间 每个CPU都有自己独立的物理地址空间
常见编程模型 OpenMP编程模型 MPI编程模型

三、多处理器系统

根据存储器组织方式或者说共享内存的实现方式,可以进一步把多处理器系统分为两类:集中式共享存储器系统结构和分布式存储器系统结构。

集中式共享存储器系统(Centralized Shared-Memory, CSM)

对于处理机数目较少的多处理机,各个处理机可以共享单个集中式存储器。当处理机数量很大时,由于访问冲突以及总线带宽的限制,访问延迟就会很大。

  • 结构:多个处理器-Cache子系统共享同一个较大的物理存储器。

  • 如何实现共享内存?因为多处理机物理上共享单个存储器,因此逻辑上的共享是自然而然的。

别称:因为只用一个单独的主存,而且这个主存对于各处理器的关系是对称的,所以各处理器访问这个主存的时间相同,所以这种结构有时被称为对称式共享存储器结构(Symmetric shared-memory MultiProcessor, SMP),或者一致性内存访问计算机(Uniform Memory Access, UMA)

ps:使用大容量Cache

对于共享存储器结构,存储器和总线的带宽往往是瓶颈,而大容量、多级Cache可以很大程度地降低对存储器和总线的带宽要求。所以共享存储器结构往往采用大容量Cache。

分布式共享存储器系统(Distributed Shared-Memory, DSM)

为了支持更多的处理机,存储器不能按照集中共享方式组织,而必须分布于各个处理机。

  • 结构:由多个独立结构构成(称为节点),每个节点包含处理器、存储器、IO系统,各个节点通过互联网络连接。

  • 如何实现共享内存?物理上分开的多个存储器的地址空间组织成逻辑上共享的地址空间,即在这种机器中,两个不同处理器中相同的物理地址指向一个存储器中的相同位置。实现这一效果需要硬件支持复杂的处理器间数据通信,同时时延也更大。

别称:由于CPU对内存字的访问时间依赖于该内存字在存储器中的存放位置,所以也被称为非一致性内存访问计算机(NonUniform Memory Access, NUMA)

Aside:超节点

每个节点还可能包含较少数目(2~8)的处理器,这些处理器之间可采用另一种技术(如总线)互联形成簇,这样的节点叫做超节点。这可以视为分布式共享存储和集中式共享存储的混合。

这种将存储器分散到各个节点的结构有两个好处:

  1. 如果大多数的访问是针对本节点的局部存储器,可降低对存储器和互联网络的带宽要求。
  2. 对局部存储器的访问延迟低

但也有缺点:

  1. 处理器之间的通信较复杂
  2. 各处理器之间访问延迟较大

四、多计算机系统

多计算机系统类似于多处理器系统中的分布式共享存储器系统,只不过多处理器的地址空间并没有组织成逻辑上共享的地址空间,或许可以称为分布式非共享存储器系统,这些私有地址空间在逻辑上是分散的。即在这种机器中,两个不同处理器中相同的物理地址分别指向两个不同存储器中的不同位置。

每个节点本质上是一台独立的计算机

五、两种通信机制

前面提到,多处理器和多计算机的最主要区别就是通信机制的不同,多处理器采用共享内存通信机制,多计算机采用消息传递通信机制。

共享内存通信机制

由于共享地址空间,可利用Load和Store指令中的地址隐含地进行数据通信。

消息传递通信机制

由于有多个地址空间,数据通信要通处理器间显式地传递消息完成。

同步

异步

gem5附带的默认配置脚本

本文是对官网教程gem5: Using the default configuration scripts的整理,主要介绍了

gem5附带了许多配置脚本,允许用户非常快速地使用gem5。

1. configs文件夹

gem5中的全部配置脚本都在configs文件夹中,该文件夹的结构如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
configs/boot:
bbench-gb.rcS bbench-ics.rcS hack_back_ckpt.rcS halt.sh

configs/common:
Benchmarks.py Caches.py cpu2000.py FileSystemConfig.py GPUTLBConfig.py HMC.py MemConfig.py Options.py Simulation.py
CacheConfig.py cores CpuConfig.py FSConfig.py GPUTLBOptions.py __init__.py ObjectList.py SimpleOpts.py SysPaths.py

configs/dist:
sw.py

configs/dram:
lat_mem_rd.py low_power_sweep.py sweep.py

configs/example:
apu_se.py etrace_replay.py garnet_synth_traffic.py hmctest.py hsaTopology.py memtest.py read_config.py ruby_direct_test.py ruby_mem_test.py sc_main.py
arm fs.py hmc_hello.py hmc_tgen.cfg memcheck.py noc_config riscv ruby_gpu_random_test.py ruby_random_test.py se.py

configs/learning_gem5:
part1 part2 part3 README

configs/network:
__init__.py Network.py

configs/nvm:
sweep_hybrid.py sweep.py

configs/ruby:
AMD_Base_Constructor.py CHI.py Garnet_standalone.py __init__.py MESI_Three_Level.py MI_example.py MOESI_CMP_directory.py MOESI_hammer.py
CHI_config.py CntrlBase.py GPU_VIPER.py MESI_Three_Level_HTM.py MESI_Two_Level.py MOESI_AMD_Base.py MOESI_CMP_token.py Ruby.py

configs/splash2:
cluster.py run.py

configs/topologies:
BaseTopology.py Cluster.py CrossbarGarnet.py Crossbar.py CustomMesh.py __init__.py MeshDirCorners_XY.py Mesh_westfirst.py Mesh_XY.py Pt2Pt.py

boot/

这些是在全系统模式下使用的rcS文件。这些文件在Linux引导后由模拟器加载,并由shell执行。其中大多数用于在全系统模式下运行时控制基准测试。有些是实用函数,如hack_back_ckpt.rcS。在全系统模拟一章中,将更深入地介绍这些文件。

common/

common目录包含许多用于创建模拟系统的辅助脚本和函数。例如,

Caches.py类似于前几章中创建的Caches.py和Caches_opts.py文件。

Options.py包含可以在命令行上设置的各种选项。比如CPU的数量、系统时钟等等。这是查看要更改的选项是否已经有命令行参数的好地方。

CacheConfig.py包含用于为经典内存系统设置缓存参数的选项和函数。

MemConfig.py提供了一些帮助函数,用于设置内存系统。

FSConfig.py包含为许多不同类型的系统设置全系统仿真所需的功能。全系统仿真将在本章中进一步讨论。

Simulation.py包含许多用于设置和运行gem5的辅助函数。该文件中包含的许多代码管理保存和恢复检查点。下面示例中的示例配置文件使用该文件中的函数来执行gem5模拟。该文件相当复杂,但它也允许在模拟运行方式上有很大的灵活性。

dram/

包含测试DRAM的脚本。

example/

此目录包含一些示例 gem5配置脚本,可以使用这些脚本即开即用地运行 gem5。具体来说,se.py 和 fs.py 非常有用。有关这些文件的更多信息可以在下一节中找到。此目录中还有一些其他实用程序配置脚本。

learning_gem5/

该目录包含learning_gem5书中的所有gem5配置脚本。

network/

此目录包含HeteroGarnet网络的配置脚本。

nvm/

此目录包含使用NVM接口的示例脚本。

ruby/

该目录包含Ruby的配置脚本及其包含的缓存一致性协议。

splash2/

该目录包含用于运行splash2基准测试套件的脚本,以及用于配置模拟系统的一些选项。

topologies/

该目录包含在创建Ruby缓存层次结构时可以使用的拓扑的实现。

2. se.py&fs.py

SE模式默认配置脚本se.py和FS模式默认配置脚本fs.py均位于目录configs/example/,下面以se.py为例介绍它们通用的命令行选项:

  • 通过--cmd=选项指定二进制文件

    1
    build/X86/gem5.opt configs/example/se.py --cmd=tests/test-progs/hello/bin/x86/linux/hello
  • 默认情况下,两个配置脚本均使用 atomic CPU和 atomic memory accesses,因此没有时间统计数据产生

    可通过--cpu-type=CPU_TYPE指定CPU类型,通过--mem-type=MEM_TYPE指定内存类型。

0%