BLOG

Record, summarize, and improve.

gem5 document2

Creating disk images for full system mode 1) Using gem5 utils to create a disk image Creating an empty image Mounting an image Unmounting Image Contents Modifications Kernel and bootloader Manipulating images with loopback devices Loopback devices Working with image files Create the actual file Partitioning Formatting 2) Using gem5 utils and chroot to create a disk image Creating a blank disk image Copying root files to the disk Setting up gem5-specific files Create a serial terminal Setup localhost Update fstab Copy the m5 binary to the disk Install new applications 3) Using QEMU to create a disk image Step 1: Create an empty disk Step 2: Install ubuntu with qemu Step 3: Boot up and install needed software Step 4: Update init script Manually installing the gem5 init script Problems and (some) solutions 4) Using Packer to create a disk image Building a Simple Disk Image with Packer a. How It Works, Briefly b. Install Required Software/Dependencies c. Customize the Packer Script i. Customizing the VM (Virtual Machine) ii. Customizing the Disk Image iii. File Transfer iv. Install Benchmark Dependencies v. Running Other Scripts on Disk Image d. Build the Disk Image i. Build ii. Inspect the Building Process Devices in full system mode I/O Device Base Classes PioPort PioDevice BasicPioDevice DmaPort DmaDevice NIC Devices Getting a list of packets on the ethernet link PCI devices m5 term Building ARM Kernel  Prerequisites Linux 4.x Kernel Checkout Kernel build Bootloaders Device Tree Blobs Memory system MemObjects Ports Connections Request Packet Access Types Packet allocation protocol Timing Flow control Response and Snoop ranges The gem5 Memory System Model Hierarchy CPU Data Cache Object Tags & Data Block MSHR and Write Buffer Queues Memory Access Ordering Coherent Bus Object Simple Memory Object Message Flow Memory Access Ordering Memory Access Ordering Replacement Policies Random Least Recently Used (LRU) Tree Pseudo Least Recently Used (TreePLRU) Bimodal Insertion Policy (BIP) LRU Insertion Policy (LIP) Most Recently Used (MRU) Least Frequently Used (LFU) First-In, First-Out (FIFO) Second-Chance Not Recently Used (NRU) Re-Reference Interval Prediction (RRIP) Bimodal Re-Reference Interval Prediction (BRRIP) Indexing Policies Set Associative Skewed Associative Classic Memory System coherence Classic Caches Interconnects Crossbars Bridges Others… Debugging Ruby SLICC + Coherence protocols: Protocol independent memory components独立于协议的内存组件 Interconnection Network Life of a memory request in Ruby Directory Structure Cache Coherence Protocols Common Notations and Data Structures Coherence Messages AccessPermissions Data Structures Coherence controller FSM Diagrams Garnet2.0: An On-Chip Network Model for Heterogeneous SoCs Invocation Configuration Topology Routing Flow Control Router Microarchitecture Buffer Management Lifecycle of a Network Traversal Running Garnet2.0 with Synthetic Traffic HeteroGarnet: A Detailed Simulator for Diverse Interconnect Systems Topology Construction Physical Links Network Interface Clock Domain Crossing Units Serializer-Deserializer Units Routing Routing Policies. Table based routing Flow Control and Buffer Management Virtual Channels Buffer Backpressure Credit-based backpressuring Life of a Message in Garnet 3.0 Injection of Message Conversion to Flits. Transmission to Local Router. Router Arbitration. Serialization-Deserialization. Area, Power and Energy Model MOESI CMP Directory Protocol Overview Related Files L1 Cache Controller Stable States and Invariants FSM Abstraction Optimizations L2 Cache Controller Stable States and Invariants FSM Abstraction Directory Controller *Stable States and FSM Abstraction Garnet Synthetic Traffic Related files How to run Parameterized Options Implementation of Garnet synthetic traffic SLICC Input To the Compiler Protocol State Machines Specifying Data Members Input for the Machine Actions Transitions Special Functions Stalling/Recycling/Waiting input ports Stalling the input port Recycling the input port Stall and wait the input port Other Compiler Features SLICC Internals MI Example Protocol Overview Related Files Stable States and Invariants Cache controller Directory controller Other features Garnet Standalone Related Files Cache Hierarchy Stable States and Invariants Cache controller Directory controller Other features Interconnection Network How to invoke the network Topology Routing Flow-Control and Router Microarchitecture Simple Network Configuration Switch Model Garnet2.0 Running the Network with Synthetic Traffic MOESI Hammer Related Files Cache Hierarchy Stable States and Invariants Cache controller Directory controller Stable States and Invariants Controller MOESI CMP token Protocol Overview Related Files Controller Description L1 Cache L2 cache Directory controller MESI Two Level Protocol Overview Related Files Controller Description *L1 cache L2 cache controller CHI CHI overview and terminology Protocol overview Protocol implementation Transaction allocation Transaction initialization Transaction execution Transaction finalization Hazard handling Performance modeling Cache block allocation and replacement modeling Supported CHI transactions Supported requests Supported snoops Writeback and evictions Hazards Other implementations notes Protocol table Replacement Policies Random Least Recently Used (LRU) Tree Pseudo Least Recently Used (TreePLRU) Bimodal Insertion Policy (BIP) LRU Insertion Policy (LIP) Most Recently Used (MRU) Least Frequently Used (LFU) First-In, First-Out (FIFO) Second-Chance Not Recently Used (NRU) Re-Reference Interval Prediction (RRIP) Bimodal Re-Reference Interval Prediction (BRRIP)

Creating disk images for full system mode

在全系统模式下,gem5依靠一个安装了操作系统的磁盘镜像来运行模拟。gem5中的一个磁盘设备从磁盘镜像中获得其初始内容。磁盘镜像文件存储了磁盘上的所有字节,就像你在实际设备上找到它们一样。其他一些系统也使用更复杂格式的磁盘镜像,并提供压缩、加密等功能。gem5目前只支持原始镜像,所以如果你有一个其他格式的镜像,你必须在模拟中使用它之前将其转换为原始镜像。通常有一些工具可以在不同的格式之间进行转换。

有多种方法可以创建可用于gem5的磁盘镜像。以下是建立磁盘镜像的四种不同方法。

  • 使用gem5 utils来创建磁盘镜像
  • 使用gem5 utils和chroot来创建磁盘镜像
  • 使用QEMU创建磁盘镜像
  • 使用Packer创建磁盘镜像

所有这些方法都是相互独立的。接下来,我们将逐一讨论这些方法。

1) Using gem5 utils to create a disk image

免责声明:这是来自旧网站,这个方法中的一些东西可能已经过时了。

因为磁盘镜像代表了磁盘本身的所有字节,它包含的不仅仅是一个文件系统。对于大多数系统的硬盘来说,镜像以分区表开始。表中的每个分区(通常只有一个)也在镜像中。如果你想操作整个磁盘,你将使用整个镜像,但如果你想只操作一个分区和/或其上的文件系统,你将需要专门选择镜像的那一部分。losetup命令(在下面讨论)有一个-o选项,可以让你指定从镜像的哪里开始。

一个在Ubuntu 12.04 64bit上使用qemu处理图像文件的youtube视频。视频分辨率可以设置为1080

Creating an empty image

你可以使用 gem5 提供的 ./util/gem5img.py 脚本来构建磁盘镜像。了解如何构建镜像是个好主意,以防出错或需要以不寻常的方式做某事。然而,在这个方法中,我们使用gem5img.py脚本来完成构建和格式化镜像的过程。如果你想了解它所做的事情的内涵,见下文。运行gem5img.py可能需要你输入sudo密码。 你不应该以根用户的身份运行你不了解的命令 你应该看看util/gem5img.py这个文件,确保它不会对你的计算机做任何恶意的操作

你可以使用gem5img.py的 "init "选项来创建一个空镜像,"new"、"partition "或 "format "来独立执行init的这些部分,"mount "或 "umount "来挂载或卸载一个现有的镜像。

Mounting an image

要在你的映像文件上挂载一个文件系统,首先要找到一个回环设备,并将其以适当的偏移量附加到你的映像上,这将在格式化部分进一步描述。

mount -o loop,offset=32256 foo.img

一个在Ubuntu 12.04 64bit上使用mount添加文件的youtube视频。视频分辨率可以设置为1080

Unmounting

要卸载一个映像,就像你平时那样使用umount命令。

umount

Image Contents

现在你可以创建一个镜像文件并挂载它的文件系统了,你会想在里面放一些文件。你可以自由地使用你想要的任何文件,但gem5的开发者发现Gentoo stage3 tarballs是一个很好的起点。它们基本上是一个几乎可启动的、相当小的Linux安装,并可用于许多架构。

如果你选择使用Gentoo的压缩包,首先把它解压到你的安装镜像中。/etc/fstab文件将有根、引导和交换设备的占位符条目。你要在适当的时候更新这个文件,删除任何你不打算使用的条目(例如,启动分区)。接下来,你要修改inittab文件,使其使用m5工具程序(在其他地方描述)来读取主机提供的init脚本并运行它。如果你允许正常的init脚本运行,你感兴趣的工作负载可能需要更长的时间来启动,你将没有办法注入你自己的init脚本来动态控制哪些基准被启动,例如,你将不得不通过一个模拟终端与模拟进行交互,这就引入了非确定性。

Modifications

默认情况下,gem5不会将对磁盘的修改存储到底层图像文件中。你所做的任何修改都将存储在中间的COW层中,并在模拟结束时被扔掉。如果你想修改底层磁盘,你可以关闭COW层。

Kernel and bootloader

另外,一般来说,gem5跳过了启动的引导程序部分,而是自己将内核加载到模拟内存中。这意味着不需要在磁盘镜像上安装像grub这样的引导程序,也不需要把你要启动的内核放在镜像上。内核是单独提供的,可以很容易地改变,而不需要修改磁盘镜像。

Manipulating images with loopback devices
Loopback devices

Linux支持回环设备,它是由文件支持的设备。通过在你的磁盘镜像上附加一个这样的设备,你可以在它上面使用通常在真实磁盘设备上运行的标准Linux命令。你可以使用带有 "loop "选项的mount命令来设置一个回环设备并将其挂载到某个地方。不幸的是,你不能指定镜像的偏移量,所以这只对文件系统镜像有用,而不是你需要的磁盘镜像。然而,你可以使用较低级别的losetup命令来设置一个回环设备,并提供适当的偏移。一旦你完成了这些,你就可以像对一个磁盘分区那样对它使用挂载命令,格式化它,等等。如果你不提供偏移量,回环设备将指的是整个镜像,你可以用你喜欢的程序来设置它的分区。

Working with image files

要从头开始创建一个空的镜像,你需要创建文件本身,对其进行分区,并用文件系统格式化(其中一个)分区。

Create the actual file

首先,决定你希望你的图像有多大。这是一个好主意,使它足够大,以容纳你知道你将需要的一切,再加上一些喘息的空间。如果你后来发现它太小了,你将不得不创建一个新的更大的图像,并将所有东西移到上面。如果你把它弄得太大了,你会不必要地占用实际的磁盘空间,使图像更难处理。一旦你决定了一个尺寸,你就想实际创建文件。基本上,你需要做的就是创建一个一定大小的、充满零的文件。一种方法是使用dd命令从/dev/zero复制适当数量的字节到新文件中。另外,你也可以创建文件,在文件中寻找到最后一个字节,然后写一个零字节。所有你跳过的空间都将成为文件的一部分,并被定义为读零,但由于你没有明确地在那里写入任何数据,大多数文件系统都足够聪明,不会将其实际存储到磁盘上。你可以通过这种方式创建一个大的图像,但在你的物理磁盘上占用的空间非常小。一旦你开始向文件写入,情况就会改变,而且如果你不小心,复制文件可能会把它扩大到全尺寸。

Partitioning

首先,使用带有-f选项的losetup命令找到一个可用的环回设备。

losetup -f

接下来,使用losetup将该设备附加到你的镜像上。如果可用的设备是/dev/loop0,你的镜像是foo.img,你将使用这样的命令。

losetup /dev/loop0 foo.img

/dev/loop0(或其他你使用的设备)现在将指的是你的整个镜像文件。使用任何你喜欢的分区程序来建立一个(或多个)分区。为了简单起见,只创建一个占整个镜像的分区可能是个好主意。我们说它占用了整个镜像,但实际上它占用了所有的空间,除了文件开头的分区表本身,以及之后可能为了DOS/bootloader的兼容性而浪费的一些空间。

从现在开始,我们要使用我们创建的新分区,而不是整个磁盘,所以我们要用losetup的-d选项释放回环设备

losetup -d /dev/loop0

Formatting

首先,像我们在上面的分区步骤中那样,使用losetup的-f选项找到一个可用的环回设备。

losetup -f

我们将再次把我们的图像附加到该设备上,但这次我们只想提到我们要在上面放置一个文件系统的分区。对于PC和Alpha系统来说,该分区通常是一个轨道,一个轨道是63个扇区,每个扇区是512字节,即63*512=32256字节。对你来说,正确的数值可能是不同的,这取决于你的图像的几何形状和布局。在任何情况下,你都应该用-o选项设置回环设备,使其代表你所感兴趣的分区。

losetup -o 32256 /dev/loop0 foo.img

接下来,使用一个适当的格式化命令,通常是mke2fs,在分区上放置一个文件系统。

mke2fs /dev/loop0

现在你已经成功地创建了一个空镜像文件。如果你打算继续使用它,你可以留下回环设备(很可能因为它仍然是空的),或者用losetup -d清理它。

losetup -d /dev/loop0

不要忘记用losetup -d命令清理连接到你的图像上的环回设备。

losetup -d /dev/loop0

2) Using gem5 utils and chroot to create a disk image

本节的讨论假定你已经查看了gem5的一个版本,并且能够在全系统模式下构建和运行gem5。在本节讨论中,我们将使用x86 ISA的gem5,而这大部分也适用于其他ISA。

Creating a blank disk image

第一步是创建一个空白磁盘镜像(通常是一个.img文件)。这与我们在第一个metod中所做的类似。我们可以使用gem5开发者提供的gem5img.py脚本。要创建一个空白磁盘镜像,默认情况下是用ext2格式的,只需运行以下内容。

> util/gem5img.py init ubuntu-14.04.img 4096

该命令创建了一个新的镜像,名为 "ubuntu-14.04.img",大小为4096MB。如果你没有创建回环设备的权限,这个命令可能需要你输入sudo密码。 你不应该以根用户的身份运行你不了解的命令 你应该看看util/gem5img.py这个文件,确保它不会对你的电脑做任何恶意的操作
我们将在本节中大量使用util/gem5img.py,所以你可能想更好地理解它。如果你只是运行util/gem5img.py,它会显示所有可能的命令。

Usage: %s [command] <command arguments>
where [command] is one of
    init: Create an image with an empty file system.
    mount: Mount the first partition in the disk image.
    umount: Unmount the first partition in the disk image.
    new: File creation part of "init".
    partition: Partition part of "init".
    format: Formatting part of "init".
Watch for orphaned loopback devices and delete them with
losetup -d. Mounted images will belong to root, so you may need
to use sudo to modify their contents
Copying root files to the disk

现在我们已经创建了一个空白磁盘,我们需要将所有的操作系统文件填充到其中。Ubuntu为这一目的明确地分发了一组文件。你可以在http://cdimage.ubuntu.com/releases/14.04/release/ 找到14.04的Ubuntu核心发行版。由于我们模拟的是一台X86机器,我们将使用ubuntu-core-14.04-core-amd64.tar.gz。下载适合你所模拟的系统的任何镜像。

接下来,我们需要挂载空白磁盘,并将所有的文件复制到磁盘上。

mkdir mnt
../../util/gem5img.py mount ubuntu-14.04.img mnt
wget http://cdimage.ubuntu.com/ubuntu-core/releases/14.04/release/ubuntu-core-14.04-core-amd64.tar.gz
sudo tar xzvf ubuntu-core-14.04-core-amd64.tar.gz -C mnt

下一步是把工作系统中的一些必要文件复制到磁盘上,这样我们就可以把新的磁盘chroot到。我们需要把/etc/resolv.conf复制到新磁盘上。

sudo cp /etc/resolv.conf mnt/etc/

Setting up gem5-specific files
Create a serial terminal

默认情况下,gem5使用串行端口来允许从主机系统到模拟系统的通信。为了使用它,我们需要创建一个串口tty。由于Ubuntu使用upstart来控制启动过程,我们需要在/etc/init中添加一个文件,该文件将初始化我们的终端。另外,在这个文件中,我们将添加一些代码来检测是否有一个脚本传递给模拟系统。如果有一个脚本,我们将执行这个脚本,而不是创建一个终端。

将以下代码放入一个名为/etc/init/tty-gem5.conf的文件中

# ttyS0 - getty
#
# This service maintains a getty on ttyS0 from the point the system is
# started until it is shut down again, unless there is a script passed to gem5.
# If there is a script, the script is executed then simulation is stopped.

start on stopped rc RUNLEVEL=[12345]
stop on runlevel [!12345]

console owner
respawn
script
   # Create the serial tty if it doesn't already exist
   if [ ! -c /dev/ttyS0 ]
   then
      mknod /dev/ttyS0 -m 660 /dev/ttyS0 c 4 64
   fi

   # Try to read in the script from the host system
   /sbin/m5 readfile > /tmp/script
   chmod 755 /tmp/script
   if [ -s /tmp/script ]
   then
      # If there is a script, execute the script and then exit the simulation
      exec su root -c '/tmp/script' # gives script full privileges as root user in multi-user mode
      /sbin/m5 exit
   else
      # If there is no script, login the root user and drop to a console
      # Use m5term to connect to this console
      exec /sbin/getty --autologin root -8 38400 ttyS0
   fi
end script
Setup localhost

如果我们要使用任何使用该设备的应用程序,我们还需要设置localhost环回设备。要做到这一点,我们需要在/etc/hosts文件中添加以下内容。

127.0.0.1 localhost
::1 localhost ip6-localhost ip6-loopback
fe00::0 ip6-localnet
ff00::0 ip6-mcastprefix
ff02::1 ip6-allnodes
ff02::2 ip6-allrouters
ff02::3 ip6-allhosts
Update fstab

接下来,我们需要在/etc/fstab中为我们希望能够从模拟系统访问的每个分区创建一个条目。只有一个分区是绝对需要的(/);然而,你可能想添加额外的分区,比如交换分区。

以下内容应该出现在文件/etc/fstab中。

# /etc/fstab: static file system information.
#
# Use 'blkid' to print the universally unique identifier for a
# device; this may be used with UUID= as a more robust way to name devices
# that works even if disks are added and removed. See fstab(5).
#
# <file system>    <mount point>   <type>  <options>   <dump>  <pass>
/dev/hda1      /       ext3        noatime     0 1
Copy the m5 binary to the disk

gem5附带了一个额外的二进制程序,它可以执行伪指令,让模拟系统与主机系统互动。要构建这个二进制文件,请在gem5/m5目录下运行make -f Makefile.<isa>,其中<isa>是您正在模拟的ISA(例如x86)。这之后,你应该有一个m5二进制文件。把这个文件复制到你新创建的磁盘上的/sbin
在用所有gem5特定的文件更新磁盘后,除非你要继续添加更多的应用程序或复制更多的文件,否则请卸载磁盘镜像。

> util/gem5img.py umount mnt

Install new applications

在你的磁盘上安装新应用程序的最简单方法是使用chroot。这个程序从逻辑上将根目录("/")改变为一个不同的目录,这里是mnt。在你改变根目录之前,你首先要在新的根目录中设置特殊的目录。要做到这一点,我们使用mount -o bind

> sudo /bin/mount -o bind /sys mnt/sys
> sudo /bin/mount -o bind /dev mnt/dev
> sudo /bin/mount -o bind /proc mnt/proc

绑定这些目录后,您现在可以 chroot

> sudo /usr/sbin/chroot mnt /bin/bash

这时你会看到一个root提示,你将进入新磁盘的/目录

你应该更新你的版本库信息。

> apt-get update

你可能想用下面的命令把宇宙仓库添加到你的列表中。注意:第一条命令在14.04中是需要的。

> apt-get install software-properties-common
> add-apt-repository universe
> apt-get update

现在,你能够通过apt-get安装任何你可以在本地Ubuntu机器上安装的应用程序。

记住,在你退出后,你需要卸载所有我们用过绑定的目录。

> sudo /bin/umount mnt/sys
> sudo /bin/umount mnt/proc
> sudo /bin/umount mnt/dev

3) Using QEMU to create a disk image

这个方法是之前创建磁盘镜像方法的后续。我们将看到如何使用qemu而不是依赖gem5工具来创建、编辑和设置磁盘镜像。本节假设你已经在你的系统上安装了qemu。在Ubuntu中,这可以通过以下方式完成

sudo apt-get install qemu-kvm libvirt-bin ubuntu-vm-builder bridge-utils

Step 1: Create an empty disk

使用qemu磁盘工具,创建一个空白的原始磁盘镜像。在这种情况下,我选择创建一个名为 "ubuntu-test.img "的8GB的磁盘。

qemu-img create ubuntu-test.img 8G

Step 2: Install ubuntu with qemu

现在我们有了一个空白磁盘,我们将使用qemu在磁盘上安装Ubuntu。我们鼓励你使用Ubuntu的服务器版本,因为gem5对显示器没有很大的支持。因此,桌面环境并不是很有用。

首先,你需要从Ubuntu网站(Ubuntu website)下载安装光盘镜像。

接下来,使用qemu从CD镜像中启动,并将系统中的磁盘设置为你上面创建的空白磁盘。Ubuntu需要至少1GB的内存才能正确安装,所以一定要将qemu配置为至少使用1GB的内存。

qemu-system-x86_64 -hda ../gem5-fs-testing/ubuntu-test.img -cdrom ubuntu-16.04.1-server-amd64.iso -m 1024 -enable-kvm -boot d

有了这个,你可以简单地按照屏幕上的指示将Ubuntu安装到磁盘镜像上。安装过程中唯一的问题是,gem5的IDE驱动似乎与逻辑分区不相称。因此,在安装Ubuntu的过程中,一定要对磁盘进行手动分区并删除任何逻辑分区。反正你也不需要磁盘上的任何交换空间,除非你要对交换空间做一些特别的处理。

Step 3: Boot up and install needed software

一旦你在磁盘上安装了Ubuntu,退出qemu并删除-boot d选项,这样你就不会再从CD上启动了。现在,你可以再次从你安装Ubuntu的主磁盘镜像上启动。

由于我们使用的是qemu,你应该有一个网络连接(尽管ping不起作用)。在qemu中启动时,你可以直接使用sudo apt-get install,在磁盘上安装任何你需要的软件。

qemu-system-x86_64 -hda ../gem5-fs-testing/ubuntu-test.img -cdrom ubuntu-16.04.1-server-amd64.iso -m 1024 -enable-kvm

Step 4: Update init script

默认情况下,gem5希望有一个修改过的init脚本,从主机上加载一个脚本在客体中执行。要使用这一功能,您需要遵循以下步骤。

或者,你也可以安装本网站上找到的用于x86的预编译二进制文件。在qemu中,你可以运行下面的程序,它为你完成上述步骤。

wget http://cs.wisc.edu/~powerjg/files/gem5-guest-tools-x86.tgz
tar xzvf gem5-guest-tools-x86.tgz
cd gem5-guest-tools/
sudo ./install

现在,你可以在你的 Python 配置脚本中使用 system.readfile 参数。这个文件将被自动加载(由 gem5init 脚本)并执行。

Manually installing the gem5 init script

首先,在主机上构建 m5 二进制文件。

cd util/m5
make -f Makefile.x86

然后,把这个二进制文件复制到客户机上,放在/sbin里。同时,从/sbin/gem5创建一个链接。

然后,为了获得gem5启动时要执行的初始脚本,创建文件/lib/systemd/system/gem5.service,内容如下。

[Unit]
Description=gem5 init script
Documentation=http://gem5.org
After=getty.target

[Service]
Type=idle
ExecStart=/sbin/gem5init
StandardOutput=tty
StandardInput=tty-force
StandardError=tty

[Install]
WantedBy=default.target

启用gem5服务并禁用ttyS0服务。如果你的磁盘启动时出现了登录提示,可能是由于没有禁用ttyS0服务造成的。

systemctl enable gem5.service

最后,创建由服务执行的init脚本。在/sbin/gem5init

#!/bin/bash -

CPU=`cat /proc/cpuinfo | grep vendor_id | head -n 1 | cut -d ' ' -f2-`
echo "Got CPU type: $CPU"

if [ "$CPU" != "M5 Simulator" ];
then
    echo "Not in gem5. Not loading script"
    exit 0
fi

# Try to read in the script from the host system
/sbin/m5 readfile > /tmp/script
chmod 755 /tmp/script
if [ -s /tmp/script ]
then
    # If there is a script, execute the script and then exit the simulation
    su root -c '/tmp/script' # gives script full privileges as root user in multi-user mode
    sync
    sleep 10
    /sbin/m5 exit
