您的位置:首页 > 其它

xen io tapdisk2基本流程分析

2014-01-03 18:38 387 查看
本文档主要分析基本的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驱动这部分需要分析

Ø 最后响应结果如何映射返回到虚拟机的交互,也需要再分析
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: