Art of Pr0gr4m

[Linux Kernel 5] Block Device Driver Example 본문

IT/Linux Kernel

[Linux Kernel 5] Block Device Driver Example

pr0gr4m 2020. 5. 4. 00:56

이번 포스트에선 저번 포스트에 이어 Block Device Driver의 예제를 작성해본다.

블록 디바이스 드라이버는 문자 디바이스 드라이버에 비해 복잡한 만큼

새로운 기반 개념들이 많이 등장했고, 이에 따른 드라이버 작성을 위한 인터페이스에 변화가 컸다.

 

사실 문자 디바이스 드라이버의 경우엔 인터넷에 떠돌아다니는 2.6버전 예제 아무거나 가져다

빌드 에러 메시지 보면서 대충 고치면 어찌저찌 동작하게 만들 수 있다.

 

반면 블록 디바이스 드라이버의 경우엔 (램디스크가 가장 대표적이다)

2.6 버전의 자료를 보고 공부하고 예제를 작성하면 어찌 되는게 없을 것이다.

아무리 고치려 해도 처음부터 싹 갈아엎어야 하는 대공사가 필요하다.

 

이게 어느정도냐면 비교적 최신 버전의 커널에 대한 다큐먼트 프로젝트 페이지를 보고

예제를 따라 작성해봐도 빌드가 되지 않을 것이다. (커널 4.19 버전)

 

블록 디바이스 드라이버의 가장 큰 변화의 이유는 mq(MultiQueue)의 도입이 아닐까 싶다.

현대 SSD의 fast random access나 다중 IO 리퀘스트 처리 성능을 잘 이끌어 내려면

multi queue의 도입은 선택이 아닌 필수이다.

그리고 현재 표준으로 사용하는 IO 스케줄러는 kyber, mq-deadline, bfq, noop 넷 중 하나인데

sort를 하지 않는 noop을 제외하고는 전부 mq를 기반으로 만들어진 스케줄러다.

이들을 지원하기 위해서 구조체나 인터페이스들이 꽤 복잡해졌다.

 

이 전 포스트들에 비해 서론이 꽤 길었는데, 아무튼 블록 디바이스 드라이버 예제는

구버전 커널에서의 작성과 굉장히 다르며 변화가 굉장히 빨라서 얼마 지나지 않아

해당 포스트 예제 또한 정상적으로 동작하지 않을 수 있음을 알리는 바이다.

(블록 디바이스 드라이버 기반 개념들을 잘 모른다면 저번 포스트를 읽고 온다.)

 

 

 

1. 장치 등록

 

int register_blkdev(unsigned int major, const char *name);
 
#define alloc_disk(minors) alloc_disk_node(minors, NUMA_NO_NODE)
 
struct gendisk *__alloc_disk_node(int minors, int node_id);
 
void add_disk(struct gendisk *disk)
 
void del_gendisk(struct gendisk *gp);
 
void put_disk(struct gendisk *disk);
 
void unregister_blkdev(unsigned int, const char *);
 
void set_capacity(struct gendisk *disk, sector_t size)
{
	disk->part0.nr_sects = size;
}
 

위 함수들은 디스크 디바이스를 등록/해제하는 주요 함수들이다.

register_blkdev()는 새로운 블록 디바이스를 등록한다. name에 해당하는 major 넘버를 얻는다.

alloc_disk()는 gendisk 구조체를 할당한다. minors 값은 디스크가 가질 수 있는 최대 파티션 갯수이다.

add_disk()는 시스템에 디스크를 할당한다. alloc_disk로 할당받은 gendisk 구조체를 초기화하여 인자로 전달한다.

del_gendisk()는 시스템에서 디스크를 제거한다.

put_disk()는 alloc_disk로 할당받은 메모리를 반환한다.

unregister_blkdev()는 블록 디바이스 자원(major 넘버 등)을 반환한다.

set_capacity()는 새로운 디스크의 capacity 값을 지정한다.

 

gendisk 구조체는 디스크 드라이버를 나타내며 아래와 같은 정보들을 갖는다.

