|
本文档主要分析基本的xen IO 流程,包括如下几个方面:
Ø Tapdisk2进程的分析
Ø 共享环的机制
Ø 数据IO的交互流程
1 tapdisk2进程
首先对tapdisk2进程进行一下简单的介绍,因为几乎所有的操作都离不开这个进程。针对每个虚拟机磁盘,dom0会在用户态分别为磁盘创建一个tapdisk2进程,该进程主要负责虚拟机磁盘和dom0内核之间的数据和命令的交互。总的说来,它是一个单线程处理流程,以“监听-处理”的运行方式循环进行。基本流程图如下:
图中的 “accept new socket”和 “request call back”是两个回调过程,中间的虚线箭头由“回调注册者”指向“回调过程”,但回调函数具体执行是在select后才会执行。
Tapdisk2 大体框架的“监听-处理”基本没有什么复杂的地方,就是tcp ip 绑定-监听-处理新请求的老套路,后面对于新请求的处理才是它的主要工作。对于新的请求,也会产生一些新的fd ,这时tapdisk2 会将新fd 生成一个event插入到evnet链表,这样,全部的事情都由一个单一的select 负责调度完全。
图中没有标出对异步IO的初始化过程,在tapdisk2 代码中由tapdisk_server_init_aio函数完成,这个过程仅仅是初始化一个异步IO的环境。Tapdisk2 用的是linux aio 异步IO接口。
当外部控制向tapdisk2 control socket 发起连接时,select监测到并回调 accept过程,连接一个新的socket ,并将新socket 加入select 对象中。当命令从新socket传递过来时,select 监测到并回调 request callback 过程,执行相应的命令,最后从socket返回结果,执行关闭新socket 等回收过程,如此便完成了一次命令的交互过程。
2 共享环
虚拟机domU 本身不读写虚拟机磁盘,对虚拟机磁盘的读写,都是dom0用户态程序tapdisk2完成的。domU会将IO请求最终转发给tapdisk2程序,tapdisk2完成读写后,再通知给domU。共享环在这中间起到一个关键性的作用。这里主要分析共享环在tapdisk2和dom0内核中的交互。IO数据流程示意图如下:
针对上图,有两点需要说明:
1 图中红色表示请求的处理流程,黄色表示响应的处理流程。在dom u 磁盘tap块设备和blktap之间,没有标出黄色的响应交互过程,该部分流程是将真实I/O的访问地址映射到dom u可以访问的地址(内核中的blktap_device_end_request函数完成),目前未完全分析透彻,故未列出。
2 dom U 并不在 dom0 的用户空间,图中dom U 的位置仅仅是为了方便画图。
2.1 共享环的建立
当一个新的虚拟机启动后,dom0为每个磁盘都会启动一个tapdisk2 进程。针对一个单独的tapdisk2而言,此时外部(由tap-ctl程序负责)会发送一个TAPDISK_MESSAGE_ATTACH给tapdisk2进程,创建一个新的socket执行命令。
完整的流程是这样的,在虚拟机启动时,tap-ctl会调用tap_ctl_create 函数,该函数负责内核tap块的分配,发送TAPDISK_MESSAGE_ATTACH和TAPDISK_MESSAGE_OPEN命令给tapdisk2进程。
环,在dom0中对应一个字符设备,tap_ctl_create调用tap_ctl_allocate_device向内核注册一个字符设备,节点/dev/xen/blktap-2/blktap%minor 对应内核中的环设备。关于环
字符设备的注册可以查看内核中的blktap_control_ioctl函数。
此后,tap_ctl 向 tapdisk2 发送TAPDISK_MESSAGE_ATTACH和TAPDISK_MESSAGE_OPEN命令。
当tapdisk2处理TAPDISK_MESSAGE_ATTACH消息时,将打开字符设备/dev/xen/blktap-2/blktap%minor,调用其mmap 方法,调用BACK_RING_INIT初始化环内存,在用户态建立“环后端”。同时内核的mmap对应方法,调用FRONT_RING_INIT初始化环内存,在内核态建立“环前端”。“环前段”和“环后端”看到的地址是同一份共享地址,目前大小是一页(4K), 除去环的一部分头信息,环中元素共有32个。
环中元素需要特别说明一下,元素个数是32个,每个元素又分别包含一个请求和响应,所以请求可以单独有32个,响应也可以单独有32个,请求和响应永远都不会互相抢占空间,它们并不是公用32个空间,而且各自拥有独立的32个空间。
至于环的读写,无非就是一个“生产者和消费者”模型,tapdisk2 环后端是请求的消费者,响应的生产者,内核中的环前端是请求的生产者、响应的消费者。
2.2 初始化环
2.2.1 初始化共享环
首先初始化共享环,前面说了,内核用一个页面共享内存的大小来映射共享环,是在内核函数blktap_ring_mmap中完成。随后调用SHARED_RING_INIT初始化共享环。
#define SHARED_RING_INIT(_s)do { \
(_s)->req_prod = (_s)->rsp_prod = 0; \
(_s)->req_event = (_s)->rsp_event =1; \
memset((_s)->pad, 0,sizeof((_s)->pad)); \
} while(0)
上面的_s 其实就是下面的结构体:
/* Shared ring page */ \
struct __name##_sring { \
RING_IDX req_prod, req_event; \
RING_IDX rsp_prod, rsp_event; \
union { \
struct { \
uint8_t smartpoll_active; \
} netif; \
struct { \
uint8_t msg; \
} tapif_user; \
uint8_t pvt_pad[4]; \
} private; \
uint8_t pad[44]; \
union __name##_sring_entry ring[1]; /*variable-length */ \
};
此时共享环的结构图如下:
2.2.2 初始化前端环:
前端环还是在内核中,紧接着共享环之后被初始化,位于函数blktap_ring_mmap中。
SHARED_RING_INIT(sring);
FRONT_RING_INIT(&ring->ring, sring,PAGE_SIZE);
#define FRONT_RING_INIT(_r, _s, __size) do { \
(_r)->req_prod_pvt = 0; \
(_r)->rsp_cons = 0; \
(_r)->nr_ents = __RING_SIZE(_s, __size); \
(_r)->sring = (_s); \
} while (0)
上面的_r 就是下面的结构体:
struct __name##_front_ring { \
RING_IDXreq_prod_pvt; \
RING_IDXrsp_cons; \
unsignedint nr_ents; \
struct__name##_sring *sring; ---> 就是指向上面的共享环 \
};
此时前端环和共享环的关系图如下:
2.2.3 初始化后端环
后端环在tapdisk2 中初始化,位于tapdisk_vbd_map_device函数中。
ring->mem = mmap(0, psize *BLKTAP_MMAP_REGION_SIZE,
PROT_READ | PROT_WRITE, MAP_SHARED,ring->fd, 0);
if(ring->mem == MAP_FAILED) {
err= -errno;
EPRINTF("failedto mmap %s: %d\n", devname, err);
gotofail;
}
ring->sring= (blkif_sring_t *)((unsigned long)ring->mem);
BACK_RING_INIT(&ring->fe_ring,ring->sring, psize);
可想而知,上面的ring->sring 就是共享环,内核和用户态都是调用mmap映射同样的一页内存。
#define BACK_RING_INIT(_r, _s, __size) do { \
(_r)->rsp_prod_pvt = 0; \
(_r)->req_cons = 0; \
(_r)->nr_ents = __RING_SIZE(_s, __size); \
(_r)->sring = (_s); \
} while (0)
上面的_r就是下面的结构体:
/* "Back" end's private variables */ \
struct __name##_back_ring { \
RING_IDXrsp_prod_pvt; \
RING_IDXreq_cons; \
unsignedint nr_ents; \
struct__name##_sring *sring; \
};
初始化后端环后,前后端环与共享环的关系如下:
2.3 共享环的操作
目前环有四种操作,放入请求-取出请求-放入响应-取出响应。代码中用是四个宏进行表示,分别是:
RING_PUSH_REQUESTS
RING_GET_REQUEST
RING_PUSH_RESPONSES
RING_GET_RESPONSE
#define RING_PUSH_REQUESTS(_r) do { \
wmb(); /*back sees requests /before/ updated producer index */ \
(_r)->sring->req_prod = (_r)->req_prod_pvt; \
} while (0)
#define RING_PUSH_RESPONSES(_r) do { \
wmb(); /*front sees responses /before/ updated producer index */ \
(_r)->sring->rsp_prod = (_r)->rsp_prod_pvt; \
} while (0)
RING_PUSH_REQUESTS和RING_PUSH_RESPONSES 这两个宏函数修改环的索引,并没有对真实的环元素进行操作,类似给环中数据打个标记,具体需要操作数据时再根据标签来获取已经可以处理的数据。可想而知,(_r)->sring->req_prod 表示当前共享环中实际的请求生产者索引,(_r)->sring->rsp_prod 表示当前共享环中实际的响应生产者的索引。共享环中没有保存消费者的索引,消费者索引由前端环和后端环自己去保存,而且可以看出,前后端环的目前消费者索引,等于共享环上次的生产者索引。而“目前的消费者索引”与“共享环本次的生产者索引”二者之间的差值,就是本次可以消费的请求或者响应的个数。
举个例子,在tapdisk2检测到共享环有请求的时候,在tapdisk_vbd_pull_ring_requests函数中处理流程如下:
rp =ring->fe_ring.sring->req_prod;
xen_rmb();
for (rc= ring->fe_ring.req_cons; rc != rp; rc++) {
req= RING_GET_REQUEST(&ring->fe_ring, rc);
++ring->fe_ring.req_cons;
……
}
上面代码一目了然,ring->fe_ring.req_cons是后端环目前的请求消费者索引,而 rp 是共享环中的请求生产者索引,二者的差值就是本次可以消费的请求个数,代码中用一个for循环取出了所有本次可以消费的请求,并进行处理。
说起索引,我们来看看前段和后端中索引的含义:
#define FRONT_RING_INIT(_r, _s, __size) do { \
(_r)->req_prod_pvt = 0; \
(_r)->rsp_cons = 0; \
(_r)->nr_ents = __RING_SIZE(_s, __size); \
(_r)->sring = (_s); \
} while (0)
#define BACK_RING_INIT(_r, _s, __size) do { \
(_r)->rsp_prod_pvt = 0; \
(_r)->req_cons = 0; \
(_r)->nr_ents = __RING_SIZE(_s, __size); \
(_r)->sring = (_s); \
} while (0)
req_prod_pvt 请求生产者索引;rsp_prod_pvt 响应消费者索引,对应内核中的环前段。rsp_prod_pvt响应生产者索引;req_cons请求消费者索引,对应用户态中的环后端。
RING_GET_REQUEST 和RING_GET_RESPONSE 两个宏会取出当前环中指定元素的地址,之后可以利用memcpy类似的动作从地址读出环中真实数据,或者向环中写入真实数据。
到此,所有环的基本操作都讲完了。
3 数据IO的基本流程
图2 DOM U 与 DOM 0 IO 总体图
Xen IO 交互是一个比较复杂的事情,细节比较多,面面俱到比较困难,只能一步步走,代码一步步看。本文准备先介绍tap U 到 tapdisk2 之间的流程。至于两头的流程,另外进行单独分析。
3.1 请求生产者
IO请求的源头来自domU, 中间取决于是否安装半虚拟化驱动,可能经过“前端-后端”或者“qemu”,但是最后都要经过虚拟机磁盘对应的tap 块设备(内核中用tdx设备来表示),由内核中的blktap 写入共享环中。这里就是请求的生成者。当用户在虚拟机domU中进行IO访问时,该请求最终被写入共享环。
对于tap设备的监测,内核中是放在blktap_ring_poll函数中进行的,名称可能有点不符合,对于tap的检查,怎么放到ring中呢?但是的确如此。看代码:
static unsigned int blktap_ring_poll(struct file*filp, poll_table *wait)
{
structblktap *tap = filp->private_data;
structblktap_ring *ring = &tap->ring;
intwork;
poll_wait(filp,&tap->pool->wait, wait);
poll_wait(filp,&ring->poll_wait, wait);
down_read(¤t->mm->mmap_sem);
if(ring->vma && tap->device.gd)
blktap_device_run_queue(tap);
up_read(¤t->mm->mmap_sem);
work =ring->ring.req_prod_pvt - ring->ring.sring->req_prod;
RING_PUSH_REQUESTS(&ring->ring);
if(work ||
ring->ring.sring->private.tapif_user.msg ||
test_and_clear_bit(BLKTAP_DEVICE_CLOSED,&tap->dev_inuse))
returnPOLLIN | POLLRDNORM;
return0;
}
内核中的poll_wait(filp, &tap->pool->wait,wait)在监听 tap上的IO请求,经过
blktap_device_run_queue--->blktap_device_make_request-->blktap_ring_submit_request一系列复杂的准备工作,最终请求被写入共享环。
这中间一系列复杂的准备工作,涉及请求内存的映射,可以暂时不管。最后在blktap_ring_submit_request函数中,可以清楚的看到,IO请求通过RING_GET_REQUEST获取环中空元素的地址,然后将真实请求填入空元素,最后更新环的ring->ring.req_prod_pvt 个数,这里已经很清楚了,该变量就是请求生产者在环中当前位置的索引。
再回到blktap_ring_poll函数,看如下代码:
work = ring->ring.req_prod_pvt -ring->ring.sring->req_prod;
RING_PUSH_REQUESTS(&ring->ring);
ring->ring.req_prod_pvt中是当前实际请求生产者的索引,ring->ring.sring->req_prod是共享环目前请求生产者的索引,二者之间的差值,是此次新生成的请求,最后通过RING_PUSH_REQUESTS将新产生的请求提交入环,至此,请求生产者完成它的工作。
3.2 请求消费者
此时回到用户态进程tapdisk2,之前共享环建立的过程中,tapdisk2已经在tapdisk_vbd_attach函数中对环设备进行了select监控,由于请求生产者已经向该环设备写入信息,此时消费者回调函数tapdisk_vbd_ring_event将执行,开始消费输入的请求。
这里又是几个老套路了。tapdisk_vbd_pull_ring_requests 函数取出环中所有新来的请求。关键代码如下:
rp =ring->fe_ring.sring->req_prod;
xen_rmb();
for (rc = ring->fe_ring.req_cons; rc != rp;rc++) {
req= RING_GET_REQUEST(&ring->fe_ring, rc);
++ring->fe_ring.req_cons;
idx = req->id;
vreq= &vbd->request_list[idx];
ASSERT(list_empty(&vreq->next));
ASSERT(vreq->secs_pending== 0);
memcpy(&vreq->req,req, sizeof(blkif_request_t));
vbd->received++;
vreq->vbd= vbd;
tapdisk_vbd_move_request(vreq,&vbd->new_requests);
同样的,ring->fe_ring.req_cons 指向前端环请求消费者索引(可以理解为上次请求消费到此位置),ring->fe_ring.sring->req_prod 指向共享环请求生产者,二者之前的差值,就是此次可以消费的请求个数。代码中通过memcpy拷贝请求信息,最后将元素移到new_requests列表。
随后调用tapdisk_vbd_issue_requests 处理每个请求,中间调用tapdisk_vbd_reissue_failed_requests对上次失败的请求进行重新提交,不细讲。最后通过tapdisk_vbd_issue_new_requests处理真正的请求。
tapdisk_vbd_issue_new_requests中循环调用tapdisk_vbd_issue_request,将new_requests上的元素都移入到pending_requests列表,同时提交异步IO请求。前面说过,tapdisk2初始化时搭建了一个异步IO请求的环境,虚拟机磁盘可能有多种类型,如VHD.LVHD.RAW等,这里通过调用不同的读写方法,进行IO请求的提交。磁盘不同读写方法的挂接准备是在tapdisk2处理TAPDISK_MESSAGE_OPEN消息时做的,有兴趣的可以看看代码。
其实这个地方还没有提交IO请求到虚拟机磁盘,而只是将请求放到请求队列,在下次tapdisk2 select处理时,才会提交IO请求到真实的虚拟机磁盘,此处可能是为了考虑效率。
这里tapdisk_vbd_issue_request函数注册一个tapdisk_vbd_complete_td_request 回调(后面还会提到),当异步IO读写真正完成时,会回调该函数,这个函数最终会调用tapdisk_vbd_complete_vbd_request函数,将pending_requests上面的请求移入completed_requests或failed_requests(取决于成功或者失败)。回调的过程是内核自动完成的,与tapdisk2调度无关。
3.3 响应生产者
当内核完成真正的IO请求后,需要将响应写入共享环,以通知内核,最后内核转达到domU 。将响应写入共享环,也是由tapdisk2完成的。
前面说过,tapdisk2是一个select 单循环,可谓是一条路走到黑。其循环处理代码如下:
void
tapdisk_server_iterate(void)
{
intret;
tapdisk_server_assert_locks();
tapdisk_server_set_retry_timeout();
tapdisk_server_check_progress();
ret =scheduler_wait_for_events(&server.scheduler); 事件的select
if (ret< 0)
DBG(TLOG_WARN,"server wait returned %d\n", ret);
tapdisk_server_check_vbds();磁盘状态检查
tapdisk_server_submit_tiocbs();异步IO提交到磁盘
tapdisk_server_kick_responses();主要是向共享环写入响应
}
tapdisk_server_submit_tiocbs() 方法上将积累的IO请求真正提交到虚拟机磁盘,它最终调用的是tapdisk_lio_submit函数,该处涉及linux异步IO 库aio 的处理,有必要以后单独写个文档进行分析, 这里不再细讲。
响应生产者其实是由内核异步IO完成,最后tapdisk2调用tapdisk_server_kick_responses函数将响应消息推入共享环。
tapdisk_server_kick_responses调用tapdisk_vbd_kick:
int
tapdisk_vbd_kick(td_vbd_t *vbd)
{
int n;
td_ring_t*ring;
tapdisk_vbd_check_state(vbd);这是一个关键的地方,下面再讲
ring =&vbd->ring;
if(!ring->sring)
return0;
n = (ring->fe_ring.rsp_prod_pvt -ring->fe_ring.sring->rsp_prod);
if (!n)
{
return0;
}
vbd->kicked+= n;
RING_PUSH_RESPONSES(&ring->fe_ring);
ioctl(ring->fd,BLKTAP_IOCTL_KICK_FE, 0);
returnn;
}
上面的(ring->fe_ring.rsp_prod_pvt -ring->fe_ring.sring->rsp_prod) 又是老套路了,用前端环响应生产者的索引减去之共享环响应生产者的索引,二者差值就是新生成出的响应,调用RING_PUSH_RESPONSES推入共享环,最后调用环命令BLKTAP_IOCTL_KICK_FE,在内核中进行处理。其实RING_PUSH_RESPONSES只是更新一下共享环响应生产者索引的位置,并没有真正做事,内核处理BLKTAP_IOCTL_KICK_FE消息时根据这个索引来真正处理响应。
前面说过异步IO完成时内核回调tapdisk_vbd_complete_td_request 函数,它肯定也是ring->fe_ring.rsp_prod_pvt 和 pending_requests 的更新者,不然这两个变量就看不到更新者了。再来看看tapdisk_vbd_complete_td_request 函数,它最后调用tapdisk_vbd_move_request函数,将已经完成的IO请求从pending_requests移除到completed_requests或failed_requests(取决于成功或者失败),但是没有看到更新ring->fe_ring.rsp_prod_pvt的地方。难道我们忽略了什么?
回去看看,tapdisk_vbd_kick 中还调用了tapdisk_vbd_check_state函数,通过名称以为只是检查vbd 的状态,其实它的工作远远不止怎么简单,说明这个函数的名称起的一点都不恰当。
在tapdisk_vbd_check_state中,针对每一个完成的异步IO请求,都进行了处理,代码如下:
tapdisk_vbd_for_each_request(vreq, tmp,&vbd->completed_requests) {
tapdisk_vbd_make_response(vbd,vreq);
list_del(&vreq->next);
tapdisk_vbd_initialize_vreq(vreq);
}
关键函数是tapdisk_vbd_make_response ,它调用了一个回调函数vbd->callback(vbd->argument, rsp) 。
通过查找,我们发现这个回调函数注册的时间非常之早,在tapdisk_vbd_create函数中进行了注册(个人感觉有时候回调设计完全没有理性可言,可能是C言语用来实现面向对象思想而调用父类方法的一种替代,虽然可以实现,但并不友好),该回调函数是tapdisk_vbd_callback,它调用tapdisk_vbd_write_response_to_ring函数如下:
static inline void
tapdisk_vbd_write_response_to_ring(td_vbd_t *vbd,blkif_response_t *rsp)
{
td_ring_t*ring;
blkif_response_t*rspp;
ring =&vbd->ring;
rspp =RING_GET_RESPONSE(&ring->fe_ring, ring->fe_ring.rsp_prod_pvt);
memcpy(rspp,rsp, sizeof(blkif_response_t));
ring->fe_ring.rsp_prod_pvt++;
}
我们终于看到了ring->fe_ring.rsp_prod_pvt的更新者,这里用RING_GET_RESPONSE取出共享环中元素的地址,然后将rsp 中的内容拷贝到环中,又是老套路了,再次说明tapdisk_vbd_check_state函数的名称取的一点也不恰当。
3.4 响应消费者
上文说了,tapdisk_vbd_kick 调用IO 命令BLKTAP_IOCTL_KICK_FE,此刻我们又要回到内核。
blktap_ring_ioctl 函数处理BLKTAP2_IOCTL_KICK_FE 消息,二者字符不一样,但的确是表示同一个消息。
blktap_read_ring 函数关键代码如下:
rp = ring->ring.sring->rsp_prod;
rmb();
for (rc= ring->ring.rsp_cons; rc != rp; rc++) {
memcpy(&rsp,RING_GET_RESPONSE(&ring->ring, rc), sizeof(rsp));
blktap_ring_read_response(tap,&rsp);
}
上面又是老套路,取出所有响应,进行处理,不再多讲。
blktap_ring_read_response –>blktap_device_end_request最终对响应中的磁盘地址进行了地址映射,将内存映射到虚拟机可以访问的位置上。这部分比较复杂,准备以后有机会单独分析透彻后,再写一个文档。至此,xen IO 的基本流程大致分析完成。
5 遗留问题
Ø Tapdisk2 的异步IO读写机制,以及不同格式磁盘的读写方式
Ø 虚拟机请求到达tap块之间的交互,即 QEMU或者PV驱动这部分需要分析
Ø 最后响应结果如何映射返回到虚拟机的交互,也需要再分析
版权声明:本文为博主原创文章,未经博主允许不得转载。 |
|