3.2 发送事件通知
事件通道的使用包含发送方和接收方两个使用者。结构上,事件通道的发送具有单向性,即只能由发送方通过事件通道发送事件通知,由接收方接收并处理事件通知。事件通知的发送,仅仅只是通过设置相应的标志位通知接收方,其本身并不包含具体的信息。
在四个类型的事件通道中,发送方和接收方不尽相同(图 34)。其中,物理中断和虚拟中断的发送方为Xen,接收方为Dom;域间通信的发送方和接收方为不同的Dom;域内通信(虚拟IPI)的发送方和接收方为相同的Dom。在物理中断、虚拟中断和域间通信中,接收方(Dom)是该事件通道的提供者。发送方通过修改事件通道对应标志位,以便事件通道所有者(接收方)在检查这些标志位时就能够获取相应的事件通知并加以处理。
而在虚拟IPI中,事件通知的发送是在Dom内部,情况有所不同。实际上,由于事件通道用于VCPU通信,因此其发送方和接收方都是VCPU(VCPU_A和VCPU_B)。由VCPU_A发送事件通知,设置相应标志位;再据结构体evtchn成员notify_vcpu_id的值,该事件通知由VCPU_B处理。通过这种方式达到VCPU_A和VCPU_B通信的目的。因此,可以认为在虚拟IPI中事件通道由发送方提供。
在上述四种类型的事件通道中,域间通信和虚拟IPI的发送方为Dom,需要通过申请超级调用(HYPERVISOR_event_channel_op)完成事件通知的发送。而虚拟中断和物理中断的发送方为Xen,只需在Xen空间调用相关的发送函数即可,不必再申请超级调用。
发送事件通知操作(EVTCHNOP_send)对应的结构体参数为结构体evtchn_send,其中输入参数为发送方提供的事件通道端口号(port)。
//xen\include\public\Event_channel.h 145-150
#define EVTCHNOP_send 4
struct evtchn_send {
/* 入惨 */
evtchn_port_t port;
};
typedef struct evtchn_send evtchn_send_t;
通过超级调用发送事件通知操作仅适用于域间通信和虚拟IPI。对于域间通信,发送方提供的事件端口为本方为建立该域间通道所提供的事件通道端口号,即本地端口号(Local Port)。通过本地端口号可以获取远端Dom和该域间通道相关的信息(通过本地端口号获取本地事件通道结构,结构体中保存了远端通讯的dom id以及port),包括Dom的ID、远端Dom提供的事件通道结构体和端口号(远端端口号,Remote Port)以及处理该事件通道事件通知的VCPU。发送时间通知时,只需调用evtchn_set_pending()设置与远端事件通道相关的标志位即可。而对于虚拟IPI,由于发送方提供的即为目标VCPU能够处理的事件通道端口号,因此直接设置标志位就可完成事件通知的发送。
//xen/include/public/event_channel.c 450-506
long evtchn_send(unsigned int lport)
{
struct evtchn *lchn, *rchn;
struct domain *ld = current->domain, *rd;
struct vcpu *rvcpu;
int rport, ret = 0;
spin_lock(&ld->evtchn_lock);
if ( unlikely(!port_is_valid(ld, lport)) )
{
spin_unlock(&ld->evtchn_lock);
return -EINVAL;
}
//获取本地端口所对应的事件通道结构
lchn = evtchn_from_port(ld, lport);
/*GOS无法通过Xen拥有的事件通道发送事件通知*/
if ( unlikely(lchn->consumer_is_xen) )
{
spin_unlock(&ld->evtchn_lock);
return -EINVAL;
}
switch ( lchn->state )
{
case ECS_INTERDOMAIN:
/*在域间通讯中,本地事件通道只是起一个获取远端信息的作用,根据本地事件通道的state从联合体u中获取远端通讯的dom id以及port,之后便可得到远端对应的事件通道结构,获取远端事件通道所绑定的vcpu(默认vcpu 0),设置pending,通知有事件到达*/
rd = lchn->u.interdomain.remote_dom;
rport = lchn->u.interdomain.remote_port;
rchn = evtchn_from_port(rd, rport);
rvcpu = rd->vcpu[rchn->notify_vcpu_id];
if ( rchn->consumer_is_xen )
{
/*Xen*/
if ( test_and_clear_bit(_VPF_blocked_in_xen,
&rvcpu->pause_flags) )
vcpu_wake(rvcpu);
}
else
{
evtchn_set_pending(rvcpu, rport);
}
break;
case ECS_IPI:
evtchn_set_pending(ld->vcpu[lchn->notify_vcpu_id], lport);
break;
case ECS_UNBOUND:
/* silently drop the notification */
break;
default:
ret = -EINVAL;
}
spin_unlock(&ld->evtchn_lock);
return ret;
}
3.3 屏蔽事件通道
屏蔽事件通道有两种方式:设置对应MASK标志位屏蔽一个特定的事件通道;设置与指定VCPU对应的evtchn_upcall_mask使得该VCPU屏蔽来自任何事件通道的事件通知。不论是MASK标志位还是evtchn_upcall_mask,都只能有Guest OS进行更新,Xen只拥有访问权限。
3.3.1 屏蔽单个事件通道
通过设置对应MASK标志位可以使绑定VCPU屏蔽该事件通道。GOS屏蔽单个事件通知只需要将共享信息页(shared_info)成员evtchn_mask中对应的位置1即可。
//linux-2.6-xen-sparse/drivers/xen/core/evtchn.c 859-864
void mask_evtchn(int port)
{
shared_info_t *s = HYPERVISOR_shared_info;
synch_set_bit(port, s->evtchn_mask);
}
EXPORT_SYMBOL_GPL(mask_evtchn);
在MASK位置1后,若该事件通道产生事件通知,系统并不会直接丢弃该事件通知,而是先要检查PENDING位。若对应PENDING位为0,则将其置1,然后才会检查MASK位选择是否丢弃该事件通道。也就是说,在屏蔽该事件通道后,事件通道也可能包含未处理的事件通知。因此,在执行取消屏蔽(Unmask)操作后,还需要检查是否有这样的事件通知存在。若存在,则需要设置evtchn_upcall_pending和evtchn_pending_sel通知VCPU进行处理。
//linux-2.6-xen-sparse/drivers/xen/core/evtchn.c 866-889
void unmask_evtchn(int port)
{
shared_info_t *s = HYPERVISOR_shared_info;
//获取dom中执行线程的vcpu id
unsigned int cpu = smp_processor_id();
vcpu_info_t *vcpu_info = &s->vcpu_info[cpu];
BUG_ON(!irqs_disabled());
/* 与其它vcpu绑定的事件通道只能通过超级调用设置 */
if (unlikely(cpu != cpu_from_evtchn(port))) {
struct evtchn_unmask unmask = { .port = port };
(void)HYPERVISOR_event_channel_op(EVTCHNOP_unmask, &unmask);
return;
}
//与当前VCPU自身绑定的事件通道MASK清理可以直接进行设置
synch_clear_bit(port, s->evtchn_mask);
/* 检查是否含有未处理的事件通知 */
if (synch_test_bit(port, s->evtchn_pending) &&
!synch_test_and_set_bit(port / BITS_PER_LONG,
&vcpu_info->evtchn_pending_sel))
vcpu_info->evtchn_upcall_pending = 1;
}
EXPORT_SYMBOL_GPL(unmask_evtchn);
与屏蔽操作不同,当前VCPU只能够对直接绑定在其上的事件通道执行取消屏蔽操作,而对于绑定到其它VCPU的事件通道,则需要通过超级调用HYPERVISOR_event_channel_op执行,即操作码EVTCHNOP_unmask。
取消屏蔽操作(EVTCHNOP_unmask)对应的结构体参数为结构体evtchn_unmask,其中输入参数为事件通道端口号(port)。
//xen\include\public\Event_channel.h 212-217
#define EVTCHNOP_unmask 9
struct evtchn_unmask {
/* IN parameters. */
evtchn_port_t port;
};
typedef struct evtchn_unmask evtchn_unmask_t;
该操作核心处理程序为evtchn_unmask()。函数在将MASK位清零后,仍然需要检查PENDING位。若存在未处理的事件通知,则调用函数vcpu_mark_events_pending()设置绑定VCPU的相应标志位,并唤醒该VCPU。
//xen/common/event_channel.c 711-743
static long evtchn_unmask(evtchn_unmask_t *unmask)
{
struct domain *d = current->domain;
shared_info_t *s = d->shared_info;
int port = unmask->port;
struct vcpu *v;
spin_lock(&d->evtchn_lock);
if ( unlikely(!port_is_valid(d, port)) )
{
spin_unlock(&d->evtchn_lock);
return -EINVAL;
}
//获得绑定了该事件通道的VCPU
v = d->vcpu[evtchn_from_port(d, port)->notify_vcpu_id];
//检查是否设置了MASK位,pengding位以及之前是否有设定事件选择器
if ( test_and_clear_bit(port, __shared_info_addr(d, s, evtchn_mask)) &&
test_bit (port, __shared_info_addr(d, s, evtchn_pending)) &&
!test_and_set_bit (port / BITS_PER_GUEST_LONG(d),
vcpu_info_addr(v, evtchn_pending_sel)) )
{
/*设置VCPU标志位evtchn_upcall_pending并唤醒该VCPU*/
vcpu_mark_events_pending(v);
}
spin_unlock(&d->evtchn_lock);
return 0;
}
由于设置MASK位后,事件通道仍然可能产生事件通道并将PENDING位置1,因此在事件通道屏蔽期间,系统仍然可以采用轮询(Poll)的方式检查其对应PENDING位并处理存在的未处理事件通知。该检查工作一般在当前正在执行的事件通知处理程序(Ecent Handler)将要退出时进行。若检查存在未处理的事件通知,则处理程序将继续执行。
此外,轮询方式也可应用在比较繁忙的事件通道中。事件通道产生事件通知,Xen通过upcall将其发送到绑定的VCPU进行处理。考虑到upcall机制的开销,采用轮询方式可以大大减少upcall的使用次数,即屏蔽该事件通道,通过轮询获取未处理的事件通知并进行处理。
3.3.2屏蔽所有事件通道
通过设置VCPU对应的evtchn_upcall_mask可以屏蔽与该VCPU绑定的事件通道。
//xen/include/asm-X86/event.h 52-60
static inline void local_event_delivery_disable(void)
{
current->vcpu_info->evtchn_upcall_mask = 1;
}
static inline void local_event_delivery_enable(void)
{
current->vcpu_info->evtchn_upcall_mask = 0;
}
3.4 获取事件通道状态
在事件通道的使用过程中,可能需要查询事件通道的状态。尽管在事件通道结构体evtchn中有成员state保存当前事件通道的状态,但是GOS无法访问该结构体,只能通过超级调用获取事件通道的状态,
获取事件通道状态操作(EVTCHNOP_status)对应的结构体参数为结构体evtchn_status,其中输入参数包括事件通道所属Dom的ID(dom)和端口号(port);输出参数包括事件通道状态(status)、绑定的VCPU(vcpu)以及保存相关信息的成员共同体(u)。
//xen/include/public/event_channel.h 161-186
struct evtchn_status {
/* 入参 */
domid_t dom;
evtchn_port_t port;
/* 出参 */
#define EVTCHNSTAT_closed 0 /* 该通道未被使用 */
#define EVTCHNSTAT_unbound 1 /* 该通道正等待域间连接 */
#define EVTCHNSTAT_interdomain 2 /* 该通道已经与远端域相连 */
#define EVTCHNSTAT_pirq 3 /* 该通道已经绑定物理中断 */
#define EVTCHNSTAT_virq 4 /* 该通道已经绑定虚拟中断 */
#define EVTCHNSTAT_ipi 5 /* 该通道绑定虚拟IPI */
uint32_t status;
uint32_t vcpu; /* 事件通道绑定的VCPU. */
union {
struct {
domid_t dom;
} unbound; /* 事件通道未被绑定 */
struct {
domid_t dom;
evtchn_port_t port;
} interdomain; /* 域间事件通道 */
uint32_t pirq; /* 物理中断事件通道 */
uint32_t virq; /* 虚拟中断事件通道 */
} u;
};
typedef struct evtchn_status evtchn_status_t;
根据事件通道结构体evtchn中定义的7种事件通道类型,操作EVTCHNOP_status可能返回6种状体。当事件通道类型为ECS_FREE和ECS_RESERVED时,操作返回同一种状态,即关闭状态,表示该事件通道可能被保留或等待分配。事件通道类型和返回状态之间的对应关系如表 32所示。
事件通道类型(state) | 返回状态(Status) | 说明 |
ECS_FREE | EVTCHNSTAT_closed | 通道关闭(未使用) |
ECS_RESERVED | EVTCHNSTAT_closed | 通道关闭(保留) |
ECS_UNBOUND | EVTCHNSTAT_unbound | 未绑定 |
ECS_INTERDOMAIN | EVTCHNSTAT_interdomain | 域间通信 |
ECS_PIRQ | EVTCHNSTAT_pirq | 物理中断 |
ECS_VIRQ | EVTCHNSTAT_virq | 虚拟中断 |
ECS_IPI | EVTCHNSTAT_ipi | 虚拟处理器间中断 |
操作核心处理函数evtchn_status()根据事件通道的类型返回不同的状态值,并将相关信息保存到共同体u中。
//xen/common/event_channel.c 596-658
static long evtchn_status(evtchn_status_t *status)
{
struct domain *d;
domid_t dom = status->dom;
int port = status->port;
struct evtchn *chn;
long rc = 0;
/*特权域Dom0可以查询其它Dom事件通道状态*/
if ( dom == DOMID_SELF )
dom = current->domain->domain_id;
else if ( !IS_PRIV(current->domain) )
return -EPERM;
if ( (d = rcu_lock_domain_by_id(dom)) == NULL )
return -ESRCH;
spin_lock(&d->evtchn_lock);
if ( !port_is_valid(d, port) )
{
rc = -EINVAL;
goto out;
}
chn = evtchn_from_port(d, port);
/*返回状态值,保存相关信息*/
switch ( chn->state )
{
case ECS_FREE:
case ECS_RESERVED:
status->status = EVTCHNSTAT_closed;
break;
case ECS_UNBOUND:
status->status = EVTCHNSTAT_unbound;
status->u.unbound.dom = chn->u.unbound.remote_domid;
break;
case ECS_INTERDOMAIN:
status->status = EVTCHNSTAT_interdomain;
status->u.interdomain.dom =
chn->u.interdomain.remote_dom->domain_id;
status->u.interdomain.port = chn->u.interdomain.remote_port;
break;
case ECS_PIRQ:
status->status = EVTCHNSTAT_pirq;
status->u.pirq = chn->u.pirq;
break;
case ECS_VIRQ:
status->status = EVTCHNSTAT_virq;
status->u.virq = chn->u.virq;
break;
case ECS_IPI:
status->status = EVTCHNSTAT_ipi;
break;
default:
BUG();
}
status->vcpu = chn->notify_vcpu_id;
out:
spin_unlock(&d->evtchn_lock);
rcu_unlock_domain(d);
return rc;
}
查询操作不仅用于查询自身Dom拥有的事件通道的状态,也可以查询其它Dom的事件通道状态。当然,只有特权域Dom0有权查询其它Dom的事件通道状态,而非特权域只能够查询本身的事件通道,即dom的值为DOMID_SELF。
3.5 关闭和重置事件通道
在超级调用HYPERVISOR_event_channel_op剩下的两个操作中,EVTCHNOP_close用于关闭事件通道,EVTCHNOP_reset则是重置事件通道。这两个操作本质上类似,都是将事件通道的状态(state)设置为“未使用”,即ECS_FREE。所不同的是,事件通道的关闭操作是关闭特定的事件通道,而重置操作则是关闭整个Dom中所有的有效事件通道。
3.5.1 关闭事件通道
事件通道关闭操作(EVTCHNOP_close)对应的结构体参数为结构体evtchn_close,其中输入参数为需要关闭的事件通道端口号(port)。
//xen/include/public/event_channel.h 134-139
#define EVTCHNOP_close 3
struct evtchn_close {
/* 入惨 */
evtchn_port_t port;
};
typedef struct evtchn_close evtchn_close_t;
通过该操作,Dom只能够关闭自己的事件通道,即在事件通道对应的(dom,port)中,dom的值为DOMID_SELF。在操作的核心函数__evtchn_close()中,函数根据不同的事件通道类型采取不同的处理方法。关闭用于物理中断的事件通道,需要在完成取消物理中断绑定之后清除物理中断对应数组pirq_to_evtchn中与该物理中断对应的值。关闭用于虚拟中断的事件通道,只需要清除虚拟中断对应数组virq_to_evtchn中对应的值。关闭用于虚拟IPI的事件通道,仅需要清除结构体evtchn成员notify_vcpu_id。关闭用于域间通信的事件通道相对要复杂些,因为关闭操作需要处理创建域间通道的另一个事件通道。在关闭本地事件通道后,远端事件通道将被设置为未绑定(ECS_UNBOUND)状态,等待下一次域间通道的建立。
//xen/include/public/event_channel.c 444-447
static long evtchn_close(evtchn_close_t *close)
{
/*只能关闭自己的事件通道*/
return __evtchn_close(current->domain, close->port);
}
//xen/include/public/event_channel.c 320-441
static long __evtchn_close(struct domain *d1, int port1)
{
struct domain *d2 = NULL;
struct vcpu *v;
struct evtchn *chn1, *chn2;
int port2;
long rc = 0;
again:
spin_lock(&d1->evtchn_lock);
if ( !port_is_valid(d1, port1) )
{
rc = -EINVAL;
goto out;
}
//获得本地事件通道结构
chn1 = evtchn_from_port(d1, port1);
/*无法关闭Xen所有的事件通道*/
if ( unlikely(chn1->consumer_is_xen) )
{
rc = -EINVAL;
goto out;
}
switch ( chn1->state )
{
case ECS_FREE:
case ECS_RESERVED:
rc = -EINVAL;
goto out;
case ECS_UNBOUND:
break;
case ECS_PIRQ:
//先要取消物理中断绑定
if ( (rc = pirq_guest_unbind(d1, chn1->u.pirq)) == 0 )
d1->pirq_to_evtchn[chn1->u.pirq] = 0;
break;
case ECS_VIRQ:
for_each_vcpu ( d1, v )
if ( v->virq_to_evtchn[chn1->u.virq] == port1 )
v->virq_to_evtchn[chn1->u.virq] = 0;
break;
case ECS_IPI:
break;
case ECS_INTERDOMAIN:
if ( d2 == NULL )
{
d2 = chn1->u.interdomain.remote_dom;
if ( unlikely(!get_domain(d2)) )
{
d2 = NULL;
goto out;
}
if ( d1 < d2 )
{
spin_lock(&d2->evtchn_lock);
}
else if ( d1 != d2 )
{
spin_unlock(&d1->evtchn_lock);
spin_lock(&d2->evtchn_lock);
goto again;
}
}
else if ( d2 != chn1->u.interdomain.remote_dom )
{
BUG_ON(d1 != current->domain);
rc = -EINVAL;
goto out;
}
/*设置另一个事件通道为未绑定状态*/
port2 = chn1->u.interdomain.remote_port;
BUG_ON(!port_is_valid(d2, port2));
chn2 = evtchn_from_port(d2, port2);
BUG_ON(chn2->state != ECS_INTERDOMAIN);
BUG_ON(chn2->u.interdomain.remote_dom != d1);
chn2->state = ECS_UNBOUND;
chn2->u.unbound.remote_domid = d1->domain_id;
break;
default:
BUG();
}
/*设置事件通道状态为ECS_FREE */
chn1->state = ECS_FREE;
chn1->notify_vcpu_id = 0;
out:
if ( d2 != NULL )
{
if ( d1 != d2 )
spin_unlock(&d2->evtchn_lock);
put_domain(d2);
}
spin_unlock(&d1->evtchn_lock);
return rc;
}
3.5.2 重置事件通道
事件通道的重置操作只与Dom相关,即操作将关闭该Dom所有有效的事件通道。事件通道重置操作(EVTCHNOP_reset)对应的结构体参数为结构体evtchn_reset,其中输入参数为需要关闭事件通道的Dom(dom)。
//xen/include/public/event_channel.h 225-230
#define EVTCHNOP_reset 10
struct evtchn_reset {
/* 入参 */
domid_t dom;
};
typedef struct evtchn_reset evtchn_reset_t;
特权域Dom0可以通过操作重置其它Dom事件通道,而非特权域只能操作自己的事件通道,即dom的值为DOMID_SELF。在核心处理函数evtchn_reset()中,重置操作实际是通过循环调用事件通道关闭操作__evtchn_close()完成。
//xen/include/public/event_channel.c 744-766
static long evtchn_reset(evtchn_reset_t *r)
{
domid_t dom = r->dom;
struct domain *d;
int i;
/*特权域Dom0可以重置其它Dom事件通道*/
if ( dom == DOMID_SELF )
dom = current->domain->domain_id;
else if ( !IS_PRIV(current->domain) )
return -EPERM;
if ( (d = rcu_lock_domain_by_id(dom)) == NULL )
return -ESRCH;
for ( i = 0; port_is_valid(d, i); i++ )
(void)__evtchn_close(d, i);
rcu_unlock_domain(d);
return 0;
}