nvme_reset_work() 함수 분석

nvme_probe() 함수의 마지막 단계에서 nvme_reset_work() 함수를 호출하여 reset 작업을 수행합니다. 먼저 nvme_reset_work() 함수의 코드를 살펴봅니다.

static void nvme_reset_work(struct work_struct *work)
{
	struct nvme_dev *dev = container_of(work, struct nvme_dev, reset_work);

	int result = -ENODEV;

	// nvme_reset_work() 함수가 다시 호출되지 않도록 NVME_CTRL_RESETTING 플래그를 확인합니다.
	if (WARN_ON(dev->ctrl.state == NVME_CTRL_RESETTING))
		goto out;

	// If we're called to reset a live controller first shut it down before moving on.
	if (dev->ctrl.ctrl_config & NVME_CC_ENABLE)
		nvme_dev_disable(dev, false);

	if (!nvme_change_ctrl_state(&dev->ctrl, NVME_CTRL_RESETTING))
		goto out;

	result = nvme_pci_enable(dev);
	if (result)
		goto out;

	result = nvme_configure_admin_queue(dev);
	if (result)
		goto out;

	nvme_init_queue(dev->queues[0], 0);
	result = nvme_alloc_admin_tags(dev);
	if (result)
		goto out;

	result = nvme_init_identify(&dev->ctrl);
	if (result)
		goto out;

	result = nvme_setup_io_queues(dev);
	if (result)
		goto out;

	if (dev->online_queues > 1)
		nvme_queue_async_events(&dev->ctrl);

	mod_timer(&dev->watchdog_timer, round_jiffies(jiffies + HZ));
	// Keep the controller around but remove all namespaces if we don't have any working I/O queue.
	if (dev->online_queues < 2) {
		dev_warn(dev->ctrl.device, "IO queues not created\n");
		nvme_kill_queues(&dev->ctrl);
		nvme_remove_namespaces(&dev->ctrl);
	} else {
		nvme_start_queues(&dev->ctrl);
		nvme_dev_add(dev);
	}

	if (!nvme_change_ctrl_state(&dev->ctrl, NVME_CTRL_LIVE)) {
		dev_warn(dev->ctrl.device, "failed to mark controller live\n");
		goto out;
	}

	if (dev->online_queues > 1)
		nvme_queue_scan(&dev->ctrl);

    return;

 out:
	nvme_remove_dead_ctrl(dev, result);
}

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_pci_enable() 함수

이제 각 단계별로 호출되는 함수를 분석해 봅니다. 먼저 nvme_pci_enable() 함수의 내용을 살펴보십시오.

static int nvme_pci_enable(struct nvme_dev *dev)
{
	u64 cap;
	int result = -ENOMEM;
	struct pci_dev *pdev = to_pci_dev(dev->dev);

	// 이전에 매핑된 BAR 영역인 nvme 장치의 메모리 공간 iomem을 활성화합니다.
	if (pci_enable_device_mem(pdev))
		return result;

	// 디바이스가 버스를 얻을 수 있도록 설정합니다.
	// 즉, 장치가 PCI 버스를 얻을 수 있도록 함수를 호출합니다.
	pci_set_master(pdev);

	// 이 nvme 장치의 DMA 영역 크기를 64비트 또는 32비트로 설정합니다.
	if (dma_set_mask_and_coherent(dev->dev, DMA_BIT_MASK(64)) &&
	    dma_set_mask_and_coherent(dev->dev, DMA_BIT_MASK(32)))
		goto disable;

	// 컨트롤러 레지스터 NVME_REG_CSTS를 읽어 컨트롤러의 상태를 확인합니다.    
	if (readl(dev->bar + NVME_REG_CSTS) == -1) {
		result = -ENODEV;
		goto disable;
	}

	// INITx/MSI/MSI-X 장치에 인터럽트 요청 할당
	result = pci_alloc_irq_vectors(pdev, 1, 1, PCI_IRQ_ALL_TYPES);
	if (result < 0)
		return result;

	// 장치의 64비트 컨트롤러 기능(CAP) 가져오기
	cap = lo_hi_readq(dev->bar + NVME_REG_CAP);

	dev->q_depth = min_t(int, NVME_CAP_MQES(cap) + 1, NVME_Q_DEPTH);
	dev->db_stride = 1 << NVME_CAP_STRIDE(cap);

	// 아래 그림의 컨트롤러 레지스터 정의 테이블에서 SQ0 Tail Doorbell의 시작 주소를 0x1000(=4096) 설정
	dev->dbs = dev->bar + 4096;
	// 다음 단락은 여기서 무시되는 Apple 버그입니다.
	if (pdev->vendor == PCI_VENDOR_ID_APPLE && pdev->device == 0x2001) {
		dev->q_depth = 2;
		dev_warn(dev->dev, "detected Apple NVMe controller, set "
			"queue depth=%u to work around controller resets\n",
			dev->q_depth);
	}

	// nvme 프로토콜의 버전이 1.2 이상인 경우 nvme_map_cmb를 호출해야 합니다.
	// 컨트롤러 메모리 버퍼 매핑 CMB의 주요 기능은 SQ/CQ의 위치를 저장하는 것입니다.
	if (readl(dev->bar + NVME_REG_VS) >= NVME_VS(1, 2, 0)) {
		dev->cmb = nvme_map_cmb(dev);
		if (dev->cmbsz) {
			if (sysfs_add_file_to_group(&dev->ctrl.device->kobj,
						    &dev_attr_cmb.attr, NULL))
				dev_warn(dev->dev,
					 "failed to add sysfs attribute for CMB\n");
		}
	}

	pci_enable_pcie_error_reporting(pdev); 	// 오류 처리
	pci_save_state(pdev); 			// Suspend 전에 장치의 현재 상태를 저장합니다.
	return 0;

 disable:
	pci_disable_device(pdev);
	return result;
}

