3
2

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 5 years have passed since last update.

BitVisorのlwIP機能の解説

Last updated at Posted at 2018-12-10

これはQiitaの下書きをふと確認したら発見した,昨年のAdvent Calendar用の記事になります.多分.
文章は結構前に書いたものだと思うので(記憶がない..),もしかすると最新版のコードと差異があるかもしれません.BitVisor 1.4~2.0あたりのコードを参照していると思います.


前回BitVisorのネットワークAPI(主にpassモジュール)とpro1000ドライバについて書きました.
ipあるいはippassモジュールを利用することでBitVisor内でlwIP機能が使えるようになります.
ここではlwIP概要と,BitVisorにおけるlwIP機能の処理の流れおよびechoサーバの実装について簡単に説明します.
lwIPに関連するコードはip/以下に存在します.

lwIPの概要

lwIPは軽量なTCP/IPスタックの実装であり,BSDライセンスであることもあって主に組み込み製品などで利用されています.
BitVisorはlwIPのnosysモードを利用しています.
これはsocket等のサポートがなく,システム全体で一つの実行スレッドしか持てませんが,その分移植が容易です.

nosysモードでは,lwIPを利用する流れはおおまかに以下のようになります.
基本はコールバック関数を設定しておいて,lwIPが必要に応じてそれを呼び出す流れになります.

  • lwIPの初期化
    • インタフェース(netif) の設定
    • lwIPがパケットを送信するための関数の登録などをする
  • サーバの追加 (listen)
    • tcp_new()でPCB (protocol control blockを追加)
    • tcp_bind(), tcp_listen()
    • tcp_accept()でacceptした際に呼ばれるコールバック関数を設定
  • lwIPの実行スレッドから netif->input()を使ってTCP/IPスタックへパケットを送る
    • あとはlwIPがパケットを解釈し,必要に応じてtcp_accept()で指定したコールバック関数や,tcp_accept()のコールバック関数の中で設定されたコールバック関数を呼び出す.

lwIP側からコネクションを貼ることも可能です.

lwIPに関する詳細はlwIPのドキュメントを参照してください.

BitVisor内でのlwIP処理の流れ

ipあるいはippassモジュールがドライバによって開始されると,以下のnet_ip_start()が呼ばれます.

static void
net_ip_start (void *handle)
{
	struct net_ip_data *p = handle;

	p->phys_func->set_recv_callback (p->phys_handle, net_ip_phys_recv, p);
	if (p->virt_func)
		p->virt_func->set_recv_callback (p->virt_handle,
						 net_ip_virt_recv, p);
	thread_new (net_thread, p, VMM_STACKSIZE);
}

ここで,passモジュールなどと同様にコールバック関数の設定をおこなった後,net_threadという新しいスレッドを立ち上げています.
このスレッドでlwIPの処理をおこないます.

net_thread()は以下のようになっています.

static void
net_thread (void *arg)
{
	struct net_ip_data *p = arg;
	struct ip_main_netif netif_arg[1];

	netif_arg[0].handle = p;
	netif_arg[0].use_as_default = 1;
	netif_arg[0].use_dhcp = config.ip.use_dhcp;
	netif_arg[0].ipaddr = config.ip.ipaddr;
	netif_arg[0].netmask = config.ip.netmask;
	netif_arg[0].gateway = config.ip.gateway;
	ip_main_init (netif_arg, 1);
	for (;;) {
		ip_main_task ();
		net_task_call ();
		schedule ();
	}
}

まず.ip_main_init()でlwIP機能の初期化をおこないます.

void
ip_main_init (struct ip_main_netif *netif_arg, int netif_num)
{
	/* Initialize TCP/IP Stack. */
	lwip_init ();

	/* Allocate context. */
	tcpip_context = mem_malloc (sizeof *tcpip_context);
	LWIP_ASSERT ("tcpip_context", tcpip_context);
	memset (tcpip_context, 0, sizeof *tcpip_context);

	/* Configure and add network interface. */
	tcpip_netif_conf (netif_arg, netif_num);
}

