BDEV和CDEV在IO操作上有很大的不同。
CDEV是直来直去的,用户进程请求文件操作syscall,syscall调用FOPS,整个调用栈就完成了。
但是BDEV要用到内核的更多机制,例如缓冲,IO调度,请求队列等。

BDEV只能以block为单位,接受输入或者输出,而CDEV是以byte为单位。所以大多数设备属于CDEV,因为他们不需要块缓冲,而且size不固定。
BDEV对IO请求存在对应的块缓冲,BDEV要先对IO请求进行排序,然后再按照排序后的IO请求来发起实际的IO。
BDEV可以随机访问,因为BDEV具有position pointer ,但是CDEV只能顺序IO。
BDEV通常不直接由用户进程通过文件操作机制进行访问,而是在BDEV上,部署IO调度,在IO调度层之上,又部署文件系统,例如ext4等,用户通过文件操作机制访问FS,由FS将用户的文件请求映射成BDEV的实际IO请求,经过IO调度之后,按照合理的顺序发给BDEV。
但是linux也保留了direct_access的方式,可以直接访问BDEV。
例如直接访问/dev/sdb1等。

BDEV的驱动架构中,有一个类似于FOPS的结构体,成为BDOPS。

struct block_device_operations{
	struct module* owner;
	
	(*open)();
	(*release)();
	(*rw_page)();
	(*ioctl)();
	(*direct_access)();
	...
};

它是驱动函数接口。

内核中,使用gendisk结构体来表示一个独立的磁盘。

struct gendisk{
	
	int major;
	int first_minor;
	int minors_num;
	char disk_name[SIZE];
	
	struct disk_part_tbl __rcu * part_tbl;
	struct hd_struct part0;
	
	const struct block_device_operations * bdops;
	struct request_queue * queue;
	void * private_data;
	
	struct kobject *slave_dir;
	...
};

GENDISK的major+minor定义了设备号。
BDOPS是DISK所关联的驱动函数接口。
DISK关联到一个REQUEST_QUEUE,它是用来描述内核发送给DISK的IO请求的队列。
DISK有一个通配句柄,通常用private_data做实体标记。(InstanceMark)

内核提供了一系列API,来操作GENDISK。

struct gendisk * alloc_disk(int minors_num);
void add_disk(struct gendisk* disk);
void del_gendisk(struct gendisk* gp);

linux用BIO来作为一个IO请求的控制块。

struct bio{
	struct bio* bio_next;
	
	struct bvec_iter bi_iter;
	struct bio_vec * bi_io_vec;
	
	unsigned long bi_flags;
	unsigned long bi_rw;
	unsigned int bi_phys_segments;

	struct block_device * bi_bdev;
	...
};

struct bvec_iter{
	sector_t bi_sector;
	unsigned int bi_size;
	unsigned int bi_index;
	unsigned int bi_bvec_done_bytes;
};

struct bio_vec{
	struct page* bv_page;
	unsigned int bv_len;
	unsigned int bv_offset;
};

BIO内嵌链节,所以BIO可以构成链表。
BIO内嵌一个BVEC_ITER,它是一个资源描述块,描述了的扇区信息。
BIO关联到一个BIO_VEC,它是一个资源描述块,描述了具体的页信息。
BIO关联到一个BDEV,表示BIO从属于哪一个BDEV。

IO调度器将BIO合并成一个request,而将多个request 排序后,组织成request queue。
linux实现的调度算法有三个,IOSCHED_NOOP,IOSCHED_DEADLINE,IOSCHED_CFQ。默认的是CFQ。
给bootargs添加参数,可以修改IO调度器的算法。
kernel elevator=deadlline
也可以通过
#echo deadline > /sys/block/DEVICE/queue/scheduler
在SHELL中修改。

来看看File Operate Mechanism With BDEV。
当用户请求文件操作时,会请求syscall,然后内核调用FS的服务函数,在FS的服务函数中,又会调用IOSCHED的服务函数,IOSCHED的函数将BIO排序,并生成request,然后添加到request_queue,然后调用BDEV 的驱动函数,从request queue中取出BIO,并进行实际IO。

来看看驱动模块需要的组件。
1)衍生设备控制块定义,并实例化。
2)驱动函数接口实例化。
3)模块加载函数编写,负责完成对象创建,注册。
4)模块卸载函数编写,执行反操作。
5)操作函数集编写。

内核提供了BDEV注册相关的API。

int register_blkdev(unsigned int major, cont char* name);
int unregister_blkdev(unsigned int major, const char* name);

request_queue* blk_init_queue(request_fn_proc* rfn, spinlock_t* lock);
blk_queue_max_hw_setors();
blk_queue_logical_block_size();
blk_cleanup_queue();

add_disk();
put_disk();

我们看到,blk_init_queue的首参是一个函数指针,这是一个Callback,用来指定具体处理request的函数。

