天天看點

android emulator虛拟裝置分析第一篇之battery一、概述二、核心中虛拟裝置的驅動程式三、emulator中的虛拟裝置

為什麼android emulator需要虛拟裝置,簡單來說就是android系統需要使用,但是host系統卻沒有,比如gps,bluetooth,battery,gsm等。另外,虛拟裝置也提供了android emulator和guest os之間交流的方式,比如emulator控制台中可以設定電池的電量,是否在充電,如圖1所示;也可以設定目前的gps坐标等等;更重要的是,将guest os中畫圖的操作放到了host中執行,android emulator才能夠比較流暢的運作guest os。

android emulator虛拟裝置分析第一篇之battery一、概述二、核心中虛拟裝置的驅動程式三、emulator中的虛拟裝置

圖1

整個虛拟裝置的架構如圖2所示,左上角是guest os;左下角既包括了kernel中虛拟裝置的驅動,也包括了emulator中虛拟裝置的模拟;右下角是android emulator。

1、guest os通過hal或者直接使用kernel提供的虛拟裝置的驅動(一般來說,虛拟裝置的驅動會提供一些字元裝置檔案,以及屬性檔案,讀寫這些檔案即可)。

2-1、從kernel的角度來看,無需關心裝置是真實的,還是虛拟的,隻需要關心裝置提供的資源,比如IO資源,中斷号,以及如何讀寫裝置的寄存器,這裡和普通的驅動程式類似。需要注意的是,虛拟裝置都挂在platform bus上,友善動态地配置設定IO記憶體空間,以及中斷号,當然platform bus本身的IO記憶體和中斷号是固定寫死的,和emulator中固定寫死的相對應。

2-2、從emulator的角度看,首先是platform bus的模拟,需要使用固定寫死的IO記憶體和中斷号,這和kernel是相對應的。然後其他虛拟裝置動态注冊IO記憶體和中斷号到這個platform bus上面。kernel對IO記憶體進行讀寫時,emulator很明顯可以得知讀寫的是哪一個虛拟實體位址,然後得到虛拟頁面。虛拟頁面有相應的資訊,可以得到一個io_index變量,使用這個io_index,可以得知該頁面是哪一個虛拟裝置的IO記憶體,以及這個虛拟裝置自己的讀寫函數,使用對應裝置的讀寫函數,讀寫虛拟裝置的寄存器(每個虛拟裝置的寄存器都放在一個結構體中),根據約定好的寄存器的功能,去接收/傳回資料。這裡知識點比較多,且涉及到了很多硬體的知識,對于純軟體的開發人員來說,過于複雜,後面會詳細講解。

3、emulator提供了一種抽象的虛拟裝置,叫做pipe,對應的裝置檔案為/dev/qemu_pipe,提供guest os和emulator通用的資料收發方法。基于這一層通用的資料收發方法,在emulator中注冊了很多qemud service,guest os可以通過讀寫/dev/qemu_pipe去和這些qemud service通信。

PS:

1、guest os中有一個qemud程序,使用虛拟裝置ttyS1去提供guest os和emulator的交流方式,是一種舊的方式,速度比較慢,已基本被pipe方式代替。

當注冊一個新的裝置時,會将裝置作為參數,probe給每一個比對的驅動程式,看看哪個驅動程式可以處理這個新的裝置。

當注冊一個新的驅動時,會将驅動作為參數,probe給每一個未被處理的比對的裝置,看看新的驅動可以處理哪一個未被處理的裝置。

通過驅動和裝置的名字進行比對。

android emulator虛拟裝置分析第一篇之battery一、概述二、核心中虛拟裝置的驅動程式三、emulator中的虛拟裝置

如果你搞過硬體,可以浏覽一下說明,即可知道這個晶片幹什麼的了,下面一段話無需再看。如果你是搞純軟體的,還是老老實實看吧。

