- QEMU/KVM源碼解析與應用
- 李強編著
- 5364字
- 2021-08-24 11:53:38
第2章 QEMU基本組件
2.1 QEMU事件循環機制
2.1.1 glib事件循環機制
“一切皆文件”是UNIX/Linux的著名哲學理念,Linux中的具體文件、設備、網絡socket等都可以抽象為文件。內核中通過虛擬文件系統(Virtual File System,VFS)抽象出一個統一的界面,使得訪問文件有統一的接口。Linux通過fd來訪問一個文件,應用程序也可以調用select、poll、epoll系統調用來監聽文件的變化。QEMU程序的運行即是基于各類文件fd事件的,QEMU在運行過程中會將自己感興趣的文件fd添加到其監聽列表上并定義相應的處理函數,在其主線程中,有一個循環用來處理這些文件fd的事件,如來自用戶的輸入、來自VNC的連接、虛擬網卡對應tap設備的收包等。這種事件循環機制在Windows系統或者其他GUI應用中非常常見。QEMU的事件循環機制基于glib,glib是一個跨平臺的、用C語言編寫的若干底層庫的集合。本節對glib提供的事件循環機制進行簡單介紹。
glib實現了完整的事件循環分發機制,在這個機制中有一個主循環負責處理各種事件,事件通過事件源描述,事件源包括各種文件描述符(文件、管道或者socket)、超時和idle事件等,每種事件源都有一個優先級,idle事件源在沒有其他高優先級的事件源時會被調度運行。應用程序可以利用glib的這套機制來實現自己的事件監聽與分發處理。glib使用GMainLoop結構體來表示一個事件循環,每一個GMainLoop都對應有一個主上下文GMainContext。事件源使用GSource表示,每個GSource可以關聯多個文件描述符,每個GSource會關聯到一個GMainContext,一個GMainContext可以關聯多個GSource。
glib的一個重要特點是能夠定義新的事件源類型,可以通過定義一組回調函數來將新的事件源添加到glib的事件循環框架中。新的事件源通過兩種方式跟主上下文交互。第一種方式是GSourceFuncs中的prepare函數可以設置一個超時時間,以此來決定主事件循環中輪詢的超時時間;第二種方式是通過g_source_add_poll函數來添加fd。
glib主上下文的一次循環包括prepare、query、check、dispatch四個過程,分別對應glib的g_main_context_prepare()、g_main_context_query()、g_main_context_check()以及g_main_context_dispatch()四個函數,其狀態轉換如圖2-1所示。

圖2-1 glib事件循環狀態轉換圖
下面簡單介紹這幾個步驟:
1)prepare:通過g_main_context_prepare()會調用事件對應的prepare回調函數,做一些準備工作,如果事件已經準備好進行監聽了,返回true。
2)query:通過g_main_context_query()可以獲得實際需要調用poll的文件fd。
3)check:當query之后獲得了需要進行監聽的fd,那么會調用poll對fd進行監聽,當poll返回的時候,就會調用g_main_context_check()將poll的結果傳遞給主循環,如果fd事件能夠被分派就會返回true。
4)dispatch:通過g_main_context_dispatch()可以調用事件源對應事件的處理函數。
上面就是glib事件循環機制的處理流程,應用程序需要做的就是把新的事件源加入到這個處理流程中,glib會負責處理事件源上注冊的各種事件。
2.1.2 QEMU中的事件循環機制
QEMU的事件循環機制如圖2-2所示。QEMU在運行過程中會注冊一些感興趣的事件,設置其對應的處理函數。如對于VNC來說,會創建一個socket用于監聽來自用戶的連接,注冊其可讀事件為vnc_client_io,當VNC有連接到來時,glib的框架就會調用vnc_client_io函數。除了VNC,QEMU中還會注冊很多其他事件監聽,如網卡設備的后端tap設備的收包,收到包之后QEMU調用tap_send將包路由到虛擬機網卡前端,若虛擬機使用qmp,那么在管理界面中,當用戶發送qmp命令過來之后,glib會調用事先注冊的tcp_chr_accept來處理用戶的qmp命令。本節將分析QEMU的事件循環實現。關于QEMU的事件循環機制,Fam Zheng在KVM Forum 2015上有一個非常不錯的演講,題為“Improving the QEMU Event Loop”,讀者可以自行搜索學習。

圖2-2 QEMU事件循環機制
可以通過如下命令啟動虛擬機。

