본문 바로가기

리눅스 커널/블록 멀티 큐

NVMe device driver code tuning & comments - 3

이번에는 nvme_queue 구조체 안에 여러개의 SQ를 만들 차례이다.

nvme_reset_work() -> nvme_setup_io_queues() -> nvme_create_io_queues()

 

먼저 모든 nvme_queue는 원래대로 SQ를 각각 하나 씩 생성하게 한다.

그리고 이것이 성공하면 다음과 같이 모든 코어에 추가로 SQ를 생성하게 한다.

static int nvme_create_io_queues(struct nvme_dev *dev)
{
	...

	if (!ret) //all nvme queue was successfully created
	{
		int result = 0, extra_sq_cnt = 0, qid = 1;
		while (!(result = nvme_create_extra_queue(dev->queues[qid], max)))
		{
			extra_sq_cnt += 1;
			if (++qid > max)
				qid = 1; //reset	
		}
		printk("extra, additional sq count: %d\n", extra_sq_cnt);
	}
	else
		printk("extra, do not attempt to create extra sq, err: %d\n", ret);
        
    ....
}

각 코어는 아래 함수를 이용하여 추가 SQ를 만들게 된다.

static int nvme_create_extra_queue(struct nvme_queue * nvmeq, unsigned int nrq)
{
	int ret = -1, depth;
	struct extra_queue * sq;
	struct nvme_dev * dev = nvmeq->dev;

	if (nvmeq->extra_cnt >= nvmeq->dev->extra_nr) //out of bound
		return -1;

	nvmeq->extra_list[nvmeq->extra_cnt] = 
		kzalloc(sizeof(struct extra_queue), GFP_KERNEL); //alloc memory for extra sq
	sq = nvmeq->extra_list[nvmeq->extra_cnt]; //register to extra list
	if (!sq)
		return -1;
	sq->size = 0; //reset
	
	depth = dev->q_depth; //reset
	sq->sq_cmds_extra = dma_alloc_coherent(dev->dev, SQ_SIZE(depth),
                                                &sq->sq_dma_addr_extra, GFP_KERNEL);
	//alloc array nvme_command

	if (!sq->sq_cmds_extra)	//fail
		goto extra_handle;

	nvmeq->extra_cnt += 1; //inc, the number of extra sq
	sq->sq_tail_extra = 0; //reset
	sq->qid_extra = nvmeq->qid + nrq * (nvmeq->extra_cnt); //new qid
	sq->q_db_extra = &dev->dbs[sq->qid_extra * 2 * dev->db_stride]; //register doorbell

	ret = adapter_alloc_sq_extra(dev, sq->qid_extra, nvmeq); //send command to device
	if (!ret) //success
		return ret;	

	//fail
err_handle:
	nvmeq->extra_cnt -= 1;
	dma_free_coherent(dev->dev, SQ_SIZE(depth), (void *)sq->sq_cmds_extra,
                                                        sq->sq_dma_addr_extra);
extra_handle:
	kfree(sq);
	return ret;
}

여기서 중요한 점은 door bell 설정이다. nvme spec에서 register definition을 이어서 살펴보면,

위와 같으며, Offset 관점으로 보면 다음과 같은 그림으로 정리할 수 있다.

SQ의 ID에 대해서 그 ID에 2를 곱한 값이 해당 SQ의 door bell register의 offset으로 볼 수 있는 것이다.

 

그리고 구현한 커널은 다음과 같은 구조를 갖는다.

상단이 코어를 나타내고, 하단의 3개가 각 코어에 할당된 SQ와 그 ID이다.

 

SQ의 생성시 Weight를 주기 위해서는 명령어를 다음과 같이 큐의 우선순위를 설정해 보내야한다.

이를 추가 큐들은 각각 HIGH, URGENT의 우선순위를 갖기 때문에 아래와 같이 추가 큐 생성을 위한 함수를 정의 하였다.

static int adapter_alloc_sq_extra(struct nvme_dev *dev, u16 qid,
                                                struct nvme_queue *nvmeq)
{
        struct nvme_command c;
        int flags = NVME_QUEUE_PHYS_CONTIG;

        if (qid <= 12)
                flags |= NVME_SQ_PRIO_HIGH;
        else
                flags |= NVME_SQ_PRIO_URGENT;

        /*
         * Note: we (ab)use the fact the the prp fields survive if no data
         * is attached to the request.
         */
        memset(&c, 0, sizeof(c));
        c.create_sq.opcode = nvme_admin_create_sq;
        c.create_sq.prp1 =
                cpu_to_le64(nvmeq->extra_list[nvmeq->extra_cnt - 1]->sq_dma_addr_extra);
        c.create_sq.sqid = cpu_to_le16(qid);
        c.create_sq.qsize = cpu_to_le16(nvmeq->q_depth - 1);
        c.create_sq.sq_flags = cpu_to_le16(flags);
        c.create_sq.cqid = cpu_to_le16(nvmeq->qid);

        return nvme_submit_sync_cmd(dev->ctrl.admin_q, &c, NULL, 0);
}

그리고 원래 갖고 있는 SQ는 다음과 같이 LOW의 Weight를 주었다.

static int adapter_alloc_sq(struct nvme_dev *dev, u16 qid,
                                                struct nvme_queue *nvmeq)
{
        struct nvme_command c;
        int flags = NVME_QUEUE_PHYS_CONTIG;

        //original queue
        //if (qid == 1)
                //flags |= NVME_SQ_PRIO_URGENT;
        //else if (qid > 1)
                flags |= NVME_SQ_PRIO_LOW;
        ...
}