Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

3.3. virtio-netドライバ

QEMUが提供する「virtio-net」デバイスのドライバを作成します。

Note

virtio-netをはじめとするvirtioデバイスは、物理的なハードウェアをソフトウェアで完全にエミュレートするのとは異なり、ゲストOS(のドライバ)が相手を仮想デバイスだと認識した上でハイパーバイザーと協調して効率的に動作するように設計されています。

Note

RISC-V版のxv6では、ディスク装置にvirtio-diskを利用しており、既にvirtioデバイスを扱うためのコードが含まれています。これを参考にして、新たにvirtio-netに対応させていきます。

virtio-netデバイスの接続

まず、QEMUを起動した際にゲストであるxv6に対してvirtio-netデバイスが提供されるようにMakefileを修正します。

📝 Makefile

...

 ifndef CPUS
 CPUS := 3
 endif
 
+TAPDEV=tap0
+TAPADDR=192.0.2.1/24
+
 QEMUOPTS = -machine virt -bios none -kernel $K/kernel -m 128M -smp $(CPUS) -nographic
 QEMUOPTS += -global virtio-mmio.force-legacy=false
 QEMUOPTS += -drive file=fs.img,if=none,format=raw,id=x0
 QEMUOPTS += -device virtio-blk-device,drive=x0,bus=virtio-mmio-bus.0

+QEMUOPTS += -netdev tap,ifname=$(TAPDEV),id=en0,script=no,downscript=no
+QEMUOPTS += -device virtio-net-device,netdev=en0,csum=off,gso=off,guest_csum=off,bus=virtio-mmio-bus.1
+
+tap:
+	@ip addr show $(TAPDEV) 2>/dev/null || (echo "Create '$(TAPDEV)'"; \
+		sudo ip tuntap add mode tap user $(USER) name $(TAPDEV); \
+		sudo sysctl -w net.ipv6.conf.$(TAPDEV).disable_ipv6=1; \
+		sudo ip addr add $(TAPADDR) dev $(TAPDEV); \
+		sudo ip link set $(TAPDEV) up; \
+		ip addr show $(TAPDEV); \
+	)
+
-qemu: check-qemu-version $K/kernel fs.img
+qemu: check-qemu-version $K/kernel fs.img tap
 	$(QEMU) $(QEMUOPTS)

...

Note

QEMUの起動オプションを記述するQEMUOPTSに対してvirtio-netを用いたネットワークデバイスを追加する設定を加えます。このネットワークデバイスはホスト側のTapデバイスに接続するようにします。

Note

virtioのトランスポート仕様にはPCIとMMIOがありますが、ここではPCIよりも実装しやすいMMIOを採用しています。

Note

QEMUは起動時にTapデバイスを作成しますが、IPアドレスの設定まではやってくれません。make qemuを実行した際にTapデバイスが存在しなければ作成してIPアドレスを設定する処理も合わせて追加しています。

再ビルドした後、make qemuを実行してxv6を起動させます。

Note

QEMUを起動する前にTapデバイスを作成してIPアドレスが設定されるはずです。

Create 'tap0'
net.ipv6.conf.tap0.disable_ipv6 = 1
79: tap0: <NO-CARRIER,BROADCAST,MULTICAST,UP> mtu 1500 qdisc fq_codel state DOWN group default qlen 1000
    link/ether 16:2a:af:19:b5:91 brd ff:ff:ff:ff:ff:ff
    inet 192.0.2.1/24 scope global tap0
       valid_lft forever preferred_lft forever
...

Note

続けてQEMUが起動してくれば大丈夫です。起動に失敗した場合はQEMUOPTへの追加内容にミスがないか確認してください。

xv6 kernel is booting

2026/08/11 10:00:00
10:00:00.101 [I] net_init: initialize... (kernel/net/net.c:277)
10:00:00.102 [I] net_init: success (kernel/net/net.c:302)
10:00:00.103 [I] net_run: startup... (kernel/net/net.c:311)
10:00:00.104 [I] net_run: success (kernel/net/net.c:319)
hart 1 starting
hart 2 starting
init: starting sh
$

MMIOアドレスのマッピング

QEMUが提供するvirtio-netデバイスをxv6で扱えるようにするための設定を行います。これは同じMMIOデバイスであるGoldfish RTCを扱えるようにした時と同じ作業です。

Note

Goldfish RTCと同様にvirtioデバイスの物理アドレスもQEMU側で決められており、1つめのvirtioデバイスが0x10001000、2つめのvirtioデバイスが0x10002000に配置されます。

📝 kernel/memlayout.h

 // 0C000000 -- PLIC
 // 10000000 -- uart0
 // 10001000 -- virtio disk
+// 10002000 -- virtio network
 // 80000000 -- qemu's boot ROM loads the kernel here,
 //             then jumps here.
 // unused RAM after 80000000.

...

 // virtio mmio interface
 #define VIRTIO0     0x10001000
 #define VIRTIO0_IRQ 1
+#define VIRTIO1     0x10002000
+#define VIRTIO1_IRQ 2

...

📝 kernel/vm.c

...
 pagetable_t
 kvmmake(void)
 {
...
   // virtio mmio disk interface
   kvmmap(kpgtbl, VIRTIO0, VIRTIO0, PGSIZE, PTE_R | PTE_W);
 
+  // virtio mmio network interface
+  kvmmap(kpgtbl, VIRTIO1, VIRTIO1, PGSIZE, PTE_R | PTE_W);
+
   // PLIC
   kvmmap(kpgtbl, PLIC, PLIC, 0x4000000, PTE_R | PTE_W);
...
 }
...

virtio-netデバイスのドライバを追加

ドライバのコードはplatform/xv6-riscvの下にdriverディレクトリを作成して配置します。

$ mkdir -p kernel/net/platform/xv6-riscv/driver

まず、ドライバが外部へ公開する関数の宣言を持つヘッダファイルを追加します。

📝 kernel/net/platform/xv6-riscv/driver/virtio_net.h

#ifndef VIRTIO_NET_H
#define VIRTIO_NET_H

#include "net.h"

extern struct net_device *
virtio_net_init(void);

#endif

続いてドライバの本体です。

📝 kernel/net/platform/xv6-riscv/driver/virtio_net.c

#include <stdio.h>
#include <stdint.h>
#include <string.h>

#include "platform.h"

#include "memlayout.h"
#include "virtio.h"

#include "util.h"
#include "net.h"
#include "ether.h"

#include "driver/virtio_net.h"

#define R(r) ((volatile uint32_t *)(VIRTIO1 + (r)))

/*
 * virtq
 */

struct virtq {
    struct virtq_desc *desc;
    struct virtq_avail *avail;
    struct virtq_used *used;
    int num;
    int last_used_idx;
    char *free;
};

static void
virtq_init(struct virtq *vq, int sel, int num)
{
    uint32_t max;

    // select queue
    *R(VIRTIO_MMIO_QUEUE_SEL) = sel;

    // ensure selected queue is not in use.
    if (*R(VIRTIO_MMIO_QUEUE_READY)) {
        panic("queue already in use");
    }

    // check maximum queue size.
    max = *R(VIRTIO_MMIO_QUEUE_NUM_MAX);
    if (max == 0) {
        panic("queue not available");
    }
    if (max < num) {
        panic("queue too short");
    }

    // allocate and zero queue memory.
    vq->desc = kalloc();
    vq->avail = kalloc();
    vq->used = kalloc();
    vq->free = kalloc();
    if (!vq->desc || !vq->avail || !vq->used || !vq->free) {
        panic("kalloc failed");
    }
    memset(vq->desc, 0, PGSIZE);
    memset(vq->avail, 0, PGSIZE);
    memset(vq->used, 0, PGSIZE);
    memset(vq->free, 0,  PGSIZE);

    // set queue size.
    vq->num = num;
    *R(VIRTIO_MMIO_QUEUE_NUM) = vq->num;

    // write physical addresses.
    *R(VIRTIO_MMIO_QUEUE_DESC_LOW) = (uint64_t)vq->desc;
    *R(VIRTIO_MMIO_QUEUE_DESC_HIGH) = (uint64_t)vq->desc >> 32;
    *R(VIRTIO_MMIO_DRIVER_DESC_LOW) = (uint64_t)vq->avail;
    *R(VIRTIO_MMIO_DRIVER_DESC_HIGH) = (uint64_t)vq->avail >> 32;
    *R(VIRTIO_MMIO_DEVICE_DESC_LOW) = (uint64_t)vq->used;
    *R(VIRTIO_MMIO_DEVICE_DESC_HIGH) = (uint64_t)vq->used >> 32;

    // queue is ready.
    *R(VIRTIO_MMIO_QUEUE_READY) = 1;

    // all descriptors start out unused.
    for (int i = 0; i < vq->num; i++) {
        vq->free[i] = 1;
    }
    vq->last_used_idx = 0;
}

// find a free descriptor, mark it non-free, return its index.
static int
virtq_alloc_desc(struct virtq *vq)
{
    for (int i = 0; i < vq->num; i++) {
        if (vq->free[i]) {
            vq->free[i] = 0;
            return i;
        }
    }
    return -1;
}

// mark a descriptor as free.
static void
virtq_free_desc(struct virtq *vq, int i)
{
    if (i >= vq->num) {
        panic("virtq_free_desc: invalid index");
    }
    if (vq->free[i]) {
        panic("virtq_free_desc: freeing free descriptor");
    }
    vq->desc[i].addr = 0;
    vq->desc[i].len = 0;
    vq->desc[i].flags = 0;
    vq->desc[i].next = 0;
    vq->free[i] = 1;
}

/*
 * virtio-net
 */

#define VIRTIO_NET_IRQ VIRTIO1_IRQ

#define RXQ 0
#define TXQ 1

#define QSIZE NUM
#define BUF_SIZE 2048

#define VIRTIO_MMIO_CONFIG 0x100
#define VIRTIO_NET_F_CSUM 0
#define VIRTIO_NET_F_GUEST_CSUM 1
#define VIRTIO_NET_F_MAC 5

struct virtio_net {
    struct net_device *dev;
    uint32_t status;
    uint64_t features;
    lock_t lock;
    struct virtq rx_q;
    struct virtq tx_q;
    char rx_bufs[QSIZE][BUF_SIZE];
    char tx_bufs[QSIZE][BUF_SIZE];
} _nic0;

struct virtio_net_hdr {
#define VIRTIO_NET_HDR_F_NEEDS_CSUM 1
#define VIRTIO_NET_HDR_F_DATA_VALID 2
#define VIRTIO_NET_HDR_F_RSC_INFO 4
    uint8_t flags;
#define VIRTIO_NET_HDR_GSO_NONE 0
#define VIRTIO_NET_HDR_GSO_TCPV4 1
#define VIRTIO_NET_HDR_GSO_UDP 3
#define VIRTIO_NET_HDR_GSO_TCPV6 4
#define VIRTIO_NET_HDR_GSO_ECN 0x80
    uint8_t gso_type;
    uint16_t hdr_len;
    uint16_t gso_size;
    uint16_t csum_start;
    uint16_t csum_offset;
    uint16_t num_buffers;
};

#define PRIV(x) ((struct virtio_net *)(x)->priv)

static int
virtio_net_open(struct net_device *dev)
{
    struct virtio_net *nic = PRIV(dev);
    char addr[ETHER_ADDR_STR_LEN];

    lock_acquire(&nic->lock);

    // set receive buffers
    for (int i = 0; i < QSIZE; i++) {
        nic->rx_q.desc[i].addr = (uint64_t)nic->rx_bufs[i];
        nic->rx_q.desc[i].len = BUF_SIZE;
        nic->rx_q.desc[i].flags = VRING_DESC_F_WRITE;
        nic->rx_q.avail->ring[i] = i;
        nic->rx_q.free[i] = 0;
    }
    __atomic_thread_fence(__ATOMIC_SEQ_CST);
    nic->rx_q.avail->idx = QSIZE;
    nic->rx_q.last_used_idx = 0;

    // tell device we're completely ready.
    nic->status |= VIRTIO_CONFIG_S_DRIVER_OK;
    *R(VIRTIO_MMIO_STATUS) = nic->status;

    // Notify the device of new RX buffers.
    *R(VIRTIO_MMIO_QUEUE_NOTIFY) = RXQ;

    lock_release(&nic->lock);

    infof("dev=%s, addr=%s", dev->name, ether_addr_ntop(dev->addr, addr, sizeof(addr)));
    return 0;
}