在此命令行下啟動的QEMU程序,其主循環事件總共包含了圖2-3所示的5個事件源,其中前面兩個qemu_aio_context和iohander_ctx都是類型為AioContext的自定義事件源,中間兩個VNC的事件源是glib標準事件源,最后一個不是QEMU通過調用g_source_attach添加的事件源,而是glib內部庫自己使用的加入到事件循環的fd。qemu_aio_context和iohandler_ctx是兩個比較特殊的自定義的類型為AioContext的事件源,前者主要用于處理QEMU中塊設備相關的異步I/O請求通知,后者用于處理QEMU中各類事件通知,這些事件通知包括信號處理的fd、tap設備的fd以及VFIO設備對應的中斷通知等。glib中事件源可以添加多個事件fd,對應的AioContext表示為每一個fd在AioContext都有記錄,glib框架在執行iohandler_ctx的分發函數時,會遍歷其上所有的fd,如果某個fd上的數據準備好了,就會調用相應的回調函數。這里需要注意,每一個事件源本身都會有一個fd,當添加一個fd到事件源時,整個glib主循環都會監聽該fd。以前述命令為例,QEMU主循環總共會監聽6個fd,其中5個是事件源本身的fd,還有一個是通過系統調用SYS_signalfd創建的用來處理信號的fd,圖2-3中的tap設備fd只是作為一個例子,在上述命令行下并不會添加該fd。任何一個fd準備好事件之后都可以喚醒主循環。本節末會對這6個fd的產生及其分析過程進行介紹。
QEMU主循環對應的最重要的幾個函數如圖2-4所示。QEMU的main函數定義在vl.c中,在進行好所有的初始化工作之后會調用函數main_loop來開始主循環。

圖2-3 QEMU事件源實例

圖2-4 QEMU主循環對應的函數
main_loop及其調用的main_loop_wait的主要代碼如下。main_loop_wait函數調用了os_host_main_loop_wait函數,在后者中可以找到對應圖2-4的相關函數,即每次main_loop循環的3個主要步驟。main_loop_wait在調用os_host_main_loop_wait前,會調用qemu_soonest_timeout函數先計算一個最小的timeout值,該值是從定時器列表中獲取的,表示監聽事件的時候最多讓主循環阻塞的事件,timeout使得QEMU能夠及時處理系統中的定時器到期事件。


QEMU主循環的第一個函數是glib_pollfds_fill,下面的代碼顯示了該函數的工作流程。該函數的主要工作是獲取所有需要進行監聽的fd,并且計算一個最小的超時時間。首先調用g_main_context_prepare開始為主循環的監聽做準備,接著在一個循環中調用g_main_context_query獲取需要監聽的fd,所有fd保存在全局變量gpollfds數組中,需要監聽的fd的數量保存在glib_n_poll_fds中,g_main_context_query還會返回fd時間最小的timeout,該值用來與傳過來的cur_timeout(定時器的timeout)進行比較,選取較小的一個,表示主循環最大阻塞的時間。

os_host_main_loop_wait在調用glib_pollfds_fill之后就完成了圖2-4的第一步,現在已經有了所有需要監聽的fd了,然后會調用qemu_mutex_unlock_iothread釋放QEMU大鎖(Big Qemu Lock,BQL),BQL會在本章第2節“QEMU線程模型”中介紹,這里略過。接著os_host_main_loop_wait函數會調用qemu_poll_ns,該函數代碼如下。它接收3個參數,第一個是要監聽的fd數組,第二個是fds數組的長度,第三個是一個timeout值,表示g_poll最多阻塞的時間。qemu_poll_ns在配置CONFIG_PPOLL時會調用ppoll,否則調用glib的函數g_poll,g_poll是一個跨平臺的poll函數,用來監聽文件上發生的事件。


qemu_poll_ns的調用會阻塞主線程,當該函數返回之后,要么表示有文件fd上發生了事件,要么表示一個超時,不管怎么樣,這都將進入圖2-4的第三步,也就是調用glib_pollfds_poll函數進行事件的分發處理,該函數的代碼如下。glib_pollfds_poll調用了glib框架的g_main_context_check檢測事件,然后調用g_main_context_dispatch進行事件的分發。

下面以虛擬機的VNC連接為例分析相應的函數調用過程。VNC子模塊在初始化的過程中會在vnc_display_open中調用qio_channel_add_watch,設置其監聽的回調函數為vnc_listen_io,該過程最終會創建一個回調函數集合為qio_channel_fd_source_funcs的事件源,其中的dispatch函數為qio_channel_fd_source_dispatch,該函數會調用vnc_listen_io函數。

以本小節最開始的命令啟動虛擬機,然后在vnc_listen_io處下斷點,使用VNC客戶端連接虛擬機,QEMU進程會中斷到調試器中,使用gdb的bt命令可以看到圖2-5所示的函數調用堆棧。
上面是QEMU效仿glib實現的主循環,但主循環存在一些缺陷,比如在主機使用多CPU的情況下伸縮性受到限制,同時主循環使用了QEMU全局互斥鎖,從而導致VCPU線程和主循環存在鎖競爭,使性能下降。為了解決這個問題,QEMU引入了iothread事件循環,把一些I/O操作分配給iothread,從而提高I/O性能。

