Qemu基础 - Timer和事件循环

Qemu基础 - Timer和事件循环

A deep dive into QEMU: a Brief History of Time | QEMU internals (airbus-seclab.github.io)

QemuTimer

先来看看QemuTimer结构体

image-20220419100028744

expire_time字面意思是过期时间,实际是触发事件,这个值为-1就永远不会触发,

timer_list,是按类型划分的链表

cb是timer触发时的回调函数

timer_new

image-20220419094931677

Qemu中新建Timer用的Timer_new函数,ms是其包装,指定单位是毫秒

QEMUClockType有4种类型的时钟,最常用的是Clock_Vitual,反映的是纯纯的虚拟机上的时间,因为虚拟机运行在物理机上,而物理机上由于存在调度,所以物理机上的时间是大于虚拟机的

image-20220419095325980

TIMER_NEW_ms的第二个参数是回调函数,第三个参数是传给回调函数的参数

image-20220419095624200

接着深入创建一个Timer对象,timer_new会根据type选择对应的timer链表

image-20220419095940390

image-20220419095951587

最终这里可以看到expire_time被初始化为-1,因为这里只是初始化一个timer,还没有设置什么时候触发

timer_mod

要想开启TImer,需要用timer_mod函数设置expire_time

image-20220419101255858

Timer-mod的原型

image-20220419101310499

关于Timerlist的初始化在后面,这里只需要知道,timer_list的active_timers挂着所有的活跃的定时器

并且Active_timers里面的元素是按时间顺序排列的,最先到期的元素排在最前面

根据这个知识,我们再来看timer_mod_ns_lockedimage-20220419174640764

For循环就是寻找timer的插入位置,timer-expired_ns时expired timer比较,当前遍历到的定时器的expiredtimer 大于要插入的定时器的expired-time时就返回false,导致break出for循环

image-20220419174713604

timer_mod_ns_locked最后返回会判断active_timers的第一个timer是否改变,如果改变就返回false,标志active_timers列表中最近的到期时间被更新了

总结一下,timer_new的时候定时器根本就没放到active_timers列表,到timer_mod时才上列表,所以只有timer_mod设置之后才会触发

Timer_del

Timer_del_locked是把时钟从队列中删除

image-20220419101831299

时钟列表初始化

在qemu_init_main_loop里调用init-clocks

qemu_init_main_loop  int  ( Er ror  int ret;  GSource •src;  *local error  Er ror  init _ clocks();  ret =  if (ret) {  return ret;  = NULL;

初始化时钟

. init clocks  void  (void)  QEMUC10CkType type;  for (type = type <  #ifdef  #endif  type++) {

MAX就是4,对应4个类型的时钟

. qemu_clock_init •  static void  (QEMUC10ckType gypg)  QEMUC10ck •clock =  Assert that the clock of type TYPE has not been initialized yet.  tl[type] NULL);  clock->type = type;  clock->enabled = (type  clock-nast —  - INT64_MIN;  QLIST_INIT >timerlists);  notifier_li st_init ) ;  tl[type] =  ? false  true);  NULL, NULL);

由于虚拟机还没启动,所以VIRTUAL类型是禁用的

根据时钟type新建一个timerlist插入到main_loop_tlg.tl里面

