粗略的把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<LockedAddr> lockedAddrList;这个变量跟上文提到的Load-link/store-conditional有关。凡是被load-link标记过的地址就会加入到这个链表中。自然这两个函数 bool checkLockedAddrList(PacketPtr pkt);, void trackLoadLocked(PacketPtr pkt);也是为了用来标记和检查packet上addr的load-link的状态的。
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<LockedAddr>::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<DeferredPacket> packetQueue链表用来存储过会发的DeferredPacket。再就是retryReq bool型变量用来标记是否需要告诉master重新发送请求,因为该请求发送的时候,内存忙,无法接收。而release函数就是在内存忙完后,把自身标记为不忙,如果有retryReq标记起告诉master重发请求的作用的。
最后来看看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队列。
暂时只能理解这么多了。。