struct gendisk {
	/* major, first_minor and minors are input parameters only,
	 * don't use directly.  Use disk_devt() and disk_max_parts().
	 */
	int major;			/* major number of driver */
	int first_minor;
	int minors;                     /* maximum number of minors, =1 for
                                         * disks that can't be partitioned. */
 
	char disk_name[DISK_NAME_LEN];	/* name of major driver */
	char *(*devnode)(struct gendisk *gd, umode_t *mode);
 
	unsigned short events;		/* supported events */
	unsigned short event_flags;	/* flags related to event processing */
 
	/* Array of pointers to partitions indexed by partno.
	 * Protected with matching bdev lock but stat and other
	 * non-critical accesses use RCU.  Always access through
	 * helpers.
	 */
	struct disk_part_tbl __rcu *part_tbl;
	struct hd_struct part0;
 
	const struct block_device_operations *fops;
	struct request_queue *queue;
	void *private_data;
 
	int flags;
	struct rw_semaphore lookup_sem;
	struct kobject *slave_dir;
 
	struct timer_rand_state *random;
	atomic_t sync_io;		/* RAID */
	struct disk_events *ev;
#ifdef  CONFIG_BLK_DEV_INTEGRITY
	struct kobject integrity_kobj;
#endif	/* CONFIG_BLK_DEV_INTEGRITY */
	int node_id;
	struct badblocks *bb;
	struct lockdep_map lockdep_map;
};

disk_name은 생성할 블록 디바이스 파일(노드)의 이름이다.

major, first_minor, minor 값은 디스크 id 값이다. (minor는 최대로 가질 수 있는 파티션 값이다.)

fops는 디스크에 연결되는 오퍼레이션을 지정한다.

queue는 IO request를 나타낼 IO queue다.

private_data는 디스크 private data를 나타내는 포인터이다.

capacity는 디스크 capacity 값으로, set_capacity로 초기화한다.

 

이를 기반으로 커널 4.x 이하 버전의 single queue 기반 초기화 코드를 간략히 작성하면 다음과 같다.

#include <linux/genhd.h>
#include <linux/blkdev.h>
 
void blockdev_init()
{
    struct gendisk *gdisk;
    int major_number = register_blkdev(0, "pr0gr4mblk");
 
    gdisk = alloc_disk(1);
 
    if (!gdisk) {
        return;
    }
 
    snprintf(gdisk->disk_name, 8, "blockdev");  /* Block device file name: "/dev/blockdev" */
    gdisk->flags = GENHD_FL_NO_PART_SCAN;
    gdisk->major = major_number;
    gdisk->fops = &blockdev_ops; 
    gdisk->first_minor = 0;
    gdisk->queue = blk_init_queue(req_fun, &lk);
 
    set_capacity(block_device->gdisk, 1024 * 512);
 
    add_disk(gdisk);
}

block_device_operations 구조체는 이 전 오퍼레이션 등록과 유사하다.

blk_init_queue는 single queue를 초기화하는데 사용한다.

multi queue가 기본인 Kernel 5버전에서는 사라진 함수이다.

인자로 IO 리퀘스트를 처리할 request_fn_proc 함수를 등록한다.

등록할 함수에서는 blk_fetch_request()로 리퀘스트를 받아서

rq_data_dir()로 direction을 확인하여 read 혹은 write 작업을 하면 된다.

어차피 최신 커널에서는 사용하지 않는 방식이기에 더 이상의 예시는 생략한다.

 

 

 

2. blk-mq 프레임워크

 

그렇다면 커널 5버전에서는 무엇이 달라졌을까?

사실 디바이스 등록 및 초기화 단계는 딱히 달라진 부분이 없다.

mq 부분이 문제라고 했으니 queue 부분이 바뀌었을 거라고 짐작할 수 있다.

blk-mq 프레임워크에서 queue 초기화는 다음 함수로 한다.

struct request_queue *blk_mq_init_sq_queue(struct blk_mq_tag_set *set,
						const struct blk_mq_ops *ops,
						unsigned int queue_depth,
						unsigned int set_flags);

