LoginSignup
2

More than 5 years have passed since last update.

BitVisorのvirtio-netドライバの解説

Last updated at Posted at 2017-12-15

前回BitVisorのpro1000のドライバについて簡単に説明しました.
BitVisorではdefconfigのネットワークデバイスのドライバオプションで"virtio"を指定することで(ドライバが対応していれば)NICをvirtioとしてゲストに見せて利用することが可能です.ここではこのvirtioドライバについて簡単に見ていこうと思います.物理デバイスのドライバが関連する箇所はpro1000のドライバを想定します.

Virtio概要

仮想化環境でデバイスを提供する際,わざわざ複雑な実デバイスをエミュレーションしなくてももっと簡単な構造でVMMとやりとりできるよね,ということで考案されたのがvirtioデバイスです.virtioの基本は実デバイス同様リングバッファ状のデータ構造を用いてデータをやりとりしますが,デバイスの初期化や送信要求の仕組みなどが単純化されています.virtio自体はNICに限定されるものではなく,ブロックデバイスなどでも利用されます.

BitVisor的な観点で見ると,virtio-netはpro1000などと比べると送受信の処理が簡単になる一方で,基本ゲスト任せにしてきたデバイスの初期化などをBitVisorがやる必要がでてきます.

現在のVirtio Specificationの最新のバージョンは1.0ですが,BitVisorのvirtioドライバは0.9.xに沿っているようです.

Virtioの詳細は以下を参照してください.

送受信の主処理

virtio-netの送受信のフローの基本は他のBitVisorのドライバと同様なので,pro1000のドライバの動作を知っていれば比較的理解は容易だと思います.
まず,pro1000のドライバの動作は以下のようになっていました.

virtio_1.png

送信時はguest_is_transmitting(), 受信時はreceive_physnic()がMMIOハンドラ経由で呼ばれます.その後の処理はネットワークAPIにより決まりますが,passモジュールの場合はsend_physnic()あるいはsend_virtnic()で処理が継続されます.

一方virtio-netを使用する場合,データ送受信の流れは以下のようになります.

virtio_2.png

図のようにguest bufferと中間状態をやりとりする関数がvirtio_net.cで定義されています.あとはネットワークAPIによってpro1000の場合と同様に処理が決定されます.
virtio-netでは送信要求をI/Oポートを通じておこなうため,I/Oポートのフックによりゲストの送信をフックしています.受信処理に関しては割り込みの説明のところで詳しく説明します.

PCI Configuration Spaceの偽装

ゲストにNICをvirtioデバイスと見せるためにはPCIのconfiguration spaceをうまく誤魔化す必要があります.virtio_net_config_readでそれをおこなっています.

void
virtio_net_config_read (void *handle, u8 iosize, u16 offset, union mem *data)
{
    struct virtio_net *vnet = handle;

    replace (iosize, offset, data, 0, 4, 0x10001AF4); /* Device/Vendor
                               * ID */
    replace (iosize, offset, data, 4, 2, vnet->cmd & 0x405);
    replace (iosize, offset, data, 8, 1, 0);
    if (!vnet->multifunction)
        replace (iosize, offset, data, 0xE, 1, 0); /* Single function
                                * device */
    replace (iosize, offset, data, 0x10, 4,
         (vnet->port & ~0x1F) | PCI_CONFIG_BASE_ADDRESS_IOSPACE);
    replace (iosize, offset, data, 0x14, 4, 0);
    replace (iosize, offset, data, 0x18, 4, 0); /* Memory space
                             * for MSI-X */
    replace (iosize, offset, data, 0x1C, 4, 0);
    replace (iosize, offset, data, 0x20, 4, 0);
    replace (iosize, offset, data, 0x24, 4, 0);
    replace (iosize, offset, data, 0x2C, 4, 0x00011AF4);
    replace (iosize, offset, data, 0x34, 1, 0); /* No
                               capabilities.
                               Probably the
                               capabilities
                               bit in the
                               status register
                               should also be
                               cleared. */
    if (vnet->msix) {
        replace (iosize, offset, data, 0x34, 1, 0x40); /* Cap */
        replace (iosize, offset, data, 0x40, 1, 0x11); /* MSI-X */
        replace (iosize, offset, data, 0x41, 1, 0);    /* No next */
        replace (iosize, offset, data, 0x42, 2, 2 |    /* 3 intr */
             (vnet->msix_enabled ? 0x8000 : 0) |
             (vnet->msix_mask ? 0x4000 : 0));
        replace (iosize, offset, data, 0x44, 4, vnet->msix); /* Addr */
        replace (iosize, offset, data, 0x48, 4, vnet->msix + 0x800);
    }
}