QEMUTimerList •  imerlist ne  (QEMUC10ckType  QEbIUTimerListN0tifyC3  void  * opaque )  QEMlJTimerList  QEMuC10Ck *clock =  timer_list =  true) ;  timer list->clock clock;  cb;  opaque;  qemu_mutex_init ('timer_l ist - >acti ve_timer s_lock) ;  timer_list,  return timer list;  list);

TimerList结构如下

A QEMUTimerList is a list of timers attached to a clock. More  than one 9EMUTimerList can be attached to each clock, for instance  used by different AioContexts / threads. Each clock also has  a list of the QEMUTimerLists associated with it, in order that  reenabling the clock can call all the notifiers.  Struct  QEMUC10ck •clock;  QemuMutex active_timers_lock;  QEMUTimer •active_timers;  list;  QEMUTimerListNotifyC3  void *notify_opaque;  lightweight method to mark the end of timer-list's running  QemuEvent timers_done_ev;

Timerlist也有对应的回调函数和回调参数,这里也把timer_list插入到了clock的timerlist里面

所有定时器列表不仅可以从main-loop_Tlg里面找,也可以从clock里面找

获取时钟时间

获取时钟上的时间用的是qemu-clock-get-ns

image-20220419173727420

realtime就是开机后的时间,调用物理机的函数获得的

static  #ifdef  if  #endif  *end if  get _ clock  inline int64 t  (void)  CLOCK MONOTONIC  (use_rt_cLock) {  struct timespec ts;  return ts.tv sec * laeeaeeaeeLL + ts.tv nsec;  else  XXX: using gettimeofday leads to problems if the date  changes, so it should be avoided. s/  return get_clock_realtime();

Virtual就是从vcpu处获得时间,除以进率就行

static inline int64_t  return qemu_clock_get_ns(type) / SCALE  mS(QEMUC10CkType gype)  us ;

Qemu事件循环机制

qemu的事件循环是基于glib的,然后qemu利用g_source_attach添加了一个新的事件源AioContext

glib使用一个GMainLoop表示一个事件循环。每个GMainLoop都有一个主上下文GMainContext

image-20220419103626787

g_main_loop_new创建一个GMainLoop对象

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
GMainLoop *
g_main_loop_new (GMainContext *context,
gboolean is_running)
{
GMainLoop *loop;

if (!context)
context = g_main_context_default();

g_main_context_ref (context);

loop = g_new0 (GMainLoop, 1);
loop->context = context;
loop->is_running = is_running != FALSE;
loop->ref_count = 1;

TRACE (GLIB_MAIN_LOOP_NEW (loop, context));

return loop;
}

如果不提供context,就会用默认的context,同时增加对context的引用

g_new0就是封装的malloc

image-20220419103747141

然后重点关注下loop run

这里的while是loop的主循环,其以is_running为循环条件,所以quit就是把其设置成false

image-20220419104753429

image-20220419104804305

Main_loop总共分为四个步骤prepare query check dispatch

image-20220419104839791

glib的事件循环核心是poll,poll涉及到IO模式,poll相关知识可以看下文

Linux IO模式及 select、poll、epoll详解 - SegmentFault 思否

在linux中,默认情况下所有的socket都是blocking的,当用户进程调用了recvfrom之后,kernel就开始了IO的第一个阶段

准备数据,即等待数据到来,等到数据到达之后(拷贝到内核的缓冲区中),再进入第二阶段

内核将数据再拷贝到用户进程中,此时readfrom才会返回

这种默认的socket IO模式就是阻塞模式,进程在调用recv之后就会卡住等待数据返回

socket还可以被设置成非阻塞模式,这样recv之后如果数据还没准备好,就会直接返回error

因此用户进程需要不断地轮询

IO多路复用,select和epoll,是指单个process可以同时处理多个网络连接的IO,原理是select poll epoll这个function会不断的轮询所负责的所有的socket,当某个socket有数据到达了,就通知用户进程

当用户进程调用了select,那么整个进程会被block,而同时,kernel会“监视”所有select负责的socket,当任何一个socket中的数据准备好了,select就会返回。这个时候用户进程再调用read操作,将数据从kernel拷贝到用户进程。

这里非阻塞socket一样是同步IO,随着其准备数据阶段完成之后,还需要拷贝数据到用户空间,这一切仍然是要等待的

异步IO是指,调用完recv的那一刻之后,进程就不再管了,

poll的标准代码看这篇博客

IO多路复用之poll总结 - Rabbit_Dale - 博客园 (cnblogs.com)

再回头看loop的四个阶段,核心就是poll

Prepare 阶段:通过调用事件对应的prepare回调函数,做一些准备工作,如果事件已经准备好进行监听了,就返回True

Query 是获得实际需要调用的文件fd

check是 当query获得了需要监听的fd之后,就会调用poll对fd进行监听,当poll返回时,结果会传递给主循环

dispatch时通过g_main_context_check将poll的结果传递给主循环

分析一个echo例子

image-20220419105253912

g_main_loop_new新建一个事件循环

定义一下echo 事件源的相关回调函数

image-20220419105313967

g_source_new是新建事件源,关键在于g_source_add_poll是把这个事件源对应的fd跟事件源挂钩,

Set_callback 是给source挂钩上callback函数

事件源只是glib做的一个包装,其本体还是fd,GSource结构如下

image-20220419105348527

image-20220419105808623

g_source_attach把事件源挂钩到context上,context是事件循环的上下文,挂到context里,才能在循环中检测事件源

而对事件源的检测实际就是将其fd加到poll里,分析下着这个例子中loop经过的四个阶段

prepare不需要做什么

image-20220419110056741

check是在query之后了,此时已经poll完了

image-20220419110112866

dispatch调用回调函数

image-20220419111151440

echo函数就是从channel中读出信息然后打印出来

image-20220419111208813

QemuBH

bh是下半部

bh给qemu内部其他模块提供一个异步延迟调用的功能,类似于Windows上的APC?

其结构体如下

1
2
3
4
5
6
7
8
9
struct QEMUBH {
AioContext *ctx; // 下半部所在的context
QEMUBHFunc *cb; // 下半部要执行的函数
void *opaque; // 函数参数
QEMUBH *next; // 下一个要执行的下半部
bool scheduled; // 使能bh,是否被调度,true:下一次dispatch会触发cb; false:下一次dispatch不会触发cb
bool idle;
bool deleted; // 标记是否将bh删除
};

这里是老版本的QEMUBH,新建bh需要三个参数,挂到哪个context上,bh回调函数以及参数opaque

image-20220419171936153

bh被挂载到context之后并不会立即执行,只有当事件循环的poll中有fd准备好后才会触发到bh,

由于fd一直没准备好,bh就会被一直卡着不执行,所以qemu留了对bh的调度接口,用于通知事件循环调度bh,这个主动调度接口是用event_notifier,,将EventNotifier中的rfd设置为事件循环要监听的fd,然后主动通知只要向EventNotiifer中的rfd写入内容,事件循环的poll就会有响应了,然后就能触发bh

delete是既设置schedule为0,让其不被调度,又设置delete为1,让下次dispatch时把他删了

image-20220419172048617

禁用bh就是设置

image-20220419172106730

Refs

(24条消息) qemu2 时钟系统分析_TangGeeA的博客-CSDN博客_qemu timer

Linux IO模式及 select、poll、epoll详解 - SegmentFault 思否

IO多路复用之poll总结 - Rabbit_Dale - 博客园 (cnblogs.com)

Stefan Hajnoczi: QEMU Internals: Overall architecture and threading model (vmsplice.net)

(24条消息) 深入理解qemu事件循环 —— 基本框架_享乐主的博客-CSDN博客_qemu事件循环机制

(24条消息) 深入理解qemu事件循环 ——下半部(bottom half)_享乐主的博客-CSDN博客