はじめに

前回は、VMX non root modeからvmm.koへVMExitしてきたときの処理を解説しました。 今回はI/O命令によるVMExitを受けて行われるユーザランドでのエミュレーション処理を解説します。

解説対象のソースコードについて

本連載では、FreeBSD-CURRENTに実装されているBHyVeのソースコードを解説しています。 このソースコードは、FreeBSDのSubversionリポジトリから取得できます。 リビジョンはr245673を用いています。

お手持ちのPCにSubversionをインストールし、次のようなコマンドでソースコードを取得してください。

svn co -r245673 svn://svn.freebsd.org/base/head src

/usr/sbin/bhyveによる仮想CPUの実行処理のおさらい

/usr/sbin/bhyveは仮想CPUの数だけスレッドを起動し、それぞれのスレッドが/dev/vmm/${name}に対してVM_RUN ioctlを発行します(図1)。 vmm.koはioctlを受けてCPUをVMX non root modeへ切り替えゲストOSを実行します(VMEntry)。

VM_RUN ioctl による仮想 CPU の実行イメージ

VM_RUN ioctl による仮想 CPU の実行イメージ

VMX non root modeでハイパーバイザの介入が必要な何らかのイベントが発生すると制御がvmm.koへ戻され、イベントがトラップされます(VMExit)。

イベントの種類が/usr/sbin/bhyveでハンドルされる必要のあるものだった場合、ioctlはリターンされ、制御が/usr/sbin/bhyveへ移ります。 /usr/sbin/bhyveはイベントの種類やレジスタの値などを参照し、デバイスエミュレーションなどの処理を行います。

今回は、この/usr/sbin/bhyveでのデバイスエミュレーション処理の部分を見ていきます。

/usr/sbin/bhyveでのI/O命令ハンドリング

前回の記事に引き続き、I/O命令でVMExitした場合について見ていきます。 VMExitに関する情報はVM_RUN ioctlの引数であるstruct vm_runのvm_exitメンバ(struct vm_exit)に書き込まれ、ioctl return時にユーザランドへコピーされます。 /usr/sbin/bhyveはこれを受け取り、vmexit->exitcodeを参照してどのようなVMExit要因だったか判定し、VMExit要因ごとの処理を呼び出します。 I/O命令でVMExitした場合のexitcodeはVM_EXIT_INOUTです。

VM_EXIT_INOUTの場合、I/Oの命令のエミュレーションに必要な情報(ポート番号、アクセス幅、書き込み値(読み込み時は不要)、I/O方向(in/out))がstruct vm_exitを介してvmm.koから渡されます。

/usr/sbin/bhyveはこの値をI/Oポートエミュレーションハンドラに渡し、I/Oポート番号からどのデバイスへのアクセスなのかを判定し、デバイスのハンドラを呼び出します。

ハンドラの実行が終わったら、/usr/sbin/bhyveはふたたびVM_RUN ioctlを発行して、ゲストマシンの実行を再開します。

では、以上のことを踏まえてソースコードの詳細を見ていきましょう。 リスト1、リスト2、リスト3、リスト4にソースコードを示します。 キャプションの丸数字で読む順番を示しています。

vmmapi.c と bhyverun.c の解説

libvmmapiはvmm.koへのioctl, sysctlを抽象化したライブラリで、/usr/sbin/bhyve, /usr/sbin/bhyvectlはこれを呼び出すことによりvmm.koへアクセスします(リスト1)。

リスト2 bhyverun.cは/usr/sbin/bhyveの中心になるコードです。

リスト1 lib/libvmmapi/vmmapi.c

......(省略)......
 280:  int
 281:  vm_run(struct vmctx *ctx, int vcpu, uint64_t rip, struct vm_exit *vmexit)
 282:  {
 283:   int error;
 284:   struct vm_run vmrun;
 285:  
 286:   bzero(&vmrun, sizeof(vmrun));
 287:   vmrun.cpuid = vcpu;
 288:   vmrun.rip = rip;
 289:  
 290:   error = ioctl(ctx->fd, VM_RUN, &vmrun);                     (1)
 291:   bcopy(&vmrun.vm_exit, vmexit, sizeof(struct vm_exit));      (2)
 292:   return (error);
 293:  }

