Xen调度分析-RT

 

前言

RT      RealTime实时调度

CPU 单处理器芯片

pcpu    单处理器芯片中的一个核。

vcpu    Xen的基本调度单位,可理解为进程。

预算    Xen的RT调度中预算指的是任务的剩余运行时间

文档所分析代码为Xen4.11版本。

经过编写过程中的反复查看,发现本文易读性较差,因此引入彩色字体,

淡灰色字体表示与主题相关性不大;

红色字体表示重要信息;

紫色字体是为了提神;

各色字体交织使用表示同色字体的连续性

认真讲,我并不十分看好Xen on arm的发展(代码量不小、前有seL4,后有Ziron),但是不得不说他是目前我们唯一可获得、可用的arm端支持虚拟化的微内核,所有微内核都是有其共同点的,即保留最核心内核功能(其实我认为应该做出某种权衡,在微内核、宏内核之间,是否可以进行更细的分割,以平衡生态、代码量、可移植性)。于是其他基于微内核的虚拟化实现,必然会走与Xen相似、甚至相同的路,如果能有其他更优秀的实现路线:比如不再依赖Domain0、更灵活的硬件外设驱动方案,也是会与Xen的思想有着千丝万缕的联系(就像Xen与Linux很多思想是一致的。于是我认为对Xen的解析,对HYP微内核的深刻理解,是可以为在微内核上的进一步发展提供灵感的,Xen当然是并不完善的,但是其优秀的成分必然也不少(不然谁会为止贡献代码呢),你可以肆意的优化甚至大规模改变流程,但是对于一个微内核虚拟化方案,所有Xen需要支持的东西,我们肯定是无法错过的;于是作为一个切入点,Xen是值得的。

 

 

目录

前言

从核心的启动函数start_xen()说起

在start_xen()

软中断softirq执行路线

软定时器机制struct timers和struct timer

总结

一次调度的处理

在schedule()

描述Xen的调度单位vcpu

Xen向GuestOS伸出了魔爪

小结

RT调度的do_schedule()

RT调度的预算补充

RT调度的唤醒

RT调度的优先级

RT调度的数据结构

RT调度的deadline

小结

进一步

 

从核心的启动函数start_xen()说起

调度时内核最核心功能之一,从内核启动开始就会对启动调度做准备,于是分析Xen内核启动过程中对调度的预初始化和初始化,是分析调度的初始步骤。Xen内核启动,会执行start_xen()过程,start_xen()是Xen微内核C代码的入口。

在start_xen()

1.  setup_cache();

微内核会检查cache属性(略);

2.  percpu_init_areas();

为每个pcpu分配空间,因为很多数据各pcpu都会有,比如cpu的频率、id等[1]。此空间用于存放各pcpu自有数据,避免各个pcpu竞争使用造成饥饿,一般称这种变量为per-cpu变量[1]

3.  set_processor_id(0);

Xen微内核启动首先会在第一个pcpu上执行,并设置所在的pcpu的id。[2]

4.  set_current((structvcpu *)0xfffff000); idle_vcpu[3][0]= current[4];

现在代码处于Xen微内核启动的第一个执行vcpu [5],其他的pcpu并未启动,最终,本vcpu会退化成为一个idle_vcpu [6]

5.  setup_virtual_regions(NULL,NULL);

略。

6.  init_traps();

运行在Xen上的GuestOS(包含特权域)都会认为自己拥有整个硬件[7],于是所有的GuestOS都会尝试执行所有计算指令(包括特权指令),而这就是CPU对虚拟化支持的意义所在(指令被分为EL0-EL3(aarch64)特权等级),每一个特权指令都不会被随意执行,当运行在Xen之上的GuestOS在硬件执行了EL2级特权指令(GuestOS内核才被允许在EL1执行),EL2特权指令将会产生异常,被Xen捕获,init_traps()就是在初始化捕获器,包含关键的CP15 CR12中HVBAR[8]等。这也是ARM架构规定特权指令等级的实施方式。

7.  smp_clear_cpu_maps();

Xen要根据设备树进行初始化设置,清除CPU位图后,将当前CPU0设入位图。

8.  device_tree_flattened=early_fdt_map(fdt_paddr);

fdt_paddr在启动start_xen()时传入,是设备树的起始地址;FDT(flatted device tree)的DTB存放有板级设备信息,大小不超过2M,最少8byte对齐,early_fdt_map()会对DTB合法性进行检查,并创建对内存的映射[9]device_tree_flattened应该是指向DTB所在内存起始地址。

9.  其他初始化

Xen在随后根据DTB信息进行了大量初始化,设置启动地址、设置页表、根据平台初始化ACPI(Advanced Configuration and Power Management Interface)的AP、完成内存初始化,这些操作与Xen调度分析关系不大,此处均不详述(主要是有些东西弄清楚耗时太长)。

10. system_state=SYS_STATE_boot

system_state全局变量标志的Xen当前的状态[10]当前,Xen完成了内存子系统的初始化,从early_boot进入boot状态。

11. vm_init();

虚拟内存管理的初始化。

12. init_IRQ();

各个中断TYPE的初始化为IRQ_TYPE_INVALID(即IRQ正在初始化)[11]

13. platform_init();

根据硬件平台的初始化,有厂商编写并根据Xen的接口封装,Xen会根据DTB信息进行匹配,得到可用的硬件平台接口,并调用接口初始化函数。[12]

14. 对定时器、中断控制器GIC、串口输出、各pcpu预初始化。

preinit_xen_time();  初始化启动所在CPU的定时器[13],查找定时器在DTB的节点,设置设备节点的使用者为DOMID_XEN[14];调用平台接口初始化此定时器,记录启动时间[15]

gic_preinit();       初始化中断控制器节点。[16]

串口初始化       初始化UART设备、串口输出的中断、申请串口环形缓冲区。

CPU初始化       检测各pcpu可用性、一般属性,初始化CPU位图,更新nr_cpu_ids(保存有pcpu数)。

15. init_xen_time();

获得定时器中断资源,放入timer_irq数组

16. gic_init();

GIC私有structgic_hw_operations * gic_hw_ops是GIC设备接口入口,此处调用其init()接口对GIC设备初始化。

17. tasklet_subsys_init();

对tasklet机制[17]进行初始化,对各pcpu创建tasklet_listsoftirq_tasklet_list;注册一个CPU的Notifiter[18];打开TASKLET_SOFTIRQ,并设定tasklet_softirq_action()为软中断处理函数。tasklet_softirq_action()调用do_tasklet_work()将struct tasklet从原有链表摘下并执行其内部提供的处理函数;执行完成后,若struct tasklet->scheduled_on[19]>=0则调用tasklet_enqueue()。tasklet_enqueue()会根据struct tasklet->is_softirq[20]决定将tasklet加入指定pcpu的softirq_tasklet_list[21]或tasklet_list[22]softirq_tasklet_list将会在软中断执行tasklet_list则会置位对应pcpu的tasklet_work_to_do变量的_TASKLET_enqueued位[23]

18. xsm_dt_init();

Xen Security Modules。Xen限制Domain的安全访问控制机制,可以定义域间、域与HYP以及相关内存、设备资源的通讯、使用。类SELinux。

19. init_timer_interrupt();

通过request_irq(),将timer_irq定时器中断资源与处理函数timer_interrupt()连接[24]

20. timer_init();

执行open_softirq(TIMER_SOFTIRQ,timer_softirq_action),将TIMER_SOFTIRQ[25]打开,并设置处理函数为timer_softirq_action()。timer_softirq_action()主要将当前pcpu的structtimer中的function执行。

21. init_idle_domain();

执行open_softirq(SCHEDULE_SOFTIRQ,schedule),将SCHEDULE_SOFTIRQ软中断打开并将schedule()调度函数给入;将所用调度器给入全局struct scheduler ops,然后执行cpu_schedule_up(),其中init_timer()将s_timer_fn()设入当前pcpu的struct schedule_data –> struct timer –>function中,s_timer_fn()会raise_softirq(SCHEDULE_SOFTIRQ)启动调度。最后init_idle_domain创建了一个空闲域。

22. 其他初始化操作

start_xen()随后对RCU进行了初始化、创建了内存管理相关的虚拟Domain、使能中断、对SMP的预初始化、调用uart的驱动接口初始化postirq、尝试启动其余pcpu。最终start_xen()创建了特权域Domain0,并退化成为idle_vcpu。

软中断softirq执行路线

1.  硬/软中断触发与执行

本质上,Xen的软硬中断触发执行过程为:硬件中断触发软件中断TIMER_SOFTIRQTIMER_SOFTIRQ负责处理所有的软定时器,软定时器们有的负责触发调度软中断有的作为vcpu的虚拟定时器源。

a)  系统启动时执行init_timer_interrupt();