tcpip_netif_conf()の中でlwIPのデータ構造であるnetifの初期化をおこなっています.

static void
tcpip_netif_conf (struct ip_main_netif *netif_arg, int netif_num)
{
	struct netif *netif;
	int i;

	/* Allocate network interface. */
	netif = mem_malloc (sizeof *tcpip_context->netif * netif_num);
	LWIP_ASSERT ("netif", netif);
	tcpip_context->netif = netif;
	tcpip_context->netif_num = netif_num;
	tcpip_context->oldip_addr = mem_malloc (sizeof *tcpip_context->
						oldip_addr * netif_num);
	LWIP_ASSERT ("tcpip_context->oldip_addr", tcpip_context->oldip_addr);

	for (i = 0; i < netif_num; i++)
		tcpip_netif_conf_sub (&netif[i],
				      netif_arg[i].ipaddr,
				      netif_arg[i].netmask,
				      netif_arg[i].gateway,
				      netif_arg[i].use_as_default,
				      netif_arg[i].use_dhcp,
				      netif_arg[i].handle,
				      &tcpip_context->oldip_addr[i]);
}

static void
tcpip_netif_conf_sub (struct netif *netif, unsigned char *ipaddr_a,
		      unsigned char *netmask_a, unsigned char *gw_a,
		      int use_as_default, int use_dhcp, void *handle,
		      ip_addr_t *oldip_addr)
{
	ip_addr_t ipaddr, netmask, gw;

	/* Setup IP address etc. */
	IP4_ADDR (&ipaddr, ipaddr_a[0], ipaddr_a[1], ipaddr_a[2], ipaddr_a[3]);
	IP4_ADDR (&netmask, netmask_a[0], netmask_a[1], netmask_a[2],
		  netmask_a[3]);
	IP4_ADDR (&gw, gw_a[0], gw_a[1], gw_a[2], gw_a[3]);
	IP4_ADDR (oldip_addr, 0, 0, 0, 0);

	/* Register the network interface. */
	netif_add (netif, &ipaddr, &netmask, &gw, handle, ip_netif_init,
		   ethernet_input);

	/* Some additional configuration... */
	if (use_as_default) {
		/* Use the interface as default one. */
		netif_set_default (netif);
	}
	if (use_dhcp) {
		/* Dynamic IP assignment by DHCP. */
		dhcp_start (netif);
	} else {
		/* Static IP assignment. */
		netif_set_up (netif);
	}
}

static err_t
ip_netif_init (struct netif *netif)
{
	netif->name[0] = 'v';
	netif->name[1] = 'm';
	netif->output = etharp_output;
	netif->linkoutput = ip_netif_output;
	netif->hwaddr_len = ETHARP_HWADDR_LEN;
	net_main_get_mac_address (netif->state, netif->hwaddr);
	netif->mtu = 1500;
	netif->flags = NETIF_FLAG_BROADCAST | NETIF_FLAG_ETHARP |
		NETIF_FLAG_LINK_UP;
	net_main_set_recv_arg (netif->state, netif);
	return ERR_OK;
}


void
net_main_set_recv_arg (void *handle, void *arg)
{
	struct net_ip_data *p = handle;

	p->input_arg = arg;
	p->input_ok = true;
}

netif_add()dhcp_start(), netif_set_up() などはlwIPの関数です.
ちょっと処理が入り組んでいますが,netif_add()の中でnetifをセットアップします.
netif_add()の引数として渡したhandlenetif->stateとして設定されます.
netif_add()の中でip_netif_init()が呼ばれ,その中のnet_main_set_recv_argによってhandlenetifを登録しています.
このhandleはもともと何だったかというと,net_new_nic()したときに作成したnet_ip_dataでした.
ネットワークAPIのコールバック関数のparamとしてこのhandleが登録されているので,コールバック関数からnetifが参照できます.

また,netif->linkoutputでlwIPがパケットを送信するための関数を登録しています.以下のように,最終的にはphys_func->send()でパケットを送信します.

