1、基本概念
事件通道(Event Channel)是Xen用於Dom和Xen之間、Dom和Dom之間的異步事件通知機制,事件通道的應用非常廣泛,Xen體系結構上的物理中斷(pIRQ)、虛擬中斷(vIRQ)、虛擬處理器間中斷(Virtual Inter-Processor Interrupt,vIPI)以及Dom域間通信(Inter-Domain Communication,IDC)均需通過事件通道實現。
事件通道機制和超級調用機制一起完成Xen和Dom之間的控制和交互,即:使用超級調用產生從Dom到Xen的同步調用;使用異步事件機制完成從Xen到Dom的通知(Notification)遞交。由事件通道提供的從Xen到Dom的通信機制能夠取代常用的利用設備中斷的遞交機制,使得采用輕量級的通知形式成爲可能。域間通信(IDC)機制,配合以I/O環(I/O Ring)爲基礎的共享內存機制使得Dom之間傳遞信息和數據更加有效。
Xen系統中,每個Dom都能夠擁有自己的事件通道。系統規定,每個Dom最多能夠分配NR_EVENT_CHANNELS個事件通道。在X86平臺上,宏NR_EVENT_CHANNELS的值爲1024,即最多能夠擁有1024個事件通道。
//xen/include/public/xen.h 47-51
/*每個Dom的事件通道個數:*/
#define NR_EVENT_CHANNELS (sizeof(unsigned long) * sizeof(unsigned long) * 64)
每個事件通道都有自己唯一的編號,即端口(port),其範圍爲0 -- NR_EVENT_CHANNELS-1。在X86平臺上,事件通道的端口範圍爲0—1023,這些事件通道被分爲8個組(Bucket),每組128個。
//xen/include/xen/sched.h 40-41
#define EVTCHNS_PER_BUCKET 128
#define NR_EVTCHN_BUCKETS (NR_EVENT_CHANNELS / EVTCHNS_PER_BUCKET)
系統爲每個Dom分配NR_EVTCHN_BUCKETS個事件通道,每個事件通道與一個結構體evtchn對應。這些結構體組成一個長度爲NR_EVTCHN_BUCKETS的結構體數組,供Dom調用。結構體數組的定義在代表Dom的結構體domain中。
//xen/include/xen/sched.h 143-227
struct domain
{
domid_t domain_id;
shared_info_t *shared_info; /* 共享數據區 */
……
/*事件通道信息*/
//數組指針,定位一個事件通道需要二維信息(哪一組中的哪一個)
struct evtchn *evtchn[NR_EVTCHN_BUCKETS];
spinlock_t evtchn_lock;
struct grant_table *grant_table;
/*對事件通道映射的中斷*/
u16 pirq_to_evtchn[NR_IRQS];
DECLARE_BITMAP(pirq_mask, NR_IRQS);
……
struct vcpu *vcpu[MAX_VIRT_CPUS];
/*CPU的掩碼將控制每個域的狀態*/
cpumask_t domain_dirty_cpumask;
……
};
在結構體domain中,長度爲NR_EVTCHN_BUCKETS的結構體數組以二維數組的形式存在,即滿足8組,每組128個事件通道的劃分。事件通道端口號和數組下標之間一一對應,它們之間的轉換由系統定義的宏完成。這些宏主要包括bucket_from_port(d,p)、port_is_valid(d,p)和evtchn_from_port(d,p)。其中,port_is_valid(d,p)用來判斷端口號是否有效以及端口號對應的事件通道是否存在;evtchn_from_port(d,p)和bucket_from_port(d,p)則是根據端口號獲取該事件通道對應的結構體evtch n以及其所在的組別。例如,端口號爲129的事件通道在結構體數組中所處的位置爲evtchn[1][1]。這三個宏的定義在文件xen/common/event_channel.c中。
//xen/common/event_channel.c 34-40
//得到該端口事件通道在domain中的組別
#define bucket_from_port(d,p) \
((d)->evtchn[(p)/EVTCHNS_PER_BUCKET])
//判斷端口號是否有效以及端口號對應的事件通道是否存在
#define port_is_valid(d,p) \
(((p) >= 0) && ((p) < MAX_EVTCHNS(d)) && \
(bucket_from_port(d,p) != NULL))
//根據端口號獲取該事件通道對應的結構體evtch n
#define evtchn_from_port(d,p) \
//首先獲得組號,再與低7位做與操作獲得具體組內標識,從而得到具體的事件通道結構體位置
(&(bucket_from_port(d,p))[(p)&(EVTCHNS_PER_BUCKET-1)])
1.1 結構體evtchn
在事件通道對應的結構體evtchn中,主要定義了事件通道的各種狀態以及不同狀態所必須的基本參數。這些通過宏定義的不同狀態既包含了事件通道的類型,又包含了事件通道在使用過程中可能的狀態。
//xen/include/xen/sched.h 43-66
struct evtchn
{
#define ECS_FREE 0 /* 該通道可用 */
#define ECS_RESERVED 1 /* 該通道被保留 */
#define ECS_UNBOUND 2 /* 該通道可以跟遠端的域綁定 */
#define ECS_INTERDOMAIN 3 /* 該通道已經綁定另一個域 */
#define ECS_PIRQ 4 /* 該通道綁定物理中斷 */
#define ECS_VIRQ 5 /* 該通道綁定虛擬中斷. */
#define ECS_IPI 6 /* 該通道綁定虛擬IPI */
u8 state; /* 該通道的狀態 */
u8 consumer_is_xen; /* 是用Xen還是由客戶使用 */
u16 notify_vcpu_id; /*用來傳遞通知的VCPU號 */
union {
struct {
domid_t remote_domid;
} unbound; /* state 值爲 ECS_UNBOUND */
struct {
u16 remote_port;
struct domain *remote_dom;
} interdomain; /* state 值爲 ECS_INTERDOMAIN */
u16 pirq; /* state 值爲 ECS_PIRQ */
u16 virq; /* state 值爲 ECS_VIRQ */
} u;
};
目前,事件通道有7種可能的狀態(如表1-1所示):ECS_FREE表示該事件通道處於等待使用的狀態,即事件通道已經完成初始化,等待分配;ECS_RESERVED表示該事件通道被系統保留,不參與分配; ECS_UNBOUND表示該事件通道處於未綁定狀態,ECS_INTERDOMAIN表示該事件通道已經處於和其它Dom綁定的狀態;ECS_PIRQ、ECS_VIRQ和ECS_IPI則分別表示該事件通道綁定某一物理中斷(pIRQ)、虛擬中斷(vIRQ)和虛擬處理器間中斷(vIPI)。
表 1-1 事件通道的狀態
狀態(state) |
說明 |
ECS_FREE |
已初始化,等待分配 |
ECS_RESERVED |
保留(關閉) |
ECS_UNBOUND |
未綁定 |
ECS_INTERDOMAIN |
與另一Dom綁定(域間通信) |
ECS_PIRQ |
物理中斷 |
ECS_VIRQ |
虛擬中斷 |
ECS_IPI |
虛擬處理器間中斷(域內通信) |
在這7種可能的狀態中,ECS_INTERDOMAIN、ECS_PIRQ、ECS_VIRQ和ECS_IPI都表示該事件通道處於綁定狀態,區別在於它們的綁定對象不同,即用途不同。事件通道根據用途可以劃分爲四種類型:域間通信、域內通信、物理中斷和虛擬中斷。
結構體evtchn包含4個成員:state表示該事件通道的類型和所處的狀態,取值上述7種狀態之一;consumer_is_xen表示該事件通道是否爲Xen使用,取值爲1則說明該事件通道的控制權屬於Xen,Dom無權對其執行關閉、發送事件通知,綁定VCPU等操作;notify_vcpu_id則表示接收並處理事件通知的本地VCPU的ID,即vcpu_id;聯合體u中保存不同類型的事件通道所必須的基本參數。聯合體u的具體取值隨state變化:若state爲ECS_UNBOUND,聯合體u中爲結構體unbound,其中包含遠端Dom的ID;若state爲ECS_INTERDOMAIN,聯合體u中爲結構體interdomain,其中包含遠端Dom的domain結構體指針和遠端Dom分配的事件通道端口;若state爲ECS_PIRQ時,聯合體u中爲事件通道所綁定的物理中斷號;若state爲ECS_VIRQ,則是所綁定的虛擬中斷號。
1.1.1 域間通信(Inter-Domain Communication,IDC)
此類事件通道用於在Dom之間建立雙向(Bi-directional)連接。所謂雙向連接,是指在域間通信中,事件通道都是成對使用的,即通信雙方Dom各需要分配一個事件通道來建立連接。建立雙向連接大致需要兩個步驟:DomA預先分配一個新的事件通道(端口),並標註爲未綁定狀態(ECS_UNBOUND),並授權其它的Dom(Remote Domain,遠端Dom)可以綁定這個端口;當遠端Dom(DomB)需要與該DomA建立連接時,DomB分配一個事件通道並與DomAin提供的端口進行綁定(ECS_INTERDOMAIN)。在連接建立後,DomA通過想本地端口發送事件通知DomB,反之亦然。域間通信大多應用在分離設備驅動模型中,Dom0和DomU通過域間通信完成對前後端設備的操作。後端設備通過發送通知消息告知前端設備有數據在等待接受;前端設備則通過發送通知消息通知後端設備有數據需要發送。
1.1.2 域內通信(Intra-Domain Communication)
域內通信可以看作是域間通信的特例,即通信雙方Dom爲同一Dom。它用於Dom內部虛擬CPU(VCPU)之間的通信,因此被稱之爲虛擬處理器間中斷(vIPI)。相應的事件通道的狀態被標記爲ECS_IPI。
1.1.3 物理中斷(pIRQ)
由於系統中物理中斷的異步性,Xen系統中的物理中斷被事件通道取代。對於有權訪問硬件設備的Dom(Dom0或IDD),可以通過爲每個物理中斷源綁定一個事件通道端口,接收和處理由硬件設備發送的物理中斷。物理中斷的綁定信息以數組的形式(pirq_to_evtchn[NR_IRQS])保存在結構體domain中。其中,數組以中斷號爲下標,成員中存放的則是事件通道端口號。用於物理中斷的事件通道是單個使用的。
1.1.4 虛擬中斷(vIRQ)
虛擬中斷和物理中斷類似,區別在於虛擬中斷中參與綁定的虛擬設備。例如,系統中虛擬計時器設備同物理計時器一樣,能夠使GOS在一個特定的時間請求計時器事件,引發計時器中斷。將該虛擬計時器中斷(VIRQ_TIMER)同事件通道綁定,在發生計時器中斷時GOS將收到來之該事件通道的事件通知。用於虛擬中斷的事件通道是單個使用的。
1.2 PENDING和MASK位
在基於事件通道的異步通知機制中,除了結構體數組evtchn[NR_EVTCHN_BUCKETS]外,還有兩個數組至關重要,即前面提到的數組evtchn_pending[]和evtchn_mask[]。事件通道異步機制的實現需要藉助於這兩個數組。
//xen/include/public/xen.h 424-474
struct shared_info {
struct vcpu_info vcpu_info[MAX_VIRT_CPUS]; //用於特定的VCPU對所有事件通道操作
/*所有VCPU對特定事件通道的操作(事件通知與屏蔽),長整型爲4字節,32位,數組中共32個長整型,總共1024個二進制位,每一位對應一個事件通道*/
unsigned long evtchn_pending[sizeof(unsigned long) * 8];
unsigned long evtchn_mask[sizeof(unsigned long) * 8];
……
};
數組evtchn_pending和evtchn_mask中分別保存與事件通道對應的兩個標誌位:PENDING位和MASK位。每個事件通道都在數組中對應這樣一對標誌位。其中,PENDING位表示在事件通道中是否存在一個未處理的事件通知;MASK位表示是否屏蔽該事件通道的事件通知。事件通道的使用包含兩個使用者:發送事件通知的發送方(Sender)和接收事件通知的接收方(Reciever)。兩者對PENDING位和MASK位的讀寫權限各不相同。
例如,在用於物理中斷或虛擬中斷的事件通道中,其發送方爲Xen,接收方爲GOS。這兩者對於PENDING位和MASK位的操作如圖所示。
1.2.1 PENDING位
PENDING位只能由發送方(Xen)設置爲1,並由接收方(GOS)在處理完事件後清除爲0。當某一事件通道產生一個事件通知時,Xen將其對應的PENDING位置1,並通過upcall向目標Dom發送該事件通知。若目標Dom處於阻塞(Blocked)狀態,則該Dom將會被調度進入運行隊列。值得注意的是,每一個事件通道中只能包含一個未處理的事件通知。也就是說,當事件通知產生時,對應的PENDIG位值已經爲1,則新產生的事件通知自動被忽略。
1.2.2 MASK位
MASK位只能由接收方(GOS)更新(0或1),發送方(Xen)對其只有訪問權限,不能夠進行修改。若事件通道的MASK位被GOS置1,則表明GOS將主動屏蔽該事件通道的事件通知。當MASK位爲1時,若該事件通道產生一個事件通知,Xen只將對應的PENDING位置1,並不會通過upcall向目標Dom發送事件通知;且若目標Dom阻塞,Xen也不會喚醒阻塞的目標Dom。爲了防止GOS處理一些不必要的upcall,在GOS更新MASK前,需要預先清除其對應的PENDING位。
1.2.3 事件通知流程
在事件通道產生一個事件通知後,Xen通過upcall向目標Dom發送該事件通知前,Xen需要進行一系列的判斷(圖 12)。在下面兩種情況下,Xen將放棄發送該事件通知:
(1)對應的PENDING位爲1。在這種情況下,表明該事件通道已經存在一個未處理的事件通知,新的事件通知將被丟棄。
(2)對應的MASK位爲1。GOS主動屏蔽了該事件通道的事件通知,Xen將放棄發送。
圖 12 Xen發送事件通知流程
事件通道的使用不僅僅侷限在Xen和Guest OS之間。在域間通信的應用中,事件通道被用於Dom之間,即事件通知對應的發送方和接收方都爲Dom(如Dom A和Dom B)。在Dom A和Dom B之間存在雙向的事件通道。這意味着通信雙方都能夠通過設置其中一個事件通道的PENDING位向另一個Dom發送事件通知;也能夠通過設置另一個事件通道的MASK位屏蔽來之另一個Dom的事件通知。對於任一個事件通道,它對應的PENDING位和MASK位的讀寫規則與Xen-GOS事件通道相同。
2、事件通道的初始化
在Xen系統中,每個Dom都擁有各自的事件通道。事件通道的初始化在Dom創建時完成,即在Dom創建函數domain_create()中調用事件通道初始化函數evtchn_init()。在X86平臺上,每個Dom最多能夠分配1024個事件通道。這些事件通道被分爲8組(Bucket),每組128個。事件通道的初始化以組爲單位。且在首次初始化過程中,僅初始化第一組的128個事件通道。只有在第一組事件通道被分配完後,纔會初始化第二組以供使用。
//xen/common/event_channel.c 949-956
int evtchn_init(struct domain *d)
{
spin_lock_init(&d->evtchn_lock);
/*初始化時,函數返回值爲0,即第一個事件通道端口號*/
if ( get_free_port(d) != 0 )
return -EINVAL;
/*保留evtchn [0][0],即第一個事件通道 */
evtchn_from_port(d, 0)->state = ECS_RESERVED;
return 0;
}
在事件通道初始化函數evtchn_init()中,具體的初始化工作由函數get_free_port()完成。但是該函數的作用不僅僅侷限於此,它實際上是在已經初始化的事件通道中順次找到第一個未分配的事件通道(ECS_FREE),若查找失敗則將初始化下一組並返回該組第一個事件通道的端口號。第一次初始化前,調用get_free_port()會找不到任何一個可用的端口,因此它將初始化第一組事件通道,並返回第一個可用的事件通道evtchn[0][0],其端口號爲0。
在事件通道初始化函數evtchn_init()中,具體的初始化工作由函數get_free_port()完成。但是該函數的作用不僅僅侷限於此,它實際上是在已經初始化的事件通道中順次找到第一個未分配的事件通道(ECS_FREE),若查找失敗則將初始化下一組並返回該組第一個事件通道的端口號。第一次初始化前,調用get_free_port()會找不到任何一個可用的端口,因此它將初始化第一組事件通道,並返回第一個可用的事件通道evtchn[0][0],其端口號爲0。
//xen/common/event_channel.c 77-96
static int get_free_port(struct domain *d)
{
struct evtchn *chn;
int port;
//在有效並且對應了事件通道的端口中尋找第一個沒有使用的事件通道的端口
for ( port = 0; port_is_valid(d, port); port++ )
if ( evtchn_from_port(d, port)->state == ECS_FREE )
return port;
if ( port == MAX_EVTCHNS(d) )
return -ENOSPC;
//分配一組事件通道空間,並將事件通道數組的首地址複製給一個事件通道結構指針
chn = xmalloc_array(struct evtchn, EVTCHNS_PER_BUCKET);
if ( unlikely(chn == NULL) )
return -ENOMEM;
/*結構體數組清零*/
memset(chn, 0, EVTCHNS_PER_BUCKET * sizeof(*chn));
bucket_from_port(d, port) = chn; /*加入新的一組事件通道*/
return port;
}
在第一次初始化過程中,get_free_port()通過memset()將分配的結構體數組全部清零,其中包括結構體evtchn成員state的值。成員state值爲0,意味着初始化後事件通道都處於未分配狀態(ECS_FREE)。在第一批初始化的事件通道中,第一個(端口號爲0)被系統保留(ECS_RESERVED)。
事件通道完成初始化後,系統爲了避免接收非正常的事件通知,將對應的MASK位全部置1以屏蔽發送事件通知,並在事件通道被綁定後t再清除MASK位;同時在進行事件通道綁定前前,需要將該事件通道對應的PENDING位清0,以保證之前可能存在的非正常事件通知不會對現在的使用產生干擾。