fi
echo "No script found"
Problems and (some) solutions

在遵循这个方法时,你可能会遇到一些问题。本页讨论了其中的一些问题和解决办法 page.。

4) Using Packer to create a disk image

本节讨论了一种自动创建与Ubuntu服务器兼容的磁盘镜像的方法。我们利用packer来完成这一工作,它利用一个.json模板文件来构建和配置磁盘镜像。该模板文件可以被配置为安装了特定基准的磁盘镜像。上述模板文件可以在这里找到 here

Building a Simple Disk Image with Packer
a. How It Works, Briefly

我们使用Packer 和 QEMU来实现磁盘创建过程的自动化。从本质上讲,QEMU负责设置虚拟机以及在构建过程中与磁盘镜像的所有互动。这些互动包括将Ubuntu服务器安装到磁盘镜像上,将文件从你的机器上复制到磁盘镜像上,以及在Ubuntu安装完毕后在磁盘镜像上运行脚本。然而,我们将不直接使用QEMU。Packer提供了一种更简单的方法,即使用JSON脚本与QEMU进行交互,这比从命令行使用QEMU更具表现力。

b. Install Required Software/Dependencies

如果尚未安装,可以使用以下方式安装 QEMU:

sudo apt-get install qemu

从官方网站(the official website.)下载 Packer 二进制文件。

c. Customize the Packer Script

默认的打包脚本template.json应该根据所需的磁盘镜像和构建过程中可获得的资源进行修改和调整。我们将把默认模板重命名为 [disk-name].json。应该修改的变量出现在[disk-name].json文件的末尾,在variables部分。用于构建磁盘镜像的配置文件,以及目录结构如下所示。

disk-image/
    [disk-name].json: packer script
    Any experiment-specific post installation script
    post-installation.sh: generic shell script that is executed after Ubuntu is installed
    preseed.cfg: preseeded configuration to install Ubuntu
i. Customizing the VM (Virtual Machine)

[disk-name].json中,有以下变量可用于定制虚拟机。

Variable Purpose Example
vm_cpus (should be modified) number of host CPUs used by VM “2”: 2 CPUs are used by the VM
vm_memory (should be modified) amount of VM memory, in MB “2048”: 2 GB of RAM are used by the VM
vm_accelerator (should be modified) accelerator used by the VM e.g. Kvm “kvm”: kvm will be used
ii. Customizing the Disk Image

[disk-name].json中,磁盘镜像的大小可以使用以下变量进行定制。

Variable Purpose Example
image_size (should be modified) size of the disk image, in megabytes “8192”: the image has the size of 8 GB
[image_name] name of the built disk image “boot-exit”
iii. File Transfer

在构建磁盘镜像时,用户需要将他们的文件(基准、数据集等)转移到磁盘镜像上。为了进行这种文件转移,在provisioners下的[disk-name].json中,你可以添加以下内容。

{"type": "file",
    "source": "post_installation.sh",
    "destination": "/home/gem5/",
    "direction": "upload"
}

上面的例子将post_installation.sh文件从主机复制到磁盘镜像中的/home/gem5/。这个方法也能够将一个文件夹从主机复制到磁盘镜像,反之亦然。需要注意的是,尾部的斜线会影响复制过程(更多细节 (more details).)。下面是一些显著的例子,说明在路径末尾使用斜线的效果。

