gem5 abstractMemory simpleMemory

        粗略的把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;

       然后是一些stats相关的变量。这没什么好说的啦。注释说的挺清楚的。不过我也没搞懂为什么要用Vector来存,以后看了别的代码回来再理解试试吧。

       再来看看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));
}

         这就是上文提到的trackLoadLocked的实现。就是LL/SC中的LL操作。如果之前有匹配contextid的LockAddr在链表中。那么只需要把旧地址改成新地址就可以了。我也不知道xc是什么啊(其实我对xc还挺有好感的)。然后如果不匹配的话,自然要给链表新加一项啦。然后是checkLockedAddrList方法。gem5自带代码注释真的好充分啊。我觉得我就是个翻译啊。只要是写操作就要调用这个函数,无论是普通的store还是store-conditional。因为是写的话,就要把被“动”过的地址从lockedAddrList链表中移出去啊。因为被“动”过了,所以以后的store-conditional肯定是fail啊。然后对于store-conditional操作,一定要是在lockedAddrList链表中才能返回successful。然后不要还要移除链表中地址相同而contexteid不同的LockAddr因为该块地址被“动”过了。

        再就是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();
}

诚如tutorial中说的一样,它除了不参与stats,还检查了一遍packetQueen。(Typically check internal (packet) buffers against request packet)暂时并不知道检查带来了什么影响。

       然后是这个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队列。

     暂时只能理解这么多了。。


 





你可能感兴趣的:(gem)