nvme_probe() 함수 분석

drivers/nvme/host/pci.c 파일을 열어 nvme_probe() 함수를 찾습니다.

nvme_probe() 함수가 주로 수행하는 작업을 요약하면 다음과 같습니다.

  • dev, dev->queues를 위한 공간을 할당합니다. 각 CPU 코어에 하나씩 할당합니다. 각 코어에 대해 IO 대기열을 할당하고 모든 코어는 Admin 대기열을 공유합니다. 여기서 대기열의 개념은 더 엄밀히 말하면 제출(Submission) 대기열과 완료(Completion) 대기열의 집합입니다.
  • ① nvme_dev_map() 함수를 호출하여 PCI Bar의 가상 주소를 가져옵니다.
  • 두 작업 변수를 초기화하고 타이머, 뮤텍스를 초기화합니다.
  • ② nvme_setup_prp_pools() 함수를 호출하여 DMA에 필요한 PRP 메모리 풀을 설정합니다.
  • ③ nvme_init_ctrl() 함수를 호출하여 NVMe 컨트롤러 구조를 초기화합니다.
  • workqueue를 통해 dev->reset_work를 예약합니다. 즉, nvme 컨트롤러를 재설정하도록 ④ nvme_reset_work() 함수가 커널 내부에서 스케쥴링되어서 실행되도록 합니다.
static int nvme_probe(struct pci_dev *pdev, const struct pci_device_id *id)
{
	int node, result = -ENOMEM;

	struct nvme_dev *dev;

	// dev_to_node를 호출하여 pci_dev의 numa 노드 가져오기.
	// 지정하지 않으면 기본값은 첫번째 numa 노드인 first_memory_node입니다. 
	node = dev_to_node(&pdev->dev);
	if (node == NUMA_NO_NODE)
		set_dev_node(&pdev->dev, first_memory_node);

	// nvme_dev 노드를 위한 공간 할당
	dev = kzalloc_node(sizeof(*dev), GFP_KERNEL, node);
	if (!dev)
		return -ENOMEM;
    
	// 각 CPU 코어에 대한 대기열을 할당합니다.
	// 각 코어에 대해 IO 대기열을 할당하고 모든 코어는 Admin 대기열을 공유합니다.
	// 여기서 대기열의 개념은 더 엄밀히 말하면 제출(Submission) 대기열과 완료(Completion) 대기열의 집합입니다.
	dev->queues = kzalloc_node((num_possible_cpus() + 1) * sizeof(void *),
							GFP_KERNEL, node);  // 1개 이상인 이유는 Admin Queue 때문입니다.
	if (!dev->queues)
		goto free;

	dev->dev = get_device(&pdev->dev); 	// 디바이스 객체의 참조 횟수 증가
	pci_set_drvdata(pdev, dev); 		// 장치 전용 데이터 포인터 설정 
	// ① PCI Bar의 가상 주소 얻기
	result = nvme_dev_map(dev);
	if (result)
		goto free;

	// 두 작업 변수를 초기화하고 작업이 실행될 때 호출되는 핸들러 등록
	INIT_WORK(&dev->reset_work, nvme_reset_work);
	INIT_WORK(&dev->remove_work, nvme_remove_dead_ctrl_work);
	setup_timer(&dev->watchdog_timer, nvme_watchdog_timer,
		(unsigned long)dev); 		// 타이머 워치독 초기화
	mutex_init(&dev->shutdown_lock); 	// 뮤텍스 초기화
	init_completion(&dev->ioq_wait); 	// 초기화 완료 
	// ② DMA에 필요한 PRP 메모리 풀 설정
	result = nvme_setup_prp_pools(dev);
	if (result)
		goto put_pci;

	// ③ NVMe 컨트롤러 구조 초기화
	result = nvme_init_ctrl(&dev->ctrl, &pdev->dev, &nvme_pci_ctrl_ops,
			id->driver_data);
	if (result)
		goto release_pools;

	// 출력 로그
	dev_info(dev->ctrl.device, "pci function %s\n", dev_name(&pdev->dev));
	// ④ reset_work를 nvme_workq 작업 대기열에 넣습니다. → nvme_reset_work()
	queue_work(nvme_workq, &dev->reset_work);

	return 0;

 release_pools:
	nvme_release_prp_pools(dev);

 put_pci:
	put_device(dev->dev);
	nvme_dev_unmap(dev);

 free:
	kfree(dev->queues);
	kfree(dev);
    return result;
}

계속해서 nvme_probe()함수에서 호출되는 네 가지 함수에(① nvme_dev_map() , ② nvme_setup_prp_pools() , ③ nvme_init_ctrl() , ④ nvme_reset_work()) 대해 살펴 봅니다.

1. nvme_dev_map() 함수

static int nvme_dev_map(struct nvme_dev *dev)
{
	struct pci_dev *pdev = to_pci_dev(dev->dev);
	if (pci_request_mem_regions(pdev, "nvme"))
		return -ENODEV;

	// IO 주소 공간을 커널의 가상 주소 공간에 매핑
	dev->bar = ioremap(pci_resource_start(pdev, 0), 8192);
	if (!dev->bar)
		goto release;

	return 0;

  release:
	pci_release_mem_regions(pdev);
	return -ENODEV;
}

관련된 코드까지 붙여놓고 보면

/* include/linux/pci.h */