ops는 항상 같은 오퍼레이션 변수일테고 flags는 플래그 설정일테지만

tag_set과 queue_depth는 생소하다.

queue_depth는 디스크 컨트롤러가 큐에 한번에 넣을 수 있는 IO request의 수를 의미한다.

병렬성과 자원 한계로 인한 오류 및 병목 사이에 적절한 뎁스를 모니터링하여 최적의 값을 산출해야 한다.

(예제에서는 대략 128의 값을 사용할 것이다.)

blk_mq_tag_set 구조체는 아래와 같은 정보들을 저장하는 통합 구조체이다.

/**
 * struct blk_mq_tag_set - tag set that can be shared between request queues
 * @map:	   One or more ctx -> hctx mappings. One map exists for each
 *		   hardware queue type (enum hctx_type) that the driver wishes
 *		   to support. There are no restrictions on maps being of the
 *		   same size, and it's perfectly legal to share maps between
 *		   types.
 * @nr_maps:	   Number of elements in the @map array. A number in the range
 *		   [1, HCTX_MAX_TYPES].
 * @ops:	   Pointers to functions that implement block driver behavior.
 * @nr_hw_queues:  Number of hardware queues supported by the block driver that
 *		   owns this data structure.
 * @queue_depth:   Number of tags per hardware queue, reserved tags included.
 * @reserved_tags: Number of tags to set aside for BLK_MQ_REQ_RESERVED tag
 *		   allocations.
 * @cmd_size:	   Number of additional bytes to allocate per request. The block
 *		   driver owns these additional bytes.
 * @numa_node:	   NUMA node the storage adapter has been connected to.
 * @timeout:	   Request processing timeout in jiffies.
 * @flags:	   Zero or more BLK_MQ_F_* flags.
 * @driver_data:   Pointer to data owned by the block driver that created this
 *		   tag set.
 * @tags:	   Tag sets. One tag set per hardware queue. Has @nr_hw_queues
 *		   elements.
 * @tag_list_lock: Serializes tag_list accesses.
 * @tag_list:	   List of the request queues that use this tag set. See also
 *		   request_queue.tag_set_list.
 */
struct blk_mq_tag_set {
	struct blk_mq_queue_map	map[HCTX_MAX_TYPES];
	unsigned int		nr_maps;
	const struct blk_mq_ops	*ops;
	unsigned int		nr_hw_queues;
	unsigned int		queue_depth;
	unsigned int		reserved_tags;
	unsigned int		cmd_size;
	int			numa_node;
	unsigned int		timeout;
	unsigned int		flags;
	void			*driver_data;
 
	struct blk_mq_tags	**tags;
 
	struct mutex		tag_list_lock;
	struct list_head	tag_list;
};
 

tag_set은 큐들 사이에서 공유될 수 있다. 주석에는 각 멤버들에 대한 설명이 적혀있다.

사실 아래에서 blk_mq_init_sq_queue를 좀 더 자세히 살펴볼 때 알 수 있겠지만,

모든 멤버를 초기화할 필요는 없다.

 

이를 기반으로 최대한 간단히 IO request 처리 코드를 작성하면 다음과 같다.

int do_request(struct request *rq, unsigned int *nr_bytes)
{
    struct bio_vec bvec;
    struct req_iterator iter;
    loff_t pos = blk_rq_pos(rq) << SECTOR_SHIFT;
    void* data;
    unsigned long data_len;
 
    rq_for_each_segment(bvec, rq, iter)
    {
        data = page_address(bvec.bv_page) + bvec.bv_offset;
        data_len = bvec.bv_len;
 
        if (rq_data_dir(rq) == WRITE) {
            printk("Writing data to the blk-mq device\n");
        } else {
            printk("Reading data from the blk-mq device\n");
        }
 
        pos += b_len;
        *nr_bytes += data_len;
    }
 
    return 0;
}
 
 
blk_status_t queue_rq(struct blk_mq_hw_ctx *hctx, const struct blk_mq_queue_data* bd)
{   
    unsigned int nr_bytes = 0;
    blk_status_t status = BLK_STS_OK;
    struct request *rq = bd->rq;
 
    blk_mq_start_request(rq);
 
    if (do_request(rq, &nr_bytes) != 0) {
        status = BLK_STS_IOERR;
    }
 
    if (blk_update_request(rq, status, nr_bytes)) {
        BUG();
    }
 
    __blk_mq_end_request(rq, status);
 
    return status;
}
 
static struct blk_mq_ops mq_ops = {
    .queue_rq = queue_rq,
};
 
void block_dev_init()
{
    struct blk_mq_tag_set tag_set;
 
    gdisk->queue = blk_mq_init_sq_queue(&tag_set, &mq_ops, 128, BLK_MQ_F_SHOULD_MERGE);
}

do_request()에서 세그먼트 단위의 request를 반복하며 처리한다.

bio_vec 구조체의 bv_page와 bv_offset 멤버로 데이터의 주소를 얻고, bv_len으로 데이터의 길이를 구한다.

rq_data_dir()로 해당 request가 write인지 read인지 판별한다.

queue_rq()에서는 request 처리를 시작 및 종료한다.

blk_mq_start_request()를 호출하면 request 처리 루틴이 시작되며, 데이터 트랜잭션 타임아웃을 설정한다.

참고로 blk_update_request() 및 __blk_mq_end_request() 루틴은 이 후 예제에서 볼 blk_mq_end_request()로 래핑할 수 있다.

 

 

 

3. blk_mq_init_sq_queue()

 

mq 초기화의 핵심인 해당 함수의 정의를 살펴보면 다음과 같다.

/*
 * Helper for setting up a queue with mq ops, given queue depth, and
 * the passed in mq ops flags.
 */
struct request_queue *blk_mq_init_sq_queue(struct blk_mq_tag_set *set,
					   const struct blk_mq_ops *ops,
					   unsigned int queue_depth,
					   unsigned int set_flags)
{
	struct request_queue *q;
	int ret;
 
	memset(set, 0, sizeof(*set));
	set->ops = ops;
	set->nr_hw_queues = 1;
	set->nr_maps = 1;
	set->queue_depth = queue_depth;
	set->numa_node = NUMA_NO_NODE;
	set->flags = set_flags;
 
	ret = blk_mq_alloc_tag_set(set);
	if (ret)
		return ERR_PTR(ret);
 
	q = blk_mq_init_queue(set);
	if (IS_ERR(q)) {
		blk_mq_free_tag_set(set);
		return q;
	}
 
	return q;
}

인자로 전달받은 tag_set을 초기화한 후 blk_mq_alloc_tag_set로 tag_set을 할당한다.

참고로 해당 함수에서는 tag_set의 내용의 이상 여부를 확인하고,

멤버의 값이나 커널 설정 등을 참고하여 메모리 할당 및 queue map이나 mutex 등을 할당하여 태그 리스트에 해당 태그를 추가한다.

마지막으로 blk_mq_init_queue()를 호출하여 request_queue를 할당 및 초기화한다.

또한 참고로 초기화는 blk_mq_init_allocated_queue()로 하는데, 여기서 tag_set의 값들을 참고하여 queue에 오퍼레이션, IO buffer, lock, 타이머 등을 등록한다.

 

여기서 보다보면 blk_mq_init_sq_queue() 함수가 queue를 하나만 등록하는 것을 볼 수 있다.

그건 이름에서도 알 수 있듯이 해당 함수가 single queue 초기화 함수여서 그렇다.

(만약 mq 프레임워크라고 해서 sq를 지원하지 않는다면 mq를 지원하지 않는 디스크 컨트롤러는 kernel 5를 사용하지 못할 것이다.)

디스크 컨트롤러가 mq를 지원해서 mq 드라이버를 작성하고 싶다면 blk_mq_alloc_tag_set() 함수와 blk_mq_init_queue() 함수를 이용하여 직접 할당 및 초기화 작업들을 해줘야한다. (이에 대한 예시는 생략한다.)

 

 

 

4. blk device driver 예제 소스

 

