Admin SQ/CQ 생성
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() 함수의 일반적인 단계는 다음과 같다는 것을 알 수 있습니다. 단계별로 살펴 봅시다.
- CAP 레지스터에서 Subsystem Reset 지원 여부 판단
- nvme_disable_ctrl() 함수 호출
- nvme_alloc_queue() 함수 호출
- nvme_enable_ctrl() 함수 호출
- 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입니다.
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에 대한 자세한 내용은 다음을 참조하십시오.
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를 나타냅니다.
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를 다음과 같이 정의합니다.
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);
}