Admin SQ/CQ 생성

이전 글에서는 nvme_pci_enable() 함수의 내용을 분석해 보았고, 이번에는 nvme_reset_work() 함수의 다른 기능들을 소개하도록 하겠습니다.

nvme_reset_work() 함수의 주요 작업은 다음과 같이 요약할 수 있습니다.

ⓐ nvme_reset_work() 함수를 입력한 후, NVME_CTRL_RESETTING 플래그를 먼저 확인하여 nvme_reset_work() 함수가 반복적으로 호출되지 않도록 합니다.

ⓑ nvme_pci_enable() 함수 호출

ⓒ nvme_configure_admin_queue() 함수 호출

ⓓ nvme_init_queue() 함수 호출

ⓔ nvme_alloc_admin_tags() 함수 호출

ⓕ nvme_init_identify() 함수 호출

ⓖ nvme_setup_io_queues() 함수 호출

ⓗ nvme_start_queues() / nvme_dev_add() 함수 호출 후 nvme_queue_scan() 함수 호출

ⓒ nvme_configure_admin_queue() 함수

nvme_configure_admin_queue() 함수의 일반적인 단계는 다음과 같다는 것을 알 수 있습니다. 단계별로 살펴 봅시다.

  1. CAP 레지스터에서 Subsystem Reset 지원 여부 판단
  2. nvme_disable_ctrl() 함수 호출
  3. nvme_alloc_queue() 함수 호출
  4. nvme_enable_ctrl() 함수 호출
  5. queue_request_irq() 함수 호출
static int nvme_configure_admin_queue(struct nvme_dev *dev)
{
	int result;
	u32 aqa;

	//컨트롤러의 CAP 레지스터 내용 가져오기 및 Subsystem Reset 기능 지원 여부 판단
	u64 cap = lo_hi_readq(dev->bar + NVME_REG_CAP);
	struct nvme_queue *nvmeq;
	dev->subsystem = readl(dev->bar + NVME_REG_VS) >= NVME_VS(1, 1, 0) ?
						NVME_CAP_NSSRC(cap) : 0;

	if (dev->subsystem &&
	    (readl(dev->bar + NVME_REG_CSTS) & NVME_CSTS_NSSRO))
		writel(NVME_CSTS_NSSRO, dev->bar + NVME_REG_CSTS);

	result = nvme_disable_ctrl(&dev->ctrl, cap);
	if (result < 0)
		return result;

	nvmeq = dev->queues[0];
	if (!nvmeq) {
		nvmeq = nvme_alloc_queue(dev, 0, NVME_AQ_DEPTH);
		if (!nvmeq)
			return -ENOMEM;
	}

	aqa = nvmeq->q_depth - 1;
	aqa |= aqa << 16;
	writel(aqa, dev->bar + NVME_REG_AQA);
	lo_hi_writeq(nvmeq->sq_dma_addr, dev->bar + NVME_REG_ASQ);
	lo_hi_writeq(nvmeq->cq_dma_addr, dev->bar + NVME_REG_ACQ);

	result = nvme_enable_ctrl(&dev->ctrl, cap);
	if (result)
		return result;

	nvmeq->cq_vector = 0;
	result = queue_request_irq(nvmeq);
	if (result) {
		nvmeq->cq_vector = -1;
		return result;
	}

	return result;
}

1. NVM Subsystem Reset 지원 여부 판단

먼저 컨트롤러의 CAP 레지스터 비트[36]는 아래 그림과 같이 NVM Subsystem Reset 지원 여부를 정의합니다. 일반적으로 NVM Subsystem은 컨트롤러, NAND 및 NVM Subsystem을 구성하는 인터페이스로 구성된 SSD입니다. img

NVMe Sepc.에 정의된 Controller-level Reset에는 5가지 주요 방법이 있습니다 .

  • NVM Subsystem Reset
  • Conventional Reset (PCI Express Hot, Warm, or Cold reset)
  • PCI Express transaction layer Data Link Down status
  • Function Level Reset (PCI reset)
  • Controller Reset (CC.EN transitions from ‘1’ to ‘0’)

물론 NVMe는 Controller-level Reset 정의 외에 비교적 간단한 Queue-level Reset도 정의하고 있습니다.

위 Reset에 대한 자세한 내용은 다음을 참조하십시오.

PCIe 이야기 #17: PCIe 시스템 Reset 모드

2. nvme_disable_ctrl() 함수

NVMe 컨트롤러를 작동할 때 nvme_disable_ctrl() 함수를 통해 장치를 비활성화한 다음 다시 nvme_enable_ctrl() 함수를 호출하여 장치를 활성화해야 합니다.

int nvme_disable_ctrl(struct nvme_ctrl *ctrl, u64 cap)
{
	int ret;

	ctrl->ctrl_config &= ~NVME_CC_SHN_MASK;
	ctrl->ctrl_config &= ~NVME_CC_ENABLE;

	ret = ctrl->ops->reg_write32(ctrl, NVME_REG_CC, ctrl->ctrl_config);
	if (ret)
		return ret;

	if (ctrl->quirks & NVME_QUIRK_DELAY_BEFORE_CHK_RDY)
		msleep(NVME_QUIRK_DELAY_AMOUNT);

	return nvme_wait_ready(ctrl, cap, false);
}

여기서 ctrl->ops는 이전 nvme_probe() 함수에서 nvme_init_ctrl() 함수 호출 중에 전달된 nvme_pci_ctrl_ops 구조체 입니다. reg_write32() 함수는 NVME_REG_CC 레지스터를 통해 장치를 비활성화합니다.

이후, nvme_wait_ready() 함수가 상태 레지스터 NVME_REG_CSTS를 읽어 장치가 실제로 정지할 때까지 기다립니다. 타임아웃 상한은 CAP 레지스터의 Bit[31:24]의 Timeout 필드에 따라 계산되며 각 단위는 500ms를 나타냅니다.