可以把裝置當作一個函數,寄存器是它的一些輸入資料、傳回資料,以及一些狀态。中斷有點像linux程式設計中的信号(signal),當裝置有資料可讀,可以接收資料,狀态發生變化等等時,可以(當然,也可以不)産生一個中斷,打斷核心的執行(CPU硬體上的打斷,不是作業系統的排程),跳轉到中斷處理函數(類似于信号處理函數,信号和信号處理函數對應,中斷号和中斷處理函數對應)。具體的跳轉方式如下:使用核心中的函數request_irq申請中斷時,填寫中斷号和中斷函數。記憶體中有一張表(數組),叫做中斷向量表,以中斷号為key,以中斷函數的位址為value,記錄了中斷函數的資訊。當中斷發生時,CPU可以得知中斷号,然後通過中斷向量表查找到對應的中斷處理函數,然後跳轉過去執行。真正的中斷函數是沒有輸入參數和傳回值,核心中提供的中斷函數是經過處理的,是以會有int

irq, void *dev_id兩個參數。虛拟裝置的寄存器的位址都很小,可以了解為偏移量,那麼base如何擷取呢?首先通過platform bus,得到IO記憶體的虛拟實體位址,然後使用ioremap将虛拟實體位址映射到核心虛拟位址中,然後可以在核心中使用。注意不能直接當成普通的記憶體來用,需要使用特殊的readb, writeb, readw, writew, readl, writel,因為硬體的寄存器,每次讀取,傳回的資料可以是不同的;如果要通過寄存器發送一個數組,那麼循環對同一個寄存器進行寫操作即可,寄存器位址不用++;另外對于讀取和寫入的順序以及操作的寬度(8bit,

16bit or 32bit)也有嚴格的要求,不是随便來的。如果當成普通記憶體通路,那麼編譯器可能會去使用緩存,CPU執行指令可能亂序,以及寬度不對,都會導緻硬體工作不正常,是以不能當成普通記憶體指針去使用。

battery驅動代碼在goldfish目錄中的drivers/misc/qemupipe/qemu_pipe.c,為了簡單起見,登出,關閉,清理的代碼就不詳細說明了。

驅動的初始化函數是:

注冊了一個名為goldfish-battery的總線裝置,它的probe函數為goldfish_battery_probe,在安裝battery驅動,或者總線上有新的裝置時會被調用,去比對驅動程式和裝置(根據驅動的名字和裝置的名字比對)。

goldfish_battery_probe先是對goldfish_battery_data結構體進行初始化,然後使用platform_get_resource去擷取裝置的IO記憶體資源,對IORESOURCE_MEM資源進行ioremap,然後将base儲存到data->reg_base中;然後使用platform_get_irq擷取中斷号,并儲存到data->irq中并使用request_irq函數注冊了中斷函數goldfish_battery_interrupt。

data->battery和data->ac都是struct power_supply,比如battery:

會有一些屬性名,屬性個數,讀取屬性的函數等資訊,power_supply_register之後,在guest os的/sys/class/power_supply/battery中會有一些檔案,檔案名都和屬性名對應,比如capacity,health,status等,讀函數也就是剛才的goldfish_battery_get_property,寫函數沒有。guest os使用者空間的程式,直接讀取這些屬性檔案,屬性檔案的内容,都來自于對寄存器的讀取,比如

這樣就可得到虛拟裝置battery的資訊。

最後,GOLDFISH_BATTERY_WRITE(data, BATTERY_INT_ENABLE, BATTERY_INT_MASK)寫BATTERY_INT_MASK到寄存器BATTERY_INT_ENABLE使能了中斷。當battery以及ac的狀态發生變化時,虛拟裝置将産生中斷(這部分代碼在emulator中),然後我們的中斷函數goldfish_battery_interrupt就會被調用了。

完整的goldfish_battery_probe代碼如下:

中斷函數goldfish_battery_interrupt,先讀取STATUS寄存器,判斷是battery的中斷事件,還是ac的

然後調用power_supply_changed去通知核心。

完整的goldfish_battery_interrupt如下:

