Qemu基础 - Timer和事件循环
A deep dive into QEMU: a Brief History of Time | QEMU internals (airbus-seclab.github.io)
QemuTimer
先来看看QemuTimer结构体
expire_time字面意思是过期时间,实际是触发事件,这个值为-1就永远不会触发,
timer_list,是按类型划分的链表
cb是timer触发时的回调函数
timer_new
Qemu中新建Timer用的Timer_new函数,ms是其包装,指定单位是毫秒
QEMUClockType有4种类型的时钟,最常用的是Clock_Vitual,反映的是纯纯的虚拟机上的时间,因为虚拟机运行在物理机上,而物理机上由于存在调度,所以物理机上的时间是大于虚拟机的
TIMER_NEW_ms的第二个参数是回调函数,第三个参数是传给回调函数的参数
接着深入创建一个Timer对象,timer_new会根据type选择对应的timer链表
最终这里可以看到expire_time被初始化为-1,因为这里只是初始化一个timer,还没有设置什么时候触发
timer_mod
要想开启TImer,需要用timer_mod函数设置expire_time
Timer-mod的原型
关于Timerlist的初始化在后面,这里只需要知道,timer_list的active_timers挂着所有的活跃的定时器
并且Active_timers里面的元素是按时间顺序排列的,最先到期的元素排在最前面
根据这个知识,我们再来看timer_mod_ns_locked
For循环就是寻找timer的插入位置,timer-expired_ns时expired timer比较,当前遍历到的定时器的expiredtimer 大于要插入的定时器的expired-time时就返回false,导致break出for循环
timer_mod_ns_locked最后返回会判断active_timers的第一个timer是否改变,如果改变就返回false,标志active_timers列表中最近的到期时间被更新了
总结一下,timer_new的时候定时器根本就没放到active_timers列表,到timer_mod时才上列表,所以只有timer_mod设置之后才会触发
Timer_del
Timer_del_locked是把时钟从队列中删除
时钟列表初始化
在qemu_init_main_loop里调用init-clocks
初始化时钟
MAX就是4,对应4个类型的时钟
由于虚拟机还没启动,所以VIRTUAL类型是禁用的
根据时钟type新建一个timerlist插入到main_loop_tlg.tl里面
TimerList结构如下
Timerlist也有对应的回调函数和回调参数,这里也把timer_list插入到了clock的timerlist里面
所有定时器列表不仅可以从main-loop_Tlg里面找,也可以从clock里面找
获取时钟时间
获取时钟上的时间用的是qemu-clock-get-ns
realtime就是开机后的时间,调用物理机的函数获得的
Virtual就是从vcpu处获得时间,除以进率就行
Qemu事件循环机制
qemu的事件循环是基于glib的,然后qemu利用g_source_attach添加了一个新的事件源AioContext
glib使用一个GMainLoop表示一个事件循环。每个GMainLoop都有一个主上下文GMainContext
g_main_loop_new创建一个GMainLoop对象
1 | GMainLoop * |
如果不提供context,就会用默认的context,同时增加对context的引用
g_new0就是封装的malloc
然后重点关注下loop run
这里的while是loop的主循环,其以is_running为循环条件,所以quit就是把其设置成false
Main_loop总共分为四个步骤prepare query check dispatch
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例子
g_main_loop_new新建一个事件循环
定义一下echo 事件源的相关回调函数
g_source_new是新建事件源,关键在于g_source_add_poll是把这个事件源对应的fd跟事件源挂钩,
Set_callback 是给source挂钩上callback函数
事件源只是glib做的一个包装,其本体还是fd,GSource结构如下
g_source_attach把事件源挂钩到context上,context是事件循环的上下文,挂到context里,才能在循环中检测事件源
而对事件源的检测实际就是将其fd加到poll里,分析下着这个例子中loop经过的四个阶段
prepare不需要做什么
check是在query之后了,此时已经poll完了
dispatch调用回调函数
echo函数就是从channel中读出信息然后打印出来
QemuBH
bh是下半部
bh给qemu内部其他模块提供一个异步延迟调用的功能,类似于Windows上的APC?
其结构体如下
1 | struct QEMUBH { |
这里是老版本的QEMUBH,新建bh需要三个参数,挂到哪个context上,bh回调函数以及参数opaque
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时把他删了
禁用bh就是设置
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)