粗略的把gem5 documentation扫了一遍。仍不知道如何建立一个以PCM位介质的内存模型。于是又google了一段时间,在gem5 users 的mailing list中找到了点有用的东西how and where to start gem5。于是按照他的建议,看了下源码src/mem/Abstrac_memory.* src/mem/simple_mem.*。似乎有了点理解。
先来看看 AbstractMemory.py
class AbstractMemory(MemObject):
type = 'AbstractMemory'
abstract = True
cxx_header = "mem/abstract_mem.hh"
# A default memory size of 128 MB (starting at 0) is used to
# simplify the regressions
range = Param.AddrRange('128MB', "Address range (potentially interleaved)")
null = Param.Bool(False, "Do not store data, always return zero")
# All memories are passed to the global physical memory, and
# certain memories may be excluded from the global address map,
# e.g. by the testers that use shadow memories as a reference
in_addr_map = Param.Bool(True, "Memory part of the global address map")
# Should the bootloader include this memory when passing
# configuration information about the physical memory layout to
# the kernel, e.g. using ATAG or ACPI
conf_table_reported = Param.Bool(True, "Report to configuration table")
首先既然是内存类,既然要继承MemObject。而MemObject最重要的地方在于定义了两个方法用来获取master port 和 slave port 。而关于master port和slave port 在gem5中的第一篇tutorial中有介绍。这里截出个人认为比较重要的两句话:1.A master module has at least one master port, a slave module at least one slave port, and an interconnect module at least one of each 2.A master module, e.g. a CPU, changes the state of a slave module,e.g. a memory through a Request transported between master ports and slave ports using Packets。也就是说master port往往是在master module上的,而master module往往是用来改变slave module的状态的。比如由CPU发出的request packet t改变了memory的状态。对了,GEM5中CPU和memory的交互往往是建立在packet上的。
然后再看该AbstractMemory定义了几个跟memory相关的属性。其中null变量是指该块内存存不了数据,你非要从上面取数据的话,只能得到0。而conf_table_reported变量是用来告诉bootloader是否要把该块内存的存在告诉内核。不告诉的话,那内核就不知道该块内核的存在。我觉得是应该配合in_addr_map变量的使用。如果in_addr_map=false的话,那么该块内存应该就不能用于全局地址映射。
好了,再来看看Abstract_mem.h:
/**
* Locked address class that represents a physical address and a
* context id.
*/
class LockedAddr {
private:
// on alpha, minimum LL/SC granularity is 16 bytes, so lower
// bits need to masked off.
static const Addr Addr_Mask = 0xf;
public:
// locked address
Addr addr;
// locking hw context
const ContextID contextId;
static Addr mask(Addr paddr) { return (paddr & ~Addr_Mask); }
// check for matching execution context
bool matchesContext(Request *req) const
{
return (contextId == req->contextId());
}
LockedAddr(Request *req) : addr(mask(req->getPaddr())),
contextId(req->contextId())
{}
// constructor for unserialization use
LockedAddr(Addr _addr, int _cid) : addr(_addr), contextId(_cid)
{}
};
首先是LockedAddr类,刚开始看到这个类的时候,我也不知道这个变量是用来干嘛的。后来google了一下,这个东西跟 Load-link/store-conditional有关。具体来说应该算是一个同步操作原语,从某种意义上来说与compare and sawp 等价。就是比如说我要修改某一地址上存储的值。我先“锁”住这块地址(这里锁不是真的锁,只是相当于标记一下),然后存新值得时候,当且仅当该地址上没有发生更新操作时,才存新值。否则,存新值操作失败。也就是分两步,第一步是load-link,第二步是store-conditional。而store不一定成功。据说它比compare and swap的厉害之处在于,即使在被标记的地址上发生的更新操作跟原值一样。也会导致store操作失败。具体可看wiki啦。然后这个类就声明了跟这个同步操作原语相关的Addr类。
然后是AbstractMemory类,这个类挑几个重点说说。首先它作为一个Memory类,提供了它应当提供的reading和writing 这块Memory的方法。但是没有提供任何计时信息。当然它代表的是一块的连续内存啦。其中uint8_t* pmemAddr;变量是用来LMemory之所以叫Memory,就是因为它是用来放数据的,虽然是个模拟程序,为了模拟的像,总得从主机上找个地方放数据吧。嗯,该指针指向的就是主机上放该块Memory数据的地方。std::list
bool writeOK(PacketPtr pkt) {
Request *req = pkt->req;
if (lockedAddrList.empty()) {
// no locked addrs: nothing to check, store_conditional fails
bool isLLSC = pkt->isLLSC();
if (isLLSC) {
req->setExtraData(0);
}
return !isLLSC; // only do write if not an sc
} else {
// iterate over list...
return checkLockedAddrList(pkt);
}
}
上面这段代码就比较厉害啦。首先pkt里面是个写请求,它是想看看该次写请求能不能写成功。嗯,它先检查了下lockedAddrList是否为空,如果该链表为空的话,要么表示没有Addr进行了store之前的load-link操作,要么表示有Addr进行了load-link操作,但是被之前的store操作清空了,或者表示该内存根本就不支持LL/SC(load-link/store-conditional)。这么一说。下面的逻辑就清楚了。而如果链表为非空的话,自然要扫一遍链表啦。如果有匹配的,就可以store啦。关于checkLockAddrList操作请继续往下看。
/** Number of total bytes read from this memory */
Stats::Vector bytesRead;
/** Number of instruction bytes read from this memory */
Stats::Vector bytesInstRead;
/** Number of bytes written to this memory */
Stats::Vector bytesWritten;
/** Number of read requests */
Stats::Vector numReads;
/** Number of write requests */
Stats::Vector numWrites;
/** Number of other requests */
Stats::Vector numOther;
/** Read bandwidth from this memory */
Stats::Formula bwRead;
/** Read bandwidth from this memory */
Stats::Formula bwInstRead;
/** Write bandwidth from this memory */
Stats::Formula bwWrite;
/** Total bandwidth from this memory */
Stats::Formula bwTotal;
再来看看Abstrac_mem.c文件吧:
void
AbstractMemory::trackLoadLocked(PacketPtr pkt)
{
Request *req = pkt->req;
Addr paddr = LockedAddr::mask(req->getPaddr());
// first we check if we already have a locked addr for this
// xc. Since each xc only gets one, we just update the
// existing record with the new address.
list::iterator i;
for (i = lockedAddrList.begin(); i != lockedAddrList.end(); ++i) {
if (i->matchesContext(req)) {
DPRINTF(LLSC, "Modifying lock record: context %d addr %#x\n",
req->contextId(), paddr);
i->addr = paddr;
return;
}
}
// no record for this xc: need to allocate a new one
DPRINTF(LLSC, "Adding lock record: context %d addr %#x\n",
req->contextId(), paddr);
lockedAddrList.push_front(LockedAddr(req));
}
再就是void AbstractMemory::access(PacketPtr pkt)方法。这是真正的对Memory访问的实现。如果pkt中要求的地址已经由Cache处理过了,那么直接返回。如果还未处理。那么分以下三种情形:swap,read,write。其中swap是指把包中存的值,于包指定的地址(在memory中)上的值进行交换。然后就是普通的read和write操作了。当然进行相关操作的时候,要更新stats信息。对了,还有一个invalidate操作是write-invalidate操作,什么都不做,因为其本来就是什么都不做(对于memory来说,主要是在cache层才有事做,我也是猜的。应该跟snoop protocol有关)。 invalidate is WriteInvalidate。所以在write前面,为了保证一致性。而functionalAccess方法就是做事情,但是不参与stats。暂时就写这么多吧。我也不知道怎么填坑。。。
---------------------------------------------------------------我是分割线-----------------------------------------------------------------------------------------------------------------------------------------------------
再来看看SimpleMemory.py。它继承自AbstractMemory。并且加入了延迟和不确定延迟变量(latence_var),并且加入了带宽限制,保证数据的传输速度不超过带宽。原来这里的带宽也是指传输速率啊。我以为是指每次的传输位宽呢刚开始。
然后是simple_mem.h。这里说到了simple_memory是只有一个port的内存控制器。 在该文件中还定义了 类:
class DeferredPacket{
public:
const Tick tick;
const PacketPtr pkt;
DeferredPacket(PacketPtr _pkt, Tick _tick) : tick(_tick), pkt(_pkt)
{ }
};
该类是用于recvTimingReq函数的。根据tutorial中介绍说:For a slave module, typically schedule an action to send a response at a later time。也就是说对于recvTimingReq,该memory会过一会在发送response。而tick就是用来记录未来发送的时刻的。然后专门定义了std::list最后来看看simple_mem.c文件。好吧,我承认我写的不走心。因为我自己也不是太理解。来看这个函数:
Tick
SimpleMemory::recvAtomic(PacketPtr pkt)
{
access(pkt);
return pkt->cacheResponding() ? 0 : getLatency();
}
跟tutorial中描述的一样,首先去执行pkt中要求的操作,即access(pkt)。然后如果cache未命中的话,返回自己本身的延迟。并没有推迟发送response的坏习惯(For a slave module, perform any state updates and turn the request into a
response)。然后是这个函数:
void
SimpleMemory::recvFunctional(PacketPtr pkt)
{
pkt->pushLabel(name());
functionalAccess(pkt);
bool done = false;
auto p = packetQueue.begin();
// potentially update the packets in our packet queue as well
while (!done && p != packetQueue.end()) {
done = pkt->checkFunctional(p->pkt);
++p;
}
pkt->popLabel();
}
然后是这个recvTimingReq。首先,该方法会检查自己是不是忙,如果memory控制器在忙的状态会把retryReq标记为true。然后返回false。都忙了,自然无法处理pkt啊,所以返回false。在这之前,如果它检查发现retryReq为true。就直接返回false,意思是我都让你待会再发了,你又发。对不起,不接收。
if (pkt->isRead() || pkt->isWrite()) {
// calculate an appropriate tick to release to not exceed
// the bandwidth limit
Tick duration = pkt->getSize() * bandwidth;
// only consider ourselves busy if there is any need to wait
// to avoid extra events being scheduled for (infinitely) fast
// memories
if (duration != 0) {
schedule(releaseEvent, curTick() + duration);
isBusy = true;
}
}
而上面这段代码,实现了对延迟的模拟。并且在这段时期内,把memory控制器标记为忙。
然后经过recvAtomic(pkt);完成pkt的要求,并且如果需要response的话,开始make response,并把response和对应的发送时间加入packetQueen队列。
暂时只能理解这么多了。。