static int
virtio_net_close(struct net_device *dev)
{
    struct virtio_net *nic = PRIV(dev);

    lock_acquire(&nic->lock);

    nic->status = *R(VIRTIO_MMIO_STATUS);

    // clear DRIVER_OK bit
    nic->status &= ~VIRTIO_CONFIG_S_DRIVER_OK;
    *R(VIRTIO_MMIO_STATUS) = nic->status;

    lock_release(&nic->lock);

    infof("dev=%s", dev->name);
    return 0;
}

static ssize_t
virtio_net_write(struct net_device *dev, const uint8_t *frame, size_t flen)
{
    struct virtio_net *nic = PRIV(dev);
    int idx;
    struct virtio_net_hdr *hdr;

    if (sizeof(*hdr) + flen > BUF_SIZE) {
        errorf("frame too long, dev=%s, flen=%zu", dev->name, flen);
        return -1;
    }

    lock_acquire(&nic->lock);

    // Allocate descriptor
    idx = virtq_alloc_desc(&nic->tx_q);
    if (idx == -1) {
        lock_release(&nic->lock);
        errorf("no free descriptor, dev=%s", dev->name);
        return -1;
    }

    // Setup virtio-net header.
    hdr = (struct virtio_net_hdr *)nic->tx_bufs[idx];
    memset(hdr, 0, sizeof(*hdr));
    hdr->gso_type = VIRTIO_NET_HDR_GSO_NONE;
    memcpy(hdr+1, frame, flen);

    // Configure descriptor
    nic->tx_q.desc[idx].addr = (uint64_t)nic->tx_bufs[idx];
    nic->tx_q.desc[idx].len = sizeof(*hdr) + flen;
    nic->tx_q.desc[idx].flags = 0; // read by device

    // Deploy descriptor in the available ring.
    nic->tx_q.avail->ring[nic->tx_q.avail->idx % nic->tx_q.num] = idx;
    __atomic_thread_fence(__ATOMIC_SEQ_CST);
    nic->tx_q.avail->idx++;
    __atomic_thread_fence(__ATOMIC_SEQ_CST);

    lock_release(&nic->lock);

    // Notify the device of a new TX packet.
    *R(VIRTIO_MMIO_QUEUE_NOTIFY) = TXQ;

    return flen;
}

static int
virtio_net_output(struct net_device *dev, uint16_t type, const uint8_t *buf, size_t len, const void *dst)
{
    uint8_t frame[ETHER_FRAME_SIZE_MAX] = {};
    struct ether_hdr *hdr;
    size_t flen, pad = 0;

    hdr = (struct ether_hdr *)frame;
    memcpy(hdr->dst, dst, ETHER_ADDR_LEN);
    memcpy(hdr->src, dev->addr, ETHER_ADDR_LEN);
    hdr->type = hton16(type);
    memcpy(hdr+1, buf, len);
    if (len < ETHER_PAYLOAD_SIZE_MIN) {
        pad = ETHER_PAYLOAD_SIZE_MIN - len;
    }
    flen = sizeof(*hdr) + len + pad;
    debugf("dev=%s, type=0x%04x, len=%zu", dev->name, type, flen);
    ether_print(frame, flen);
    if (virtio_net_write(dev, frame, flen) == -1) {
        return -1;
    }
    return 0;
}

static int
virtio_net_input(struct net_device *dev, uint8_t *frame, size_t flen)
{
    struct ether_hdr *hdr;
    uint16_t type;

    if (flen < (ssize_t)sizeof(*hdr)) {
        errorf("too short");
        return -1;
    }
    hdr = (struct ether_hdr *)frame;
    if (memcmp(dev->addr, hdr->dst, ETHER_ADDR_LEN) != 0) {
        if (memcmp(ETHER_ADDR_BROADCAST, hdr->dst, ETHER_ADDR_LEN) != 0) {
            /* for other host */
            return -1;
        }
    }
    type = ntoh16(hdr->type);
    debugf("dev=%s, type=0x%04x, len=%zd", dev->name, type, flen);
    ether_print(frame, flen);
    return net_input(type, (uint8_t *)(hdr+1), flen - sizeof(*hdr), dev);
}

static void
virtio_net_isr(unsigned int irq, void *arg)
{
    struct net_device *dev;
    struct virtio_net *nic;

    (void)irq;
    dev = (struct net_device *)arg;
    nic = PRIV(dev);

    lock_acquire(&nic->lock);

    // Acknowledge the interrupt and clear the status by writing it back.
    *R(VIRTIO_MMIO_INTERRUPT_ACK) = *R(VIRTIO_MMIO_INTERRUPT_STATUS) & 0x3;
    __atomic_thread_fence(__ATOMIC_SEQ_CST);

    // Process completed descriptors from the tx used ring.
    while (nic->tx_q.last_used_idx != nic->tx_q.used->idx) {
        int ring_idx = nic->tx_q.last_used_idx % nic->tx_q.num;
        int idx = nic->tx_q.used->ring[ring_idx].id;
        virtq_free_desc(&nic->tx_q, idx);
        nic->tx_q.last_used_idx++;
    }

    // Process incoming packets from the rx used ring.
    while (nic->rx_q.last_used_idx != nic->rx_q.used->idx) {
        int ring_idx = nic->rx_q.last_used_idx % nic->rx_q.num;
        int idx = nic->rx_q.used->ring[ring_idx].id;
        int len = nic->rx_q.used->ring[ring_idx].len;
        int hdrlen = sizeof(struct virtio_net_hdr);
        if (len < hdrlen || len - hdrlen > ETHER_FRAME_SIZE_MAX) {
            errorf("invalid length, dev=%s, len=%d", dev->name, len);
        } else {
            virtio_net_input(dev, (uint8_t *)nic->rx_bufs[idx] + hdrlen, len - hdrlen);
        }
        // Recycle the receive buffer descriptor.
        nic->rx_q.desc[idx].addr = (uint64_t)nic->rx_bufs[idx];
        nic->rx_q.desc[idx].len = BUF_SIZE;
        nic->rx_q.avail->ring[nic->rx_q.avail->idx % nic->rx_q.num] = idx;
        __atomic_thread_fence(__ATOMIC_SEQ_CST);
        nic->rx_q.avail->idx++;
        nic->rx_q.last_used_idx++;
    }
    __atomic_thread_fence(__ATOMIC_SEQ_CST);

    lock_release(&nic->lock);

    // Notify the device of new RX buffers.
    *R(VIRTIO_MMIO_QUEUE_NOTIFY) = RXQ;
}

static struct net_device_ops virtio_net_ops = {
    .open = virtio_net_open,
    .close = virtio_net_close,
    .output = virtio_net_output,
};

struct net_device *
virtio_net_init(void)
{
    struct virtio_net *nic = &_nic0;
    struct net_device *dev;

    lock_init(&nic->lock);

    // find virtio-net device
    if (*R(VIRTIO_MMIO_MAGIC_VALUE) != 0x74726976 ||
        *R(VIRTIO_MMIO_VERSION) != 2 ||
        *R(VIRTIO_MMIO_DEVICE_ID) != 1 || // network device
        *R(VIRTIO_MMIO_VENDOR_ID) != 0x554d4551) {
        errorf("device not found");
        return NULL;
    }

    debugf("device found");

    // reset device
    nic->status = 0;
    *R(VIRTIO_MMIO_STATUS) = nic->status;

    // set ACKNOWLEDGE status bit
    nic->status |= VIRTIO_CONFIG_S_ACKNOWLEDGE;
    *R(VIRTIO_MMIO_STATUS) = nic->status;

    // set DRIVER status bit
    nic->status |= VIRTIO_CONFIG_S_DRIVER;
    *R(VIRTIO_MMIO_STATUS) = nic->status;

    // negotiate features
    nic->features = *R(VIRTIO_MMIO_DEVICE_FEATURES);
    nic->features &= ~(1ULL << VIRTIO_RING_F_EVENT_IDX);
    nic->features &= ~(1ULL << VIRTIO_NET_F_CSUM);
    nic->features &= ~(1ULL << VIRTIO_NET_F_GUEST_CSUM);
    *R(VIRTIO_MMIO_DRIVER_FEATURES) = nic->features;

    // tell device that feature negotiation is complete.
    nic->status |= VIRTIO_CONFIG_S_FEATURES_OK;
    *R(VIRTIO_MMIO_STATUS) = nic->status;

    // re-read status to ensure FEATURES_OK is set.
    if (!(*R(VIRTIO_MMIO_STATUS) & VIRTIO_CONFIG_S_FEATURES_OK)) {
        errorf("FEATURES_OK failed");
        return NULL;
    }

    // initialize TXQ/RXQ
    virtq_init(&nic->tx_q, TXQ, QSIZE);
    virtq_init(&nic->rx_q, RXQ, QSIZE);

    // setup device driver structure
    dev = net_device_alloc();
    if (!dev) {
        errorf("net_device_alloc() failure");
        return NULL;
    }
    dev->type = NET_DEVICE_TYPE_ETHERNET;
    dev->mtu = ETHER_PAYLOAD_SIZE_MAX;
    dev->flags = (NET_DEVICE_FLAG_BROADCAST | NET_DEVICE_FLAG_NEED_ARP);
    dev->hlen = ETHER_HDR_SIZE;
    dev->alen = ETHER_ADDR_LEN;
    memcpy(dev->broadcast, ETHER_ADDR_BROADCAST, ETHER_ADDR_LEN);
    if (nic->features & (1 << VIRTIO_NET_F_MAC)) {
        for (int i = 0; i < ETHER_ADDR_LEN; i++) {
            dev->addr[i] = *(volatile uint8_t *)((uint64_t)(VIRTIO1 + VIRTIO_MMIO_CONFIG + i));
        }
    } else {
        errorf("device does not provide a MAC address");
        return NULL;
    }
    dev->ops = &virtio_net_ops;
    dev->priv = nic;
    if (net_device_register(dev) == -1) {
        errorf("net_device_register() failure");
        return NULL;
    }
    nic->dev = dev;
    if (intr_register(VIRTIO_NET_IRQ, virtio_net_isr, INTR_IRQ_SHARED, dev) == -1) {
        errorf("intr_register() failure");
        return NULL;
    }
    return dev;
}

