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文件。