static xxx_req(struct request_queue* q);

来看一个简单的例子。

static int vdsk_major = 0;
static char vdsk_name[] = "vdsk";


struct vdsk_dev{
	u8* data;
	int size;

	struct gendisk* gd;
	struct request_queue* queue;
	spinlock_t lock;
};
static struct vdsk_dev* vdsk = NULL;

static struct block_device_operations vdsk_bdops = {
	.owner = THIS_MODULE,
	.getgeo = vdsk_getgeo,
};

static int vdsk_getgeo(struct block_device* bdev, struct hd_geometry* geo)
{
	geo->cylinders = VDSK_CYLINDERS;
	geo->heads = VDSK_HEADS;
	geo->sectors = VDSK_SECTORS;
	geo->start = 0;
	
	return 0;
}

static int __init vdsk_init(void)
{
	vdsk_major = register_blkdev(vdsk_major, vdsk_name);
	
	vdsk = kzalloc(sizeof(struct vdsk_dev), GFP_KERNEL);
	
	vdsk->size = VDSK_SIZE;
	vdsk->data = vmalloc(vdsk->size);

	spin_lock_init(&vdsk->lock);

	vdsk->queue = blk_init_queue(vdsk_request, &vdsk->lock);
	blk_queue_logical_block_size(vdsk->queue, VDSK_SECTOR_SIZE);
	vdsk->queue->queuedata = vdsk;

	vdsk->gd = alloc_disk(VDSK_MINORS);
	vdsk->gd->major = vdsk_major;
	vdsk->gd->first_minor = 0;
	vdsk->gd->fops = &vdsk_bdops;
	vdsk->gd->queue = vdsk->queue;
	vdsk->gd->private_data = vdsk;
	snprintf(vdsk->gd->disk_name, 32, "vdsk%c", 'a');
	set_capacity(vdsk->gd, VDSK_SECTOR_TOTAL);
	add_disk(vdsk->gd);
	
	return 0;
}
module_init(vdsk_init);


static void __exit vdsk_exit(void)
{
	del_gendisk(vdsk->gd);
	put_disk(vdsk_gd);
	blk_cleanup_queue(vdsk->queue);
	vfree(vdsk->data);
	kree(vdsk);
	unregister_blkdev(vdsk_major, vdsk_name);
}
module_exit(vdsk_exit);

static void vdsk_request(struct request_queue* rq)
{
	struct vdsk_dev* vdskp;
	struct request* req;
	struct bio* bio;
	struct bio_vec bvec;
	struct bvec_iter iter;
	unsigned long offset;
	unsigned long nbytes;
	char* buffer;

	vdskp = rq->queuedata;
	
	req = blk_fetch_request(rq);
	while(req != NULL){
		...
		__rq_for_each_bio(bio, req){
			...
			bio_for_each_segment(bvec, bio, iter){
				buffer = __bio_kmap_atomic(bio, iter);
				offset = iter.bi_sector * VDSK_SECTOR_SIZE;
				nbytes = bvec.bv_len;
	
				if((offset+ nbytes)> get_capacity(vdskp->gd) * VDSK_SECTOR_SIZE){
					return;
				}
				
				if(bio_data_dir(bio) == WRITE)
					memcpy(vdskp->data+offset, buffer, nbytes);
				else
					memcpy(buffer, vdskp->data+offset, nbytes);
					
				__bio_kunmap_atomic(bio);
			}
			...
		}
		...
		if(!__blk_end_request_cur(req, 0))
			req = blk_fetch_request(q);
	}

}

首先就是定义衍生的BDEV。这里是Vdsk_dev。
然后我们全局实例化这个vdsk_dev。但是,这里使用了一个小技巧,就是“实体标签”(InstanceTag)。并没有静态分配vdsk_dev的实体,而只是静态分配了vdsk_dev的一个InstanceTag。

然后我们全局实例化一个驱动接口。这里是vdsk_bdops 。
其中的vdsk_getgeo函数,我们编写函数。

然后我们编写模块加载函数,这里是vdsk_init。在函数里,我们完成了模块的部署,包括对象创建,内核注册等。
我们首先从内核中申请了合法的major。
然后创建并初始化了vdsk的实体,并用全局的句柄来标记这个实体。
然后请求内核服务提供了一个可用的request_queue的对象句柄,并填充到vdsk的成员中。然后初始化这个request queue。为这个request queue绑定了Callback,并设置了queue 的回溯引用指针,将vdsk传递给queue。
vdsk->queue->queuedata = vdsk;
形成环路。
然后创建并初始化了gendisk的实体,并填充到vdsk的成员中。其中,用snprintf把一个字符串拷贝给disk_name。我们这里取名是vdska。这里,同样设置了gd的回溯引用指针,将vdsk传递给gd。
vdsk->gd->private_data = vdsk;
形成环路。
然后,向内核注册gendisk。