Note

virtioデバイスの初期化の流れ。

  • STATUSレジスタに0を書き込んでデバイスをリセットする。
  • STATUSレジスタにACKNOWLEDGEビットをセットする。
  • STATUSレジスタにDRIVERビットをセットする。
  • デバイスとドライバの間で機能をネゴシエーションする。
    • DEVICE_FEATURESレジスタを読んでデバイスが提供する機能を取得する。
    • DEVICE_FEATURESで提供された機能のうち、使用する機能だけを選び(使わない機能を落として)DRIVER_FEATURESにセットする。
  • STATUSレジスタにFEATURES_OKをセットする。
  • STATUSレジスタを読み取ってFEATURES_OKがセットされていることを確認する。
    • FEATURES_OKがセットされていなかったらネゴシエーション失敗。
  • 送受信キューの初期化
  • 受信キューにパケットを格納するためのバッファを設定
  • STATUSレジスタにDRIVER_OKをセットする。

Note

ネゴシエーション時の注意点。

  • VIRTIO_RING_F_EVENT_IDXフラグを取り除いています。このフラグが設定されているとイベント通知が最適化され、ドライバ側で適切に対処しないと次のイベント通知が受け取れなくなってしまいます。
  • チェックサムオフロード関連のフラグを取り除いています。VIRTIO_NET_F_CSUM(送信オフロード)とVIRTIO_NET_F_GUEST_CSUM(受信オフロード)をいずれも無効化しています。これらのオフロードが有効だと、デバイスがチェックサムの計算・検証を肩代わりし、パケットがチェックサム未計算のままドライバへ渡されることがあります。その状態だと自作プロトコルスタックのチェックサム検証(IP/ICMP/UDP/TCP)が成立しません。あわせて、QEMUの起動オプションでもcsum=off,guest_csum=offを指定し、デバイス側でオフロードを提供しない設定にしています。

Note

送受信キュー初期化の流れ。

  • QUEUE_SELレジスタに初期化したいキューのインデックス(0から始まる番号)を書き込む。
  • QUEUE_READYレジスタを読み取る
    • 読み取った値が0でなかったらそのキューは既に初期化済みで使用されているので失敗。
  • QUEUE_NUM_MAXレジスタを読み取ってデバイスがサポートするキューの最大サイズを取得する
    • 値が0の場合、そのインデックスのキューは存在しない。
  • キューを構成する3つの領域(Descriptor Table, Available Ring, Used Ring)それぞれにメモリを確保する。モダンなvirtio(MMIOバージョン2)では領域ごとに独立した物理アドレスをレジスタで通知できるため、3つの領域が物理的に連続している必要はない(本ドライバも各領域を個別にkalloc()している)。
  • QUEUE_NUMレジスタにドライバが使用するキューのサイズ(ディスクリプタ数)を書き込む。
  • 確保した各領域の物理アドレスを、対応するレジスタに書き込んでデバイスに通知する。
  • QUEUE_READYレジスタに1を書き込み、キューを有効化する。