위의 코드에서 우리는 nvme_pci_enable() 함수가 실행 중에 많은 NVMe 컨트롤러 레지스터를 접근한다는 것을 알수 있습니다(아래 표 참조).

img

pci_alloc_irq_vectors_affinity() 함수

nvme_pci_enable() 함수에서 수행되는 또 다른 중요한 작업은 장치에 인터럽트를 할당하는 것입니다. nvme 장치는 세 가지 인터럽트 모드를 지원합니다. INITx/MSI/MSI-X 이 세 가지 인터럽트 모드의 구체적인 차이점은 NVMe 이야기 #4: Interrupts 메커니즘을 참조하세요. 인터럽트 모드를 할당하는 pci_alloc_irq_vectors() 함수는 실제로 pci_alloc_irq_vectors_affinity() 함수를 호출합니다.

int pci_alloc_irq_vectors_affinity(struct pci_dev *dev, unsigned int min_vecs,
				   unsigned int max_vecs, unsigned int flags,
				   const struct irq_affinity *affd)
{
	static const struct irq_affinity msi_default_affd;
	int vecs = -ENOSPC;

	// Affinity에 대해 IRQ가 자동으로 할당되면 IRQ 인터럽트가 모든 CPU에 할당됩니다.
	// nvme_pci_enable() 함수에서 호출되면, *affd=NULL
	if (flags & PCI_IRQ_AFFINITY) {
		if (!affd)
			affd = &msi_default_affd;
        	if (affd->pre_vectors + affd->post_vectors > min_vecs)
				return -EINVAL;
		if (affd->pre_vectors + affd->post_vectors == min_vecs)
				affd = NULL;
	} else {
		if (WARN_ON(affd))
			affd = NULL;
	}

	// MSI-X 인터럽트 할당, MSI-X 기능 구조 구성
	if (flags & PCI_IRQ_MSIX) {
		vecs = __pci_enable_msix_range(dev, NULL, min_vecs, max_vecs,
				affd);
		if (vecs > 0)
			return vecs;
	}

	// MSI 인터럽트 할당, MSI 기능 구조 구성
	if (flags & PCI_IRQ_MSI) {
		vecs = __pci_enable_msi_range(dev, min_vecs, max_vecs, affd);
		if (vecs > 0)
			return vecs;
	}

	// INITx 인터럽트 할당    
	if ((flags & PCI_IRQ_LEGACY) && min_vecs == 1) {
		pci_intx(dev, 1);
		return 1;
	}

	return vecs;
}

이 세 가지 인터럽트를 동시에 활성화할 수 없다는 점에 유의해야 합니다. 예를 들어 MSI-X 인터럽트를 사용하려면 INITx 및 MSI 인터럽트를 비활성화해야 합니다.

지금까지 nvme_pci_enable() 함수에 대해 대략적으로 분석해 보았고, nvme_configure_admin_queue() 함수에 대해서는 다음 글에서 다음에 분석 하겠습니다.

추가 정보