Skip to content

Commit b203585

Browse files
committed
Fixes part19
1 parent 802739e commit b203585

File tree

1 file changed

+145
-77
lines changed

1 file changed

+145
-77
lines changed

part19.md

+145-77
Original file line numberDiff line numberDiff line change
@@ -1,142 +1,210 @@
1-
# ハイパーバイザーの作り方
2-
第19回 bhyveにおける仮想NICの実装
1+
---
2+
authors:
3+
- 'Takuya ASADA [email protected]'
4+
title: |
5+
ハイパーバイザの作り方~ちゃんと理解する仮想化技術~
6+
第19回 bhyveにおける仮想NICの実装
7+
references:
8+
- id: part3
9+
title: 第3回 I/O 仮想化「デバイス I/O 編」
10+
URL: 'http://syuu1228.github.io/howto_implement_hypervisor/part3.pdf'
11+
- id: part11
12+
title: 第11回 virtioによる準仮想化デバイス その1「virtioの概要とVirtio PCI」
13+
URL: 'http://syuu1228.github.io/howto_implement_hypervisor/part11.pdf'
14+
- id: part12
15+
title: 第12回 virtioによる準仮想化デバイス その2「Virtqueueとvirtio-netの実現」
16+
URL: 'http://syuu1228.github.io/howto_implement_hypervisor/part12.pdf'
17+
...
18+
319
はじめに
20+
========
421
これまでに、ゲスト上で発生したIOアクセスのハンドリング方法、virtio-netの仕組みなど、仮想NICの実現方法について解説してきました。
522
今回の記事では、/usr/sbin/bhyveが、仮想NICのインタフェースであるvirt-netに届いたパケットをどのように送受信しているのかを解説していきます。
23+
624
bhyveにおける仮想NICの実装
25+
==========================
726
bhyveでは、ユーザプロセスである/usr/sbin/bhyve にて仮想IOデバイスを提供しています。また、仮想IOデバイスの一つであるNICは、TAPを利用して機能を提供しています。
827
bhyveでは仮想NICであるTAPを物理NICとブリッジすることにより、物理NICが接続されているLANへ参加させることができます(図1)。
9-
例として、ゲストOSがパケットを送信しようとした場合、どのような経路を経て物理NICへとパケットが送出されていくのか、ゲストOSがパケットを送信しようとした場合を例として見てみましょう。
10-
1,ゲストOSはvirtio-netドライバを用いて、共有メモリ上のリングバッファにパケットを書き込み、IOポートアクセスによってハイパーバイザにパケット送出を通知します。
11-
IOポートアクセスによってVMExitが発生し、CPUの制御がホストOSのvmm.koのコードに戻ります。vmm.koはこのVMExitを受けてioctlをreturnし、ユーザランドプロセスである/usr/sbin/bhyveへ制御を移します。
12-
2,ioctlのreturnを受け取った/usr/sbin/bhyveは仮想NICの共有メモリ上のリングバッファからパケットを取り出します。
13-
注)仮想NICのデータ構造とインタフェースの詳細に関しては、連載第11回・第12回を参照して下さい。
14-
3,2で取り出したパケットをwrite()システムコールで/dev/net/tunへ書き込みます。
15-
4,TAPはブリッジを経由して物理NICへパケットを送出します。
16-
図1,パケット送信手順
28+
29+
どのような経路を経て物理NICへとパケットが送出されていくのか、ゲストOSがパケットを送信しようとした場合を例として見てみましょう。
30+
31+
![パケット送信手順](figures/part19_fig1)
32+
33+
1. NICへのI/O通知
34+
-----------------
35+
ゲストOSはvirtio-netドライバを用いて、共有メモリ上のリングバッファにパケットを書き込み、IOポートアクセスによってハイパーバイザにパケット送出を通知します。IOポートアクセスによってVMExitが発生し、CPUの制御がホストOSのvmm.koのコードに戻ります[^1]。vmm.koはこのVMExitを受けてioctlをreturnし、ユーザランドプロセスである/usr/sbin/bhyveへ制御を移します。
36+
37+
2. 共有メモリからパケット取り出し
38+
---------------------------------
39+
ioctlのreturnを受け取った/usr/sbin/bhyveは仮想NICの共有メモリ上のリングバッファからパケットを取り出します[^2]。
40+
41+
3. tap経由でパケット送信
42+
------------------------
43+
2で取り出したパケットをwrite()システムコールで/dev/net/tunへ書き込みます。
44+
45+
4. bridge経由で物理NICへ送信
46+
----------------------------
47+
TAPはブリッジを経由して物理NICへパケットを送出します。
48+
1749
受信処理ではこの逆の流れを辿り、物理NICからtapを経由して/usr/sbin/bhyveへ届いたパケットがvirtio-netのインタフェースを通じてゲストOSへ渡されます。
50+
51+
[^1]: I/Oアクセスの仮想化とVMExitについては連載 @part3 を参照してください。
52+
[^2]: 仮想NICのデータ構造とインタフェースの詳細に関しては、連載 @part11 ・@part12 を参照して下さい。
53+
1854
TAPとは
19-
bhyveで利用されているTAPについてもう少し詳しくみていきましょう。
20-
TAPはFreeBSDカーネルに実装された仮想イーサネットデバイスで、ハイパーバイザ/エミュレータ以外ではVPNの実装によく使われています(1)。
21-
物理NIC用のドライバは物理NICとの間でパケットの送受信処理を行いますが、TAPは/dev/net/tunを通じてユーザプロセスとの間でパケットの送受信処理を行います。
22-
このユーザプロセスがSocket APIを通じて、TCPv4でVPNプロトコルを用いて対向ノードとパケットのやりとりを行えば、TAPは対向ノードにレイヤ2で直接接続されたイーサーネットデバイスに見えます。
55+
=======
56+
57+
bhyveで利用されているTAPについてもう少し詳しくみていきましょう。TAPはFreeBSDカーネルに実装された仮想Ethernetデバイスで、ハイパーバイザ/エミュレータ以外ではVPNの実装によく使われています[^3]。
58+
59+
物理NIC用のドライバは物理NICとの間でパケットの送受信処理を行いますが、TAPは/dev/net/tunを通じてユーザプロセスとの間でパケットの送受信処理を行います。このユーザプロセスがSocket APIを通じて、TCPv4でVPNプロトコルを用いて対向ノードとパケットのやりとりを行えば、TAPは対向ノードにレイヤ2で直接接続されたイーサーネットデバイスに見えます。
60+
2361
これがOpenVPNなどのVPNソフトがTAPを用いて実現している機能です(図2)。
24-
(1) 正確にはTUN/TAPとして知られており、TAPがイーサネットレイヤでパケットを送受信するインタフェースを提供するのに対しTUNデバイスはIPレイヤでパケットを送受信するインタフェースを提供します。また、TUN/TAPはFreeBSDの他にもLinux、Windows、OS Xなど主要なOSで実装されています。
25-
図2,通常のNICドライバを使ったネットワークとTAPを使ったVPNの比較
62+
2663
では、ここでTAPがどのようなインタフェースをユーザプロセスに提供しているのか見ていきましょう。
2764
TAPに届いたパケットをUDPでトンネリングするサンプルプログラムの例をコードリスト1に示します。
65+
66+
#### コードリスト1,TAPサンプルプログラム(Ruby)
2867
```
2968
require "socket"
3069
TUNSETIFF = 0x400454ca
3170
IFF_TAP = 0x0002
3271
PEER = "192.168.0.100"
3372
PORT = 9876
3473
# TUNTAPをオープン
35-
tap = open("/dev/net/tun", “r+")
# TUNTAPのモードをTAPに、インタフェース名を”tapN”に設定
36-
tap.ioctl(TUNSETIFF, ["tap%d", IFF_TAP].pack("a16S"))
# UDPソケットをオープン
37-
sock = UDPSocket.open
# ポート9876をLISTEN
74+
tap = open("/dev/net/tun", “r+")
75+
# TUNTAPのモードをTAPに、インタフェース名を”tapN”に設定
76+
tap.ioctl(TUNSETIFF, ["tap%d", IFF_TAP].pack("a16S"))
77+
# UDPソケットをオープン
78+
sock = UDPSocket.open
79+
# ポート9876をLISTEN
3880
sock.bind("0.0.0.0", 9876)
39-
while true
 # ソケットかTAPにパケットが届くまで待つ
40-
41-
## ret = IO::select([sock, tap])ret[0].each do |d|
42-
if d == tap # TAPにパケットが届いた場合
 # TAPからパケットを読み込んでソケットに書き込み
43-
sock.send(tap.read(1500), 0, Socket.pack_sockaddr_in(PORT, PEER))
44-
else # ソケットにパケットが届いた場合
 # ソケットからパケットを読み込んでTAPに書き込み
45-
tap.write(sock.recv(65535))
46-
end
47-
end
81+
while true
82+
# ソケットかTAPにパケットが届くまで待つ
83+
ret = IO::select([sock, tap])
84+
ret[0].each do |d|
85+
if d == tap # TAPにパケットが届いた場合
86+
# TAPからパケットを読み込んでソケットに書き込み
87+
sock.send(tap.read(1500), 0, Socket.pack_sockaddr_in(PORT, PEER))
88+
else # ソケットにパケットが届いた場合
89+
# ソケットからパケットを読み込んでTAPに書き込み
90+
tap.write(sock.recv(65535))
91+
end
92+
end
4893
end
4994
```
50-
コードリスト1,TAPサンプルプログラム(Ruby)
5195

