Murphi入门(八)—— German协议解析
本文讲述但不仅限于:
- German协议流程分析
- Murphi执行German协议注意要点
基本概念
German缓存一致性协议适用于共享内存的并发多处理器系统。
简单来说,整个系统抽象成三个部分:
- cache:缓存器——分别为每个程序存储一个临时数据
- home:通道管理器——管理cache读、写数据的并发发生
- Memory:内存——存储数据,可提供读、写操作
根据数据流向,可以分为以下两部分:
- 从内存流向cache:cache向home提出申请,要求读入mem中的数据
- 从cache流向内存:cache对内存中存储的数据进行重写
作为管理者,home执行的职责主要有三个关键点:
- 读可以共享
- 写不可共享
- 读写不可同时进行
而管理者的管理方法类似于三次握手,发送完消息之后,还需要等待对方回复,再执行下一步操作。当然,管理者还有两个个小本本,分别记录共享中的cache和无效中的cache,方便管理。
home还会记得当下是哪个cache在提出申请,以及申请的命令是什么,这就是CurPtr和CurCmd。
cache有三种状态:I(invalid)无效、S(share)读共享、E(Exclusive)写互斥。三种状态不能同时共存,且多cache的状态可以有多I,多S,但最多只能有一个E。
管理者与cache直接交流的方式是通过3个通道,每个通道可传递的消息不同:
- chanel1: cache向home发送——sendReqS, sendReqEI, sendReqES
- chanel2: home向cache发送——InvS, InvE, GntS, GntE
- chanel3: cache向home发送——InvAck
还一个互斥变量,类似于锁机制,叫做ExGntd,是布尔变量,表示是否互斥。只有在写操作正在进行时,才会是true。这样保证一次只会有一个cache对内存进行写操作。
图解
如果看不懂了上述概念和流程,那么用一个直观的流程图来表示German协议的执行过程(例子为3cache模型)
规则解读
初始化
1 | ruleset d: DATA do startstate "Init" |
所有cache:
- 状态为I
- 数据为1或2
所有cache不共享,也不需要进行无效操作,所以ShrSet和InvSet都是false
没有cache在申请操作,因此CurCmd为空,CurPtr未定义
没有cache在进行写操作,因此ExGntd为false
执行结果:(执行方法见下一section)
1 | Startstate Init, d:1 fired. |
通道1相关规则
- SendReqS
- SendReqEI
- SendReqES
- RecvReq
解读
cache向通道申请的内容包括:申请读、申请写。
I、S、E三种操作,互相转换的关系如图:
代码
1 | ruleset j: NODE do |
通道2相关规则
- SendInvE
- SendInvS
- SendGntS
- SendGntE
- RecvGntS
- RecvGntE
解读
通道2是home向cache发送命令的通道,主要用于
- 使cache无效,发生情况有:
- 某个cache想申请写内存,这时候其他cache必须无效
- 某个cache想读内存中的数据,但这时候有其他cache正在写,这时候就要让那个正在写的cache带着现有的数据返回内存,并不再操作
- 通过请求,告知申请的cache可以执行操作,发生的情况有:
- 某个cache申请读操作,且现在没有cache正在写
- 某个cache申请读操作,且现在没有在写和读的cache
- cache收到home的确认,进行原来想要进行的操作,发生的情况有:
- cache得到允许,可以读数据,且数据随通道带回来了
- cache得到允许,可以写数据,且数据随通道带回来了
代码
1 | ruleset i: NODE do |
通道3相关规则
- SendInvAck
- RecvInvAck
解读
命令从cache流向home,用于确认之前收到的home传来的使无效命令。home因为有cache想申请写操作,或想申请读操作但有cache在写,所以要先使在读或在写的cache先无效。
- SendInvAck:cache收到无效命令,如果正在读,就退出读状态;如果正在写,就退出写状态,且将数据由通道带回。
- RecvInvAck:home收到cache从通道3带回来的回应,知道该cache已经无效了,如果此时没有其他的非无效cache,则可以继续;如果刚刚返回的cache是正在写的cache,则它返回时还会带着修改过的数据,则还需要将互斥锁解锁,再将数据写回内存。
代码
1 | ruleset i: NODE do |
Murphi执行German
常数
已有murphi程序,将cache数量设置成3,数据数量设置成2,即:
1 | const |
编译
将terminal的路径cd到german的murphi程序所在的地方,执行:
1 | ../../src/mu n_g2k.m |
其中mu为murphi编译器,将murphi文件编译成cpp
再将cpp编译成.o:
1 | g++ -ggbd -o n_g2k.cpp n_g2k.m -I ../../include -lm |
之后就可以执行了
1 | ./n_g2k.o |
注意
但是因为murphi设置的默认开辟的状态集(状态hash表)大小有限,所以此时会提示“too many active states”,提示我们无法遍历完所有的状态,并且告诉我们,总共需要322583k的大小。
也就是说,要想遍历完所有的状态,则需要加参数 -k或 -m。
所以执行:
1 | ./n_g2k.o -k322583 |
显示结果
但是这样执行完之后没有感觉,只有在执行了68.44s之后得出“no error found”的结果。如果想显示结果,可以加-ta 或 -tf 或-pr 或-p。
- -ta: 输出所有状态至少一次
- -tf: 所有状态只输出一次,且只输出与前一状态的改变量
- -pr:会显示每一条规则执行几次,但无关顺序
- -p: 从初始状态到最后,每一步都会详细展开,但是因为过于详细,所以占用空间太大
当设置了某个安全性质,想知道怎样最快到达该路径时,记得加参数-vdfs,这样就是深度优先,能够最快到达,否则默认的是广度优先。
最后的输出语句就成了:
1 | ./n_g2k.o -k322583 -ta -vdfs |
结果输出
结果输出,有两种方式:
- murphi自带:
-d ./
- 终端自带:
>./trace.txt
但要注意,murphi自带的-d,是需要在编译的时候,就把它解析成哈希表模式,也就是在编译时:
1 | ../../src/mu -c n_g2k.m |
加上参数-c
,这样在后面.o文件执行的时候才不会报错。不过经试验,-d
命令在Mac上并不使用得了,可能是murphi文件本身的问题,因为并没有报错。
如果在ubuntu下,会自动存储到./路径下的(该路径可修改,根据自己的需要自行修改)trace文件夹中。
终端自带的就没什么好说了,就是这个命令而已,路径也可以修改。
安全性质
可以通过加安全性质的方式,知道路径怎么运行,也就能更方便地知道规则是怎么执行的,就不用人工自己去判断了。
增添的方法是,写invariant。如:想知道什么时候cache会从状态I变到状态S,则设置安全性质为:
1 | rulleset i:NODE do |
那么,只要有一个Cache的状态变成S,就会break,并根据参数-ta,-vdfs,会进行深度优先的搜索,输出最短的到达该状态的一条路径。
多设置一些安全性质,就能比较清晰的知道该协议是怎样运行的了。
Tips
当然,如果当你没有对协议这么清楚的认识,只想先看看协议自己跑一下,可以通过调整参数-k的大小,来先看一些步骤。具体方法是:
1 | ./n_g2k.o -k100 -ta -vdfs |
-k100是表示只开辟100k大小的状态表,这样不会跑到太多。-ta或者-tf都行,这样不会太繁琐,可以只看到改变的量,以及不会看到重复的状态。-vdfs是因为比较方便看运算的路径,毕竟人的思维比较习惯深度地思考,而广度的思考不利于理解协议。
总结
对于一个新拿到手的协议,可以先运行上一个section的Tips,跑着看看。等有了更深刻的认识之后,可以用安全性质来做测试。最后就是完全理解协议。当然,能有可视化的方法,就更好理解了。
目前正在思考有没有方法可以直接可视化。python里提供了一些画图工具,如matplot或者z3都可以画图。问题就是如何将murphi的一条条规则转换成图。如果针对全局变量进行迁移,最多可以生成类似以下的图。
但是如果希望生成这样的图
可能得先给出这样的图,然后根据-ta的输出,每次更改改变了的数据,最后将生成的一张张图进行视频合成输出,方可实现。
还是需要对python之类的工具有更深刻的理解和实践才能再有更多的想法呀。深深感到自己的心有余而力不足,加油努力吧~