simple-ssd

created : 2020-06-10T10:39:41+00:00
modified : 2020-08-26T11:54:02+00:00

ssd

새롭게 알게된 것

  • [[gem5]]

    다운로드

  • simplessd 공홈
  • 하라는 대로 하면 된다. (FullSystem은 example 을 실행해도 그대로 안되서, 그냥 standalone 을 먼저 봐보기로 했다.)

문서 읽기

  • 그냥 홈페이지에 있는 문서를 읽어보자. 중요하니까 정리해놨겠지

그림으로 그려보기

  • simplessd drawio

    Host Interface Layer

  • HIL 이라고도 불리는 Host Interface Layer에 대한 설명이다.
  • HIL 은 host side에 있는 host controller, host controller 에게 추상화된 API를 제공해주는 SSD Interface

Host Controller

  • NVMe, SATA and UFS를 구현해 놓았으며, Open-Channel SSD 는 NVMe 를 상속받음.
Host Interface
  • hil/nvme/interface.hhSimpleSSD::DMAInterface를 상속하여 선언된 SimpleSSD::HIL::NVMe::Interface 를 보자.
class DMAInterface {
 public:
  DMAInterface() {}
  virtual ~DMAInterface() {}

  virtual void dmaRead(uint64_t, uint64_t, uint8_t *, DMAFunction &,
                       void * = nullptr) = 0;
  virtual void dmaWrite(uint64_t, uint64_t, uint8_t *, DMAFunction &,
                        void * = nullptr) = 0;
};
class Interface : public SimpleSSD::DMAInterface {
 protected:
  Controller *pController;

 public:
  virtual void updateInterrupt(uint16_t, bool) = 0;
  virtual void getVendorID(uint16_t &, uint16_t &) = 0;
};
  • DMAInterface 에서는 Direct Memory Access 를 위해서 dmaRead, dmaWrite 를 제공한다.
  • updateInterrupt 는 host의 특정 interrupt vector 에 interrupt를 보낸다.
  • getVendorId 는 NVMe의 Identify Controller가 vendor Id와 subsystem vendor ID를 필요로 하기 때문에 존재하는 method 이다.
Controller and Firmware
  • NVMe controller/firmware 는 아래 3가지 컴포넌트 (Controller, Subsystem and Namespace)로 구성된다.
    • Controller는 모든 queue 연산(SQ 에서 request 를 읽고, CQ에 request를 쓰고, 인터럽트를 발생시키는)을 담당한다.
    • Subsystem은 모든 NVMe의 admin commands를 다루며, Namespace를 제어하고, SSD Layer에 I/O를 실행한다.
    • Namespace는 모든 NVMe의 I/O commands 를 다룬다.
Controller
  • 모든 queue 연산을 담당하는 Controller는 hil/nvme/controller.hhSimpleSSD::HIL::NVMe::Controller 로 정의되어 있다.
  • EventEngine은 주기적으로 Controller::work를 유발시키며, 이때 주기는 WorkPeriod 설정값을 참조한다.
  • Controller::work 는 설정된 중재 함수(Controller::collectSQueue)를 사용하여 모든 submission queue를 모은다.
  • 새로운 Request는 내부의 FIFO Queue(Controller::lSQFIFO) 에 넣어지며, 새로운 Request 가 있다면, Controller:handleRequest를 호출해야 한다. 이때 이 함수는 AbstractSubsystem::submitReqeust를 유발한다.

  • AbstractSubsystem 이 request를 처리하고 난 뒤, Controller::submit 함수가 불려지게 되며, 내부적으로 존재하는 FIFO Queue(Controller::lCQFIFO)에 완료된 걸 넣는다.
  • Controller::submit 함수는 Controller::completion 함수를 부르는 event를 관리한다.
  • Controller::completion 함수는 CQ entry를 채우고, Controller::updateInterrupt 를 사용해서 interrupt를 보낸다.
  • Controller::updateInterrupt는 단순히 Interface::updateInterrupt를 호출한다.