需要注意一下struct goldfish_battery_data是如何傳遞給中斷函數和platform_device的。

在看虛拟裝置之前,最好把platform bus的驅動程式也看了arch/x86/mach-goldfish/pdev_bus.c

讀取NAME_LEN可以得到bus上一個裝置的名字長度,讀取IO_BASE可以得到IO記憶體的起始位址,讀取IO_SIZE可以得到IO記憶體的大小,這些都很容易了解。往GET_NAME寄存器寫一個指針,然後裝置名稱被虛拟bus寫入這個指針,也還好。需要注意的是BUS_OP,先寫BUS_OP,開始裝置的枚舉,每次讀BUS_OP,如果是PDEV_BUS_OP_ADD_DEV,說明有新的裝置,并切換下一個裝置,切換之後,再次讀取NAME_LEN,IO_BASE,IO_SIZE将傳回的下一個裝置的資訊了;如果是PDEV_BUS_OP_DONE,說明枚舉完畢,沒有新的裝置了。

首先是把和emulator約定好的IO記憶體和中斷号資訊提供給核心:

然後是注冊platform bus的驅動:

goldfish_pdev_bus_probe函數比battery的probe還要簡單,就不詳細說明了,注意最後往PDEV_BUS_OP寫東西,開始裝置的模拟(寫PDEV_BUS_OP,虛拟裝置會觸發中斷,然後在中斷函數裡面進行裝置的枚舉)。

中斷函數goldfish_pdev_bus_interrupt就是不斷讀取PDEV_BUS_OP,如果傳回PDEV_BUS_OP_ADD_DEV,就調用goldfish_new_pdev去添加裝置,如果傳回PDEV_BUS_OP_DONE就結束。

goldfish_new_pdev通過讀取寄存器,獲得新裝置的名稱,IO記憶體,中斷号等資訊,擷取裝置的資訊後,添加裝置結構體到pdev_bus_new_devices連結清單,這樣battery的驅動就可以得到battery裝置結構體中的IO記憶體和中斷号的資訊了(platform_get_resource)。

最後調用了schedule_work(&pdev_bus_worker)函數。goldfish_pdev_worker是worker,類似于tasklet,注冊後會在以後某一時刻運作,而不會占用中斷上下文的時間。該函數主要用于更新三個連結清單,新加裝置,已删除裝置,已注冊裝置。

核心相關的東西結束了,後面的都是emulator虛拟裝置的東西了,會比較難以了解,而且沒有什麼資料。

首先是虛拟裝置的寄存器,定義了寄存器的位址,然後使用結構體goldfish_battery_state儲存寄存器的資訊,當對這個結構體讀寫時,就是讀寫寄存器,用來模拟寄存器。

這段代碼實作了類似于序列化的東西,和模拟裝置關系不大,以及之後出現的load和save大多數都是一樣的東西,不會贅述。

虛拟裝置的初始化函數是goldfish_battery_init,往寄存器結構體裡面塞了一些預設值,比如名字,電量什麼的。最後調用了goldfish_device_add去添加裝置到bus,這個函數非常關鍵,動态配置設定了每個裝置的IO記憶體空間,以及中斷号,設定了對應IO記憶體的讀寫函數數組以及寄存器結構體,後面将詳細說明。

goldfish_battery_read和goldfish_battery_write是虛拟裝置的寄存器的讀寫函數,給定寄存器結構體,以及寄存器(就是偏移量),去模拟寄存器的讀寫。

注意讀BATTERY_INT_STATUS之後,如果有中斷标志位,則清空,因為程式已經讀到了有新的中斷事件,沒必要再去觸發一次中斷了。

讀寫函數有三組,分别對應8bit,16bit,32bit的寬度去讀寫,會在goldfish_device_add時指定這兩個讀寫函數數組。

注意platform bus本身也是一個裝置,也在裝置連結清單中。

初始化相關的代碼,goldfish_device_init和goldfish_device_bus_init中指定的base, size, irq, irq_count是固定寫死的,和核心中的代碼對應。