b)  init_timer_interrupt()执行request_irq(),将硬件定时器处理函数指向timer_interrupt()[26]

c)  timer_interrupt()函数会raise_softirq(TIMER_SOFTIRQ),预计softirq将会在中断返回处执行。

d)  do_softirq()[27]调用__do_softirq(0)执行所有在softirq_handlers中的软中断函数。

e)  核心函数指针数组softirq_handlers,记录有软中断处理函数,其主要元素有TIMER_SOFTIRQ[28]

SCHEDULE_SOFTIRQ[29]

NEW_TLBFLUSH_CLOCK_PERIOD_SOFTIRQ

RCU_SOFTIRQ

TASKLET_SOFTIRQ

NR_COMMON_SOFTIRQS。

2.  软中断/定时器设置

a)  open_softirq()时会把处理函数设入softirq_handler函数指针数组。

b)  init_timer()会将软定时器放入per-cpu变量timers,并设置软定时器处理函数。

软定时器机制struct timers和struct timer

1.  管理timer的timers

struct timers是per-cpu变量timers的数据结构,其中有struct timer,分为struct timer **heap,struct *list,struct timer *running,struct list_head inactive。

a)  structtimer **heap是一个timer堆,大概按照超时顺序排列。如果发现所插入timer为堆内首个timer,则会软件产生一个TIMER_SOFTIRQ,堆满才用链表。

b)  structtimer list是一个timer链表,按照超时时间大小排列,在struct timer ** heap空间不够时,才会用链表。发现所插入timer是链表第一个元素时会软件产生TIMER_SOFTIRQ。

c)  structtimer running存放正在执行的timer,在timer的function被执行时会将其从timer堆或timer链表中移除,然后实际执行时被加入到running链表。

d)  structtimer inactive是timer被deactivate之后的存放之处。

2.  vcpu的timer

a)  periodic_timer

用于发生vcpu的虚拟定时器中断。

b)  singleshot_timer

c)  poll_timer

3.  调度触发的timer

per-cpu变量struct schedule_data中的s_timer软定时器是调度的触发定时器,当TIMER_SOFTIRQ触发此软定时器,软定时器处理函数s_timer_fn()将会触发SCHEDULE_SOFTIRQ软中断,发生调度。

4.  补充预算的timer-实时调度

structscheduler的sched_data元素指向实现调度器算法的私有数据结构,对于RT调度此数据为struct rt_private,rt_private中的repl_timer软定时器是RT调度用于补充预算的定时器,每次补充预算完成后,会设置软定时器的出发时间为下一个投入执行的vcpu的deadline。

5.  Domain的看门狗(默认每个域可有2个)

structdomain中的watchdog_timer[]软定时器保存有Domain的看门狗定时器,若被触发说明Domain没有喂狗,可能出错,会将Domain关闭。

6.  structvtimer的定时器

与Domain虚拟中断相关的软定时器,略。

总结

start_xen()启动到当前位置并未完成,Xen调度相关初始化主线已经分析清楚。CPU硬件中断[30]处理函数绑定[31]处理函数中会发起TIMER_SOFTIRQ软中断;系统软中断TIMER_SOFTIRQ绑定的处理函数[32]会执行所有当前pcpu挂载在timers中的软定时器的处理函数;调度器的软定时器处理函数是s_timer_fn()[33],会触发SCHEDULE_SOFTIRQ软中断; SCHEDULE_SOFTIRQ软中断的处理函数为schedule(),是系统调度入口。[34]

一次调度的处理