source destination direction Effect
foo.txt /home/gem5/bar.txt upload copy file (host) to file (image)
foo.txt bar/ upload copy file (host) to folder (image)
/foo /tmp upload mkdir /tmp/foo (image); cp -r /foo/* (host) /tmp/foo/ (image);
/foo/ /tmp upload cp -r /foo/* (host) /tmp/ (image)

如果directiondownload,文件将从图像中复制到主机。

NoteThis is a way to run script once after installing Ubuntu without copying to the disk image.

iv. Install Benchmark Dependencies

要安装这些依赖项,你可以使用bash脚本post_installation.sh,它将在Ubuntu安装和文件复制完成后运行。例如,如果我们想安装gfortran,在post_installation.sh中添加以下内容。

echo '12345' | sudo apt-get install gfortran;

在上面的例子中,我们假设用户的密码是12345。这本质上是一个bash脚本,在文件复制完成后在虚拟机上执行,你可以把这个脚本修改为bash脚本,以适应任何目的。

v. Running Other Scripts on Disk Image

In [disk-name].json, we could add more scripts to . Note that the files are on the host, but the effects are on the disk image. For example, the following example runs post_installation.sh after Ubuntu is installed,

[disk-name].json中,我们可以为provisioners添加更多脚本。注意,这些文件在主机上,但效果在磁盘镜像上。例如,下面的例子在Ubuntu安装完毕后运行post_installation.sh

{"type": "shell",
    "execute_command": "echo '{{ user `ssh_password` }}' | {{.Vars}} sudo -E -S bash '{{.Path}}'",
    "scripts":
    ["post-installation.sh"
    ]}
d. Build the Disk Image
i. Build

为了建立一个磁盘镜像,首先使用模板文件进行验证。

./packer validate [disk-name].json

然后,模板文件可用于构建磁盘映像:

./packer build [disk-name].json

在一台相当新的机器上,构建过程应该不超过15分钟就能完成。带有用户定义的名称(image_name)的磁盘镜像将在一个名为[image_name]-image的文件夹中产生。 We recommend to use a VNC viewer in order to inspect the building process.

ii. Inspect the Building Process

在构建磁盘镜像的过程中,Packer将运行一个VNC(虚拟网络计算)服务器,你可以通过VNC客户端连接到VNC服务器,看到构建过程。VNC客户端有很多选择。当你运行Packer脚本时,它会告诉你VNC服务器使用的是哪个端口。例如,如果它说qemu: Connecting to VM via VNC (127.0.0.1:5932),VNC 端口是 5932。要从VNC客户端连接到VNC服务器,使用127.0.0.1:5932这个地址,端口号为5932。如果你需要端口转发,将VNC端口从远程机器转发到本地机器,请使用SSH隧道连接

ssh -L 5932:127.0.0.1:5932 <username>@<host>

这个命令将把5932端口从主机转发到你的机器上,然后你就可以用VNC浏览器的127.0.0.1:5932地址连接到VNC服务器。

注意:当Packer安装Ubuntu时,终端屏幕上会显示 "等待SSH "而长时间没有任何更新。这不是Ubuntu安装是否产生任何错误的指标。因此,我们强烈建议至少使用VNC查看器来检查图像构建过程。

Devices in full system mode

I/O Device Base Classes

The base classes in src/dev/*_device.* allow devices to be created with reasonable ease. The classes and virtual functions that must be implemented are listed below. Before reading the following it will help to be familiar with the .

src/dev/*_device.*中的基类允许合理轻松地创建设备。下面列出了必须实现的类和虚拟函数。在阅读以下内容之前,熟悉 Memory_System 会有所帮助。

PioPort

PioPort类是一个编程的I/O端口,所有对地址范围敏感的设备都使用它。该端口接收所有的内存访问类型,并将它们分到设备必须响应的一个read()write()调用中。设备还必须提供addressRanges()函数,用它来返回它所感兴趣的地址范围。如果需要,一个设备可以有一个以上的PIO端口。然而,在正常情况下,它只有一个端口,并在调用addressRange()函数时返回多个范围。唯一需要多个PIO端口的情况是,如果你的设备想要单独连接到两个内存对象。

PioDevice

这是一个基类,所有对地址范围敏感的设备都继承于此。有三个纯虚拟函数,所有设备都必须实现addressRanges(), read(), and write()。选择我们所处的模式等魔法是由PioPort处理的,所以设备不需要费心。

每个设备的参数应该在一个派生自PioDevice::Params的Params结构中。

BasicPioDevice

由于大多数PioDevice只响应一个地址范围,BasicPioDevice提供了一个addressRanges()和正常Pio延迟和设备响应的地址的参数。由于设备的大小通常是不可配置的,所以不使用参数,任何从这个类继承的东西都应该在其构造函数中把它的大小写入pioSize。

DmaPort

DmaPort(在dma_device.hh)只用于设备掌握的访问。recvTimingResp()方法必须对它发出的请求的响应(无论是否被acked)可用。该端口有两个公共方法dmaPending(),返回dma端口是否繁忙(例如,它仍在尝试发送最后一个请求的所有片段)。所有的代码都是通过dmaAction()访问的,将请求分解成适当大小的块,收集可能的多个响应,并对设备作出响应。一个命令、起始地址、大小、完成事件和可能的数据被交给该函数,然后在请求完成后执行完成事件process()方法。在内部,代码使用DmaReqState来管理它所收到的块,并知道何时执行完成事件。

DmaDevice

这是一个基类,一个DMA非pci设备会继承自这个基类,但是目前M5中没有这样的设备存在。这个类确实有一些方法dmaWrite(), dmaRead()可以从DMA读或写操作中选择适当的命令。

NIC Devices

gem5模拟器有两种不同的网络接口卡(NIC)设备,可用于通过模拟以太网链接将两个模拟实例连接在一起。

Getting a list of packets on the ethernet link

你可以通过创建一个Etherdump对象,设置它的文件参数,并将EtherLink上的dump参数设置为它来获得以太网链接上的数据包列表。这在我们的 fs.py 示例配置中很容易实现,只需添加命令行选项 --etherdump=<filename>即可。产生的文件将被命名为<file>,并且是标准的 pcap 格式。这个文件可以用 wireshark或其他任何能理解 pcap 格式的东西来读取。

PCI devices

待办。解释平台和系统,它们之间的关系,以及它们各自的作用。

m5 term

m5term程序允许用户连接到全系统gem5提供的模拟控制台界面。只需换到util/term目录并建立m5term。

% cd gem5/util/term
% make
gcc  -o m5term term.c
% make install
sudo install -o root -m 555 m5term /usr/local/bin

m5term的用法是:

`./m5term <host> <port>`

`<host> is the host that is running gem5

<port> is the console port to connect to. gem5 defaults to
using port 3456, but if the port is used, it will try the next
higher port until it finds one available.

If there are multiple systems running within one simulation,
there will be a console for each one.  (The first system's
console will be on 3456 and the second on 3457 for example)

m5term uses '~' as an escape character.  If you enter
the escape character followed by a '.', the m5term program
will exit.`

m5term可以用来与模拟器进行交互式工作,不过用户通常必须设置各种终端设置才能工作。

一个略为简短的m5term操作实例。

% m5term localhost 3456
==== m5 slave console: Console 0 ====
M5 console
Got Configuration 127
memsize 8000000 pages 4000
First free page after ROM 0xFFFFFC0000018000
HWRPB 0xFFFFFC0000018000 l1pt 0xFFFFFC0000040000 l2pt 0xFFFFFC0000042000 l3pt_rpb 0xFFFFFC0000044000 l3pt_kernel 0xFFFFFC0000048000 l2reserv 0xFFFFFC0000046000
CPU Clock at 2000 MHz IntrClockFrequency=1024
Booting with 1 processor(s)
...
...
VFS: Mounted root (ext2 filesystem) readonly.
Freeing unused kernel memory: 480k freed
init started:  BusyBox v1.00-rc2 (2004.11.18-16:22+0000) multi-call binary

PTXdist-0.7.0 (2004-11-18T11:23:40-0500)

mounting filesystems...
EXT2-fs warning: checktime reached, running e2fsck is recommended
loading script...
Script from M5 readfile is empty, starting bash shell...
# ls
benchmarks  etc         lib         mnt         sbin        usr
bin         floppy      lost+found  modules     sys         var
dev         home        man         proc        tmp         z
#

Building ARM Kernel 

本页包含为在ARM上运行的gem5构建最新内核的说明。

如果你不想自己构建内核(或磁盘镜像),你仍然可以下载一个预构建的版本(download a prebuilt version.)。

Prerequisites

这些说明是用于运行无头系统的。这是一个更加 "服务器 "风格的系统,没有帧缓冲器。该说明是使用下面链接的资源库中最新的已知有效的标签创建的,但是每个部分中的表格列出了以前已知有效的标签。要在x86主机上构建内核,你需要ARM交叉编译器和设备树编译器。如果你运行的是Ubuntu或Debian的一个合理的新版本,你可以通过apt获得所需软件。

apt-get install gcc-arm-linux-gnueabihf gcc-aarch64-linux-gnu device-tree-compiler

如果你不能使用这些预制的编译器,接下来最简单的方法就是从ARM获得所需的编译器。

下载(其中之一)并确保二进制文件在你的PATH上。

根据你的交叉编译器的确切来源,下面使用的编译器名称将需要小的改动。

要实际运行内核,你需要下载或编译gem5的引导程序。详情请参见本文档中的引导程序(bootloaders)部分。

Linux 4.x

较新的用于ARM的gem5内核(v4.x及以后的版本)是基于vanilla Linux内核的,通常有少量的补丁,以使其更好地与gem5一起工作。这些补丁是可选的,您应该也能使用香草内核。然而,这需要你自己配置内核。较新的内核都使用 VExpress_GEM5_V1 gem5 平台,用于 AArch32 和 AArch64。

Kernel Checkout

要检查内核,请执行以下命令:

git clone https://gem5.googlesource.com/arm/linux

仓库包含每个gem5内核版本的标签和主要Linux版本的工作分支。请查看项目页面( project page ),了解标签和分支的列表。克隆命令默认会签出最新的发布分支。要检查 v4.14 分支,请在版本库中执行以下命令。

git checkout -b gem5/v4.14

Kernel build

要编译内核,在版本库中执行以下命令。

make ARCH=arm64 CROSS_COMPILE=aarch64-linux-gnu- gem5_defconfig
make ARCH=arm64 CROSS_COMPILE=aarch64-linux-gnu- -j `nproc`

测试刚刚构建的内核:

./build/ARM/gem5.opt configs/example/arm/starter_fs.py --kernel=/tmp/linux-arm-gem5/vmlinux \
--disk-image=ubuntu-18.04-arm64-docker.img

Bootloaders

gem5有两种不同的引导程序。一个是32位内核的,一个是64位内核的。它们可以用以下命令来编译。

make -C system/arm/bootloader/arm
make -C system/arm/bootloader/arm64

Device Tree Blobs

描述gem5附带的操作系统硬件所需的DTB文件。要构建它们,执行以下命令:

make -C system/arm/dt

我们建议只有在你打算修改这些设备树文件时才使用它们。如果没有,我们建议你依靠DTB自动生成:通过运行不带-dtb选项的FS脚本,gem5将根据实例化的平台自动生成DTB。

一旦你编译了二进制文件,就把它们放到M5_PATH的二进制文件目录中。

Memory system

M5的新内存系统(在第一个2.0测试版中引入)的设计目标如下。

  1. 在计时模式下,统一了计时和功能访问。在旧的内存系统中,定时访问没有数据,只是说明了做一个操作所需的时间。然后,一个单独的功能访问实际上使操作对系统可见。这种方法很混乱,它允许模拟组件意外地作弊,并阻止内存系统返回与时间有关的值,这对执行中的CPU模型是不合理的。
  2. 简化内存系统代码--删除大量的模板和重复的代码。
  3. 让改变更容易,特别是允许除共享总线外的其他内存互连。

关于新的一致性协议的细节,在2.0b4中引入(连同大量的缓存模型重写),见一致性协议Coherence Protocol.。

MemObjects

All objects that connect to the memory system inherit from MemObject. This class adds the pure virtual functions getMasterPort(const std::string &name, PortID idx) and getSlavePort(const std::string &name, PortID idx) which returns a port corresponding to the given name and index. This interface is used to structurally connect the MemObjects together.

所有连接到内存系统的对象都继承于MemObject。这个类增加了纯虚拟函数getMasterPort(const std::string &name, PortID idx)getSlavePort(const std::string &name, PortID idx),它们返回与给定名称和索引相应的端口。这个接口被用来在结构上将MemObjects连接在一起。

Ports

内存系统的下一个重要部分是端口的概念。端口是用来将内存对象相互连接的。它们总是成对出现,有一个主端口和一个从端口,我们把另一个端口对象称为同伴。这些都是用来使设计更加模块化的。有了端口,每一种类型的对象之间的特定接口就不需要被创建。每个内存对象都必须至少有一个端口才能发挥作用。一个主模块,如CPU,有一个或多个MasterPort实例。一个从属模块,比如一个内存控制器,有一个或多个SlavePorts。一个互连组件,如高速缓存、网桥或总线,有MasterPort和SlavePort实例。

在端口对象中,有两组函数。send*函数是由拥有该端口的对象在端口上调用的。例如,为了在内存系统中发送一个数据包,CPU会调用myPort->sendTimingReq(pkt)来发送一个数据包。每个发送函数都有一个相应的recv函数,在端口的对等物上调用。因此,上面sendTimingReq()调用的实现将只是在从属端口上的peer->recvTimingReq(pkt)。使用这种方法,我们只有一个虚拟的函数调用惩罚,但保留了通用的端口,可以连接到任何内存系统对象。

主端口可以发送请求并接收响应,而从端口则接收请求并发送响应。由于一致性协议,从属端口也可以发送窥探请求和接收窥探响应,而主端口拥有镜像接口。

Connections

在 Python 中,端口是模拟对象的第一类属性,与 Params 很相似。两个对象可以使用赋值运算符来指定它们的端口应该被连接。与普通的变量或参数赋值不同,端口连接是对称的。 A.port1 = B.port2B.port2 = A.port1具有相同的意义。主端口和从端口的概念也存在于Python对象中,当端口连接在一起时,会做一个检查。

像母线这样具有潜在的无限数量的端口的对象使用 "向量端口"。对一个向量端口的赋值是将对等体追加到连接列表中,而不是覆盖之前的连接。

在C++中,内存端口是在所有对象被实例化后由Python代码连接在一起的

Request

一个请求对象封装了由CPU或I/O设备发出的原始请求。这个请求的参数在整个事务中是持久的,所以一个请求对象的字段对于一个给定的请求最多只能被写入一次。有一些构造函数和更新方法,允许在不同的时间(或根本不写)写入对象的字段子集。对所有请求字段的读取是通过访问器方法提供的,这些访问器方法验证被读取字段中的数据是有效的。

请求对象中的字段通常对真实系统中的设备是不可用的,所以它们通常应该只用于统计或调试,而不是作为架构值。

请求对象的字段包括

  • Virtual address. This field may be invalid if the request was issued directly on a physical address (e.g., by a DMA I/O device).虚拟地址。如果请求是直接在物理地址上发出的(例如,由DMA I/O设备发出),该字段可能无效。
  • Physical address.
  • Data size.
  • Time the request was created.
  • The ID of the CPU/thread that caused this request. May be invalid if the request was not issued by a CPU (e.g., a device access or a cache writeback).引起这个请求的CPU/线程的ID。如果该请求不是由CPU发出的(例如,设备访问或缓存回写),则可能是无效的。
  • 引起该请求的PC。如果该请求不是由CPU发出的,也可能是无效的.
Packet

数据包用于封装内存系统中两个对象之间的传输(例如,L1和L2缓存)。这与 "请求 "不同,在 "请求 "中,一个 "请求 "从请求者一直到最终的目的地,然后再返回,沿途可能由几个不同的 "包 "来传递。

对许多数据包字段的读取是通过访问器方法提供的,访问器方法验证被读取字段中的数据是有效的。

一个数据包包含以下内容,所有这些都被访问器访问,以确定数据的有效性:

  • 地址。这是用于将数据包路由到其目标(如果没有明确设置目的地)并在目标处处理数据包的地址。它通常来自请求对象的物理地址,但在某些情况下可能来自虚拟地址(例如,在执行地址转换之前访问完全虚拟的缓存)。它可能与原始请求地址不完全相同:例如,在高速缓存缺失时,数据包地址可能是要获取的块的地址,而不是请求地址。
  • The size. 同样,这个大小可能与原始请求的大小不一样,就像在高速缓存缺失的情况下。
  • 指向正在操作的数据的指针。
    • dataStatic()dataDynamic()dataDynamicArray()设置,它们分别控制当数据包是、不是、用delete和用delete[]时是否释放与数据包有关的数据。
    • 如果没有被上述方法之一allocate()设置,就会被分配,数据在数据包被销毁时被释放。(调用总是安全的)。
    • 可以通过调用 getPtr() 来检索指针
    • get()set()可以用来操作数据包中的数据。get()方法做的是客体到主机的endian转换,set方法做的是主机到客体的endian转换。
  • 表示成功、BadAddress、未确认和未知的状态。
  • 与数据包关联的命令属性列表
    • 注意:状态字段中的数据和命令属性有一些重叠。这主要是为了使一个数据包在acked时容易被重新初始化,或者容易被原子或功能访问所重复使用。
  • 一个SenderState指针,它是一个虚拟的基础不透明结构,用于保持与数据包相关的状态,但具体到发送设备(例如MSHR)。这个状态的指针在数据包的响应中被返回,以便发送者可以快速查找处理数据包所需的状态。一个特定的子类将从这里派生出来,以携带特定的发送设备的状态。
  • 一个CoherenceState指针,它是一个虚拟的基础不透明结构,用于保持与相干性相关的状态。一个特定的子类将从这里派生出来,以携带特定的相干协议的状态。
  • 指向请求的指针。
Access Types

端口支持三种类型的访问。

  1. Timing - 时序访问是最详细的访问。它们反映了我们对现实计时的最大努力,包括排队延迟和资源争夺的建模。一旦一个定时请求被成功发送,在未来的某个时间点,发送请求的设备将得到响应,或者在请求无法完成的情况下得到一个NACK(更多内容见下文)。计时和原子访问在内存系统中是不能共存的。
  2. Atomic - 原子访问是一种比详细访问更快的访问。它们用于快速前进和预热缓存,并返回一个完成请求的大致时间,没有任何资源争夺或排队延迟。当一个原子访问被发送时,响应会在函数返回时提供。原子访问和定时访问不能共存于内存系统中。
  3. Functional - 与原子访问一样,功能访问也是瞬间发生的,但与原子访问不同的是,它们可以与原子或定时访问共存于内存系统中。功能性访问用于加载二进制文件,检查/改变模拟系统中的变量,以及允许远程调试器连接到模拟器上。重要的是,当一个设备收到功能访问时,如果它包含一个数据包队列,所有的数据包都必须搜索功能访问所影响的请求或响应,它们必须被适当地更新。Packet::intersect()fixPacket()方法可以帮助解决这个问题。
Packet allocation protocol

数据包对象的分配和删除协议根据访问类型的不同而不同。(我们在这里谈论的是低级别的C++ new/delete问题,而不是与一致性协议有关的东西)。

  • 原子性和功能性:数据包对象由请求者拥有。响应者必须用响应覆盖请求包(通常使用Packet::makeResponse()方法)。没有规定一个请求可以有多个响应者。由于响应总是在sendAtomic()sendFunctional()返回之前产生,请求者可以静态地或在堆栈中分配数据包对象。
  • Timing: 计时事务由两个单向消息组成,一个请求和一个响应。在这两种情况下,数据包对象必须由发送方动态分配。取消分配是接收者的责任(或者,对于广播相干包,目标设备,通常是内存)。在请求的接收方正在生成响应的情况下,它可以选择为其响应重用请求包,以节省调用deletenew的开销(并获得使用makeResponse()的便利)。然而,这种优化是可选的,请求者决不能依赖在响应请求时收到相同的数据包对象。请注意,当响应者不是目标设备时(如在缓存到缓存的传输中),那么目标设备仍然会删除请求包,因此响应的缓存必须为其响应分配一个新的包对象。另外,由于目标设备可能会在传送时立即删除请求包,任何希望引用广播包的其他内存设备都必须在包被传送的地方复制一个包,因为被传送的包的指针不能被依赖而保持有效。
Timing Flow control

计时请求模拟的是一个真实的内存系统,所以与功能访问和原子访问不同,它们的响应不是即时的。因为定时请求不是瞬时的,所以需要流量控制。当一个定时数据包通过sendTiming()发送时,该数据包可能被接受,也可能不被接受,其信号是返回true或false。如果返回false,该对象不应该再尝试发送数据包,直到它收到recvRetry()调用。在这个时候,它应该再次尝试调用sendTiming();但是数据包可能再次被拒绝。注意:原来的数据包不需要重新发送,可以发送一个更优先的数据包。一旦sendTiming()返回true,该数据包可能仍然无法到达目的地。对于需要响应的数据包(即pkt->needsResponse()为真),任何内存对象都可以拒绝确认该数据包,将其结果改为Nacked,并将其送回其源。然而,如果它是一个响应包,就不能这样做。true/false返回的目的是用于本地流控制,而nacking是用于全局流控制。在这两种情况下,响应都不能被nacked。

Response and Snoop ranges

内存系统中的范围是通过让对地址范围敏感的设备在其从属端口对象中提供getAddrRanges的实现来处理的。这个方法返回一个它所响应的地址的AddrRangeList。当这些范围改变时(例如,从PCI配置发生),设备应该在其从属端口上调用sendRangeChange(),以便新的范围被传播到整个层次结构。这正是在init()过程中发生的事情;所有的内存对象都调用sendRangeChange(),并发生大量的范围更新,直到每个人的范围都被传播到系统的所有总线上。

The gem5 Memory System

该文件描述了gem5中的内存子系统,重点是CPU的简单内存事务(read或write)期间的程序流程。

Model Hierarchy

本文件中使用的模型由两个失序(O3)ARM v7 CPU组成,带有相应的L1数据缓存和简单内存。它是通过运行具有以下参数的gem5创建的。

configs/example/fs.py –-caches –-cpu-type=arm_detailed –-num-cpus=2

Gem5使用Simulation Objects派生对象作为构建存储系统的基本模块。它们通过建立了主/从层次结构的端口连接。数据流在主端口启动,而响应信息和窥探查询出现在从端口。

Image in a image block

CPU

数据缓存对象Cache实现了一个标准的缓存结构:

Image in a image block

详细描述O3 CPU模型并不在本文件的范围之内,因此这里只介绍一些关于该模型的相关说明。

Read access是通过向DCache对象的端口发送消息来启动的。如果DCache拒绝该消息(因为被阻塞或繁忙),CPU将刷新管道,访问将在以后重新尝试。访问在收到DCache的回复信息(ReadRep)后完成。

Write access是通过将请求存入存储缓冲区开始的,存储缓冲区的上下文被清空,并在每次勾选时被发送到DCache。DCache也可以拒绝该请求。当收到来自DCache的写回复(WriteRep)消息时,写访问就完成了。

Load & store缓冲区(用于读和写访问)对活动内存访问的数量没有任何限制。因此,CPU的内存访问请求的最大数量不受CPU模拟对象的限制,而是受底层内存系统模型的限制。

实现了拆分内存访问。

由CPU发送的信息包含了被访问区域的内存类型(正常、设备、强排序和可缓存)。然而,这并没有被模型的其他部分所使用,它们对内存类型采取了更简化的方法。

Data Cache Object

implements a standard cache structure:

数据缓存对象(Data Cache object)实现了一个标准的缓存结构:

匹配特定缓存标签的Cached memory reads(带有Valid & Read标志)将在可配置的时间后完成(通过向CPU发送ReadResp)。否则,该请求将被转发到缺失状态和处理寄存器 (MSHR) 块。

匹配特定缓存标签的Cached memory writes(具有 Valid、Read 和 Write)将在相同的可配置时间后完成(通过发送WriteResp CPU)。否则,该请求将被转发到缺失状态和处理寄存器(MSHR)块。

未缓存的内存读取被转发到 MSHR 块。

未缓存的内存写入被转发到 WriteBuffer 块。

Evicted& dirty)缓存行被转发到WriteBuffer块。

如果以下任何一项为真,CPU对数据缓存的访问将被阻止。

  • MSHR块已满。(MSHR的缓冲区的大小是可以配置的)。
  • 回写块已满。(该块的缓冲区的大小是可以配置的)。
  • 针对同一内存缓存行的未完成的内存访问数量达到了可配置的阈值--详见MSHR和写缓冲区。

处于阻塞状态的数据缓存将拒绝来自从属端口(来自CPU)的任何请求,不管它是否会导致缓存命中或错过。请注意,主端口上传入的信息(响应信息和窥探请求)永远不会被拒绝。

缓存命中不可缓存的内存区域(根据ARM的预测行为)将使缓存行失效并从内存中获取数据。

Tags & Data Block

缓存行(在源代码中被称为块)被组织成具有可配置的关联性和大小的集合。它们有以下的状态标志:

  • Valid. 它持有数据。地址标签是有效的
  • Read. 如果没有设置这个标志,任何读请求都不会被接受。例如,缓存行是有效的,当它等待写标志完成写访问时,是不可读的。
  • Write. 它可以接受写入。带有写标志的高速缓存行识别唯一状态--没有其他高速缓存存储器持有该副本。
  • Dirty. evicted时需要写回

Read access will hit cache line if address tags match and Valid and Read flags are set. Write access will hit cache line if address tags match and Valid, Read and Write flags are set.

如果地址标签匹配,并且设置了有效和读标志,则读访问将命中高速缓存行。如果地址标签匹配,并且设置了 "Valid"、"Read"和 "Write"标志,则写访问将击中高速缓存行。

MSHR and Write Buffer Queues

Miss Status and Handling Register (MSHR) queue holds the list of CPU’s outstanding memory requests that require read access to lower memory level. They are:

Miss Status and Handling Register(MSHR)缺失状态和处理寄存器队列保存着CPU的未完成的内存请求列表,这些请求需要读到较低的内存级别。它们是

  • Cached Read misses.
  • Cached Write misses.
  • Uncached reads.

WriteBuffer 队列容纳以下内存请求:

  • Uncached writes.
  • Writeback from evicted (& dirty) cache lines.
Image in a image block

每个内存请求都被分配给相应的MSHR对象(上图中的READ或WRITE),该对象代表为了完成命令而必须读取或写入的特定内存块(缓存线)。如上图所示,针对同一缓存行的缓存读/写有一个共同的MSHR对象,并将通过一次内存访问完成。

区块的大小(以及因此对下层内存的读/写访问的大小)是。

  • 用于缓存访问和写回的缓存行的大小;
  • 如 CPU 指令中指定的非缓存访问。

一般来说,数据缓存模型只区分两种内存类型:

  • Normal Cached memory. 它始终被视为回写、读写分配。
  • Normal uncached, Device and Strongly Ordered types 正常的未缓存、设备和强序类型被同等对待(作为未缓存的内存)。
Memory Access Ordering

一个唯一的订单号被分配给每个CPU的读/写请求(因为它们出现在从属端口)。MSHR对象的顺序号是从第一个分配的读/写中复制出来的。

这两个队列中的每一个的内存读/写都按顺序执行(根据分配的顺序号)。当两个队列都不是空的时候,模型将执行从MSHR块中读取的内存,除非WriteBuffer是满的。然而,它将始终保持同一(或重叠)内存缓存行(块)上的读/写顺序。

总之:

  • 对缓存内存的访问顺序不会被保留,除非它们的目标是同一个缓存行。例如,1号、5号和10号的访问将在同一个tick中同时完成(仍按顺序)。5号访问将在3号之前完成。
  • 所有未缓存的内存写入的顺序被保留。Write#6总是在Write#13之前完成。
  • 所有未缓存的内存读取的顺序被保留下来。读取#2总是在读取#8之前完成。
  • 读取和写入未缓存访问的顺序不一定被保留,除非它们的访问区域重叠了。因此,写#6总是在读#8之前完成(它们的目标是同一个内存块)。然而,Write#13可能在Read#8之前完成。

Coherent Bus Object

Image in a image block

Coherent Bus对象为snoop协议提供基本支持。

从属端口上的所有请求都被转发到适当的主端口。对缓存内存区域的请求也被转发到其他从属端口(作为snoop请求)。

主端口的回复被转发到适当的从端口。

主端口的snoop请求被转发到所有从属端口。

从属端口的窥探回复被转发到请求来源的端口。(注意,窥探请求的源头可以是从属端口或主端口)。

在发生以下任何事件后,总线会宣布自己被封锁一段时间,这段时间是可配置的。

  • 一个数据包被发送(或发送失败)到一个从属端口。
  • 回复消息被发送到主端口。
  • 一个从属端口的Snoop响应被发送到另一个从属端口。

处于阻塞状态的总线拒绝以下传入消息:

  • Slave port requests.
  • Master port replies.
  • Master port snoop requests.

Simple Memory Object

它永远不会阻止从属端口的访问。

内存读/写立即生效。(当收到请求时,读或写被执行)。

回复信息在可配置的时间段后发送。

Message Flow

Memory Access Ordering

下图显示的是带有 "有效 "和 "读取 "标志的读取访问,击中了数据缓存线。

Image in a image block

缓存缺失的读取访问将产生以下的信息序列:

Image in a image block

请注意,总线对象从未从DCache2和Memory对象那里得到响应。它向内存和数据缓存发送非常相同的ReadReq包(消息)对象。当数据缓存想要回复窥探请求时,它用MEM_INHIBIT标志标记消息,告诉Memory对象不要处理这个消息。

Memory Access Ordering

下面的图显示的是写访问,击中DCache1高速缓存行的有效和写标志。

Image in a image block

下图显示的是写访问,击中了DCache1的缓存行,有Valid标志,但没有Write标志--这被认为是写缺失。DCache1发出UpgradeReq以获得写权限。DCache2::snoopTiming将使被击中的缓存行失效。注意,UpgradeResp消息并不携带数据。

Image in a image block

下图显示了DCache中的写缺失。ReadExReq使DCache2的缓存行失效。ReadExResp携带内存缓存行的内容。

Image in a image block

Replacement Policies

Gem5有多个实施的替换政策。每一个都使用其特定的替换数据来确定驱逐时的替换受害者。

所有的替换策略都优先考虑使无效块成为受害者。

一个替换策略由reset()、touch()、invalidate()和getVictim()方法组成。每一个都以不同的方式处理替换数据。

  • reset()用于初始化一个替换数据(即验证)。它应该只在条目插入时被调用,并且在无效之前不得再次被调用。对一个条目的第一次触摸必须始终是一个reset()。
  • touch()用于对替换数据的访问,因此应该在入口访问时调用。它更新替换数据。
  • invalidate()在一个条目失效时被调用,可能是由于一致性处理。它使该条目在下一次受害者搜索中尽可能地被驱逐。在进行重置()之前,一个条目不需要被无效化。当模拟开始时,所有条目都是无效的。
  • getVictim()在出现失误时被调用,并且必须进行驱逐。它在所有的替换候选者中寻找具有最差替换数据的条目,通常优先驱逐无效的条目。

我们简单介绍一下Gem5中实现的替换策略。 如果需要更多的信息,可以研究Cache替换策略维基百科页面(Cache Replacement Policies Wikipedia page),或者相关的论文。

Random

最简单的替换策略;它不需要替换数据,因为它在候选人中随机选择一个受害者。

Least Recently Used (LRU)

它的替换数据包括最后的触摸时间戳,并根据它来选择受害者:它的年龄越大,其各自的条目就越有可能成为受害者。

Tree Pseudo Least Recently Used (TreePLRU)

LRU的一个变种,使用二进制树,通过1位指针跟踪条目的使用频率。

Bimodal Insertion Policy (BIP)

双模插入策略(Bimodal Insertion Policy)与LRU类似,但是,根据双模节流参数(btp),区块有一个被插入为MRU的概率。btp越高,新区块作为MRU被插入的可能性就越大。

LRU Insertion Policy (LIP)

LRU插入策略(LRU Insertion Policy)包括一个LRU替换策略,它不是插入具有最近一次触摸时间戳的区块,而是将其作为LRU条目插入。在随后对区块的触摸中,其时间戳被更新为MRU,如同LRU一样。它也可以被看作是一个BIP,其中插入一个新块作为最近使用的可能性是0%。

Most Recently Used (MRU)

最近使用的政策是根据受害者的最近性来选择替代受害者,然而,与LRU相反,条目越新,越有可能成为受害者。

Least Frequently Used (LFU)

受害者是用参考频率选择的。最小的参考条目被选择驱逐,不管它被触摸了多少次,或者距离上次触摸已经过去了多长时间。

First-In, First-Out (FIFO)

受害者是用插入的时间戳来选择的。如果没有无效的条目存在,最古老的条目将成为受害者,不管它被碰过多少次。

Second-Chance

第二次机会替换政策(Second-Chance)与先进先出政策类似,但是条目在受害之前会有第二次机会。如果一个条目本来是下一个受害的,但它的第二机会位被设置,这个位被清除,该条目被重新插入到FIFO的末端。在一次失误之后,一个条目被插入,其第二机会位被清除。

Not Recently Used (NRU)

最近未使用(NRU)是LRU的一个近似值,它使用一个单一的位来确定一个块是否会在近期或远期被重新引用。如果该位是1,它很可能很快就不会被引用,所以它被选为替换的受害者。当一个区块成为受害者时,它的所有共同替换候选者的重新引用位都会被增加。

Re-Reference Interval Prediction (RRIP)

再参考间隔预测(Re-Reference Interval Prediction (RRIP))是NRU的一个扩展,它使用一个再参考预测值来确定块在近期是否会被重新使用。RRPV的值越高,块离它的下一次访问就越远。从原始论文来看,这种RRIP的实现也被称为静态RRIP(SRRIP),因为它总是插入具有相同RRPV的块。

Bimodal Re-Reference Interval Prediction (BRRIP)

双模态再参考区间预测(Bimodal Re-Reference Interval Prediction (BRRIP))是RRIP的一个扩展,与双模态插入策略一样,有一个概率不像LRU那样插入区块。这个概率是由双模节流参数(btp)控制的。

Indexing Policies

索引策略决定了一个块根据其地址被映射到的位置。

索引策略最重要的方法是getPossibleEntries()和regenerateAddr()。

  • getPossibleEntries() 决定了一个给定的地址可以被映射到的条目列表。
  • regenerateAddr() 使用存储在一个条目中的地址信息来确定其完整的原始地址。

关于缓存索引策略的进一步信息,请参考维基百科上的以下文章 Placement Policies and Associativity.

Set Associative

集合关联索引策略是类表结构的标准,可以进一步分为直接映射(或1路集合关联)、集合关联和全关联(N路集合关联,其中N为表项的数量)。

集合关联型高速缓存可以被看作是一个倾斜的关联型高速缓存,其倾斜函数对每一种方式都映射为相同的值。

Skewed Associative

The skewed associative indexing policy has a variable mapping based on a hash function, so a value x can be mapped to different sets, based on the way being used. Gem5 implements skewed caches as described in “Skewed-Associative Caches”, from Seznec et al.

Note that there are only a limited number of implemented hashing functions, so if the number of ways is higher than that number then a sub-optimal automatically generated hash function is used.

倾斜关联索引策略有一个基于哈希函数的变量映射,所以一个值x可以根据使用方式被映射到不同的集合。Gem5实现了Seznec等人的 " “Skewed-Associative Caches”, from Seznec et al "中描述的倾斜缓存。

请注意,只有有限数量的散列函数可以实现,所以如果方式的数量超过这个数字,那么就会使用一个次优的自动生成的散列函数。

Classic Memory System coherence

M5 2.0b4引入了一个大幅重写和简化的缓存模型,包括一个新的一致性协议。(旧的2.0前的缓存模型已经被修补过了,以适应2.0beta中引入的新的Memory System,但没有重写以利用新的内存系统的功能)。

新的一致性协议的主要特点是,它被设计成可以与或多或少的任意缓存层次(多个缓存都在多个层次上)一起工作。相比之下,旧的协议将共享限制在一条总线上。

在现实世界中,系统架构会对协议所能容纳的缓存数量或配置有所限制。要设计一个完全现实而又对任意配置有效的协议是不现实的。为了使我们的协议能够适用于(几乎)任意的配置,我们目前牺牲了一点现实性和一点可配置性。我们的意图是,这个协议对于研究系统行为中除一致性机制以外的其他方面的研究人员来说是足够的。专门研究一致性的研究人员可能希望用所研究的特定协议的实现来取代默认的一致性机制。

该协议是一个MOESI窥探协议。包容不是强制的;在一个CMP配置中,你有几个L1,其总容量是它们共享的共同L2容量的一个重要部分,包容可能是非常低效的。

来自上层缓存的请求(那些更接近CPU的请求)以预期的方式向内存传播:L1的失误被广播到本地L1/L2总线上,在那里它被该总线上的其他L1侦查,(如果没有回应)由L2提供服务。如果请求在L2中失误,那么经过一些延迟(目前设定为等于L2的命中延迟),L2将在其内存侧总线上发出请求,在那里它可能会被其他L2窥探,然后被发布到L3或内存。

不幸的是,以类似的方式向后递增传播窥探请求是无数几乎难以解决的竞赛条件的来源。真正的系统通常不会这样做;一般来说,你希望在L2总线上的一个窥探操作就能告诉你整个L1/L2层次结构中的块的状态。有一些方法可以做到这一点。

  1. 只管窥探L2,但要强制纳入,使L2也有你需要的关于L1的所有信息,这是我们已经拒绝的想法。
  2. 为L2的所有L1保留一套额外的标签,这样这些标签就可以同时被窥探(见Compaq Piranha)--如果你的层次不是太深的话,这是合理的,但是现在你必须根据上层缓存的数量、大小和配置来确定下层缓存中标签的大小,这是一个配置上的痛苦。
  3. 窥探L1与L2并行,如果它们都在同一个芯片上,这并不难(我相信英特尔从奔腾Pro开始这样做;不确定他们是否仍然在Core2芯片上这样做,或者AMD是否也这样做,但我怀疑是这样)--也是合理的,但为这些窥探添加明确的路径也会使配置过程变得非常麻烦。

我们通过引入 "express snoops "来解决这个难题,它是一种特殊的snoop请求,即使系统在计时模式下运行,也会被即时和原子地传播到层次结构中(很像 Memory System 页面上描述的原子模式访问)。从功能上看,这与上面的选项2或3非常相似,但由于窥探是沿着常规总线互连传播的,所以没有额外的配置开销。有一些时间上的不准确,但是如果我们假设在真实的硬件中存在专门的路径用于这些窥探(或者用于维护低级缓存中的上层标签的额外副本),那么这些差异可能是很小的。

(更多内容请看:缓存如何知道它的请求是什么时候完成的?以及其他引人入胜的问题...)

注意:从2.0b4开始,这个协议仍然有一些错误,特别是如果你有多个L2,每个L2后面有多个L1,但我相信它对任何在2.0b3中工作的配置都是有效的。

Classic Caches

默认的高速缓存是一个非阻塞的高速缓存,有MSHR(缺失状态保持寄存器)和WB(写缓冲区),用于读取和写入缺失。缓存也可以启用预取功能(一般在最后一级缓存)。

There are multiple possible replacement policies and indexing policies implemented in gem5. These define, respectively, the possible blocks that can be used for a block replacement given an address, and how to use the address information to find a block's location. By default the cache lines are replaced using LRU (least recently used), and indexed with the Set Associative policy.

Interconnects

Crossbars

横梁上的两类流量是内存映射数据包和窥探数据包。内存映射的请求在内存层次结构中往下走,响应在内存层次结构中往上走(相同的返回路径)。窥探请求在水平方向上进入缓存层次,窥探响应在水平方向上进入缓存层次(同样的返回路径)。正常的窥探是水平进行的,而快速窥探则是在缓存层次结构上进行的。

Image in a image block
Bridges
Others…

Debugging

在经典内存系统中,有一项功能是在调试器(如gdb)中显示特定块的一致性状态。这个功能是建立在经典内存系统对功能访问的支持上。(请注意,这个功能目前很少使用,而且可能有bug)。

如果你注入一个命令设置为PrintReq的函数式请求,数据包会穿越内存系统(就像普通的函数式请求一样),但在任何匹配的对象上(其他排队的数据包、缓存块等),它只是打印出该对象的一些信息。

在Port上有一个名为printAddr()的辅助方法,它接收一个地址并建立一个适当的PrintReq数据包,然后将其注入。因为它的传播机制与普通的功能请求相同,所以它需要从一个能在整个内存系统中传播的端口注入,比如在CPU。在MemTest、AtomicSimpleCPU和TimingSimpleCPU对象上有一些帮助性的printAddr()方法,它们只是在各自的缓存端口调用printAddr()。(注意:后两者是未经测试的)。

把这一切放在一起,你可以这样做。

(gdb) set print object
(gdb) call SimObject::find(" system.physmem.cache0.cache0.cpu")
$4 = (MemTest *) 0xf1ac60
(gdb) p (MemTest*)$4
$5 = (MemTest *) 0xf1ac60
(gdb) call $5->printAddr(0x107f40)

system.physmem.cache0.cache0
  MSHRs
    [107f40:107f7f] Fill   state:
      Targets:
        cpu: [107f40:107f40] ReadReq
system.physmem.cache1.cache1
  blk VEM
system.physmem
  0xd0

这说明cache0.cache0为该地址分配了一个MSHR,以服务于来自CPU的目标ReadReq,但它还没有进入服务状态(否则它就会被标记为服务状态);该块在cache1.cache1中是有效的、排他的,并且被修改过,该字节在物理内存中的值为0xd0。

显然,这不一定是你想要的所有信息,但它是相当有用的。请自由扩展。还有一个目前没有使用的verbosity参数,可以利用它来获得不同级别的输出。

注意,需要额外的 "p (MemTest*)4",因为尽管 "set print object "显示的是派生类型,但在内部,gdb仍然认为指针是基本类型的,所以如果你试图在4美元的指针上直接调用printAddr,你会得到这个。

(gdb) call $4->printAddr(0x400000)
Couldn't find method SimObject::printAddr

Ruby

Ruby为内存子系统实现了一个详细的模拟模型。它为具有各种替换策略的包容性/排他性缓存层次、一致性协议实现、互连网络、DMA和内存控制器、启动内存请求和处理响应的各种排序器建模。这些模型是模块化的、灵活的和高度可配置的。这些模型的三个关键方面是。

  1. 关注点的分离--例如,一致性协议规范与替换策略和缓存索引映射是分开的,网络拓扑结构与实现是分开的。
  2. 丰富的可配置性--几乎所有影响内存层次功能和时间的方面都可以控制。
  3. 快速原型设计--一种高级规范语言SLICC,用于指定各种控制器的功能。

下图取自ISCA 2005的GEMS教程,显示了Ruby中主要组件的高层视图。

Image in a image block

For a tutorial-based approach to Ruby see Part III of Learning gem5

SLICC + Coherence protocols:

SLICC 是指实现高速缓存一致性的规范语言。它是一种特定领域的语言,用于指定缓冲区一致性协议。从本质上讲,缓存一致性协议的行为就像一个状态机。SLICC被用来指定状态机的行为。由于目的是尽可能地模拟硬件,SLICC对可以指定的状态机施加了限制。例如,SLICC可以对一个周期内可以发生的转换数量进行限制。除了协议规范之外,SLICC还将内存模型中的一些组件结合在一起。如下图所示,状态机从互连网络的输入端口获取输入,并在网络的输出端口排队输出,从而将缓存/内存控制器与互连网络本身联系起来。

Image in a image block

支持以下缓存一致性协议:

  1. MI_example: 示例协议,1级缓存。
  2. MESI_Two_Level: 单芯片,二级缓存,严格包含的层次结构.
  3. MOESI_CMP_directory: 多芯片,2级缓存,非包容性(既不是严格意义上的包容性,也不是排他性)的层次结构。
  4. MOESI_CMP_token: 2级缓存。TODO.
  5. MOESI_hammer: 单个芯片,2级私有缓存,严格的排他性层次结构。
  6. Garnet_standalone: 以独立方式运行 Garnet 网络的协议。
  7. MESI Three Level: 3级缓存,严格包容的层次结构。基于MESI两级,有一个额外的L0高速缓存。
  8. CHI: 灵活的协议,实现了Arm的AMBA5 CHI事务。支持具有MESI或MOESI一致性的可配置的高速缓存层次结构。

这里(here)已经对协议中常用的符号和数据结构进行了详细描述。

Protocol independent memory components独立于协议的内存组件
  1. Sequencer
  2. Cache Memory
  3. Replacement Policies
  4. Memory Controller

一般来说,缓存一致性协议的独立组件包括排序器、缓存内存结构、缓存替换策略和内存控制器。序列器类负责向内存子系统(包括高速缓存和片外内存)提供来自处理器的加载/存储/原子内存请求。每一个内存请求由内存子系统完成后,也通过序列器将响应送回处理器。系统中模拟的每个硬件线程(或核心)都有一个序列器。缓存内存模型是一个集合关联的缓存结构,具有可参数化的大小、关联性和替换策略。系统中的L1、L2、L3缓存(如果存在的话)是缓存内存的实例。缓存替换策略被保存在缓存内存的模块中,因此不同的缓存内存实例可以使用他们选择的不同替换策略。目前有两种替换策略--LRU和Pseudo-LRU--随版本发布。内存控制器负责模拟和服务任何在模拟系统的所有片上缓存中失误的请求。内存控制器目前很简单,但是它忠实地模拟了DRAM禁止竞争和DRAM刷新。它还模拟了DRAM缓冲区的关闭页策略。

Interconnection Network

互联网络将内存层次结构的各个组成部分(高速缓存、内存、DMA控制器)连接在一起。

Image in a image block

互联网络的关键组成部分是:

  1. Topology
  2. Routing
  3. Flow Control
  4. Router Microarchitecture

关于网络模型实现的更多细节在此(here)描述。

另外,互联网络也可以用外部模拟器TOPAZ来代替。这个模拟器可以在gem5中运行,并且比原来的ruby网络模拟器增加了大量的功能。它包括,新的高级路由器微架构、新的拓扑结构、精确性能可调的路由器模型、加速网络模拟的机制等。

Life of a memory request in Ruby

在这一节中,我们将提供一个关于内存请求如何被Ruby整体服务的高层次概述,以及它在Ruby中经历了哪些组件。对于每个组件中的详细操作,请参考之前描述每个组件的单独章节。

  1. 来自gem5的内核或硬件上下文的内存请求通过RubyPort::recvTiming接口(在 src/mem/ruby/system/RubyPort.hh/cc)进入Ruby的管辖范围。模拟系统中Rubyport实例化的数量等于硬件线程上下文或内核的数量(在非多线程内核的情况下)。每个核边上的一个端口与一个相应的RubyPort绑定。
  2. 内存请求以gem5数据包的形式到达,RubyPort负责将其转换为RubyRequest对象,以便被Ruby的各个组件理解。它还发现该请求是否是针对某些PIO的,并将数据包操纵到正确的PIO。最后,一旦它生成了相应的RubyRequest对象并确定该请求是一个正常的内存请求(不是PIO访问),它就会将该请求传递给带有端口的Sequencer::makeRequest接口(变量ruby_port持有它的指针)的附属Sequencer对象。请注意,Sequencer类本身是RubyPort类的派生类。
  3. 正如在描述Ruby的Sequencer类一节中提到的,在一个模拟系统中,Sequencer对象的数量与硬件线程上下文的数量一样多(这也等于系统中RubyPort对象的数量),Sequencer对象和硬件线程上下文之间存在着一对一的映射。一旦内存请求到达Sequencer::makeRequest,它就会对请求进行各种核算和资源分配,最后将请求推送到Ruby的连贯缓存层次,以满足请求,同时考虑到服务的延迟。在考虑到L1缓存的访问延迟后,请求被推送到缓存层次中,通过将请求排队到强制队列中。强制队列(变量名m_mandatory_q_ptr)有效地充当了序列器和SLICC生成的高速缓存一致性文件之间的接口。
  4. L1缓存控制器(由SLICC根据一致性协议规范生成)从强制队列中删除请求并查找缓存,进行必要的一致性状态转换和/或根据要求将请求推送到缓存层次的下一级。SLICC生成的Ruby代码的不同控制器和组件通过Ruby的MessageBuffer类的实例(src/mem/ruby/buffers/MessageBuffer.cc/hh)进行通信,它可以作为有序或无序的缓冲器或队列。同时,为了满足一个内存请求,在不同的步骤中的延迟也会被计入相应的调度enqueue-ing和dequeue-ing操作。如果在L1缓存中可以找到所请求的缓存块,并且具有所需的一致性权限,那么该请求将被满足并立即返回。否则,请求将通过MessageBuffer被推送到下一级缓存层次。一个请求可以一直推到Ruby的内存控制器(在许多协议中也称为目录)。一旦请求得到满足,它就会通过MessageBuffers在层次结构中向上推送。
  5. MessageBuffers也作为相干信息的入口点,与片上互连建模。MesageBuffers是根据指定的互连拓扑结构连接的。因此,相干信息相应地通过这个片上互连。
  6. 一旦请求的缓存块在L1缓存中可用,并具有所需的一致性权限,L1缓存控制器就会根据请求的类型调用其readCallback或'writeCallback'方法来通知相应的Sequencer对象。请注意,在调用Sequencer的这些方法时,已经隐含地考虑到了服务请求的延迟。
  7. 然后Sequencer清除相应请求的会计信息,然后调用RubyPort::ruby_hit_callback方法。这最终将请求的结果返回给前端(gem5)的核心/硬件上下文的相应端口。

Directory Structure

  • src/mem/
    • protocols: SLICC相干协议的规范
    • slicc: SLICC 解析器和代码生成器的实现
    • ruby
      • common: 经常使用的数据结构,例如:地址(含位操作方法)、柱状图、数据块
      • filters: 各种bloom过滤器(来自 GEMS 的陈旧代码)
      • network: 互联的实现、样本拓扑结构规范、网络功率计算、用于连接控制器的消息缓冲器
      • profiler: 分析缓存事件、内存控制器事件
      • recorder: 缓存预热和访问跟踪记录
      • slicc_interface: 消息数据结构,各种映射(如地址到目录节点),实用功能(如地址与int之间的转换,将地址转换为高速缓存行地址)。
      • structures: 独立于协议的内存组件--CacheMemory, DirectoryMemory
      • system: 黏合组件 – Sequencer, RubyPort, RubySystem

Cache Coherence Protocols

Common Notations and Data Structures

Coherence Messages

这些在每个协议的<协议名称>-msg.sm文件中都有描述。

Message Description
ACK/NACK 对那些在决定下一步行动之前等待解决方向的请求进行正/负确认。例如,回写请求、排他性请求。
GETS 对共享权限的请求,以满足CPU的负载或IFetch。
GETX 请求独占访问权。
INV 无效化请求。这可以由一致性协议本身触发,也可以由下一个高速缓存级别/目录触发,以强制包含或触发DMA访问的回写,从而获得最新的数据拷贝。
PUTX 缓存块的回写请求。一些协议(如MOESI_CMP_directory)可能只对独占数据的回写请求使用。
PUTS 请求写回共享状态的缓存块。
PUTO 请求写回拥有状态的高速缓存块。
PUTO_Sharers 请求写回处于拥有状态的缓存块,但存在该块的其他共享者。
UNBLOCK 消息,以解除对封锁协议的下一个缓存级别/目录的封锁。
AccessPermissions

这些与每个缓存块相关联,并决定该块上允许哪些操作。它与相干性协议状态密切相关。

Permissions Description
Invalid 缓存块是无效的。在执行加载/存储之前,必须首先获得该块(从内存层次结构的其他地方)。对无效块不做任何操作(除了可能发送一个ACK)。对替换没有动作。相关的一致性协议状态是I或NP,是每个协议中的稳定状态。
Busy TODO
Read_Only 唯一允许的操作是加载、回写、失效。在过渡到其他状态之前不能进行存储。
Read_Write 允许加载、存储、回写、失效。通常表示该块是脏的。
Data Structures
  • Message Buffers:TODO
  • TBE Table: TODO
  • Timer Table: 这维护了一个基于地址的定时器地图。对于每个目标地址,可以关联一个超时值并添加到定时器表中。例如,这个数据结构被MOESI_CMP_directory协议的L1高速缓存控制器实现使用,以触发高速缓存块的单独超时。在内部,定时器表使用事件队列来安排超时。TimerTable支持一个基于轮询的接口isReady()来检查超时是否已经发生。地址上的超时可以用set()方法设置,用unset()方法删除。
  • Related Files:
    • src/mem/ruby/system/TimerTable.hh: 声明 TimerTable 类
    • src/mem/ruby/system/TimerTable.cc: 实现TimerTable类的方法,处理设置地址和超时,使用事件队列调度事件。
Coherence controller FSM Diagrams
  • 有限状态机仅显示稳定状态
  • 使用符号对转换进行注释 “Event list” or “Event list : Action list” or “Event list : Action list : Event list”. 例如,Store : GETX表示在一个Store事件中,发送了一个GETX消息,而GETX : Mem Read表示在收到一个GETX消息时,发送了一个内存读取请求。只列出了主要的触发器和动作。
  • 可选的动作(例如,根据区块是否变脏而进行的回写)被括在[ ]内。
  • 在图中,过渡标签与跨越过渡标签的弧或最近的弧相关联。

Garnet2.0: An On-Chip Network Model for Heterogeneous SoCs

Garnet2.0是gem5中的一个详细的互连网络模型。 它正在积极开发中,具有更多功能的补丁将定期推送到gem5中。 其他正在开发中的与Garnet相关的补丁和工具支持(不属于repo的一部分)可以在佐治亚理工学院的 Garnet page at Georgia Tech页面找到。

Garnet2.0建立在2009年发布的原始Garnet模型的基础上。

如果您对Garnet的使用有助于发表论文,请引用以下论文。

    @inproceedings{garnet,
      title={GARNET: A detailed on-chip network model inside a full-system simulator},
      author={Agarwal, Niket and Krishna, Tushar and Peh, Li-Shiuan and Jha, Niraj K},
      booktitle={Performance Analysis of Systems and Software, 2009. ISPASS 2009. IEEE International Symposium on},
      pages={33--42},
      year={2009},
      organization={IEEE}
    }

Garnet2.0提供了一个周期精确的片上网络路由器的微架构实现。它利用了gem5的ruby内存系统模型所提供的拓扑和路由基础结构。默认的路由器是一个最先进的1周期流水线。通过在拓扑结构中指定,支持在任何路由器中添加任意数量的额外延迟。

Garnet2.0还可以通过在路由器和链路中设置适当的延迟来模拟片外互连网络。

  • Related Files:
    • src/mem/ruby/network/Network.py
    • src/mem/ruby/network/garnet2.0/GarnetNetwork.py
    • src/mem/ruby/network/Topology.cc

Invocation

可以通过添加-network=garnet2.0来启用garnet网络。

Configuration

Garnet2.0使用Network.py中的通用网络参数:

  • number_of_virtual_networks: 这是虚拟网络的最大数量。活动的虚拟网络的实际数量由协议决定。
  • control_msg_size: 控制信息的大小,字节数。Network.cc中的m_data_msg_size被设置为以字节为单位的块大小+ control_msg_size。

其他参数在garnet2.0/GarnetNetwork.py中指定。

  • ni_flit_size:字节大小,以字节为单位。Flits是信息从一个路由器发送到另一个路由器的粒度。默认值是16(=>128比特)。[这个16的默认值导致控制信息在1个flit内,而数据信息在5个flit内]。Garnet要求ni_flit_size与bandwidth_factor(在network/BasicLink.py中)相同,因为它并不模拟网络中的可变带宽。这也可以在命令行中用-link-width-bits来设置。
  • vcs_per_vnet: 每个虚拟网络的虚拟通道(VC)数量。这也可以在命令行中用-vcs-per-vnet来设置,默认为4。
  • buffers_per_data_vc: 数据信息类中每个VC的flit-buffers数量。由于数据信息占用5个flits,这个值可以在1-5之间。默认为4。
  • buffers_per_ctrl_vc: 控制信息类中每个VC的flit-buffers数量。因为控制信息占用1个flit,而一个VC一次只能容纳一个信息,所以这个值必须是1。默认为1。
  • routing_algorithm: 0: 基于权重的表格(默认),1:XY,2:自定义。更多细节如下。

Topology

Garnet2.0利用了gem5的ruby内存系统模型所提供的 Topology 基础设施。任何异质的拓扑结构都可以被建模。拓扑文件中的每个路由器都可以被赋予一个独立的延迟,该延迟覆盖了默认值。此外,每个链接都有2个可选的参数:src_outport和dst_inport,它们是每个链接的源和目的路由器的输出和输入端口名称的字符串。这些参数可以在garnet2.0中用来实现自定义的路由算法,如接下来所述。例如,在一个Mesh中,从西到东的链路的src_outport设置为 "西",dst_inport设置为 "东"。

  • Network Components:
    • GarnetNetwork: 这是实例化所有网络接口、路由器和链路的顶级对象。 Topology.cc 调用方法在 NI 和路由器之间添加“外部链接”,在路由器之间添加“内部链接”。
    • NetworkInterface: 每个 NI 通过一侧的 MsgBuffer 接口连接到一个一致性控制器。它有一个链接到另一个路由器。每个协议消息都被放入一个 one-flit control 或 multi (default=5)-flit data(取决于它的 vnet),并注入路由器。多个 NI 可以连接到同一个路由器(例如,在 Mesh 拓扑中,缓存和目录控制器通过各个 NI 连接到同一个路由器)。
    • Router: 路由器管理输出链路的仲裁,以及路由器之间的流量控制。
    • NetworkLink: 网络链接携带flits. 它们可以是以下 3 种类型之一:EXT_OUT_(路由器到 NI)、EXT_IN_(NI 到路由器)和 INT_(内部路由器到路由器)
    • CreditLink: 信用链路在路由器之间携带 VC/缓冲信用以进行流量控制。

Routing

Garnet2.0利用gem5的ruby内存系统模型提供的路由基础设施。默认的路由算法是基于最短路径的确定性表格路由算法。链路权重可用于优先考虑某些链路而不是其他链路。有关如何填充路由表的详细信息,请参阅src/mem/ruby/network/Topology.cc。

自定义路由:为了模拟自定义路由算法,例如自适应,我们提供了一个框架来命名每个链路的src_outport和dst_inport方向,并在garnet中使用它们来实现路由算法。例如,在Mesh中,West-first可以通过沿着“西”出口链路发送flit,直到flit不再有任何X-hops剩余,然后随机(或基于下一个路由器VC可用性)选择其余链路中的一个来实现。有关如何实现outportComputeXY()的详细信息,请参阅src/mem/ruby/network/garnet2.0/RoutingUnit.cc。类似地,可以实现outportComputeCustom(),并通过在命令行中添加-routing-algorithm=2来调用它。

多播消息:模拟的网络没有硬件多播支持。多播消息在网络接口处被分解为多个单播消息。
Translation: Garnet2.0利用gem5的ruby内存系统模型提供的路由基础设施。默认的路由算法是基于最短路径的确定性表格路由算法。链路权重可用于优先考虑某些链路而不是其他链路。有关如何填充路由表的详细信息,请参阅src/mem/ruby/network/Topology.cc。

Flow Control

控制流量控制被用于设计中。每个VC可以容纳一个数据包。设计中有两种VCs - 控制和数据。每个VC的缓冲深度可以从GarnetNetwork.py中独立控制。默认值是1-flit深的控制VCs,以及4-flit深的数据VCs。控制数据包的默认大小是1-flit,数据包是5-flit。

Router Microarchitecture

Garnet2.0路由器执行以下操作:

  1. 缓冲器写入(BW):传入的flit被缓冲在其VC中。
  2. 路由计算(RC):缓冲的flit计算其输出端口,并将此信息存储在其VC中。
  3. 交换分配(SA):所有缓冲的flit都尝试为下一个周期预留交换端口。[分配以可分离的方式进行:首先,每个输入使用输入仲裁器选择一个输入VC,并发出交换请求。然后,每个输出端口通过输出仲裁器解决冲突]。有序虚拟网络中的所有仲裁器都是排队的,以维持点对点的顺序。所有其他仲裁器都是轮询的。
  4. VC选择(VS):SA的获胜者选择一个空闲的VC(如果是HEAD / HEAD_TAIL flit)从其输出端口。
  5. 交换穿越(ST):赢得SA的flit穿越交叉连接开关。
  6. 链路穿越(LT):从交叉连接穿越链路到达下一个路由器。

在默认设计中,BW,RC,SA,VS和ST都发生在1个周期内。 LT发生在下一个周期。

多周期路由器:可以通过在拓扑文件中指定每个路由器的延迟或更改src / mem / ruby / network / BasicRouter.py中的默认路由器延迟来模拟多周期路由器。 这是通过使缓冲的flit在路由器中等待(延迟-1)周期,然后才有资格参加SA来实现的。

Buffer Management

每个路由器输入端口有 number_of_virtual_networks 个Vnets,每个Vnets有 vcs_per_vnet 个VCs。控制Vnets的VCs有 buffers_per_ctrl_vc 个缓冲区(默认值为1),数据Vnets的VCs有 buffers_per_data_vc 个缓冲区(默认值为4)。积分用于传递有关空闲VCs和每个VC中缓冲区数量的信息。

Lifecycle of a Network Traversal

  • NetworkInterface.cc::wakeup():
    • 每个连接到一个一端的一个协同协议控制器,另一端连接一个路由器的NI。
    • 从适当的vnet中接收协同协议缓冲区的消息,并将其转换为网络数据包,然后发送到网络中。
      • garnet2.0增加了在此处捕获网络跟踪的能力(正在开发中)。
    • 从网络接收flits,提取协议消息并将其发送到适当的vnet中的协同协议缓冲区。
    • 管理与其附加的路由器的流量控制(即,信用)。
    • 将NI的消费flit/credit输出链接放入全局事件队列,其时间戳设置为下一个周期。事件队列调用消费者中的wakeup函数。
  • NetworkLink.cc::wakeup():
    • 从NI/路由器接收flits,并在m_latency周期延迟后将其发送到NI/路由器。
    • 每个链接的默认延迟值可以从命令行设置(请参阅configs/network/Network.py),每个链接的延迟可以在拓扑文件中覆盖。
    • 将链接的消费者(NI/路由器)放入全局事件队列,其时间戳设置为m_latency周期后。
    • 事件队列调用消费者中的wakeup函数。
  • Router.cc::wakeup():
    • 循环遍历所有InputUnits并调用它们的wakeup();
    • 循环遍历所有OutputUnits并调用它们的wakeup();
    • 调用SwitchAllocator的wakeup();
    • 调用CrossbarSwitch的wakeup();
    • 当其中任何一个模块(InputUnit,OutputUnit,SwitchAllocator,CrossbarSwitch)有一个准备好的flit/credit可以在本周期处理时,路由器的wakeup函数就会被调用。
  • InputUnit.cc::wakeup():
    • 如果本周期准备好,从上游路由器读取输入flit;
    • 对于HEAD/HEAD_TAIL flits,执行路由计算,并更新VC中的路由;
    • 缓冲flit(m_latency-1)周期,并从该周期开始标记它为SwitchAllocation可用。
      • 每个路由器的默认延迟可以从命令行设置(请参阅configs/network/Network.py)
      • 每个路由器的延迟(即,管道阶段数)可以在拓扑文件中设置。
  • OutputUnit.cc::wakeup():
    • 如果本周期准备好,从下游路由器读取输入信用;
    • 在适当的输出VC状态中增加信用;
    • 如果信用携带is_free_signal为true,则将输出VC标记为空闲。
  • SwitchAllocator.cc::wakeup():
    • 注意:SwitchAllocator在其中执行VC仲裁和选择。
    • SA-I(或SA-i):循环遍历每个输入端口的所有输入VC,以轮流方式选择一个。
      • 对于HEAD/HEAD_TAIL flits,只选择一个输出端口至少有一个空闲输出VC的输入VC。
      • 对于BODY/TAIL flits,只选择其输出VC中有信用的输入VC。
    • 为此VC放置一个输出端口的请求。
    • SA-II(或SA-o):循环遍历所有输出端口,以轮流方式将一个输入VC(在SA-I期间放置了请求)选为此输出端口的获胜者。
      • 对于HEAD/HEAD_TAIL flits,执行outvc分配(即,从输出端口选择一个空闲VC)。
      • 对于BODY/TAIL flits,减少输出vc中的信用。
    • 从输入VC读取flit,并将其发送到CrossbarSwitch;
    • 向上游路由器发送此输入VC的增加信用信号。
      • 对于HEAD_TAIL/TAIL flits,在信用中标记is_free_signal为true;
      • 输入单元将信用发送到上游路由器的信用链接。
    • 如果有任何flits准备好下一个周期的SA,则重新安排路由器以唤醒下一个周期。
  • CrossbarSwitch.cc::wakeup():
    • 循环遍历所有输入端口,并将获胜的flit从其输出端口发送到输出链接上。
    • 将路由器的消费flit输出链接放入全局事件队列,其时间戳设置为下一个周期。
    • 事件队列调用消费者中的wakeup函数。
  • NetworkLink.cc::wakeup():
    • 从NI/路由器接收flits,并在m_latency周期延迟后将其发送到NI/路由器。
    • 每个链接的默认延迟值可以从命令行设置(请参阅configs/network/Network.py),每个链接的延迟可以在拓扑文件中覆盖。
    • 将链接的消费者(NI/路由器)放入全局事件队列,其时间戳设置为m_latency周期后。
    • 事件队列调用消费者中的wakeup函数。

Running Garnet2.0 with Synthetic Traffic

Garnet2.0可以独立运行并使用合成流量。详细信息请参见:Garnet Synthetic Traffic

HeteroGarnet: A Detailed Simulator for Diverse Interconnect Systems

HeteroGarnet改进了广受欢迎的Garnet 2.0网络模型,可以准确模拟新兴的互连系统。具体而言,HeteroGarnet增加了对时钟域岛、支持多频域的网络交叉以及能够连接到多个物理链路的网络接口控制器的支持。它还通过引入新的可配置序列化器 - 反序列化器组件来支持可变带宽链路和路由器。HeteroGarnet集成到gem5存储库中作为Garnet 3.0。HeteroGarnet建立在2009年发表的原始Garnet模型的基础上。

Image in a image block
Image in a image block

如果您对 HeteroGarnet 的使用有助于发表论文,请引用以下论文:

    @inproceedings{heterogarnet,
        author={Bharadwaj, Srikant and Yin, Jieming and Beckmann, Bradford and Krishna, Tushar},
        booktitle={2020 57th ACM/IEEE Design Automation Conference (DAC)},
        title={Kite: A Family of Heterogeneous Interposer Topologies Enabled via Accurate Interconnect Modeling},
        year={2020},
        volume={},
        number={},
        pages={1-6},
        doi={10.1109/DAC18072.2020.9218539}
	}
Image in a image block

Topology Construction

HeteroGarnet允许用户使用Python配置文件作为拓扑来配置复杂的拓扑。 整个拓扑配置可以包括系统的完整互连定义,包括任何异构组件。 定义拓扑的一般流程包括以下步骤:

  1. 确定系统中路由器的总数并实例化它们。
    1. 使用 Router 类来实例化各个路由器。
    2. 根据要求配置每个路由器的属性,例如时钟域、支持的迁移宽度。
      routers = Router(id, latency, clock_domain,
               flit_width, supported_vnets,
               vcs_per_vnet)
  2. 使用外部物理互连连接连接到端点(例如,核心、缓存、目录)的路由器。
    1. 使用 ExternalLink 类实例化连接端点的链接。
    2. 根据需要配置每个外部链接的属性,例如时钟域、链接宽度。
    3. 根据互连拓扑,启用时钟域交叉 (CDC) 和串行器解串器 (SerDes) 单元。
      external_link = ExternalLink(id, latency, clock_domain,
                            flit_width, supported_vnets,
                            serdes_enable, cdc_enable)
  3. 根据拓扑连接网络中的各个路由器。
    1. 使用 InternalLink 类实例化连接端点的链接。
    2. 根据要求配置每个内部链路的属性,例如时钟域、链路宽度。
    3. 根据互连拓扑,启用时钟域交叉和串行器-解串器单元。
      internal_link = InternalLink(id, latency, clock_domain,
                            flit_width, supported_vnets,
                            serdes_enable, cdc_enable)

Garnet 3.0 还提供了几个预配置脚本(./configs/Network/Network.py),它们可以自动完成其他一些步骤,例如实例化网络接口、域跨越和SerDes单元。 下面讨论了用于配置拓扑的几种类型的单元。

Physical Links

Garnet 3.0中的物理链路模型代表了互联线本身。链接是一个单独的实体,具有自己的延迟、宽度和可以传输的flit类型。链接还支持基于信用的反压机制。与升级的Garnet 3.0路由器类似,每个Garnet 3.0链接可以使用适当的参数配置为操作频率和宽度。这允许以不同频率运行的链接和路由器相互连接。

Network Interface

网络接口控制器(NIC)是一个对象,它位于网络端点(例如缓存,DMA节点)和互连系统之间。 NIC接收来自控制器的消息并将其转换为固定长度的flits,简称为流量控制单元。根据发出的物理链路,这些flits的大小是适当的。网络接口还负责发出和接收flits的流量控制和缓冲区管理。 Garnet 3.0允许将多个端口附加到单个端点。因此,NIC决定某个消息/ flit必须调度到哪里。

Clock Domain Crossing Units

为了支持多个时钟域,Garnet 3.0引入了时钟域交叉(CDC)单元,如下图(左)所示,它由先进先出(FIFO)缓冲器组成,可以在网络模型中的任何位置实例化。 CDC单元使得具有不同时钟域的体系结构成为可能。每个CDC单元的延迟可配置。延迟也可以根据连接到它的时钟域动态计算。这使得可以准确建模DVFS技术,因为CDC延迟通常是生产者和消费者的操作频率的函数。

Serializer-Deserializer Units

在建模SoC和异构架构中,另一个关键特性是支持系统中的各种互连宽度。考虑GPU内两个路由器之间的链接以及内存控制器和片上内存之间的链接。这两个链接可能宽度不同。为了实现这种配置,Garnet 3.0引入了如下图所示的序列化器解序列化器单元,它可以在位宽边界将flits转换为适当的宽度。这些SerDes单元可以与前面描述的CDC单元类似地在Garnet 3.0拓扑中实例化。

Image in a image block

Routing

路由算法决定flits如何穿过拓扑结构。路由策略的目标是最大限度地减少争用,同时最大限度地提高互连提供的带宽。Garnet 3.0提供了几种标准路由策略,用户可以从中选择。

Routing Policies.

已经提出了几种通用路由策略,用于通过互连网络的 flit 的无死锁路由。

Table based routing

Garnet 还具有基于表的路由策略,用户可以选择使用基于权重年龄的系统来设置自定义路由策略。较低权重的链接优于配置为具有较高权重的链接。

Flow Control and Buffer Management

流量控制机制确定了互连系统中的缓冲区分配。一个好的流量控制系统的目标是将缓冲区分配对系统中消息的整体延迟的影响降到最低。这些机制的实现通常涉及到互连系统中物理数据包的微观管理。

缓存控制器生成的一致性消息通常被分解为固定长度的flits(流量控制单元)。携带消息的一组flits通常称为数据包。数据包可以有一个头flit,一个身体flit和一个尾flit来携带消息的内容以及数据包本身的任何其他元数据。已经提出并在各种资源分配粒度上实施了几种流量控制技术。

Garnet 3.0实现了一种基于信用的flit级流量控制机制,支持虚拟通道。

Virtual Channels

网络中的虚拟通道 (VC) 充当单独的队列,可以在两个路由器或仲裁器之间共享物理线路(物理链路)。虚拟通道主要用于缓解队头阻塞。然而,它们也被用作避免死锁的手段。

Buffer Backpressure

大多数互连网络的实现都不允许在遍历期间丢弃数据包或迁移。因此,需要使用背压机制来严格管理迁移。

Credit-based backpressuring

信用基础的回压机制通常用于低延迟实现flit-stalling。信用跟踪下一个中间目的地可用缓冲区的数量,每次发送flit时都会减少总缓冲区。目的地在释放时会发回一个信用。

互联系统中的路由器执行仲裁、缓冲区分配和网络流量控制。路由器微架构的目标是在提供flit最小的每跳延迟的同时,最小化路由器内的争用。路由器微架构的复杂性也会影响互联系统的整体能耗和面积消耗。

Life of a Message in Garnet 3.0

在本节中,我们将描述消息在缓存控制器单元生成后在 NoC 中的生命周期。我们以 Garnet 3.0 为例来描述这个过程,但是一般的建模原理也可以扩展到其他软件模拟/建模工具。

Image in a image block

系统的总体流程如上图所示。它显示了一个简单的示例场景,其中一条消息由缓存控制器生成,发往另一个缓存控制器,该缓存控制器通过物理链路、串行器-解串器单元和时钟域交叉通过路由器连接。

Injection of Message

源缓存控制器创建一条消息并将一个或多个缓存控制器指定为目的地。然后将此消息注入消息队列。缓存控制器通常有多个传出和传入消息缓冲区,用于不同类型的消息.

Conversion to Flits.

一个网络接口控制器单元(NIC)连接到每个缓存控制器。此NIC唤醒并消耗消息队列中的消息。然后将每条消息转换为单播消息,然后根据发出的物理链路支持的大小将其分解为固定长度的flits。然后,根据下一跳的缓冲区可用性,根据目的地,路由策略和消息类型,对这些flits进行调度以进行传输。

Transmission to Local Router.

每个网络接口都连接到一个或多个“本地”路由器,这些路由器可以通过“外部”链路连接。一旦计划了一个迁移,它就会通过这些外部链路进行传输,这些外部链路会在一段定义的等待时间后将迁移传递给路由器。

Router Arbitration.

路由器是一个多级单元,跳跃器唤醒它。路由器包含输入缓冲器、VC分配、交换仲裁和交叉接头单元。到达时,跳跃器首先被放入输入缓冲器队列中。路由器中有几个输入缓冲器队列争夺下一跳的输出链路和VC。这是通过VC分配和交换仲裁阶段完成的。一旦一个跳跃器被选择传输,交叉接头阶段将跳跃器导向输出链路。然后,信用被发送回NIC,因为输入缓冲器空间被释放,为下一个跳跃器到达做准备。

Serialization-Deserialization.

序列化-反序列化 (SerDes) 是一个可选单元,可以根据设计要求启用。 SerDes 单元消耗 flit 并将其适当地转换为传出的 flit 大小。除了处理数据包之外,SerDes 还通过序列化或反序列化信用单元来处理信用系统。

Area, Power and Energy Model

Orion2.0 和 DSENT 等框架为 NoC 路由器和链路的各种构建块提供面积和功率模型。 HeteroGarnet 将 DSENT 作为外部工具集成,以在模拟结束时报告面积、功率和能量(取决于活动)。

MOESI CMP Directory

Protocol Overview
  • TODO: 缓存层次结构
  • 与 MESI 协议相比,MOESI 协议引入了一个额外的拥有状态。
  • MOESI 协议还包括许多 MESI 协议中不可用的合并优化。
Related Files
  • src/mem/protocols
    • MOESI_CMP_directory-L1cache.sm: L1缓存控制器规范
    • MOESI_CMP_directory-L2cache.sm: L2缓存控制器规范
    • MOESI_CMP_directory-dir.sm: directory控制器规范
    • MOESI_CMP_directory-dma.sm: DMA控制器规格
    • MOESI_CMP_directory-msg.sm: 消息类型规范
    • MOESI_CMP_directory.slicc: 容器文件
L1 Cache Controller
Stable States and Invariants
States Invariants
MM 缓存块由该节点独占并可能被修改(类似于传统的“M”状态)。
MM_W 缓存块由该节点独占并可能被修改(类似于传统的“M”状态)。在此状态下不允许替换和 DMA 访问。该块在超时后自动转换为 MM 状态。
O 缓存块归该节点所有。它没有被这个节点修改过。没有其他节点以独占模式持有该块,但共享者可能存在。
M 缓存块被保持在独占模式,但不被写入(类似于传统的 "E "状态)。没有其他节点持有这个块的副本。在这种状态下不允许存储。
M_W 缓存块被保持在独占模式,但不被写入(类似于传统的 "E "状态)。没有其他节点持有这个块的副本。只允许加载和存储。沉默的升级发生在存储的MM_W状态。替换和DMA访问在这个状态下是不允许的。该块在超时后自动转换到M状态。
S 缓存块被1个或多个节点保持在共享状态。在这种状态下不允许存储。
I 缓存块无效。
FSM Abstraction

这里here介绍一下控制器FSM图中使用的符号 

Image in a image block
Optimizations
States Description
SM 一个GETX已经发出,为即将到来的对缓存区块的存储获得独占权限,但该区块的一个旧拷贝仍然存在。在这种状态下,存储和替换是不允许的。
OM 一个GETX已经发出,为即将存储到缓存块获得独占权限,数据已经收到,但所有预期的确认还没有到达。在这种状态下不允许存储和替换。

这里here介绍一下控制器FSM图中使用的符号。

Image in a image block
L2 Cache Controller
Stable States and Invariants
Intra-chip Inclusion Inter-chip Exclusion States Description
Not in any L1 or L2 at this chip May be present at other chips NP/I 该芯片上的缓存块无效。
Not in L2, but in 1 or more L1s at this chip May be present at other chips ILS 该缓存块不存在于该芯片的L2上。它是由该芯片的L1节点本地共享的。
ILO 该缓存块不存在于该芯片的L2。这个芯片上的某个L1节点是这个缓存块的所有者。
ILOS 该缓存块不存在于该芯片的L2。这个芯片上的某个L1节点是这个缓存块的所有者。在这块芯片上也有这个缓存块的L1共享者。
Not present at any other chip ILX 该缓存块不存在于该芯片的L2中。它被这块芯片上的某个L1节点以独占模式持有。
ILOX 该缓存块不存在于该芯片的L2。它是由这块芯片独家持有的,这块芯片中的某个L1节点是该块的所有者。
ILOSX 该缓存块不存在于该芯片的L2处。它是由这块芯片独家持有的。这个芯片中的一些L1节点是这个块的所有者。在这个芯片中也有这个缓存块的L1共享者。
In L2, but not in any L1 at this chip May be present at other chips S 该缓存块不存在于该芯片的L1处。它以共享模式存在于本芯片的L2,也有可能是跨芯片共享。
O 该缓存块不存在于该芯片的L1上。它是在这个芯片的L2上以自有模式保存的。它也有可能是跨芯片共享的。
Not present at any other chip M 该缓存块不存在于该芯片的L1处。它存在于该芯片的L2,并有可能被修改。
Both in L2, and 1 or more L1s at this chip May be present at other chips SLS 该缓存块在该芯片上以共享模式存在于L2。在这块芯片上存在本地L1的共享者。它也有可能是跨芯片共享的。
OLS 该缓存块在该芯片上以自有模式存在于L2。在这块芯片上存在本地L1的共享者。它也有可能是跨芯片共享的。
Not present at any other chip OLSX 该缓存块在该芯片上以自有模式存在于L2。在这块芯片上存在本地的L1共享者。它是由这块芯片独家持有的。
FSM Abstraction

该控制器分2部分描述。第一张图片显示了所有 "芯片内包容 "类别之间以及类别1、3、4内的转换。第二张图片显示了类别2(不在L2,但在该芯片的1个或多个L1)内的过渡。

这里描述了控制器FSM图中使用的符号。涉及其他芯片的转换用棕色注释。

Image in a image block

下面的第二张图扩大了上图的中央六边形部分,以显示第二类内的转换(不在L2,但在这个芯片的1个或多个L1)。

这里描述了控制器FSM图中使用的符号。涉及其他芯片的转换用棕色标注。

Image in a image block
Directory Controller
*Stable States and

Invariants**

States Invariants
M 缓存区只由1个节点(也是所有者)持有,处于独占状态。这个区块没有共享者。该数据可能与内存中的数据不同。
O 缓存区正好被1个节点所拥有。这个块可能有共享者。该数据可能与内存中的数据不同。
S 缓存块由1个或多个节点以共享状态持有。没有节点对该块有所有权。数据与内存中的数据是一致的(检查)。
I 缓存块无效。
FSM Abstraction

这里介绍一下控制器FSM图中使用的符号。

Image in a image block

Garnet Synthetic Traffic

Garnet合成流量提供了一个框架,用于模拟Garnet网络的受控输入。这对于网络测试/调试,或仅用合成流量进行网络模拟是非常有用的。

注意:Garnet合成流量注入器只适用于Garnet_standalone一致性协议。

Related files

  • configs/example/garnet_synth_traffic.py: 调用网络测试器的文件
  • src/cpu/testers/garnet_synthetic_traffic: 文件执行测试器。
    • GarnetSyntheticTraffic.py
    • GarnetSyntheticTraffic.hh
    • GarnetSyntheticTraffic.cc

How to run

首先用Garnet_standalone一致性协议构建gem5。Garnet_standalone协议是与ISA无关的,因此我们用NULL ISA构建它。

scons build/NULL/gem5.debug PROTOCOL=Garnet_standalone

Example command:

./build/NULL/gem5.debug configs/example/garnet_synth_traffic.py  \
        --num-cpus=16 \
        --num-dirs=16 \
        --network=garnet2.0 \
        --topology=Mesh_XY \
        --mesh-rows=4  \
        --sim-cycles=1000 \
        --synthetic=uniform_random \
        --injectionrate=0.01

Parameterized Options

System Configuration Description
–num-cpus cpus的数量。这是网络中源(注入)节点的数量。
–num-dirs 目录的数量。这是网络中目标(弹射)节点的数量。
–network 网络模型:简单或garnet2.0。使用garnet2.0来运行合成流量。
–topology 连接cpus和dirs与网络路由器/交换机的拓扑结构。关于不同拓扑结构的更多细节可以找到(这里)[Interconnection_Network#Topology].
–mesh-rows 网格中的行数。仅当''-topology''为''Mesh_''或''MeshDirCorners_''时有效。
Network Configuration Description
–router-latency garnet路由器中管道阶段的默认数量。必须是>=1。可以在拓扑文件中以每个路由器为基础被覆盖。
–link-latency 网络中每个链接的默认延迟。必须是>=1。可以在拓扑文件中以每个链路为基础进行覆盖。
–vcs-per-vnet 每个虚拟网络的 VC 数量。
–link-width-bits garnet网络内所有链接的宽度(以位为单位). Default = 128.
Traffic Injection Description
–sim-cycles 模拟应该运行的周期总数。
–synthetic 要注入的合成流量的类型。目前支持以下合成流量模式。uniform_random', 'tornado', 'bit_complement', 'bit_reverse', 'bit_rotation', 'neighbour', 'shuffle', and 'transpose'.
–injectionrate 流量注入率,单位是数据包/节点/周期。小数点后的精确数字可以由''-precision''控制,在''garnet_synth_traffic.py''中默认设置为3。
–single-sender-id 只从这个发送者注入。要从所有节点发送,设置为-1。
–single-dest-id 只向这个目的地发送。要向合成流量模式指定的所有目的地发送,设置为-1。
–num-packets-max 每个cpu节点要注入的最大数据包数量。默认值是-1(一直注入到模拟周期)。
–inj-vnet 只在这个vnet(0、1或2)中注入。0和1是1-lit,2是5-lit。设置为-1可以在所有的网内随机注入。

Implementation of Garnet synthetic traffic

合成流量注入器在GarnetSyntheticTraffic.cc中实现。生成和发送数据包的步骤顺序如下。

  • 每个周期,每个CPU都以-injectionrate的概率执行伯努利试验,以确定是否生成数据包。
  • 如果-num-packets-max为非负数,每个CPU在生成-num-packets-max数量的数据包后停止生成新的数据包。注入器在-sim-cycles后终止。
  • 如果CPU必须生成新的数据包,它将根据合成流量类型(-synthetic)计算新数据包的目的地。
  • 这个目的地嵌入到数据包地址的块偏移后的位中。
  • 生成的数据包被随机标记为ReadReq、INST_FETCH或WriteReq,并发送到Ruby端口(src / mem / ruby / system / RubyPort.hh / cc)。
  • Ruby端口将数据包转换为RubyRequestType:LD、RubyRequestType:IFETCH和RubyRequestType:ST,然后发送到序列器,序列器又将其发送到Garnet_standalone缓存控制器。
  • 缓存控制器从数据包地址中提取目标目录。
  • 缓存控制器将LD、IFETCH和ST分别注入虚拟网络0、1和2。
    • LD和IFETCH被注入为控制数据包(8字节),而ST被注入为数据数据包(72字节)。
  • 数据包穿过网络并到达目录。
  • 目录控制器简单地将其丢弃。

SLICC

SLICC是一种针对特定领域的语言,用于指定缓存一致性协议。 SLICC编译器为不同的控制器生成C++代码,可以与Ruby的其他部分协同工作。 编译器还生成协议的HTML规范。 默认情况下禁用HTML输出。 要启用HTML输出,请在编译时将选项“SLICC_HTML = True”传递给scons。

Input To the Compiler

SLICC编译器将指定协议中涉及的控制器的文件作为输入。.slicc文件指定所考虑的特定协议使用的不同文件。例如,如果试图用SLICC指定MI协议,那么我们可以使用MI.slicc作为文件,指定该协议所需的所有文件。指定协议所需的文件包括不同控制器的状态机的定义,以及这些控制器之间传递的网络信息的定义。
这些文件的语法与C++相似。使用PLY (Python Lex-Yacc)编写的编译器解析这些文件以创建一个抽象语法树(AST)。然后,AST被遍历以建立一些内部数据结构。最后,编译器通过再次遍历该树来输出C++代码。AST代表了在状态机中存在的不同结构的层次。我们接下来描述这些结构。

Protocol State Machines

在这一节中,我们将仔细研究包含状态机规范的文件中的内容。

Specifying Data Members

每个状态机都用SLICC的机器数据类型来描述。每个机器都有几种不同类型的成员。缓存和目录控制器的机器分别包括缓存内存和目录内存数据成员。我们将使用 src/mem/protocol 中提供的 MI 协议作为我们的运行示例。所以下面是你可能想开始写一个状态机的方法

machine(MachineType:L1Cache, "MI Example L1 Cache")
  : Sequencer * sequencer,
    CacheMemory * cacheMemory,
    int cache_response_latency = 12,
    int issue_latency = 2 {
      // Add rest of the stuff
    }

为了让控制器接收来自系统中不同实体的消息,机器有一些消息缓冲区。这些作为机器的输入和输出端口。下面是一个指定输出端口的例子。

 MessageBuffer requestFromCache, network="To", virtual_network="2", ordered="true";
 MessageBuffer responseFromCache, network="To", virtual_network="4", ordered="true";

注意,消息缓冲器有一些属性需要正确指定。另一个例子,这次是指定输入端口的。

 MessageBuffer forwardToCache, network="From", virtual_network="3", ordered="true";
 MessageBuffer responseToCache, network="From", virtual_network="4", ordered="true";

接下来,机器包括了一个机器可能达到的状态的声明。在缓存一致性协议中,状态可以有两种类型--稳定的和暂时的。如果在没有任何活动的情况下(例如,来自另一个控制器对该块的请求),缓存块将永远保持在该状态下,那么该缓存块就被称为稳定状态。瞬时状态是在稳定状态之间转换所需要的。当两个稳定状态之间的转换不能以原子方式进行时,就需要用到瞬态。接下来是一个例子,显示了状态是如何被声明的。SLICC有一个关键字state_declaration,必须用于声明状态。

state_declaration(State, desc="Cache states") {
   I, AccessPermission:Invalid, desc="Not Present/Invalid";
   II, AccessPermission:Busy, desc="Not Present/Invalid, issued PUT";
   M, AccessPermission:Read_Write, desc="Modified";
   MI, AccessPermission:Busy, desc="Modified, issued PUT";
   MII, AccessPermission:Busy, desc="Modified, issued PUTX, received nack";
   IS, AccessPermission:Busy, desc="Issued request for LOAD/IFETCH";
   IM, AccessPermission:Busy, desc="Issued request for STORE/ATOMIC";
}

状态I和M是这个例子中唯一的稳定状态。再次注意,某些属性必须与状态一起被指定。

状态机需要指定它可以处理的事件,从而从一个状态过渡到另一个状态。SLICC提供了关键字enumeration,可以用来指定可能的事件集。举个例子来说明这个问题 --

enumeration(Event, desc="Cache events") {
   // From processor
   Load,       desc="Load request from processor";
   Ifetch,     desc="Ifetch request from processor";
   Store,      desc="Store request from processor";
   Data,       desc="Data from network";
   Fwd_GETX,        desc="Forward from network";
   Inv,        desc="Invalidate request from dir";
   Replacement,  desc="Replace a block";
   Writeback_Ack,   desc="Ack from the directory for a writeback";
   Writeback_Nack,   desc="Nack from the directory for a writeback";
}

在开发协议机的过程中,我们可能需要定义代表内存系统中不同实体的结构。SLICC为这个目的提供了关键词结构。一个例子如下

structure(Entry, desc="...", interface="AbstractCacheEntry") {
   State CacheState,        desc="cache state";
   bool Dirty,              desc="Is the data dirty (different than memory)?";
   DataBlock DataBlk,       desc="Data in the block";
}

使用SLICC结构的好处是,它能自动为你生成不同字段的get和set函数。它还写了一个漂亮的打印函数,并重载了<<操作符。但是,如果你想自己做所有的事情,你可以在结构的声明中使用关键字external。这将阻止SLICC为这个结构生成C++代码。

structure(TBETable, external="yes") {
   TBE lookup(Address);
   void allocate(Address);
   void deallocate(Address);
   bool isPresent(Address);
}

事实上,在 src/mem/protocol/RubySlicc_*.sm 文件中存在许多预定义类型。你可以利用它们,或者如果你需要新的类型,你也可以定义新的类型。你也可以使用关键字interface来利用C++中的继承特性。注意,目前SLICC只支持公共继承。

我们也可以像在C++中那样声明和定义函数。有一些函数,编译器希望总是由控制器来定义。这些函数包括

  • getState()
  • setState()
Input for the Machine

由于协议是状态机,我们需要指定机器在接受输入时如何从一个状态过渡到另一个状态。如前所述,每个机器都有几个输入和输出端口。对于每个输入端口,in_port关键字用于指定机器的行为,当该输入端口收到消息时。下面是一个例子,显示了声明输入端口的语法。

in_port(mandatoryQueue_in, RubyRequest, mandatoryQueue, desc="...") {
  if (mandatoryQueue_in.isReady()) {
    peek(mandatoryQueue_in, RubyRequest, block_on="LineAddress") {
      Entry cache_entry := getCacheEntry(in_msg.LineAddress);
      if (is_invalid(cache_entry) &&
          cacheMemory.cacheAvail(in_msg.LineAddress) == false ) {
        // make room for the block
        trigger(Event:Replacement, cacheMemory.cacheProbe(in_msg.LineAddress),
                getCacheEntry(cacheMemory.cacheProbe(in_msg.LineAddress)),
                TBEs[cacheMemory.cacheProbe(in_msg.LineAddress)]);
      }
      else {
        trigger(mandatory_request_type_to_event(in_msg.Type), in_msg.LineAddress,
                cache_entry, TBEs[in_msg.LineAddress]);
      }
    }
  }
}

正如你所看到的,in_port接收了多个参数。第一个参数, mandatoryQueue_in, 是文件中使用的 in_port 的标识符。下一个参数,RubyRequest,是这个输入端口所接收的消息的类型。每个输入端口使用一个队列来存储消息,队列的名称是第三个参数。

关键字 peek 用来从输入端口的队列中提取消息。这个关键字的使用隐含地声明了一个变量in_msg,其类型与输入端口声明中指定的相同。这个变量指向队列中最前面的消息。它可以用来访问消息的字段,如上面的代码所示。

一旦对传入的消息进行了分析,就可以使用这个消息来采取一些适当的行动,改变机器的状态。这是用关键字触发器完成的。触发器函数实际上只在SLICC代码中使用,在生成的代码中不存在。相反,这个调用被转换为对doTransition()函数的调用,在生成的代码中出现。doTransition()函数是由SLICC为每个状态机自动生成的。触发参数的数量取决于机器本身。一般来说,触发器的输入参数是需要处理的消息的类型,这个消息的地址,该地址的缓存和交易缓冲器条目。

trigger也会增加一个计数器,在进行转换之前会检查这个计数器。在一个Ruby周期中,可以进行的转换次数是有限制的。这样做是为了更接近于基于硬件的状态机。 @TODO:如果没有更多的转换了会怎样?唤醒会中止吗?

Actions

在这一节中,我们将讨论如何定义一个状态机可以执行的动作。当状态机收到一些输入信息时,这些动作将被调用,然后用来进行转换。让我们来看看如何利用关键词action的例子。

action(a_issueRequest, "a", desc="Issue a request") {
   enqueue(requestNetwork_out, RequestMsg, latency=issue_latency) {
   out_msg.Address := address;
     out_msg.Type := CoherenceRequestType:GETX;
     out_msg.Requestor := machineID;
     out_msg.Destination.add(map_Address_to_Directory(address));
     out_msg.MessageSize := MessageSizeType:Control;
   }
}

第一个输入参数是动作的名称,第二个参数是用于生成文档的缩写,最后一个参数是动作的描述,用于HTML文档和C++代码的注释。

每个动作都被转换为一个同名的C++函数。生成的C++代码在函数头中隐含了最多三个输入参数,这也取决于机器的情况。这些参数是正在进行操作的内存地址,与该地址有关的缓存和事务缓冲区条目。

接下来要看的有用的东西是enqueue关键字。这个关键字用于将动作产生的消息排到一个输出端口。这个关键字需要三个输入参数,即输出端口的名称、要排队的消息的类型和这个消息可以被取消排队的延迟。注意,如果随机化被启用,指定的延迟将被忽略。关键字的使用隐含地声明了一个变量out_msg,由后续语句填充。

Transitions

一个过渡函数是一个从状态集和事件集的交叉积到状态集的映射。SLICC提供了关键字transition来指定状态机的过渡函数。下面是一个例子

transition(IM, Data, M) {
   u_writeDataToCache;
   sx_store_hit;
   w_deallocateTBE;
   n_popResponseQueue;
}

在这个例子中,初始状态是IM。如果在该状态下发生了一个Data类型的事件,那么最终状态将是M。在进行转换之前,状态机可以对它所维护的结构执行某些动作。在给定的例子中,u_writeDataToCache就是一个动作。所有这些操作都是以原子的方式进行的,也就是说,在完成过渡所指定的一系列操作之前,不会有其他事件发生。

为了便于使用,可以提供事件和状态的集合作为过渡的输入。这些集合的交叉产物将映射到相同的最终状态。请注意,最终状态不能是一个集合。如果对于一个特定的事件,最终状态与初始状态相同,那么最终状态可以被省略。

transition({IS, IM, MI, II}, {Load, Ifetch, Store, Replacement}) {
   z_stall;
}
Special Functions
Stalling/Recycling/Waiting input ports

SLICC和由此产生的状态机的一个更复杂的内部特征是如何处理由于缓存块处于瞬时状态而无法处理事件的情况。有几种可能的方法来处理这种情况,每种解决方案都有不同的权衡。本小节试图解释这些差异。请给gem5-user列表发电子邮件,以便进一步跟进。

Stalling the input port

处理无法处理的事件的最简单方法是简单地停滞输入端口。正确的方法是在过渡语句中加入 "z_stall "动作。

transition({IS, IM, MI, II}, {Load, Ifetch, Store, Replacement}) {
   z_stall;
}

在内部,SLICC将为这一转换返回一个ProtocolStall,在停滞的消息被处理之前,将不会有来自相关输入端口的后续消息被处理。然而,其他的输入端口将被分析为准备好的消息并被平行处理。虽然这是一个相对简单的解决方案,但人们可能会注意到,在同一个输入端口上停滞不相关的消息会导致过度和不必要的停滞。

有一点需要注意的是,不要像这样把过渡语句留空。

transition({IS, IM, MI, II}, {Load, Ifetch, Store, Replacement}) {
   // stall the input port by simply not popping the message
}

这将导致SLICC为这个转换返回成功,SLICC将继续重复分析同一个输入端口。其结果是最终的死锁。

Recycling the input port

性能更好但更不现实的解决方案是在输入端口回收停滞的消息。这样做的方法是使用 "zz_recycleMandatoryQueue "动作。

`action(zz_recycleMandatoryQueue, "\z", desc="Send the head of the mandatory queue to the back of the queue.") {
   mandatoryQueue_in.recycle();
}`

`transition({IS, IM, MI, II}, {Load, Ifetch, Store, Replacement}) {
   zz_recycleMandatoryQueue;
}`

这个动作的结果是,转换返回一个协议停顿,违规的消息被移到FIFO输入端口的后面。因此,同一输入端口上其他不相关的消息可以被处理。这个解决方案的问题是,回收的消息可能会在每个周期被分析和重新分析,直到一个地址改变状态。

Stall and wait the input port

一个更好,但更复杂的解决方案是 "拖延和等待 "违规的输入信息。这样做的方法是使用 "z_stallAndWaitMandatoryQueue "动作。

`action(z_stallAndWaitMandatoryQueue, "\z", desc="recycle L1 request queue") {
   stall_and_wait(mandatoryQueue_in, address);
}`

`transition({IS, IM, IS_I, M_I, SM, SINK_WB_ACK}, {Load, Ifetch, Store, L1_Replacement}) {
   z_stallAndWaitMandatoryQueue;
}`

这个动作的结果是转换返回成功,这很好,因为stall_and_wait将违规的消息从输入端口移到了与输入端口相关的边表。该消息在被唤醒之前不会再被分析。在这期间,其他不相关的消息将被处理。

停滞和等待的复杂部分是,停滞的消息必须被其他消息/转换明确地唤醒。特别是,将一个地址移动到基础状态的过渡应该唤醒等待该地址的潜在的停滞消息。

`action(kd_wakeUpDependents, "kd", desc="wake-up dependents") {
   wakeUpBuffers(address);
}`

`transition(M_I, WB_Ack, I) {
   s_deallocateTBE;
   o_popIncomingResponseQueue;
   kd_wakeUpDependents;
}`

替换特别复杂,因为停滞的地址与他们实际等待改变的地址不相关。在这些情况下,所有等待的信息都必须被唤醒。

`action(ka_wakeUpAllDependents, "ka", desc="wake-up all dependents") {
   wakeUpAllBuffers();
}`

`transition(I, L2_Replacement) {
   rr_deallocateL2CacheBlock;
   ka_wakeUpAllDependents;
}`
Other Compiler Features
  • SLICC支持以ifelse形式的条件语句。请注意,SLICC不支持else if
  • 每个函数都有返回类型,也可以是void。返回值不能被忽略。
  • SLICC对指针变量的支持有限。支持is_valid()和is_invalid()操作,用于测试给定指针是否“不为NULL”和“为NULL”。关键字OOD(Out of Domain的缩写)扮演C ++中使用的关键字NULL的角色。
  • SLICC不支持**!**(非运算符)。
  • SLICC支持静态类型转换。为此,提供了关键字static_cast。例如,在以下代码片段中,将AbstractCacheEntry类型的变量转换为Entry类型的变量。

   Entry L1Dcache_entry := static_cast(Entry, "pointer", L1DcacheMemory[addr]);

SLICC Internals

C++到Slicc的接口 - @注:这些文件分别做什么/定义什么?

  • src/mem/protocol/RubySlicc_interaces.sm
    • RubySlicc_Exports.sm
    • RubySlicc_Defines.sm
    • RubySlicc_Profiler.sm
    • RubySlicc_Types.sm
    • RubySlicc_MemControl.sm
    • RubySlicc_ComponentMapping.sm

Variable Assignments

  • 使用:=运算符来分配类中的成员(例如在RubySlicc_Types.sm中定义的成员)。
    • 在SLICC文件中提到的名称上自动添加一个m_

MI Example

Protocol Overview
  • 这是一个简单的缓存一致性协议,用于使用SLICC指定协议。
  • 这个协议假定有一个1级缓存层次结构。缓存是每个节点的私有缓存。缓存由目录控制器保持一致性。由于层次结构只有1级,因此不需要包含/排除要求。
  • 这个协议不区分加载和存储。
  • 这个协议不能实现LL/SC指令的语义,因为外部GETS请求命中LL/SC序列中的块会窃取独占权限,从而导致SC指令失败。
Related Files
  • src/mem/protocols
    • MI_example-cache.sm: 高速缓存控制器规范
    • MI_example-dir.sm: directory控制器规范
    • MI_example-dma.sm: DMA控制器规格
    • MI_example-msg.sm: 消息类型规范
    • MI_example.slicc: 容器文件
Stable States and Invariants
States Invariants
M 该缓存块已被该节点访问(读/写)。没有其他节点持有该缓存块的副本。
I 该节点的缓存块无效

这里 here 介绍一下控制器FSM图中使用的符号。

Cache controller
  • Requests, Responses, Triggers:
    • 从内核加载、取指、存储
    • 自我替代
    • 来自目录控制器的数据
    • 从目录控制器转发的请求(干预)。
    • 来自目录控制器的回写确认
    • 来自目录控制器的失效(在 dma 活动上)
Image in a image block
  • 主要操作:
    • 对于来自核心的负载/指令获取/存储请求:
    • 检查相应的块是否存在于M状态中,如果是,则返回命中
    • 否则,如果处于I状态,则向目录控制器发出GETX请求
    • 对于来自自身的替换触发器:
      • 驱逐该块,向目录控制器发出回写请求
      • 等待目录控制器的确认(以防止竞争)
    • 对于来自目录控制器的转发请求:
      • 这意味着,当其他节点生成请求时,该块处于此节点的M状态
      • 它将块直接发送到请求节点(缓存到缓存传输)
      • 它从此节点驱逐该块
    • 无效与替换类似
Directory controller
  • 请求、响应、触发器。
    • 来自内核的GETX,转发到内核的GETX
    • 来自内存的数据,到核心的数据
    • 来自内核的回写请求,对内核的回写确认
    • 来自DMA控制器的DMA读、写请求
Image in a image block
  • 主要操作。
    • 该目录保持跟踪哪个核有一个处于M状态的块。它将这个核心指定为该块的所有者。
    • 在一个核的GETX请求中:
      • 如果区块不存在,就会启动一个内存获取请求。
      • 如果区块已经存在,那么这意味着请求是由其他内核发出的
        • 在这种情况下,一个转发的请求被发送到原始所有者那里。
        • 区块的所有权被转移到请求者身上。
    • 在来自一个核心的回写请求中。
      • 如果该核是所有者,数据被写到内存中,确认被送回给该核
      • 如果该核不是所有者,就会发回一个NACK:
        • 这可能发生在一个竞赛条件下
        • 当其他核心的转发请求正在进行时,该核心驱逐了该块,而目录已经改变了该核心的所有权。
        • 驱逐的核心持有数据,直到转发的请求到达。
    • 在DMA访问(读/写)中
      • 无效信息被发送到所有者节点(如果有的话)。否则,数据将从内存中获取。
      • 这确保了最新的数据是可用的。
Other features
  • MI协议不支持LL/SC语义。来自远程核心的负载将使缓存块失效
  • 这个协议没有超时机制。

Garnet Standalone

This is a dummy cache coherence protocol that is used to operate Garnet in a standalone manner. This protocol works in conjunction with the Garnet Synthetic Traffic injector.

这是一个假的缓存一致性协议,用于以独立的方式运行Garnet。该协议与Garnet Synthetic Traffic一起工作。

Related Files
  • src/mem/protocols
    • Garnet_standalone-cache.sm: cache controller specification
    • Garnet_standalone-dir.sm: directory controller specification
    • Garnet_standalone-msg.sm: message type specification
    • Garnet_standalone.slicc: container file
Cache Hierarchy

这个协议假设了一个1级的缓存层次。缓存的作用是简单地将消息从cpu发送到适当的目录(基于地址),在适当的虚拟网络(基于消息类型)。它不跟踪任何状态。事实上,与其他协议不同,没有创建CacheMemory。目录从缓存中接收消息,但不送回任何消息。这个协议的目标是能够模拟/测试只是互连网络。

Stable States and Invariants
States Invariants
I 所有缓存块的默认状态
Cache controller
  • 请求、响应、触发器:
    • 从内核加载、取指令、存储。

网络测试器(在 src/cpu/testers/networktest/networktest.cc 中)产生 ReadReq、INST_FETCH 和 WriteReq 类型的数据包,它们分别被 RubyPort(在 src/mem/ruby/system/RubyPort.hh/cc 中)转换为 RubyRequestType:LD、RubyRequestType:IFETCH 和 RubyRequestType:ST。这些消息通过Sequencer到达缓存控制器。这些消息的目的地是由流量类型决定的,并嵌入地址中。更多的细节可以在这里找到。

  • Main Operation:
    • 缓存的目标只是作为底层互连网络的一个源节点。它不跟踪任何状态。
    • 在来自核心的 LD 上:
      • 它返回一个命中,并且
      • 将地址映射到一个目录,并在请求vnet(0)中为其发布一个类型为MSG、大小为Control(8字节)的消息。
      • 注意:通过取消对Network_test-cache.sm中a_issueRequest动作的适当行的注释,也可以使vnet 0成为广播,而不是向某个特定目录发送定向信息。
    • 在来自核心的 IFETCH 上:
      • 它返回一个命中,并且
      • 将地址映射到一个目录,并在前向vnet(1)中为其发布一个类型为MSG、大小为Control(8字节)的消息。
    • 在来自核心的 ST 上:
      • 它返回一个命中,并且
      • 将地址映射到一个目录,并在响应vnet(2)中为其发布一个类型为MSG、大小为Data(72字节)的消息。
    • 注意:request、forward和response只是用来区分虚拟网络,但在该协议中没有任何物理意义。
Directory controller
  • 请求、响应、触发器:
    • 来自核心的MSG
  • Main Operation:
    • 该目录的目标只是作为底层互连网络中的一个目标节点。它不跟踪任何状态。
    • 目录在收到消息时,只需弹出它的传入队列。
Other features

此协议仅假定 3 个 vnet。

Interconnection Network

这里描述了gem5的ruby内存系统内部的互连网络模型的各个组成部分。

How to invoke the network

Simple Network:

./build/<ISA>/gem5.debug \
                      configs/example/ruby_random_test.py \
                      --num-cpus=16  \
                      --num-dirs=16  \
                      --network=simple
                      --topology=Mesh_XY  \
                      --mesh-rows=4

默认的网络是简单的,默认的拓扑结构是crossbar。

Garnet network:

./build/<ISA>/gem5.debug \
                      configs/example/ruby_random_test.py  \
                      --num-cpus=16 \
                      --num-dirs=16  \
                      --network=garnet2.0 \
                      --topology=Mesh_XY \
                      --mesh-rows=4

Topology

各个控制器之间的连接是通过python文件指定的。所有外部链接(控制器和路由器之间)都是双向的。所有的内部链接(路由器之间)都是单向的--这允许每个链接上的每个方向的权重偏向于路由决策。

  • Related Files:
    • src/mem/ruby/network/topologies/Crossbar.py
    • src/mem/ruby/network/topologies/CrossbarGarnet.py
    • src/mem/ruby/network/topologies/Mesh_XY.py
    • src/mem/ruby/network/topologies/Mesh_westfirst.py
    • src/mem/ruby/network/topologies/MeshDirCorners_XY.py
    • src/mem/ruby/network/topologies/Pt2Pt.py
    • src/mem/ruby/network/Network.py
    • src/mem/ruby/network/BasicLink.py
    • src/mem/ruby/network/BasicRouter.py
  • Topology Descriptions:
    • Crossbar: 每个控制器(L1/L2/Directory)都连接到一个简单的开关。每个交换机连接到一个中央交换机(模拟横梁)。这可以通过-topology=Crossbar从命令行调用。
    • CrossbarGarnet: 每个控制器(L1/L2/Directory)都通过一个石榴石路由器(内部模拟横梁和分配器)与其他每个控制器相连。这可以在命令行中通过-topology=CrossbarGarnet调用。
    • Mesh_*: 这种拓扑结构要求目录的数量与cpus的数量相等。路由器/交换机的数量等于系统中cpus的数量。每个路由器/交换机连接到一个L1、一个L2(如果存在)和一个目录。网格中的行数必须由-mesh-rows来指定。这个参数也可以创建非对称的网状结构.
      • Mesh_XY: 具有XY路由的网格。所有X方向的链接的权重为1,而所有Y方向的链接的权重为2,这迫使所有信息在使用Y方向的链接之前先使用X方向的链接。它可以在命令行中通过-topology=Mesh_XY调用。
      • Mesh_westfirst: 网格的西向优先路由。所有西向链接的权重为1,其他链接的权重为2,这迫使所有信息在使用其他链接之前首先使用西向链接。它可以在命令行中通过-topology=Mesh_westfirst调用。
    • MeshDirCorners_XY: 这种拓扑结构要求目录的数量等于4。路由器/交换机的数量等于系统中cpu的数量。每个路由器/交换机连接到一个L1,一个L2(如果存在)。每个角落的路由器/交换机连接到一个目录。可以在命令行中通过-topology=MeshDirCorners_XY调用。网格中的行数必须由-mesh-rows来指定。使用XY路由算法。
    • Pt2Pt: 每个控制器(L1/L2/Directory)都通过直接链接与其他每个控制器相连。这可以在命令行中通过以下方式调用
    • Pt2Pt: All to all点对点连接
Image in a image block

在每个拓扑结构中,每个链接和每个路由器都可以独立地传递一个参数来覆盖默认值(在BasicLink.py和BasicRouter.py中)。

  • Link Parameters:
    • latency: 链路内遍历延迟.
    • weight: 与此链接相关的权重。这个参数被路由表在决定路由时使用,接下来在 Routing中解释。
    • bandwidth_factor: 只被simple network用来指定链接的宽度,单位是字节。这转化为一个带宽乘数(simple/SimpleLink.cc),单个链接的带宽变成带宽乘数x端点_bandwidth(在SimpleNetwork.py中指定)。在garnet中,带宽是由GarnetNetwork.py中的ni_flit_size指定的)
  • Internal Link Parameters:
    • src_outport: 带有源路由器输出端口名称的字符串。
    • dst_inport: 带有目标路由器输入端口名称的字符串。

这两个参数可以被路由器用来实现garnet2.0中的自定义路由算法。

  • Router Parameters:
    • latency: 每个路由器的延时。仅由garnet2.0支持。

Routing

Table-based Routing (Default): 基于拓扑结构,最短路径图的遍历被用来填充每个路由器/交换机的路由表。这在src/mem/ruby/network/Topology.cc中完成。默认的路由算法是基于表的,并试图选择链接遍历次数最少的路由。在拓扑文件中,链接可以被赋予权重以模拟不同的路由算法。例如,在Mesh_XY.py和MeshDirCorners_XY.py中,Y方向的链接被赋予2的权重,而X方向的链接被赋予1的权重,导致XY方向的穿越。在Mesh_westfirst.py中,西向链接的权重为1,而所有其他链接的权重为2。在garnet2.0中,路由算法在权重相同的链接中随机选择。在简单网络中,它静态地在具有相同权重的链接之间进行选择。

Custom Routing algorithms: 在garnet2.0中,我们提供了额外的支持来实现自定义(包括自适应)的路由算法(参见 src/mem/ruby/network/garnet2.0/RoutingUnit.cc中的outportComputeXY())。链接的src_outport和dst_inport字段可以用来给每个链接起自定义的名字(例如,如果是网状的方向),这些可以在garnet内部用来实现任何路由算法。通过设置-routing-algorithm=2,可以从命令行中选择一个自定义路由算法。参见configs/network/Network.py和src/mem/ruby/network/garnet2.0/GarnetNetwork.py。

Flow-Control and Router Microarchitecture

Ruby支持两种网络模型:Simple和Garnet,它们分别对详细建模和模拟速度进行了权衡。

Simple Network

Ruby 中的默认网络模型是简单网络。

  • Related Files:
    • src/mem/ruby/network/Network.py
    • src/mem/ruby/network/simple
    • src/mem/ruby/network/simple/SimpleNetwork.py

Configuration

简单网络使用Network.py中的通用网络参数。

  • number_of_virtual_networks: 这是虚拟网络的最大数量。活动的虚拟网络的实际数量由协议决定。
  • control_msg_size: 控制信息的大小,字节数。Network.cc中的m_data_msg_size被设置为以字节为单位的块大小+ control_msg_size。

其他参数在simple/SimpleNetwork.py中指定。

  • buffer_size: 每个交换机输入和输出端口的缓冲区的大小。值为0意味着无限的缓冲。
  • endpoint_bandwidth: 网络端点的带宽,以1000个字节为单位。
  • adaptive_routing: 这样就可以根据输出缓冲区的占用情况进行自适应路由。

Switch Model

简单网络建立了逐跳网络遍历的模型,但抽象出了交换机内的详细建模。交换机在simple/PerfectSwitch.cc中建模,而链路则在simple/Throttle.cc中建模。流量控制是通过在发送前监测输出链路的可用缓冲区和可用带宽来实现的。

Image in a image block
Garnet2.0

here此处是新 (2016) Garnet2.0 网络的详细信息。

Running the Network with Synthetic Traffic

互联网络可以以独立的方式运行,并输入合成流量。我们建议用garnet2.0来做这件事。

Running Garnet Standalone with Synthetic Traffic

MOESI Hammer

这是AMD Hammer协议的一个实现,它被用于AMD的Hammer芯片(也被称为Opteron或Athlon 64)。该协议既实现了最初的HyperTransport协议,也实现了较新的ProbeFilter协议。该协议还包括一个全位目录模式。

Related Files
  • src/mem/protocols
    • MOESI_hammer-cache.sm: cache controller specification
    • MOESI_hammer-dir.sm: directory controller specification
    • MOESI_hammer-dma.sm: dma controller specification
    • MOESI_hammer-msg.sm: message type specification
    • MOESI_hammer.slicc: container file
Cache Hierarchy

这个协议实现了一个2级的私有高速缓存层次结构。它为每个核分配了独立的指令和数据L1缓存,以及一个统一的L2缓存。这些缓存对每个内核都是私有的,并由一个共享的缓存控制器控制。该协议在L1和L2高速缓存之间执行排除法。

Stable States and Invariants
States Invariants
MM 该缓存块由该节点独家持有,并有可能被本地修改(类似于传统的 "M "状态)。
O 该缓存块为该节点所拥有。它没有被这个节点修改过。没有其他节点以独占模式持有该块,但共享者可能存在。
M 缓存区被保持在独占模式,但不被写入(类似于传统的 "E "状态)。没有其他节点持有这个块的副本。在这种状态下不允许存储。
S 缓存行持有数据的最新、正确的副本。系统中的其他处理器也可能在共享状态下持有该数据的副本。缓存行可以被读取,但在此状态下不能写入。
I 缓存行是无效的,没有保存数据的有效副本。
Cache controller

这里here描述了控制器FSM图中使用的符号。

MOESI_hammer支持缓存刷新。为了刷新一个缓存行,缓存控制器首先向目录发出GETF请求,以阻止该行,直到刷新完成。然后它发出一个PUTF,写回缓存行。

Image in a image block
Directory controller

MOESI_hammer内存模块,与典型的目录协议不同,不包含任何目录状态,而是向系统中的所有处理器广播请求。同时,它从DRAM中获取数据并将响应转发给请求者。

probe filter: TODO

Stable States and Invariants
States Invariants
NX 不是所有者,存在探测过滤器条目,在 O 处阻止 Owner。
NO 不是所有者,存在探测过滤器条目,在所有者的 E/M 中阻止。
S 数据干净,存在指向当前所有者的探测过滤器条目。
O 数据干净,存在探测过滤器条目。
E 独占所有者,没有探测过滤器条目。
Controller

此处here描述了控制器 FSM 图中使用的符号。

Image in a image block

MOESI CMP token

Protocol Overview
  • 该协议还模拟了 2 级缓存层次结构。
  • 它通过明确地交换和计算令牌来维持一致性许可。
  • 在开始时,为每个缓存块分配固定数量的令牌,令牌的数量保持不变。
  • 要写一个区块,处理器必须拥有该区块的所有令牌。读取时,至少需要一个令牌。
  • 该协议还有一个持久的消息支持,以避免饥饿。
Related Files
  • src/mem/protocols
    • MOESI_CMP_token-L1cache.sm: L1 cache controller specification
    • MOESI_CMP_token-L2cache.sm: L2 cache controller specification
    • MOESI_CMP_token-dir.sm: directory controller specification
    • MOESI_CMP_token-dma.sm: dma controller specification
    • MOESI_CMP_token-msg.sm: message type specification
    • MOESI_CMP_token.slicc: container file
Controller Description
L1 Cache
States Invariants
MM 该缓存块由该节点独家持有,并有可能被修改(类似于传统的 "M "状态)。
MM_W 缓存块由该节点独家持有,并有可能被修改(类似于传统的 "M "状态)。在这种状态下不允许替换和DMA访问。该块在超时后自动过渡到MM状态。
O 该缓存块为该节点所拥有。它没有被这个节点修改过。没有其他节点以独占模式持有该块,但共享者可能存在。
M 缓存区被保持在独占模式,但不被写入(类似于传统的 "E "状态)。没有其他节点持有这个块的副本。在这种状态下不允许存储。
M_W 缓存区被保持在独占模式,但不被写入(类似于传统的 "E "状态)。没有其他节点持有这个块的副本。只允许加载和存储。沉默的升级发生在存储的MM_W状态。替换和DMA访问在这个状态下是不允许的。该块在超时后自动转换到M状态。
S 缓存块被1个或多个节点保持在共享状态。在这种状态下不允许存储。
I 缓存块无效。
L2 cache
States Invariants
NP 该缓存块由该节点独家持有,并有可能被本地修改(类似于传统的 "M "状态)。
O 该缓存块为该节点所拥有。它没有被这个节点修改过。没有其他节点以独占模式持有该块,但共享者可能存在。
M 缓存区被保持在独占模式,但不被写入(类似于传统的 "E "状态)。没有其他节点持有这个块的副本。在这种状态下不允许存储。
S 缓存行持有数据的最新、正确的副本。系统中的其他处理器也可能在共享状态下持有该数据的副本。缓存行可以被读取,但在此状态下不能写入。
I 缓存行是无效的,没有保存数据的有效副本。
Directory controller
States Invariants
O Owner .
NO Not Owner.
L Locked.

MESI Two Level

Protocol Overview
  • 这个协议建立了两级缓存层次结构的模型。L1高速缓存是一个核的私有缓存,而L2高速缓存是各核之间共享的。L1缓存被分成指令缓存和数据缓存。
  • Inclusion在L1和L2缓存之间保持不变。
  • 在高水平上,该协议有四个稳定的状态,M、E、S和I。一个处于M状态的区块意味着该区块是可写的(即有排他性的权限),并且已经被清空(即它是片上唯一有效的拷贝)。 E状态代表缓存块有独占权限(即可写),但还没有被写入。 S状态表示该缓存块只可读,并且在多个私有缓存和共享缓存中可能存在多个副本。 I状态意味着该缓存块无效。
  • 片上高速缓存的一致性是通过目录一致性方案来维持的,其中目录信息与共享二级高速缓存中的相应缓存块共处一地。
  • 该协议有四种类型的控制器 - L1高速缓存控制器、L2高速缓存控制器、目录控制器和DMA控制器。L1缓存控制器负责管理L1指令缓存和L1数据缓存。L1缓存控制器的实例化数量等于模拟系统中的内核数量。L2高速缓存控制器负责管理共享的L2高速缓存,并通过目录一致性方案保持片上数据的一致性。目录控制器作为内存控制器/片外主内存的接口,也负责跨多个芯片的一致性/和来自DMA控制器的外部一致性请求。DMA控制器负责满足一致性的DMA请求。
  • 这个协议的主要优化之一是,如果L1缓存请求一个数据块,甚至是读取权限,L2缓存控制器如果发现没有其他核心拥有这个数据块,就会以独占权限返回这个缓存块。这是一种优化,因为它预见到一个缓存块的读取很快就会被同一个核心写入,因此通过这种优化可以节省一个额外的请求。这正是E状态退出的原因(即当一个缓存块可写但尚未写入时)。
  • 该协议支持从私有L1缓存中无声地驱逐干净的缓存块。这意味着没有被写入并且只有可读权限的缓存块可以在不通知二级缓存的情况下从私有的L1缓存中删除缓存块。这种优化有助于减少对二级缓存控制器的回写流量。
Related Files
  • src/mem/protocols
    • MESI_CMP_directory-L1cache.sm: L1 cache controller specification
    • MESI_CMP_directory-L2cache.sm: L2 cache controller specification
    • MESI_CMP_directory-dir.sm: directory controller specification
    • MESI_CMP_directory-dma.sm: dma controller specification
    • MESI_CMP_directory-msg.sm: coherence message type specifications. This defines different field of different type of messages that would be used by the given protocol
    • MESI_CMP_directory.slicc: container file
Controller Description
*L1 cache

controller**

States Invariants and Semantic/Purpose of the state
M 该缓存块只被一个L1缓存保持在独占状态。这个区块没有共享者。该数据可能是系统中唯一有效的拷贝。缓存块的副本是可写的,也是可读的。
E 缓存块被正好只有一个L1缓存持有独占权限。与M状态的区别在于,缓存块是可写(和可读)的,但还没有被写入。
S 该缓存块被1个或多个L1缓存和/或L2缓存保持在共享状态。该区块只能被读取。没有缓存可以拥有该缓存块的独占权限。
I / NP 缓存块无效。
IS 暂时性状态。这意味着已经对缓存区发出了GETS(读)请求,并在等待响应。缓存块既不可读也不可写。
IM 暂时的状态。这意味着GETX(写)请求已经发出,等待响应。缓存区既不可读也不可写。
SM 暂时性状态。这意味着缓存块最初处于S状态,然后发出UPGRADE(Write)请求以获得该块的独占许可,并等待响应。缓存块是可读的。
IS_I 暂时性状态。这意味着在IS状态下,缓存控制器从二级缓存的目录中收到了无效信息。这种情况的发生是由于其他核心对同一缓存块的写入导致的竞赛条件,而给定的核心正试图获得相同的缓存块进行读取。缓存块既不可读也不可写。
M_I 瞬时状态。这个状态表示缓存正在试图从其缓存中替换一个处于M状态的缓存块,并且已经向L2缓存的目录发出回写(PUTX),但是在等待回写确认。
SINK_WB_ACK 瞬时状态。当等待来自L2缓存的目录的回写确认时,L1缓存收到干预(来自其他核心的转发请求),就会达到这种状态。这表明在发出的回写目录和另一个高速缓存的请求之间发生了竞赛。这也表明回写已经失去了竞争(即在它到达L2高速缓存的目录之前,另一个核心的请求已经到达L2)。这个状态对于避免复杂的竞赛条件是非常重要的,如果回写在目录处被悄悄地丢弃,就会发生复杂的竞赛条件。
L2 cache controller

回顾一下,片上目录与L2高速缓存中的相应缓存块共处一地。因此,在L2缓存块中的以下状态编码了关于L2缓存中的缓存块的状态和权限的信息,以及可能存在于一个或多个私有L1缓存中的缓存块的一致性状态。除了相干状态之外,每个缓存块还有两个更重要的字段,有助于做出适当的相干行动。这些字段是Sharers字段,它可以被认为是一个位向量,表明哪些私有L1缓存可能拥有该缓存块。另一个重要的字段是Owner字段,它是私人L1高速缓存的身份,如果该高速缓存块在L1高速缓存中被独占的话。

States Invariants and Semantic/Purpose of the state
NP 缓存块不存在于片上缓存的层次结构中。
SS 缓存块可能存在于多个私有缓存中,且仅为可读模式(即在私有缓存中处于 "S "状态)。与该缓存块相对应的 "Sharers "向量应该给出在其缓存中可能有该缓存块的私人缓存的身份。二级缓存中的缓存块是有效的和可读的。
M 缓存块只存在于L2缓存中,并且有独占权限。L1高速缓存的读/写请求(GETS/GETX)可以直接从L2高速缓存中满足。
MT 缓存块在一个具有独占权限的私有L1缓存中。二级缓存中的数据可能是过时的。拥有该缓存块的L1缓存的身份可以在与该缓存块相关的 "所有者 "字段中找到。任何来自其他内核/私有L1缓存的读/写(GETS/GETX)请求都需要转发给该缓存块的所有者。L2不能自己服务请求。
M_I 这是一个瞬时状态。这种状态表明,缓存正在试图从其缓存中替换缓存块,并且已经向目录控制器(作为主存储器的接口)发出了回写(PUTX/PUTS),但在等待回写确认。该数据既不可读也不可写。
MT_I 它是一个短暂的状态。这个状态表示缓存正试图从其缓存中替换一个MT状态的缓存块。缓存块的当前所有者(私有L1缓存)的无效性已经发出,等待从所有者L1缓存中回写。请注意,这个Invalidation(称为back-invalidation)在确保L1和L2缓存之间保持包容方面是很重要的。这些数据既不可读也不可写。
MCT_I 它是一个瞬时状态。这个状态与MT_I相同,只是已知L2缓存中的数据处于清洁状态。这些数据既不可读也不可写。
I_I 它是一个瞬时状态。L2缓存正试图替换SS状态的缓存块,而L2的缓存块处于清洁状态。无效信息已经被发送到该缓存块的所有潜在共享者(L1缓存)。L2缓存的目录正在等待所有需要的Acknowledgements从L1缓存到达。请注意,这个Invalidation(称为back-invalidation)在确保L1和L2缓存之间保持包容方面起到了重要作用。这些数据既不可读也不可写。
S_I 它是一个瞬时状态。和I_I一样,只是L2缓存中的缓存块的数据是脏的。这意味着与I_I的情况不同,数据需要被发送到主内存。缓存块既不可读也不可写。
ISS 这是一个瞬时的状态。L2从一个私有的L1缓存中收到了一个GETS(读)请求,是为了一个不存在于片上缓存的缓存块。读取请求已经被发送到主内存(目录控制器),并等待内存的响应。只有当请求是针对数据高速缓存块(而不是指令高速缓存块)时,才会达到这个状态。这个状态的目的是,如果发现只有一个L1缓存请求了该缓存块,那么该缓存块将以独占权限返回给请求者(尽管它被请求为阅读权限)。该缓存块既不可读也不可写。
IS 它是一个短暂的状态。该状态类似于ISS,只是如果请求的缓存块是指令缓存块,或者在等待内存的响应时有多个核心请求同一个缓存块,就会达到这个状态而不是ISS。一旦请求的缓存块从主存储器中到达,该缓存块将以只读的权限发送给请求者。缓存块在这个状态下既不能读也不能写。
IM 它是一个瞬时的状态。当L1 GETX(写)请求被L2高速缓存收到时,就会达到这个状态,这个高速缓存块不在片上高速缓存的层次中。对独占模式下的缓存块的请求已经发出到主存储器,但响应尚未到达。缓存块在这个状态下既不能读也不能写。
SS_MB 它是一个瞬时状态。一般来说,任何名字以 "B "结尾的状态(比如这个)也意味着它是一个阻塞的一致性状态。这意味着目录在等待来自私有L1高速缓存的一些响应,直到它收到所需的响应,任何其他的请求都不会被接受(即请求被有效地序列化)。当一个L1缓存请求一个具有独占权限的缓存块(即GETX或UPGRADE),并且缓存块的一致性状态处于SS状态时,就会达到这种特殊状态。这意味着被请求的缓存块在私有L1缓存中可能有可读的副本。因此,在给予请求者独占权限之前,L1缓存中所有的可读副本都需要被无效化。这个状态表明,所需的失效已经被发送到潜在的共享者(L1缓存),并且请求者已经被告知在获得缓存块的独占权限之前所需的失效确认的数量。一旦请求者L1缓存得到所需数量的无效确认,它就会通过UNBLOCK消息通知主任,这使得目录可以脱离这个阻塞的一致性状态,此后它就可以恢复对给定缓存块的其他请求。在这种状态下,缓存块既不可读也不可写。
MT_MB 它是一个短暂的状态,也是一个阻塞状态。当L2缓存的目录向请求者L1缓存发送了一个具有独占权限的缓存块,但还没有收到请求者L1缓存确认收到独占权限的UNBLOCK,就会达到这种状态。在这个状态下,缓存块既不可读也不可写。
MT_IIB 它是一个短暂的状态,也是一个阻塞状态。当收到读取请求(GETS)的请求时,就会达到这个状态,这个缓存块目前在另一个私有的L1缓存中拥有排他性的权限(即目录状态是MT)。在这样的请求中,L2高速缓存的目录将请求转发给当前所有者L1高速缓存并过渡到这个状态。在这个缓存块被解封之前,需要发生两个事件(从而开始受理这个缓存块的进一步请求)。当前所有者缓存块需要向L2缓存发送一个回写,以更新L2缓存的最新值。请求者L1缓存也需要向L2缓存发送UNBLOCK,表明它已经得到了所请求的具有所需一致性权限的缓存块。在这个状态下,高速缓存块在L2高速缓存中既不可读也不可写。
MT_IB 这是一个瞬时状态,也是一个阻塞状态。当在MT_IIB状态下,L2缓存控制器收到来自请求者L1缓存的UNBLOCK,但还没有收到来自该块的前主人L1缓存的回写,就会达到这种状态。在这个状态下,缓存块在L2缓存中既不可读也不可写。
MT_SB 它是一个瞬时的状态,也是一个阻塞的状态。当在MT_IIB状态下,L2缓存控制器从上一个所有者L1缓存中接收到块的回写,而尚未从当前请求者那里接收到缓存块的UNBLOCK,就会达到这个状态。在这个状态下,缓存块在二级缓存中既不可读也不可写。

CHI

CHI ruby协议提供了一个单一的缓存控制器,可以在缓存层次结构的多个层面上重复使用,并配置为MESI和MOESI缓存一致性协议的多个实例建模。这个实现是基于 Arm’s AMBA 5 CHI specification 规范,为大型SoC设计的设计空间探索提供了一个可扩展的框架。

CHI overview and terminology

CHI(Coherent Hub Interface)提供了一个组件架构和事务级的规范,以模拟MESI和MOESI缓存的一致性。CHI定义了三个主要组件,如下图所示:

Image in a image block
  • 请求节点发起交易并向内存发送请求。请求节点可以是一个完全一致的请求节点(RNF),这意味着请求节点在本地缓存数据,并应响应窥探请求。
  • 互联(ICN),它是请求节点的响应者。在协议层面上,互连是一个封装了系统中完全一致的主节点(HNF)的组件。
  • 从属节点(SNF),它与内存控制器连接。

一个HNF是特定地址范围的一致性点(PoC)和序列化点(PoS)。HNF负责向RNF发出任何所需的窥探请求或向SNF发出内存访问请求,以完成交易。HNF还可以封装一个共享的最后一级缓存,并包括一个用于目标窥探的目录。

CHI specification还为非相干请求者(RNI)和非相干地址范围(HNI和SNI)定义了特定类型的节点,例如,属于IO组件的内存范围。在Ruby中,IO访问不经过缓存一致性协议,所以只有CHI的完全一致性节点类型被实现。在本文档中,我们交替使用RN / RNF、HN / HNF和SN/SNF等术语。我们还使用术语上游和下游分别指内存层次结构中的上一级(即朝向cpu)和下一级(即朝向内存)的组件。

Protocol overview

CHI协议的实现主要由两个控制器组成:

  • Memory_Controller (src/mem/ruby/protocol/chi/CHI-mem.sm) 实现了一个CHI从属节点。它接收来自主节点的内存读或写请求,并与gem5的经典内存控制器接口。
  • Cache_Controller (src/mem/ruby/protocol/chi/CHI-cache.sm) 通用高速缓存控制器状态机。

为了允许完全灵活的缓存层次,Cache_Controller可以被配置为在请求节点和主节点内的任何缓存级别(例如L1D,私有L2,共享L3)的模型。此外,它还支持其他Ruby协议中没有的多种功能:

  • 为每个请求类型提供可配置的缓存块分配和取消分配策略。
  • 为传入和传出的请求提供统一的或单独的事务缓冲区。
  • MESI 或 MOESI 操作。
  • 目录和高速缓存标记以及数据阵列停顿。
  • 参数,在请求处理流程的多个步骤中注入延迟。这使我们能够更密切地校准性能。

该实现定义了以下缓存状态:

  • I: line is invalid
  • SC: line is shared and clean
  • UC: line is exclusive/unique and clean
  • SD: line is shared and dirty
  • UD: line exclusive/unique and dirty
  • UD_TUD with timeout.当一个存储条件失败并导致线路从I过渡到UD时,如果失败的次数超过一定的阈值(配置定义),我们就过渡到UD_T。在UD_T中,线路在一定的周期内不能从请求者那里被驱逐(也是配置定义的);之后线路就会转到UD。这对于避免某些情况下的活锁是必要的。

下图给出了控制器被配置为L1高速缓存时的状态转换概况:

Image in a image block

过渡被注释为来自cpu的传入请求(或内部产生的请求,如替换)和下游发送的传出请求。为简单起见,该图省略了不改变状态的请求(例如,缓存点击)和无效的窥探(最终状态总是I)。为了简单起见,它也只显示了MOESI协议中的典型状态转换。在CHI中,最终状态将最终由响应者返回的数据类型决定(例如,请求者可能在响应ReadShared时收到UDUC数据)。

下面的数字显示了中级缓存控制器(例如,私有L2、共享L3、HNF等)的转换。

Image in a image block
Image in a image block

和前面的情况一样,为了简单起见,省略了缓存点击率。除了缓存状态外,还定义了以下目录状态来跟踪上游缓存中存在的行:

  • RU:上游请求者在 UC 或 UD 中有line
  • RSC: 一个或多个上游请求者在 SC 中有line
  • RSD: 一个上游请求者在 SD 中有line;其他可能在 SC
  • RUSCRSC + 当前域剧具有独占访问权限
  • RUSDRSD + 当前域剧具有独占访问权限

当该行同时出现在本地缓存和上游缓存中时,有可能出现以下组合状态:

  • UD_RSCSD_RSCUC_RSCSC_RSC
  • UD_RUUC_RU
  • UD_RSDSD_RSD

RUSCRUSD状态(在上面的数字中省略)是用来跟踪控制器仍有排他性访问权限的行,而不是在它的本地高速缓存中。这在一个非包容性的缓存中是可能的,在这个缓存中,本地块可以被取消分配而不需要对上游副本进行回溯验证。

当缓存控制器是一个HNF(home node)时,状态交易基本上与中级缓存相同,除了这些区别:

  • 发送ReadNoSnp是为了从下游获得数据,因为下游的组件只有SN(从属节点)。
  • 在缓存和目录缺失时,如果启用了DMT(直接内存传输),就会使用。
  • 在缓存缺失和目录命中时,如果启用了DCT(直接缓存传输),就会使用。

关于DCT和DMT交易的更多信息,见 CHI specification中的1.7和2.3.1节。DMT和DCT是CHI的功能,允许请求的数据源直接向原始请求者发送数据。在DMT请求中,SN直接向RN发送数据(而不是先发送至HN,再由HN转发至RN),而在DCT中,HN要求被窥探的RN(窥探者)直接向原始请求者发送线路的副本。在启用DCT的情况下,HN也可以要求窥探者将数据发送给HN和原始请求者,因此HN也可以缓存数据。这取决于配置参数所定义的分配策略。注意,分配策略也会改变缓存状态的转换。为了简单起见,上图说明了一个包容性的缓存。

下面是影响协议行为的缓存控制器的主要配置参数列表(详情和完整的参数列表请参考协议SLICC规范)。

  • downstream_destinations: 定义了向下游发送的请求的目的地,并用于建立缓存层次结构。请参考configs/ruby/CHI.py中的create_system函数,了解如何为每个内核建立一个具有私有L1I、L1D和L2缓存的系统
  • is_HN: 当控制器被用作一个地址范围的主节点和一致性点时设置。对于其他每个缓存级别必须是false。
  • enable_DMT and enable_DCT: 当控制器是一个主节点时,这使得直接的内存传输和直接的高速缓存传输能够满足传入的读取请求。
  • allow_SD: 允许共享dirty状态。这在MOESI和MESI操作之间进行切换。
  • alloc_on_readsharedalloc_on_readunique, and alloc_on_readonce: 是否分配一个缓存块来存储用于响应相应读请求的数据
  • alloc_on_writeback: 是否分配一个缓存块来存储从回写请求中收到的数据
  • dealloc_on_unique and dealloc_on_shared: 如果该行在上游缓存中成为唯一的或共享的,则取消分配本地缓存块
  • dealloc_backinv_unique and dealloc_backinv_shared: 如果一个本地缓存块由于替换而被删除,也会使上游缓存中的任何唯一或共享的行的副本失效
  • number_of_TBEs,number_of_snoop_TBEs, and number_of_repl_TBEs: TBE表中的传入请求、传入窥探和替换的条目数量
  • unify_repl_TBEs: 替换使用与触发它的请求相同的TBE槽。在这种情况下,number_of_repl_TBEs被忽略。

这些参数会影响缓存控制器的性能:

  • read_hit_latency and read_miss_latency: 读取请求分别在本地缓存中命中或未命中的流水线延迟。
  • snoop_latency: 传入监听的管道延迟。
  • write_fe_latency and write_be_latency: 处理写请求的前端和后端管道延迟。前端延时适用于发送确认响应和下一步行动之间。后端是应用于请求者在收到确认和发送写数据之间。
  • allocation_latency: TBE分配和事务初始化之间的延迟。
  • cache: 连接到该控制器的CacheMemory包括一些参数,如大小、关联性、标签和数据延迟,以及库的数量。

Section Protocol implementation gives an overview of the protocol implementation while Section Supported CHI transactions describe the implemented subset of the the AMBA 5 CHI spec. The next sections refer to specific files in the protocol source code and include SLICC snippets of the protocol. Some snippets where slightly simplified compared to the actual SLICC specification.

Protocol implementation 部分给出了协议实现的概述,而支持的Supported CHI transactions描述了AMBA 5 CHI规范的实现子集。接下来的章节提到了协议源代码中的特定文件,包括协议的SLICC片段。一些片段与实际的SLICC规范相比略有简化。

Protocol implementation

下图给出了缓存控制器的实现:

Image in a image block

在Ruby中,高速缓存控制器是通过使用SLICC语言定义一个状态机来实现的。状态机的转换是由到达输入队列的消息触发的。在我们的具体实现中,为每个CHI通道定义了单独的传入和传出消息队列。开始一个新交易的传入请求和窥探消息要经过相同的请求分配过程,我们分配一个交易缓冲条目(TBE),并将请求或窥探移到一个准备启动的交易内部队列。如果交易缓冲区已满,请求将被拒绝,并发送重试消息。

对于从输入/rdy队列中脱队的消息,要执行的操作取决于目标缓存行的状态。如果该行是本地缓存,则该行的数据状态被存储在缓存中,而如果该行存在于任何上游缓存中,则目录状态被存储在目录条目中。对于有未完成请求的行,瞬时状态被保存在TBE中,并在事务完成后复制回高速缓存和/或目录中。下图描述了事务生命周期中的各个阶段以及缓存控制器中主要组件(输入/输出端口、TBETable、缓存、目录和SLICC状态机)之间的相互作用。这些阶段在后面的章节中会有更详细的描述。

Image in a image block
Transaction allocation

下面的代码片断显示了如何处理reqIn端口的传入请求。reqIn端口接收来自CHI的请求通道的传入信息:

in_port(reqInPort, CHIRequestMsg, reqIn) {
  if (reqInPort.isReady(clockEdge())) {
    peek(reqInPort, CHIRequestMsg) {
      if (in_msg.allowRetry) {
        trigger(Event:AllocRequest, in_msg.addr, 
              getCacheEntry(in_msg.addr), getCurrentActiveTBE(in_msg.addr));
      } else {
        trigger(Event:AllocRequestWithCredit, in_msg.addr,
              getCacheEntry(in_msg.addr), getCurrentActiveTBE(in_msg.addr));
      }
    }
  }
}

The allowRetry field indicates messages that can be retried. Requests that cannot be retried are only sent by a requester that previously received credit (see RetryAck and PCrdGrant in the CHI specification). The transition triggered by Event:AllocRequest or Event:AllocRequestWithCredit executes a single action which either reserves space in the TBE table for the request and moves it to the reqRdy queue, or sends a RetryAck message):

allowRetry字段表示可以重试的消息。不能重试的请求只由先前获得信用的请求者发送(见CHI规范中的RetryAckPCrdGrant)。由Event:AllocRequestEvent:AllocRequestWithCredit触发的转换执行一个动作,该动作要么在TBE表中为请求保留空间并将其移到reqRdy队列,要么发送RetryAck消息):

action(AllocateTBE_Request) {
  if (storTBEs.areNSlotsAvailable(1)) {
    // reserve a slot for this request
    storTBEs.incrementReserved();
    // Move request to rdy queue
    peek(reqInPort, CHIRequestMsg) {
      enqueue(reqRdyOutPort, CHIRequestMsg, allocation_latency) {
        out_msg := in_msg;
      }
    }
  } else {
    // we don't have resources to track this request; enqueue a retry
    peek(reqInPort, CHIRequestMsg) {
      enqueue(retryTriggerOutPort, RetryTriggerMsg, 0) {
        out_msg.addr := in_msg.addr;
        out_msg.event := Event:SendRetryAck;
        out_msg.retryDest := in_msg.requestor;
        retryQueue.emplace(in_msg.addr,in_msg.requestor);
      }
    }
  }
  reqInPort.dequeue(clockEdge());
}

Notice we don’t create and send a RetryAck message directly from this action. Instead we create a separate trigger event in the internal retryTrigger queue. This is necessary to prevent resource stalls from halting this action. Section Performance modeling below explains resource stalls in more details.

注意我们没有直接从这个动作中创建和发送RetryAck消息。相反,我们在内部retryTrigger队列中创建了一个单独的触发事件。这是必要的,以防止资源停滞使这个动作停止。下面的 Performance modeling 详细解释了资源滞留。

来自Sequencer对象(当控制器被用作L1缓存时,通常连接到CPU)的传入请求和窥探请求通过seqInsnpIn端口到达,处理方式类似,除了:

  • 它们不支持重试。如果没有可用的TBE,就会产生一个资源停滞,我们在下一个周期再试。
  • snoops从一个单独的TBETable分配TBE,以避免死锁。
Transaction initialization

一旦一个请求被分配了一个TBE并被移到reqRdy队列,就会触发一个事件来启动交易。我们为每个不同的请求类型触发一个不同的事件:

in_port(reqRdyPort, CHIRequestMsg, reqRdy) {
  if (reqRdyPort.isReady(clockEdge())) {
    peek(reqRdyPort, CHIRequestMsg) {
      CacheEntry cache_entry := getCacheEntry(in_msg.addr);
      TBE tbe := getCurrentActiveTBE(in_msg.addr);
      trigger(reqToEvent(in_msg.type), in_msg.addr, cache_entry, tbe);
    }
  }
}

每个请求都需要不同的初始化操作,这取决于该行的初始状态。为了说明这个过程,让我们以一个处于SC_RSC状态的行的ReadShared请求为例(在本地缓存中共享干净,在上游缓存中共享干净):

transition(SC_RSC, ReadShared, BUSY_BLKD) {
  Initiate_Request;
  Initiate_ReadShared_Hit;
  Profile_Hit;
  Pop_ReqRdyQueue;
  ProcessNextState;
}
  • Initiate_Request初始化分配的TBE。这个动作将本地缓存和目录中分配的任何状态和数据复制到TBE中。
  • Initiate_ReadShared_Hit设置了一套需要执行的动作来完成这个特定的请求(见下文)。.
  • Profile_Hit 更新缓存统计信息。
  • Pop_ReqRdyQueue将请求信息从reqRdy队列中移除。
  • ProcessNextState执行Initiate_ReadShared_Hit定义的下一个动作。

Initiate_ReadShared_Hit 定义如下:

action(Initiate_ReadShared_Hit) {
  tbe.actions.push(Event:TagArrayRead);
  tbe.actions.push(Event:ReadHitPipe);
  tbe.actions.push(Event:DataArrayRead);
  tbe.actions.push(Event:SendCompData);
  tbe.actions.push(Event:WaitCompAck);
  tbe.actions.pushNB(Event:TagArrayWrite);
}

tbe.actions存储了为了完成一个动作而需要触发的事件列表。在这种特殊情况下,TagArrayReadReadHitPipeDataArrayRead引入了延迟,以模拟缓存控制器管道延迟和读取缓存/目录标签阵列和缓存数据阵列(见Performance modeling)。 SendCompData设置并发送ReadShared请求的数据响应,WaitCompAck设置TBE以期待请求者的完成确认。最后,TagArrayWrite引入了更新目录状态以跟踪新共享者的延迟。

Transaction execution

After initialization, the line will transition to the BUSY_BLKD state as show in transition(SC_RSC, ReadShared, BUSY_BLKD)BUSY_BLKD is a transient state indicating the line has now an outstanding transaction. In this state, the transaction is driven either by incoming response messages in the rspIn and datIn ports or trigger events defined in tbe.actions.

The ProcessNextState action is responsible for checking tbe.actions and enqueuing trigger event messages into actionTriggers at the end of all transitions to the BUSY_BLKD state. ProcessNextState first checks for pending response messages. If there are no pending messages, it enqueues a message to actionTriggers in order to trigger the the event at the head of tbe.actions. If there are pending responses, then ProcessNextState does nothing as the transaction will proceed once all expected responses are received.

Pending responses are tracked by the expected_req_resp and expected_snp_resp fields in the TBE. For instance, the ExpectCompAck action, executed from the transition triggered by WaitCompAck, is defined as follows:

初始化后,线路将过渡到BUSY_BLKD状态,如transition(SC_RSC, ReadShared, BUSY_BLKD)中所示。 BUSY_BLKD是一个短暂的状态,表示该线路现在有一个未完成的交易。在这种状态下,事务由rspIndatIn端口中传入的响应消息或tbe.actions中定义的触发事件驱动。
ProcessNextState动作负责检查tbe.actions,并在所有到BUSY_BLKD状态的转换结束后将触发事件消息排入actionTriggers。 ProcessNextState首先检查是否有未决的响应消息。如果没有悬而未决的消息,它就向actionTriggers排队发送消息,以便触发tbe.actions头部的事件。如果有悬而未决的响应,那么ProcessNextState不做任何事情,因为一旦收到所有预期的响应,事务就会继续进行。
待定响应由TBE中的expected_req_respexpected_snp_resp字段来跟踪。例如,ExpectCompAck动作由WaitCompAck触发的转换执行,定义如下。

action(ExpectCompAck) {
  tbe.expected_req_resp.addExpectedRespType(CHIResponseType:CompAck);
  tbe.expected_req_resp.addExpectedCount(1);
}

这导致事务等待,直到收到CompAck响应。

当事务有待处理的响应时,可以允许一些动作执行。这种动作使用tbe.actions.pushNB(即推送/非阻塞)来排队。在上面的例子中,tbe.actions.pushNB(Event:TagArrayWrite)模拟了在事务等待CompAck响应时正在执行的一个标签写入。

Transaction finalization

当它没有更多的待定响应,并且tbe.actions为空时,事务就结束了。 ProcessNextState检查这个条件,并将一个 "finalizer "触发消息排入actionTriggers。在处理这个事件时,当前的缓存线状态和共享/所有权信息决定了该线的最终稳定状态。如果有必要,数据和状态信息在缓存和目录中被更新,并且TBE被去分配。

Hazard handling

每个控制器只允许每条高速缓存线有一个活动事务。如果一个新的请求或窥探在缓存线处于瞬时状态时到达,这将产生CHI标准中定义的危险。我们处理危险的方法如下:

Request hazards: 如前所述,分配一个TBE,但新的事务初始化被推迟,直到当前事务结束,线路恢复到稳定状态。这是通过将请求信息从reqRdy移到一个单独的滞留缓冲区来实现的。当当前事务结束时,所有停滞的消息都会被加回到reqRdy中,并按照原来的到达顺序进行处理。

Snoop hazards: CHI规范不允许snoops被现有的请求阻滞。如果一个事务正在等待下游发送的请求的响应(例如,我们发送了一个ReadShared,正在等待数据响应),我们必须接受并处理snoop。只有在请求已经被响应者接受并且保证完成的情况下,窥探才会被停滞(例如,一个有待处理数据的ReadShared,但已经被RespSepData响应所吸纳)。为了区分这些情况,我们使用BUSY_INTR瞬时状态。

BUSY_INTR表示事务可以被窥探打断。当窥探到达处于这种状态的线路时,如前所述分配一个窥探TBE,其状态根据当前活动TBE初始化。窥探TBE然后成为当前活动的TBE。任何由snoop引起的缓存状态和共享/所有权的变化都会在deallocate snoop之前复制回原TBE。当snoop到达一个处于BUSY_BLKD状态的行时,我们会拖延snoop,直到当前事务结束或过渡到BUSY_INTR

Performance modeling

如前所述,当一个事务被初始化时,缓存线的状态是立即知道的,并且可以在没有任何延迟的情况下读取和写入缓存线。这使得协议的功能方面更容易实现。为了建立时间模型,我们使用显式动作来给事务引入延迟。例如,在ReadShared代码片断中:

action(Initiate_ReadShared_Hit) {
  tbe.actions.push(Event:TagArrayRead);
  tbe.actions.push(Event:ReadHitPipe);
  tbe.actions.push(Event:DataArrayRead);
  tbe.actions.push(Event:SendCompData);
  tbe.actions.push(Event:WaitCompAck);
  tbe.actions.pushNB(Event:TagArrayWrite);
}

TagArrayReadReadHitPipeDataArrayReadTagArrayWrite没有任何功能意义。它们的存在是为了引入真正的缓存控制器管道中存在的延迟,在这种情况下:标签读取延迟,命中管道延迟,数据阵列读取延迟,以及标签更新延迟。这些动作引入的延迟是由配置参数定义的。

除了明确添加的延迟外。SLICC有资源滞后的概念来模拟资源争夺。给定一组在过渡期间执行的动作,SLICC编译器自动生成代码,检查这些动作所需的所有资源是否可用。如果有任何资源不可用,就会产生一个资源滞留,过渡就不会被执行。导致资源停滞的消息会保留在输入队列中,协议会在下一个周期尝试再次触发转换。

SLICC编译器以不同方式检测资源。

  1. 隐含地说。输出端口就是这种情况。如果一个动作排队等待新的消息,输出端口的可用性会被自动检查。
  2. check_allocate 语句添加到操作中。
  3. 使用资源类型注释转换。

我们使用 (2) 来检查 TBE 的可用性。请参阅下面的代码段:

action(AllocateTBE_Snoop) {
  // No retry for snoop requests; just create resource stall
  check_allocate(storSnpTBEs);
  ...
}

这预示着SLICC编译器在执行包括AllocateTBE_Snoop动作的任何转换之前,要检查storSnpTBEs结构是否有一个TBE槽可用。

下面的片段体现了(3)。

transition({BUSY_INTR,BUSY_BLKD}, DataArrayWrite) {DataArrayWrite} {
  ...
}

DataArrayWrite注解向SLICC编译器发出信号,以检查DataArrayWrite资源类型的可用性。 这些注解中使用的资源请求类型必须由协议明确定义,以及如何检查它们。在我们的协议中,我们定义了以下类型来检查缓存标记和数据阵列中的银行的可用性:

enumeration(RequestType) {
  TagArrayRead;
  TagArrayWrite;
  DataArrayRead;
  DataArrayWrite;
}

void recordRequestType(RequestType request_type, Addr addr) {
  if (request_type == RequestType:DataArrayRead) {
    cache.recordRequestType(CacheRequestType:DataArrayRead, addr);
  }
  ...
}

bool checkResourceAvailable(RequestType request_type, Addr addr) {
  if (request_type == RequestType:DataArrayRead) {
    return cache.checkResourceAvailable(CacheResourceType:DataArray, addr);
  }
  ...
}

当我们在事务上使用注解时,SLICC编译器需要实现checkResourceAvailablerecordRequestType

Cache block allocation and replacement modeling

考虑一下下面这个ReadShared miss的事务初始化代码:

action(Initiate_ReadShared_Miss) {
  tbe.actions.push(Event:ReadMissPipe);
  tbe.actions.push(Event:TagArrayRead);
  tbe.actions.push(Event:SendReadShared);
  tbe.actions.push(Event:SendCompData);
  tbe.actions.push(Event:WaitCompAck);
  tbe.actions.push(Event:CheckCacheFill);
  tbe.actions.push(Event:TagArrayWrite);
}

所有由于窥探或下游发送的请求而修改缓存行或收到的缓存行数据的事务,都使用CheckCacheFill动作触发事件。这个事件会触发一个过渡,执行以下动作:

  • 检查我们是否需要在本地缓存中存储当前的缓存行数据。
  • 检查我们是否已经为该行分配了一个缓存块。如果没有,则尝试分配一个块。如果没有区块,则选择一个受害者区块进行替换。
  • 对缓存填充的延迟建模。

当执行替换时,一个新的事务被初始化,以跟踪任何下游发送的WriteBack或Evict请求和/或窥探backinvalidation(如果缓存控制器被配置为执行包容性)。根据配置参数,替换的TBE使用专用TBETable的资源或重用触发替换的TBE的相同资源。在这两种情况下,触发替换的事务完成后不需要等待替换过程。

注意CheckCacheFill并没有实际写入数据到缓存块。如果只是确保在需要时分配一个缓存块,触发替换,并模拟缓存填充延迟。如前所述,TBE数据在事务最终完成过程中需要时被复制到缓存中。

Supported CHI transactions

所有的交易都按照 AMBA5 CHI Issue D specification中的描述来实现。接下来的章节将更详细地解释公共文件中没有固定的具体实施选择。

Supported requests

支持以下传入请求:

  • ReadShared
  • ReadNotSharedDirty
  • ReadUnique
  • CleanUnique
  • ReadOnce
  • WriteUniquePtl and WriteUniqueFull

当收到任何请求时,clusivity配置参数在事务初始化过程中被评估,doCacheFilldataToBeInvalid标志被设置在为请求分配的事务缓冲区条目中。 doCacheFill表示我们应该在本地缓存中保留该行的任何有效副本;dataToBeInvalid表示我们必须在完成事务时使本地副本失效。

当接收到ReadSharedReadUnique时,如果数据以所需的状态存在于本地缓存中(例如ReadUniqueUCUD),则会向请求者发送一个CompData响应。响应类型取决于dataToBeInvalid的值。

  • If dataToBeInvalid==true
    • The unique and/or dirty state is always propagated
    • For a ReadNotSharedDirtyCompData_SC is always sent if local state is SD and the line is written-back using WriteCleanFull
  • Else:
    • In response to a ReadUnique: propagate dirty state, i.e., CompData_UD or CompData_UC.
    • In response to a ReadShared or ReadNotSharedDirty: send CompData_SC. If fwd_unique_on_readshared configuration parameter is set, the ReadShared is handled as a ReadUnique if the line doesn’t have other sharers.

When receiving a ReadOnceCompData_I is always sent if the data is present at the local cache. For WriteUniquePtl handling see below.

If there is a cache miss, multiple actions may be performed depending on whether or not doCacheFill and dataToBeInvalid==false; and DCT or DMT is enabled:

  • ReadShared / ReadNotSharedDirty:
    • If dir state is RSD or RU:
      • If DCT disabled: send SnpShared to owner; cache the line locally (if doCacheFill) and send response to requester.
      • If DCT enabled: send SnpSharedFwd to owner; if doCacheFill==true, the retToSrc field is set so the line can be cached locally.
    • If dir state is RSC:
      • If DCT disabled: send SnpOnce to one of the sharers; cache the line locally (if doCacheFill) and send response to requester.
      • If DCT enabled: send SnpSharedFwd to one of the sharers; if doCacheFill==true, the retToSrc field is set so the line can be cached locally.
    • Otherwise: issue a ReadShared / ReadNotSharedDirty or ReadNoSnp (if HNF). In the HNF configuration, ReadNoSnp is issued with DMT if DMT is enabled.
    • For ReadNotSharedDirtySnpNotSharedDirty and SnpNotSharedDirtyFwd is sent instead.
  • ReadUnique:
    • If dir state is RU,RUSD,RUSC:
      • If DCT disabled or clusivity is inclusive: send SnpUnique to owner; cache the line locally (if doCacheFill ) and sent response to requester.
      • If DCT enabled and clusivity is exclusive: send SnpUniqueFwd to owner.
    • If dir state is RSC/RSD:
      • Send SnpUnique with retToSrc=true to invalidate sharers and obtain dirty line (in case of RSD)
      • If not HNF: send CleanUnique downstream to obtain unique permissions.
    • Otherwise: issue a ReadUnique or ReadNoSnp (if HNF). In the HNF configuration, ReadNoSnp is issued with DMT if DMT is enabled.
    • For RUSC amd RSC, if multiple sharers, only one sharer is selected as target of the above snoops. The other sharers are invalidated using SnpUnique with retToSrc=false.
  • ReadOnce:
    • If dir entry exists:
      • If DCT disabled: send SnpOnce to one of the sharers; send received data response to requester.
      • If DCT enabled: send SnpOnceFwd to one of the sharers.
    • Otherwise: issue a ReadOnce or ReadNoSnp (if HNF). In the HNF configuration, ReadNoSnp is issued with DMT if DMT is enabled.
  • CleanUnique:
    • Send SnpCleanInvalid to all sharers/owner except original requestor.
    • If not HNF: send CleanUnique downstream to obtain unique permissions.
    • If has dirty line, requestor has clean line, and doCacheFill==false: writeback the line with WriteCleanFull.
  • WriteUniquePtl/WriteUniqueFull:
    • If data present in local cache on UC or UD states:
      • Issue SnpCleanInvalid if there are any sharers.
      • Perform the write in the local cache.
    • If no UC/UD data locally:
      • If HNF:
        • Issue SnpCleanInvalid if there are any sharers.
        • Merge any received snoop response data with the WriteUnique data.
        • If has a full line and doCacheFill set, cache the line locally, otherwise writeback to memory (WriteNoSnp or WriteNoSnpPtl).
      • If no HNF:
        • Forwards the WriteUniquePtl and any received data to the downstream cache.
        • Incoming snoops will cause any locally cached data to become invalid while handling the request.
Supported snoops

缓存控制器发出并接受以下侦听:

  • SnpShared and SnpSharedFwd
  • SnpNotSharedDirty and SnpNotSharedDirtyFwd
  • SnpUnique and SnpUniqueFwd
  • SnpCleanInvalid
  • SnpOnce and SnpOnceFwd

窥探响应是根据规范中定义的线路的当前状态生成的。数据与窥探响应一起返回,取决于数据状态和窥探者设置的retToSrc的值。如果retToSrc被设置,窥探响应总是包括数据。

  • SnpShared / SnpNotSharedDirty:
    • Snoopee always returns data is the line is dirty, unique or retToSrc.
    • retToSrc is set if the snooper needs to cache the line.
    • Final snoopee state always shared clean.
  • SnpUnique:
    • Snoopee always returns data is the line is dirty, unique or retToSrc.
    • retToSrc is set if the snooper needs to cache the line.
    • Final snoopee state always invalid.
  • SnpCleanInvalid:
    • Same as SnpUnique, except data is not returned if line is unique and clean.
  • SnpSharedFwd:
    • retToSrc is set if the snooper needs to cache the line.
    • Line forwarded as dirty if dirty
    • Final snoopee state always shared clean
  • SnpNotSharedDirtyFwd:
    • retToSrc is set if the snooper needs to cache the line.
    • Always returns data if line was dirty at the snoopee; line always forwarded as clean.
    • Final snoopee state always shared clean.
  • SnpUniqueFwd:
    • Same as SnpUnique, except data is never returned to the snooper (as defined by the spec)
  • SnpOnce:
    • Always generated with retToSrc=true and snoopee always returns data.
    • Accepted in any state (except invalid). Final snoopee state does not change.
  • SnpOnceFwd:
    • Same as SnpOnce, except data is never returned to the snooper.

如果snoopee在任何状态下都有分享者,则向所有分享者的上游发送相同的请求。对于SnpSharedFwd/SnpNotSharedDirtyFwd和SnpUniqueFwd,分别发送一个SnpShared/SnpNotSharedFwd或SnpUnique。对于一个收到的SnpOnce,只有在本地不存在该线路的情况下,才会向上游发送一个SnpOnce。在这个特定的实现中,上游缓存总是有一个拥有该行的目录条目。 Snoops不会被发送到没有该行的缓冲区。

Writeback and evictions

当一个缓存行由于容量原因需要被驱逐时,控制器内部会触发回写(目前不支持缓存维护操作)。关于替换的更多信息,请看 Cache block allocation and replacement modeling。这些内部事件的产生取决于控制器的配置参数:

  • GlobalEviction: evict a line from the current and all upstream caches. This applies if dealloc_backinv_unique or dealloc_backinv_shared parameters are set.
  • LocalEviction: evict a line without backinvaliding upstream caches.

首先,我们删除本地缓存块(所以导致驱逐的请求可以分配一个新块并完成)。 对于GlobalEviction,一个SnpCleanInvalid被发送到所有上游缓存。一旦收到所有的snoops响应(可能带有脏数据),就会进行LocalEviction。LocalEviction是通过发出适当的请求来完成的,如下所示。

  • WriteBackFull, if the the line is dirty
  • WriteEvictFull, if the line is unique and clean
  • WriteCleanFull, if the the line is dirty, but there are clean sharers
  • Evict, if the line is shared and clean

对于HNF配置,行为略有改变。 WriteNoSnp到SNF,而不是WriteBackFull,如果线路是干净的,就不发出请求。
WriteBack*Evict请求在下游缓存中的处理方式如下。

  • WriteBackFull / WriteEvictFull / WriteCleanFull:
    • If alloc_on_writeback, a cache block may need to be allocated. If there are no free blocks, a LocalEviction is triggered for a cache line in the target cache set. The victim line is selected based on the replacement policy implemented by object pointed by the cache parameter (which can be configured separately).
    • Send a CompDBIDResp to the requester.
    • Once data is received, update local cache and remove requestor from directory (if WriteBackFull / WriteEvictFull).
  • Evict:
    • Remove requestor from directory and reply with Comp\_I.
Hazards

对当前有未完成交易的行的请求总是被搁置,直到交易完成。在有一个未完成的请求时收到的窥探将按照规范中的要求进行处理:

  • For an outstanding CleanUnique:
    • Snoop response is sent immediately and the current line state is changed accordingly.
    • Notice we don’t model the UCE and UDP states from the CHI spec. If the line is invalidated while the requester waits for a CleanUnique response, it immediately follows up with a ReadUnique.
  • For outstanding WriteBackFull/WriteEvictFull/WriteCleanFull that have not yet been acked with a CompDBIDResp; or Evict before Comp_I is received:
    • Snoop response is sent immediately and the current line state is changed accordingly.
    • The state of the line that will be written back will the state after the snoop.
  • If a snoop is received while the current transaction is waiting for snoop responses from upstream caches, the incoming snoop is stalled until all pending responses from upstream are received and any follow-up request is sent. This can happen in these scenarios:
    • During a global replacement
    • An accepted ReadUnique that required snooping upstream caches

当有一个未完成的交易时,可能会收到多个窥探。在这个特定的实现中,一个SnpSharedSnpSharedFwd之后可能是一个SnpUniqueSnpCleanInvalid。然而,不可能有来自下游缓存的并发的窥视。

传入的请求和snoops都需要分配一个TBE。为了防止交易缓冲区满时出现死锁,一个单独的缓冲区被用来分配snoop TBEs。窥探不允许重试,所以如果窥探TBE表满了,snpIn端口中的消息就会停滞,可能会导致互连中窥探通道的严重拥堵。

Other implementations notes
  • If an HNF uses DMT, it will send ReadNoSnpSep instead of ReadNoSnp if the enable_DMT_early_dealloc configuration parameter is set. This allow the HNF to deallocate the TBE earlier.
  • Order bit field is not implemented, thus ReadReceipt responses are never used except for ReadNoSnpSep. Request ordering, when required, is enforced by Ruby by serializing requests at the requester. At the cache controller, requests to the same line are handled in the order of arrival. Requests to different lines can be handled in any order, however they are typically handled in order of arrival given that there are resources available.
  • Exclusive accesses and atomic requests are not implemented. Ruby has its own global monitor in the Sequencer to manage exclusive load and stores. Atomic operations also handled by Ruby and they only require a ReadUnique at the protocol level.
  • CompAck response is always sent when stated as optional in the spec. Requesters always wait for CompAck (if required or optional) before finalizing the transaction and deallocating resources.
  • Separate Comp and DBIDresp used only for WriteUnique requests. DBIDresp is sent after receiving all snoop responses; Comp is sent after DBIDresp and accounting for the front-end write latency (write_fe_latency).
  • Memory attribute fields are not implemented.
  • DoNotGoToSD field is not implemented.
  • CBusy is not implemented.
  • WriteDataCancel responses are never used.
  • Error handling is not implemented.
  • Cache stashing is not implemented.
  • Atomic transactions are not implemented.
  • DMV transactions are not implemented.
  • Any request not listed in the protocol table below is not supported in this implementation.
Protocol table

Click here

Replacement Policies

Gem5 has multiple implemented replacement policies. Each one uses its specific replacement data to determine a replacement victim on evictions.

All of the replacement policies prioritize victimizing invalid blocks.

A replacement policy consists of a reset(), touch(), invalidate() and getVictim() methods. Each of which handles the replacement data differently.

  • reset() is used to initialize a replacement data (i.e., validate). It should be called only on entry insertion, and must not be called again until invalidation. The first touch to an entry must always be a reset().
  • touch() is used on accesses to the replacement data, and as such should be called on entry accesses. It updates the replacement data.
  • invalidate() is called whenever an entry is invalidated, possibly due to coherence handling. It makes the entry as likely to be evicted as possible on the next victim search. An entry does not need to be invalidated before a reset() is done. When the simulation starts all entries are invalid.
  • getVictim() is called when there is a miss, and an eviction must be done. It searches among all replacement candidates for an entry with the worst replacement data, generally prioritizing the eviction of invalid entries.

We briefly describe the replacement policies implemented in Gem5. If further information is required, the Cache Replacement Policies Wikipedia page, or the respective papers can be studied.

Random

The simplest replacement policy; it does not need replacement data, as it randomly selects a victim among the candidates.

Least Recently Used (LRU)

Its replacement data consists of a last touch timestamp, and the victim is chosen based on it: the oldest it is, the more likely its respective entry is to be victimized.

Tree Pseudo Least Recently Used (TreePLRU)

A variation of the LRU that uses a binary tree to keep track of the recency of use of the entries through 1-bit pointers.

Bimodal Insertion Policy (BIP)

The Bimodal Insertion Policy is similar to the LRU, however, blocks have a probability of being inserted as the MRU, according to a bimodal throttle parameter (btp). The highest btp is, the highest is the likelihood of a new block being inserted as MRU.

LRU Insertion Policy (LIP)

The LRU Insertion Policy consists of a LRU replacement policy that instead of inserting blocks with the most recent last touch timestamp, it inserts them as the LRU entry. On subsequent touches to the block, its timestamp is updated to be the MRU, as in LRU. It can also be seen as a BIP where the likelihood of inserting a new block as the most recently used is 0%.

Most Recently Used (MRU)

The Most Recently Used policy chooses replacement victims by their recency, however, as opposed to LRU, the newest the entry is, the more likely it is to be victimized.

Least Frequently Used (LFU)

The victim is chosen using the reference frequency. The least referenced entry is chosen to be evicted, regardless of the amount of times it has been touched, or how long has passed since its last touch.

First-In, First-Out (FIFO)

The victim is chosen using the insertion timestamp. If no invalid entries exist, the oldest one is victimized, regardless of the amount of times it has been touched.

Second-Chance

The Second-Chance replacement policy is similar to FIFO, however entries are given a second chance before being victimized. If an entry would have been the next to be victimized, but its second chance bit is set, this bit is cleared, and the entry is re-inserted at the end of the FIFO. Following a miss, an entry is inserted with its second chance bit cleared.

Not Recently Used (NRU)

Not Recently Used (NRU) is an approximation of LRU that uses a single bit to determine if a block is going to be re-referenced in the near or distant future. If the bit is 1, it is likely to not be referenced soon, so it is chosen as the replacement victim. When a block is victimized, all its co-replacement candidates have their re-reference bit incremented.

Re-Reference Interval Prediction (RRIP)

Re-Reference Interval Prediction (RRIP) is an extension of NRU that uses a re-reference prediction value to determine if blocks are going to be re-used in the near future or not. The higher the value of the RRPV, the more distant the block is from its next access. From the original paper, this implementation of RRIP is also called Static RRIP (SRRIP), as it always inserts blocks with the same RRPV.

Bimodal Re-Reference Interval Prediction (BRRIP)

Bimodal Re-Reference Interval Prediction (BRRIP) is an extension of RRIP that has a probability of not inserting blocks as the LRU, as in the Bimodal Insertion Policy. This probability is controlled by the bimodal throtle parameter (btp).