寫寄存器的函數是goldfish_bus_write,如果是寫PDEV_BUS_OP_INIT,那麼調用goldfish_bus_op_init函數,如果裝置連結清單非空,将産生一個中斷事件,核心代碼中的中斷函數将得到執行,去進行platform bus驅動中所說的裝置的枚舉。其他的沒什麼特别的。

讀寄存器的函數是goldfish_bus_read,每次讀取PDEV_BUS_OP,都會疊代一個新的裝置,傳回值說明是否有新的裝置,其他的沒什麼特别的。

關于觸發中斷的函數void goldfish_device_set_irq(struct goldfish_device *dev, int irq, int level)需要詳細說明一下。

最多有15個虛拟中斷,兩片8259A級連,從片接在主片的IRQ2上(IRQ from 0 to 7 for every chip)。

goldfish_device_add放在最後,因為這是一個最最重要的函數,可以解答核心對虛拟裝置的寄存器進行讀寫時,emulator怎麼知道是哪一個虛拟裝置被通路了,哪一個虛拟寄存器被通路了,應該怎麼模拟這個虛拟寄存器的讀寫。

這麼重要的函數,當然隻有幾行,調用了其他的函數。這裡先簡要說明下,goldfish_add_device_no_io是根據目前空閑的IO記憶體位址和中斷号,去給新的裝置配置設定IO記憶體和中斷号的(如果base or irq不等于0,說明靜态配置設定好了);cpu_register_io_memory維護了三個數組,分别是三個讀函數的數組,三個寫函數的數組,虛拟裝置寄存器結構體的數組,數組下标為io_index,是動态配置設定的,注意有幾個io_index是保留的;cpu_register_physical_memory配置設定虛拟實體記憶體頁,并将io_index<<3|subwidth儲存在了頁面資訊PhysPageDesc結構體的phys_offset中。

動态配置設定虛拟裝置的IO記憶體和中斷号的函數為goldfish_add_device_no_io,注意x86上有幾個中斷号是保留的。

折騰三個數組的函數是cpu_register_io_memory,注意io_index是動态配置設定的,每一個虛拟裝置對應一個io_index,通過io_index可以找到這個虛拟裝置的三個讀寫函數和寄存器結構體。注意io_index的最大值是IO_MEM_NB_ENTRIES:

函數的傳回值是io_index << 3 | subwidth,subwidth标記三個讀寫函數是否有NULL的。

當得知io_index以及寄存器(偏移量)時,就可以調用虛拟裝置自己的讀寫函數去讀寫寄存器結構體,進行裝置的模拟了。如何在kernel寫寄存器時,得知這個io_index呢,下面分析。

第三個函數cpu_register_physical_memory配置設定虛拟實體記憶體,并且将io_index << 3 | subwidth儲存在了PhysPageDesc結構體的phys_offset中了。

實體記憶體管理的代碼很複雜,隻需要了解普通的ram是按頁配置設定,并且phys_offset=0,表示是普通ram;IO記憶體也是按頁配置設定的,并且phys_offset就是剛才的io_index << 3 | subwidth,如果IO記憶體占了多個頁面,那麼每個頁面的phys_offset是相同的(region_offset不同),可以找到相同的io_index。

下面是幾個宏的定義,注意IO_MEM_ROM,IO_MEM_UNASSIGNED,IO_MEM_NOTDIRTY是get_free_io_mem_idx預先保留的幾個io_index。

如果使用kvm加速的話,當讀寫MMIO時,會退出:

addr1, xxx)函數。這兩個函數是對cpu_register_io_memory所維護的三個數組的包裝。這樣,就可以使用寄存器對應的虛拟裝置的讀寫函數和寄存器結構體以及偏移量去模拟寄存器的讀寫了。

haxm和tcg原理類似。

參考資料:

驅動程式的編寫可以看:LINUX裝置驅動程式(第3版)

硬體的知識,可以看看郭天祥51單片機的視訊

别看譚浩強

繼續閱讀