この記事は 自作OS Advent Calendar 21日目の記事です。
前回のおさらい
前回の初期化編では NIC の設定と初期化を行い、ドライバから NIC を操作できるようにしました。今回はついにそれを利用してバッファの送受信を行ってみましょう!
今回やること
送信処理、受信処理の順に書いていきます。受信処理では、バッファの受信はポーリングで行います。割り込みで受信するようにするのは次回に行います。
送信処理
Nic::Send の実装
Nic
クラスにSend
メソッドを実装していきます。実装の概要は準備編で見た通りです。
- 送信するバッファのアドレスと長さを取得する。
- アドレスと長さを
TAIL
が指す送信ディスクリプタに記述する。 - 送信ディスクリプタのその他のフィールド (command, status) を適切に初期化する。
- TAIL をインクリメントする。
実装は以下の通りです。
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 を呼び出す
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 ファイルにダンプさせる。
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 が生成されます。
確かに Hello, World!
と送信されているようです!
受信処理
次に受信処理です。
Nic::Receive の実装
Nic
クラスにReceive
メソッドを実装していきます。実装の概要は準備編で見た通りです。
- 受信するバッファのポインタを引数に取る。
- TAIL が指すディスクリプタの次のディスクリプタが、受信しているかどうかを確認し、その受信バッファのアドレスと長さを取得する。
- 受信したかどうかの判定は、ディスクリプタの
command
フィールドにDD
というフラグがあり、これが立っているときは受信しているという合図になる。
- 受信したかどうかの判定は、ディスクリプタの
- 引数に取ったバッファに、受信したバッファをコピーする。
- TAIL をインクリメントする。
- バッファの長さを返す。
実装は以下の通りです。
#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 を呼び出す
ではこれもメイン関数から呼び出すことでデバッグしてみましょう!
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 がなんらかのパケットを受け取ったことを画面に表示します。
また、ダンプされた .pcap ファイルを見ることでも確認できます。MikanOS は ARP リクエストを受け取ったようです。
まとめ
以上でバッファの送受信ができるようになりました!次回は受信したことを割り込みで通知させるような設定を行います。
次回: まだ
-
E1000 ではなく E1000E を使うのは、QEMU の E1000 だと割り込みが行われないからです。より厳密には、E1000 には MSI のシミュレーションが実装されていないからです。 ↩