最初刚开始玩Sheepdog(Distributed Storage System for QEMU/KVM)的时候就想着要是能把它作为一个像硬盘一样的块设备,直接挂在终端机器上就好玩了,直到最近工作不是特别忙的时候才有时间想想这事情,花了好几个晚上终于把给Sheepdog写好了一个块设备驱动sheepdev,可以把集群中创建好的VDI直接注册到kernel中,然后使用用户态的相关工具fdisk/mkfs/mount等对这个块设备进行分区格式化挂载等,代码已经提交到upstream,至于还要修改几次,最终能不能merge到upstream中去都还不确定,先简单记录一下自己的工作经历。
块设备驱动作为一个kernel module,在module init的时候我注册了一个proc entry /proc/sheep,通过这个proc entry对这个块设备进行控制,比如:
echo “add 127.0.0.1:7070 a5d05d” > /proc/sheep
可以将VDI Id为a5d05d的VDI作为一个块设备安装到本地,/dev目录下会出现一个/dev/sheepa这样的块设备文件,添加第二个后出现/dev/sheepb,以此类推。
也可以通过下面的方法来删除已经注册上来的块设备:
echo “del sheepa” > /proc/sheep
在安装块设备的时候根据用户提供的VDI Id和sheep地址,首先来向sheep创建一个长连接,并通过该连接向sheep获取该VDI的inode数据,该长连接创建后会一起保持用于driver与sheep的通信,只有获取inode的过程是同步的,后期的读写操作都是异步进行的。
在获取到inode数据之后,inode中包含了这个VDI的size等信息,在创建disk之前这个size信息是唯一需要从sheep获取的,关于sector size这些信息,由于我们是一个虚拟的块设备,都延用了内核通用的块设备大小512字节,sectors数也是通过inode->size/kernel_sector_size计算出来的,创建disk的函数如下:
static int sheep_add_disk(struct sheepdev *dev) { int ret; struct request_queue *queue; dev->disk = alloc_disk(SHEEP_BLKDEV_MINORS); if (!dev->disk) { DBPRT("allocate gendisk failuren"); ret = -EBUSY; return ret; } queue = blk_init_queue(sheep_request, &dev->que_lock); /* 4M boundary */ blk_queue_segment_boundary(queue, 0x3fffff); dev->disk->major = sheepdev_major; dev->disk->first_minor = dev->minor * SHEEP_BLKDEV_MINORS; dev->disk->queue = queue; dev->disk->fops = &sheepdev_ops; dev->disk->private_data = dev; snprintf(dev->disk->disk_name, sizeof(dev->disk->disk_name), SHEEP_BLKDEV_NAME"%c", dev->minor + 'a'); set_capacity(dev->disk, dev->sectors); add_disk(dev->disk); return 0; }
调用set_capacity()之后kernel会调用check_partition()进行分区检查,这时候如果request处理流程还没有完成的话,kernel会hang在set_capacity(),所以在最初编码测试的阶段add_disk()之前我先把capacity设置为0。
在这里调用了blk_queue_segment_boundary()这个函数把请求的boundary设置了4M,sheepdog的文件存储是4M大小的固定块文件,超出4M处理起来就麻烦了。
在add_disk()函数完成之后kernel就开始发request过来了,这时候的request处理函数必须要打通的。
request中包含了经过io scheduler合并过的bio,我们只需要把这些bio连接起来当做一个大的读/写请求发出去便可以了,sheepdev用了一个单一个长连接来处理sheep的读写请求,但创建了两个内核线程sheep_req和sheep_fin。
sheep_request()这个callback函数会将block层发来的请求摘出来放到一个pending_request队列中,每放进一个请求后就会唤醒sheep_req线程,sheep_req这个kernel thread做的事情是从这个队列中把请求取出来,然后根据请求的内容来确定是读还是写,然后把读/写的数据连接合并,不论读写sheep_req都会把请求直接发送出去,而不会同步地等待返回的数据,然后sheepdev会把这个请求再放入一个finish_list队列中,这时候请求附加了一个sheepdog协议中的请求号,每一个请求客户端都会对其进行+1,一般的cs通信协议都会有这样一个请求ID号,用于协议的异步处理,我之前的hybrid聊天客户端也用了这种办法。
sheep_req在向finish_list中放入一个请求后便会去唤醒sheep_fin这个kernel thread,sheep_fin被唤醒后会去读与sheep的长连接中的数据,获取一个请求的response,这个response消息中包含了一个请求ID,sheepdev根据这个ID来从finish_list中找出对应的request,并根据response的结果调用下面的函数结束这个request:
static void sheep_end_request(struct request *req, int ret) { struct request_queue *q = req->q; unsigned long flags; spin_lock_irqsave(q->queue_lock, flags); __blk_end_request_all(req, ret); spin_unlock_irqrestore(q->queue_lock, flags); }
Sheepdog我认为有一点设计不合理的地方就是在创建某个object对象之后,对应的inode数据不会被sheep自动更新,而是需要依靠客户端程序来更新,这样就给客户端的实现带来了一些繁琐,如果修改的话涉及到的代码范围比较广(collie/qemu),我也不打算对它做什么修改了。
由于Sheepdog这个问题,对于SD_OP_CREATE_AND_WRITE_OBJ这样的请求而言,它返回后并不能代表这个block request的结束,必须等待更新inode的请求结束后才可以决定对应的block request是否结束。
关于这个驱动目前核心的就这些东西,没有什么特别复杂的东西,只是一些kernel driver设计的繁琐的东西,原理了解了就很简单了。
FROM:http://basiccoder.com/sheepdog-block-device-driver.html
转载请注明:爱开源 » Sheepdog块设备驱动