Xen调度入口函数位于文件”./xen/common/schedule.c”中的static voidschedule(void)。调度的触发方式会有多种,在Xen中确定已知的有定时触发、中断返回触发、任务阻塞触发[35]。所有触发的调度最终都会传导到schedule。

在schedule()

1.  switch( *tasklet_work )

tasklet_work[36]是当前pcputasklet work的状态标志,可以为TASKLET_enqueued、TASKLET_scheduled以及TASKLET_enqueued|TASKLET_scheduled,如果仅有TASKLET_scheduled置位,表示不需要处理,否则调度器会执行idle_vcpu,idle_vcpu会执行do_tasklet(),并调整标志位。do_tasklet()会调用do_tasklet_work(),在do_tasklet_work()中,tasklet的func会被执行;只有在tasklet链表中不再有元素时,do_tasklet()会清除TASKLET_enqueued标志位。在TASKLET_enqueued标志位被清除时schedule()会清除TASKLET_scheduled。

2.  stop_timer(&sd->s_timer);

sd是当前pcpu的schedule_data,其中包含锁、struct vcpu *curr[37]、void *sched_priv[38]、struct timer s_timer[39]、urgent的vcpu计数。各pcpu的struct timers数据[40]中有struct timer,分为struct timer **heap[41],struct *list[42],struct timer *running[43],struct list_head inactive[44],此处将struct timer->status设置为TIMER_STATUS_inactive,并加入timer所属pcpu下struct timers数据的inactive链表。[45]

3.  next_slice= sched->do_schedule(sched, now, tasklet_work_scheduled);

调用当前pcpu的调度器计算下一个要投入运行的任务,其中next_slice包含3个元素:structvcpu *task[46]、s_time_t time[47]、bool_t migrated[48];sched为调度器结构体,指向当前pcpu调度器指针;do_schedule()为调度器调度计算函数入口[49];now=NOW()是CPU时间;tasklet_work_scheduled是tasklet需要调度投入执行的标志[50]

4.  next =next_slice.task;

next是将会被投入运行的任务。

5.  sd->curr= next;

至此,schedule函数选好了投入运行的任务,记录到sd的curr[51]

6.  设置投运任务的运行时间

if (next_slice.time >= 0 ) set_timer(&sd->s_timer, now +next_slice.time);

next_slice.time只有在下一个任务使idle_vcpu的时候才会小于0;set_timer()将会给当前pcpu的调度定时器续时,时长决定于next_slice.time。

7.  省略

TRACE_3D()、TRACE_4D()会记录调度的切换,不予分析;当计算完发现即将投入运行的任务还是之前的任务,则会直接投入运行。

8.  vcpu_runstate_change();

修改被调度出局任务的runstate,runstate记录有vcpu在各状态停留时间,根据被调度出局的原因:阻塞、离线、可执行[52],是一个struct vcpu_runstate_info数据结构,包含int state[53]、uint64_t state_entry_time[54]、uint64_t time[4][55]

9.  prev->last_run_time= now;

记录被调度出局的vcpu的出局时间。

10. vcpu_runstate_change(next,RUNSTATE_running, now);

记录即将投运任务的runstate。

11. next->is_running= 1;

标志着vcpu正在运行中。

12. stop_timer(&prev->periodic_timer);

关闭调度出局的vcpu的periodic_timer,其处理函数为vcpu_periodic_timer_fn()[56],periodic_timer的作用是向vcpu定时发出虚拟中断信号[57];此处将之关闭即不再发出此虚拟中断。

13. if (next_slice.migrated ) sched_move_irqs(next);

对于即将投入运行的vcpu,如果是从别的cpu迁移过来的,则需要调整他的IRQ到当前的cpu上[58]

14. vcpu_periodic_timer_work(next);

即将投入运行的vcpu的periodc_timer的启动:首先检测当前是否到vcpu的周期,决定是否发出virq;然后检查并迁移periodic_timer到在当前pcpu名下[59];最后设置vcpu下一个virq发生点。

15. context_switch(prev,next);

进行了任务切换[60]

描述Xen的调度单位vcpu

structvcpu 分析

vcpu是Xen的基本调度单位,其数据结构structvcpu复杂[61],下面将分析其关键元素。

int

vcpu_id;

vcpu识别号

int            

processor;

vcpu运行的pcpu号

vcpu_info_t    

*vcpu_info;

NC

struct domain  

*domain;

指向vcpu所在的域

(即描述虚拟机的主数据结构)

s_time_t      

periodic_period;

时间计数,周期时长

s_time_t       

periodic_last_event;

时间计数,上次周期开始时间

struct timer    

periodic_timer;

周期定时器,给vcpu提供虚拟中断的

struct timer    

poll_timer;   

/* timeout for SCHEDOP_poll */

void           

*sched_priv;   

指向vcpu所处调度器的私有数据结构,与调度算法密切相关,由算法实现者定义,对于RT调度,此指针指向struc rt_vcpu

struct vcpu_runstate_info

runstate;

记录vcpu运行状态、进入此运行状态的时间,在各个运行状态累积时间。

uint64_t

last_run_time;

记录调度出去的时间

bool          

is_initialised;

/* Initialization completed for this VCPU?

bool           

is_running;

正在pcpu上运行的标志

unsigned long   

pause_flags;

vcpu被暂停标志

atomic_t        

pause_count;

被暂停计数

cpumask_var_t   

cpu_hard_affinity;

允许vcpu执行的pcpu位图

cpumask_var_t   

cpu_hard_affinity_tmp;

/* Used to change affinity temporarily. */

    cpumask_var_t   

cpu_hard_affinity_saved;

    /* Used to restore affinity across S3. */

    cpumask_var_t   

cpu_soft_affinity;

    /* Bitmask of CPUs on which this VCPU prefers to run. */

struct arch_vcpu

arch;

 

记录有ARM的各个寄存器值(含pc、sp)以及其他不认识的,在任务切换时大显身手

    cpumask_var_t   

vcpu_dirty_cpumask;

/* Bitmask of CPUs which are holding onto this VCPU's state. */

struct vcpu还有很多很多元素,以上元素占比<50%,其他元素与调度关系不大,省略。

structrt_vcpu分析