blkdev.h

#ifndef __BLKDEV_H__
#define __BLKDEV_H__
 
#ifndef SECTOR_SHIFT
#define SECTOR_SHIFT	9
#endif
 
#ifndef SECTOR_SIZE
#define SECTOR_SIZE		(1 << SECTOR_SHIFT)
#endif
 
#ifndef SUCCESS
#define SUCCESS			0
#endif
 
#define BLKDEV_NAME		"pr0gr4m-blkdev"
#define BLKDEV_BUFSIZ	(16 * PAGE_SIZE)
 
typedef struct block_cmd {
} block_cmd_t;
 
typedef struct block_dev {
	sector_t capacity;
	u8 *data;
	atomic_t open_counter;
 
	struct blk_mq_tag_set tag_set;
	struct request_queue *queue;
	struct gendisk *gdisk;
} block_dev_t;
 
static int blkdev_alloc_buffer(block_dev_t *dev);
static void blkdev_free_buffer(block_dev_t *dev);
static int blkdev_add_device(void);
static void blkdev_remove_device(void);
 
#endif

 

blkdev.c

#include <linux/init.h>
#include <linux/device.h>
#include <linux/kernel.h>
#include <linux/module.h>
#include <linux/blkdev.h>
#include <linux/blk-mq.h>
#include <linux/genhd.h>
#include <linux/hdreg.h>
#include <linux/uaccess.h>
#include "blkdev.h"
 
static int blkdev_major = 0;
static block_dev_t *blkdev_dev = NULL;
 
static int do_request(struct request *rq, unsigned int *nr_bytes)
{
	int ret = SUCCESS;
	struct bio_vec bvec;
	struct req_iterator iter;
	block_dev_t *dev = rq->q->queuedata;
	loff_t pos = blk_rq_pos(rq) << SECTOR_SHIFT;
	loff_t dev_size = (loff_t)(dev->capacity << SECTOR_SHIFT);
 
	printk("[pr0gr4m-blkdev] request start from sector %lld\n",
			blk_rq_pos(rq));
 
	rq_for_each_segment(bvec, rq, iter) {
		unsigned long b_len = bvec.bv_len;
		void *b_buf = page_address(bvec.bv_page) + bvec.bv_offset;
 
		if ((pos + b_len) > dev_size)
			b_len = (unsigned long)(dev_size - pos);
		if (b_len < 0)
			b_len = 0;
 
		if (rq_data_dir(rq) == WRITE)
			memcpy(dev->data + pos, b_buf, b_len);
		else
			memcpy(b_buf, dev->data + pos, b_len);
 
		pos += b_len;
		*nr_bytes += b_len;
	}
 
	return ret;
}
 
static blk_status_t queue_rq(struct blk_mq_hw_ctx *hctx,
		const struct blk_mq_queue_data *bd)
{
	unsigned int nr_bytes = 0;
	blk_status_t status = BLK_STS_OK;
	struct request *rq = bd->rq;
 
	blk_mq_start_request(rq);
 
	if (do_request(rq, &nr_bytes) != 0)
		status = BLK_STS_IOERR;
	printk("[pr0gr4m-blkdev] request process %d bytes\n", nr_bytes);
 
#if 1
	blk_mq_end_request(rq, status);
#else
	if (blk_update_request(rq, status, nr_bytes))
		BUG();
	__blk_mq_end_request(rq, status);
#endif
	return BLK_STS_OK;
}
 
 
 
static int dev_open(struct block_device *bd, fmode_t mode)
{
	block_dev_t *dev = bd->bd_disk->private_data;
	if (dev == NULL) {
		printk(KERN_ERR "[pr0gr4m-blkdev] open error");
		return -ENXIO;
	}
 
	atomic_inc(&dev->open_counter);
	printk("[pr0gr4m-blkdev] device was opened\n");
	return 0;
}
 
static void dev_release(struct gendisk *gd, fmode_t mode)
{
	block_dev_t *dev = gd->private_data;
	if (dev == NULL) {
		printk(KERN_ERR "[pr0gr4m-blkdev] Invalid to release disk");
	} else {
		atomic_dec(&dev->open_counter);
		printk("[pr0gr4m-blkdev] device was closed\n");
	}
}
 