Subsystem
  • admin commmands를 다루는 NVMe Subsystem은 hil/nvme/subsystem.hhSimpleSSD::HIL::NVMe::Subsystem 으로 선언되어 있다.
  • Open-channel SSD Subsystem은 hil/nvme/ocssd.hhSimpleSSD::HIL::NVMe::OpenChannelSSD로 선언되어 있다.
  • 두 subsystem 모두 hil/nvme/abstract_subsystem.hhSimpleSSD::HIL::NVMe::AbstractSubsystem 을 상속한다.
  • 기본적으로 아래쪽 설명들은 NVMe subsystem 을 기반으로 하고 있는데, Open-Channel SSD 랑 사소한 차이만 있기 때문이다.

  • NVMe Subsystem은 2부분으로 나뉘는데, command handling, I/O request handling.
  • Subsystem이 여러 namespaces를 가질수 있으므로, Subsystem은 반드시 I/O requests 를 Namespaces 로부터 HIL의 SSD Interface로 넘겨줘야 한다.

  • 각 NVMe 명령들은
    • I/O 명령이라면, Subsystem::submitCommand 함수를 통해서 특정한 Namespace로 가지게 되며.
    • 관리용(admin) 명령이라면, submitCommand 함수는 OPCODE를 확인한 이후 적절한 함수를 호출하게 된다.
  • 모든 명령들은 완료됨을 알리기 위해서 Controller::submit 를 유발한다.

  • 특정한 I/O 함수들(Subsystem::read, write, flush and trim)이 불리게 된다면, I/O unit으로 번역된뒤, SSD Intreface의 함수들로 간다. (HIL::HIL::read, HIL::HIL::write, HIL::HIL::flush and HIL::HIL::trim);

  • SSD를 선형적으로 쪼개기 위해서, Subsystem은 multiple Namespaces를 유지한다.
    • 예를 들어 1TB SSD 가 4K 논리적 block을 사용한다고 하자. 이때 512GB, 256GB, 256GB 용량으로 3개의 Namespaces로 쪼갤수 있다.
    • Subsystem은 offset과 length를 찾아서 namespaces를 할당할 것이고, Subsystem은 할당되지 않은 공간을 처음 맞는 Namespaces에 할당한다.(first-fit). 만약 공간이 없다면 할당이 실패한다.
  • 각 Namespace 마다 offset과 length를 가지고 있으며, 이 값은 SSD interface를 위해 I/O unit으로 번역될때 쓰인다.
Namespace
  • I/O 명령을 다루는 NVMe Namespace는 hil/nvme/namespace.hhSimpleSSD::HIL::NVMe::Namespace로 선언되어 있으며. NVMe Subsystem과 비슷한 구조를 다룬다.(둘다 command를 다룬다.)
  • I/O 명령이 오면, Namespace 는 Subsystem의 해당 함수들을 호출한다.(read, write, flush and trim).

Serial AT Attachment

  • Serial AT Attachment - SATA 는 SSD와 HDD를 위한 전통적 interface이다.
  • SimpleSSD 에서는 Serial ATA Advanced host Controller Interface (AHCI) 1.3.1 에 기반한 STATA HBA를 구현하였고, Serial ATA Revision 3.0 에 기반을 둔 STAT PHY와 프로토콜을 구현하였다.
Host interface
  • hil/sata/interface.hh에 선언된 SimpleSSD::HIL::SATA::Interface 추상 클래스는 simulator에게 common API를 제공한다.
  • SimpleSSD-FUllSystem 에서 src/dev/storage/sata_interrface.hh에 선언된 SATAInterface 에서 어떻게 상속하는지 확인할 수 있다.

  • SimpleSSD::HIL::SATA::Interface 은 오직 virtual void updateInterrupt(bool) = 0를 interrupt posting 을 위해서 포함하고 있다.
Host Bus Adapter
  • SATA는 Host Bus Adapter(HBA) 라고 불리는 host sid controller 를 필요로 한다.
  • HBA 디자인은 다양하지만, 우리는 누구나 접근할수 있는 AHCI spcification을 사용했다.

  • 우리는 단 하나의 SimpleSSD 인스턴스만 HBA에 연결되기 때문에 단 하나의 port 만 구현했다.

  • Interface는 AHCI registser(Genetic Host Controller registers and Port registers)를 HBA::writeAHCIRegister 함수를 통해서 쓴다.
  • PxCl register에 bits를 쓰는 건 그에 대항하는 NCQ에 새로운 요청이 있다는걸 의미한다.
  • HBA는 내부 FIFO(HBA::lRequestQueue)에 request를 넣기 위해서 HBA::processCommand 를 호출한다.

  • NVMe와 동일하게 Event Engine은 주기적으로 HBA::work 함수를 호출하고, 이 함수는 HBA::handleRequest 함수를 호출하고와 Device::submitCommand 함수가 불리게 된다.

  • command handling 이후 DeviceHBA::submitFIS 함수를 FIS 응답을 host에게 돌려주기 위해서 부른다. HBA::submitFIS 함수는 HBA::lResponseQueue에 응답을 넣고, HBA::handleResponse 함수를 호출하는 event를 예약한다. handleResponse 함수는 내부의 FIFO에서부터 첫번째 response를 읽고, NCQ에 써서 Interface::updateInterrupt 함수를 사용해 Interrupt를 보낸다.