struct rt_vcpu是RT调度专有数据结构,如下为全部元素:

struct list_head

q_elem

作为一个链表元素,可能被放在RunQ链表、DeplQ链表或为空。

struct list_head

replq_elem

作为链表元素可能被放在ReplQ或为空

s_time_t

period

设定参数,vcpu的虚拟中断触发周期;

默认周期RTDS_DEFAULT_PERIOD为10毫秒

s_time_t

budget

设定参数,vcpu作为Xen中的任务,被调度时要设定的预算值,即允许持续执行的时间;

默认预算RTDS_DEFAULT_BUDGET为4毫秒

s_time_t

cur_budget

运行时参数,vcpu剩余预算值

s_time_t

last_start

运行时参数,vcpu开始执行时间

s_time_t

cur_deadline

运行时参数,vcpu的deadline

struct rt_dom

*rt_dom

NC

struct vcpu

*vcpu

指向所描述vcpu主体

unsigned

priority_level

vcpu的调度优先级

unsigned

flag

标志位

RTDS_scheduled表示此vcpu是否正在pcpu上执行;

RTDS_delayed_runq_add表示此vcpu被调度暂停执行时,将会被加入Runq还是DeplQ;

RTDS_depleted表示此vcpu是否还有预算;

RTDS_extratime置位时,预算耗尽将会自动补充

Xen向GuestOS伸出了魔爪

之所以这么写这个标题,是因为写到这里,我发现,这一部分的解析才刚刚露出冰山一角,这一部分是指Xen作为Hypervisor对操作GuestOS的支撑部分,我在想是不是要在这个文档里写这个问题;因为这。。。不属于调度了吧(还是属于?)?这已经属于GuestOS的调度基础支撑了。

RT调度的do_schedule()

Xen的RT调度入口函数是位于文件”./xen/common/sched_rt.c”中的static struct task_slice rt_schedule(conststruct scheduler *ops, s_time_t now,bool_t tasklet_work_scheduled)。主要处理:任务选择,预算结算,返回所选择的任务、将运行时间以及是否迁移自其他pcpu。本章将会分析rt_schedule()函数的实现。

1.  cpumask_clear_cpu(cpu,&prv->tickled);

从tickled中清楚当前pcpu位图,tickled置位是因为执行了runq_tickle(),runq_tickle()有点发现优先级排序不对了,会整理,然后就会产生一次软中断调度[62]

2.  burn_budget(ops,scurr, now);

将当前vcpu的预算结算掉(idle_vcpu除外),对于RT调度,系统定义了私有的数据结构struct rt_vcpu[63],其中的last_start元素[64]记录上次投入运行的时间,可以计算出当前vcpu已经执行的时长[65],在计算完后也要再次将last_start=now;cur_budget记录vcpu运行时预算[66],当cur_budget<=0,如果flags元素[67]中存在RTDS_exteratime标志[68],则提升其优先级、补充其预算[69],否则置位flags元素的RTDS_depleted标志[70]

3.  tasklet_work_scheduled[71]被置位时:snext = rt_vcpu(idle_vcpu[cpu]);

有tasklet需要正式调度过去执行[72],就不会发生runq_pick()[73]操作,即将要投入运行的任务将会是idle_vcpu,只需做一些记录即可,作为空闲vcpu会不停地执行tasklet。

4.  snext= runq_pick(ops, cpumask_of(cpu));

遍历可执行vcpu链表,综合vcpu所处Domain的有效pcpu位图[74]vcpu的硬亲和pcpu位图[75]实际pcpu位图[76],得到下一个运行的vcpu[77]。需要注意的是这里的选择vcpu并不考虑剩余预算的问题,如果没有预算系统将会失败,也就是说,RunQ中vcpu必有预算

5.  if (snext == NULL ) snext = rt_vcpu(idle_vcpu[cpu]);

如果没有合适的vcpu被选到,则执行idle_vcpu。

6.  查看是否需要执行之前的vcpu

if( !is_idle_vcpu(current) &&  

vcpu_runnable(current) &&

scurr->cur_budget > 0&&

is_idle_vcpu(snext->vcpu) ||compare_vcpu_priority(scurr, snext) > 0 ) )

snext = scurr

如果之前的vcpu,不是idle_vcpu、可执行[78]、预算存在并且新选的vcpu是idle_vcpu或比之前vcpu优先级低,则不会切换。

总结成人话就是:如果选完以后发现是空闲vcpu并且原先的vcpu不空闲,那么继续执行之前的vcpu;如果选出的vcpu优先级不如之前的高,并且之前的vcpu还有预算,那么还是执行之前的vcpu。这是RT调度的重要原则之一。

7.  if ( snext != scurr &&

!is_idle_vcpu(current) &&

vcpu_runnable(current) )

__set_bit(__RTDS_delayed_runq_add, &scurr->flags);

如果下一个要投入运行的vcpu不是当前vcpu,并且当前运行的vcpu也不是idle_vcpu,并且当前vcpu还是可执行;置位struct rt_vcpu->flags的__RTDS_delayed_runq_add[79]

8.  snext->last_start= now;

更新下一个投入执行的vcpu的投入时间

9.  if ( snext->vcpu->processor != cpu ){

snext->vcpu->processor = cpu;

ret.migrated = 1;

}

检查此vcpu是否迁移自非当前pcpu[80],对于产生迁移的需要更新vcpu->processor;ret是是函数返回值task_slice数据结构,置位其migrated元素,提示迁移发生[81]

10. ret.time= snext->cur_budget;

cur_budget存有当前vcpu将会被投入运行的时长,放入在返回值struct task_slice中time元素[82]

11. ret.task= snext->vcpu;

将选定的vcpu设置到返回值中。

RT调度的预算补充

当vcpu的预算消耗完,需要补充预算的vcpu们会被放到ReplQ链表,由软定时器repl_timer[83]触发的处理函数repl_timer_handler()将会为之补充预算。下面将分析repl_timer_handler()功能。

1.  遍历ReplQ链表,在遍历过程中,会不断给ReplQ中的vcpu元素补充预算、按周期延展deadline、将优先级置0,并加入到临时链表等待进一步处理;预算补充完后,检测如果vcpu还在RunQ/DeplQ,则会拔、插[84]回RunQ/DeplQ[85]。再次遍历过程中,如果遇到deadline未到的vcpu应截止遍历,仅处理已补充预算的vcpu。

