天天看點

qemu intel i6300esb watchdog虛拟外設分析1.簡介2.添加外設3.編寫喂狗程式測試效果4.分析qemu代碼reference

文章目錄

  • 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

  1. linux下的watchdog ↩︎ ↩︎
  2. Watchdog device ↩︎
  3. Intel® 6300ESB I/O Controller Hub Datasheet ↩︎ ↩︎ ↩︎

繼續閱讀