Memory System
M5's new memory system (introduced in the first 2.0 beta release) was designed with the following goals:
- Unify timing and functional accesses in timing mode. With the old memory system the timing accesses did not have data and just accounted for the time it would take to do an operation. Then a separate functional access actually made the operation visible to the system. This method was confusing, it allowed simulated components to accidentally cheat, and prevented the memory system from returning timing-dependent values, which isn't reasonable for an execute-in-execute CPU model.
- Simplify the memory system code -- remove the huge amount of templating and duplicate code.
- Make changes easier, specifically to allow other memory interconnects besides a shared bus.
For details on the new coherence protocol, introduced (along with a substantial cache model rewrite) in 2.0b4, see Coherence Protocol.
MemObjects
All objects that connect to the memory system inherit from MemObject
. This class adds the pure virtual function getPort(const std::string &name)
which returns a port corresponding to the given name. This interface is used to connect memory objects together with the help of a connector (see below).
Ports
The next large part of the memory system is the idea of ports. Ports are used to interface memory objects to each other. They will always come in pairs and we refer to the other port object as the peer. These are used to make the design more modular. With ports a specific interface between every type of object doesn't have to be created. Every memory object has to have at least one port to be useful.
There are two groups of functions in the port object. The send* functions are called on the port by the object that owns that port. For example to send a packet in the memory system a CPU would call myPort->sendTiming(pkt)
to send a packet. Each send function has a corresponding recv function that is called on the ports peer. So the implementation of the sendTiming()
call above would simply be peer->recvTiming(pkt)
. Using this method we only have one virtual function call penalty but keep generic ports that can connect together any memory system objects.
Connections
Memory ports are connected together by the python code doing something like p1 = obj1->getPort(); p2 = obj2->getPort(); p1->setPeer(p2); p2->setPeer(p1);
with the appropriate port names if any. This is done after all objects are instantiated.
Request
A request, the overall memory request consisting of the parts of the request that are persistent throughout the transaction.
A request contains the following all of which are accessed by accessors to be certain the data is valid:
- A physical address
- A virtual address
- A size
- The time the request was created
- The PC that caused this request if any
- The Cpu/Thread that caused this request, if any.
Packet
A Packet is used to encapsulate a transfer between two objects in the memory system (e.g., the L1 and L2 cache). This is in contrast to a Request where a single Request travels all the way from the requester to the ultimate destination and back, possibly being conveyed by several different Packets along the way.
A packet contains the following all of which are accessed by accessors to be certain the data is valid:
- An address for this request.
- A size for this request.
- A pointer to the data being manipulated.
- Set by
dataStatic()
,dataDynamic()
, anddataDynamicArray()
which control if the data associated with the packet is freed when the packet is, not, withdelete
, and withdelete []
respectively. - Allocated if not set by one of the above methods
allocate()
and the data is freed when the packet is destroyed. (Always safe to call). - A pointer can be retrived by calling
getPtr()
-
get()
andset()
can be used to manipulate the data in the packet. The get() method does a guest-to-host endian conversion and the set method does a host-to-guest endian conversion.
- Set by
- A status indicating Success, BadAddress, Not Acknowleged, and Unknown.
- A list of command attributes associated with the packet
- Note: There is some overlap in the data in the status field and the command attributes. This is largely so that a packet an be easily reinitialized when nacked or easily reused with atomic or functional accesses.
- A
SenderState
pointer which is a virtual base opaque structure used to hold state associated with the packet but specific to the sending device (e.g., an MSHR). A pointer to this state is returned in the packet's response so that the sender can quickly look up the state needed to process it. A specific subclass would be derived from this to carry state specific to a particular sending device. - A
CoherenceState
pointer which is a virtual base opaque structure used to hold coherence-related state. A specific subclass would be derived from this to carry state specific to a particular coherence protocol. - A pointer to the request.
Access Types
There are three types of accesses supported by the ports.
- Timing - Timing accesses are the most detailed access. They reflect our best effort for realistic timing and include the modeling of queuing delay and resource contention. Once a timing request is successfully sent at some point in the future the device that sent the request will either get the response or a NACK if the request could not be completed (more below). Timing and Atomic accesses can not coexist in the memory system.
- Atomic - Atomic accesses are a faster than detailed access. They are used for fast forwarding and warming up caches and return an approximate time to complete the request without any resource contention or queuing delay. When a atomic access is sent the response is provided when the function returns. Atomic and timing accesses can not coexist in the memory system.
- Functional - Like atomic accesses functional accesses happen instantaneously, but unlike atomic accesses they can coexist in the memory system with atomic or timing accesses. Functional accesses are used for things such as loading binaries, examining/changing variables in the simulated system, and allowing a remote debugger to be attached to the simulator. The important note is when a functional access is received by a device, if it contains a queue of packets all the packets must be searched for requests or responses that the functional access is effecting and they must be updated as appropriate. The
Packet::intersect()
andfixPacket()
methods can help with this.
Timing Flow control
Timing requests simulate a real memory system so unlike functional and atomic accesses their response is not instantaneous. Beacuse the timing requests are not instataneous flow control is needed. When a timing packet is sent via sendTiming()
the packet may or may not be accepted (signaled by returning true or false). If false is returned the object should not attempt to sent anymore packets until it received a recvRetry()
call. At this time is should again try to call sendTiming()
however the packet may again be rejected. Note: The original packet does not need to be resent, a higher priority packet can be sent as. Once sendTiming()
returns true the packet may still not be able to make it to its destination. For packets that required a response (e.g. pkt->isResponse()
is true. Any memory object can not acknowleged the packet by chaning its result to Nacked
and sending it back to its source. However if it is a respose packet, this can not be done. The true/false return is supposed to be used for local flow control while nacking is for global flow control. In both cases a a response can not be nacked.
Response and Snoop ranges
Ranges in the memory system is handled by a devices that are sensitive to an address range providing an implementation for getDeviceAddressRanges
in their port objects. This method returns two AddrRangeList
s one of addresses it responds to and one of addresses it snoops. Upon this range changing (e.g. pci configuration taking place) the device should call sendStatusChange(Port::RangeChange)
on its port so that the new range is propigated to the entire hierarchy. This is precisely what happents during init()
all memory objects call sendStatusChange(Port::RangeChange)
and a flurry of range updates occur until everyones ranges have been propigated to all busses in the system.
Port Descendants
There are several types of ports that inherit from Port
and are very generic.
-
FunctionalPort
provides easy methonds from writing and reading physical addresses. It is only meant to load data into memory and update constants before the simulation begins. -
VirtualPort
provides the same methods that theFunctionalPort
however the addresses passed to it are virtual addresses and a translation is done to get the physical address. If noThreadContext
is passed to the constructor the virtual->physical translation must be static (e.g. Alpha Superpage accesses), otherwise aThreadContext
is required to do the translation.