2.  遍历上一步补充过预算的vcpu,如果发现vcpu正在运行,但优先级并不比RunQ上的下一个vcpu高[86],这是不合理的,触发runq_tickle()[87];如果vcpu不在执行,判断其RTDS_depleted标志位(判断完清除之)并确认其在RunQ/DeplQ,同样触发runq_tickle()。

RT调度的唤醒

vcpu任务的唤醒由rt_vcpu_wake()执行,对于以下vcpu状态:

1.  vcpu正在指定的pcpu执行,更改per-cpu状态vcpu_wake_running,唤醒结束。

2.  vcpu处于RunQ/DeplQ,更改per-cpu状态vcpu_wake_onrunq,唤醒结束。

唤醒vcpu可直接执行完毕

对于普通情况,检测vcpu处于可执行状态/不可执行状态,相应的更改per-cpu状态标志。如果vcpu的deadline已超时,更新其deadline、预算、last_start=now、优先级置0[88];否则无需操作。如果vcpu RTDS_scheduled[89]置位,则置位RTDS_delayed_runq_add,以便于在此vcpu经过rt_context_saved()时会将之加入RunQ/DeplQ;如果之前判断deadline超时,则此处需要将其在ReplQ上插拔一次,以防止其在ReplQ链表不存在或顺序问题,完成后唤醒即结束。对于vcpu RTDS_scheduled未置位,即vcpu未在pcpu上执行,则需要将其插入ReplQ、RunQ/DeplQ、然后用runq_tickle()检查一下是否该触发软中断[90]唤醒结束

RT调度的优先级

RT调度的优先级首先由struct rt_vcpu->priority_level决定,priority_level越小的vcpu优先级越高;在同优先级的情况下,会比较structrt_vcpu->cur_deadline,deadline越近的vcpu优先级越高。具体可查看函数compare_vcpu_priority()。

RT调度的数据结构

RT调度有3个链表[91],由所有的pcpu[92]共享一个是有序[93]的RunQ链表,链接有预算且可执行的vcpu;一个是无序DeplQ链表,链接没有预算等待补充预算之后就能跑的vcpu;最后是一个有序[94]的ReplQ链表,链接等待补充预算的vcpu

RunQ和DeplQ是互斥的两个链表,即一个vcpu不能同时在RunQ和DeplQ上;但一个vcpu应该同时在RunQ和ReplQ上,或同时在DeplQ和ReplQ上。

ReplQ只能在vcpu唤醒、初创的时候,插入RunQ/DeplQ(或is_running态)才会被插入ReplQ;当vcpu不再被调度时,需要离开ReplQ(也会离开RunQ/DeplQ)。

如果vcpu是由于进入睡眠[95]、被销毁[96]、迁移离开调度[97],则离开3大链表。

RT调度的deadline

RT调度的deadline位于rt_vcpu->cur_deadline,唯一可以补充cur_deadline的位置是rt_update_deadline(),而在普通运行阶段[98],唯有软定时器repl_tmer的处理函数repl_timer_handler()会调用rt_update_deadline()为vcpu更新deadline,软定时器repl_timer的触发时间总是按照RunQ即将投入运行的任务的cur_deadline设定。

进一步

经过上述分析可以全面的了解Xen调度的前因后果,RT调度的实现考量,之后还需要进一步分析以及目前Xen的调度在ARM平台存在的一些问题如下。

1.  总结分析可能触发调度的点。

2.  Xen对vcpu的切换过程,如寄存器保存、新寄存器值设入。

3.  Xen在已启动的pcpu中如何启动其他pcpu。

在vcpu_initialise()时,会有

   vcpu->arch.saved_context.sp = (register_t)v->arch.cpu_info;

   vcpu->arch.saved_context.pc = (register_t)continue_new_vcpu;

    是如何工作的。

4.  目前的RT调度,各CPU均采用同一任务链表,调度需要获取锁,是存在存在竞争、浪费的。

5.  Xen跨CPU调度仅根据一些固定设置,不是阻尼的方式防止,这很明显会引起不必要的cachemiss降低效率。

6.  Xen不能感知GuestOS的空闲、于是当vcpu执行idle线程时,Xen也不会有动作,是否可以修改GuestOS的idle线程。

7.  Xen对大小核并未考虑。

 

 

[1] Xen应该是继承自Linux的各pcpu变量定义:DECLARE_PER_CPU(unsignedint, cpu_id)是获得已被定义的per-cpu变量。一次引用声明,各pcpu分别有了cpuid变量,当代码所处pcpu执行this_cpu(cpuid),即可获得此pcpu的cpuid

[2] 具体代码为this_cpu(cpuid)=0,第一个percpu变量的使用实例。

[3] idle_vcpu是一个全局数组,每一个物理pcpu都会有一个idle_vcpu,当这个物理pcpu没有任务时,就会把它取出来执行。

[4] current是一个宏变量,通过this_vcpu(curr_vcpu)获得,即为本物理pcpu中执行的vcpu。

[5] Xen想要将所有的执行进程都作为vcpu来表述,可以理解为vcpu即为Xen的进程,vcpu是Xen微内核的关键概念,后续还会提到并解释。

[6] idle_vcpu是Xen中空闲进程的。

[7] 想象没有CPU对虚拟化的硬支持,Xen如何实现对GuestOS的管控:对于GuestOS下发的每一条计算机指令,Xen都需要将这条指令检查过之后再投入计算机执行,如同Java的虚拟机(效率更低),效率就像是笑话(CPU50%以上的时间在检查指令)。但如果不这样做,一旦被GuestOS获得CPU的使用权,GuestOS将会获得完整的硬件使用权,如果GuestOS将硬件调度定时器的处理函数指向自己的调度函数,Xen就会像BootLoader一样被扔到一边,之后的GuestOS也都成为废纸,所谓域间隔离也都只能是空谈。

[8] CP15 CR12下HVBAR是Hypervisor的Vector Base Address Register存储异常向量地址;注意aarch64,与aarch32一些特权寄存器使用方式有变化。

