4
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

MikanOS に NIC ドライバを実装する - 送受信編

Last updated at Posted at 2021-12-21

この記事は 自作OS Advent Calendar 21日目の記事です。

前回のおさらい

前回の初期化編では NIC の設定と初期化を行い、ドライバから NIC を操作できるようにしました。今回はついにそれを利用してバッファの送受信を行ってみましょう!

今回やること

送信処理、受信処理の順に書いていきます。受信処理では、バッファの受信はポーリングで行います。割り込みで受信するようにするのは次回に行います。

送信処理

Nic::Send の実装

NicクラスにSendメソッドを実装していきます。実装の概要は準備編で見た通りです。

  • 送信するバッファのアドレスと長さを取得する。
  • アドレスと長さを TAIL が指す送信ディスクリプタに記述する。
  • 送信ディスクリプタのその他のフィールド (command, status) を適切に初期化する。
  • TAIL をインクリメントする。

実装は以下の通りです。

kernel/net/nic/e1000.cpp
uint8_t Nic::Send(void *buf, uint16_t length) {
    // ディスクリプタリングを書き換える
    t_descriptor *desc = &this->desc_ring_addr_[this->tail_];
    desc->buffer_address = (uintptr_t)buf;
    desc->length = length;
    desc->cmd = desc->cmd | T_DESC_CMD_EOP;
    desc->sta = 0;

    // NIC の TAIL をインクリメントする
    this->tail_++;
    SetNicReg(TDT_OFFSET, this->tail_);

    // 送信処理の完了を待つ
    uint8_t send_status = 0;
    while(!send_status) {
      send_status = desc->sta & 0x0fu;
    }
    return send_status;
  }
  • T_DESC_CMD_EOP は送信するバッファがパケットの終わりである時に立てるフラグですが、今回は 1 つのパケットを 1 つだけのバッファで送ることとするので、毎回 T_DESC_CMD_EOP を立てています。
  • status の値は NIC の送信処理が完了した時に NIC が書き換えるので、これを見ることで送信が完了したかどうかを確認することができます。この値を return することは必要ではないです。(たぶん)

なんとこれだけでパケットが送信されます!ではメイン関数からこれを呼び出してみましょう!

Nic::Send を呼び出す

kernel/main.cpp
extern "C" void KernelMainNewStack(
    const FrameBufferConfig& frame_buffer_config_ref,
    const MemoryMap& memory_map_ref,
    const acpi::RSDP& acpi_table,
    void* volume_image,
    EFI_RUNTIME_SERVICES* rt) {
  // ...

  net::e1000::Initialize();
  char message_buffer[] = "Hello, World!";
  uint8_t sta = net::e1000::nic->Send(message_buffer, sizeof(message_buffer));
  printk("sta: %d\n", sta);

  // ...
}

これでネットワークに Hello, World! という文字列が流れることになります。ちゃんと送信処理がされているか確認してみましょう!

送信処理のデバッグ

QEMU を使ってデバッグします。QEMU の実行引数に、以下のような設定を追加します。

  • E1000E (E1000 と互換性のある NIC) をデバイスに追加する。 1
    • MAC アドレスを指定する。
  • ホスト OS から MikanOS にポートフォワーディングする。(後の受信処理のため)
  • E1000E が送受信するバッファを .pcap ファイルにダンプさせる。
osbook/devenv/run_image.sh
sudo qemu-system-x86_64 \
    -m 1G \
    -drive if=pflash,format=raw,readonly,file=$DEVENV_DIR/OVMF_CODE.fd \
    -drive if=pflash,format=raw,file=$DEVENV_DIR/OVMF_VARS.fd \
    -drive if=ide,index=0,media=disk,format=raw,file=$DISK_IMG \
    -device nec-usb-xhci,id=xhci \
    -device usb-mouse -device usb-kbd \
    -monitor stdio \
    -netdev user,id=net0,hostfwd=tcp:127.0.0.1:1234-:80 \
    -object filter-dump,id=fiter0,netdev=net0,file=dump.pcap \
    -device e1000e,netdev=net0,mac=52:54:00:12:34:56 \
    $QEMU_OPTS

これで MikanOS を起動してみると、dump.pcap が生成されます。

スクリーンショット 2021-12-21 13.34.17.png

確かに Hello, World! と送信されているようです!

受信処理

次に受信処理です。

Nic::Receive の実装

NicクラスにReceiveメソッドを実装していきます。実装の概要は準備編で見た通りです。

  • 受信するバッファのポインタを引数に取る。
  • TAIL が指すディスクリプタの次のディスクリプタが、受信しているかどうかを確認し、その受信バッファのアドレスと長さを取得する。
    • 受信したかどうかの判定は、ディスクリプタの command フィールドに DD というフラグがあり、これが立っているときは受信しているという合図になる。
  • 引数に取ったバッファに、受信したバッファをコピーする。
  • TAIL をインクリメントする。
  • バッファの長さを返す。

実装は以下の通りです。

kernel/net/nic/e1000.cpp
#define R_DESC_STA_DD 0b00000001
uint16_t Nic::Receive (void *buf) {
    uint32_t next_tail = (r_tail_ + 1) % R_DESC_NUM;
    r_descriptor *desc = &r_desc_ring_addr_[next_tail];
    uint16_t len = 0;

    if (desc->status & R_DESC_STA_DD) {
      len = desc->length + 1;
      memcpy(buf, (void *)desc->buffer_address, len);

      desc->status = 0;

      r_tail_++;
      SetNicReg(RDT_OFFSET, r_tail_);
    }

    return len;
  }

呼び出し側は、以下のようにしてこのメソッドを使います。

  • 受信するバッファを用意する。
  • これを Nic::Receive の引数に渡す。
  • Nic::Receive の返り値の長さだけバッファを読み出す。

Nic::Receive を呼び出す

ではこれもメイン関数から呼び出すことでデバッグしてみましょう!

kernel/main.cpp
extern "C" void KernelMainNewStack(
    const FrameBufferConfig& frame_buffer_config_ref,
    const MemoryMap& memory_map_ref,
    const acpi::RSDP& acpi_table,
    void* volume_image,
    EFI_RUNTIME_SERVICES* rt) {
  // ...
  net::e1000::Initialize();

  char packet_buffer[PACKET_SIZE];
  uint32_t len = 0;
  while(len == 0) {
    len = net::e1000::nic->Receive(packet_buffer);
  }
  printk("Received: 0x%x", packet_buffer);

  // ...
}

受信処理のデバッグ

以下の手順でデバッグします。

  • MikanOS を起動 (QEMU の起動オプションをさっきと同様)
  • ホスト OS のターミナルから curl localhost:1234 を叩く

すると、MikanOS がなんらかのパケットを受け取ったことを画面に表示します。

スクリーンショット 2021-12-21 14.36.13.png

また、ダンプされた .pcap ファイルを見ることでも確認できます。MikanOS は ARP リクエストを受け取ったようです。

スクリーンショット 2021-12-21 14.45.52.png

まとめ

以上でバッファの送受信ができるようになりました!次回は受信したことを割り込みで通知させるような設定を行います。

次回: まだ


  1. E1000 ではなく E1000E を使うのは、QEMU の E1000 だと割り込みが行われないからです。より厳密には、E1000 には MSI のシミュレーションが実装されていないからです。 

4
1
0

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
  3. You can use dark theme
What you can do with signing up
4
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?