この文章は自分がBitVisorのpro1000ドライバを読んだ時のメモです.
結構前の話なのでもしかすると最新版のコードと差異があるかもしれません.
BitVisor Summit6の発表資料(の前半)も合わせてご覧ください.
ネットワークデバイスの基礎
まず一般のネットワークデバイスの構成について,主にデータ処理の観点から簡単に説明します.
受信処理
一般にNICはデータをやりとりするために(多くの他のデバイスと同様)Ring buffer状のデータ構造を利用します.
以下に受信時に利用する典型的なデータ構造を示します.
このring bufferのエントリ一つ一つはdescriptorと呼ばれます.baseポインタでring bufferの先頭を,headとtailでring bufferのどの位置に有効なデータがあるかを保持します.
データを受信するためにまずOS(ドライバ)はdescriptorに必要な情報を書き込み,tailポインタをインクリメントします.
このdescriptorには受信したデータを書き込むためのバッファのアドレスの他,バッファのサイズなどの情報が含まれます.
NICはパケットを受信した際,headポインタが指すdescriptorの内容に従って受信データをDMAし,head ポインタをインクリメントします.
ドライバがパケットが到着したことを知る方法は大きく分けて2種類あります.
一つは割り込み,もう一つはポーリングです.ポーリングする際は,事前にheadポインタの値を記憶しておけば,その後headポインタがインクリメントされている場合にその分だけパケットが受信されていることが分かります.
またNICはパケットを受信した際にdescriptorに受信完了した旨やパケットのチェックサムを書き戻す (write-backする) ため,descriptorがwrite-backされているかどうかでもパケットの受信が確認できます.
[head, tail)の間にあるdescriptorの所有権はNICにあることになっており,ドライバが触ることは禁じられています.
一方でそれ以外の領域の所有権はドライバにあります.パケットを受信していると有効なdescriptorがどんどん無くなってしまうので,ドライバは適当なタイミングでdescriptorを更新する必要があります.
もし仮に有効なdescriptorがない場合,パケットは破棄されることになります.
なお,receiveのことをRxと略記することがあります.
送信処理
送信も受信処理と同様にring bufferでデータを管理します.受信の場合と異なる点は,送信の場合descriptorが指すバッファのアドレスに送信したいデータが格納されているという点です.
NICがパケットを送信したら,headポインタをその分インクリメントします.
事前にheadポインタの位置を記憶しておくことで,どれだけ送信されたかが分かります.また受信の場合と同様に送信完了をwrite-backするものもあります.
設定によっては送信完了を割り込みで通知させることも可能です.
送信が完了したら送信に使ったバッファは再利用ができるようになります.
ドライバは適当なタイミングでdescriptorを更新します.
なお,transmissionのことをTxと略記することがあります.
デバイスの設定方法
NICを使用する際にはdescriptor ringを適切に設定する必要があります.
まずドライバはdescriptor ring用のメモリを確保します.
その後,NICの適切なレジスタに書き込むことでdescriptor ringのbase addressを設定します.
通常NICのレジスタにはMMIO経由でアクセスできます.headポインタやtailポインタなどの更新もレジスタ経由で更新します.
BitVisorにおけるネットワークデバイス
BitVisorでネットワークデバイスのI/Oを補足して処理をしたい場合,単純にはゲストのOSが設定したdescriptorとバッファを直接参照する方法があります.
しかし,この場合処理の度にゲストOSの動作を全て止めなければBitVisorが処理を完了する前にゲストがデータに触らないことを保証できません.
データはDMAで転送されるため,これを厳密に保証することは困難です.
そこでBitVisorではShadow Descriptorという方法を使用します.
BitVisorはゲストOSとは別にNIC用のdescriptorとバッファを作成します.実際のNICはこのBitVisorが作成したShadow Descriptorに対して処理をおこないます.
データを受信した際はshadow bufferからゲストのバッファへデータをコピーします.
データを送信する場合はゲストのバッファから一旦shadow bufferへデータをコピーし,送信をおこないます.
BitVisorはゲストのNICのMMIOレジスタへのアクセスをフックすることでこのことを実現します.
Shadow DescriptorやShadow Bufferの領域はEPTによりゲストからアクセスすることはできません.
ネットワークデバイスの設定
BitVisorで実際にネットワークデバイスを扱うためには,defconfigでどのデバイスにどのドライバを割り当てるかを指定する必要があります.
その設定の際に,以下のオプションを指定することができます.
-
net
-
net=...
の形でさらにオプションを指定-
pass
- 上述したshadow descriptorを使ってゲストのI/Oをフック
-
ip
- BitVisor内部のlwipの機能を利用する.デバイスはBitVisorが占有し,ゲストからは見えなくなる.
-
ippass
- BitVisor内部のlwipの機能を利用しつつpassと同様にゲストにもデバイスを見せる
-
vpn
- VPN機能を利用する
- "" (空文字)
- デバイスをゲストから隠す
-
-
-
tty=1
- デバイスをログ出力として利用する
-
virtio=1
- デバイスをvirtioとしてゲストに見せる
defconfigによるPCIデバイスに対するドライバの割り当ての詳細については[4]を参照してください.
ゲストにvirtioを見せる場合やデバイスをBitVisorが占有する場合,BitVisorがデバイスを初期化します.
一方でゲストOSにデバイスを見せる場合(pass
やippass
),ゲストOSにデバイスの初期化はまかせます.
ネットワークAPI
BitVisorではpass
やip
などの機能(モジュール)はネットワークAPIを利用して作成されています.
ネットワークAPIはnet/netapi.c
で定義されています.
各ネットワークドライバは,パケットを送受信する際にネットワークAPI経由で設定に応じたモジュールのコールバック関数を呼ぶようになっています.
このコールバック関数ですが,大きく分けて2種類あり,一つは物理NICからBitVisorがパケットを受信した際に呼ばれるもの,もう一つがゲストが送信しようとした際に呼ばれるものです.
ネットワークモジュールの動作
簡単に各ネットワークモジュールが何をするのかを説明します.
pass
やnull(空文字)モジュールはnet/netapi.c
,ip
およびippass
モジュールはip/net_main.c
,vpn
モジュールはvpn/kernel.c
で定義されています.
null
nullモジュールはパケットを単純に破棄します.
virtio=1
かつ net=null
net=
(空文字),あるいはnetを指定しない (2017-12-10 hdk_2様コメントより修正) とすると,ゲストからvirtioが見えるものの,ゲストから送信しようとしたときに以下のコールバック関数が呼ばれ,結果として何もされずパケットは破棄されます.
物理NICがパケットを受信しても同様にパケットは破棄されます.
virtio=1
でなければデバイスはそもそもゲストからは見えないようになります.
static void
netapi_net_null_recv_callback (void *handle, unsigned int num_packets,
void **packets, unsigned int *packet_sizes,
void *param, long *premap)
{
/* Do nothing. */
}
pass
passモジュールはshadow descriptorを使ってゲストのパケット送受信をフックするためのものです.
passモジュールによりゲストにNICを見せつつ,BitVisorのログ出力用にこのデバイスが使えるようになります.
passモジュールでは以下のコールバック関数が呼ばれます.
関数ポインタで表されているので実態が分かりにくいですが,物理NICがパケットを受信した際はゲストのdescriptorとbufferにデータをコピーする関数,
ゲストがパケットを送信する場合はゲストのdescriptorとbufferをshadow descriptorとshadow bufferへコピー(して送信する)関数が呼ばれます.
(この関数は各ネットワークデバイスが設定します)
static void
netapi_net_pass_recv_callback (void *handle, unsigned int num_packets,
void **packets, unsigned int *packet_sizes,
void *param, long *premap)
{
struct net_pass_data2 *p = param;
p->func->send (p->handle, num_packets, packets, packet_sizes, true);
}
ip及びippass
ipモジュールはBitVisor内臓のlwIPの機能を使うようになります.受信パケットはlwIPスタックの方へ送られます.
一方で,ゲストからNICは隠蔽されます.
ippassモジュールを使うとlwIP機能を使いつつ,passと同様にNICをゲストに見せます.
物理NICがパケットを受信した場合のコールバック関数は以下のようになっています.
static void
net_ip_phys_recv (void *handle, unsigned int num_packets, void **packets,
unsigned int *packet_sizes, void *param, long *premap)
{
struct net_ip_data *p = param;
if (p->pass)
p->virt_func->send (p->virt_handle, num_packets, packets,
packet_sizes, true);
if (p->input_ok)
net_main_input_queue (p, packets, packet_sizes, num_packets);
}
ここで p->pass
の場合,つまり ippass
であれば,p->virt_func->send()
によってゲスト側へパケットを送信します.
(phys_recv
というのは,物理NICから受信したという意味です)
またlwIPの準備ができていれば (p->input_ok
), 後でlwIPのスレッドがそのパケットを操作するために,受信したパケットをキューに追加します.
ゲストが送信をする場合は以下のコールバック関数が呼ばれます.
static void
net_ip_virt_recv (void *handle, unsigned int num_packets, void **packets,
unsigned int *packet_sizes, void *param, long *premap)
{
struct net_ip_data *p = param;
if (p->pass)
p->phys_func->send (p->phys_handle, num_packets, packets,
packet_sizes, true);
}
ippass
であればpass
と同様にshadow descriptorへデータをコピーして送信します.
(virt_recv
というのは,ゲスト側から受信したという意味です)
そうでなければパケットは破棄されます.(ip
かつvirtio=0
ならNICは隠蔽されます)
vpn
VPN機能は私は使ったことがないのでここでは割愛します.すみません.名前の通りVPNしているはずです..
ネットワークモジュールの登録
net/netapi.c
にあるnet_register()
関数でモジュールを登録します.
各モジュールはINITFUNC
を通じて初期化時に自身のモジュールを登録しています.
void
net_register (char *netname, struct netfunc *func, void *param)
{
struct netlist *p;
p = alloc (sizeof *p);
p->name = netname;
p->func = func;
p->param = param;
p->next = netlist_head;
netlist_head = p;
}
ここで,重要なのはstruct netfunc *func
で,この構造体で初期化時やモジュールを開始したときに呼ばれる関数を保持しています.
例えば,nullモジュールやpassモジュールでは以下のようになっています.
static struct netfunc net_null = {
.new_nic = netapi_net_null_new_nic,
.init = netapi_net_null_init,
.start = netapi_net_null_start,
};
static struct netfunc net_pass = {
.new_nic = netapi_net_pass_new_nic,
.init = netapi_net_pass_init,
.start = netapi_net_pass_start,
};
net_new_nic()
関数を使って登録したモジュールを取得できます.
初期化とモジュールの開始
ドライバは初期化のときに net_init()
によりモジュールを初期化します.
基本はnetfunc
で指定した初期化関数(init)を呼ぶだけです.
初期化に成功すればtrueが返ります.
bool
net_init (struct netdata *handle, void *phys_handle, struct nicfunc *phys_func,
void *virt_handle, struct nicfunc *virt_func)
{
if (!handle->func->init (handle->handle, phys_handle, phys_func,
virt_handle, virt_func))
return false;
if (handle->tty) {
handle->tty_phys_handle = phys_handle;
handle->tty_phys_func = phys_func;
}
return true;
}
ここで,phys_func
及びvirt_func
はstruct nicfunc
構造体となっていますが,この構造体には実際に物理NICから受信or送信する関数や,guestのdescriptorへデータをコピーする関数が含まれています.
この関数はドライバが設定します.例えばpro1000では以下のようになっています(なお,実際にはvirtioを使用する場合virt_func
は違うものが利用されます).
static struct nicfunc phys_func = {
.get_nic_info = getinfo_physnic,
.send = send_physnic,
.set_recv_callback = setrecv_physnic,
.poll = poll_physnic,
}, virt_func = {
.get_nic_info = getinfo_virtnic,
.send = send_virtnic,
.set_recv_callback = setrecv_virtnic,
};
上のモジュールの例でphys_func->send()
やvirt_func->send()
をモジュールが呼び出していますが,それはこの構造体で指定された関数を呼び出すことになります.
set_recv_callback
はモジュールを呼び出すためのコールバック関数を設定するための関数です.
初期化をすればモジュールが使えるようになるかというとそうではなくて,実際にはモジュールをstartさせる必要があります.
そのためにはnet_start()
を呼びます.
void
net_start (struct netdata *handle)
{
struct nicinfo info;
if (handle->tty) {
handle->tty_phys_func->get_nic_info (handle->tty_phys_handle,
&info);
memcpy (handle->mac_address, info.mac_address,
sizeof handle->mac_address);
tty_udp_register (net_tty_send, handle);
}
handle->func->start (handle->handle);
}
こちらも基本はnetfunc
で指定したstart関数を呼ぶだけです.
start関数は基本的に適切なコールバック関数を設定します.例えば,passモジュールは以下のようになっています.
static void
netapi_net_pass_start (void *handle)
{
struct net_pass_data *p = handle;
p->phys.func->set_recv_callback (p->phys.handle,
netapi_net_pass_recv_callback,
&p->virt);
p->virt.func->set_recv_callback (p->virt.handle,
netapi_net_pass_recv_callback,
&p->phys);
}
このset_recv_callback()
はnet_init()
で指定したphys_func
あるいはvirt_func
のset_recv_callback
関数です.
これによりようやくモジュールのコールバック関数が設定されることになります.
nullモジュールでは以下のようになっています.
static void
netapi_net_null_start (void *handle)
{
struct net_null_data *p = handle;
p->phys_func->set_recv_callback (p->phys_handle,
netapi_net_null_recv_callback, NULL);
if (p->virt_func)
p->virt_func->set_recv_callback (p->virt_handle,
netapi_net_null_recv_callback,
NULL);
}
p->virt_func
が1の場合,つまりvirtioを見せる場合はゲスト送信に対応するコールバック関数を登録します.
そうでない場合はNICは隠蔽されておりそもそもvirt_func
が存在しないため,何もしません.
ネットワークAPIとドライバの初期化の流れをまとめると以下のようになります.
- モジュールを
net_register()
でBitVisor本体に登録 - ドライバは
net_new_nic()
でモジュールを取得 -
net_init()
でモジュールを初期化.このときコールバック関数から呼び出すことのできるドライバの送受信関数を設定. -
net_start()
でモジュールの開始.ドライバのコールバック関数を設定. - ドライバはデータを送受信する際にコールバック関数を呼び出す.
Pro/1000ドライバ
具体例としてBitVisorに内臓されているイーサネットドライバの一つであるpro1000のドライバについてみていこと思います.
主に関連するコードはdriver/net/pro1000.c
です.
NICのデータ処理の流れは最初に説明した通りですが,実際のNICのドライバは色々と機能を持っているため複雑なことが多いです.
ただし,BitVisorのデバイスドライバの基本は前述したようにShadowingであり,できる限りデバイスの設定動作などはゲストにまかせ,サポートする機能も最低限に留めているので普通のドライバよりかはシンプルかなと思います.ただし準パススルードライバならではの処理もあり,特にshadow descriptorとguest descriptorのバッファサイズが一致しない場合パケットを分割する必要があります.
名称について
Intel Pro/1000というのはIntelが販売している1GbE NICの1種です.
Pro/1000にはいくつか種類があり,それにはPCI接続のPro/1000 GT, PCI-X接続のPro/1000 MT, PCIe接続のPro1000 CTなどが含まれます.
Pro/1000 GTやMTはネットワークコントローラとしてIntel 8254xシリーズを利用しています.一方pro1000 CTはIntel 8257xシリーズのものを利用しています.8254xシリーズのの仕様は[1],8257xシリーズの仕様は[2]にあります.
Intelが販売している1GbE NICには他にも種類があり,例えば最近だとI350[3]と呼ばれるコントローラを利用したNICが販売されています.
新しいコントローラの方が新しい機能が追加されていたりしますが,基本的な部分は共通(なはず)です.そこでIntelの1GbE NICについて知りたい場合はまずは8257xシリーズの仕様に当たればいいと思います.
本文章でも8257xシリーズの仕様に基づいて説明します.
LinuxではPro/1000のドライバはe1000 (PCI用)あるいはe1000e (PCIe用)と呼ばれています.また,igbと呼ばれるI350などで使えるドライバもあります.
一般にpro1000やe1000と言った場合はIntelの特定のNICの製品ではなく,Intelの1GbE NIC全般のことを指すと思います.
ドキュメント
主に[2]の3.2, 3.3節に受信と送信のためのdescriptorの設定について書いてあります.
また13章にNICを設定するためのレジスタについて書いてあります.
レジスタへのアクセス
pro1000のレジスタにはMMIOでアクセスできます.MMIOためのアドレスはPCI Configuratio SpaceのBAR0に格納されています.
また,I/Oポートからアクセスすることも可能です(BAR2 or 4を利用).
[2]のTable 13.1にレジスタ一覧があります.
Descriptor
8257xでは,descriptorに複数の種類があり,receive descriptorの場合基本は
- Legacy Rx Descriptor
- Extended Rx Descriptor
の2種類,transmission descriptorの場合
- Legacy Tx Descriptor
- TCP/IP Context Descriptor
- TCP/IP Data Descriptor
の3種類あります.
pro1000.c
の中ではそれぞれrdesc
, rdesc_ext1
, tdesc
, tdesc_dext0
, tdesc_dext1
という名称の構造体でデータ構造が定義されています.
また,x8257シリーズではRx descriptor ringは2つ,Tx descriptor ringは1つ持てるようになっています.
Rx Descriptor
RFCTL
レジスタのEXTSEN
が0かつRCTL.DTYP
= 00bのとき,Legacy modeとなり,以下のdescriptorを利用します.
各フィールドの詳細は仕様を確認してください.
下段の領域はwrite-backで書き戻される領域です.StatusフィールドのDDビットを見るとwrite-backされたかどうかが分かります.
一方でRFCTL
レジスタのEXTSEN
が1かつRCTL.DTYP
= 00bのとき,Extended Rx descriptorが利用されます.
Extended Rx descriptorではwrite-backされるものが異なります.まず,送信時は以下のdescriptorを利用します.
送信が完了すると,以下のようにwrite-backがおこなわれます.
図に示した通り,バッファのアドレス領域もwrite-backされるため,ドライバはどこにデータが転送されたかを記憶しておく必要があります.
Tx Descriptor
TDESC.DEXT = 0
のとき,以下のLegacy Tx Descriptorが利用されます.
TDESC.DEXT = 1
かつ,DTYP
フィールが0000b
のとき,TCP/IP Context Descriptorになります.
このdescriptorは特殊なdescriptorで,このdescriptorの次のエントリにあるTCP/IP Data descriptorのパケットのオフローディングを計算するために利用されます.
このdescriptor自体はパケットを送信しません.
TDESC.DEXT = 1
かつ,DTYP
フィールが0001b
のとき,TCP/IP Data Descriptorになります.
pro1000ドライバが利用するデータ構造
pro1000ドライバでは主に以下の2つの構造体を利用します.
struct data {
int i;
int e;
int io;
int hd;
bool disable;
void *h;
void *map;
uint maplen;
phys_t mapaddr;
struct data2 *d;
};
struct data2 {
spinlock_t lock;
u8 *buf;
long buf_premap;
uint len;
bool dext1_ixsm, dext1_txsm;
uint dext0_tucss, dext0_tucso, dext0_tucse;
uint dext0_ipcss, dext0_ipcso, dext0_ipcse;
uint dext0_mss, dext0_hdrlen, dext0_paylen, dext0_ip, dext0_tcp;
bool tse_first, tse_tcpfin, tse_tcppsh;
u16 tse_iplen, tse_ipchecksum, tse_tcpchecksum;
struct desc_shadow tdesc[2], rdesc[2];
struct data *d1;
struct netdata *nethandle;
bool initialized;
net_recv_callback_t *recvphys_func, *recvvirt_func;
void *recvphys_param, *recvvirt_param;
u32 rctl, rfctl, tctl;
u8 macaddr[6];
struct pci_device *pci_device;
u32 regs_at_init[PCI_CONFIG_REGS32_NUM];
bool seize;
bool conceal;
LIST1_DEFINE (struct data2);
void *virtio_net;
char virtio_net_bar_emul;
struct pci_msi *virtio_net_msi;
};
data2
のdesc_shadow
でshadow descriptorを持ちます.
struct desc_shadow {
bool initialized;
union {
u64 ll;
u32 l[2];
} base;
u32 len;
u32 head, tail;
union {
struct {
struct tdesc *td;
phys_t td_phys;
void *tbuf[NUM_OF_TDESC];
} t;
struct {
struct rdesc *rd;
phys_t rd_phys;
void *rbuf[NUM_OF_RDESC];
long rbuf_premap[NUM_OF_RDESC];
} r;
} u;
};
shadow descriptorでは,base
でゲストのdescriptorのbaseアドレスを保持します.
td_phys
or rd_phys
がBitVisorが管理するdescriptornのbaseアドレスになります.
初期設定
まずpro1000ドライバの初期設定からみていきます.
デバイスドライバの登録
BitVisorでデバイスドライバを使用するためには,pci_register_driver()
を利用してまずデバイスドライバを登録する必要があります.
pro1000.cの末尾にあるPCI_DRIVER_INIT (vpn_pro1000_init);
によって,BitVisorが起動してPCIデバイスの初期化する際にvpn_pro1000_init
が呼ばれます.
このvpn_pro1000_init()
の中でpci_register_driver()
を呼んでいます.
static void
vpn_pro1000_init (void)
{
LIST1_HEAD_INIT (d2list);
pci_register_driver (&pro1000_driver);
return;
}
ここでpro1000_driver
は以下のような構造体です.
static struct pci_driver pro1000_driver = {
.name = driver_name,
.longname = driver_longname,
.driver_options = "tty,net,virtio",
.device = "class_code=020000,id="
/* 31608004.pdf */
"8086:105e|" /* Dual port */
"8086:1081|"
"8086:1082|"
// 長いので省略
"8086:15b7",
.new = pro1000_new,
.config_read = pro1000_config_read,
.config_write = pro1000_config_write,
};
.new
にデバイス初期化の関数,config_read
及びconfig_write
でPCIのコンフィギュレーション空間にアクセスする際に呼び出される関数を登録しています.
.device
のところにかかれているのはこのドライバが対応しているPCIデバイスのIDです.
BitVisorのdefconfigで適当に設定することで,pro1000 NICに対してこのドライバを設定することができます.
デバイスの初期化
defconfigによって,あるデバイスに対してBitVisorのpro1000ドライバを利用するように設定されている場合,
pro1000_driver
の記述に従ってpro1000_new
が呼ばれ,デバイスが初期化されます.
pro1000_new
では最初にconfigファイルでの設定を読み込んで,ttyやvirtioのオプションの有無を確認しています.その後vpn_pro1000_new
を呼びます.
option_tty
はオプションでtty=1
を設定したらtrue
,option_virtio
はオプションでvirtio=1
を指定したらtrue
になります.
また,option_net
はconfigにおいてnet=xxx
で指定した文字列(pass
とかippass
とか)が入ります.
static void
pro1000_new (struct pci_device *pci_device)
{
bool option_tty = false;
char *option_net;
bool option_virtio = false;
if (pci_device->driver_options[0] &&
pci_driver_option_get_bool (pci_device->driver_options[0], NULL)) {
option_tty = true;
}
option_net = pci_device->driver_options[1];
if (pci_device->driver_options[2] &&
pci_driver_option_get_bool (pci_device->driver_options[2], NULL))
option_virtio = true;
vpn_pro1000_new (pci_device, option_tty, option_net, option_virtio);
}
vpn_pro1000_new
という名称ですが,これはBitVisor作成当時はネットワークドライバはvpnのためのものだったので,そのときの名残でvpnとついているんじゃないかと思います.多分.
ひとまずvirtio部分は省略して主処理を抜粋したものが以下になります.
d2 = alloc (sizeof *d2);
memset (d2, 0, sizeof *d2);
d2->nethandle = net_new_nic (option_net, option_tty);
alloc_pages (&tmp, NULL, (BUFSIZE + PAGESIZE - 1) / PAGESIZE);
memset (tmp, 0, (BUFSIZE + PAGESIZE - 1) / PAGESIZE * PAGESIZE);
d2->buf = tmp;
d2->buf_premap = net_premap_recvbuf (d2->nethandle, tmp, BUFSIZE);
spinlock_init (&d2->lock);
d = alloc (sizeof *d * 6);
for (i = 0; i < 6; i++) {
d[i].d = d2;
d[i].e = 0;
pci_get_bar_info (pci_device, i, &bar_info);
reghook (&d[i], i, &bar_info);
}
d->disable = false;
d2->d1 = d;
get_macaddr (d2, d2->macaddr);
pci_device->host = d;
pci_device->driver->options.use_base_address_mask_emulation = 1;
d2->pci_device = pci_device;
// virtioは省略
d2->seize = net_init (d2->nethandle, d2, &phys_func, NULL, NULL);
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);
}
主に以下のことをしています.
-
net_new_nic()
によって利用するネットワークモジュールを取得 - PCIのBARの情報の取得
- レジスタアクセスのフック関数の登録
- ネットワークモジュールの初期化
- デバイスの所有権がBitVisorにある場合(ゲストにデバイスを見せない場合),デバイスを初期化して
net_start()
でネットワークモジュールを開始- ゲストにデバイスを見せる場合はゲストに初期化をさせます (ゲストが初期化するまでデバイスは使えません)
ここで重要なのがreghook()
関数です.この中のmmio_register()
によってBARで指定されるMMIO空間にゲストがアクセスした際に,mmhandler()
が呼ばれるように設定します.
これは実際には指定した領域に関してEPTのread/write許可を外すことでゲストのアクセスをフックしています.
(ゲストがその領域にアクセスした際にEPT violationによりVM Exitが発生する.)
mmhandler()
に関しては送信・受信を説明する際に詳しくみていきます.
static void
reghook (struct data *d, int i, struct pci_bar_info *bar)
{
if (bar->type == PCI_BAR_INFO_TYPE_NONE)
return;
unreghook (d);
d->i = i;
d->e = 0;
if (bar->type == PCI_BAR_INFO_TYPE_IO) {
d->io = 1;
d->hd = core_io_register_handler (bar->base, bar->len,
iohandler, d,
CORE_IO_PRIO_EXCLUSIVE,
driver_name);
} else {
d->mapaddr = bar->base;
d->maplen = bar->len;
d->map = mapmem_gphys (bar->base, bar->len, MAPMEM_WRITE);
if (!d->map)
panic ("mapmem failed");
d->io = 0;
d->h = mmio_register (bar->base, bar->len, mmhandler, d);
if (!d->h)
panic ("mmio_register failed");
}
d->e = 1;
}
また,BARで指定される領域がIO領域の場合はcore_io_register_handler()
でiohandler()
が呼ばれるように設定します.
ただし,iohandler()
は実際は特に何もしません.
static int
iohandler (core_io_t io, union mem *data, void *arg)
{
printf ("%s: io:%08x, data:%08x\n",
__func__, *(int*)&io, data->dword);
return CORE_IO_RET_DEFAULT;
}
この部分,ioレジスタは隠蔽しないといけないような気がするんですがいいんでしょうか.. 誰か詳しい方教えてください..
さて,net_init()
の部分ですが,これは何をモジュールとして利用するかで異なります.
例えば,passモジュールを利用する場合,以下の関数が呼ばれますが,この時点ではvirt_func
はNULLであるため実際には特に何も初期化せずにfalse
を返します.
(実は,もしvirtioを利用する場合はvirt_func
にvirtioのものを設定して初期化をおこないます)
static bool
netapi_net_pass_init (void *handle, void *phys_handle,
struct nicfunc *phys_func, void *virt_handle,
struct nicfunc *virt_func)
{
struct net_pass_data *p = handle;
if (!virt_func)
return false;
p->phys.handle = phys_handle;
p->phys.func = phys_func;
p->virt.handle = virt_handle;
p->virt.func = virt_func;
return true;
}
一方nullモジュールであればゲストにはNICを見せないので関数を設定してtrue
を返します.
static bool
netapi_net_null_init (void *handle, void *phys_handle,
struct nicfunc *phys_func, void *virt_handle,
struct nicfunc *virt_func)
{
struct net_null_data *p = handle;
p->phys_handle = phys_handle;
p->phys_func = phys_func;
p->virt_handle = virt_handle;
p->virt_func = virt_func;
return true;
}
net_init()
でtrueが返った場合,seize_pro1000()
関数を呼んでデバイスの初期化(descriptorの初期化を含む)をします.
そうでなければ,ゲストがデバイスを初期化するタイミングで再びnet_init()
を呼びます.
shadow descriptorの設定
前述の通り,NICのMMIOレジスタにゲストがアクセスした際は,mmhandler()
が呼ばれます.
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;
}
if (d2->seize) {
if (!wr)
memset (buf, 0, len);
return 1;
}
spinlock_lock (&d2->lock);
mmhandler2 (d1, d2, gphys, wr, buf, len, flags);
spinlock_unlock (&d2->lock);
return 1;
}
virtio部分は一旦置いておくと,この関数は基本はmmhandler2()
を呼び出すだけです.
もしNICをBitVisorが所有している場合 (d2->seize == true
)は何もしません(readは0を返す).
mmhandler2()
は以下のようになっています.
static void
mmhandler2 (struct data *d1, struct data2 *d2, phys_t gphys, bool wr,
union mem *buf, uint len, u32 flags)
{
union mem *q;
if (d1 != &d2->d1[0])
goto skip;
if (handle_desc (gphys - d1->mapaddr, len, wr, buf, false, d2,
0x3800, &d2->tdesc[0]))
return;
if (handle_desc (gphys - d1->mapaddr, len, wr, buf, false, d2,
0x3900, &d2->tdesc[1]))
return;
if (handle_desc (gphys - d1->mapaddr, len, wr, buf, true, d2,
0x2800, &d2->rdesc[0]))
return;
if (handle_desc (gphys - d1->mapaddr, len, wr, buf, true, d2,
0x2900, &d2->rdesc[1]))
return;
// 省略
skip:
q = (union mem *)(void *)((u8 *)d1->map + (gphys - d1->mapaddr));
if (wr) {
if (len == 1)
q->byte = buf->byte;
else if (len == 2)
q->word = buf->word;
else if (len == 4)
q->dword = buf->dword;
else
panic ("len=%u", len);
} else {
if (len == 1)
buf->byte = q->byte;
else if (len == 2)
buf->word = q->word;
else if (len == 4)
buf->dword = q->dword;
else
panic ("len=%u", len);
}
}
MMIOのどのレジスタにアクセスするかによって処理が変わります.
0x2800, 0x2900, 0x3800, 0x3900 というのはNICのdescriptorに関するアドレスで,この領域にアクセスしてきた場合はhandle_desc()
を呼びます.
もし特に条件にマッチするものがなければそのままNICのレジスタへ直接アクセスしています (skip:
以降の処理)
ちなみにどういうときに最初のd1 != &d2->d1[0]
が成り立つのかはよく分かりません..
handle_desc()
がですが,これは実際にアクセスするレジスタに応じて処理をしています.
static bool
handle_desc (uint off1, uint len1, bool wr, union mem *buf, bool recv,
struct data2 *d2, uint off2, struct desc_shadow *s)
{
if (rangecheck (off1, len1, off2 + 0x00, 4)) {
/* Transmit/Receive Descriptor Base Low */
init_desc (s, d2, off2, !recv);
if (wr)
s->base.l[0] = buf->dword & ~0xF;
else
buf->dword = s->base.l[0];
} else if (rangecheck (off1, len1, off2 + 0x04, 4)) {
....
}
例えば,0x2800へアクセスする場合,これはreceive descriptorのbase addressの下位32bitを保持するレジスタです.
一番最初にこのレジスタにアクセスする場合はinit_desc()
でshadow descriptorを初期化します.
そして,もしゲストがbase addressを更新しようとしている場合は,shadow descriptorのbaseフィールドにそのアドレスを保持します.
他のdescriptor addressに関しても同様のことをします.
init_desc()
は以下のようになっています.
static void
init_desc (struct desc_shadow *s, struct data2 *d2, uint off2, bool transmit)
{
if (s->initialized)
return;
if (transmit) {
init_desc_transmit (s, d2, off2);
} else {
init_desc_receive (s, d2, off2);
}
s->initialized = true;
if (transmit && !d2->initialized) {
net_init (d2->nethandle, d2, &phys_func, d2, &virt_func);
d2->initialized = true;
net_start (d2->nethandle);
}
}
init_desc_transmit()
あるいは init_desc_receive()
の中でBitVisorがメモリを確保して,実際にデバイスが使用するdescriptorを設定します.
また,Tx descriptorを最初に初期化したときにnet_init()
およびnet_start()
でネットワークモジュールを開始します.
(もしBitVisorがデバイスを所有している場合,mmhandler()
からこのinit_desc()
が呼ばれることはなく,ネットワークモジュールが2回初期化されるということもありません.)
ここで,もしnet_init()
の引数として渡されるphys_func
, virt_func
は以下のように定義されています.
static struct nicfunc phys_func = {
.get_nic_info = getinfo_physnic,
.send = send_physnic,
.set_recv_callback = setrecv_physnic,
.poll = poll_physnic,
}, virt_func = {
.get_nic_info = getinfo_virtnic,
.send = send_virtnic,
.set_recv_callback = setrecv_virtnic,
};
パケットの送信
ここではpassモジュールを利用していることを想定して,実際にどうパケットが送信されるかを説明します.
一番最初で説明したように,ゲストがパケットを送信しているかどうかはTx descriptorのtailポインタへのアクセスをフックすることで分かります.
mmhandler2()
からhandle_desc()
を呼んだ際,tail ポインタへゲストが書き込みをおこなったらguest_is_transmitting()
を呼びます.
static bool
handle_desc (uint off1, uint len1, bool wr, union mem *buf, bool recv,
struct data2 *d2, uint off2, struct desc_shadow *s)
{
...
} else if (rangecheck (off1, len1, off2 + 0x18, 4)) {
/* Transmit/Receive Descriptor Tail */
init_desc (s, d2, off2, !recv);
if (wr)
s->tail = buf->dword & 0xFFFF;
else
buf->dword = s->tail;
if (wr && !recv)
guest_is_transmitting (s, d2);
} else {
...
guest_is_transmitting()
は以下のようになっています.
static void
guest_is_transmitting (struct desc_shadow *s, struct data2 *d2)
{
struct tdesc *td;
u32 i, j, l;
u64 k;
if (d2->d1->disable) /* PCI config reg is disabled */
return;
if (!(d2->tctl & 2)) /* !EN: Transmit Enable */
return;
i = s->head;
j = s->tail;
k = s->base.ll;
l = s->len;
while (i != j) {
td = mapmem_gphys (k + i * 16, sizeof *td, MAPMEM_WRITE);
ASSERT (td);
if (process_tdesc (d2, td))
break;
unmapmem (td, sizeof *td);
i++;
if (i * 16 >= l)
i = 0;
}
s->head = i;
*(u32 *)(void *)((u8 *)d2->d1[0].map + 0xC8) |= 0x1; /* interrupt */
}
ここで,s->head
がゲストが更新する前のtailポインタの位置,s->tail
がゲストが更新したtailポインタの位置です.
whileループ内でs->tail
までの未処理のdescriptor一つ一つに関してprocess_tdesc()
で処理をおこないます.
なお,mapmem_gphys()
により,ゲストのdescriptorのメモリ領域をbitvisorが参照できる用にbitvisorのメモリ空間にマッピングしています.送信が終わったらIntrerupt Cause Set Regisetr (0xc8) のTXQE (Transmit Queue Empty Interrupt)をセットします.ゲストの設定次第で割り込みが発生します.
実際に送信処理をおこなうのがprocess_tdesc()
ですが,この関数は結構長いです.
その理由は前述した3種類のTx descriptorに応じて処理を変えなければならないからです.
とりあえず,一番単純なLegcay descriptorに関しては以下のようになります.
static int
process_tdesc (struct data2 *d2, struct tdesc *td)
{
struct tdesc_dext0 *td0;
struct tdesc_dext1 *td1;
phys_t tdaddr;
uint tdlen;
bool dextlast = false;
uint dextsize = 0;
if (td->cmd_dext) {
...
} else if (td->addr && td->len) {
/* Legacy Transmit Descriptor */
tdaddr = td->addr;
tdlen = td->len;
tdesc_copytobuf (d2, &tdaddr, &tdlen);
if (td->cmd_eop) {
if (!td->cmd_ifcs)
printf ("FIXME: IFCS=0\n");
if (td->cmd_ic)
printf ("FIXME: IC=1\n");
if (d2->recvvirt_func) {
void *packet_data[1];
UINT packet_sizes[1];
long packet_premap[1];
packet_data[0] = d2->buf;
packet_sizes[0] = d2->len;
packet_premap[0] = d2->buf_premap;
d2->recvvirt_func (d2, 1, packet_data,
packet_sizes,
d2->recvvirt_param,
packet_premap);
}
if (td->cmd_rs)
td->sta_dd = 1;
d2->len = 0;
}
}
return 0;
}
tdescd_copytobuf()
でdescriptrが指すバッファの内容を,shadow bufferへコピーします.
static void
tdesc_copytobuf (struct data2 *d2, phys_t *addr, uint *len)
{
u8 *q;
int i;
i = BUFSIZE - d2->len;
if (i > *len)
i = *len;
q = mapmem_gphys (*addr, i, 0);
memcpy (d2->buf + d2->len, q, i);
d2->len += i;
unmapmem (q, i);
*addr += i;
*len -= i;
}
なんだか若干ややこしい構成をしていますが,これは後述するようにパケットが分割されている場合に複数回tdesc_copytobuf()
を呼ぶことがあるためです.
とりあえず,今の所は最初はd2->len == 0
でd2->buf
の先頭からデータがコピーされます.
コピーする際はdescriptorのアクセスと同様に,まずmapmem_gphys()
でbitvisorのメモリ空間にバッファの内容をマッピングして,コピーしています.
shadow bufferの方へデータをコピーしたあと,どうするかというと d2->recvvirt_func()
で対応するネットワークモジュールのコールバック関数を呼びます.
このrecvvirt_func()
はsetrecv_virtnic()
で設定されています.
static void
setrecv_virtnic (void *handle, net_recv_callback_t *callback, void *param)
{
struct data2 *d2 = handle;
d2->recvvirt_func = callback;
d2->recvvirt_param = param;
}
それでは誰がこのコールバック関数を設定したかというと,net_start()
したときにネットワークモジュールが設定します.
passモジュールの場合以下のようになっています.
static void
netapi_net_pass_start (void *handle)
{
struct net_pass_data *p = handle;
p->phys.func->set_recv_callback (p->phys.handle,
netapi_net_pass_recv_callback,
&p->virt);
p->virt.func->set_recv_callback (p->virt.handle,
netapi_net_pass_recv_callback,
&p->phys);
}
passモジュールをnet_init()
したとき,phys.func
およびvirt.func
は以下のように設定しました.
static struct nicfunc phys_func = {
.get_nic_info = getinfo_physnic,
.send = send_physnic,
.set_recv_callback = setrecv_physnic,
.poll = poll_physnic,
}, virt_func = {
.get_nic_info = getinfo_virtnic,
.send = send_virtnic,
.set_recv_callback = setrecv_virtnic,
};
従ってrecvvirt_func
にはphys_func
をパラメータとしてnetapi_net_pass_callback()
が設定されます.
そして,netapi_net_pass_callback()
は以下のようになっています.
static void
netapi_net_pass_recv_callback (void *handle, unsigned int num_packets,
void **packets, unsigned int *packet_sizes,
void *param, long *premap)
{
struct net_pass_data2 *p = param;
p->func->send (p->handle, num_packets, packets, packet_sizes, true);
}
...ということで以上を総合すると,recvvirt_func()
により呼ばれるp->func->send()
はsend_physnic()
です(長かった).
static void
send_physnic (void *handle, unsigned int num_packets, void **packets,
unsigned int *packet_sizes, bool print_ok)
{
struct data2 *d2 = handle;
if (!print_ok && !d2->tdesc[0].initialized)
return;
send_physnic_sub (d2, num_packets, packets, packet_sizes, print_ok);
}
send_physnic()
はdescriptorが初期化されていればsend_physnic_sub()
を呼びます.
これが実際の送信処理をおこないます.
static void
send_physnic_sub (struct data2 *d2, UINT num_packets, void **packets,
UINT *packet_sizes, bool print_ok)
{
struct desc_shadow *s;
uint i, off2;
u32 *head, *tail, h, t, nt;
struct tdesc *td;
if (d2->d1->disable) /* PCI config reg is disabled */
return;
if (!(d2->tctl & 2)) /* !EN: Transmit Enable */
return;
s = &d2->tdesc[0]; /* FIXME: 0 only */
off2 = 0x3800; /* FIXME: 0 only */
write_mydesc (s, d2, off2, true);
head = (void *)((u8 *)d2->d1[0].map + off2 + 0x10);
tail = (void *)((u8 *)d2->d1[0].map + off2 + 0x18);
h = *head;
t = *tail;
if (h >= NUM_OF_TDESC || t >= NUM_OF_TDESC)
return;
for (i = 0; i < num_packets; i++) {
nt = t + 1;
if (nt >= NUM_OF_TDESC)
nt = 0;
if (h == nt) {
if (print_ok)
printf ("transmit buffer full\n");
break;
}
if (packet_sizes[i] >= TBUF_SIZE) {
if (print_ok)
printf ("transmit packet too large\n");
continue;
}
memcpy (s->u.t.tbuf[t], packets[i], packet_sizes[i]);
td = &s->u.t.td[t];
td->len = packet_sizes[i];
td->cso = 0;
td->cmd_eop = 1;
td->cmd_ifcs = 1;
td->cmd_ic = 0;
td->cmd_rs = 0;
td->cmd_rsv = 0;
td->cmd_dext = 0;
td->cmd_vle = 0;
td->cmd_ide = 0;
td->sta_dd = 0;
td->sta_ec = 0;
td->sta_lc = 0;
td->sta_rsv = 0;
td->reserved = 0;
td->css = 0;
td->special = 0;
t = nt;
}
*tail = t;
}
write_mydesc()
では,もしdescriptorのbaseアドレスをまだ設定していなかったら設定します.
その後,descriptorのエントリを適切に埋めて,最後にtailポインタを更新します.
前述のようにdescriptorは複数ありますが,BitVisor自体はLegacyのものを利用するようです.
パケットサイズがTBUF_SIZE
以下であれば最初からdescriptorのバッファへデータをコピーすればコピーが省略できる気がします.
さて,ゲストが利用するTx descriptorがLegacyで無かった場合,処理が長いので細かい解説は省略していますが,以下のようなことをしているようです.
- TCP/IP Context Descriptorの場合,
d2
にどのフィールドをoffload計算するかを記憶 - TCP/IP Data Descriptorの場合,offloadの計算が必要であればまずチェックサムを計算. その後Legacyと同様に
recvvirt_func()
を呼んで送信- offloadは実際はBitVisorが計算するようです
- パケットが分割されている場合 (複数のdescriptorのバッファで指定される場合), なるべく一つにまとめて送信する
- 分割送信に関しては仕様の3.6節に書いてあります.
パケットの受信
今度はパケットの受信についてみていきたいと思います.
パケットを受信すると,デバイスは割り込みを発生させます.割り込みの発生理由はICR (Interrupt Cause Register)と呼ばれるデバイスのレジスタに格納されています.
BitVisorでは基本割り込みはパススルーで,ゲストに対応させます.そして,ゲストが割り込みを処理する際にICRにアクセスをしてきたとき,実際にパケットを受信します. (実際はvirtioを利用する場合は異なる動作をすることがあります)
具体的には,mmhandler2()
でICRのアクセスをフックしています.
mmhandler2 (struct data *d1, struct data2 *d2, phys_t gphys, bool wr,
union mem *buf, uint len, u32 flags)
{
union mem *q;
...
if (rangecheck (gphys - d1->mapaddr, len, 0xC0, 4)) {
/* Interrupt Cause Read Register */
if (d2->rdesc[0].initialized)
receive_physnic (&d2->rdesc[0], d2, 0x2800);
if (d2->rdesc[1].initialized)
receive_physnic (&d2->rdesc[1], d2, 0x2900);
}
...
ここで,0xC0がICRのオフセットです.ここでreceive_physnic()
を呼んで受信処理をします.
この後実際に0xC0の値を読み込んでゲストに返します.
receive_physnic()
は以下のようになっています.
static void
receive_physnic (struct desc_shadow *s, struct data2 *d2, uint off2)
{
u32 *head, *tail, h, t, nt;
void *pkt[16];
UINT pktsize[16];
long pkt_premap[16];
int i = 0, num = 16;
struct rdesc *rd;
write_mydesc (s, d2, off2, false);
head = (void *)((u8 *)d2->d1[0].map + off2 + 0x10);
tail = (void *)((u8 *)d2->d1[0].map + off2 + 0x18);
h = *head;
t = *tail;
if (h >= NUM_OF_RDESC || t >= NUM_OF_RDESC)
return;
for (;;) {
nt = t + 1;
if (nt >= NUM_OF_RDESC)
nt = 0;
if (h == nt || i == num) {
if (d2->recvphys_func)
d2->recvphys_func (d2, i, pkt, pktsize,
d2->recvphys_param,
pkt_premap);
if (h == nt)
break;
i = 0;
}
t = nt;
rd = &s->u.r.rd[t];
pkt[i] = s->u.r.rbuf[t];
pktsize[i] = rd->len;
pkt_premap[i] = s->u.r.rbuf_premap[t];
if (!(d2->rctl & 0x4000000)) /* !SECRC */
pktsize[i] -= 4;
if (!rd->status_eop) {
printf ("status EOP == 0!!\n");
continue;
}
if (rd->err_ce) {
printf ("recv CRC error\n");
continue;
}
if (rd->err_se) {
printf ("recv symbol error\n");
continue;
}
if (rd->err_rxe) {
printf ("recv RX data error\n");
continue;
}
i++;
}
*tail = t;
}
ここでは最大16個分パケットを読み取りd2->recvphys_func()
を呼ぶのを全パケット処理するまで繰り返します.
なお,最初にreceive descriptorは一杯に埋めておくため,tail
ポインタの次のエントリから受信したパケットが入っていることになります.
d2->recvphys_func()
ですが,passモジュールの場合,送信の場合と同様に考えると,send_virtnic()
が呼ばれることが分かります.
static void
send_virtnic (void *handle, unsigned int num_packets, void **packets,
unsigned int *packet_sizes, bool print_ok)
{
struct data2 *d2 = handle;
struct desc_shadow *s;
uint i;
s = &d2->rdesc[0]; /* FIXME: 0 only */
for (i = 0; i < num_packets; i++)
sendvirt (d2, s, packets[i], packet_sizes[i]);
}
受信したパケットについて,sendvirt()
が呼ばれます.
この関数も少々長いですが,1) ゲストのRx DescriptorがLegacyかどうか, 2) ゲストのdescriptorのバッファサイズが十分な長さがあるかどうか, に応じて処理が別れています.
とりあえずゲストのバッファサイズが十分ある場合の処理を抜粋すると以下のようになります.
static void
sendvirt (struct data2 *d2, struct desc_shadow *s, u8 *pkt, uint pktlen)
{
...
i = s->head;
j = s->tail;
k = s->base.ll;
l = s->len;
if (d2->rctl & 0x4000000) /* SECRC: Strip CRC */
asize = 0;
pktlen += asize;
while (pktlen > 0) {
copied = 0;
if (i == j)
return;
rd = mapmem_gphys (k + i * 16, sizeof *rd, MAPMEM_WRITE);
...
buf = mapmem_gphys (rd->addr, bufsize, MAPMEM_WRITE);
ASSERT (buf);
if (pktlen <= bufsize) {
if (pktlen > asize) {
memcpy (buf, pkt, pktlen - asize);
memcpy (buf + pktlen - asize, abuf, asize);
} else {
/* asize >= pktlen */
memcpy (buf, abuf + (asize - pktlen), pktlen);
}
if (d2->rfctl & 0x8000) {
rd1 = (void *)rd;
rd1->mrq = 0;
rd1->rsshash = 0;
rd1->ex_sta = 0x7; /* DD, EOP, IXSM */
rd1->ex_err = 0; /* no errors */
rd1->len = pktlen;
rd1->vlantag = 0;
} else {
rd->len = pktlen;
rd->status_pif = 0;
rd->status_ipcs = 0;
rd->status_tcpcs = 0;
rd->status_udpcs = 0;
rd->status_vp = 0;
rd->status_eop = 1;
rd->status_dd = 1;
rd->err_rxe = 0;
rd->err_ipe = 0;
rd->err_tcpe = 0;
rd->err_seq = 0;
rd->err_se = 0;
rd->err_ce = 0;
rd->vlantag = 0;
}
copied = pktlen;
} else {
...
}
unmapmem (buf, bufsize);
unmapmem (rd, sizeof *rd);
pkt += copied;
pktlen -= copied;
i++;
if (i * 16 >= l)
i = 0;
}
s->head = i;
*(u32 *)(void *)((u8 *)d2->d1[0].map + 0xC8) |= 0x80; /* interrupt */
}
mapmem_gphys()
でゲストのdescriptorとそのdescriptorが指すバッファをマッピングします.
descriptorのバッファに受信したデータをコピーしたあと,descriptorの種類によって適当にwrite-backします.
(d2->rfctl & 0x8000
であればextened Rx descriptorです).
処理が終わった後はInterrupt Cause Set RegisterのRXT0 (Receiver Timer Interrupt)をセットします.
もしゲストのdescriptorのバッファサイズが小さい場合は,パケット分割してコピーしています.
ipモジュールの場合
ここまでpassモジュールを例に説明してきましたが,ipモジュールの場合,ゲストがデバイスにアクセスすることはないためpassのように受信はできません.
そこでBitVisorではどうしているかというと,ipモジュールのときはそれ専用にデバイスをpollingするためのスレッドをBitVisor内に立ち上げます (ip/net_main.c
のnet_thread()
).
このスレッドはBitVisorに制御が移った時に適当にスケジューリングされて実行されます.
従って,VMExitがほとんど発生せずBitVisorに制御がなかなか移らない場合はなかなかパケットが送受信できないことになります.
ttyの送信
BitVisorからログをネットワークで送信する際はどうしているかというと,ゲストがパケットを送信するのと同様にsend_physnic()
を呼んであげるだけです.
ttyオプションを有効にした場合,net_init()
の中でtty_phys_func
を設定しています.
bool
net_init (struct netdata *handle, void *phys_handle, struct nicfunc *phys_func,
void *virt_handle, struct nicfunc *virt_func)
{
if (!handle->func->init (handle->handle, phys_handle, phys_func,
virt_handle, virt_func))
return false;
if (handle->tty) {
handle->tty_phys_handle = phys_handle;
handle->tty_phys_func = phys_func;
}
return true;
}
そして,net_tty_send()
がtty_phys_func()
を使ってパケットを送信します.
static void
net_tty_send (void *tty_handle, void *packet, unsigned int packet_size)
{
struct netdata *handle = tty_handle;
char *pkt;
pkt = packet;
memcpy (pkt + 0, config.vmm.tty_mac_address, 6);
memcpy (pkt + 6, handle->mac_address, 6);
handle->tty_phys_func->send (handle->tty_phys_handle, 1, &packet,
&packet_size, false);
}
ちなみに,このnet_tty_send()
がいつ呼ばれるかというと,net_start()
したときにtty_udp_register()
でtty送信用にこのモジュールを登録しています.
void
net_start (struct netdata *handle)
{
struct nicinfo info;
if (handle->tty) {
handle->tty_phys_func->get_nic_info (handle->tty_phys_handle,
&info);
memcpy (handle->mac_address, info.mac_address,
sizeof handle->mac_address);
tty_udp_register (net_tty_send, handle);
}
handle->func->start (handle->handle);
}
tty_udp_register()
は core/tty.c
内にあって,リストにモジュールを追加します.
void
tty_udp_register (void (*tty_send) (void *handle, void *packet,
unsigned int packet_size), void *handle)
{
struct tty_udp_data *p;
p = alloc (sizeof *p);
p->tty_send = tty_send;
p->handle = handle;
LIST1_ADD (tty_udp_list, p);
}
printf()
などをしたとき,最終的にtty_udp_putchar()
(か,tty_udp_syslog_putchar()
) が呼ばれますが,この中で実際に送信しています.
static void
tty_udp_putchar (unsigned char c)
{
struct tty_udp_data *p;
unsigned int pktsiz;
char pkt[64];
if (config.vmm.tty_syslog.enable) {
tty_syslog_putchar (c);
return;
}
LIST1_FOREACH (tty_udp_list, p) {
memcpy (pkt + 12, "\x08\x00", 2);
pktsiz = mkudp (pkt + 14, "\x00\x00\x00\x00", 10,
"\xE0\x00\x00\x01", 10101, (char *)&c, 1) + 14;
p->tty_send (p->handle, pkt, pktsiz);
}
}
その他のネットワーク用ドライバ
BitVisorではPro/1000の他に,以下のネットワークドライバが存在します.
- pro100
- RTL8169
- bnx
- X540
- virtio
ドライバごとに対応している機能に制限がある場合があります.
例えば,X540のドライバは現状BitVisorがtty送信するためのもので,shadow descriptorを用いてゲストのI/Oをフックするような用途には対応していません.
まとめ
BitVisorのネットワークAPIと,pro1000の構造について書きました.
なんだかながながと書きましたが,BitVisorでパケットをフックして何かしたい,という場合passモジュールを使って,
netapi_net_pass_recv_callback()
の中でパケットをいじるか,ippassモジュールを使って net_ip_virt_recv()
or net_ip_phys_recv()
の中でパケットをいじるのが楽なんじゃないかなと思います.
もちろん,モジュールを新規に追加してもいいと思います.
参考資料
- [1] Intel, PCI/PCI-X Family of Gigabit Ethernet Controllers Software Developer’s Manual, https://www.intel.com/content/dam/doc/manual/pci-pci-x-family-gbe-controllers-software-dev-manual.pdf
- [2] Intel, PCIe* GbE Controllers Open Source Software Developer’s Manual, https://www.intel.com/content/dam/www/public/us/en/documents/manuals/pcie-gbe-controllers-open-source-manual.pdf
- [3] Intel, Intel® Ethernet Controller I350 Family, https://www.intel.co.jp/content/www/jp/ja/embedded/products/networking/ethernet-controller-i350-family-documentation.html
- [4] hdk_2, BitVisorのvmm.driver.pciの指定の仕方, https://qiita.com/hdk_2/items/f906c352f44a2afa6ad9