Note

virtioのキューを構成する3つの領域について。

  • Descriptor Table
    • 送受信したいデータの実体(パケットなど)がメモリのどこにあるのか、そのアドレスとサイズを保持する。
  • Available Ring
    • ドライバは処理してほしいディスクリプタの番号をこのリングに書き込み、デバイスに新しく処理を要求する。
  • Used Ring
    • デバイスは処理が終わったディスクリプタの番号をこのリングに書き込み、ドライバに処理が完了したことを通知する。

プロトタイプ宣言の追加

追加した関数のプロトタイプ宣言をkernel/defs.hに追加します。

📝 kernel/defs.h

...

 struct spinlock;
 struct sleeplock;
 struct stat;
 struct superblock;
+struct net_device;
 struct timeval;
 struct tm;

...

 // net/net.c
 int             net_init(void);
 int             net_run(void);
 
+// net/platform/xv6-riscv/driver/virtio_net.c
+struct net_device* virtio_net_init(void);
+
 // number of elements in fixed-size array
 #define NELEM(x) (sizeof(x) / sizeof((x)[0]))

ドライバの初期化関数の呼び出し

kernel/main.cmain()virtio_net_init()の呼び出しを追加します。追加する場所はnet_init()net_run()のあいだです。

📝 kernel/main.c

...

     virtio_disk_init(); // emulated hard disk
     printdate();
     net_init();         // network stack
+    virtio_net_init();  // emulated network card
     net_run();          // start networking
     userinit();         // first user process

...

Makefileの修正

ソースファイルを追加したのでMakefileを修正します。

📝 Makefile

 OBJS = \
...
   $P/platform.o \
   $P/intr.o \
+  $P/driver/virtio_net.o \
   $L/stdio.o \
   $L/stdlib.o

...

--include $K/*.d $U/*.d $N/*.d $P/*.d $L/*.d
+-include $K/*.d $U/*.d $N/*.d $P/*.d $P/driver/*.d $L/*.d

 clean: 
 	rm -f *.tex *.dvi *.idx *.aux *.log *.ind *.ilg \
 	*/*.o */*.d */*.asm */*.sym \
-	$N/*.o $N/*.d $P/*.o $P/*.d $L/*.o $L/*.d \
+	$N/*.o $N/*.d $P/*.o $P/*.d $P/driver/*.o $P/driver/*.d $L/*.o $L/*.d \
 	$K/kernel fs.img \

...

動作確認

再ビルドした後、make qemuを実行してxv6を起動させます。

xv6 kernel is booting

2026/08/11 10:00:00
10:00:00.101 [I] net_init: initialize... (kernel/net/net.c:277)
10:00:00.102 [I] net_init: success (kernel/net/net.c:302)
10:00:00.103 [D] virtio_net_init: device found (kernel/net/platform/xv6-riscv/driver/virtio_net.c:394)
10:00:00.104 [I] net_device_register: success, dev=net0, type=0x0002 (kernel/net/net.c:58)
10:00:00.105 [I] net_run: startup... (kernel/net/net.c:311)
10:00:00.106 [I] virtio_net_open: dev=net0, addr=52:54:00:12:34:56 (kernel/net/platform/xv6-riscv/driver/virtio_net.c:195)
10:00:00.107 [I] net_run: success (kernel/net/net.c:319)
hart 1 starting
hart 2 starting
init: starting sh
$

Note

出力されているログからvirtio-netデバイスの初期化とプロトコルスタックへの登録に成功していることが分かります。

Important

この時点ではまだパケットを受信できません。受信の割り込みハンドラ(virtio_net_isr())はintr_register()で登録済みですが、それを呼び出すハードウェア割り込みの仕組み(PLICの設定とトラップからのディスパッチ)は次の3.4で実装します。それまでは、デバイスが受信キューにパケットを積んで割り込みを上げても、virtio_net_isr()が起動されないためパケットは処理されません。実際にパケットを受信できるようになるのは3.4を終えてからです。