看懂gem5对MSI的建模(二)
好了,开始看代码,先看MSI-msg.sm:
`enumeration(CoherenceRequestType, desc="Types of request messages") {
GetS, desc="Request from cache for a block with read permission";
GetM, desc="Request from cache for a block with write permission";
PutS, desc="Sent to directory when evicting a block in S (clean WB)";
PutM, desc="Sent to directory when evicting a block in M";
// "Requests" from the directory to the caches on the fwd network
Inv, desc="Probe the cache and invalidate any matching blocks";
PutAck, desc="The put request has been processed.";
}
enumeration(CoherenceResponseType, desc="Types of response messages") {
Data, desc="Contains the most up-to-date data";
InvAck, desc="Message from another cache that they have inv. the blk";
}这是各个通道发送请求和应答的所有类型,除了类型外还应该有以下信息:structure(RequestMsg, desc="Used for Cache->Dir and Fwd messages",
interface="Message") {
// NOTE: You can't name addr "Addr" because it would conflict with the
// Addr type.
Addr addr, desc="Physical address for this request";
CoherenceRequestType Type, desc="Type of request";
MachineID Requestor, desc="Node who initiated the request";
NetDest Destination, desc="Multicast destination mask";
DataBlock DataBlk, desc="data for the cache line";
// NOTE: You must use MessageSize as the name of this variable, and it's
// required that you have a MessageSize for each type of message. You will
// the the error "panic: MessageSizeType() called on wrong message!"
MessageSizeType MessageSize, desc="size category of the message";
// This must be overridden here to support functional accesses
bool functionalRead(Packet *pkt) {
// Requests should never have the only copy of the most up-to-date data
return false;
}
bool functionalWrite(Packet *pkt) {
// No check on message type required since the protocol should read
// data block from only those messages that contain valid data
return testAndWrite(addr, DataBlk, pkt);
}
}
structure(ResponseMsg, desc="Used for Cache->Dir and Fwd messages",
interface="Message") {
Addr addr, desc="Physical address for this response";
CoherenceResponseType Type, desc="Type of response";
MachineID Sender, desc="Node who is responding to the request";
NetDest Destination, desc="Multicast destination mask";
DataBlock DataBlk, desc="data for the cache line";
MessageSizeType MessageSize, desc="size category of the message";
int Acks, desc="Number of acks required from others";
// This must be overridden here to support functional accesses
bool functionalRead(Packet *pkt) {
if (Type == CoherenceResponseType:Data) {
return testAndRead(addr, DataBlk, pkt);
}
return false;
}
bool functionalWrite(Packet *pkt) {
// No check on message type required since the protocol should read
// data block from only those messages that contain valid data
return testAndWrite(addr, DataBlk, pkt);
}
}地址、数据,发送者编号、目标编号,数据大小、应答数量(比如invack数量)、这里fwd数据类型和request类型是一样的,也就是说fwdgetS就是getS,只不过发送者一个是dictory一个是L1. 再看MSI-cache.sm : Sequencer *sequencer; // Incoming request from CPU come from this
CacheMemory *cacheMemory; // This stores the data and cache states
bool send_evictions; // Needed to support O3 CPU and mwait
// Other declarations
// Message buffers are required to send and receive data from the Ruby
// network. The from/to and request/response can be confusing!
// Virtual networks are needed to prevent deadlock (e.g., it is bad if a
// response gets stuck behind a stalled request). In this protocol, we are
// using three virtual networks. The highest priority is responses,
// followed by forwarded requests, then requests have the lowest priority.
// Requests to the directory
MessageBuffer * requestToDir, network="To", virtual_network="0",
vnet_type="request";
// Responses to the directory or other caches
MessageBuffer * responseToDirOrSibling, network="To", virtual_network="2",
vnet_type="response";
// Requests from the directory for fwds, invs, and put acks.
MessageBuffer * forwardFromDir, network="From", virtual_network="1",
vnet_type="forward";
// Responses from directory and other caches for this cache's reqs.
MessageBuffer * responseFromDirOrSibling, network="From",
virtual_network="2", vnet_type="response";
// This is all of the incoming requests from the core via the sequencer
MessageBuffer * mandatoryQueue;sequencer是一个CPU可以发送请求给L1 cache的指针,但是说真的,在下面代码里,sequencer更像是一个L1返回数据、地址、驱逐完成信号的输出通道,所以暂时可以先这么理解即可(理解有什么功能,而不是理解定义) MessageBuffer定义了一个通道指针,这里通道都是阻塞的,并且有优先级,virtual_network越大优先级越大,但是gem5内部定义了一个一周期内最大处理事务数为32,也就是说如果三个通道同时来数据(即使优先级高的事务发生stall),L1也能在一周期能处理这三个请求。 mandatoryQueue这个是CPU发送请求给L1的通道。 接下来是event类型: enumeration(Event, desc="Cache events") {
// From the processor/sequencer/mandatory queue
Load, desc="Load from processor";
Store, desc="Store from processor";
// Internal event (only triggered from processor requests)
Replacement, desc="Triggered when block is chosen as victim";
// Forwarded reqeust from other cache via dir on the forward network
FwdGetS, desc="Directory sent us a request to satisfy GetS. ";
//"We must have the block in M to respond to this.";
FwdGetM, desc="Directory sent us a request to satisfy GetM. ";
//"We must have the block in M to respond to this.";
Inv, desc="Invalidate from the directory.";
PutAck, desc="Response from directory after we issue a put. ";
//"This must be on the fwd network to avoid";
//"deadlock.";
// Responses from directory
DataDirNoAcks, desc="Data from directory (acks = 0)";
DataDirAcks, desc="Data from directory (acks > 0)";
// Responses from other caches
DataOwner, desc="Data from owner";
InvAck, desc="Invalidation ack from other cache after Inv";
// Special internally triggered event to simplify implementation
LastInvAck, desc="Triggered after the last ack is received";
}datadirnoacks之前的那几个event都是老熟人了,下面那几个我们来看看触发条件: in_port(response_in, ResponseMsg, responseFromDirOrSibling) {
// NOTE: You have to check to make sure the message buffer has a valid
// message at the head. The code in in_port is executed either way.
if (response_in.isReady(clockEdge())) {
// Peek is a special function. Any code inside a peek statement has
// a special variable declared and populated: in_msg. This contains
// the message (of type RequestMsg in this case) at the head.
// "forward_in" is the port we want to peek into
// "RequestMsg" is the type of message we expect.
peek(response_in, ResponseMsg) {
// Grab the entry and tbe if they exist.
Entry cache_entry := getCacheEntry(in_msg.addr);
TBE tbe := TBEs[in_msg.addr];
// The TBE better exist since this is a response and we need to
// be able to check the remaining acks.
assert(is_valid(tbe));
// If it's from the directory...
if (machineIDToMachineType(in_msg.Sender) ==
MachineType:Directory) {
if (in_msg.Type != CoherenceResponseType:Data) {
error("Directory should only reply with data");
}
// Take the in_msg acks and add (sub) the Acks we've seen.
// The InvAck will decrement the acks we're waiting for in
// tbe.AcksOutstanding to below 0 if we haven't gotten the
// dir resp yet. So, if this is 0 we don't need to wait
assert(in_msg.Acks + tbe.AcksOutstanding >= 0);
if (in_msg.Acks + tbe.AcksOutstanding == 0) {
trigger(Event:DataDirNoAcks, in_msg.addr, cache_entry,
tbe);
} else {
// If it's not 0, then we need to wait for more acks
// and we'll trigger LastInvAck later.
trigger(Event:DataDirAcks, in_msg.addr, cache_entry,
tbe);
}`
相信我我也不想贴这么长的注释,一段一段删除太麻烦了!而且不贴会漏信息————这里就是说如果数据从dictory中response通道中来,那么由前面的MSI-msg.sm中可以知道只有两种事务:data和invack,所以这里前面有一个if帮我们判断出这个事务类型是data,因此我们可以想到一种情况:我们发出了getM,于是需要等待其他core把他们的S变为I,但是如果说刚好没有core L1拥有这个地址的S数据,因此dictory会返回data(包含Acks=0)交给源L1,这个时候就会出现DataDirNoAcks,同理,如果有core拥有这个S数据,那么就发出data(Acks >= 0)交给源L1,然后源L1收到后就触发DataDirAcks事务。(这里前提是当发出getM时,这个数据是多核共享状态S,此时gem5里的dir模型(在MSI-dir.sm)会访问内存而不是让共享者分享他们的数据尽管共享者有合法的数据且由其他L1交给源L1更快,这是代码里可以性能改进的一个点。
至于dataowner、invack、lastinvack事务,dataowner是在L1发出getM请求给dictory后,dictory找到data owner(其他L1)并且让他通过reponse通道转发数据给源L1时就会触发。invack和dataowner情况一样,因为getM后不只是要数据,也要被告知其他cache L1已经把这个缓存行对应数据变为I了,但是这里又有点特殊,因为getM后有可能是有L1独占了该缓存行,有可能是共享了该缓存行,不管是哪个,当最后一个L1发送invack到来时,就会触发LastInvcak事务,而如果不是最后一个invack,那么触发的是InvAck事务。
这里很重要也很绕,因为名字有的重叠了,注意事务(event)和数据类型(CoherenceResponseType|CoherenceRequestType)的区分。
了解这些后,我们可以看一些gem5自带的结构体(之所以说是gem5自带的,是因为这些基本不会去改,而且结构体里的单元用的数据类型以及函数都是gem5自带的,没研读gem5代码基本不可能掌握):
` structure(Entry, desc="Cache entry", interface="AbstractCacheEntry") {
State CacheState, desc="cache state";
DataBlock DataBlk, desc="Data in the block";
}
// TBE is the "transaction buffer entry". This stores information needed
// during transient states. This is like an MSHR. It functions as an MSHR
// in this protocol, but the entry is also allocated for other uses.
structure(TBE, desc="Entry for transient requests") {
State TBEState, desc="State of block";
DataBlock DataBlk, desc="Data for the block. Needed for MI_A";
int AcksOutstanding, default=0, desc="Number of acks left to receive.";
}
// Table of TBE entries. This is defined externally in
// src/mem/ruby/structures/TBETable.hh. It is templatized on the TBE
// structure defined above.
structure(TBETable, external="yes") {
TBE lookup(Addr);
void allocate(Addr);
void deallocate(Addr);
bool isPresent(Addr);
}
/*************************************************************************/
// Some declarations of member functions and member variables.
// The TBE table for this machine. It is templatized under the covers.
// NOTE: SLICC mangles names with the machine type. Thus, the TBE declared
// above will be L1Cache_TBE in C++.
// We also have to pass through a parameter to the machine to the TBETable.
TBETable TBEs, template="<L1Cache_TBE>", constructor="m_number_of_TBEs";
// Declare all of the functions of the AbstractController that we may use
// in this file.
// Functions from clocked object
Tick clockEdge();
// Functions we must use to set things up for the transitions to execute
// correctly.
// These next set/unset functions are used to populate the implicit
// variables used in actions. This is required when a transition has
// multiple actions.
void set_cache_entry(AbstractCacheEntry a);
void unset_cache_entry();
void set_tbe(TBE b);
void unset_tbe();
// Given an address and machine type this queries the network to check
// where it should be sent. In a real implementation, this might be fixed
// at design time, but this function gives us flexibility at runtime.
// For example, if you have multiple memory channels, this function will
// tell you which addresses to send to which memory controller.
MachineID mapAddressToMachine(Addr addr, MachineType mtype);
// Convience function to look up the cache entry.
// Needs a pointer so it will be a reference and can be updated in actions
Entry getCacheEntry(Addr address), return_by_pointer="yes" {
return static_cast(Entry, "pointer", cacheMemory.lookup(address));
}
/*************************************************************************/
// Functions that we need to define/override to use our specific structures
// in this implementation.
// Required function for getting the current state of the block.
// This is called from the transition to know which transition to execute
State getState(TBE tbe, Entry cache_entry, Addr addr) {
// The TBE state will override the state in cache memory, if valid
if (is_valid(tbe)) { return tbe.TBEState; }
// Next, if the cache entry is valid, it holds the state
else if (is_valid(cache_entry)) { return cache_entry.CacheState; }
// If the block isn't present, then it's state must be I.
else { return State:I; }
}
// Required function for setting the current state of the block.
// This is called from the transition to set the ending state.
// Needs to set both the TBE and the cache entry state.
// This is also called when transitioning to I so it's possible the TBE and/
// or the cache_entry is invalid.
void setState(TBE tbe, Entry cache_entry, Addr addr, State state) {
if (is_valid(tbe)) { tbe.TBEState := state; }
if (is_valid(cache_entry)) { cache_entry.CacheState := state; }
}`
这些是老天的馈赠,用他们跟用printf一样直接用即可,反正就是通过这些函数能修改或者了解某个地址对应tbe或者缓存行的当前状态,得到目标地址在哪个缓存行,在哪个tbe(如果没有,则is_valid(tbe|cache)会过不了关,然后就是分配tbe、cacheentry给某个地址,释放tbe,、cacheentry。functionalread/write我不懂,ai说是功能读写不影响时序。
接下来是in_port out_port,说难也不难,理解上面全部后这些是小case
out_port(request_out, RequestMsg, requestToDir); out_port(response_out, ResponseMsg, responseToDirOrSibling);
out_port(接口名字,传递信息类型(在MSI-msg.sm里头),方向)
in_port太长了,而且只要记得in_port只是在对输入的信息进行判断,判断是什么类型以及谁发的之后对该地址对应的tbe或者缓存行触发某个事务。
哦对了这里给你一个问题,通过tbe处于中间态,那么cache line应该处在什么态?
答案:变化前的态,或者无所谓,因为tbe永远是首先被访问的而不是cacheline,tbe是某个地址对应的最新情况!
感觉篇幅有点长了,先到这吧
浙公网安备 33010602011771号