static inline int
pci_request_mem_regions(struct pci_dev *pdev, const char *name)
{
	return pci_request_selected_regions(pdev,
			    pci_select_bars(pdev, IORESOURCE_MEM), name);
}

nvme_dev_map() 함수의 실행 프로세스는 세 단계로 진행되는 것을 알 수 있습니다.

  • 1 단계: 반환 값이 BAR mask인 pci_select_bars() 함수를 호출합니다. PCI 장치의 헤더 구성 공간에는 6개의 32비트 BAR 레지스터(아래 그림 참조)가 있으므로 mask의 각 비트 값은 BAR 중 하나가 설정되었는지 여부를 나타냅니다.

img

  • 2단계: pci_request_selected_regions() 함수 호출. 이 함수의 매개변수 중 하나는 이전에 pci_select_bars() 함수를 호출하여 반환된 mask 값으로 해당 BAR를 유지하고 다른 사람이 사용하지 못하도록 하는 기능입니다.
  • 3단계: ioremap() 함수를 호출합니다. Linux에서는 물리적 주소에 직접 액세스할 수 없으며 가상 주소에 매핑해야 하며 ioremap() 함수가 이러한 목적을 위한 것입니다. 매핑 후 dev->bar를 통해 nvme 장치 레지스터를 직접 접근할 수 있습니다. 그러나 코드에서는 pci_select_bars() 함수의 반환 값에 따라 매핑할 BAR를 결정하지 않습니다. 대신 BAR0를 직접 매핑합니다. 그 이유는 NVMe 프로토콜이 BAR0를 메모리 맵의 기본 주소로 지정하기 때문입니다.

img

2. nvme_setup_prp_pools() 함수

nvme_setup_prp_pools() 함수는 두 개의 DMA 풀을 생성합니다. prp_small_pool은 블록 크기가 256바이트인 메모리를 제공하고, prp_page_pool은 주로 다양한 길이의 prp 목록을 최적화하기 위해 Page_Size(포맷 중에 결정됨, 예: 4KB) 블록 크기의 메모리를 제공합니다.

static int nvme_setup_prp_pools(struct nvme_dev *dev)
{
	dev->prp_page_pool = dma_pool_create("prp list page", dev->dev,
						PAGE_SIZE, PAGE_SIZE, 0);
	if (!dev->prp_page_pool)
		return -ENOMEM;

	/* Optimisation for I/Os between 4k and 128k */
	dev->prp_small_pool = dma_pool_create("prp list 256", dev->dev,
						256, 256, 0);
	if (!dev->prp_small_pool) {
		dma_pool_destroy(dev->prp_page_pool);
		return -ENOMEM;
	}

	return 0;
}

PRP 구조 및 예에 대한 자세한 분석은 다음을 참조하십시오.

NVMe 이야기 #4: 주소 지정 모델 PRP 및 SGL 분석

3. nvme_init_ctrl() 함수

int nvme_init_ctrl(struct nvme_ctrl *ctrl, struct device *dev,
		const struct nvme_ctrl_ops *ops, unsigned long quirks)
{
	int ret;

	ctrl->state = NVME_CTRL_NEW;
	spin_lock_init(&ctrl->lock);
	INIT_LIST_HEAD(&ctrl->namespaces);
	mutex_init(&ctrl->namespaces_mutex);
	kref_init(&ctrl->kref);
	ctrl->dev = dev;
	ctrl->ops = ops;
	ctrl->quirks = quirks;
	INIT_WORK(&ctrl->scan_work, nvme_scan_work);
	INIT_WORK(&ctrl->async_event_work, nvme_async_event_work);
    
	ret = nvme_set_instance(ctrl);
	if (ret)
		goto out;

	ctrl->device = device_create_with_groups(nvme_class, ctrl->dev,
				MKDEV(nvme_char_major, ctrl->instance),
				ctrl, nvme_dev_attr_groups,
				"nvme%d", ctrl->instance);
	if (IS_ERR(ctrl->device)) {
		ret = PTR_ERR(ctrl->device);
		goto out_release_instance;
	}
	get_device(ctrl->device);
	ida_init(&ctrl->ns_ida);

	spin_lock(&dev_list_lock);
	list_add_tail(&ctrl->node, &nvme_ctrl_list);
	spin_unlock(&dev_list_lock);

	return 0;

out_release_instance:
	nvme_release_instance(ctrl);

out:
	return ret;
}

위의 코드에서 nvme_init_ctrl() 함수의 기능은 device_create_with_groups() 함수를 호출하여 nvme0이라는 문자 장치를 생성하는 것임을 알 수 있습니다. 이 nvme0의 0은 nvme_set_instance() 함수를 통해 얻습니다. 이 과정에서 ida_get_new() 함수를 통해 고유 인덱스 값을 얻습니다.

/* drivers/nvme/host/core.c */

static int nvme_set_instance(struct nvme_ctrl *ctrl)
{
	int instance, error;

	do {
		if (!ida_pre_get(&nvme_instance_ida, GFP_KERNEL))
			return -ENODEV;

		spin_lock(&dev_list_lock);
		error = ida_get_new(&nvme_instance_ida, &instance);
		spin_unlock(&dev_list_lock);
	} while (error == -EAGAIN);

	if (error)
		return -ENODEV;

	ctrl->instance = instance;
	return 0;
}

4. nvme_reset_work() 함수

nvme_rest_work() 함수는 매우 긴 작업 흐름으로 많은 콘텐츠가 포함되어 다음에 분석 하겠습니다.