圖2-5 vnc連接fd的事件處理函數堆棧
2.1.3 QEMU自定義事件源
QEMU自定義了一個新的事件源AioContext,有兩種類型的AioContext,第一類用來監聽各種各樣的事件,比如iohandler_ctx,第二類是用來處理塊設備層的異步I/O請求,比如QEMU默認的qemu_aio_context或者模塊自己創建的AioContext。這里只關注第一種情況,即事件相關的AioContext。下面的代碼列出了AioContext結構中的主要成員。

這里簡單介紹一下AioContext中的幾個成員。
● source:glib中的GSource,每一個自定義的事件源第一個成員都是GSource結構的成員。
● lock:QEMU中的互斥鎖,用來保護多線程情況下對AioContext中成員的訪問。
● aio_handlers:一個鏈表頭,其鏈表中的數據類型為AioHandler,所有加入到AioContext事件源的文件fd的事件處理函數都掛到這個鏈表上。
● notify_me和notified都與aio_notify相關,主要用于在塊設備層的I/O同步時處理QEMU下半部(Bottom Halvs,BH)。
● first_bh:QEMU下半部鏈表,用來連接掛到該事件源的下半部,QEMU的BH默認掛在qemu_aio_context下。
● notifier:事件通知對象,類型為EventNotifier,在塊設備進行同步且需要調用BH的時候需要用到該成員。
● tlg:管理掛到該事件源的定時器。
剩下的結構與塊設備層的I/O同步相關,這里略過。
AioContext拓展了glib中source的功能,不但支持fd的事件處理,還模擬內核中的下半部機制,實現了QEMU中的下半部以及定時器的管理。
接下來介紹AioContext的相關接口,這里只以文件fd的事件處理為主,涉及AioContext與塊設備層I/O同步的代碼會省略掉。首先是創建AioContext函數的aio_context_new,該函數的核心調用如下。

aio_context_new函數首先創建分配了一個AioContext結構ctx,然后初始化代表該事件源的事件通知對象ctx->notifier,接著調用了aio_set_event_notifier用來設置ctx->notifier對應的事件通知函數,初始化ctx中其他的成員。
aio_set_event_notifier函數調用了aio_set_fd_handler函數,后者是另一個重要的接口函數,其作用是添加或者刪除事件源中的一個fd。如果作用是添加,則會設置fd對應的讀寫函數,aio_set_fd_handler即可用于從AioContext中刪除fd,也可以用于添加fd,下面的代碼去掉了刪除事件源中fd監聽處理的步驟,其代碼如下。

aio_set_fd_handler的第一個參數ctx表示需要添加fd到哪個AioContext事件源;第二個參數fd表示添加的fd是需要在主循環中進行監聽的;is_external用于塊設備層,對于事件監聽的fd都設置為false;io_read和io_write都是對應fd的回調函數,opaque會作為參數調用這些回調函數。
aio_set_fd_handler函數首先調用find_aio_handler查找當前事件源ctx中是否已經有了fd,考慮新加入的情況,這里會創建一個名為node的AioHandler,使用fd初始化node->pfd.fd,并將其插入到ctx->aio_handlers鏈表上,調用glib接口g_source_add_poll將該fd插入到了事件源監聽fd列表中,設置node事件讀寫函數為io_read,io_write函數,根據io_read和io_write的有無設置node->pfd.events,也就是要監聽的事件。aio_set_fd_handler調用之后,新的fd事件就加入到了事件源的aio_handlers鏈表上了,如圖2-6所示。

圖2-6 AioContext的aio_handlers鏈表
aio_set_fd_handler函數一般被塊設備相關的操作直接調用,如果僅僅是添加一個普通的事件相關的fd到事件源,通常會調用其封裝函數qemu_set_fd_handler,該函數將事件fd添加到全部變量iohandler_ctx事件源中。
glib中自定義的事件源需要實現glib循環過程中調用的幾個回調函數,QEMU中為AioContext事件源定義了名為aio_source_funcs的GSourceFuns結構。

這幾個函數是自定義事件源需要實現的,這里介紹一下最重要的事件處理分派函數aio_ctx_dispatch。aio_ctx_dispatch代碼如下,其會調用aio_dispatch,aio_dispatch要完成3件事:第一是BH的處理,第二是處理文件fd列表中有事件的fd,第三是調用定時器到期的函數。這里分析一下文件fd的處理部分。


