内容简介:Event-channel in Xen
这两天在看Xen的事件通知event-channel机制。同时也很大程度上参考了小明的 这篇博客 ,欢迎围观。
废话不多说,直接进入正题。
首先,关于event-channel的概念,可以直接参照小明的博客:
Event Channel是Xen提供的通信机制,Xen允许Guest将以下四种中断映射成为event-channel。
- 利用Pass-through的方式将硬件直接交给某个Guest,或使用支持SR-IOV的硬件时是可以直接使用这个中断的。
- 但是Guest有的时候需要一些中断(e.g. 时钟中断)来完成某个功能,因此Xen提供了虚拟中断(VIRQ),Hypervisor设置某个bit使得Guest以为有了中断。
- Interdomain communication, 虚拟机之间的通信需要依赖某个机制。
- Intradomain communication, 虚拟机内部通信,属于Interdomain Communication的一种特殊情况, (DomID相同,cpuID不同)
在event-channel部分, remote port往往与evtchn互换使用,而local port则与irq互换使用。
一个Guest会通过event-channel绑定到一个事件源(event source)上,并设置对应的handler. 其中,事件源可以是另一个dom的port(Case 3, 4), 真实的物理中断(Case 1)或者一个虚拟中断(Case 2)。当event-channel建好后,事件源就可以通过这个Channel发通知给接收端。
在这片文章中,我们主要来看看在Xen的实现中,基于虚拟机之间inter-domain的event-channel机制是如何实现的。
FIFO-based event-channel
首先我们先来看一下Xen中event channel的数据结构。在早期Xen的实现中,event-channel实现的是一种被称为2-level的event-channel,由于它的scalability不好,所以后来实现了一种被称为fifo-based event channel,这也是当前Xen默认使用的event-channel机制,有一个 文档 专门对其进行了描述。
如下图所示,简单来说,fifo-based event-channel将event-channel分成了16个不同的priority,每个priority对应一个queue,每个queue对应一组单链表形式组织的event array。每个event array都是由一系列32 bits长度的event words组成,而每个event word包含了3个bits(pending bit, mask bit和linked bit),以及一个17bits长度的link信息,指向下一个event word。
而在Xen的实现中,每个vcpu都有一个类型为 struct evtchn_fifo_vcpu 的变量 evtchn_fifo ,如下图所示:
里面的 event_fifo_queue 就是前面所说的对应于每个priority的queue,而queue中的head和tail域就会指向相应的event word。这些event word实际存在于event array中,在Xen的实现中,每个domain都有一个类型为 struct evtchn_fifo_domain 的变量 evtchn_fifo ,如下图所示:
其中 *event_array[] 存储了event array的实际数据。同时我们还可以看到,在 struct domain 数据结构中,还有一个类型为 struct evtchn * 的变量 evtchn 和类型为 struct evtchn ** 的变量 evtchn_group 。这些数组存储了event-channel的对象,他们通过一个两级的数据结构组成,如下图所示:
其中,每个group包含了一个page大小的bucket指针,而每个bucket包含了一个page大小的 struct evtchn 的数组。其中,第一个bucket(即bucket 0)可以直接通过 d->evtchn 变量访问。
因此,给定一个port,我们可以通过 evtchn_from_port 函数计算得出其相应的 struct evtchn 对象:
#define group_from_port(d, p) \
((d)->evtchn_group[(p) / EVTCHNS_PER_GROUP])
#define bucket_from_port(d, p) \
((group_from_port(d, p))[((p) % EVTCHNS_PER_GROUP) / EVTCHNS_PER_BUCKET])
static inline struct evtchn *evtchn_from_port(struct domain *d, unsigned int p)
{
if ( p < EVTCHNS_PER_BUCKET )
return &d->evtchn[p];
return bucket_from_port(d, p) + (p % EVTCHNS_PER_BUCKET);
}
另外,给定一个port,我们也可以通过 evtchn_fifo_word_from_port 函数计算得出其相应的 event word :
static inline event_word_t *evtchn_fifo_word_from_port(struct domain *d,
unsigned int port)
{
unsigned int p, w;
if ( unlikely(port >= d->evtchn_fifo->num_evtchns) )
return NULL;
p = port / EVTCHN_FIFO_EVENT_WORDS_PER_PAGE;
w = port % EVTCHN_FIFO_EVENT_WORDS_PER_PAGE;
return d->evtchn_fifo->event_array[p] + w;
}
HVM虚拟机中event-channel事件通知流程
在了解了event channel的数据结构之后,我们就可以来介绍整个事件通知的流程是怎么样的。这里我们利用PVHVM中的front-end driver举例进行说明。在PVHVM架构中,CPU和内存是通过VT-x的硬件虚拟化实现的,而I/O依然是通过split I/O实现的。在split I/O模型中,客户虚拟机中的front-end driver在往共享内存中填好I/O数据之后,会调用 flush_request 函数:
static int blkif_queue_rq()
{
...
blkif_queue_request(qd->rq, rinfo);
...
flush_requests(rinfo);
...
}
在 flush_request 函数中,会通过event-channel机制最终调用 HYPERVISOR_event_channel_op(EVTCHNOP_send) 通知hypervisor:
static inline void flush_requests(struct blkfront_ring_info *rinfo)
{
...
notify_remote_via_irq(rinfo->irq);
}
void notify_remote_via_irq(int irq)
{
int evtchn = evtchn_from_irq(irq);
...
notify_remote_via_evtchn(evtchn);
}
static inline void notify_remote_via_evtchn(int port)
{
struct evtchn_send send = { .port = port };
(void)HYPERVISOR_event_channel_op(EVTCHNOP_send, &send);
}
而在hypervisor中, EVTCHNOP_send 的处理函数会调用 evtchn_send ,在该函数中,会根据local port得到其相应的remote domain和remote port,然后调用 evtchn_set_pending 函数:
int evtchn_send(struct domain *d, unsigned int lport)
{
lchn = evtchn_from_port(d, lport);
switch ( lchn->state )
{
case ECS_INTERDOMAIN:
rd = lchn->u.interdomain.remote_dom;
rport = lchn->u.interdomain.remote_port;
rchn = evtchn_from_port(rd, rport);
evtchn_set_pending(rvcpu, rport);
break;
...
}
}
static void evtchn_set_pending(struct vcpu *v, int port)
{
v->domain->evtchn_port_ops->set_pending(v, evtchn_from_port(v->domain, port));
}
由于Xen采用的是fifo based event channel,所以最终会调用到 evtchn_fifo_set_pending 函数:
static void evtchn_fifo_set_pending(struct vcpu *v, struct evtchn *evtchn)
{
port = evtchn->port;
word = evtchn_fifo_word_from_port(d, port);
test_and_set_bit(EVTCHN_FIFO_PENDING, word);
...
q = &v->evtchn_fifo->queue[evtchn->priority];
if ( q->tail )
{
tail_word = evtchn_fifo_word_from_port(d, q->tail);
linked = evtchn_fifo_set_link(d, tail_word, port);
}
if ( !linked )
write_atomic(q->head, port);
q->tail = port;
...
vcpu_mark_events_pending(v);
...
}
在该函数中,会将driver domain中port相对应的event word中的pending bit设上,同时将其加入priority相对应的event queue中的tail中,并且将tail指向它。最后它会调用 vcpu_mark_events_pending 函数:
void vcpu_mark_events_pending(struct vcpu *v)
{
test_and_set_bit(0, (unsigned long *)&vcpu_info(v, evtchn_upcall_pending));
}
该函数会将 vcpu_info 中的 evtchn_upcall_pending 置上。
然后hypervisor就返回客户虚拟机了。可以看出,这个事件通知的机制是异步的,在其返回客户虚拟机的时候,该事件其实还并没有被通知到back-end driver。那么,back-end driver又是什么时候被通知到要处理相应的时间的呢?
在Xen需要返回back-end driver所在的driver domain(我们假设driver domain为HVM)的时候,即在调用vmentry之前,会首先调用一个 vmx_intr_assist 函数:
... .Lvmx_do_vmentry: call vmx_intr_assist ... VMLAUNCH
void vmx_intr_assist(void)
{
do {
intack = hvm_vcpu_has_pending_irq(v);
...
}
...
vmx_inject_extint(intack.vector, intack.source);
}
其中 hvm_vcpu_has_pending_irq 会根据 evtchn_upcall_pending 的值返回一个 source 为 hvm_intsrc_vector 的 struct hvm_intack 数据结构:
struct hvm_intack hvm_vcpu_has_pending_irq(struct vcpu *v)
{
struct hvm_domain *plat = &v->domain->arch.hvm_domain;
if ( (plat->irq.callback_via_type == HVMIRQ_callback_vector)
&& vcpu_info(v, evtchn_upcall_pending) ){
return hvm_intack_vector(plat->irq.callback_via.vector);
}
...
}
而 vmx_inject_extint 则会将该 struct hvm_intack 中的 vector 值写入VMCS中的 VM_ENTRY_INTR_INFO 域中。因此在vmentry回driver domain的时候,则会根据这个 vector 的值触发相应的handler。
那么,这个 source 为 hvm_intsrc_vector 的 struct hvm_intack 中的 vector 域是什么呢?
我们发现,在每个HVM虚拟机(包括driver domain)启动的时候,会调用一个 xen_set_callback_via 函数:
int xen_set_callback_via(uint64_t via)
{
struct xen_hvm_param a;
a.domid = DOMID_SELF;
a.index = HVM_PARAM_CALLBACK_IRQ;
a.value = via;
return HYPERVISOR_hvm_op(HVMOP_set_param, &a);
}
void xen_callback_vector(void)
{
callback_via = HVM_CALLBACK_VECTOR(HYPERVISOR_CALLBACK_VECTOR);
rc = xen_set_callback_via(callback_via);
...
alloc_intr_gate(HYPERVISOR_CALLBACK_VECTOR, xen_hvm_callback_vector);
}
它将 HYPERVISOR_CALLBACK_VECTOR 对应的handler设置成了 xen_hvm_callback_vector
而在Xen处理这个hypercall时,会调用 hvm_set_callback_via 函数:
void hvm_set_callback_via(struct domain *d, uint64_t via)
{
...
switch ( hvm_irq->callback_via_type = via_type )
{
...
case HVMIRQ_callback_vector:
hvm_irq->callback_via.vector = (uint8_t)via;
break;
...
}
}
所以在这里它将 source 为 hvm_intsrc_vector 的 struct hvm_intack 中的 vector 设置成了 HYPERVISOR_CALLBACK_VECTOR 。
也就是说在vmentry回客户虚拟机之后,会调用 HYPERVISOR_CALLBACK_VECTOR 对应的handler xen_hvm_callback_vector 。
接下来我们来看看这个 xen_hvm_callback_vector 函数的实现。
在 entry.S 中,它被设置成了 xen_evtchn_do_upcall 函数
apicinterrupt3 HYPERVISOR_CALLBACK_VECTOR \ xen_hvm_callback_vector xen_evtchn_do_upcall
void xen_evtchn_do_upcall(struct pt_regs *regs)
{
struct vcpu_info *vcpu_info = __this_cpu_read(xen_vcpu);
do {
vcpu_info->evtchn_upcall_pending = 0;
xen_evtchn_handle_events(cpu); /* call evtchn_ops->handle_events(cpu); -> */
} while (...)
}
其中 evtchn_ops->handle_events(cpu) 最终会调用到 evtchn_fifo_handle_events :
static void evtchn_fifo_handle_events(unsigned cpu)
{
...
consume_one_event(cpu, control_block, q, &ready, drop);
...
}
最终在 consume_one_event 中会调用 handle_irq_for_port(port) 处理相应的event。
这就是HVM中event-channel时间通知的整个过程。
PV虚拟机中event-channel事件通知流程
当然,如果driver domain是domain-0,也就是说其不是HVM,而是PV的话,那么采用的又是另外一套流程。
在PV虚拟机初始化的时候会调用 register_callback 函数注册一个 CALLBACKTYPE_event :
void __init xen_pvmmu_arch_setup(void)
{
...
register_callback(CALLBACKTYPE_event, xen_hypervisor_callback);
...
}
static int register_callback(unsigned type, const void *func)
{
struct callback_register callback = {
.type = type,
.address = XEN_CALLBACK(__KERNEL_CS, func),
.flags = CALLBACKF_mask_events,
};
return HYPERVISOR_callback_op(CALLBACKOP_register, &callback);
}
而在Xen中,会将该callback的地址付给 VCPU_event_addr 变量:
static long register_guest_callback(struct callback_register *reg)
{
...
switch ( reg->type )
{
case CALLBACKTYPE_event:
v->arch.pv_vcpu.event_callback_eip = reg->address;
...
}
...
}
#define VCPU_event_addr 1312 /* offsetof(struct vcpu, arch.pv_vcpu.event_callback_eip) */
之后Xen在会通过这个callback来直接调用PV虚拟机中相应的 xen_hypervisor_callback 函数,该函数最终也会调用到实际的处理函数 xen_evtchn_do_upcall 。
以上就是本文的全部内容,希望本文的内容对大家的学习或者工作能带来一定的帮助,也希望大家多多支持 码农网
猜你喜欢:本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。
Introduction to Tornado
Michael Dory、Adam Parrish、Brendan Berg / O'Reilly Media / 2012-3-28 / USD 23.99
Tornado is a scalable, non-blocking web server and web application framework written in Python. It is also light-weight to deploy, fun to write for, and incredibly powerful. Tornado was written with p......一起来看看 《Introduction to Tornado》 这本书的介绍吧!