img

static int nvme_wait_ready(struct nvme_ctrl *ctrl, u64 cap, bool enabled)
{
	unsigned long timeout =
		((NVME_CAP_TIMEOUT(cap) + 1) * HZ / 2) + jiffies;
	u32 csts, bit = enabled ? NVME_CSTS_RDY : 0;
	int ret;

	while ((ret = ctrl->ops->reg_read32(ctrl, NVME_REG_CSTS, &csts)) == 0) {
		if (csts == ~0)
			return -ENODEV;
		if ((csts & NVME_CSTS_RDY) == bit)
			break;

		msleep(100);
		if (fatal_signal_pending(current))
			return -EINTR;
		if (time_after(jiffies, timeout)) {
			dev_err(ctrl->device,
				"Device not ready; aborting %s\n", enabled ?
						"initialisation" : "reset");
			return -ENODEV;
		}
	}

	return ret;
}

3. nvme_alloc_queue() 함수

장치가 비활성화된 후 처음으로 nvmeq를 접근하면 이때 값은 Null입니다. nvme_alloc_queue() 함수를 호출하여 NVMe 대기열을 할당해야 합니다.

static struct nvme_queue *nvme_alloc_queue(struct nvme_dev *dev, int qid,
							int depth)
{
	struct nvme_queue *nvmeq = kzalloc(sizeof(*nvmeq), GFP_KERNEL);
	if (!nvmeq)
		return NULL;

	/* ① */
	nvmeq->cqes = dma_zalloc_coherent(dev->dev, CQ_SIZE(depth),
					  &nvmeq->cq_dma_addr, GFP_KERNEL);
	if (!nvmeq->cqes)
		goto free_nvmeq;

	/* ② */
	if (nvme_alloc_sq_cmds(dev, nvmeq, qid, depth))
		goto free_cqdma;

	nvmeq->q_dmadev = dev->dev;
	nvmeq->dev = dev;
	/* ③ */
	snprintf(nvmeq->irqname, sizeof(nvmeq->irqname), "nvme%dq%d",
			dev->ctrl.instance, qid);
	spin_lock_init(&nvmeq->q_lock);
	nvmeq->cq_head = 0;
	nvmeq->cq_phase = 1;
	nvmeq->q_db = &dev->dbs[qid * 2 * dev->db_stride];
	nvmeq->q_depth = depth;
	nvmeq->qid = qid;
	nvmeq->cq_vector = -1;
	dev->queues[qid] = nvmeq;
	dev->queue_count++;

	return nvmeq;

 free_cqdma:
	dma_free_coherent(dev->dev, CQ_SIZE(depth), (void *)nvmeq->cqes,
							nvmeq->cq_dma_addr);
 free_nvmeq:
	kfree(nvmeq);
	return NULL;
}

① 먼저 dma_zalloc_coherent() 함수를 호출하여 DMA 사용을 위한 완료 대기열용(nvme_completion 구조체) 메모리를 할당합니다. nvmeq->cqes는 커널에서 사용하기 위해 요청된 메모리의 가상 주소입니다. 그리고 nvmeq->cq_dma_addr은 DMA 컨트롤러에서 사용하기 위한 이 메모리의 물리적 주소입니다.

② 다음으로 nvme_alloc_sq_cmds() 함수를 호출하여 제출 대기열을 처리합니다. NVMe Spec. 버전이 1.2 이상이고 CMB가 제출 큐를 지원하는 경우 CMB를 사용하십시오. 그렇지 않으면 dma_alloc_coherent() 함수를 사용하여 완료 대기열과 같은 메모리를 할당합니다.

③ 이후 nvmeq->irqname은 등록이 중단되었을 때의 이름을 나타냅니다. nvme%dq%d에서 볼 수 있듯이 마지막으로 생성된 nvme0q0 및 nvme0q1이며 하나는 admin 대기열용이고 하나는 I/O 대기열용임을 알 수 있습니다.

nvme_alloc_queue() 함수가 NVMe 대기열을 할당한 후, NVMe Admin 대기열의 속성과 할당된 Admin SQ/CQ 메모리 주소를 레지스터에 기록해야 합니다.

writel(aqa, dev->bar + NVME_REG_AQA);
lo_hi_writeq(nvmeq->sq_dma_addr, dev->bar + NVME_REG_ASQ);
lo_hi_writeq(nvmeq->cq_dma_addr, dev->bar + NVME_REG_ACQ);

NVMe Spec.은 AQA, ASQ 및 ACQ를 다음과 같이 정의합니다.

img

img

img

4. nvme_enable_ctrl() 함수

Admin 대기열에 메모리를 할당한 후, nvme_enable_ctrl() 함수를 호출하여 장치를 활성화합니다. 이 함수는 nvme_disable_ctrl() 함수와 유사하나 과정이 역순이므로 넘어갑니다.

5. queue_request_irq() 함수

nvme_configure_admin_queue() 함수의 마지막 단계는 queue_request_irq() 함수입니다. 이 기능의 주된 역할은 인터럽트 처리 기능을 설정하는 것입니다. 기본적으로 IRQ 쓰레드를 사용하지 않고 인터럽트 컨텍스트의 인터럽트 처리를 사용합니다.

static int queue_request_irq(struct nvme_queue *nvmeq)
{
	if (use_threaded_interrupts)
		return request_threaded_irq(nvmeq_irq(nvmeq), nvme_irq_check,
				nvme_irq, IRQF_SHARED, nvmeq->irqname, nvmeq);
	else
		return request_irq(nvmeq_irq(nvmeq), nvme_irq, IRQF_SHARED,
				nvmeq->irqname, nvmeq);
}