nvme_probe() 함수 분석
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 중 하나가 설정되었는지 여부를 나타냅니다.
- 2단계: pci_request_selected_regions() 함수 호출. 이 함수의 매개변수 중 하나는 이전에 pci_select_bars() 함수를 호출하여 반환된 mask 값으로 해당 BAR를 유지하고 다른 사람이 사용하지 못하도록 하는 기능입니다.
- 3단계: ioremap() 함수를 호출합니다. Linux에서는 물리적 주소에 직접 액세스할 수 없으며 가상 주소에 매핑해야 하며 ioremap() 함수가 이러한 목적을 위한 것입니다. 매핑 후 dev->bar를 통해 nvme 장치 레지스터를 직접 접근할 수 있습니다. 그러나 코드에서는 pci_select_bars() 함수의 반환 값에 따라 매핑할 BAR를 결정하지 않습니다. 대신 BAR0를 직접 매핑합니다. 그 이유는 NVMe 프로토콜이 BAR0를 메모리 맵의 기본 주소로 지정하기 때문입니다.
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 구조 및 예에 대한 자세한 분석은 다음을 참조하십시오.
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() 함수는 매우 긴 작업 흐름으로 많은 콘텐츠가 포함되어 다음에 분석 하겠습니다.