Device
  • hil/sata/device.hh 에 선언된 SimpleSSD::HIL::SATA::Device는 HBA와 연결하는 Device이다.
  • Device는 아래 나열된 ATA 명령어(ATA/ATAPI Command Set -2 (ACS-2) 와 (AT Attachment 8 - ATA/ATAPI Command Set (ATA8-ACS))를 다룰수 있다.
    • FLUSH CACHE
    • FLUSH CACHE EXT
    • IDENTIFY DEVICE
    • READ DMA
    • READ DMA EXT
    • READ FPDMA QUEUED
    • READ SECTOR
    • READ SECTOR EXT
    • READ VERIFY SECTOR
    • READ VERIFY SECTOR EXT
    • SET FEATURE
    • WRITE DMA
    • WRTIE DMA EXT
    • WRITE FPDMA QUEUED
    • WRTIE SECTOR
    • WRTIE SECTOR EXT
  • Device 의 구현이 NVMe Subsystem에 비해 간단해 보일수 있는데, 이는 Namespace를 관리할 필요가 없기 때문이다.
  • 모든 명령어들은 Device::submitCommand 함수를 통과하며 적절하게 다루어진다.
  • 완료 이후, 각 명령어들은 명령의 결과를 HBA에게 보고하기(report) 위해서 HBA::submitFIS를 호출한다.
  • I/O 와 관련된 READ*, WRITE* and FLUSH* 는 SSD Interface의 함수 (HIL::HIL::read, write and flush) 를 호출한다.

Universal Flash Storage

  • Universal Flash Storage - UFS : 모바일용 저장소를 위해 디자인된 interface. 대부분의 스마트폰들이 이 UFS interface를 사용하고 있다.
  • SimpleSSD 에서는 Universal Flash Storage (UFS) Host Controller Interface (JESD223)을 기반으로 UFS Host Controller를 구현하였으며, UFS PHY는 Specification for M-PHY Version 4.0 을, UFS protocol은 Universal Flash Storage (UFS) Version 2.1 (JESD220C)를 기반으로 한다.
Host Interface
  • SimpleSSD::HIL::UFS::Interface 는 simulator에게 common API를 제공하기 위해 추상 클래스로 hil/ufs/interface.hh 에 정의되어 있다. 어떻게 구체화 하는지는 SimpleSSD_FullSystem의 UFSInterfacescr/dev/storage/ufs_interface.hh 에 나와 있다.
  • SimpleSSD::HIL::UFS::Interface 는 단지 generateInterrrupt()clearInterrupt() 를 포함한다.
Host Controller inteface
  • SATA처럼 UFS도 UFS Host Controller Interface - UFSHCI 를 제공한다. 이 인터페이스는 Universal Flash Storage (UFS) Host Controller Interface (JESD223)를 따라간다.
  • UFSHCI 는 hil/ufs/host.hhSimpleSSD::HIL::UFS::Host로 구현되어 있다.

  • Interface 는 UFSHCI register에 Host::writeRegister 함수를 사용해서 쓴다.
  • UFS는 3가지 종류의 명령이 있는데
    • UFS Trasnport Protocol Transfer (UTP Transfer)
    • UFS Transport Protocol Task Management (UTP Task)
    • UFS InterConnect Command (UIC Command)
  • 각 명령어들은 request를 알리는데 서로 다른 doorbell을 사용하며
    • REG_UTRLDBR 는 UTP Transfer
    • REG_UTMRLDBR 는 UTP Task
    • REG_UICCMDR 는 UIC Command
  • 를 확인할때 사용할수 있다.

  • 리눅스 커널과 UFS 2.1에서 UTP Task는 구현되지 않았기 때문에 SimpleSSD 에서도 구현하지 않았다.
  • UIC Commands 는 오직 UFS hardware를 초기화 할때만 사용하며(M-PHY link 부팅 등), 그러므로 우린 오직 2가지 기초적 명령어들만 구현했다.
  • NVMe와 SATA처럼 Event Engine 은 주기적으로 Host::work 함수를 호출하며. Host::handleRequest 함수는 work에 의해서 호출되고 request가 유효한지를 검증한다. Host::processUTPCommand 는 request를 파싱하고, request에 적합한 handler를 부른다.

  • UFS Transfer는 3가지 유형의 명령어들로 정의된다.
    • Command (SCSI protocol)
    • Native UFS Command
    • Device Management Command
  • Native UFS Command는 UFS 2.1 에서 정의되지 않았기 때문에 우린 Command 와 Device Management Command 만을 구현했다.

  • Device::processCommand 함수는 Command 를 다루고 Device::processQueryCommand 함수는 Device Management Command 를 다룬다.
  • handleRequest 함수에서 완료 루틴(Completion routine) 이 선언되어 있으며 이는 doRequestdoWrite 라는 lambda 함수들을 확인하면 된다. doWrite 함수는 completion routine(Host::completion)을 예약하며, completion 함수는 Interface::updateInterrupt 함수를 사용해서 Interrupt를 보낸다.
Device
  • UFSHCI 와 연결해주는 Device 는 hil/ufs/device.hhSimpleSSD::HIL::UFS::Device 를 정의한다.
  • UFS 는 SCSI commands set(SCSI Block Commands - 3 (SBC-3)SCSI Primary Commands - 4(SPC-4)) 을 I/O 하기 위해서 사용gksek.
  • Device 는 아래 나오는 SCSI 명령어들로 다루어진다.
    • INQUERY
    • MODE SELECT 10
    • MODE SENSE 10
    • READ 6
    • READ 10
    • READ CAPACITY 10
    • READ CAPACITY 16
    • START STOP UNIT
    • TEST UNIT READY
    • REPORT LUNS
    • VERIFY 10
    • WRITE 6
    • WRITE 10
    • SYNCHRONIZE CACHE 10
  • Device 의 구현은 SATA와 비슷하지만 2가지 명령어 handling 함수를 가진다. (processCommandprocessQueryCommand)
  • 완료 이후 각 SCI command 함수는 callback handler 를 호출하며, 이때 processCommand 함수의 인자로서 콜백은 제공한다.
  • processQueryCommand 는 즉시 결과를 리턴한다. I/O 관련 명령 READ*, WRITE*SYNCHRONIZE CACHE 는 SSD Interface 함수들 (HIL::HIL::read, write and flush) 를 호출한다.

nvme 소스코드와의 비교

  • [[nvme]] 페이지에 나름 정리한걸 올려놨다.
  • driver 부분을 주로 비교하고 나머진 아직 openssd 를 읽으면서 해야할듯.

프로그램 시작점

  • simplessd에서 sim/main.cc 파일의 220번째 줄을 보면 RequestGenerator 가 있다. 여기서 submitIO() 가 bio를 호출 (실제로는 시뮬레이션이지만, 편의상)하고, iocallback()이 주기적으로 rescheduleSubmit() 를 호출하면서 IO를 발생시킨다.
  • 여기서는 bil/interface.hhDriverInterface 를 만들어 두어서 BIO layer를 구현해둔듯 하다. scheduler는 단순히 DriverInterfacesubmitIO를 호출하는게 전부임.

BIO 발생 -> 드라이버

request로부터 opcode 분리
  • linux 의 drivers/nvme/host/core.cnvme_queue_rq()에서 호출하는 nvme_setup_cmd() 와 SimpleSSD의 sil/nvme/nvme.ccDriver::submitIO() 가 서로 같은 일을 하고 있음.
  • 명령어의 종류에 따라 (READ, WRITE, TRIM(DISCARD), FLUSH) 기본적인 setup과정을 호출해준다.
    SimpleSSD 구현

    ```cpp void Driver::submitIO(BIL::BIO &bio) { uint32_t cmd[16]; PRP *prp = nullptr; static ResponseHandler callback = this { _io(status, context); };

memset(cmd, 0, 64);

uint64_t slba = bio.offset / LBAsize; uint32_t nlb = (uint32_t)DIVCEIL(bio.length, LBAsize);

cmd[1] = namespaceID; // NSID

if (bio.type == BIL::BIO_READ) { cmd[0] = SimpleSSD::HIL::NVMe::OPCODE_READ; // CID, FUSE, OPC cmd[10] = (uint32_t)slba; cmd[11] = slba » 32; cmd[12] = nlb - 1; // LR, FUA, PRINFO, NLB

prp = new PRP(bio.length);
prp->getPointer(*(uint64_t *)(cmd + 6), *(uint64_t *)(cmd + 8));  // DPTR   }   else if (bio.type == BIL::BIO_WRITE) {
cmd[0] = SimpleSSD::HIL::NVMe::OPCODE_WRITE;  // CID, FUSE, OPC
cmd[10] = (uint32_t)slba;
cmd[11] = slba >> 32;
cmd[12] = nlb - 1;  // LR, FUA, PRINFO, DTYPE, NLB

prp = new PRP(bio.length);
prp->getPointer(*(uint64_t *)(cmd + 6), *(uint64_t *)(cmd + 8));  // DPTR   }   else if (bio.type == BIL::BIO_FLUSH) {
cmd[0] = SimpleSSD::HIL::NVMe::OPCODE_FLUSH;  // CID, FUSE, OPC   }   else if (bio.type == BIL::BIO_TRIM) {
cmd[0] = SimpleSSD::HIL::NVMe::OPCODE_DATASET_MANAGEMEMT;  // CID, FUSE, OPC
cmd[10] = 0;                                               // NR
cmd[11] = 0x04;                                            // AD

prp = new PRP(16);
prp->getPointer(*(uint64_t *)(cmd + 6), *(uint64_t *)(cmd + 8));  // DPTR

// Fill range definition
uint8_t data[16];

memset(data, 0, 16);
memcpy(data + 4, &nlb, 4);
memcpy(data + 8, &slba, 8);

prp->writeData(0, 16, data);   }

submitCommand(1, (uint8_t *)cmd, callback, new IOWrapper(bio.id, prp, bio.callback)); }


###### nvme(driver) 구현
```c
blk_status_t nvme_setup_cmd(struct nvme_ns *ns, struct request *req,
		struct nvme_command *cmd)
{
	blk_status_t ret = BLK_STS_OK;

	nvme_clear_nvme_request(req);

	memset(cmd, 0, sizeof(*cmd));
	switch (req_op(req)) {
	case REQ_OP_DRV_IN:
	case REQ_OP_DRV_OUT:
		memcpy(cmd, nvme_req(req)->cmd, sizeof(*cmd));
		break;
	case REQ_OP_FLUSH:
		nvme_setup_flush(ns, cmd);
		break;
	case REQ_OP_WRITE_ZEROES:
		ret = nvme_setup_write_zeroes(ns, req, cmd);
		break;
	case REQ_OP_DISCARD:
		ret = nvme_setup_discard(ns, req, cmd);
		break;
	case REQ_OP_READ:
	case REQ_OP_WRITE:
		ret = nvme_setup_rw(ns, req, cmd);
		break;
	default:
		WARN_ON_ONCE(1);
		return BLK_STS_IOERR;
	}

	cmd->common.command_id = req->tag;
	trace_nvme_setup_cmd(req, cmd);
	return ret;
}
submit command
  • nvme에서는 struct blk_mq_hw_ctx *hctx 를 parameter로 넣어주므로써 자연스럽게 nvme queue를 admin queue와 submission queue를 처리하는데, SimpleSSD 의 경우에는 uint16_t iv 라는 값을 전달하므로써 이를 처리한다.
  • 궁금점은 nvme에서는 spin_lock 을 걸어서 복사하는 과정에서 또다른 bio가 오는 것을 생각하고 있는 듯한 구현인데, SimpleSSD 의 경우에는 따로 lock을 거는 게 없다. 단일 쓰레드에서만 도는 걸 생각하는건가? 여기는 잘 모르겠다.
    SimpleSSD 구현

    ```cpp void Driver::submitCommand(uint16_t iv, uint8_t *cmd, ResponseHandler &func, void *context) { uint16_t cid = 0; uint16_t opcode = cmd[0]; uint16_t tail = 0; uint64_t tick = engine.getCurrentTick(); Queue *queue = nullptr;

// Push to queue if (iv == 0) { increaseCommandID(adminCommandID); cid = adminCommandID; queue = adminSQ; } else if (iv == 1 && ioSQ) { increaseCommandID(ioCommandID); cid = ioCommandID; queue = ioSQ; } else { SimpleSSD::panic(“I/O Submission Queue is not initialized”); }

memcpy(cmd + 2, &cid, 2); queue->setData(cmd, 64); tail = queue->getTail();

// Push to pending cmd list pendingCommandList.push_back(CommandEntry(iv, opcode, cid, context, func));

// Ring doorbell pController->ringSQTailDoorbell(iv, tail, tick); queue->incrHead(); }

##### nvme구현
```c
/**
 * nvme_submit_cmd() - Copy a command into a queue and ring the doorbell
 * @nvmeq: The queue to use
 * @cmd: The command to send
 * @write_sq: whether to write to the SQ doorbell
 */
static void nvme_submit_cmd(struct nvme_queue *nvmeq, struct nvme_command *cmd,
			    bool write_sq)
{
	spin_lock(&nvmeq->sq_lock);
	memcpy(nvmeq->sq_cmds + (nvmeq->sq_tail << nvmeq->sqes),
	       cmd, sizeof(*cmd));
	if (++nvmeq->sq_tail == nvmeq->q_depth)
		nvmeq->sq_tail = 0;
	if (write_sq)
		nvme_write_sq_db(nvmeq);
	spin_unlock(&nvmeq->sq_lock);
}

Work

SimpleSSD
  • simplessd에선 Driver::submitCommand() 에서 pendingCommandListCommandEntry를 넣은 뒤, pController->ringSQTailDoorbell 를 해주는데, 이를 따라가보면, 인자로 넣어준 SQ의 tail을 이동시키는게 전부다…?
  • 여기서부터 순간 방향성을 잃었는데, SQ 넣고 어떻게 되지? 싶었다. 한번 SQ에 엑세스 하는걸 봐보자
  • 그러면 Controller::work() 가 나오게 된다. 이 함수는 workEvent 라는 변수에 담겨서 Controller::writeRegister()에서 schedule에 등록된다. 이는 Driver::init() 에서 Step 5. 에서 일어난다.
  • 따라서 SimpleSSD 는 workInterval (기본값 : 50000, 50ns) 마다 SQ를 검사해서 처리하는 방식이다.
    nvme(driver)
  • nvme_write_sq_db() 를 호출하는데, 이는 결국 writel()를 호출해서 SQ tail 주소에 command를 쓰는 구조이다.
  • 그렇다면 nvme에서 command는 어떻게 되는가? : TODO: 흠…. 아직 잘 모르겠는데? nvme_scan_work() 가 있긴한데, 이게 user 가 scan work를 강제로 SSD에 시키는 건지가 모르겠는데, 근데 그런 구조면, 글러먹은게 IO 연산을 하기 위해서 직접 다 해줘야되는건데? 그러면 굳이 scheduler가 linux kernel level에 존재할 필요가 없는데? 그냥 scheduler layer 없이 SSD scan work 를 조절하면 되는데? 일단 추정은 ssd 내부에 존재하는건데, 이건 OpenChannelSSD 를 읽어보고 알아내야할듯.

  • 흐음…. 이건 잘 못찾았다. Controller 내부에 있다고 가정하고 계속 읽어 나가야할듯

SSD Interface

  • HIL 의 SSD Interface는 단순한데 hil/hil.hhSimpleSSD::HIL::HIL 로 정의되어 있다.
  • I/O request를 host controller로 부터 받아 Interal Cache Layer 에 넘겨준다.

Internal Cache Layer

  • 여기서는 I/O buffer model (data cache)인 Internal Cache Layer (이하 ICL) 을 알아본다.
  • ICL은 추상 클래스로 되어 있어서 상속 받아서 구현해볼수 있고 기본적으로는 set-associative cache 로 구현된 Generic Cache를 확인한다.

Abstract Class

  • icl/abstract_cache.hhAbstractCache 클래스로 선언되어 있으며 5가지 가상함수가 존재한다.(Statobject에서 파생된 3가지 함수들도 추가로 있다.)
  • ICL 은 각 가상함수로부터 I/O requests와 고유한 알고리즘에 의해서 bffer /IIO 를 가진다. data eviction이 필요하거나, flush request가 있을때 I/O를 FTL 에 넘긴다.
  • SSD의 Data buffer는 느린 NAND I/O 성능을 감추기 위해서 매우 중요하다. buffer algorithm의 작은 변화도 성능을 크게 바꿀수 있다.

Set-Associative Cache

  • icl/generic_cache.hhSimpleSSD::ICL::GenericCache 라고 선언된 set-associative cache를 제공하며, 아래 후술될 인자를 조정할수 있다.
    • CacheSize : Buffer Capacity.
    • CacheWaySize : Set associativity of cache. set size will automatically caclculated
    • EnableReadCache: Enable read data caching.
    • EnableReadPrefetch: enable read-ahead and prefetch.
    • ReadPrefetchMode: Specify how many data should be read-ahead/prefetch.
    • ReadPrefetchCount : Threshold for read-ahead/prefetch(# of sequential I/O).
    • ReadPrefetchRatio : Threshold for read-ahead/prefetch(data size of sequential I/O).
    • EnableWriteCache : enable write data caching.
    • EvictPolicy : Specify which algorithm to use to select victim cache line.
    • EvictMode : Specify how many data should be evicted when cache is full.
    • CacheLatency : Set cache metadata accesss latency.

호출구조

  • hil/nvme/controller.cc 에서 Controller::work()를 확인해보면 DMAContext 를 만들어주고 호출하는 걸 알수 있다.
  • 여기서 checkQueue() 를 호출하면서 DMAFunction 타입인 func를 호출하게된다. 이때 funcdoQueue 이고 이는 결국 collectSQueue의 인자로 주어졌던 CPUContext가 가지고 있는 함수를 호출한다. 이때 이건 결국 handleRequest를 호출하게 된다. 이는 SubSystem 에 request를 전달하게 된다.
  • 결론은 결국 request는 처리되서 subsystem에 도달하게 되고, 이게 어떻게 처리되는지는 Subsystem::submitCommand()를 확인하면 알수 있다. 여기서 admin command 들을 처리하고, 아닌 것들은 그대로 Namespace::submitCommand() 로 넘어오게 된다. ```cpp void Namespace::submitCommand(SQEntryWrapper &req, RequestFunction &func) { /* Skip */ // NVM commands else { switch (req.entry.dword0.opcode) { case OPCODE_FLUSH: flush(req, func); break; case OPCODE_WRITE: write(req, func); break; case OPCODE_READ: read(req, func); break; case OPCODE_COMPARE: compare(req, func); break; case OPCODE_DATASET_MANAGEMEMT: datasetManagement(req, func); break; default: resp.makeStatus(true, false, TYPE_GENERIC_COMMAND_STATUS, STATUS_INVALID_OPCODE);

       response = true;
    
       break;    }  }   }
    

if (response) { func(resp); } }


 * 이때 `read` 함수등 은 다시 `Subsystem::read()`를 호출하고 이 내부는
```cpp
void Subsystem::read(Namespace *ns, uint64_t slba, uint64_t nlblk,
                     DMAFunction &func, void *context) {
  Request *req = new Request(func, context);
  DMAFunction doRead = [this](uint64_t, void *context) {
    auto req = (Request *)context;

    pHIL->read(*req);

    delete req;
  };

  convertUnit(ns, slba, nlblk, *req);

  execute(CPU::NVME__SUBSYSTEM, CPU::CONVERT_UNIT, doRead, req);
}
  • HIL::read() 를 부르게 된다. 이렇게 불리게 된 HIL::read() 의 내부에서 ICL::read() 를 부르는 구조이다.
  • read 부분만 봤는데, 다른 부분도 비슷할거라고 생각한다.

Cache 로직 공부

  • 결국 GenericCache::read()(읽기의 경우) 가 호출되게 되는데 그 과정을 적어보자 ```cpp /* return 값이 true 면 hit, 아니면 miss 이다. */ bool GenericCache::read(Request &req, uint64_t &tick) { bool ret = false;

debugprint(LOG_ICL_GENERIC_CACHE, “READ | REQ %7u-%-4u | LCA %” PRIu64 “ | SIZE %” PRIu64, req.reqID, req.reqSubID, req.range.slpn, req.length);

if (useReadCaching) { /* 읽기용 Cache가 있는지를 확인한다. / / start logical page number ? 의 약자인듯 / uint32_t setIdx = calcSetIndex(req.range.slpn); / Set-Associative Cache 를 참조 */ uint32_t wayIdx; uint64_t arrived = tick;

/* 이건 먼지 모르겠다. 연속적인 request를 체크하는건가 ?
 * 찾아보니 prefetch 와 관련된 건데, 언제나 predict 를 sequential 이라고 생각하고 하는건가?
 * 물론 prefetch 할 영역을 고르는데 어려운 알고리즘을 쓰면 문제가 많겠지만 Sequential 만 prefetch 하는게
 * 좋다라는 논문이 있나?
 * TODO: 논문 리딩
*/
if (useReadPrefetch) {
  checkSequential(req, readDetect);
}

wayIdx = getValidWay(req.range.slpn, tick);

// Do we have valid data?
if (wayIdx != waySize) {
  uint64_t tickBackup = tick;

  // Wait cache to be valid
  if (tick < cacheData[setIdx][wayIdx].insertedAt) {
    tick = cacheData[setIdx][wayIdx].insertedAt;
  }

  // Update last accessed time
  cacheData[setIdx][wayIdx].lastAccessed = tick;

  // DRAM access
  pDRAM->read(&cacheData[setIdx][wayIdx], req.length, tick);

  debugprint(LOG_ICL_GENERIC_CACHE,
             "READ  | Cache hit at (%u, %u) | %" PRIu64 " - %" PRIu64
             " (%" PRIu64 ")",
             setIdx, wayIdx, arrived, tick, tick - arrived);

  ret = true;

  // Do we need to prefetch data?
  if (useReadPrefetch && req.range.slpn == prefetchTrigger) {
    debugprint(LOG_ICL_GENERIC_CACHE, "READ  | Prefetch triggered");

    req.range.slpn = lastPrefetched;

    // Backup tick
    arrived = tick;
    tick = tickBackup;

    goto ICL_GENERIC_CACHE_READ;
  }
}
// We should read data from NVM
else {
ICL_GENERIC_CACHE_READ:
  FTL::Request reqInternal(lineCountInSuperPage, req);
  std::vector<std::pair<uint64_t, uint64_t>> readList;
  uint32_t row, col;  // Variable for I/O position (IOFlag)
  uint64_t dramAt;
  uint64_t beginLCA, endLCA;
  uint64_t beginAt, finishedAt = tick;

  if (readDetect.enabled) {
    // TEMP: Disable DRAM calculation for prevent conflict
    pDRAM->setScheduling(false);

    if (!ret) {
      debugprint(LOG_ICL_GENERIC_CACHE, "READ  | Read ahead triggered");
    }

    beginLCA = req.range.slpn;

    // If super-page is disabled, just read all pages from all planes
    if (prefetchMode == MODE_ALL || !bSuperPage) {
      endLCA = beginLCA + lineCountInMaxIO;
      prefetchTrigger = beginLCA + lineCountInMaxIO / 2;
    }
    else {
      endLCA = beginLCA + lineCountInSuperPage;
      prefetchTrigger = beginLCA + lineCountInSuperPage / 2;
    }

    lastPrefetched = endLCA;
  }
  else {
    beginLCA = req.range.slpn;
    endLCA = beginLCA + 1;
  }

  for (uint64_t lca = beginLCA; lca < endLCA; lca++) {
    beginAt = tick;

    // Check cache
    if (getValidWay(lca, beginAt) != waySize) {
      continue;
    }

    // Find way to write data read from NVM
    setIdx = calcSetIndex(lca);
    wayIdx = getEmptyWay(setIdx, beginAt);

    if (wayIdx == waySize) {
      wayIdx = evictFunction(setIdx, beginAt);

      if (cacheData[setIdx][wayIdx].dirty) {
        // We need to evict data before write
        calcIOPosition(cacheData[setIdx][wayIdx].tag, row, col);
        evictData[row][col] = cacheData[setIdx] + wayIdx;
      }
    }

    cacheData[setIdx][wayIdx].insertedAt = beginAt;
    cacheData[setIdx][wayIdx].lastAccessed = beginAt;
    cacheData[setIdx][wayIdx].valid = true;
    cacheData[setIdx][wayIdx].dirty = false;

    readList.push_back({lca, ((uint64_t)setIdx << 32) | wayIdx});

    finishedAt = MAX(finishedAt, beginAt);
  }

  tick = finishedAt;

  evictCache(tick);

  for (auto &iter : readList) {
    Line *pLine = &cacheData[iter.second >> 32][iter.second & 0xFFFFFFFF];

    // Read data
    reqInternal.lpn = iter.first / lineCountInSuperPage;
    reqInternal.ioFlag.reset();
    reqInternal.ioFlag.set(iter.first % lineCountInSuperPage);

    beginAt = tick;  // Ignore cache metadata access

    // If superPageSizeData is true, read first LPN only
    pFTL->read(reqInternal, beginAt);

    // DRAM delay
    dramAt = pLine->insertedAt;
    pDRAM->write(pLine, lineSize, dramAt);

    // Set cache data
    beginAt = MAX(beginAt, dramAt);

    pLine->insertedAt = beginAt;
    pLine->lastAccessed = beginAt;
    pLine->tag = iter.first;

    if (pLine->tag == req.range.slpn) {
      finishedAt = beginAt;
    }

    debugprint(LOG_ICL_GENERIC_CACHE,
               "READ  | Cache miss at (%u, %u) | %" PRIu64 " - %" PRIu64
               " (%" PRIu64 ")",
               iter.second >> 32, iter.second & 0xFFFFFFFF, tick, beginAt,
               beginAt - tick);
  }

  tick = finishedAt;

  if (readDetect.enabled) {
    if (ret) {
      // This request was prefetch
      debugprint(LOG_ICL_GENERIC_CACHE, "READ  | Prefetch done");

      // Restore tick
      tick = arrived;
    }
    else {
      debugprint(LOG_ICL_GENERIC_CACHE, "READ  | Read ahead done");
    }

    // TEMP: Restore
    pDRAM->setScheduling(true);
  }
}

tick += applyLatency(CPU::ICL__GENERIC_CACHE, CPU::READ);   }   else {
FTL::Request reqInternal(lineCountInSuperPage, req);

pDRAM->write(nullptr, req.length, tick);

pFTL->read(reqInternal, tick);   }

stat.request[0]++;

if (ret) { stat.cache[0]++; }

return ret; }

```