[9] early_fdt_map()调用的create_mappings()函数透露出Xen对内存管理的信息,但由于本文档重点分析Xen的调度机制,在此不予详述。

[10] Xen的状态可能为:SYS_STATE_early_boot、SYS_STATE_boot、SYS_STATE_smp_boot、SYS_STATE_active、SYS_STATE_suspend、SYS_STATE_resume等

[11] 其他还有IRQ_TYPE_NONE(默认的普通IRQ_TYPE)、IRQ_TYPE_EDGE_RISING(上升沿触发的中断)、IRQ_TYPE_EDGE_FALLING(下降沿触发的中断)、IRQ_TYPE_EDGE_BOTH(上升、下降沿均会触发的中断),其他还有IRQ_TYPE_LEVEL_HIGH、IRQ_TYPE_LEVEL_LOW、IRQ_TYPE_LEVEL_MASK、IRQ_TYPE_SENSE_MASK等类型或组合类型的中断。

[12] 此处Xen需要的初始化的接口少的可怜,init()估计是留给厂商将硬件启动,init_time()获得硬件提供的一个定时器来作为系统的时钟滴答(内核基于此感知时间,如触发调度,超时操作);specific_mapping()让厂商把外设的内存地址送到struct domain,后期启动Dom0时,Dom0能看到外设。另外Xen还需要厂商提供的reset()、poweroff(),估计是重启、关机操作。另外还可以有给出一个禁止pass-through的设备表告诉Xen。quirks()函数未知。arm32还会有smp_init()、cpu_up()函数。

[13] 对于启动了ACPI的设备,ACPI会接管初始化操作

[14] 这可能是Xen作为一个域的编号7FF2U,Dom0由此编号访问Hypervisor(如xl对Xen的操作?)。

[15] 启动时间的记录在boot_count,从P15 CR14读出。

[16] GIC是中断控制器,对于无ACPI平台,系统直接读DTB完成初始化,对于有ACPI节点,由ACPI提供的接口完成。

[17] Xen中tasklet.c源码中显示是由1992年Linus Torvalds的代码基础上修改而来,总体机制变化不大。

[18] Notifiter是内核的常用通信机制,主体为一个按优先级排列的链表,被插入当前pcpu下。

[19] scheduled_on>=0表示此tasklet应该放在某pcpu上执行,而scheduled_on的值即为pcpu的id,因此应被放到对应pcpu的softirq_tasklet_list或tasklet_list,如果为-1则不会特别处理。

[20] is_softirq将会只能在软中断处理。

[21] 加入softirq_tasklet_list的同时,指定pcpu的softirq_tasklet_list无内容,则为此CPU软产生一个TASKLET_SOFTIRQ软中断。

[22] 加入tasklet_list时若指定pcpu的tasklet_list无内容则为此CPU软产生一个SCHEDULE_SOFTIRQ。

[23] 在REF _Ref517287190 \h  \* MERGEFORMAT 一次调度的处理 08D0C9EA79F9BACE118C8200AA004BA90B02000000080000000E0000005F005200650066003500310037003200380037003100390030000000, REF_Ref517287151 \h  \* MERGEFORMAT next_slice = sched->do_schedule(sched, now,tasklet_work_scheduled); 08D0C9EA79F9BACE118C8200AA004BA90B02000000080000000E0000005F005200650066003500310037003200380037003100350031000000会用到此变量。

[24] timer_irq数组有四个成员TIMER_PHYS_SECURE_PPI、TIMER_PHYS_NONSECURE_PPI、TIMER_VIRT_PPI、TIMER_HYP_PPI,详见章节REF _Ref517685421 \h  \* MERGEFORMAT 软中断softirq执行路线 08D0C9EA79F9BACE118C8200AA004BA90B02000000080000000E0000005F005200650066003500310037003600380035003400320031000000

[25] TIMER_SOFTIRQ的处理函数)是所有软中断启动之源

[26] timer_irq数组记录有定时器中断资源,将与处理函数timer_interrupt()连接;timer_irq数组有四个成员及其处理函数:TIMER_PHYS_SECURE_PPI(可能是TZ相关)、TIMER_PHYS_NONSECURE_PPI <->timer_interrupt、   TIMER_VIRT_PPI <->timer_interrupt、TIMER_HYP_PPI<->timer_interrupt。

[27] 分析猜测do_softirq()

[28] 由硬件中断触发

[29] 由软定时器触发,软定时器由TIMER_SOFTIRQ触发;这就是调度的软中断。

[30] 详情见章节REF _Ref517291038 \h  \* MERGEFORMAT 在start_xen() 08D0C9EA79F9BACE118C8200AA004BA90B02000000080000000E0000005F00520065006600350031003700320039003100300033003800000012.init_IRQ()、14.preinit_xen_time()、15.init_xen_time()。

[31] 详情见章节REF _Ref517291038 \h  \* MERGEFORMAT 在start_xen() 08D0C9EA79F9BACE118C8200AA004BA90B02000000080000000E0000005F00520065006600350031003700320039003100300033003800000019. init_timer_interrupt()。

[32] 详情见章节REF _Ref517291038 \h  \* MERGEFORMAT 在start_xen() 08D0C9EA79F9BACE118C8200AA004BA90B02000000080000000E0000005F00520065006600350031003700320039003100300033003800000020.timer_init()。

[33] 详情见章节REF _Ref517291038 \h  \* MERGEFORMAT 在start_xen() 08D0C9EA79F9BACE118C8200AA004BA90B02000000080000000E0000005F00520065006600350031003700320039003100300033003800000021.init_idle_domain()。

[34] 根据Linux内核,软中断的处理会在中断退出时发生,可能写在汇编代码中,内核代码中找到的对do_softirq函数的调用,并不能确认有效。

[35]分析能触发调度的时机是非常重要的,但由于能力所限,短时间内不能枚举,此处仅列举分析过程中确定可以触发调度的时机。

[36] tasklet_work=&this_cpu(tasklet_work_to_do)是当前pcpu变量,由DECLARE_PER_CPU定义。

[37] 当前正在运行的任务vcpu数据结构指针。

[38] 这是调度器的私有数据结构入口。