static int dev_ioctl(struct block_device *bd, fmode_t mode, 
		unsigned int cmd, unsigned long arg)
{
	return -ENOTTY;
}
 
#ifdef CONFIG_COMPAT
static int dev_compat_ioctl(struct block_device *bd, fmode_t mode, 
		unsigned int cmd, unsigned long arg)
{
	return -ENOTTY;
}
#endif
 
static struct blk_mq_ops mq_ops = {
	.queue_rq = queue_rq
};
 
static const struct block_device_operations blk_fops = {
	.owner = THIS_MODULE,
	.open = dev_open,
	.release = dev_release,
	.ioctl = dev_ioctl,
#ifdef CONFIG_COMPAT
	.compat_ioctl = dev_compat_ioctl
#endif
};
 
 
static int blkdev_alloc_buffer(block_dev_t *dev)
{
	dev->capacity = BLKDEV_BUFSIZ >> SECTOR_SHIFT;
	dev->data = kmalloc(dev->capacity << SECTOR_SHIFT, GFP_KERNEL);
	if (dev->data == NULL) {
		printk(KERN_ERR "[pr0gr4m-blkdev] kmalloc error");
		return -ENOMEM;
	}
	return SUCCESS;
}
 
static void blkdev_free_buffer(block_dev_t *dev)
{
	if (dev->data) {
		kfree(dev->data);
		dev->data = NULL;
		dev->capacity = 0;
	}
}
 
static int blkdev_add_device(void)
{
	int ret = SUCCESS;
	struct gendisk *disk;
	struct request_queue *queue;
	block_dev_t *dev = kzalloc(sizeof(block_dev_t), GFP_KERNEL);
	if (dev == NULL) {
		printk(KERN_ERR "[pr0gr4m-blkdev] kzalloc error");
		return -ENOMEM;
	}
	blkdev_dev = dev;
 
	do {
		if ((ret = blkdev_alloc_buffer(dev)) != SUCCESS)
			break;
 
#if 1
		dev->tag_set.cmd_size = sizeof(block_cmd_t);
		dev->tag_set.driver_data = dev;
 
		queue = blk_mq_init_sq_queue(&dev->tag_set, &mq_ops, 128,
				BLK_MQ_F_SHOULD_MERGE);
		if (IS_ERR(queue)) {
			ret = PTR_ERR(queue);
			printk(KERN_ERR "[pr0gr4m-blkdev] blk_mq_init_sq_queue error");
			break;
		}
		dev->queue = queue;
#else
		dev->tag_set.ops = &mq_ops;
		dev->tag_set.nr_hw_queues = 1;
		dev->tag_set.queue_depth = 128;
		dev->tag_set.numa_node = NUMA_NO_NODE;
		dev->tag_set.cmd_size = sizeof(block_cmd_t);
		dev->tag_set.flags = BLK_MQ_F_SHOULD_MERGE;
		dev->tag_set.driver_data = dev;
 
		ret = blk_mq_alloc_tag_set(&dev->tag_set);
		if (ret) {
			printk(KERN_ERR "[pr0gr4m-blkdev] blk_mq_alloc_tag_set error");
			break;
		}
 
		queue = blk_mq_init_queue(&dev->tag_set);
		if (IS_ERR(queue)) {
			ret = PTR_ERR(queue);
			printk(KERN_ERR "[pr0gr4m-blkdev] blk_mq_init_queue error");
			break;
		}
		dev->queue = queue;
#endif
 
		dev->queue->queuedata = dev;
		if ((disk = alloc_disk(1)) == NULL) {
			printk(KERN_ERR "[pr0gr4m-blkdev] alloc_disk error");
			ret = -ENOMEM;
			break;
		}
 
		disk->flags |= GENHD_FL_NO_PART_SCAN;		// only one partition
		disk->flags |= GENHD_FL_REMOVABLE;
 
		disk->major = blkdev_major;
		disk->first_minor = 0;
		disk->fops = &blk_fops;
		disk->private_data = dev;
		disk->queue = dev->queue;
		sprintf(disk->disk_name, "%s%d", BLKDEV_NAME, 0);
		set_capacity(disk, dev->capacity);
		dev->gdisk = disk;
 
		add_disk(disk);
		printk("[pr0gr4m-blkdev] block device was createdc\n");
	} while (false);
 
	if (ret) {
		blkdev_remove_device();
		printk(KERN_ERR "[pr0gr4m-blkdev] Failed to add block device\n");
	}
	return ret;
}
 
