|
内容目录
第一章总体结构................................................................................................................................4
第一节 主要对象...........................................................................................................................5
1)domain.....................................................................................................................................5
2)vcpu..........................................................................................................................................6
3)arch_vcpu.................................................................................................................................6
第二章初始化....................................................................................................................................7
第一节 第一部份...........................................................................................................................7
第二节__start_xen.........................................................................................................................9
第三节 AP初始化..........................................................................................................................9
第三章调度......................................................................................................................................11
第一节 调度器接口.....................................................................................................................11
第二节 调度核心.........................................................................................................................12
第三节 时钟中断.........................................................................................................................14
第四章内存管理..............................................................................................................................15
第一节 初始内存分配.................................................................................................................15
第二节 boot分配器......................................................................................................................16
第三节 堆分配器.........................................................................................................................17
第四节 页框管理.........................................................................................................................18
1)页框管理结构.......................................................................................................................18
2)页框号的管理.......................................................................................................................21
第五章页表管理..............................................................................................................................22
第一节 页表模式.........................................................................................................................23
第二节 dom0页表的构建............................................................................................................23
第三节 domU页表的构建...........................................................................................................24
第四节 Xen线性空间..................................................................................................................24
第五节 缺页中断.........................................................................................................................27
第六节 页表助手.........................................................................................................................29
第七节 Shadow页表....................................................................................................................30
第六章事件管道..............................................................................................................................31
第一节 事件的处理.....................................................................................................................32
第二节 事件管道hypercall..........................................................................................................32
2第三节事件管道设备.................................................................................................................35
第七章设备模型..............................................................................................................................35
第一节 设备模型.........................................................................................................................35
第二节 授权表.............................................................................................................................37
第九章hypercall..............................................................................................................................38
第一节 hypercall初始化..............................................................................................................38
第一章 总体结构
Xen是一个开源的虚拟化管理软件,用于将硬件虚拟化,呈现给上层系统一个和真实处理器一样的"软"处理器,也即虚拟机.可以创建的虚拟机个数理论上是无限的,因此,使用Xen能够很好的利用底层硬件的计算能力.例如,可以在一个硬件平台上同时运行多个操作系统,用户可以控制每个操作系统分配的资源,处理器时间等.下面是Xen总体结构的一个示意图:
客户系统
Linux,Windows...
Xen
paravirtulization
hvm
Xen core
硬件层
x86,powerpc...
如上图所示,Xen支持两种虚拟化方式,一种叫作paravirtulization,这种方式下,Xen通过提供一套称之为hypercall的系统调用接口来实现虚拟.这套接口是非常底层的面向处理器的接口.现有的系统,例如Linux等,要进行必要的移植才能运行在paravirt环境中.而运行在操作系统上的应用软件无需修改.
另一种方式称之为hvm(hardware virtualmachine),这是基于x86架构下的VMX(INTEL)或者SVM(AMD)技术的一种虚拟化,hvm利用了处理器内建的虚拟化支持.运行在hvm上的客户系统无需任何修改就可以运行在Xen上,也就是说hvm对客户系统是完全透明的.
客户系统,是指运行在虚拟机上的代码,一般都是操作系统级的软件.Xen在初始化完毕后会运行一个指定的客户系统,这是Xen的第一个客户系统,一般称之为dom0.其它的客户系统统称为domU.
安全模型,由于Xen是运行在硬件上的一个管理层软件,必须拥有硬件的全部控制权,因此Xen运行在x86的ring0层,dom0可以选择运行在ring0或者ring1,domU只能运行在ring3中.
第一节 主要对象
xenoprof
|
arch_domain -- domain -- vcpu -- arch_vcpu -- paging_vcpu
| | / \
paging_domain evtchn paging_mode shadow_vcpu
/ | \
shadow_domain | hap_domain
|
log_dirty_domain
1)domain
domain,域,是Xen的中心概念,一个域可以看作为物理计算机系统的虚拟,具有如下的属
性:
✔ 每个域都有一个ID,下面ID是特殊的域
ID
说明
0
Xen初始化后默认运行的域,一般称之为dom0,使用paravirt虚拟
技术.运行其中的系统可以用于提供一个存取Xen功能的介面
IDLE_DOMAIN_ID
空闲域
✔ 是否hvm使能,这个属性决定了运行在域中的客户系统将采用何种虚拟化,如果是hvm使能,意味着使用hvm虚拟,否则使用paravirt虚拟.
域是否hvm使能由创建函数domain_create参数:domcr_flags决定,目前这个参数的值为0或者DOMCRF_hvm,使用后者将创建hvm使能的域
✔ 每个域可以管理最多MAX_VIRT_CPUS个虚拟机.每个虚拟机需要调用alloc_vcpu来初始化,vcpu的初始化要经过几个层次:
alloc_vcpu
|
vcpu_initialise
/ \
hvm vcpu初始化 paravirt vcpu初始化
一个域实际分配的vcpu应该等于物理处理器的个数,一个多vcpu的域可以看作为对smp系统的虚拟,单vcpu的域可以看作为up系统的虚拟
2)vcpu
虚拟机,Xen用vcpu结构来代表一个虚拟机.vcpu是物理处理器的虚拟,其中最重要的部份就是相当于物理处理器的寄存器的虚拟机的状态了.例如,libxc中在创建好一个domU之后,就会调用hypercall设置虚拟机状态,包括用户寄存器值,页表寄存器值,domU入口地址等等.通过libxc创建的domU都运行在vcpu0上.
用户在配置domU时可以设定该domU中的vcpu个数,dom0的vcpu数由dom0_max_vcpus命令行参数决定.
idle_vcpu数组,这是一个NR_CPUS大小的vcpu数组,每个物理处理器在初始化时会分配一个空闲vcpu,用于运行空闲进程.所有的空闲vcpu都属于空闲域.
3)arch_vcpu
arch_vcpu提供vcpu体系相关部份的接口,其中非常重要的三个接口函数是ctxt_switch_from和ctxt_switch_to以及schedule_tail,这三个接口用于在VM切换时完成体系相关的上下文保存,恢复和最终的切换.目前系统支持paravirt,hvm两种体系,因此也有三个(hvm下两个子体系)这样的接口:
这样VM切换的基本逻辑为:
1. ctxt_switch_from(p)
2. ctxt_switch_to(n)
3. schedule_tail(n)
第二章 初始化
第一节 第一部份
这部份是指进入C部份的__start_xen函数之前的初始化过程,主要由head.S, trampoline.S, x86_32.S几部份组成:
一,head.S,主要工作为
✔ 装入GDT(trampoline_gdt)
GDT项
说明
段选择子
1
ring0,code,32-bit mode
BOOT_CS32(0x0008)
2
ring0,code,64-bit mode
BOOT_CS64(0x0010)
3
ring0,data
BOOT_DS(0x0018)
4
real-mode code
BOOT_PSEUDORM_CS(0x0020)
5
5 real-mode data
BOOT_PSEUDORM_DS(0x0028)
✔ 进入保护模式
✔ 初始化页表,将线性空间的0-12M和__PAGE_OFFSET-__PAGE_OFFSET+12M都映射到物理地址的0-12M;而将线性空间的12M-16M映射到物理地址的12M-16M(注意,这时并没有启用分页机制):
-------------- <- __PAGE_OFFSET +12M
...
-------------- <- __PAGE_OFFSET
...
16M -> --------------<- -------------- <- 16M
... ...
12M -> --------------<- -------------- <- 12M
... ...
0M -> --------------<- -------------- <- 0M
物理地址空间 线性地址空间
✔ 解析早期命令行参数
✔ 调整trampoline.S代码的内存位置,移动到BOOT_TRAMPOLINE(0x8c00处)
✔ 跳转到trampoline_boot_cpu_entry
二,trampoline.S,主要工作为
✔ 进入实模式,读取内存,磁盘,视频信息
✔ 再次进入保护模式
装入新的GDT(gdt_table)
GDT项
说明
段选择子
0xe001
ring0,code,base 0,limit 4G
__HYPERVISOR_CS(0xe008)
0xe002
ring0,data,base 0,limit 4G
__HYPERVISOR_DS(0xe010)
0xe003
ring1,code,base 0,limit 3.xxG
FLAT_RING1_CS(0xe019) FLAT_KERNEL_CS
0xe004
ring1,data,base 0,limit 3.xxG
FLAT_RING1_DS(0xe021)
FLAT_RING1_SS
FLAT_KERNEL_DS
FLAT_KERNEL_SS
0xe005
ring3,code,base 0,limit 3.xxG
FLAT_RING3_CS(0xe02b)
FLAT_USER_CS
0xe006
ring3,data,base 0,limit 3.xxG
FLAT_RING3_DS(0xe033)
FLAT_RING3_SS
FLAT_USER_DS
FLAT_USER_SS
加载前面初始化了的页表,启用分页机制,跳转到__high_start
三, x86_32.S,这个文件提供__high_start入口,主要工作为
✔ 装入堆栈指针,注意,Xen会在栈顶分配一个cpu_info结构(参见下图),这个结构包含很多重要的成员:1)客户系统的切换上下文2)当前运行的vcpu指针3)物理处理器编号
✔ IDT的处理,整个idt_table的向量入口都初始化ignore_int,这个中断处理函数打印"Unknown interrupt(cr2=XXXXXXXX)"信息后系统进入循环
✔ 如果是BSP,跳转到__start_xen否则,跳转到start_secondary
四,内存映像(用虚拟地址标注)
_end -> -----------------
...
-----------------
cpu_info
esp-> -----------------
...
cpu0_stack -> -----------------
x86_32.S
trampoline_end,__high_start ->-----------------
trampoline.S
trampoline_start ->-----------------
head.S
start -> -----------------
第二节__start_xen
这个函数进一步初始化Xen系统,主要逻辑如下:
void __init __start_xen(multiboot_info_t*mbi)
{
//注意,默认的情况下,参数mbi将从堆栈传递,这个值是前面汇编代码中的ebx值
//解析命令行
//初始化console
//整理内存信息
//为dom0模块保留内存,并更新页表
//初始化boot allocator.这是一个内存分配器.在这之后,end_boot_allocator调用之前
的初始化都可以使用这个分配器来分配内存
//初始化堆分配器
//end_boot_allocator,结束boot allocator
//early_boot = 0; 标志"早期"初始化结束
//trap_init,初始化IDT
//smp_prepare_cpus,AP的初始化
//do_initcalls
//创建dom0
//domain_unpause_by_systemcontroller(dom0),调度dom0
//reset_stack_and_jump(init_done),Xen进入idle循环
}
第三节 AP初始化
__start_xen通过smp_prepare_cpus函数开始AP的初始化过程:
void __init smp_prepare_cpus(unsigned intmax_cpus)
{
for every APs
{
do_boot_cpu
{
prepare_idle_vcpu //为AP准备vcpu
wakeup_secondary_cpu(apicid, start_eip)
//start_eip:trampoline_realmode_entry
}
}
}
AP在接收到SIPI消息后从trampoline.S的trampoline_realmode_entry开始执行,最后进入start_secondary:
void __devinit start_secondary(void*unused)
{
unsigned int cpu = booting_cpu;
set_processor_id(cpu);
set_current(idle_vcpu[cpu]);
this_cpu(curr_vcpu) = idle_vcpu[cpu];
...
startup_cpu_idle_loop();
}
第三章 调度
Xen是一个VMM,即虚拟机监视器,它的一个重要功能就是调度不同的虚拟机到处理器上运行.Xen调度的基本方法是虚拟机按时间片运行.
调度器接口决定下一个可调度的虚拟机以及运行的时间片大小的功能,Xen把这部份功能抽象为调度器.用户可以根据自己的需要实现不同的调度器,从而实现不同的调度策略.
第一节 调度器接口
Xen用调度器接口structscheduler来代表这个调度器,每个调度器都需要填充这样一个结构并向Xen注册,结构的重要成员如下:
成员
说明
name
调度器名称,目前系统中实现了两个调度器:sedf和credit,前者已经过时
init
调度器初始化
init_domain
在域创建时(sched_init_domain),这个回调函数负责设置域中调度器相关的数据结构
destroy_domain
在域析构时(sched_destroy_domain)
init_vcpu
在创建一个新的vcpu时(sched_init_vcpu),这个函数负责设置vcpu中调度器相关的数据结构
destroy_vcpu
在vcpu析构时(sched_destroy_vcpu)
sleep
在sched_sleep_nosync中调用
wake
在vcpu_wake中调用,这个函数一般会调用schedule
do_schedule
在schedule中调用
pick_cpu
在vcpu_migrate中调用
adjust
在sched_adjust中调用
dump_setting
在dump_runq中调用
dump_cpu_state
在dump_runq中调用
Xen并没有提供一个调度器注册函数,要添加新的调度器只能修改schedulers数组,目前这个数组中有如下成员:
static struct scheduler *schedulers[] = {
&sched_sedf_def,
&sched_credit_def,
NULL
}
task_slice,调度器用这个结构和调度核心接口,包括了下一个运行的VM以及运行的时间片大小
struct task_slice {
struct vcpu *task;
s_time_t time;
}
当调度核心需要调度一个虚拟机时就会咨询当前的调度器,调度器通过返回这样的一个结构来确定下一个调度运行的虚拟机
第二节 调度核心
Xen的调度器核心为schedule函数,这个函数实现VM切换操作,主要逻辑如下:
static void schedule(void)
{
struct vcpu *prev = current;
...
next_slice = ops.do_schedule(now);//咨询调度器,得到下一个需要调度的VM
...
r_time = next_slice.time;//运行时间片
next = next_slice.task;//下一个VM
...
set_timer(&sd->s_timer, now + r_time);//定时
...
context_switch(prev, next);
...
set_current(next);
...
__context_switch();
...
schedule_tail(next);//这一步完成切换
}
static void __context_switch(void)
{
struct cpu_user_regs *stack_regs =guest_cpu_user_regs();
...
if ( !is_idle_vcpu(p) )
{
memcpy(&p->arch.guest_context.user_regs,
stack_regs,
CTXT_SWITCH_STACK_BYTES);//保存现场
...
p->arch.ctxt_switch_from(p);
}
if ( !is_idle_vcpu(n) )
{
memcpy(stack_regs,
&n->arch.guest_context.user_regs,
CTXT_SWITCH_STACK_BYTES);//恢复现场
n->arch.ctxt_switch_to(n);
}
...
}
schedule_tail函数完成最后的切换,不同的体系下,这个函数有不同的实现,但是都要调用reset_stack_and_jump
✔ reset_stack_and_jump
#define reset_stack_and_jump(__fn) \
__asm__ __volatile__ ( \
"mov %0,%%"__OP"sp; jmp "STR(__fn) \
: : "r" (guest_cpu_user_regs()) : "memory" )
这段代码进行栈帧的切换,最后根据__fn的不同将实现不同的功能:有的__fn会调整栈帧,最后利用ret指令实现向栈帧中地址的跳转,例如在paravirt下;有的__fn则读取栈帧中数据写入到另外的控制结构中,然后用别的方式实现切换,例如在hvm下;有的__fn则是一个死循环,例如reset_stack_and_jump(idle_loop)中的idle_loop
✔ paravirt,这个体系下有2个切换函数:
static void continue_idle_domain(structvcpu *v)
{
reset_stack_and_jump(idle_loop);
}
static void continue_nonidle_domain(structvcpu *v)
{
reset_stack_and_jump(ret_from_intr);
}
✔ hvm下分别是vmx_do_resume和svm_do_resume,类似的这两个函数也会在最后调用reset_stack_and_jump.例如,vmx是调用reset_stack_and_jump(vmx_asm_do_vmentry),vmx_asm_do_vmentry会把栈帧中的eip,esp,eflags写入到vmcs区域,最后用vmlaunch或vmresume指令进入VM.所以hvm下对栈帧的利用和在paravirt下还是不一样的,后者的切换方式和linux的任务切换是类似的.
第三节 时钟中断
Xen利用时钟中断来实现虚拟机按时间片运行的功能.当有时钟中断发生时,会调用下面
函数:
fastcall void smp_apic_timer_interrupt(struct cpu_user_regs * regs)这个函数设置定时器软中断标志后就返回了,紧接着会执行下面的代码序列:
ENTRY(ret_from_intr)
GET_CURRENT(%ebx)
movl UREGS_eflags(%esp),%eax
movb UREGS_cs(%esp),%al
//根据这个来判断时钟中断进入的是客户系统(ring3)还是Xen(ring0)
testl $(3|X86_EFLAGS_VM),%eax
//如果是客户系统,则执行所有的软中断,例如,如果有调度定时器到期,
//系统就会调用schedule函数,切换掉当前的虚拟机,需要注意的是当进入
//schedule函数时,堆栈指针刚好指向一个cpu_user_regs结构,这个结构会
//在__context_switch中保存到vcpu的状态中
jnz test_all_events
//否则返回Xen
jmp restore_all_xen
第四章 内存管理
Xen在初始化的过程中在不同的阶段会使用不同的内存管理方法(内存分配器),这些方法依次是:e820内存分配器,boot内存分配器,堆分配器.堆分配器是Xen正常运行下的主内存分配器,所有的客户系统的内存都是从这个分配器上分配.
第一节 初始内存分配
要点如下:
✔ 内存信息来源
mmap_type
说明
Xen-e820
内存信息由mem.S使用e820获取
Xen-e801
内存信息由mem.S使用e801获取
Multiboot-e820
内存信息由bootloader使用e820获取
Multiboot-e801
内存信息由bootloader使用e801获取
✔ 不管使用哪种方法,xen都使用e820_raw数组来记录系统最初的内存信息,这个数组的大小为e820_raw_nr
✔ 整理为e820,这是一个e820map类型的变量,xen通过下面调用将e820_raw转化为e820max_page = init_e820(memmap_type, e820_raw,&e820_raw_nr)max_page为内存上限的页框数.之后,e820就成为xen内存管理的主要数据结构,所有的内存分配操作都是围绕这个结构来进行的
✔ e820内存分配
int __init reserve_e820_ram(struct e820map*e820, uint64_t s, uint64_t e)这个函数从e820上分配地址从s到e的内存
✔ 页表的更新1:e820中的内存必须映射到页表中才能使用:
for ( i = boot_e820.nr_map-1; i >= 0;i-- )
{
...
map_pages_to_xen(
(unsigned long)maddr_to_bootstrap_virt(s),
s >> PAGE_SHIFT, (e-s) >> PAGE_SHIFT, PAGE_HYPERVISOR);
...
}
Xen的页表使用的是2M页框和4K页框的混合模式,当需要映射的内存满足一定的条件时,将按照2M页框来管理,否则按4K页框管理.上面代码仅映射16M-BOOTSTRAP_DIRECTMAP_END(x86_32下是1G)间并且是Superpage对齐的内存,另外x86_32下,线性地址和物理地址是一致的,参见下面宏定义:
#if defined(CONFIG_X86_64)
#define BOOTSTRAP_DIRECTMAP_END (1UL<< 32) /* 4GB */
#define maddr_to_bootstrap_virt(m)maddr_to_virt(m)
#else
#define BOOTSTRAP_DIRECTMAP_END (1UL<< 30) /* 1GB */
#define maddr_to_bootstrap_virt(m) ((void*)(long)(m))
#endif
注:这一步映射是为了满足移动module的需要,参见下图.
第二节 boot分配器
----------------<- initial_images_end =
min(1G,physical mem end)
dom0 kernel
----------------<- initial_images_start >= 16M
...
xenheap_phys_end -> ----------------
Xen堆
xenheap_phys_start -> ----------------
位图
bitmap_start -> ----------------
some round
_end -> ----------------
xen kernel
----------------
内存管理初始化示意图
boot内存分配器是一个暂时性的内存管理器.boot分配器的建立分为两步:
✔ init_boot_allocator,在_end后面(可能会有一些对齐)建立页框位图,位图大小取决于物理内存的多少
✔ 历遍e820数组,将xenheap_phys_end开始的内存登记到boot分配器中;对于大于16M-1G范围的内存还要映射到Xen的页表中(注1),这时的映射为1:1的映射,也就是说物理地址和线性地址一致.
注1:我们知道0-16M的空间在更早的初始化过程中已经映射到xen的页表中了,到这一步时,xen的线性空间0-1G __PAGE_OFFSET- +12M范围已经映射boot内存的使用,在Xen调用end_boot_allocator之前,可以用alloc_boot_pages函数来分配boot内存.例如,init_frametable就分配了boot内存.
unsigned long __init alloc_boot_pages(
unsigned long nr_pfns, unsigned long pfn_align)
用这个函数分配nr_pfns个连续页框,返回的是起始页框的页框号,pfn_align为起始页框的对齐参数,必须是2的幂,例如,可以要求起始页框在1,2,4等处对齐
第三节 堆分配器
堆分配器是xen的主内存分配器,这是一个和linux的内存分配器类似的分配器,使用(结点,区,order)的三元组来刻画内存,一个空闲的页框属于哪个区是由下面宏来计算的:
#define pfn_dom_zone_type(_pfn) (fls(_pfn)– 1)
例如,页框1属于0区,2,3属于1区,4,5...7属于2区,8,9...15属于3区等等.即,一个区的内存范围为[2^n,2^(n+1)-1],n为区号.在每个区中则按最大可用块的原则进行管理.
堆内存的建立分为两步:
✔ Xen堆,即xenheap_phys_start和xenheap_phys_end之间的内存.这是一个特殊的堆,这个堆上的内存映射到Xen线性空间DIRECTMAP_VIRT_START/DIRECTMAP_VIRT_END.参见alloc_xenheap_pages函数说明.Xen堆使用init_xenheap_pages函数来初始化
✔ Dom堆,管理除Xen堆之外的所有内存.用于Domain内存分配.Dom堆在boot分配器结束后初始化堆分配器的分配,堆分配器进行内存分配的核心函数为alloc_heap_pages,这个函数原型如下:
static struct page_info *alloc_heap_pages(
unsigned int zone_lo,
unsigned int zone_hi,
unsigned int cpu,
unsigned int order)
zone_lo和zone_hi为堆内存区范围,cpu用来计算需要从中分配的结点,order指明分配2^order个连续页框.Xen堆内存使用alloc_xenheap_pages来分配:
void *alloc_xenheap_pages(unsigned int order)
这个函数分配2^order个连续页框,返回的是线性空间DIRECTMAP_VIRT_START/ DIRECTMAP_VIRT_END中的地址:我们知道0-12M间的内存是映射到__PAGE_OFFSET开始的线性空间的,而__PAGE_OFFSET 和DIRECTMAP_VIRT_START是相等的,所以分配的线性地址是可以存取的.
Dom堆使用alloc_domheap_pages函数来分配,这个函数除了从堆中分配页框外,还会将分配的内存记录到域中:
struct page_info *alloc_domheap_pages(
struct domain *d,
unsigned int order,
unsigned int flags)
第四节 页框管理
1)页框管理结构
对于每个页框,Xen都分配一个page_info结构,称之为页框的管理结构.
struct page_info
{
struct list_head list;
u32 count_info;
union {
struct {
u32 _domain;
unsigned long type_info;
} __attribute__ ((packed)) inuse;
struct {
u32 order;
cpumask_t cpumask;
} __attribute__ ((packed)) free;
}u;
union {
u32 tlbflush_timestamp;
unsigned long shadow_flags;
};
};
页框管理结构记录了页框的使用情况,重要的成员如下:
✔ count_info这是一个32位的整数,用作页框的引用计数,各个位定义如下位说明
位
说明
0-25
引用计数
26-28
3-bit PAT/PCD/PWT cache-attribute hint
29
PGC_page_table,当页框用作页表时
30
PGC_out_of_sync,当标记为和Shadow页框不同步
31
PGC_allocated,当页框分配给客户系统后
✔ type_info
位
说明
0-25
类型引用计数
26
PGT_pae_xen_l2,仅PAE,页框是一个包含Xen私有映射的二级页目录
27
PGT_validated,页框的当前类型已验证
28
PGT_pinned,客户系统锁定了页框的当前类型
29-31
页框类型,参见下表
✔ 页框类型(这些用途是互斥的)
值
类型
说明
000b
PGT_none
无特殊用途
001b
PGT_l1_page_table
1级页表
010b
PGT_l2_page_table
2级页表
011b
PGT_l3_page_table
3级页表
100b
PGT_l4_page_table
4级页表
101b
PGT_gdt_page
GDT
110b
PGT_ldt_page
LDT
111b
PGT_writable_page
可写页
✔ shadow_flags,当这个页框被Shadow时,这个成员会被设置,
位
名称
说明
0
SH_type_none
1
SH_type_min_shadow SH_type_l1_32_shadow
2
SH_type_fl1_32_shadow
3
SH_type_l2_32_shadow
4
SH_type_l1_pae_shadow
5
SH_type_fl1_pae_shadow
6
SH_type_l2_pae_shadow
7
SH_type_l2h_pae_shadow
8
SH_type_l1_64_shadow
9
SH_type_fl1_64_shadow
10
SH_type_l2_64_shadow
11
SH_type_l2h_64_shadow
12
SH_type_l3_64_shadow
13
SH_type_l4_64_shadow SH_type_max_shadow
14
SH_type_p2m_table
15
SH_type_monitor_table
16
SH_type_unused
当需要设定一个页框的用途时,调用下面函数:
int get_page_type(struct page_info *page, unsigned long type)
page为需要设定用途的页框,type必须是上面”页框类型”表”类型”列中的值之一或者这些值之一与PGT_pae_xen_l2的或值.这个函数会:
✔ 增加type_info的引用计数
✔ 如果type与page现有的用途不一致,则page会被标记为未验证(清除PGT_validated位),但是PGT_writable_page例外,因为这个用途无需额外验证
✔ 如果page被标记为未验证,调用alloc_page_type验证之,如果成功,标记page为已验证当需要引用一个页框时,调用下面函数:
int get_page(struct page_info *page, struct domain *domain)
这个函数增加page的count_info引用计数
static inline int get_page_and_type(structpage_info *page,
struct domain *domain,
unsignedlong type)
这个函数则先调用get_page然后调用get_page_type
2)页框号的管理
Xen将系统内存按照每4K一个单位进行编号,称之为页框号,整个页框空间为0-max_page,这个空间中的页框号也叫机器页框号,或mfn,这个页框号是处理器可以识别的;当内存分配到客户系统中后,由于客户系统一般都要求连续的内存空间,因此,这些页框被重新编号,一般从0开始,这个编号叫客户物理页框号,或gpfn,客户系统工作在这个编号上.两者常常需要相互转换,因此Xen维护两个表:
一个是mfn到gpfn的映射表,这个表可以通过下列途径进行更新:
✔ 直接更新(使用set_gpfn_from_mfn函数),dom0的创建过程中就是使用这个方法
✔ 通过hypercall:XENMEM_populate_physmap,这个调用向Xen申请一定数量的内存,调用者需要传入一个gpfn的列表,Xen会为每个gpfn分配内存,并在映射表中建立关联.这个调用会返回一个客户系统分配到的mfn的列表,客户系统使用这个列表建立p2m映射
✔ 通过hypercall:MMU_MACHPHYS_UPDATE,调用者传入一个(mfn,gpfn)对,Xen将会更新
mfn的对应到新的gpfn
另一个是gpfn到mfn的映射表,这个表通过下列途径进行更新:
✔ 直接更新,例如dom0就是直接写入vphysmap_start数组
✔ 没启用PG_translate时,利用hypercall:XENMEM_populate_physmap的返回值(参见上面说明).例如,libxc在为domU申请内存后,就会将这个返回值暂存到p2m_host中,并最终通过mfn_list传递给domU.对于domU来说,其启动结构(start_info)成员mfn_list指向这个映射表, domU在调用某些要求mfn的hypercall时可以利用这个表来查找对应的mfn.在启用PG_translate的情况下,hypercall:XENMEM_populate_physmap不会返回mfn列表,客户系统不需要自己维护这样的一个列表,Xen会自动维护.
一些有用的宏/函数:
gfn_to_mfn(d, g, t)
返回g对应的mfn
static inline unsigned long gmfn_to_mfn(struct domain *d, unsigned longgpfn)
同gfn_to_mfn
static inline unsigned long mfn_to_gfn(struct domain *d, mfn_t mfn)
返回mfn对应的gpfn
第五章 页表管理
x86体系下,代码是通过线性地址空间来存取内存的,而处理器在执行内存操作时使用的是物理地址,因此需要一个叫页表的数据结构实现两者的转换.x86提供了多种页表映射模式,Xen只使用了其中的几种模式,参见第一节.页表可以是”稀疏”的,也就是说线性空间的某个部份在页表中可能并没有建立映射,当代码存取这部份空间时就会发生”缺页”,这时处理器会引发一个缺页中断,这个中断负责建立缺页地址的页表项.
Xen在初始化完毕后会建立自己的缺页中断处理函数,发生缺页的情况后,Xen会首先获得控制权,进行必要的处理,如果需要再将控制转移到客户系统的缺页管理函数.
需要澄清的是Xen中有两种类型的页表:
✔ Xen自身的页表,Xen运行在线性空间中,因此也需要页表.以x86_32为例,初始化完毕后Xen页表的映射关系如下(从高到低):
线性地址空间
说明
IOREMAP_VIRT_START至
IOREMAP_VIRT_END
ioremap()/map_domain_page_global()使用
DIRECTMAP_VIRT_START至
DIRECTMAP_VIRT_END
Xen堆分配空间.映射0-12M内存范围
RDWR_MPT_VIRT_START至
RDWR_MPT_VIRT_END
mfn->gpfn映射表
FRAMETABLE_VIRT_START至
FRAMETABLE_VIRT_END
page_info数组
16M-1G
Dom堆分配空间.映射16M-1G内存范围(注)
12M-16M
映射12M-16M内存范围
0M-12M
映射0M-12M内存范围,Xen映像
注:取决于1G和实际内存中的小者
✔ 域页表
线性地址空间
说明
PERDOMAIN_VIRT_START至
PERDOMAIN_VIRT_END
map_domain_page()
LINEAR_PT_VIRT_START至
LINEAR_PT_VIRT_END
domain页表空间
RO_MPT_VIRT_START至
RO_MPT_VIRT_END
domain的p2m表
v_start至v_end domain
映像
第一节 页表模式
Xen在x86_32配置下支持两种页表配置,每种配置下,Xen kernel和客户系统分别使用不同页框大小页表模式:
一,PAE模式
✔ 客户系统页表模式,线性地址被分解为4部分,对应3级页表,参见下表:
线性空间划分
页框大小
31-30
29-21
20-12
11-0
4K
l3页表
l2页表
l1页表
---
✔ Xen页表模式,线性地址被分解为3部分,对应2级页表,参见下表:
线性空间划分
页框大小
31-30
29-21
20-0
---
4K
idle_pg_table
idle_pg_table_l2
实际上Xen kernel使用的是混合页表模式,既有2M的页框,也有4K的页框
二,非PAE模式
✔ 客户系统页表模式,线性地址被分解为3部分,对应2级页表,参见下表:
线性空间划分
页框大小
31-22
l2页表
21-12
11-0
4K
l1页表
---
✔ Xen kernel页表模式,线性地址被分解为2部分对应1级页表,参见下表:
线性空间划分
页框大小
31-22
21-0
4M
idle_pg_table
---
第二节 dom0页表的构建
从前面可知,dom0映像会从Dom堆上分配内存,这部份内存就包括了dom0页表所需空间,具体的说从vpt_start到vpt_end之间的空间用来构建dom0的页表.根据是否启用了PAE模式,这段空间的分配如下:
偏移(页框)
用途
page_info
说明
PAE模式
0
第3级页表
PGT_l3_page_table
PGT_pinned
实际只使用4个表项
1-3
第2级页表
PGT_l2_page_table
初始化为idle_pg_table_l2
4
第2级页表
PGT_l2_page_table|
PGT_pae_xen_l2
5-
第1级页表
PGT_l1_page_table
v_start至v_end空间所需第1级页表从这里分配
非PAE模式
0
第2级页表
PGT_l2_page_table
PGT_pinned
初始化为idle_pg_table
1-
第1级页表
PGT_l1_page_table
v_start至v_end空间所需第
1级页表从这里分配
第三节 domU页表的构建
domU用libxc中的提供的函数来创建,当然libxc最终调用的是hypercall来完成相关功能.主要流程如下:
✔ 根据domU映像大小计算所需页表
✔ 分配页表所需内存
✔ 映射页表内存到dom0线性空间中以便读写
✔ 设置页表:将domU映像映射入线性空间,包括页表自身
✔ 设置vcpu context中的cr3
第四节 Xen线性空间
这部份线性空间驻留在Xen和domain线性空间的高端,并不是所有的空间范围都有效,例如,FRAMETABLE_VIRT_START/FRAMETABLE_VIRT_END就仅对Xen有效,
IOREMAP_VIRT_END ->---------------- <- 0xFFFFFFFF
4M
IOREMAP_VIRT_START ->---------------- <- DIRECTMAP_VIRT_END
12M
PERDOMAIN_VIRT_END
MAPCACHE_VIRT_END ->---------------- <- DIRECTMAP_VIRT_START
4M
MAPCACHE_VIRT_START ->----------------
4M
PERDOMAIN_VIRT_START ->---------------- <- SH_LINEAR_PT_VIRT_END
8M/4M
LINEAR_PT_VIRT_END ->---------------- <- SH_LINEAR_PT_VIRT_START
8M/4M
LINEAR_PT_VIRT_START ->---------------- <- RDWR_MPT_VIRT_END
16M/4M
FRAMETABLE_VIRT_END ->---------------- <- RDWR_MPT_VIRT_START
96M/24M
FRAMETABLE_VIRT_START ->---------------- <- RO_MPT_VIRT_END
16M/4M
----------------<- RO_MPT_VIRT_START
✔FRAMETABLE_VIRT_START/FRAMETABLE_VIRT_END,页框管理结构struct page_info数组.变量frame_table指向这个数组的开头.init_frametable分配并安装这个区间所需页框.和这个数组相关的一些宏如下:
宏
说明
mfn_to_page(mfn)
机器页框<->page_info结构
page_to_mfn(pg)
virt_to_page(va)
Xen堆线性地址<->page_info结构
page_to_virt(pg)
maddr_to_page(ma)
机器地址<->page_info结构
page_to_maddr(pg)
✔ RDWR_MPT_VIRT_START/RDWR_MPT_VIRT_END,这段空间是mfn到gpfn的映射表,所需页框(注意,这是2M大小的页框)在paging_init中分配并安装,与之相关的宏如下:
宏
说明
machine_to_phys_mapping
映射表的线性地址
set_gpfn_from_mfn(mfn, pfn)
设置mfn->pfn映射
get_gpfn_from_mfn(mfn)
获取mfn对应的pfn
✔ RO_MPT_VIRT_START和RO_MPT_VIRT_END,对于paging_mode_translate客户系统,这部份空间是gpfn到mfn的映射表(p2m表),所需内存由domU的创建者分配,例如,在libxc中alloc_magic_pages(xc_dom_x86.c)函数就负责计算并分配p2m表所需页框.分配的页框通过start_info.mfn_list传递给客户系统;对于Xen,这部份空间和RDWR_MPT_VIRT_START-RDWR_MPT_VIRT_END空间对应的都是一样的页框,即,是mfn到gpfn的映射,只不过这部份空间为只读映射,详细情况参见paging_init函数
✔ LINEAR_PT_VIRT_START/LINEAR_PT_VIRT_END,这部份空间用于存取domain的页表
✔ PERDOMAIN_VIRT_START/PERDOMAIN_VIRT_END,这部份空间所需页表由domain.arch.mm_perdomain_pt提供,这部份页表在arch_domain_create中计算并分配,对于dom0在construct_dom0中安装,对于domU是在create_pae_xen_mappings中安装.初始化情况如下:
偏移
(l1_pgentry_t
个数)
名称
说明
14
FIRST_RESERVED_GDT_PAGE
从这里开始,vcpuid
<<GDT_LDT_VCPU_SHIFT处为一个指
向gdt_table的l1表项,共
MAX_VIRT_CPUS个
fixme
domain.arch.mapcache.l1tab
MAPCACHE_VIRT_START/MAPCACHE_VIRT_END空间所需页表,共1024个表
项,刚好映射,这个空间段:4M
✔ MAPCACHE_VIRT_START/MAPCACHE_VIRT_END,这部份空间用于临时映射一些页框,以便
Xen对页框的读写,参见下面函数:
函数
说明
void *map_domain_page(unsigned long mfn)
映射mfn到上述空间中
void unmap_domain_page(void *va)
取消va处的映射
注:这部份空间所需页表来自d->arch.mapcache.l1tab,参见mapcache_domain_init函数
✔DIRECTMAP_VIRT_START/DIRECTMAP_VIRT_END,Xen堆线性空间,即,所有从Xen堆分配的内存,其线性地址都落入这个范围,相关的宏如下:
宏
说明
virt_to_maddr(va)/__pa(x)
Xen堆线性地址<->机器地址
maddr_to_virt(ma)/__va(x)
✔ IOREMAP_VIRT_START/IOREMAP_VIRT_END,这部份空间也是用于临时映射一些页框,以便Xen对页框的读写,所需页表在paging_init中分配并安装.使用下面函数来映射页框到这个空间:
函数
说明
void *map_domain_page_global(unsigned
long mfn)
映射mfn到上述空间中
void unmap_domain_page_global(void *va)
取消va处的映射
第五节 缺页中断
我们知道,Xen对MMU也进行了虚拟化,当发生缺页中断时,Xen的中断逻辑将首先被执行:
asmlinkage void do_page_fault(structcpu_user_regs *regs)
{
...
if ( unlikely(fixup_page_fault(addr, regs) != 0) )
return;
if ( unlikely(!guest_mode(regs)) )
{
if ( spurious_page_fault(addr, regs) )
return;
if ( likely((fixup = search_exception_table(regs->eip)) != 0) )
{
...
regs->eip = fixup;
return;
}
...
}
propagate_page_fault(addr, regs->error_code);//调用客户系统的缺页逻辑
}
static int fixup_page_fault(unsigned longaddr, struct cpu_user_regs *regs)
{
if ( unlikely(IN_HYPERVISOR_RANGE(addr)) )//addr>=HYPERVISOR_VIRT_START
{
if ( paging_mode_external(d) && guest_mode(regs) )
{
int ret = paging_fault(addr, regs);
if ( ret == EXCRET_fault_fixed )
trace_trap_two_addr(TRC_PV_PAGING_FIXUP, regs->eip, addr);
return ret;
}
if ( (addr >= GDT_LDT_VIRT_START) && (addr <GDT_LDT_VIRT_END) )
return handle_gdt_ldt_mapping_fault(
addr - GDT_LDT_VIRT_START,regs);
return 0;
}
if ( VM_ASSIST(d, VMASST_TYPE_writable_pagetables) &&
guest_kernel_mode(v, regs) &&
((regs->error_code & PFEC_write_access) == PFEC_write_access)&&
ptwr_do_page_fault(v, addr, regs) )
return EXCRET_fault_fixed;
if ( paging_mode_enabled(d) )//启用了页表助手
{
int ret = paging_fault(addr, regs);//调用页表助手缺页逻辑
if ( ret == EXCRET_fault_fixed )
trace_trap_two_addr(TRC_PV_PAGING_FIXUP, regs->eip, addr);
return ret;
}
}
从上面可知,在没有启用页表助手的情况下,这个缺页逻辑基本上等同于客户系统的缺页逻辑.
第六节 页表助手
页表助手是指domain.arch.paging.mode成员的值,这些值直接影响着Xen页表管理逻辑,参见下表:
位
名称
说明
相关宏
10-12
修饰符
001b:PG_refcounts
paging_mode_refcounts
010b:PG_log_dirty
paging_mode_log_dirty
011b:PG_translate
paging_mode_translate
100b:PG_external
paging_mode_external
20
PG_SH_enable
启用Shadow助手
paging_mode_shadow
shadow_mode_enabled
shadow_mode_refsounts
shadow_mode_log_dirty
shadow_mode_translate
shadow_mode_external
21
PG_HAP_enable
启用HAP助手
paging_mode_hap
另外paging_mode_enabled定义为:
#define paging_mode_enabled(_d) ((_d)->arch.paging.mode)
含义是是否启用了页表助手页表助手接口,用于实现特定的页表助手,Xen的缺页逻辑在适当的时候调用这个接口,参见第五节.目前,Xen实现了两个页表助手:Shadow和HAP,因此也存在两个这样的接口,这个接口用结构paging_mode来代表,其成员如下:
成员
说明
page_fault
缺页处理函数
invlpg
gva_to_gfn
update_cr3
如果domain是paging_mode_enabled,这个成员将会在
update_cr3函数中被调用
update_paging_modes
write_p2m_entry
write_guest_entry
cmpxchg_guest_entry
guest_map_l1e
guest_get_eff_l1e
第七节 Shadow页表
Shadow页表是和客户页表一样的一套页表,在启用Shadow助手时,Xen会用这套页表替换客户页表.要点如下:
✔ 更新cr3时Shadow将拷贝客户的顶级页表(下面代码是以x86_32pae为例):
//得到客户页表页框
gmfn = pagetable_get_mfn(v->arch.guest_table);
//映射到线性空间中,这里guest_idx =0
gl3e = ((guest_l3e_t *)sh_map_domain_page(gmfn)) + guest_idx;
//拷贝
for ( i = 0; i < 4 ; i++ )
v->arch.paging.shadow.gl3e = gl3e;
for ( i = 0; i < 4; i++ )
{
//创建Shadow,Shadow页框将记录在arch_vcpu.shadow_table数组中
sh_set_toplevel_shadow(...);
}
//创建l3table
for ( i = 0; i < 4; i++ )
{
smfn = pagetable_get_mfn(v->arch.shadow_table);
v->arch.paging.shadow.l3table =
(mfn_x(smfn) == 0)
? shadow_l3e_empty()
: shadow_l3e_from_mfn(smfn,_PAGE_PRESENT);
}
//最后更新cr3指向Shadow页表
v->arch.cr3 = virt_to_maddr(&v->arch.paging.shadow.l3table);
✔ 其它层级的Shadow页表则是在缺页中断处理函数中建立的
static int sh_page_fault(struct vcpu *v,
unsigned long va,
struct cpu_user_regs*regs)
{
...
ptr_sl1e = shadow_get_and_create_l1e(v, &gw, &sl1mfn, ft);
...
//生成sl1e表项
l1e_propagate_from_guest(v, gw.l1e, gmfn, &sl1e, ft, p2mt);
//写入之
r = shadow_set_l1e(v, ptr_sl1e, sl1e, sl1mfn);
}
当启用Shadow时,Xen会从domain堆上分配一些Shadow功能所需的页表,这些页表记录在domain.arch.paging.shadow.freelists中,这是一个不同大小内存块的数组,相同大小的内存块链接在一起,最大的内存块为2^(SHADOW_MAX_ORDER + 1).当需要从这个内存池分配内存时,使用下面函数:
mfn_t shadow_alloc(struct domain *d,
u32 shadow_type,
unsigned long backpointer)
这个函数返回与shadow_type对应大小的内存,当某个客户页表需要Shadow时,就可以调用这个函数来分配一个Shadow页.Shadow助手维护一个Shadow页页框号和客户页页框号对应关系的高速缓存(shadow hash),每当Shadow一个客户页表时就在hash表中创建一个条目.
第六章 事件管道
事件管道(event channel)是向域发送消息的一种通信机制.管道的一端连接一个域,另一端连接一个事件源,这些事件源可以是其它的域,物理中断,虚拟中断和IPI消息,当连接的是两个域时,管道可以用于域间的双向通信.每个这样的管道都有一个编号,称之为端口.一个域可以打开的端口数为 NR_EVENT_CHANNELS(1024)个.下面是事件管道用途的列表:
用途
说明
ECS_FREE
管道可供分配
ECS_RESERVED
管道被保留
ECS_UNBOUND
等待远端接入
ECS_INTERDOMAIN
远端已接入或已接入到远端
ECS_PIRQ
物理IRQ管道
ECS_VIRQ
虚拟IRQ管道
ECS_IPI IPI
消息管道
第一节 事件的处理
是指Xen是如何实现管道机制的,要点如下:
✔ 当某个管道有事件发生时,domain.shared_info->evtchn_pending的对应位会被置位. 随后Xen会调用vcpu_mark_events_pending,这个函数会设置vcpu.vcpu_info->evtchn_upcall_pending成员
✔ 在每次从中断中返回时,evtchn_upcall_pending会被检查,如果这个成员值不为0,Xen
会进入事件处理流程(详情参见entry.S中的test_all_events)
✔ 一般来说客户系统会调用do_set_callbacks或者do_callback_op hypercall向Xen注册自己的事件回调函数(前者是后者的简化版).例如,mini-os调用前者设置事件处理函数为hypervisor_callback.客户系统在这个回调函数中实现自己的事件处理逻辑.
第二节 事件管道hypercall
调用
说明
EVTCHNOP_alloc_unbound
客户系统用这些调用来使用Xen的事件管道机制,列表如下:从域中分配一个等待某个远端域连接的事件管道.注,只
有特权域,例如dom0才能从任意域中分配端口,普通域只能本地分配端口.调用返回新分配的本地端口.这种类型的管道是留做inerdomain类型管道的接入的
EVTCHNOP_bind_interdomain
建立一个当前域(注1)到远端域的管道.这个调用需要指定远端域和远端端口号,远端域的端口必须是预先用alloc_unbound为本域分配的.调用会返回管道的本地端口号.注,这是一个双向管道,即,管道的两端都可以向对方发送信号
EVTCHNOP_bind_virq
建立虚拟中断(virq)事件管道.这个调用对当前域进行操作,并且要指定域内的通知vcpu(注2),如果virq为全局virq(注3),通知 vcpu只能是0.当需要引发virq时,调用send_guest_vcpu_virq或send_guest_global_virq函数
EVTCHNOP_bind_ipi
建立IPI消息的事件管道.这个调用对当前域进行操作,并且要指定域内IPI信号的通知vcpu.用EVTCHNOP_send调用发送IPI信号
EVTCHNOP_bind_pirq
建立物理中断(pirq)事件管道.这个调用对当前域进行操作.这个调用会把当前域加入到pirq处理函数的客户系统列表中,这样在发生pirq时,客户 系统就会收到中断.注:pirq管道是需要一个通知vcpu参数的,但是在这个调用中没有设置,需要再调用一次EVTCHNOP_bind_vcpu设置
EVTCHNOP_close
关闭指定域中的指定的管道.关闭后,管道状态被设置为ECS_FREE,如果是interdoamin管道,远端管道被设置为ECS_UNBOUND
EVTCHNOP_send
用于向事件管道发送一个消息.这个调用对当前域进行操作.仅支持IPI和INTERDOMAIN管道,对于前者,相当于给通知vcpu发送一个IPI信号,对于后者则是向远端域的通知vcpu发送信号
EVTCHNOP_status
读取指定域的指定管道状态.注,只有特权域,例如dom0才能读取任意域的管道状态,否则只能对自己进行此操作
EVTCHNOP_bind_vcpu
设置管道的通知vcpu,仅支持virq(仅当virq为全局时),unbound,interdomain,pirq管道
EVTCHNOP_unmask
启用通知vcpu上的指定管道.这个调用对当前域进行操作
EVTCHNOP_reset
对指定域上的所有管道调用EVTCHNOP_close.注,只有特权域才能对其它域进行此操作,普通域只能对自己进行此操作
注1:当前vcpu所属域
注2:即evtchn的notify_vcpu_id成员,下面是evtchn的成员,为了便于对上述调用的理解,将evtchn结构列示在下面:
struct evtchn
{
u8 state; /* ECS_* */
u8 consumer_is_xen; /* Consumed by Xen or by guest? */
u16 notify_vcpu_id; /* VCPU forlocal delivery notification */
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;
#ifdef FLASK_ENABLE
void *ssid;
#endif
};
注3:即virq_is_global(virq),全部的virq列表如下:
virq
名称
全局
说明
0
VIRQ_TIMER
否
vcpu定时器中断.可以通过set_periodic_timer hypercall来设置定时器间隔
1
VIRQ_DEBUG
否
要求客户系统生成调试信息
2
VIRQ_CONSOLE
是
由紧急console驱动产生,通知dom0接收到新字符
3
VIRQ_DOM_EXC
是
域异常中断(dom0).在域关闭时产生该
中断
4
VIRQ_TBUF
是
跟踪缓冲区有记录产生(dom0)
6
VIRQ_DEBUGGER
是
当有客户系统因调试而暂停时(dom0)
7
VIRQ_XENOPROF
否
xenprofile有新的数据时
8
VIRQ_CON_RING
是
由console驱动产生,通知dom0接收到virq 名称 全局 说明新字符
16-23
VIRQ_ARCH_0-VIRQ_ARCH_7
取决于体系
第三节 事件管道设备
这个设备是dom0-linux下用户空间和Xen事件系统间的接口.设备路径为/dev/xen/evtchn.要点如下:
✔ 每次打开这个设备时,Xen将会分配一个页面作为环形读写区和一个管理结构.读写的基本元素为管道端口号.
✔ 读操作,当没有事件发生时,读操作会休眠,如果有事件发生,读入的是一个管道端口号的数组,该数组中的所有端口都有事件发生.
✔ 写操作,这个操作开启指定的事件管道.
✔ 可以通过ioctl来进行上面的事件管道操作.
第七章 设备模型
user tools
|
unix socket
|
user space xenstored
----------|---------- per-domain xenstoreshared page
kernel xenbus
如上图所示,Xen使用一个叫xenstore的中心数据库来存储设备信息,这是一个按树状结构组织的数据库,例如,每个域在/local/domain/目录下有一个对应目录,目录名就是域ID号,域目录下最重要的两个和设备配置有关的子目录是backend和device,分别存放后端和前端设备的配置参数.
xenstore由xenstored守护者进程来管理,这个进程通过unix socket响应用户空间的请求.例如,用户空间工具可以配置某个域中的前后端设备.xenstored可以响应的消息如下表所示:
消息
说明
XS_DIRECTORY
列出指定目录下的内容
XS_READ
读取某个结点值
XS_WRITE
写入某个结点值
XS_MKDIR
创建一个目录
XS_RM
移除一个目录
XS_GET_PERMS
读取某个目录的操作权限
XS_SET_PERMS
设置某个目录的操作权限
XS_DEBUG
XS_WATCH
监听某个路径,当路径发生改变时将调用设定的回调函数
XS_UNWATCH
解除路径监听
XS_TRANSACTION_START
开始一个事务
XS_TRANSACTION_END
结束一个事务
XS_INTRODUCE
引入一个域,建立起xenstored守护者进程和xenbus之间的通信区
XS_IS_DOMAIN_INTRODUCED
域是否已引入
XS_RELEASE
释放XS_INTRODUCE时创建的xenstored和xenbus之间的连接
XS_GET_DOMAIN_PATH
返回指定域的路径,即,/local/domain/<domid>
XS_RESUME
另外每个域在创建时都会分配一个xenstore page,这个页框将作为xenbus和xenstored之间的通信区.当设备驱动需要读写配置信息时,就通过xenbus来存取.
第一节 设备模型
Xen的设备模型将设备分为两部份,一部份叫后端,一部份叫前端.后端一般运行在dom0中,负责和真正的设备打交道,而前端运行在domU中,为domU中的客户系统提供设备.后端和前端之间通过事件管道进行通信.下面以块设备为例说明Xen设备模型的基本工作流程:
gendisk
|
blkback<----channel----> blkfront
|
blkif
上图是块设备模型的一个总体结构,blkfront和blkback分别是块设备的前端和后端,在xenstore中的目录为device/vbd和backend/vbd.blkif这个对象代表的是后端的真实设备,由它向物理设备派发请求.gendisk则是呈现给domU的设备.所有的块设备请求通过blkfront传递给blkback再由后者派发到物理设备.整个系统初始化的要点如下:
1,blkfront设备
1,blkfront_probe,这是blkfront设备驱动的探测函数
1,读取xs:virtual-device参数
2,分配并填写一个blkfront_info结构,并用这个结构调用talk_to_backend
2,talk_to_backend,setup_blkring
1,分配ring缓冲区,这是前后端的主通信区,并将缓冲区授权给后端存取
2,分配一个unbound的事件管道,该管道通过一个irq连接blkif_int函数,该函数处理后端信号:更新请求的执行情况
3,将第一步中缓冲区的授权号写入xs:ring-ref参数;将第二步中分配的端口号写入xs:evtchn-channel参数;设置xs:protocol参数为XEN_IO_PROTO_ABI_NATIVE
4,切换状态为XenbusStateInitialised,这个状态会触发后端的connect_ring和update_blkif_status
3,connect
1,读取后端设备的sectors,info,sector-size参数
2,添加vbd设备:xlvbd_add
3,切换状态为XenbusStateConnected
4,添加一个gendisk
2,blkback设备
1,blkback_probe,这是blkback设备驱动的探测函数
1,分配一个blkif结构
2,监听xs:physical-device参数,这个参数是MAJOR:MINOR的格式,监听处理函数会根据这个物理设备号创建相关结构,核心是得到一个block_device结构
3,切换状态为XenbusStateInitWait
2,connect_ring
1,读取前端设备的ring-ref,event-channel,protocol参数
2,映射blkif
1,分配blk_ring_area
2,映射ring缓冲区到blk_ring_area
3,初始化blk_rings,实际也是指向了ring缓冲区
4,interdomain连接到前端的事件管道,并连接到blkif_be_int函数,该函数处理前端信号:唤醒blkif_schedule线程处理块设备请求
3,update_blkif_status
1,在xs中生成sectors,info,sector-size块设备参数
2,切换状态为XenbusStateConnected,这个状态会触发前端的connect
3,创建一个跑blkif_schedule的线程,线程的名称为blkback.<domid>.<name>,其中name根据xs的dev参数生成.这个线程负责执行ring缓冲区中的块设备请求
第二节 授权表
xen <-shared[] -> domU
授权表(grant table),用于实现不同客户系统间共享页框的功能,如上图所示,Xen和域之间通过一个共享数组shared来交换授权表信息.当一个域需要授权某个域访问其页框时,就在shared数组中填写相关的授权信息.当需要时,Xen读取该信息将共享页框映射到被授权域.因为授权表用况大量出现在设备驱动中(例如,块设备驱动中,客户系统的IO页框会授权给后端设备读写),所以放在本章讲解.Xen用域的grant_table成员来管理授权表系统,重要成员如下:
✔ shared,这是一个struct grant_entry类型的数组,Xen会和客户系统共享shared数组,因此,客户系统可以通过读写相应的grant_entry条目的方式来授权某个页框(frame)到某个域(domid)
struct grant_entry {
uint16_t flags;
domid_t domid;
uint32_t frame;
}
下面这个宏用于存取授权表e对应的shared条目,t是域的grant_table成员shared_entry(t, e)
✔ active,这是一个struct active_grant_entry类型的数组,当域需要访问某个授权表时,调用map_grant_ref hypercall来映射授权表到线性空间中供存取.这时Xen会使用一个active条目来记录这个映射.frame从shared数组条目复制过来
struct active_grant_entry {
u32 pin;
domid_t domid;
unsigned long frame;
}
✔ 下面这个宏用于存取授权表e对应的active条目,t是域的grant_table成员active_entry(t, e)
第九章 hypercall
hypercall是Xen提供给客户系统的交互介面,客户系统通过hypercall存取硬件资源.根据Xen的安全模型,Xen运行在ring0层,而dom0可以运行在ring0或ring1层,domU运行在ring3层,运行在不同层的客户系统,其hypercall有不同的调用方法.
第一节 hypercall初始化
为了能够使用hypercall,客户系统必须分配一个hypercall页框,并将页框地址告诉Xen(通过HYPERCALL_PAGE标签),Xen在加载客户系统时就会在这个页框中创建hypercall调用,具体的说就是在页框中植入一些特定的代码,根据客户系统安全层次/体系的不同,将有不同的代码模版,参见下表:
体系
子体系/安全层次
指令模版
paravirt
ring0
pushf
cli
mov i,eax
lcall __HYPERVISOR_CS,&hypercall
ret
对于iret为:
push %eax
pushf
cli
mov i,%eax
lcall __HYPERVISOR_CS,&hypercall
ring1,ring3
mov i,eax
int 0x82
ret
对于iret为:
体系 子体系/安全层次 指令模版
push %eax
mov __HYPERCALL_iret,eax
int 0x82
hvm
vmx
mov i,%eax
vmcall
ret
svm
略
i为hypercall的调用号.最终形成如下的调用页框(假设ring3的情况)
hypercall_page:
mov 0,eax
int 0x82
ret
...
mov 1,eax
int 0x82
ret
...
...
mov n,eax
int 0x82
ret
上面每个代码模版会相差32个字节,因此,客户系统的hypercall调用模版为:
call hypercall_page + nr * 32
其中nr为hypercall调用号,hypercall的参数则依次用eax,ebx,ecx,edx,esi,edi来传递xen支持的hypercall入口在entry.S中(hypercall_table),列表如下(按调用号排序):
1. do_set_trap_table /* 0 */
2. do_mmu_update
3. do_set_gdt
4. do_stack_switch
5. do_set_callbacks
6. do_fpu_taskswitch /* 5 */
7. do_sched_op_compat
8. do_platform_op
409. do_set_debugreg
10.do_get_debugreg
11.do_update_descriptor /* 10 */
12.do_ni_hypercall
13.do_memory_op
14.do_multicall
15.do_update_va_mapping
16.do_set_timer_op /* 15 */
17.do_event_channel_op_compat
18.do_xen_version
19.do_console_io
20.do_physdev_op_compat
21.do_grant_table_op /* 20 */
22.do_vm_assist
23.do_update_va_mapping_otherdomain
24.do_iret
25.do_vcpu_op
26.do_ni_hypercall /* 25 */
27.do_mmuext_op
28.do_xsm_op
29.do_nmi_op
30.do_sched_op
31.do_callback_op /* 30 */
32.do_xenoprof_op
33.do_event_channel_op
34.do_physdev_op
35.do_hvm_op
36.do_sysctl /* 35 */
37.do_domctl
38.do_kexec_op
本文不对这些hypercall作详细讲解. |
|