リスト2 usr.sbin/bhyve/bhyverun.c

......(省略)......
 294:  static int
 295:  vmexit_inout(struct vmctx *ctx, struct vm_exit *vme, int *pvcpu)
 296:  {
 297:   int error;
 298:   int bytes, port, in, out;
 299:   uint32_t eax;
 300:   int vcpu;
 301:  
 302:   vcpu = *pvcpu;
 303:  
 304:   port = vme->u.inout.port;                                   (6)
 305:   bytes = vme->u.inout.bytes;
 306:   eax = vme->u.inout.eax;
 307:   in = vme->u.inout.in;
 308:   out = !in;
 309:  
......(省略)......
 322:   error = emulate_inout(ctx, vcpu, in, port, bytes, &eax, strictio);  (7)
 323:   if (error == 0 && in)                                       (16)
 324:       error = vm_set_register(ctx, vcpu, VM_REG_GUEST_RAX, eax);
 325:  
 326:   if (error == 0)
 327:       return (VMEXIT_CONTINUE);                               (17)
 328:   else {
 329:       fprintf(stderr, "Unhandled %s%c 0x%04x\n",
 330:           in ? "in" : "out",
 331:           bytes == 1 ? 'b' : (bytes == 2 ? 'w' : 'l'), port);
 332:       return (vmexit_catch_inout());
 333:   }
 334:  }
......(省略)......
 508:  static vmexit_handler_t handler[VM_EXITCODE_MAX] = {
 509:   [VM_EXITCODE_INOUT]  = vmexit_inout,                        (5)
 510:   [VM_EXITCODE_VMX]    = vmexit_vmx,
 511:   [VM_EXITCODE_BOGUS]  = vmexit_bogus,
 512:   [VM_EXITCODE_RDMSR]  = vmexit_rdmsr,
 513:   [VM_EXITCODE_WRMSR]  = vmexit_wrmsr,
 514:   [VM_EXITCODE_MTRAP]  = vmexit_mtrap,
 515:   [VM_EXITCODE_PAGING] = vmexit_paging,
 516:   [VM_EXITCODE_SPINUP_AP] = vmexit_spinup_ap,
 517:  };
 518:  
 519:  static void
 520:  vm_loop(struct vmctx *ctx, int vcpu, uint64_t rip)
 521:  {
......(省略)......
 532:   while (1) {                                                 (19)
 533:       error = vm_run(ctx, vcpu, rip, &vmexit[vcpu]);          (3)
 534:       if (error != 0) {
 535:           /*
 536:            * It is possible that 'vmmctl' or some other process
 537:            * has transitioned the vcpu to CANNOT_RUN state right
 538:            * before we tried to transition it to RUNNING.
 539:            *
 540:            * This is expected to be temporary so just retry.
 541:            */
 542:           if (errno == EBUSY)
 543:               continue;
 544:           else
 545:               break;
 546:       }
 547:  
 548:       prevcpu = vcpu;
 549:                  rc = (*handler[vmexit[vcpu].exitcode])(ctx, &vmexit[vcpu],
 550:                                                         &vcpu);       (4)
 551:       switch (rc) {
 552:                  case VMEXIT_SWITCH:
 553:           assert(guest_vcpu_mux);
 554:           if (vcpu == -1) {
 555:               stats.cpu_switch_rotate++;
 556:               vcpu = fbsdrun_get_next_cpu(prevcpu);
 557:           } else {
 558:               stats.cpu_switch_direct++;
 559:           }
 560:           /* fall through */
 561:       case VMEXIT_CONTINUE:
 562:                          rip = vmexit[vcpu].rip + vmexit[vcpu].inst_length;   (18)
 563:           break;
 564:       case VMEXIT_RESTART:
 565:                          rip = vmexit[vcpu].rip;
 566:           break;
 567:       case VMEXIT_RESET:
 568:           exit(0);
 569:       default:
 570:           exit(1);
 571:       }
 572:   }
 573:   fprintf(stderr, "vm_run error %d, errno %d\n", error, errno);
 574:  }

inout.c

