Mctrain's Blog

What I learned in IT, as well as thought about life

Event-channel in Xen

| Comments

这两天在看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。

fifo-event

而在Xen的实现中,每个vcpu都有一个类型为struct evtchn_fifo_vcpu的变量evtchn_fifo,如下图所示:

event_fifo_vcpu

里面的event_fifo_queue就是前面所说的对应于每个priority的queue,而queue中的head和tail域就会指向相应的event word。这些event word实际存在于event array中,在Xen的实现中,每个domain都有一个类型为struct evtchn_fifo_domain的变量evtchn_fifo,如下图所示:

event_fifo_domain

其中*event_array[]存储了event array的实际数据。同时我们还可以看到,在struct domain数据结构中,还有一个类型为struct evtchn *的变量evtchn和类型为struct evtchn **的变量evtchn_group。这些数组存储了event-channel的对象,他们通过一个两级的数据结构组成,如下图所示:

event group and bucket

其中,每个group包含了一组page大小(512个)的bucket指针,而每个bucket包含了一个page大小的struct evtchn的数组。其中,第一个bucket(即bucket 0)可以直接通过d->evtchn变量访问。

因此,给定一个port,我们可以通过evtchn_from_port函数计算得出其相应的struct evtchn对象:

1
2
3
4
5
6
7
8
9
10
11
#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

1
2
3
4
5
6
7
8
9
10
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函数:

linux-src/drivers/block/xen-blkfront.c
1
2
3
4
5
6
7
8
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:

linux-src/drivers/block/xen-blkfront.c
1
2
3
4
5
static inline void flush_requests(struct blkfront_ring_info *rinfo)
{
  ...
  notify_remote_via_irq(rinfo->irq);
}
linux-src/drivers/xen/events/events_base.c
1
2
3
4
5
6
void notify_remote_via_irq(int irq)
{
  int evtchn = evtchn_from_irq(irq);
  ...
  notify_remote_via_evtchn(evtchn);
}
linux-src/include/xen/events.h
1
2
3
4
5
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函数:

xen/common/event_channel.c
1
2
3
4
5
6
7
8
9
10
11
12
13
14
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;
    ...
  }
}
1
2
3
4
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函数:

xen/common/event_fifo.c
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
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函数:

1
2
3
4
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函数:

xen/arch/x86/hvm/vmx/entry.S
1
2
3
4
5
...
.Lvmx_do_vmentry:
  call vmx_intr_assist
  ...
  VMLAUNCH
xen/arch/x86/hvm/vmx/intr.c
1
2
3
4
5
6
7
8
9
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的值返回一个sourcehvm_intsrc_vectorstruct hvm_intack数据结构:

xen/arch/x86/hvm/irq.c
1
2
3
4
5
6
7
8
9
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。

那么,这个sourcehvm_intsrc_vectorstruct hvm_intack中的vector域是什么呢?

我们发现,在每个HVM虚拟机(包括driver domain)启动的时候,会调用一个xen_set_callback_via函数:

linux-src/drivers/xen/events/events_base.c
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
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函数:

xen/arch/x86/hvm/irq.c
1
2
3
4
5
6
7
8
9
10
11
12
13
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;
      ...

  }
}

所以在这里它将sourcehvm_intsrc_vectorstruct 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函数

linux-src/arch/x86/entry/entry_64.S
1
2
apicinterrupt3 HYPERVISOR_CALLBACK_VECTOR \
  xen_hvm_callback_vector xen_evtchn_do_upcall
linux-src/drivers/xen/events/events_base.c
1
2
3
4
5
6
7
8
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

linux-src/drivers/xen/events/events_fifo.c
1
2
3
4
5
6
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

linux-src/arch/x86/xen/setup.c
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
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变量:

xen/arch/x86/x86_64/traps.c
1
2
3
4
5
6
7
8
9
10
11
static long register_guest_callback(struct callback_register *reg)
{
  ...
  switch ( reg->type )
  {
  case CALLBACKTYPE_event:
    v->arch.pv_vcpu.event_callback_eip    = reg->address;
    ...
  }
  ...
}
xen/include/asm-x86/asm-offsets.h
1
#define VCPU_event_addr 1312 /* offsetof(struct vcpu, arch.pv_vcpu.event_callback_eip) */

之后Xen在会通过这个callback来直接调用PV虚拟机中相应的xen_hypervisor_callback函数,该函数最终也会调用到实际的处理函数xen_evtchn_do_upcall

Comments