static err_t
ip_netif_output (struct netif *netif, struct pbuf *p)
{
	char buf[1600];
	int offset = 0, len;

	if (p && !p->next) {
		/* Fast path */
		net_main_send (netif->state, p->payload, p->len);
		return ERR_OK;
	}
	for (; p; p = p->next) {
		len = p->len;
		if (offset + len > sizeof buf)
			len = sizeof buf - offset;
		memcpy (buf + offset, p->payload, len);
		offset += len;
	}
	net_main_send (netif->state, buf, offset);
	return ERR_OK;
}

void
net_main_send (void *handle, void *buf, unsigned int len)
{
	struct net_ip_data *p = handle;

	p->phys_func->send (p->phys_handle, 1, &buf, &len, true);
}

初期化を行なった後は無限ループで処理をおこないます.
ここで,ip_main_task()ではtcpip_netif_poll()によってパケットの受信をおこないます.
これは実際にはドライバが設定したpoll関数を呼び出します.
ipモジュールを利用する場合,ゲストからはNICが見えず割り込みが発生しないため,明示的にこの関数を呼んでパケットを受信します.

void
ip_main_task (void)
{
	/* Check IP address change of network interface. */
	tcpip_netif_ipaddr_check ();

	/* Handle packet reception. */
	tcpip_netif_poll ();

	/* Do timer related tasks. */
	sys_check_timeouts ();
}