5296
ユーザプロセスがTAPとやりとりを行うには、/dev/net/tunデバイスファイルを用います。
97+
5398
パケットの送受信は通常のファイルIOと同様にread()、write()を用いる事が出来ますが、送受信処理を始める前にTUNSETIFF ioctlを用いてTAPの初期化を行う必要があります。
5499
ここでは、TUNTAPのモード(TUNを使うかTAPを使うか)とifconfigに表示されるインタフェース名の指定を行います。
100+
55101
ここでTAPに届いたパケットをUDPソケットへ、UDPソケットに届いたパケットをTAPへ流すことにより、TAPを出入りするパケットをUDPで他ノードへトンネリングすることが出来ます(図2右相当の処理)。
102+
103+
![通常のNICドライバを使ったネットワークとTAPを使ったVPNの比較](figures/part19_fig2)
104+
105+
[^3]: 正確にはTUN/TAPとして知られており、TAPがイーサネットレイヤでパケットを送受信するインタフェースを提供するのに対しTUNデバイスはIPレイヤでパケットを送受信するインタフェースを提供します。また、TUN/TAPはFreeBSDの他にもLinux、Windows、OS Xなど主要なOSで実装されています。
106+
56107
bhyveにおける仮想NICとTAP
108+
=========================
57109
VPNソフトではTAPを通じて届いたパケットをユーザプロセスからVPNプロトコルでカプセル化して別ノード送っています。
110+
58111
ハイパーバイザでTAPを用いる理由はこれとは異なり、ホストOSのネットワークスタックに仮想NICを認識させ物理ネットワークに接続し、パケットを送受信するのが目的です。
59112
このため、VPNソフトではソケットとTAPの間でパケットをリダイレクトしていたのに対して、ハイパーバイザでは仮想NICとTAPの間でパケットをリダイレクトする事になります。
113+
60114
それでは、このリダイレクトの部分についてbhyveのコードを実際に確認してみましょう(リスト2)。
115+
116+
#### コードリスト2,/usr/sbin/bhyveの仮想NICパケット受信処理
61117
```
62118
/* TAPからデータが届いた時に呼ばれる */
63119
static void
64120
pci_vtnet_tap_rx(struct pci_vtnet_softc *sc)
65121
{
66-
struct vqueue_info *vq;
67-
struct virtio_net_rxhdr *vrx;
68-
uint8_t *buf;
69-
int len;
70-
struct iovec iov;
71-
〜 略 〜
72-
vq = &sc->vsc_queues[VTNET_RXQ];
73-
vq_startchains(vq);
74-
〜 略 〜
75-
do {
76-
〜 略 〜
 /* 受信キュー上の空きキューを取得 */
77-
assert(vq_getchain(vq, &iov, 1, NULL) == 1);
78-
〜 略 〜
79-
vrx = iov.iov_base;
80-
q buf = (uint8_t *)(vrx + 1); /* 空きキューのアドレス */
81-
/* TAPから空きキューへパケットをコピー */
82-
len = read(sc->vsc_tapfd, buf,
83-
iov.iov_len - sizeof(struct virtio_net_rxhdr));
84-
/* TAPにデータが無ければreturn */
85-
if (len < 0 && errno == EWOULDBLOCK) {
122+
struct vqueue_info *vq;
123+
struct virtio_net_rxhdr *vrx;
124+
uint8_t *buf;
125+
int len;
126+
struct iovec iov;
127+
〜 略 〜
128+
vq = &sc->vsc_queues[VTNET_RXQ];
129+
vq_startchains(vq);
130+
〜 略 〜
131+
do {
132+
〜 略 〜
133+
/* 受信キュー上の空きキューを取得 */
134+
assert(vq_getchain(vq, &iov, 1, NULL) == 1);
135+
〜 略 〜
136+
vrx = iov.iov_base;
137+
buf = (uint8_t *)(vrx + 1); /* 空きキューのアドレス */
138+
/* TAPから空きキューへパケットをコピー */
139+
len = read(sc->vsc_tapfd, buf,
140+
iov.iov_len - sizeof(struct virtio_net_rxhdr));
141+
/* TAPにデータが無ければreturn */
142+
if (len < 0 && errno == EWOULDBLOCK) {
143+
〜 略 〜
144+
vq_endchains(vq, 0);
145+
return;
146+
}
147+
〜 略 〜
148+
memset(vrx, 0, sizeof(struct virtio_net_rxhdr));
149+
vrx->vrh_bufs = 1; /* キューに接続されているバッファ数 */
150+
〜 略 〜
151+
vq_relchain(vq, len + sizeof(struct virtio_net_rxhdr));
152+
} while (vq_has_descs(vq)); /* 空きキューがある間繰り返し */
86153
〜 略 〜
87-
vq_endchains(vq, 0);
88-
return;
89-
}
90-
〜 略 〜
91-
memset(vrx, 0, sizeof(struct virtio_net_rxhdr));
92-
vrx->vrh_bufs = 1; /* キューに接続されているバッファ数 */
93-
〜 略 〜
94-
vq_relchain(vq, len + sizeof(struct virtio_net_rxhdr));
95-
} while (vq_has_descs(vq)); /* 空きキューがある間繰り返し */
96-
〜 略 〜
97-
vq_endchains(vq, 1);
154+
vq_endchains(vq, 1);
98155
}
99156
```
100-
コードリスト2,/usr/sbin/bhyveの仮想NICパケット受信処理
157+
101158
この関数はsc->vsc_tapfdをkqueue()/kevent()でポーリングしているスレッドによってTAPへのパケット着信時コールバックされます。
102159
コードの中では、virtio-netの受信キュー上の空きエリアを探して、TAPからキューが示すバッファにデータをコピーしています。
103160
これによって、TAPへパケットが届いた時は仮想NICへ送られ、仮想NICからパケットが届いた時はゲストOSに送られます。
104161
その結果、bhyveの仮想NICはホストOSにとってLANケーブルでtap0へ接続されているような状態になります。
162+
105163
TAPを用いたネットワークの構成方法
106-
前述の状態になった仮想NICでは、IPアドレスが適切に設定されていればホストOSとゲストOS間の通信が問題なく行えるようになります。
107-
しかしながら、このままではホストとの間でしか通信が出来ず、インターネットやLAN上の他ノードに接続する方法がありません(2)。
108-
これを解決するには、ホストOS側に標準的に搭載されているネットワーク機能を利用します。
109-
1つの方法は、既に紹介したブリッジを使う方法で、TAPと物理NICをデータリンクレイヤで接続し、物理NICの接続されているネットワークにTAPを参加させることです。
110-
しかしながら、WiFiでは仕様によりブリッジが動作しないという制限があったり、LANから1つの物理PCに対して複数のIP付与が許可されていない環境で使う場合など、ブリッジ以外の方法でゲストのネットワークを運用したい場合があります。
111-
この場合は、NATを使ってホストOSでアドレス変換を行った上でIPレイヤでルーティングを行います。
112-
bhyveではこれらの設定を自動的に行う仕組みを特に提供しておらず(3)、TAPにbhyveを接続する機能だけを備えているので、自分でコンフィグレーションを行う必要があります。
113-
リスト3,4に設定例を示します。
164+
=================================
165+
前述の状態になった仮想NICでは、IPアドレスが適切に設定されていればホストOSとゲストOS間の通信が問題なく行えるようになります。しかしながら、このままではホストとの間でしか通信ができず、インターネットやLAN上の他ノードに接続する方法がありません。この点においては、2台のPCをLANケーブルで物理的に直接つないている環境と同じです。
166+
167+
これを解決するには、ホストOS側に標準的に搭載されているネットワーク機能を利用します。1つの方法は、すでに紹介したブリッジを使う方法で、TAPと物理NICをデータリンクレイヤで接続し、物理NICの接続されているネットワークにTAPを参加させることます。しかしながら、WiFiでは仕様によりブリッジが動作しないという制限があったり、LANから1つの物理PCに対して複数のIP付与が許可されていない環境で使う場合など、ブリッジ以外の方法でゲストのネットワークを運用したい場合があります。
168+
169+
この場合は、NATを使ってホストOSでアドレス変換を行ったうえでIPレイヤでルーティングを行います[^4]。bhyveではこれらの設定を自動的に行うしくみをとくに提供しておらず、TAPにbhyveを接続する機能だけを備えているので、自分でコンフィギュレーションを行う必要があります。
170+
171+
リスト3、4に/etc/rc.confの設定例を示します。なお、OpenVPNなどを用いたVPN接続に対してブリッジやNATを行う場合も、ほぼ同じ設定が必要になります。
172+
173+
#### リスト3,ブリッジの場合
114174
```
115175
cloned_interfaces="bridge0 tap0"
116176
autobridge_interfaces="bridge0"
117177
autobridge_bridge0="em0 tap*"
118178
ifconfig_bridge0="up"
119179
```
120-
リスト3,ブリッジの場合
180+
181+
#### リスト4,NATの場合
121182
```
122-
firewall_enable="YES"
123-
firewall_type="OPEN"
183+
firewall_enable="YES"
184+
firewall_type="OPEN"
124185
natd_enable="YES"
125186
natd_interface="em0"
126187
gateway_enable="YES"
127188
cloned_interfaces="tap0"
128189
ifconfig_tap0="inet 192.168.100.1/24 up"
129190
dnsmasq_enable="YES"
130191
```
131-
リスト4,NATの場合
132-
(2) この点においては、2台のPCをLANケーブルで物理的に直接つないている環境と同じです。
133-
(3) NATを使わずにルーティングだけを行うこともできますが、その場合はLAN上のノードからゲストネットワークへの経路が設定されていなければなりません。一般的にはそのような運用は考えにくいので、NATをつかう事が殆どのケースで適切だと思われます。
134-
なお、OpenVPNなどを用いたVPN接続に対してブリッジやNATを行う場合も、ほぼ同じ設定が必要になります。
192+
193+
[^4]:NATを使わずにルーティングだけを行うこともできますが、その場合はLAN上のノードからゲストネットワークへの経路が設定されていなければなりません。一般的にはそのような運用は考えにくいので、NATを使うことがほとんどのケースで適切だと思われます。
194+
135195
まとめ
196+
======
197+
136198
今回は仮想マシンのネットワークデバイスについて解説しました。
137-
次回は、仮想マシンのストレージデバイスについて解説します。 ライセンス
199+
次回は、仮想マシンのストレージデバイスについて解説します。
200+
201+
ライセンス
138202
==========
139203

140204
Copyright (c) 2014 Takuya ASADA. 全ての原稿データ は
141205
クリエイティブ・コモンズ 表示 - 継承 4.0 国際
142206
ライセンスの下に提供されています。
207+
208+
参考文献
209+
========
210+

0 commit comments

Comments
 (0)