inout.cはI/O命令エミュレーションを行うコードです。 実際にはI/Oポートごとの各デバイスエミュレータのハンドラを管理する役割を担っており、要求を受けるとデバイスエミュレータのハンドラを呼び出します。 呼び出されたハンドラが実際のエミュレーション処理を行います。

リスト3 usr.sbin/bhyve/inout.c

......(省略)......
  72:  int
  73:  emulate_inout(struct vmctx *ctx, int vcpu, int in, int port, int bytes,
  74:         uint32_t *eax, int strict)
  75:  {
  76:   int flags;
  77:   uint32_t mask;
  78:   inout_func_t handler;
  79:   void *arg;
  80:  
  81:   assert(port < MAX_IOPORTS);
  82:  
  83:   handler = inout_handlers[port].handler;                     (8)
  84:  
  85:   if (strict && handler == default_inout)
  86:       return (-1);
  87:  
  88:   if (!in) {
  89:       switch (bytes) {
  90:       case 1:
  91:           mask = 0xff;
  92:           break;
  93:       case 2:
  94:           mask = 0xffff;
  95:           break;
  96:       default:
  97:           mask = 0xffffffff;
  98:           break;
  99:       }
 100:       *eax = *eax & mask;
 101:   }
 102:  
 103:   flags = inout_handlers[port].flags;
 104:   arg = inout_handlers[port].arg;
 105:  
 106:   if ((in && (flags & IOPORT_F_IN)) || (!in && (flags & IOPORT_F_OUT)))
 107:       return ((*handler)(ctx, vcpu, in, port, bytes, eax, arg));  (9)
 108:   else
 109:       return (-1);
 110:  }
......(省略)......
 141:  int
 142:  register_inout(struct inout_port *iop)                       (10)
 143:  {
 144:   assert(iop->port < MAX_IOPORTS);
 145:   inout_handlers[iop->port].name = iop->name;
 146:   inout_handlers[iop->port].flags = iop->flags;
 147:   inout_handlers[iop->port].handler = iop->handler;
 148:   inout_handlers[iop->port].arg = iop->arg;
 149:  
 150:   return (0);
 151:  }

consport.c

consport.cはBHyVe専用の準仮想化コンソールドライバです。 現在はUART(Universal Asynchronous Receiver Transmitter)エミュレータが導入されたので必ずしも使う必要がなくなったのですが、デバイスエミュレータとしては最も単純な構造をしているので、デバイスエミュレータの例として取り上げました。

リスト4 usr.sbin/bhyve/inout.c

......(省略)......
  95:  static void
  96:  ttywrite(unsigned char wb)
  97:  {
  98:   (void) write(STDOUT_FILENO, &wb, 1);                        (15)
  99:  }
 100:  
 101:  static int
 102:  console_handler(struct vmctx *ctx, int vcpu, int in, int port, int bytes,
 103:       uint32_t *eax, void *arg)
 104:  {
 105:   static int opened;
 106:  
 107:   if (bytes == 2 && in) {
 108:       *eax = BVM_CONS_SIG;
 109:       return (0);
 110:   }
 111:  
 112:   if (bytes != 4)
 113:       return (-1);
 114:  
 115:   if (!opened) {
 116:       ttyopen();
 117:       opened = 1;
 118:   }
 119:   
 120:   if (in)                                                     (13)
 121:       *eax = ttyread();
 122:   else
 123:       ttywrite(*eax);                                         (14)
 124:  
 125:   return (0);
 126:  }
 127:  
 128:  static struct inout_port consport = {
 129:   "bvmcons",
 130:   BVM_CONSOLE_PORT,
 131:   IOPORT_F_INOUT,
 132:   console_handler                                             (12)
 133:  };
 134:  
 135:  void
 136:  init_bvmcons(void)
 137:  {
 138:  
 139:   register_inout(&consport);                                  (11)
 140:  }

まとめ

I/O命令によるVMExitを受けて行われるユーザランドでのエミュレーション処理について、ソースコードを解説しました。 今回までで、ハイパーバイザの実行サイクルに関するソースコードの解説を一通り行ったので、次回はvirtioのしくみについて見ていきます。

ライセンス

Copyright (c) 2014 Takuya ASADA. 全ての原稿データ は クリエイティブ・コモンズ 表示 - 継承 4.0 国際 ライセンスの下に提供されています。