QEMU JIT/KVM
QEMUは動的バイナリ翻訳(Dynamic Binary Translation)メカニズムを採用しており、中間コードを介して、実行時にゲストアーキテクチャの機械語をホストアーキテクチャの機械語形式にJIT(Just-In-Time)で翻訳し、翻訳結果をホストプロセッサに直接実行させます。このため、QEMUは異なるアーキテクチャ間でも良好なパフォーマンスを発揮します。QEMUのこの仮想化加速方式はTCG(Tiny Code Generator)と呼ばれます。
さらに、QEMUは他の仮想マシン管理プログラムを使用して加速することも可能です。最も一般的なのはKVM(Kernel Virtual Machine)の使用です。KVMはLinuxのカーネルドライバモジュールであり、ハードウェア支援仮想化技術を採用し、Linuxホストを仮想マシン管理プログラムにすることができます。この場合、仮想マシン管理プログラムはハードウェアの直上に位置するため、第一種仮想マシン管理プログラムに分類されます。
オペレーティングシステムを経由する必要がないため、ハードウェアリソースに直接アクセスでき、KVMのパフォーマンスは純粋なQEMUより優れています。QEMUがKVMと組み合わせて使用される場合、I/O仮想化はQEMUが担当し、プロセッサとメモリの仮想化はKVMが担当します。この方法は、直接QEMUを使用する場合に比べてパフォーマンスを大幅に向上させ、直接KVMを使用する場合に比べて強力なプラットフォーム機能を提供します。したがって、両者は互いの利点を発揮し、相乗効果を生み出します。
QEMUにおけるJITは、実行時にターゲットアーキテクチャの命令をホストアーキテクチャの命令に動的に翻訳し、翻訳結果を再利用するためにキャッシュします。フローは以下の通りです:
ターゲットアーキテクチャコード(例:ARMプログラム)
↓ 実行時翻訳
QEMU JITコンパイラ(TCG:Tiny Code Generator)
↓ 生成
ホストアーキテクチャコード(例:x86命令)
↓ 実行
直接CPU上で実行
TCGはQEMUのJITエンジンのコアであり、TCGの実装にはnative JITとTCIの2種類の技術があります。それぞれについて後で説明します。
実験環境
実験プラットフォーム情報:
まず、X86バージョンとARMバージョンの2つのプラットフォームのQEMUをコンパイルします。
- コードの取得:
wget https://download.qemu.org/qemu-4.1.0.tar.bz2
- 依存関係のインストール:
sudo apt-get install build-essential pkg-config zlib1g-dev libglib2.0-0 libglib2.0-dev libsdl2-dev libpixman-1-dev libfdt-dev autoconf automake libtool librbd-dev libaio-dev flex bison libattr1-dev libcap-ng-dev libcap-dev
- コンパイル
./configure --target-list=arm-softmmu --audio-drv-list=alsa,pa --prefix=/home/caozilong/Workspace/qemu/install
続いて make -j4 を実行
- DEBパッケージの生成
sudo apt-get install checkinstall
sudo checkinstall make install
-
インストール結果
-
分析
HOSTマシンがX86アーキテクチャであるため、ここでのtcg-target.inc.cは必ずX86ディレクトリのtcg-target.inc.cファイルになります。
ARMプラットフォーム上:
ARMプラットフォームではarmディレクトリのtcg-target.inc.cが使用されます
原理的には、Qemuは動的バイナリ翻訳(Binary Translation)技術を採用しており、複数のソース(HOST)とターゲット(target)間で異なる命令セットをクロスプラットフォームでエミュレーション実行できます。以下の図のように、限られた数の基本アーキテクチャ上で、基本アーキテクチャ数をはるかに超えるターゲットISAアーキテクチャを実行することができます。全体的な動作モデルは以下の通りです:
targetディレクトリとtcgディレクトリの役割をどのように判断するかは、const TranslatorOpsオブジェクトの位置によって判断できます。このオブジェクトはターゲットアーキテクチャの命令ISAの翻訳(JIT)関数ポインタを登録しており、targetディレクトリで定義されているため、TARGETディレクトリはシミュレートされるターゲット命令アーキテクチャを記述していることになります。
また、TCIディレクトリの位置によってソースマシン(HOST)がどのディレクトリであるかを判断することもできます。TCIディレクトリはtcg/tciにあり、tciディレクトリと同じレベルのアーキテクチャディレクトリはすべてソースマシンディレクトリです。
helperとTCGのディスパッチ
TCG翻訳エンジン
KVM加速仮想化プロセスは高速ですが、ターゲットプラットフォームとHOSTプラットフォームが同一アーキテクチャである必要があります。一方、TCGエンジンにはこの制限がありません。TCGはターゲットマシンアーキテクチャのバイナリ命令をHOST命令に翻訳し、事前に割り当てられたcode_gen_bufferに配置します。code_gen_bufferは実行可能権限を持っており、CPUはその中に置かれた命令を実行できます。この方法でローカルCPUの加速を実現します。
実行可能属性を持つMAPのCODE_GEN_BUFFERの割り当てプロセス:
実行可能な匿名領域マッピングの例:
#include <unistd.h>
#include <stdio.h>
#include <sys/mman.h>
#include <fcntl.h>
#include <stdlib.h>
#include <errno.h>
#include <string.h>
#define BUFFER_SIZE 0x4000
int multiply(int a, int b)
{
return a * b;
}
typedef int (*func_ptr)(int, int);
int main(void)
{
int i;
unsigned int *mem_addr;
func_ptr func;
#if 0
mem_addr = mmap(NULL, BUFFER_SIZE, PROT_READ | PROT_WRITE, MAP_SHARED|MAP_ANONYMOUS, -1, 0);
#else
mem_addr = mmap(NULL, BUFFER_SIZE, PROT_READ | PROT_WRITE | PROT_EXEC, MAP_SHARED|MAP_ANONYMOUS, -1, 0);
#endif
if(mem_addr == (void*)-1) {
printf("%s line %d, バッファのマッピングに失敗しました。エラー: %s.\n", __func__, __LINE__, strerror(errno));
return -1;
}
printf("%s line %d mem_addr = %p エラー: %s.\n", __func__, __LINE__, mem_addr, strerror(errno));
memcpy(mem_addr, multiply, 64);
func = (func_ptr)mem_addr;
printf("%s line %d, mem_addr = %p, mem_addr(7,8) = %d.\n", __func__, __LINE__, mem_addr, func(7, 8));
for(i = 0; i < 16; i ++) {
mem_addr[i] = i * 2;
printf("0x%02x ", mem_addr[i]);
}
printf("\n");
munmap((void *)mem_addr, BUFFER_SIZE);
return 0;
}
KVMがサポートされていない場合、TCIを有効にして仮想マシンインタプリタを起動し、すべてのアーキテクチャを互換性を持たせることができます:
--enable-tcg-interpreterを設定しない場合、TCGのバックエンドはnative x86_64です。つまり、TARGETの命令セットをローカルのx86_64命令に翻訳し、ローカルCPUに実行させます。
TCIをサポートするコンパイルオプション:--enable-tcg-interpreterを追加すると、TCGの実装バックエンドが変化します。この時点でTCGのバックエンドはTCG with bytecode interpreterになります。つまり、TARGETの命令はホスト上で純粋にインタプリタ実行に依存し、インタプリタの効率は非常に低いです。