文章目錄
- 1.簡介
- 2.添加外設
- 3.編寫喂狗程式測試效果
- 4.分析qemu代碼
-
- 4.1.開啟i6300esb調試開關
- 4.2.裝置實作i6300esb_realize
- 4.3.裝置重新開機函數i6300esb_reset
- 4.4.讀寫IO端口寄存器
- 4.5.讀寫IO記憶體寄存器
- 4.6.逾時機制
-
- 4.6.1.逾時處理函數
- 4.6.2.如何判斷逾時
- reference
1.簡介
本文介紹qemu是如何模拟和使用intel 6300esb晶片組的watchdog功能的,watchdog的基本概念,可以參考1,本文不涉及詳細介紹如何使用centos或者ubuntu等發行版自帶的喂狗程式,如有相關操作,隻是為了示範如何觸發qemu的一些相關函數的調用。本文用的代碼版本為開源的qemu2.8.0。
2.添加外設
libvirt中添加
...
<devices>
<watchdog model='i6300esb'/>
</devices>
...
或者直接在qemu指令行中添加
-device i6300esb,id=watchdog0,bus=pci.0,addr=0x6 -watchdog-action reset
其中-watchdog-action是設定watchdog timeout時間過後的行為的,此處拿reset做實驗,其他選項可以自行查閱qemu指令行或者libvirt xml2
添加成功之後可以在guest OS中看到外設
-device i6300esb,id=watchdog0,bus=pci.0,addr=0x6 -watchdog-action reset
3.編寫喂狗程式測試效果
原始代碼引用自1,稍微做了點修改
#include <linux/watchdog.h>
#include <string.h>
#include <stdio.h>
#include <fcntl.h>
#define WDT_DEVICE_FILE "/dev/watchdog"
int main(void)
{
int g_watchdog_fd = -1;
int timeout = 0;
int timeout_reset = 120;
int ret = 1;
int sleep_time = 10;
//開啟watchdog
g_watchdog_fd = open(WDT_DEVICE_FILE, O_RDWR);
if (g_watchdog_fd == -1)
{
printf("Error in file open WDT device file(%s)...\n", WDT_DEVICE_FILE);
return 0;
}
//擷取watchdog的逾時時間(heartbeat)
ioctl(g_watchdog_fd, WDIOC_GETTIMEOUT, &timeout);
printf("default timeout %d sec.\n", timeout);
//設定watchdog的逾時時間(heartbeat)
ioctl(g_watchdog_fd, WDIOC_SETTIMEOUT, &timeout_reset);
printf("We reset timeout as %d sec.\n", timeout_reset);
//喂狗
while(1){
//喂狗
ret = ioctl(g_watchdog_fd, WDIOC_KEEPALIVE, 0);
//喂狗也通過寫檔案的方式,向/dev/watchdog寫入字元或者數字等
// static unsigned char food = 0;
//write(g_watchdog_fd, &food, 1);
if (ret != 0) {
printf("Feed watchdog failed. \n");
close(g_watchdog_fd);
return -1;
} else {
printf("Feed watchdog every %d seconds.\n", sleep_time);
}
//feed_watchdog_time是喂狗的時間間隔,要小于watchdog的逾時時間
sleep(10);
}
//關閉watchdog
write(g_watchdog_fd, "V", 1);
//以下方式實測并不能關閉watchdog
//ioctl(g_watchdog_fd, WDIOC_SETOPTIONS, WDIOS_DISABLECARD)
close(g_watchdog_fd);
}
4.分析qemu代碼
4.1.開啟i6300esb調試開關
\* hw/watchdog/wdt_i6300esb.c *\
#define I6300ESB_DEBUG 1
然後重新編譯安裝qemu,列印的内容分三個階段,剛啟動qemu,就可以看到如下列印
i6300esb: i6300esb_realize: I6300State = 0x55e80b28d670
i6300esb: i6300esb_reset: I6300State = 0x55e80b28d670
i6300esb: i6300esb_disable_timer: timer disabled
以上列印,是由于qemu初始化虛拟外設i6300esb的時候調用的函數産生的,然後輸入c啟動guest os,會進一步的産生列印
(qemu) c
/home/qemu-2.8.0/cpus.c resume_all_vcpus 1372
/home/qemu-2.8.0/cpus.c resume_all_vcpus 1372
/home/qemu-2.8.0/cpus.c resume_all_vcpus 1372
/home/qemu-2.8.0/cpus.c resume_all_vcpus 1372
(qemu)
i6300esb: i6300esb_config_read: addr = 0, len = 2//Vendor ID 8086h Read Only
i6300esb: i6300esb_config_read: addr = a, len = 2//Sub Class Code Register (SCC) 80h Read Only
i6300esb: i6300esb_config_read: addr = e, len = 1//Header Type Register (HEDT) 00h Read Only
i6300esb: i6300esb_config_read: addr = 0, len = 2//Vendor ID 8086h Read Only
i6300esb: i6300esb_config_read: addr = a, len = 2
i6300esb: i6300esb_config_read: addr = e, len = 1
i6300esb: i6300esb_config_read: addr = 0, len = 2
i6300esb: i6300esb_config_read: addr = 0, len = 4
i6300esb: i6300esb_config_read: addr = 8, len = 4//Revision ID Register (RID) See NOTE: Read Only
......
i6300esb: i6300esb_config_write: addr = 60, data = 3, len = 2//WDT Configuration 00h Read/Write
i6300esb: i6300esb_config_read: addr = 68, len = 1//WDT Lock Register 00h Read/Write
i6300esb: i6300esb_config_write: addr = 68, data = 0, len = 1
i6300esb: i6300esb_disable_timer: timer disabled
i6300esb: i6300esb_mem_writew: addr = c, val = 80
i6300esb: i6300esb_mem_writew: addr = c, val = 86
i6300esb: i6300esb_mem_readw: addr = c
i6300esb: i6300esb_mem_writew: addr = c, val = 80
i6300esb: i6300esb_mem_writew: addr = c, val = 86
i6300esb: i6300esb_mem_writew: addr = c, val = 300//0000 0011 0000 0000//第8位是調用重置定時器函數,第九位是重置reboot flag
//這裡其實調用了i6300esb_restart_timer,但是d->enabled為0,是以直接傳回了,沒有列印
i6300esb: i6300esb_mem_writew: addr = c, val = 80
i6300esb: i6300esb_mem_writew: addr = c, val = 86
i6300esb: i6300esb_mem_writel: addr = 0, val = 3c00//0011 1100 0000 0000//第12位也是重置reboot flag
i6300esb: i6300esb_mem_writew: addr = c, val = 80
i6300esb: i6300esb_mem_writew: addr = c, val = 86
i6300esb: i6300esb_mem_writel: addr = 4, val = 3c00//0011 1100 0000 0000
i6300esb: i6300esb_mem_writew: addr = c, val = 80
i6300esb: i6300esb_mem_writew: addr = c, val = 86
i6300esb: i6300esb_mem_writew: addr = c, val = 100//0000 0001 0000 0000,以後正常工作是寫這個數字,上面的應該都是測試用的,出現100之後,這個函數就不列印了
i6300esb: i6300esb_config_read: addr = 0, len = 4
i6300esb: i6300esb_config_read: addr = 4, len = 4
....
這些列印是由于guest OS的核心在初始化的時候,産生了IO exit或者MMIO exit,由kvm傳回到qemu,進行io讀寫的模拟産生的。在guest os中執行喂狗程式,qemu産生的列印如下
6300esb: i6300esb_config_write: addr = 68, data = 2, len = 1
i6300esb: i6300esb_restart_timer: stage 1, timeout 96000
i6300esb: i6300esb_mem_writew: addr = c, val = 80
i6300esb: i6300esb_mem_writew: addr = c, val = 86
i6300esb: i6300esb_mem_writel: addr = 0, val = 7800, stage = 2
i6300esb: i6300esb_mem_writew: addr = c, val = 80
i6300esb: i6300esb_mem_writew: addr = c, val = 86
i6300esb: i6300esb_mem_writel: addr = 4, val = 7800, stage = 2
i6300esb: i6300esb_mem_writew: addr = c, val = 80
i6300esb: i6300esb_mem_writew: addr = c, val = 86
i6300esb: i6300esb_mem_writew: addr = c, val = 100
i6300esb: i6300esb_restart_timer: stage 1, timeout 96000
i6300esb: i6300esb_mem_writew: addr = c, val = 80
i6300esb: i6300esb_mem_writew: addr = c, val = 86
i6300esb: i6300esb_mem_writew: addr = c, val = 100
i6300esb: i6300esb_restart_timer: stage 1, timeout 96000
i6300esb: i6300esb_mem_writew: addr = c, val = 80
i6300esb: i6300esb_mem_writew: addr = c, val = 86
i6300esb: i6300esb_mem_writew: addr = c, val = 100
i6300esb: i6300esb_restart_timer: stage 1, timeout 96000
i6300esb: i6300esb_mem_writew: addr = c, val = 80
i6300esb: i6300esb_timer_expired: stage 2
2019-07-23T08:58:45.111484Z qemu-system-x86_64: network script /etc/qemu-ifdown failed with status 256
最後強制使用ctrl+c退出喂狗程式,由于第3節中的喂狗程式沒有處理強制退出的信号,是以程式沒有将watchdog硬體關閉,導緻i6300esb watchdog硬體等待逾時,進而觸發reset效果,這就是宏觀上的工作方式,後面會分析幾個主要函數。
4.2.裝置實作i6300esb_realize
/* vl.c:4574*/
if (qemu_opts_foreach(qemu_find_opts("device"),
device_init_func, NULL, NULL)) {
exit(1);
}
以上函數是在qemu主線程中,通過一個循環,周遊所有qemu指令行中的device配置,進行裝置的初始化,這裡的調用關系比較複雜,還涉及到不同的總線,此處隻非精确的列出大緻調用層級,可以看到i6300esb是PCI裝置:
|----->device_init_func
|----->qdev_device_add
|----->device_set_realized
|----->pci_qdev_realize
|----->i6300esb_realize
因為這是通用架構,是以其他代碼本文不分析,有興趣的可以查閱其他文檔,關于QDEV的,這裡主要看i6300esb_realize函數:
/* hw/watchdog/wdt_i6300esb.c:418 */
/* 該函數負責實作虛拟外設,本質上和記憶體條pc.ram沒有差別,隻是io裝置增加了ops,會執行一些動作,原理可以自行學習io虛拟化的流程*/
static void i6300esb_realize(PCIDevice *dev, Error **errp)
{
I6300State *d = WATCHDOG_I6300ESB_DEVICE(dev);//通用裝置類型的參數,類型轉換成I6300State類型
i6300esb_debug("I6300State = %p\n", d);
d->timer = timer_new_ns(QEMU_CLOCK_VIRTUAL, i6300esb_timer_expired, d);//添加一個定時器,第二個參數就是注冊回調函數
//以後該定時器到期或者逾時時候可以觸發的行為,就由該函數決定
d->previous_reboot_flag = 0;
//這裡就是初始化一個标記,代表之前沒有重新開機過(應該是由于定時器逾時造成的重新開機,人工重新開機不确定算不算)
memory_region_init_io(&d->io_mem, OBJECT(d), &i6300esb_ops, d,
"i6300esb", 0x10);
//為該外設配置設定存儲空間,注意這裡是io空間,不是記憶體空間,是以調用的是memory_region_init_io,因為這個函數的第三個參數會附帶各種行為,
//這些行為就是為了模拟外設的讀寫的,而記憶體不需要這樣模拟,是以沒有這些ops
pci_register_bar(&d->dev, 0, 0, &d->io_mem);
//将該裝置注冊到PCI總線上,就代表裝置插到PCI插槽上面了
/* qemu_register_coalesced_mmio (addr, 0x10); ? */
}
4.3.裝置重新開機函數i6300esb_reset
該函數是一個配置重置函數,裝置最開始初始化完畢之後會調用一次,作為模拟真實硬體各種寄存器的初始值,注意,這裡說的初始化,
是指的qemu程式模拟外設的初始化,而不是虛拟機的guest OS開機的過程中裝置驅動的初始化,各主要字段詳細的作用,需要對照手冊看3,這裡的初始化值沒有那麼重要,因為在guest OS啟動過程中,驅動程式會調用i6300esb_mem_writel函數将配置都修改掉,當逾時
的時候,guest OS重新開機或者關機之前又要調用本函數,之後guest OS再開機,再次加載驅動,又重新改寫配置,反複循環。初始化的時候調用關系如下:
|----->qemu_system_reset//初始化完成後會調用一次重置函數,這樣各種參數全部都重新整理成初始值了,為啟動做準備
|----->qemu_devices_reset
|----->qbus_reset_all_fn
|----->i6300esb_reset
在分析i6300esb_reset函數之前,得先了解i6300esb WDT功能流程3,該晶片組功能很強大,qemu2.8中僅僅模拟了watchdog的功能,是以,隻需要看相應的章節就可以了,抽象出來的工作流程如下:
按照上圖的邏輯,4.2節中的i6300esb_realize函數就相當于工廠裡面生産出來了一個i6300esb裝置,pci_register_bar函數就相當于把該裝置插入到pci插槽上,随後上圖中的‘1.上電’和‘2.硬體初始化’,就是開機自檢,初始化各種硬體,設定各種寄存器的初始值,在qemu裡,因為不是完全按照實體電路的時序來走的,稍微有點偏差,但是思想上是一樣的,這裡就可以大緻對應i6300esb_reset函數,雖然并沒有真正的reset,但是函數體的内容就是起的這個效果,對每個狀态和寄存器變量進行初始化,來模拟真正硬體裝置的初始值。i6300esb WDT開始工作後分兩個階段,一開始是進入stage1,開始計時,如果逾時到設定的時間的一半的時候,會觸發一個内部中斷3,然後進入stage2,這個内部中斷目前意義不大,qemu并沒有做額外的行為,隻是列印了一條告警資訊,真實硬體具體有什麼内涵,還不得而知。到了stage2,如果再次逾時,則會觸發watchdog注冊的行為,在真實硬體中,晶片會通過WDT_TOUT引腳發送一個外部中斷,會直接讓系統關機或者重新開機,在qemu中,則會調用注冊好的回調函數i6300esb_timer_expired。
/* hw/watchdog/wdt_i6300esb.c:149 */
/*
*/
static void i6300esb_reset(DeviceState *dev)
{
PCIDevice *pdev = PCI_DEVICE(dev);
I6300State *d = WATCHDOG_I6300ESB_DEVICE(pdev);
i6300esb_debug("I6300State = %p\n", d);
i6300esb_disable_timer(d);//去使能該裝置的定時器
/* NB: Don't change d->previous_reboot_flag in this function. */
d->reboot_enabled = 1;
d->clock_scale = CLOCK_SCALE_1KHZ;
d->int_type = INT_TYPE_IRQ;//根據晶片手冊,這個代表WDT配置寄存器的0-1bit位,初始化為0
d->free_run = 0;
d->locked = 0;
d->enabled = 0;//裝置初始化是沒有使能的
d->timer1_preload = 0xfffff;//stage1狀态的逾時時間,寄存器mmio位址為Base + 00h
d->timer2_preload = 0xfffff;//stage2狀态的逾時時間,寄存器mmio位址為Base + 04h
d->stage = 1;//從stage1開始
d->unlock_state = 0;
}
其他個别變量暫時不用管,隻看最主要的幾個,最核心的就是三個,timer1_preload,timer2_preload和stage,分别代表第一階段逾時時間,第二階段逾時時間,以及目前階段是多少。
/* qemu-timer.c:404 */
static void i6300esb_disable_timer(I6300State *d)
{
i6300esb_debug("timer disabled\n");
timer_del(d->timer);//從該定時器所屬的QEMUTimerList中,找到自己,并且删除
}
4.4.讀寫IO端口寄存器
上一節的代碼執行完成後,就是硬體已經做好準備了,直到qemu初始化完成,也不會再調用和i6300esb相關的函數了,在guest os開始啟動後,qemu會反複調用i6300esb_config_write,i6300esb_config_read這兩個函數(也會調用其他函數,下一節再分析),這兩個函數的列印和産生原因在4.4節中已經進行了說明,本文不會深入對guest os的驅動進行分析,僅僅會簡要提到部分關鍵内容。
/* hw/watchdog/wdt_i6300esb.c:215 */
static void i6300esb_config_write(PCIDevice *dev, uint32_t addr,
uint32_t data, int len)
{
I6300State *d = WATCHDOG_I6300ESB_DEVICE(dev);
int old;
i6300esb_debug("addr = %x, data = %x, len = %d\n", addr, data, len);
/*當qemu有以下列印的時候,就是配置WDT Configuration Register,對WDT_INT_TYPE等資訊進行配置*/
/*i6300esb: i6300esb_config_write: addr = 60, data = 3, len = 2*/
if (addr == ESB_CONFIG_REG && len == 2) {//0x60,
d->reboot_enabled = (data & ESB_WDT_REBOOT) == 0;
d->clock_scale =
(data & ESB_WDT_FREQ) != 0 ? CLOCK_SCALE_1MHZ : CLOCK_SCALE_1KHZ;
d->int_type = (data & ESB_WDT_INTTYPE);//0x3 & 0x11
/*當qemu有以下列印的時候,就是配置WDT Lock Register,設定定時器的使能*/
/*i6300esb: i6300esb_config_write: addr = 68, data = 0, len = 1*/
} else if (addr == ESB_LOCK_REG && len == 1) {//0x68
if (!d->locked) {
d->locked = (data & ESB_WDT_LOCK) != 0;
d->free_run = (data & ESB_WDT_FUNC) != 0;
old = d->enabled;
d->enabled = (data & ESB_WDT_ENABLE) != 0;
/*如果之前定時器是關閉的,現在收到打開指令,則調用定時器重置功能,然後進入stage 1*/
if (!old && d->enabled) /* Enabled transitioned from 0 -> 1 */
i6300esb_restart_timer(d, 1);
else if (!d->enabled)
i6300esb_disable_timer(d);
}
} else {
pci_default_write_config(dev, addr, data, len);//絕大多數走的這裡,這裡都是一些PCI通用配置,沒有涉及到
//定時器,不影響WDT定時器功能的了解。
}
}
i6300esb_config_read函數結構和以上函數相似,,可以看到,隻有addr為ESB_CONFIG_REG或者ESB_LOCK_REG的時候,才會由本函數來模拟效果,其他addr,都會直接調用通用的pci函數進行處理。可以看出,這種虛拟化外設的架構層級的設計思想,将所有通用的pci操作,抽象出來,單獨實作,然後把特定的功能截獲,進行模拟,跟真正的硬體的層級還是有一定差別的,例如,該i6300esb代碼是放在hw/watchdog目錄下,說明該代碼隻模拟watchdog功能,而無視此晶片組完整的其他功能,而完整的i6300esb全稱是Intel 6300ESB I/O Controller Hub,也就是我們通常所說的南橋晶片,是有很多功能的,是以在qemu的虛拟化主機闆上,是不需要模拟南橋晶片的實體形态的,隻需要針對在相應的IO通路,或者mmio通路的時候,做相應的處理,使其能夠達到硬體的效果,就可以了。
/* hw/watchdog/wdt_i6300esb.c:244 */
static uint32_t i6300esb_config_read(PCIDevice *dev, uint32_t addr, int len)
{
I6300State *d = WATCHDOG_I6300ESB_DEVICE(dev);
uint32_t data;
i6300esb_debug ("addr = %x, len = %d\n", addr, len);
if (addr == ESB_CONFIG_REG && len == 2) {
data =
(d->reboot_enabled ? 0 : ESB_WDT_REBOOT) |
(d->clock_scale == CLOCK_SCALE_1MHZ ? ESB_WDT_FREQ : 0) |
d->int_type;
return data;
} else if (addr == ESB_LOCK_REG && len == 1) {
data =
(d->free_run ? ESB_WDT_FUNC : 0) |
(d->locked ? ESB_WDT_LOCK : 0) |
(d->enabled ? ESB_WDT_ENABLE : 0);
return data;
} else {
return pci_default_read_config(dev, addr, len);
}
}
4.5.讀寫IO記憶體寄存器
其中i6300esb_mem_writew和i6300esb_mem_writel函數是最重要的,read函數基本作用不大。i6300esb_mem_writew函數就是處理喂狗的函數。
/* hw/watchdog/wdt_i6300esb.c:312 */
static void i6300esb_mem_writew(void *vp, hwaddr addr, uint32_t val)
{
I6300State *d = vp;
i6300esb_debug("addr = %x, val = %x\n", (int) addr, val);
/*對0xc位址所代表的寄存器先寫入0x80,然後寫入0x86是一個固定操作,代表可以擷取一次對0xc寄存器的寫權限*/
/*是以guest os核心驅動程式需要執行一條喂狗指令的時候,必須執行這個,見下一個代碼段*/
if (addr == 0xc && val == 0x80)
d->unlock_state = 1;//解鎖第一階段
else if (addr == 0xc && val == 0x86 && d->unlock_state == 1)
d->unlock_state = 2;//解鎖第二階段,代表解鎖成功
else {
if (d->unlock_state == 2) {//如果是已經解鎖成功
if (addr == 0xc) {//判斷寄存器mmio位址
if ((val & 0x100) != 0)//如果滿足這裡,代表是使用者程式在執行喂狗指令
/* This is the "ping" from the userspace watchdog in
* the guest ...
*/
i6300esb_restart_timer(d, 1);//喂狗成功則重新開機定時器
/* Setting bit 9 resets the previous reboot flag.
* There's a bug in the Linux driver where it sets
* bit 12 instead.
*/
if ((val & 0x200) != 0 || (val & 0x1000) != 0) {
d->previous_reboot_flag = 0;//這裡可以不管
}
}
d->unlock_state = 0;
}
}
}
驅動執行寫寄存器操作之前,例如writel(val, ESB_TIMER2_REG);,需要先執行一下代碼打開該寄存器的寫權限,而且有效次數隻有一次,想寫入下一個資料,比如再次重複此操作。
/*driver/watchdog/i6300esb.c:*/
static inline void esb_unlock_registers(void)
{
writew(ESB_UNLOCK1, ESB_RELOAD_REG);
writew(ESB_UNLOCK2, ESB_RELOAD_REG);
}
i6300esb_mem_writel函數是用來配置timeout時間
/*driver/watchdog/i6300esb.c:345*/
static void i6300esb_mem_writel(void *vp, hwaddr addr, uint32_t val)
{
I6300State *d = vp;
i6300esb_debug ("addr = %x, val = %x, stage = %d \n", (int) addr, val, d->unlock_state);
/*這裡的0x80和0x86并沒有使用到,應該隻是為了以防萬一*/
if (addr == 0xc && val == 0x80)
d->unlock_state = 1;
else if (addr == 0xc && val == 0x86 && d->unlock_state == 1)
d->unlock_state = 2;
else {
if (d->unlock_state == 2) {
if (addr == 0)
d->timer1_preload = val & 0xfffff;//這裡是給stage 1階段的定時器寫時間
else if (addr == 4)
d->timer2_preload = val & 0xfffff;//這裡是給stage 2階段的定時器寫時間
d->unlock_state = 0;
}
}
}
4.6.逾時機制
4.6.1.逾時處理函數
第一次逾時會調用這個函數,此處stage=1,這裡沒有對i6300esb的内部中斷進行模拟,因為沒有意義,然後調用i6300esb_restart_timer(d, 2)進入stage2,并且重新開機計時器,記錄下半階段的時間,如果也逾時了,則調用watchdog_perform_action()進行處理,這個處理函數比較簡單,就不分析了。
/*driver/watchdog/i6300esb.c:182*/
static void i6300esb_timer_expired(void *vp)
{
I6300State *d = vp;
i6300esb_debug("stage %d\n", d->stage);
if (d->stage == 1) {
/* What to do at the end of stage 1? */
switch (d->int_type) {
case INT_TYPE_IRQ:
fprintf(stderr, "i6300esb_timer_expired: I would send APIC 1 INT 10 here if I knew how (XXX)\n");
break;
case INT_TYPE_SMI:
fprintf(stderr, "i6300esb_timer_expired: I would send SMI here if I knew how (XXX)\n");
break;
}
/* Start the second stage. */
i6300esb_restart_timer(d, 2);
} else {
/* Second stage expired, reboot for real. */
if (d->reboot_enabled) {
d->previous_reboot_flag = 1;
watchdog_perform_action(); /* This reboots, exits, etc */
i6300esb_reset(&d->dev.qdev);
}
/* In "free running mode" we start stage 1 again. */
if (d->free_run)
i6300esb_restart_timer(d, 1);
}
}
4.6.2.如何判斷逾時
判斷逾時是在主循環裡面做的,大緻流程如下
|----->main_loop()//主循環,基于glib mainloop的
|----->main_loop_wait(nonblocking)
|----->qemu_clock_run_timers(type)
|----->timerlist_run_timers(main_loop_tlg.tl[type]);
|----->i6300esb_timer_expired(opaque)
主循環線程,會反複調用timerlist_run_timers來周遊每個定時器連結清單,判斷逾時時間,下面隻粘貼關鍵代碼
bool timerlist_run_timers(QEMUTimerList *timer_list)
{
QEMUTimer *ts;
int64_t current_time;
bool progress = false;
QEMUTimerCB *cb;
void *opaque;
/*omit*/
/*擷取目前時間*/
current_time = qemu_clock_get_ns(timer_list->clock->type);
for(;;) {//這個循環,結束條件為本輪次函數調用沒有任何定時器逾時了為止
qemu_mutex_lock(&timer_list->active_timers_lock);
ts = timer_list->active_timers;//擷取激活的定時器連結清單的head用來周遊
if (!timer_expired_ns(ts, current_time)) {//判斷是否逾時
qemu_mutex_unlock(&timer_list->active_timers_lock);
break;
}
//如果逾時,則從目前連結清單中剔除,并且調用逾時函數,此處涉及到i6300esb的定時器,則調用i6300esb_timer_expired
/* remove timer from the list before calling the callback */
timer_list->active_timers = ts->next;
ts->next = NULL;
ts->expire_time = -1;
cb = ts->cb;
opaque = ts->opaque;
qemu_mutex_unlock(&timer_list->active_timers_lock);
/* run the callback (the timer list can be modified) */
cb(opaque);//i6300esb_timer_expired
progress = true;
}
out:
qemu_event_set(&timer_list->timers_done_ev);
return progress;
}
整體設計就是這樣,這個看門狗子產品是學習qemu定時器的最佳案例。
reference
- linux下的watchdog ↩︎ ↩︎
- Watchdog device ↩︎
- Intel® 6300ESB I/O Controller Hub Datasheet ↩︎ ↩︎ ↩︎