前回の話を思い出すと,パケットを受信した際にコールバック関数が呼ばれます.ipあるいはippassモジュールの場合は以下が呼ばれます.

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->virt_func->send ()はゲストOS側へパケットを渡す関数 (passモジュールと同様)です.
net_main_input_queue()で,BitVisorのlwIPスレッドが処理するために受信したパケットと,それを処理するための関数(net_main_input_directをキューに追加します.

static void
net_main_input_queue (struct net_ip_data *p, void **packets,
		      unsigned int *packet_sizes, unsigned int num_packets)
{
	struct net_ip_input_data *data;
	unsigned int i;

	for (i = 0; i < num_packets; i++) {
		/* Note: pbuf_alloc() must be called in the network
		 * thread, but this function is not. */
		/* FIXME: memcpy() is called twice for every input
		 * packet. Copying to pbuf directly is better. */
		data = alloc (sizeof *data);
		data->p = p;
		data->buf = alloc (packet_sizes[i]);
		data->len = packet_sizes[i];
		memcpy (data->buf, packets[i], packet_sizes[i]);
		net_main_task_add (net_main_input_direct, data);
	}
}

void
net_main_task_add (void (*func) (void *arg), void *arg)
{
	struct net_task *p;

	p = alloc (sizeof *p);
	p->func = func;
	p->arg = arg;
	spinlock_lock (&net_task_lock);
	LIST1_ADD (net_task_list, p);
	spinlock_unlock (&net_task_lock);
}

さて,ここまでがnet_thread()のループ内でパケットを受信した際に実行される処理です.
この後,net_task_call()により実際に受信したパケットを処理していきます.

static void
net_task_call (void)
{
	struct net_task *p;

	spinlock_lock (&net_task_lock);
	while ((p = LIST1_POP (net_task_list))) {
		spinlock_unlock (&net_task_lock);
		p->func (p->arg);
		free (p);
		spinlock_lock (&net_task_lock);
	}
	spinlock_unlock (&net_task_lock);
}

ここで,先ほど追加したデータならp->funcnet_main_input_direct(), p->argは受信したパケットを含むデータ(net_ip_input_data構造体)です.

static void
net_main_input_direct (void *arg)
{
	struct net_ip_input_data *data = arg;

	ip_main_input (data->p->input_arg, data->buf, data->len);
	free (data->buf);
	free (data);
}

で,結局ip_main_input()が呼ばれるわけですが,これは以下のようになっています.

void
ip_main_input (void *arg, void *buf, unsigned int len)
{
	struct netif *netif = arg;
	struct eth_hdr *ethhdr;
	struct pbuf *p, *q;
	int offset;

	ethhdr = buf;
	switch (ntohs (ethhdr->type)) {
	case ETHTYPE_IP:
	case ETHTYPE_ARP:
		p = pbuf_alloc (PBUF_RAW, len, PBUF_POOL);
		offset = 0;
		LWIP_ASSERT ("pbuf_alloc", p);
		LWIP_ASSERT ("p->tot_len == len", p->tot_len == len);
		for (q = p; q; q = q->next) {
			LWIP_ASSERT ("q->payload", q->payload);
			LWIP_ASSERT ("offset + q->len <= len",
				     offset + q->len <= len);
			memcpy (q->payload, buf + offset, q->len);
			offset += q->len;
		}
		LWIP_ASSERT ("offset == len", offset == len);
		LWIP_ASSERT ("offset > 12 + 4", offset > 12 + 4);
		LWIP_ASSERT ("p->len > 12 + 4", p->len > 12 + 4);
		if (netif->input (p, netif) != ERR_OK)
			printf ("IP/ARP Input Error.\n");
		break;
	}
}

最終的にnetif->input()で受信したパケットをTCP/IPスタックへ渡します.
ARPでパケットであればlwIPが応答を返します.

なお,BitVisorのスレッドは全てnon-preemptiveなため,ループ内で明示的にschedule()を呼んでいます.

lwIPを利用したサーバ

ここまででlwIPのスレッドが動作し,ARP応答をlwIPが返してくれるようになりましたが,実際に何か有用なことをするにはサーバを立てる必要があります.
ip/echo-server.cにechoサーバの例があるので,この動作について簡単に説明します.

echoサーバを立てるためにechoctlというメッセージハンドラがip/echoctl.cで定義されており,dbgshからechoctl; server start 8888とかやるとポート8888でechoサーバがlistneするようになります.
これはどうなっているのかというと,echoctl.cの中でサーバを立ち上げるためにtcpip_begin (echoctl_echo_server_start, a);を呼んでいます.
この関数はnet_main_task_add()で関数を登録するたけです.lwIPを実行するスレッドでサーバを立ち上げなければならないためこうしているわけです.

void
tcpip_begin (tcpip_task_fn_t *func, void *arg)
{
	net_main_task_add (func, arg);
}

lwIPのスレッドの中でechoctl_echo_server_start()が実行されます.

static void
echoctl_echo_server_start (void *arg)
{
	struct arg *a = arg;

	echo_server_init (a->port);
	free (a);
}

void
echo_server_init (int port)
{
  echo_pcb = tcp_new();
  if (echo_pcb != NULL)
  {
    err_t err;

    err = tcp_bind(echo_pcb, IP_ADDR_ANY, port);
    if (err == ERR_OK)
    {
      echo_pcb = tcp_listen(echo_pcb);
      tcp_accept(echo_pcb, echo_accept);
    }
    else 
    {
      /* abort? output diagnostic? */
    }
  }
  else
  {
    /* abort? output diagnostic? */
  }
}

最終的に呼ばれるecho_server_init()の中で,tcp_bind(), tcp_listen()によっていわゆるソケットにbindとlistenをし,さらにtcp_accept()で実際にacceptした際のコールバック関数を設定します.
netif->input()でlwIPのTCP/IPスタックにパケットを渡した際,条件にあうものがあればこのtcp_accept()で登録した関数を呼んでくれます.

もし外部からechoサーバへのアクセスがあればecho_accept()が実行されます.

static err_t
echo_accept(void *arg, struct tcp_pcb *newpcb, err_t err)
{
  err_t ret_err;
  struct echo_state *es;

  LWIP_UNUSED_ARG(arg);
  LWIP_UNUSED_ARG(err);

  /* commonly observed practive to call tcp_setprio(), why? */
  tcp_setprio(newpcb, TCP_PRIO_MIN);

  es = (struct echo_state *)mem_malloc(sizeof(struct echo_state));
  if (es != NULL)
  {
    es->state = ES_ACCEPTED;
    es->pcb = newpcb;
    es->retries = 0;
    es->p = NULL;
    /* pass newly allocated es to our callbacks */
    tcp_arg(newpcb, es);
    tcp_recv(newpcb, echo_recv);
    tcp_err(newpcb, echo_error);
    tcp_poll(newpcb, echo_poll, 0);
    ret_err = ERR_OK;
  }
  else
  {
    ret_err = ERR_MEM;
  }
  return ret_err;

この中のtcp_recv()でパケットを受信した際に呼ばれるコールバック関数を設定しています.

static err_t
echo_recv(void *arg, struct tcp_pcb *tpcb, struct pbuf *p, err_t err)
{
  struct echo_state *es;
  err_t ret_err;

  LWIP_ASSERT("arg != NULL",arg != NULL);
  es = (struct echo_state *)arg;
  if (p == NULL)
  {
    /* remote host closed connection */
    es->state = ES_CLOSING;
    if(es->p == NULL)
    {
       /* we're done sending, close it */
       echo_close(tpcb, es);
    }
    else
    {
      /* we're not done yet */
      tcp_sent(tpcb, echo_sent);
      echo_send(tpcb, es);
    }
    ret_err = ERR_OK;
  }
  else if(err != ERR_OK)
  {
    /* cleanup, for unknown reason */
    if (p != NULL)
    {
      es->p = NULL;
      pbuf_free(p);
    }
    ret_err = err;
  }
  else if(es->state == ES_ACCEPTED)
  {
    /* first data chunk in p->payload */
    es->state = ES_RECEIVED;
    /* store reference to incoming pbuf (chain) */
    es->p = p;
    /* install send completion notifier */
    tcp_sent(tpcb, echo_sent);
    echo_send(tpcb, es);
    ret_err = ERR_OK;
  }
  else if (es->state == ES_RECEIVED)
  {
    /* read some more data */
    if(es->p == NULL)
    {
      es->p = p;
      tcp_sent(tpcb, echo_sent);
      echo_send(tpcb, es);
    }
    else
    {
      struct pbuf *ptr;

      /* chain pbufs to the end of what we recv'ed previously  */
      ptr = es->p;
      pbuf_chain(ptr,p);
    }
    ret_err = ERR_OK;
  }
  else if(es->state == ES_CLOSING)
  {
    /* odd case, remote side closing twice, trash data */
    tcp_recved(tpcb, p->tot_len);
    es->p = NULL;
    pbuf_free(p);
    ret_err = ERR_OK;
  }
  else
  {
    /* unknown es->state, trash data  */
    tcp_recved(tpcb, p->tot_len);
    es->p = NULL;
    pbuf_free(p);
    ret_err = ERR_OK;
  }
  return ret_err;
}

echo_recv()は少々長いですが,TCP/IPの状態によって場合分けされていて,ES_RECEIVEDでデータを受信していたらecho_send()を使ってデータを送り返します.またtcp_sent()で送信後に呼ばれるコールバック関数を設定しています.コネクションがクローズされたらそれに応じた適切な処理をおこないます.

lwIPではこのようにコールバック関数を繋げて処理を書いていきます.

まとめ

BitVisroのlwIP機能について簡単に説明しました.ipモジュールもしくはippassモジュールを使えばlwIP機能がセットアップされるので,後はechoサーバを参考にすれば比較的簡単にTCP/IPサーバが書けると思います.
一つ注意点としてはlwIPの機能はそれ専用のスレッドから呼ぶ必要があります.
サーバを立ち上げる際はechoサーバのようにtcpip_begin()を使うか,ipモジュール開始時からサーバを立てたいのであればip_main_init()直後にサーバを立ち上げる処理をnet_thread()の中に追加してしまってもよいと思います.
また現在複数のNICでipモジュールを使うことはできないようになっています(net_new_ip_nic()の時点で失敗する).

3
2
1

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
3
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?