[39] 调度定时器,软中断将会处理其内function函数。

[40] struct timers也是per-cpu变量,依旧是继承自Linux的各pcpu变量定义,struct timers并未被DECLARE_PER_CPU引用声明,由宏DEFINE_PER_CPU定义,DEFINE_PER_CPU是对per-cpu变量的真实定义,而DECLARE_PER_CPU是对per-cpu变量的引用声明。&this_cpu(a)访问本pcpu的per-cpu变量&per_cpu(a,cpu0)访问cpu0核的per-cpu变量。

[41]根据add_to_heap()和remove_from_heap()的分析,struct timer **heap是一个timer堆,大概按照超时顺序排列。如果发现所插入timer为堆内首个timer,则会软件产生一个TIMER_SOFTIRQ,堆满才用链表。

[42] struct timers中的struct timer list是一个timer链表,按照超时时间大小排列,在struct timer ** heap空间不够时,才会用链表。发现所插入timer是链表第一个元素时会软件产生TIMER_SOFTIRQ。

[43] struct timers中的struct timer running在timer的function被执行时(见20.中timer_softirq_action()),会从timer堆或timer链表中移除,然后实际执行时(execute_timer())被加入到running链表。

[44] struct timers中的struct timer inactive是timer被deactivate之后的存放之处。

[45] timers和timer有着完整的机制描述,可在章节 REF _Ref517280274 \h  \* MERGEFORMAT 软定时器机制struct timers和struct timer 08D0C9EA79F9BACE118C8200AA004BA90B02000000080000000E0000005F005200650066003500310037003200380030003200370034000000查看。

[46] vcpu是Xen调度的基本单位。

[47] 指示当前task_slice将会运行多久,主要来自调度器自定数据结构中xx_vcpu->cur_budget。

[48] 指示这个任务是否是从其他pcpu迁移到这里的。

[49] 下文分析了 REF _Ref517285580 \h  \* MERGEFORMAT RT调度的do_schedule() 08D0C9EA79F9BACE118C8200AA004BA90B02000000080000000E0000005F005200650066003500310037003200380035003500380030000000的实现。

[50] tasklet_work_scheduled的设置在本章 REF_Ref517287690 \r \h  \* MERGEFORMAT 1 08D0C9EA79F9BACE118C8200AA004BA90B02000000080000000E0000005F005200650066003500310037003200380037003600390030000000.处完成在RT调度中,tasklet_work_scheduled置位会使调度器在选择任务使选中idle_vcpu,idle_vcpu内有tasklet处理函数入口do_tasklet()。

[51] sd变量描述同本章 REF _Ref517288519 \r \h  \* MERGEFORMAT 2 08D0C9EA79F9BACE118C8200AA004BA90B02000000080000000E0000005F005200650066003500310037003200380038003500310039000000

[52] 可执行状态RUNSTATErunnable遇到tasklet占用、高优先级的vcpu等是会被调度出去的。

[53] state元素记录vcpu当前所处状态:RUNSTATE_running、RUNSTATE_runnable、RUNSTATE_blocked、RUNSTATE_offline,详情可见章节 REF _Ref517290931 \h  \* MERGEFORMAT 描述Xen的调度单位vcpu 08D0C9EA79F9BACE118C8200AA004BA90B02000000080000000E0000005F005200650066003500310037003200390030003900330031000000。

[54] vcpu进入当前状态的时间。

[55] state元素的4个状态,分别对应数组中的四个元素,记录有当前vcpu在四个状态的累计时间。

[56] init_timer(&v->periodic_timer,vcpu_periodic_timer_fn, v, v->processor);详见章节REF _Ref517280274 \h  \* MERGEFORMAT 软定时器机制structtimers和struct timer 08D0C9EA79F9BACE118C8200AA004BA90B02000000080000000E0000005F005200650066003500310037003200380030003200370034000000。

[57] 通过evtchn_port_set_pending()向vcpu发送了一个中断信号,章节’ REF _Ref517344185 \h  \* MERGEFORMAT Xen向GuestOS伸出了魔爪 08D0C9EA79F9BACE118C8200AA004BA90B02000000080000000E0000005F005200650066003500310037003300340034003100380035000000可能有。

[58] 这又是一个大活儿啊,另外还涉及到Xen对中断机制的操作,同样去章节’ REF _Ref517344185 \h  \* MERGEFORMAT Xen向GuestOS伸出了魔爪 08D0C9EA79F9BACE118C8200AA004BA90B02000000080000000E0000005F005200650066003500310037003300340034003100380035000000可能有。

[59] migrate_timer()完成,详见章节 REF_Ref517280274 \h  \* MERGEFORMAT 软定时器机制structtimers和struct timer 08D0C9EA79F9BACE118C8200AA004BA90B02000000080000000E0000005F005200650066003500310037003200380030003200370034000000(不一定有)。

[60] context_switch()需要做很多很多事,不过功能却只有一个,任务切换,于是先不分析细节。

[61] 还好我已经搞明白了不少。

[62] 查找到3处使用runq_tickle()的位置,分别是vcpu刚醒、上下文切换时刚保存完当前vcpu现场,调度完

[63] 详见章节REF _Ref517353329 \h  \* MERGEFORMAT RT调度的私有数据结构 08D0C9EA79F9BACE118C8200AA004BA90B02000000080000000E0000005F005200650066003500310037003300350033003300320039000000。

[64] 下文中REF _Ref517362375 \h  \* MERGEFORMAT snext->last_start = now; 08D0C9EA79F9BACE118C8200AA004BA90B02000000080000000E0000005F005200650066003500310037003300360032003300370035000000在下一个vcpu投入运行前更新了此元素。

[65] 在注释中看到,这个时长计算在嵌入式虚拟化中似乎会出现负数的错误,并未弄清如何产生。

[66] 在此处RT调度的预算只可能被减少,另外有一个timer会调用repl_timer_handler()来为失去预算的vcpu补充预算。详见章节REF _Ref517354017 \h  \* MERGEFORMAT RT调度的预算补充 08D0C9EA79F9BACE118C8200AA004BA90B02000000080000000E0000005F005200650066003500310037003300350034003000310037000000。