然后,我们编写模块卸载函数,执行逆操作。

然后,我们编写驱动操作函数集。这里是vdsk_open,vdsk_release,vdsk_ioctl等。
我们为request queue配置了Callback,这是需要向内核提供的服务函数。
我们编写这个Callback.其主体操作就是while中循环处理每个一个req。这里使用了三层嵌套循环。第一层是while,第二层是for_each_bio,每次取出一个BIO,第三层是for_each_segment,每次取出一个BIO_VEC.

我们在SHELL中对这个DISK进行测试。

#depmod
#modprobe vdsk
vdska: unknown partition table
# fdisk /dev/vdska
p primary partition(1-4)
:[p]
partition number(1-4):
[1]
first cylinder:
[1]
last cylinder:
[256]
w write partition tabel
[w]
partition table has been altered.
vdska:vdska1

#mkfs.ext2 /dev/vdska1
filesystem label=
os type : linux
block size=1024
fragment size=1024
2048 inodes,8184 blocks
409 blocks(5%) reserved for the super user
first data block=1
1 block group
8192 blocks per group, 8192 fragments per group
2048 inodes per group

#mount -t ext2 /dev/vdska1 /mnt
#echo "hello block device" > /mnt/test.txt
#cat /mnt/test.txt
hello block device

#rm /mnt/test.txt
#umount /mnt
#rmmod vdsk

这里面注意几个命令,
fdisk,我们创建的裸磁盘名字是/dev/vdska,经过格式化后,出现了分区名字/dev/vdska1.

mkfs.ext2,用来为格式化后创建的分区/dev/vdska1部署文件系统。

mount用来将一个BDEV的Partition挂载到某个目录上。当我们把/dev/vdska1这个partition挂载到/mnt后,后续的对路径的解析就会去找vdska1上的文件组织。
例如后面使用的/mnt/test.txt,其实是vdska1_ext2_root/test.txt。
为什么不能直接使用/dev/vdska1/test.txt呢?
linux中的文件规则是只能是directory才能进行后续的路径解析。
/mnt是一个directory,但是/dev/vdska1 它是一个Partition,不是directory。所以必须mount以后才能正常使用。当然,如果把partition作为一个设备文件来用,也是可以的,但是只能用direct_access方式。

linux中,MMC/SD是一种常用的BDEV,drivers/mmc中是相关的驱动。
又分为card,core, host三个子目录。
其中card中实现BDEV的驱动,它和BDEV subsystem对接。具体的协议是经过core层的接口,最终通过host完成传输。
card中,除了有标准的MMC/SD的card之外,还有一些使用SDIO接口的外设,例如sdio_uart.c。
core目录中除了给card提供接口之外,也定义了host驱动的框架。

架构层次上,FS使用card层提供的服务,card层使用core层提供的服务,core层使用host层提供的服务。
例如:
drivers/mmc/card/queue.c中,定义了mmc_init_queue()函数,

int mmc_init_queue(struct mmc_queue*mq, struct mmc_card*card, spinlock_t*lock, const char* subname)
{
	...
	mq->queue = blk_init_queue(mmc_request_fn, lock);
	...
}

从中可以看到,实际上,它使用了BDEV的request_queue。

mmc_request_fn会唤醒MMC对应的内核线程来处理请求,该线程对应的处理函数是mmc_queue_thread(),在线程中会调用MMC对应的Callback,mq.issue_fn().

static int mmc_queue_thread(void* d)
{
	...
	req = blk_fetch_request(q);
	mq->mqrq_cur->req = req;
	...
	mq->issue_fn(mq, req);
	...
}

这个callback指向drivers/mmc/card/block.c中的mmc_blk_issue_rq()函数。

static struct mmc_blk_data* mmc_blk_alloc_req(...)
{
	...
	md->queue.issue_fn = mmc_blk_issue_rq;
	md->queue.data = md;
	...
}

mmc_blk_issue_rq函数,最终会调用drivers/mmc/core/core.c中的mmc_start_req函数。

static int mmc_blk_issue_rw_rq(...)
{
	...
	areq = mmc_start_req(card->host, areq, (int *)&status);
	...
}

从中可以看出,这个core中的函数,利用card绑定的host的驱动接口,又调用了host驱动函数。位于drivers/mmc/host目录中。
如,
host->ops->pre_req(),
host->ops->enable(),
host->ops->disable(),
host->ops->request()。

ops是一个驱动操作集接口,

struct mmc_host_ops{
	(*pre_req)(...);
	(*enable)(...);
	(*disable)(...);
	(*request)(...);
	...
};

目前大多数SOC的MMC/SDIO控制器,都是SDHCI(secure digital host controller interface),所以,一般都是直接重用drivers/mmc/host/sdhic.c驱动文件,或者drivers/mmc/host/sdhic-pltfm.c文件。