replace()という関数が挟まっていますが,例えばreplace (iosize, offset, data, 0, 4, 0x10001AF4);ならconfiguration spaceの先頭から4byteは0x10001AF4に偽装します.これはvirtioデバイスのdevice/vendor idです.また,BAR0 (オフセット0x10)はvirtioレジスタにアクセスするためのIOポートを返していることがわかります.vnet->msixの部分はMSI-X割り込みに関する部分です(後述).

このvirtio_net_config_read()は実際にどこで呼ばれるかというと,pro1000を使う場合は以下のようにまずstruct pci_driverの中でconfiguration spaceのread時に呼ばれる関数が登録されています.

static struct pci_driver pro1000_driver = {
    .name       = driver_name,
    .longname   = driver_longname,
    .driver_options = "tty,net,virtio",
    .device     = "class_code=020000,id="
    ...
    .new        = pro1000_new,  
    .config_read    = pro1000_config_read,
    .config_write   = pro1000_config_write,
}

そしてpro1000_config_readの中でvirtioを有効にしていたらvirtio_net_config_read()を呼んでいます.

static int
pro1000_config_read (struct pci_device *pci_device, u8 iosize,
             u16 offset, union mem *data)
{
    struct data *d1 = pci_device->host;
    struct data2 *d2 = d1[0].d;

    if (d2->virtio_net) {
        pci_handle_default_config_read (pci_device, iosize, offset,
                        data);
        if (offset == 0x24) {
            pci_handle_default_config_read (pci_device, iosize,
                            0x10, data);
            if (d2->virtio_net_bar_emul)
                data->dword = pci_device->
                    base_address_mask[0];
            return CORE_IO_RET_DONE;
        }
        virtio_net_config_read (d2->virtio_net, iosize, offset, data);
        return CORE_IO_RET_DONE;
    }
    if (!d2->seize)
        return vpn_pro1000_config_read (pci_device, iosize, offset,
                        data);
    /* provide fake values 
       for reading the PCI configration space. */
    memset (data, 0, iosize);
    return CORE_IO_RET_DONE;
}

この際,最初にpci_handle_default_config_read()を呼んでいて,virtioドライバで書き換えられない部分は実デバイスのものがそのまま読み込まれます. またoffset == 0x24 の処理はMSI-X割り込みに関するところです.

読み出しと同様にvirtio_net_config_write()でconfiguration spaceの書き込みをフックして対処しています.特にBAR0のアドレスを書き換えた場合,それに応じてフックするI/Oポートを変更します.またMSI-Xの設定の対応もしています.

I/Oポートのフック

virtio_net_iohandler()でI/Oポートのアクセスをフックして適当な処理をおこなっています.
VirtioのI/O空間は以下のようになっています(virtio-netの場合).

virtio_3.png

I/O空間の先頭にvirtio headerがあり,その後デバイス特有のヘッダが続きます.
一つ注意が必要なのは,MSI-Xを使用するかどうかでvirtio headerのサイズが変わります.
MSI-Xを使用する場合はオフセット0x14にMSI-Xの情報が入ります.

virtio_net_iohandler()の先頭で,

    if (vnet->msix_enabled && port >= 0x14) {
        if (port < 0x14 + 4)
            port += 0x100;
        else
            port -= 4;
    }

という処理をおこなっていますが,これはもしMSI-Xを利用する場合,

  1. virtio-net headerへのアクセスならportを-4ずらす
  2. virtio headerのMSI-Xのアクセスならportに0x100足す

としています.こうすることでMSI-Xを利用する/しないにかかわらずその後のswitch文の処理の共通化を可能にしています.

特に,オフセット0x10へのアクセスの場合,これはQUEUE_NOTIFYになります.
書き込みデータが0の場合,つまり受信用キューに対するものの場合,vnet->ready=trueにして受信準備ができたことを設定します.
そうでない場合,(つまり送信用キューに対してのnotifyの場合),virtio_net_recv()で送信処理をおこないます.

        case 0x10:
            if (!data->byte) {
                if (!(vnet->cmd & 0x400) || vnet->msix_enabled)
                    vnet->intr_enable (vnet->intr_param);
                vnet->ready = true;
            } else {
                virtio_net_recv (vnet);
            }
            break;

割り込み処理

さて,BitVisorのvitioドライバの中でやや複雑なのが割り込み処理です.
Bitvisorのvirtio-netでは,以下の割り込みのパターンをサポートしています. 

  1. INTx
  2. MSI-X

(もともとvirtioの仕様的にサポートしているのがINTxとMSI-Xで,MSIに関してはvirtioの仕様では特に何も述べられていません)

INTxを使う場合

ゲストがINTxを使う場合はpro1000の場合と基本は同じで,割り込みは実デバイスのものをそのままパススルーします (PCI configuration spaceのInterupt Pin, Interrupt Lineをそのまま見せる).
すると割り込み発生時にゲストのvirtioドライバがI/O空間のオフセット0x13のISR status registerにアクセスしてくるので,そこでフックして適当な処理をします.このレジスタの最下位1bitが1の場合,このvirtioデバイスで割り込みが発生したことを意味します.

        case 0x13:
            vnet->intr_clear (vnet->intr_param);
            if (vnet->intr) {
                vnet->intr = false;
                data->byte = 1;
            } else if (vnet->intr2) {
                vnet->intr2 = false;
                data->byte = 1;
            } else {
                data->byte = 0;
            }
            break;

pro1000の場合,vnet->intr_clear()pro1000_intr_clear()です.

static void
pro1000_intr_clear (void *param)
{
    struct data2 *d2 = param;
    volatile u32 *icr = (void *)(u8 *)d2->d1[0].map + 0xC0;

    *icr |= 0xFFFFFFFF;
    poll_physnic (d2);
}

ここでは実デバイスのICRをクリアした後,poll_physnic()で受信処理をおこないます.
仮にpassモジュールを使用する場合は最終的にvirtio_net_send()が呼ばれることになります.

virtio_net_send()を見ると,もしパケットを実際に受信し,かつ割り込みが有効になっている場合はvnet->intrtrueにしています.これにより結果としてゲストがISRを読み込むとき1が返り,ゲストは受信処理をおこないます.
またvirtio_net_recv()を見ると,パケット送信時に割り込みが有効になっていたらvnet->intr2trueにしています.これにより送信完了の割り込みにも対応しているようです.

MSI-Xを使う場合

BitVisorでは,virtioの割り込みとしてゲストにMSI-Xを使う場合,実デバイスはMSIで割り込みをするようにしておき,BitVisorがMSIの割り込みを受けそれをMSI-Xの割り込みとしてゲストに通知する,という手法を取ります.
なんでこんなことをするかというのはBitVisro Summit5の関連スライドを是非参照していただきたいのですが,簡単に言ってしまうとそうでないと動かないデバイスがあったというのが理由のようです.

pro1000.cの中では初期化の際,virtio時にMSI-Xを使えるように,MSIの設定をおこないます.

static void 
vpn_pro1000_new (struct pci_device *pci_device, bool option_tty,
         char *option_net, bool option_virtio)
{
    ...
    if (option_virtio) {
        d2->virtio_net = virtio_net_init (&virtio_net_func,
                          d2->macaddr,
                          pro1000_intr_clear,
                          pro1000_intr_set,
                          pro1000_intr_disable,
                          pro1000_intr_enable, d2);
    }
    if (d2->virtio_net) {
        pro1000_disable_io (pci_device, d);
        d2->virtio_net_msi = pci_msi_init (pci_device, pro1000_msi,
                           d2);
        if (d2->virtio_net_msi)
            virtio_net_set_msix (d2->virtio_net, 0x5,
                         pro1000_msix_disable,
                         pro1000_msix_enable, d2);
        pci_device->driver->options.use_base_address_mask_emulation =
            0;
        net_init (d2->nethandle, d2, &phys_func, d2->virtio_net,
              virtio_net_func);
        d2->seize = true;

pci_msi_init()でMSIの設定をしています.

struct pci_msi *
pci_msi_init (struct pci_device *pci_device,
          int (*callback) (void *data, int num), void *data)
{
    u32 cmd;
    u8 cap;
    u32 capval;
    int num;
    struct pci_msi *msi;
    u32 maddr, mupper;
    u16 mdata;

    if (!pci_device)
        return NULL;
    pci_config_read (pci_device, &cmd, sizeof cmd, PCI_CONFIG_COMMAND);
    if (!(cmd & 0x100000))  /* Capabilities */
        return NULL;
    pci_config_read (pci_device, &cap, sizeof cap, 0x34); /* CAP */
    while (cap >= 0x40) {
        pci_config_read (pci_device, &capval, sizeof capval, cap);
        if ((capval & 0xFF) == 0x05)
            goto found;
        cap = capval >> 8;
    }
    return NULL;
found:
    num = exint_pass_intr_alloc (callback, data);
    if (num < 0 || num > 0xFF)
        return NULL;
    msi = alloc (sizeof *msi);
    msi->cap = cap;
    msi->dev = pci_device;
    maddr = 0xFEEFF000;
    mupper = 0;
    mdata = 0x4100 | num;
    pci_config_write (pci_device, &maddr, sizeof maddr, cap + 4);
    pci_config_write (pci_device, &mupper, sizeof mupper, cap + 8);
    pci_config_write (pci_device, &mdata, sizeof mdata, cap + 12);
    return msi;
}

MSIの設定はPCI Configuration SpaceのCapabilityのIDが0x05のフィールドでおこないます.
exint_pass_intr_alloc()を利用して,MSIの割り込み番号と,その割り込みが発生した場合に呼ばれるコールバック関数をBitVisorに登録しています.今回この関数はpro1000_msiです.
ちなみにMSIの割り込み番号は0x10~0x1Fの間で利用できるものを使います (なぜこうするかの説明はBitVisor Summit5のスライドの14ページを参照してください).

BitVisorが割り込みをフックしたとき,core/exint_pass.cdo_exint_pass()に従ってコールバック関数が呼ばれます.

void
do_exint_pass (void)
{
    ulong rflags;
    int num;

    current->vmctl.read_flags (&rflags);
    if (rflags & RFLAGS_IF_BIT) { /* if interrupts are enabled */
        num = do_externalint_enable ();
        num = exint_pass_intr_call (num);
        if (num >= 0)
            current->exint.exintfunc_default (num);
        current->vmctl.exint_pending (false);
        current->vmctl.exint_pass (!!config.vmm.no_intr_intercept);
    } else {
        current->vmctl.exint_pending (true);
        current->vmctl.exint_pass (true);
    }
}

MSIを設定したデバイスからの割り込みならpro1000_msi()が呼ばれます.

static int
pro1000_msi (void *data, int num)
{
    struct data2 *d2 = data;

    return virtio_intr (d2->virtio_net);
}

結局のところ,MSI割り込みが発生したらvirtio_intr()が呼ばれます.

int
virtio_intr (void *handle)
{
    struct virtio_net *vnet = handle;

    vnet->intr_clear (vnet->intr_param);
    if (vnet->msix_mask)
        return -1;
    if (vnet->intr2) {
        vnet->intr2 = false;
        return virtio_intrnum (vnet, 1);
    } else if (vnet->intr) {
        vnet->intr = false;
        return virtio_intrnum (vnet, 0);
    } else {
        /* Workaround for Windows driver... */
        return virtio_intrnum (vnet, 0);
    }
}

最初にvnet->intr_clear()を呼び,INTxの場合と同様にパケットの受信処理がおこなわれます.
その後,virtio_internum()を通じてMSI-Xの割り込み番号を返します.

static int
virtio_intrnum (struct virtio_net *vnet, int queue)
{
    u16 vec = vnet->msix_quevec[queue];

    if (vec < 3 && !(vnet->msix_table_entry[vec].mask & 1) &&
        !vnet->msix_table_entry[vec].upper &&
        (vnet->msix_table_entry[vec].addr & 0xFFF00000) == 0xFEE00000) {
        if (0)
            printf ("virtio intr %d %08X%08X, %08X\n", queue,
                vnet->msix_table_entry[vec].upper,
                vnet->msix_table_entry[vec].addr,
                vnet->msix_table_entry[vec].data);
        return vnet->msix_table_entry[vec].data & 0xFF;
    }
    return -1;
}

ちなみに,MSI-Xを利用する場合は割り込みが発生してもゲストはISRにアクセスしません.
さて,それではゲストドライバはMSI-Xをどう設定しているでしょうか.
MSI-XはConfiguration SpaceのCapability IDが0x40のフィールドで設定をおこないます.
そこで,virito_net_config_read()では以下のようにMSI-X用のcapabilityフィールドを用意していました.

void
virtio_net_config_read (void *handle, u8 iosize, u16 offset, union mem *data)
{
    ...
    if (vnet->msix) {
        replace (iosize, offset, data, 0x34, 1, 0x40); /* Cap */
        replace (iosize, offset, data, 0x40, 1, 0x11); /* MSI-X */
        replace (iosize, offset, data, 0x41, 1, 0);    /* No next */
        replace (iosize, offset, data, 0x42, 2, 2 |    /* 3 intr */
             (vnet->msix_enabled ? 0x8000 : 0) |
             (vnet->msix_mask ? 0x4000 : 0));
        replace (iosize, offset, data, 0x44, 4, vnet->msix); /* Addr */
        replace (iosize, offset, data, 0x48, 4, vnet->msix + 0x800);
    }

MSI-XがMSIと異なるのは,割り込みの発生方法やアドレスを設定するテーブルが直接configuration spaceにあるのではなく,BARで指定されるMMIO空間にあるという点です.上のコードでは /* Addr */とコメントしてある行のvnet->msixがMSI-X TableがおかれるBARのインデックスを返します.

このBARの領域をどうしているかというと,pro1000ではとりあえずBAR5を利用しています.
virtio_net_set_msixの引数でvnet->msixが0x5に初期化されていることが確認できます.

そして,ゲストのドライバがBAR5のアドレスを読み込もうとした際,実デバイスのBAR0のアドレスを返します.

static int
pro1000_config_read (struct pci_device *pci_device, u8 iosize,
             u16 offset, union mem *data)
{
    ...
    if (d2->virtio_net) {
        ...
        if (offset == 0x24) {
            pci_handle_default_config_read (pci_device, iosize,
                            0x10, data);
            ...
        }
...

つまり,実デバイスのBAR0のMMIO空間を流用するわけです.
pro1000のmmhandlerの中でvirtioが有効の場合は,偽装したMSI-Xのテーブルエントリを返すようになっています.

static int
mmhandler (void *data, phys_t gphys, bool wr, void *buf, uint len, u32 flags)
{
    struct data *d1 = data;
    struct data2 *d2 = d1->d;

    if (d2->virtio_net && d2->virtio_net_msi && d1 == &d2->d1[0]) {
        virtio_net_msix (d2->virtio_net, wr, len, gphys - d1->mapaddr,
                 buf);
        return 1;
    }
...
void
virtio_net_msix (void *handle, bool wr, u32 iosize, u32 offset,
         union mem *data)
{
    struct virtio_net *vnet = handle;

    if (offset < sizeof vnet->msix_table_entry) {
        void *p = vnet->msix_table_entry;
        u32 end = offset + iosize;
        if (end > sizeof vnet->msix_table_entry)
            end = sizeof vnet->msix_table_entry;
        if (wr)
            memcpy (p + offset, data, end - offset);
        else
            memcpy (data, p + offset, end - offset);
        if (0 && wr)
            printf ("MSI-X[0x%04X] = 0x%08X\n", offset,
                data->dword & ((2u << (iosize * 8 - 1)) - 1));
    } else if (offset <= 0x800 && offset + iosize > 0x800) {
        if (!wr) {
            memset (data, 0, iosize);
            (&data->byte)[0x800 - offset] = 0;
        }
    }
}

個人的になかなか芸術的な動作だと思います.

初期化の流れ

以上がおおまかなvirtioの動作ですが,最後に初期化について少し書いておきます.
vpn_pro1000_new()をしたとき,MSIの設定をするのはさきほど書いた通りですが,virtioを使用する場合はd2->sizetrueになります.そしてd2->sizeがtrueのときはseize_pro1000によりデバイスを初期化しています.

また,net_init()virtio_net_funcを渡すことでネットワークAPIのコールバック関数からvirtioの関数が呼ばれるようになります.

static void 
vpn_pro1000_new (struct pci_device *pci_device, bool option_tty,
         char *option_net, bool option_virtio)
{
    ...
    if (option_virtio) {
        d2->virtio_net = virtio_net_init (&virtio_net_func,
                          d2->macaddr,
                          pro1000_intr_clear,
                          pro1000_intr_set,
                          pro1000_intr_disable,
                          pro1000_intr_enable, d2);
    }
    if (d2->virtio_net) {
        pro1000_disable_io (pci_device, d);
        d2->virtio_net_msi = pci_msi_init (pci_device, pro1000_msi,
                           d2);
        if (d2->virtio_net_msi)
            virtio_net_set_msix (d2->virtio_net, 0x5,
                         pro1000_msix_disable,
                         pro1000_msix_enable, d2);
        pci_device->driver->options.use_base_address_mask_emulation =
            0;
        net_init (d2->nethandle, d2, &phys_func, d2->virtio_net,
              virtio_net_func);
        d2->seize = true;
    } else {
    ...
    }
    if (d2->seize) {
        pci_system_disconnect (pci_device);
        /* Enabling bus master and memory space again because
         * they might be disabled after disconnecting firmware
         * drivers. */
        pro1000_enable_dma_and_memory (pci_device);
        seize_pro1000 (d2);
        net_start (d2->nethandle);
    }
    ...
}

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
What you can do with signing up
2