[67] flags同样是struct rt_vcpu的元素,定义有详尽的标志位,详见章节REF _Ref517353329 \h  \* MERGEFORMAT RT调度的私有数据结构 08D0C9EA79F9BACE118C8200AA004BA90B02000000080000000E0000005F005200650066003500310037003300350033003300320039000000。

[68] 根据注释,RTDS_extratime标志是想让进程有多x轮的预算,以至于在没有优先级比他高的vcpu,他就可以一直跑?因为默认sched_init_vcpu()创建vcpu时都会alloc_vdata(),alloc_vdata()是scheduler的接口,在RT的实现都给加了此标志。只有在hypercall的do_domctl() XEN_DOMCTL_SCHEDOP_putvcpuinfo操作是没有XEN_DOMCTL_SCHEDRT_extra标志时,才会被清楚此标志位。

[69] 我很怀疑这种操作的合理性,

[70] 表示vcpu预算耗尽,在之后的处理中将会被加入到预算耗尽的vcpu链表,等待补充预算。

[71] tasklet需要处理标志,详见REF _Ref517358187 \h  \* MERGEFORMAT 在schedule() 08D0C9EA79F9BACE118C8200AA004BA90B02000000080000000E0000005F005200650066003500310037003300350038003100380037000000的 REF_Ref517287690 \h  \* MERGEFORMAT switch ( *tasklet_work ) 08D0C9EA79F9BACE118C8200AA004BA90B02000000080000000E0000005F005200650066003500310037003200380037003600390030000000。

[72] 此种情况发生在tasklet_list的任务在tasklet触发后,发现需要此pcpu执行,即当前pcpu的tasklet_work_to_do被置位了,于是在调度时遇到,则需要执行。详见章节REF _Ref517291038 \h  \* MERGEFORMAT 在start_xen() 08D0C9EA79F9BACE118C8200AA004BA90B02000000080000000E0000005F005200650066003500310037003200390031003000330038000000的 REF_Ref517357878 \h  \* MERGEFORMAT tasklet_subsys_init(); 08D0C9EA79F9BACE118C8200AA004BA90B02000000080000000E0000005F005200650066003500310037003300350037003800370038000000

[73] 算法选择可投入执行的vcpu

[74] vcpu所在struct domain中有struct cpupool,cpupool内cpu_valid用位图表示可用pcpu们。

[75] vcpu中有cpu_hard_affinity位图,表示vcpu仅可在哪几个pcpu上执行。

[76] smp_processor_id()可获得。

[77] 找到的第一个vcpu即为投入运行的vcpu,于是此链表顺序很重要,包含调度原则

[78] 没有暂停标志和暂停计数,并且其所在域也没有

[79] 在rt_context_saved()这个vcpu会被加入到RunQ,rt_context_saved()是在启动投运vcpu前执行的context_saved()里调用。

[80] 跨pcpu的任务迁移必然会引起大量cache miss,降低效率,因此原则上跨pcpu的调度应该有一定阻尼,很明显Xen的RT没有做。

[81]章节 REF_Ref517358187 \h  \* MERGEFORMAT 在schedule() 08D0C9EA79F9BACE118C8200AA004BA90B02000000080000000E0000005F005200650066003500310037003300350038003100380037000000中 REF_Ref517287151 \h  \* MERGEFORMAT next_slice = sched->do_schedule(sched, now,tasklet_work_scheduled); 08D0C9EA79F9BACE118C8200AA004BA90B02000000080000000E0000005F005200650066003500310037003200380037003100350031000000也有介绍, REF_Ref517362153 \h  \* MERGEFORMAT if ( next_slice.migrated ) sched_move_irqs(next); 08D0C9EA79F9BACE118C8200AA004BA90B02000000080000000E0000005F005200650066003500310037003300360032003100350033000000同样根据此处判断结果。

[82] 章节REF _Ref517358187 \h  \* MERGEFORMAT 在schedule() 08D0C9EA79F9BACE118C8200AA004BA90B02000000080000000E0000005F005200650066003500310037003300350038003100380037000000中 REF_Ref517363192 \h  \* MERGEFORMAT 设置投运任务的运行时间 08D0C9EA79F9BACE118C8200AA004BA90B02000000080000000E0000005F005200650066003500310037003300360033003100390032000000时使用此元素。

[83] 详见章节REF _Ref517280274 \h  \* MERGEFORMAT 软定时器机制structtimers和struct timer 08D0C9EA79F9BACE118C8200AA004BA90B02000000080000000E0000005F005200650066003500310037003200380030003200370034000000。

[84] 先从RunQ/DeplQ链表取出,再插入,可以重新决定vcpu进RunQ/DeplQ,如果进入RunQ还可以重新排列其位置。

[85] 此处预算刚补充完毕,应该会插回RunQ。

[86] 因为Xen的RT调度所有pcpu共用待RunQ链表,经过重新充能先后时可能出现低优先级被先执行的情况的。

[87] runq_tickle()会检查指定scheduler的新加vcpu是否有机会调度,如果有,则触发调度软中断。

[88] rt_update_deadline()操作。

[89] 标志vcpu在pcpu上执行。

[90] 如果被唤醒vcpu优先级高的话。

[91] 每个RT调度的scheduler实体有3个链表,多个scheduler(RT/Credit)共存是可能的。

[92] 源文件中存在注释表述的是CPU pool,用pcpu是为了不用解释CPU pool。

[93] 排列顺序是优先级高在前,同优先级的deadline紧急的靠前。

[94] 排列顺序同样是优先级高在前,同优先级的deadline紧急的靠前与RunQ用同样的插入函数和优先级比较方式。

[95] rt_vcpu_sleep()是调度器接口sleep的RT实现

[96] rt_vcpu_remove()是调度器接口remove_vcpu的RT实现,在销毁vcpu(sched_destroy_vcpu())、迁移domain(sched_move_domain())被使用。

[97] rt_context_saved()是调度器context_saved的RT实现,

[98] vcpu初始化、vcpu唤醒也会执行rt_update_deadline()操作。

 

发布了24 篇原创文章 · 获赞 3 · 访问量 2343

猜你喜欢

转载自blog.csdn.net/ytfy339784578/article/details/103946311
XEN
rt