aio_dispatch_handlers函數會遍歷aio_handlers,遍歷監聽fd上的事件是否發生了。fd發生的事件存在node->pfd.revents中,注冊時指定需要接受的事件存放在node->pfd.events中,revents變量保存了fd接收到的事件。對應G_IO_IN可讀事件來說,會調用注冊的fd的io_read回調,對G_IN_OUT可寫事件來說,會調用注冊的fd的io_write函數。當然,如果當前的fd已經刪除了,則會刪除這個節點。
2.1.4 QEMU事件處理過程
上一節介紹了QEMU的自定義事件源,本節以signalfd的處理為例介紹QEMU事件處理的過程。signalfd是Linux的一個系統調用,可以將特定的信號與一個fd綁定起來,當有信號到達的時候fd就會產生對應的可讀事件。以如下命令啟動虛擬機。

在sigfd_handler函數下斷點,在另一個終端向QEMU發送SIGALARM信號,命令如下,其中2762是QEMU進程號。

在第一個命令行的中斷中可以看到QEMU進程已經在sigfd_handler函數被中斷下來,圖2-7顯示了此時的函數調用情況,從中可以看到整個過程調用了glib的事件分發函數g_main_context_dispatch,然后調用了AioContext自定義事件源的回調函數aio_ctx_dispatch,最終調用到QEMU為信號注冊的可讀回調函數sigfd_handler。

圖2-7 sigfd_handler函數的棧回溯
下面對這個過程進行簡單分析,首先分析signal事件源的初始化。vl.c中的main函數會調用qemu_init_main_loop進行AioContext事件源的初始化,該函數代碼如下。

qemu_init_main_loop函數調用qemu_signal_init將一個fd與一組信號關聯起來,qemu_signal_init調用了之前提到的qemu_set_fd_handler函數,設置該signalfd對應的可讀回調函數為sigfd_handler。qemu_set_fd_handler在首次調用時會調用iohandler_init創建一個全局的iohandler_ctx事件源,這個事件源的作用是監聽QEMU中的各類事件。最終qemu_signal_init會在iohandlers_ctx的aio_handlers上掛一個AioHandler節點,其fd為這里的signalfd,其io_read函數為這里的sigfd_handler。
qemu_init_main_loop函數接著會調用aio_context_new創建一個全局的qemu_aio_context事件源,這個事件源主要用于處理BH和塊設備層的同步使用。
最后,該函數調用aio_get_g_source和iohandler_get_g_source分別獲取qemu_aio_context和iohandler_ctx的GSource,以GSource為參數調用g_source_attach兩個AioContext加入到glib的主循環中去。
將信號對應的fd加入事件源以及將事件源加入到glib的主循環之后,QEMU就會按照2.1.2節所述,在一個while循環中進行事件監聽。當使用kill向QEMU進程發送SIGALARM信號時,signalfd就會有可讀信號,從而導致glib的主循環返回調用g_main_context_dispatch進行事件分發,這會調用到aio_ctx_dispatch,最終會調用到qemu_signal_init注冊的可讀處理函數sigfd_handler。
2.1.5 QEMU主循環監聽的fd解析
2.1.2節中介紹了QEMU的事件循環機制,并且在隨后的幾節中介紹了與事件循環機制相關的源碼,本節將實際分析QEMU主事件循環監聽的fd來源。首先以如下命令啟動虛擬機。

為了方便稍后的敘述,這里再把glib_pollfds_fill的代碼和行號列在圖2-8中:

圖2-8 glib_pollfds_fill函數源碼
使用gdb在第200行下斷點。

輸入“r”讓QEMU進程運行起來。
gpollfds是一個數組,存著所有需要監聽的fd,其成員類型為pollfd,成員都存放在gpollfds.data中,所以這里可以判斷到底監聽了哪些fd。圖2-9顯示了所有監聽的fd,總共有6個fd,分別是4、6、8、9、e、f。

圖2-9 QEMU監聽的fd
從圖2-9可以看出來,第一個fd 4是在monitor_init_globals初始化調用iohandler_init并創建iohander_ctx時調用的,其本身對應iohander_ctx中的事件通知對象的fd。gdb繼續輸入“c”讓程序運行起來,在隨后的g_source_add_poll斷點中可以看到6、8、e、f這幾個fd的來源。6是調用qemu_signal_init創建signalfd對應的fd, 8是qemu_aio_context對應的fd, e和f是vnc創建的fd。但是沒有fd 9的信息。
找到QEMU對應的進程id,查看/proc/目錄下該QEMU進程對應fd情況,如圖2-10所示。這里可以看到fd 9是一個eventfd,其雖然在glib事件循環監聽中,但是其并沒有通過g_source_add_poll加入。

圖2-10 QEMU進程fd分布
在eventfd函數下斷點,每次停下來之后在gdb中輸入finish命令完成當前函數的執行,然后查看rax寄存器的值,當其是9的時候查看堆棧,結果如圖2-11所示。從中可以看出,fd 9這個eventfd是由glib庫自己創建使用的。
這樣,glib監聽的6個fd就搞清楚了。當然,如果給QEMU提供不同的參數,其監聽的fd也會隨著變化。

圖2-11 fd 9注冊過程