static void blkdev_remove_device(void)
{
	block_dev_t *dev = blkdev_dev;
	if (dev) {
		if (dev->gdisk)
			del_gendisk(dev->gdisk);
 
		if (dev->queue) {
			blk_cleanup_queue(dev->queue);
			dev->queue = NULL;
		}
 
		if (dev->tag_set.tags)
			blk_mq_free_tag_set(&dev->tag_set);
 
		if (dev->gdisk) {
			put_disk(dev->gdisk);
			dev->gdisk = NULL;
		}
 
		blkdev_free_buffer(dev);
		kfree(dev);
		blkdev_dev = NULL;
 
		printk("[pr0gr4m-blkdev] block device was removed\n");
	}
}
 
 
static int __init blkdev_init(void)
{
	int ret = SUCCESS;
	blkdev_major = register_blkdev(blkdev_major, BLKDEV_NAME);
	if (blkdev_major <= 0) {
		printk(KERN_ERR "[pr0gr4m-blkdev] register_blkdev error");
		return -EBUSY;
	}
 
	if ((ret = blkdev_add_device()) != SUCCESS)
		unregister_blkdev(blkdev_major, BLKDEV_NAME);
	return ret;
}
 
static void __exit blkdev_exit(void)
{
	blkdev_remove_device();
	if (blkdev_major > 0)
		unregister_blkdev(blkdev_major, BLKDEV_NAME);
}
 
module_init(blkdev_init);
module_exit(blkdev_exit);
MODULE_LICENSE("GPL");

참고로 블록 디바이스 드라이버의 경우엔 모듈 라이센스가 GPL이 아닌 경우 사용할 수 없는 인터페이스가 생긴다.

 

 

 

5. 테스트 코드

 

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <fcntl.h>
#include <unistd.h>
#include <errno.h>
 
#define BUF_LEN		1024
#define DEV_NAME	"/dev/pr0gr4m-blkdev0"
 
int main()
{
    static char buf[1024];
    int fd;
	off_t off;
 
    if ((fd = open(DEV_NAME, O_RDWR)) < 0) {
		perror("open error");
    }
 
    if (write(fd, "hello", strlen("hello")) < 0) {
		perror("write error");
    }
 
	off = lseek(fd, 0, SEEK_SET);
    if (read(fd, buf, strlen("hello")) < 0) {
		perror("read error");
    } else {
        printf("%s\n", buf);
    }
 
	off = lseek(fd, 1024, SEEK_SET);
	if (write(fd, "pr0gr4m", strlen("pr0gr4m")) < 0) {
		perror("write error");
    }
 
	off = lseek(fd, 1024, SEEK_SET);
    if (read(fd, buf, strlen("pr0gr4m")) < 0) {
		perror("read error");
    } else {
        printf("%s\n", buf);
    }
 
    if (close(fd) != 0) {
		perror("close error");
    }
 
    return 0;
}

 

디바이스를 열어서 쓰고 읽고 출력한 후,

lseek 함수로 random access를 하여 쓰고 읽고 내용을 출력한다.

 

 

 

6. 실행 결과

 

blkdev 드라이버 테스트 결과
dmesg 결과

 

 

원하는 동작이 정상적으로 이루어진걸 볼 수 있다.

이 외에 실전적인 드라이버 예시를 보고 싶다면 nvme 드라이버를 참고하고,

mq 적용 예시는 kyber 스케줄러 소스를 참고하면 된다.