Zephyrのecho_serverサンプルを理解する

echo_server

github.com

Zephyrのecho_serverサンプルを解析し、Zephyrでのネットワークアプリケーション作成方法の理解を目指します。

一通り見てみると、割と普通のTCPサーバアプリケーションでした。

configs

重要そうなkernel configを確認します。

|> prf.conf

# Generic networking options
# ネットワーク機能を有効化します
CONFIG_NETWORKING=y
CONFIG_NET_UDP=y
CONFIG_NET_TCP=y
CONFIG_NET_IPV6=y
...

# Logging
# ログと統計情報出力を有効化します
CONFIG_NET_LOG=y
CONFIG_NET_STATISTICS=y
...

# Network shell
# Zephyrではsubsystemごとにshellを持ちます。
# ネットワークのshellを有効化しています。
CONFIG_NET_SHELL=y

今回は、enc28j60 Ethernet MACコントローラを使用します。 このコントローラ用のkernel configを確認します。

|> overlay-enc28j60

# L2 Ethernetはここで有効化しています。
CONFIG_NET_L2_ETHERNET=y

# ENC28J60 Ethernet Device
# driverを組み込みます
CONFIG_ETH_ENC28J60=y
# インスタンスを1つ作成します。
CONFIG_ETH_ENC28J60_0=y
# device treeで公開されるSPIのソフトウェアインタフェースを指定します。
CONFIG_ETH_ENC28J60_0_SPI_PORT_NAME="SPI_1"

Zephyrのおもしろいところは、device treeでハードウェアを定義して、kernel configでソフトウェアを定義する、という使い分けをしている点です。

SPI_1という名前のSPIポートを使用する設定をしています。このSPI_1は、device treeにおいて、実際のGPIOピンなどを利用したハードウェア定義に対してつけたラベルです。 これがZephyrにおけるソフトウェアインタフェースとして、kernel configで利用されます。

source files

いくつかのファイルに分割されています。トップは、echo-server.cです。

$ ls src/
common.h  echo-server.c  tcp.c  test_certs.h  udp.c  vlan.c

本記事では、TCPの動作を追っていきます。 udp.cvlan.cは解析対象外にします。

include header

まずは、includeしているheaderを確認します。

#include <zephyr.h>
#include <linker/sections.h>
#include <errno.h>

#include <net/net_pkt.h>
#include <net/net_core.h>
#include <net/net_context.h>
#include <net/net_app.h>

#include "common.h"

linker/sections.hが興味深いですが、どこで使われているのか、よくわかりませんでした…。

common.hは、関数が宣言されています。 後は、2つのマクロ定義があります。

#define MY_PORT 4242

#define MAX_DBG_PRINT 64

mainの開始と終了

echo_serverのmain()を見ていきます。

void main(void)
{
    init_app();

    if (IS_ENABLED(CONFIG_NET_TCP)) {
        start_tcp();
    }
...
    k_sem_take(&quit_lock, K_FOREVER);

    NET_INFO("Stopping...");

    if (IS_ENABLED(CONFIG_NET_TCP)) {
        stop_tcp();
    }
...
}

init_app()を見ます。

static struct k_sem quit_lock;
...

static inline int init_app(void)
{
    k_sem_init(&quit_lock, 0, UINT_MAX);

    // 今回は対象外です。
    init_vlan();

    return 0;
}

セマフォを使っています。

Zephyr doc Semaphoresを見る限りでは普通ですね。

k_sem_init()の第3引数では、セマフォカウントの初期値を0、リミットをUINT_MAXに設定しています。

main()に戻って、start_tcp()を一旦飛ばして、k_sem_take()が何をしているかを説明します。

 if (IS_ENABLED(CONFIG_NET_TCP)) {
        start_tcp();
    }
...
    k_sem_take(&quit_lock, K_FOREVER);

    NET_INFO("Stopping...");

    if (IS_ENABLED(CONFIG_NET_TCP)) {
        stop_tcp();
    }

k_sem_take(&quit_lock, K_FOREVER);では、第2引数においてミリ秒単位で指定した期間でセマフォが獲得できない場合、タイムアウトになります。 ここでは、K_FOREVERを指定しており、これはセマフォが取得できるまで、制限なく待ち続けることを意味します。

このアプリケーションは、セマフォが獲得できると、TCPを停止して、アプリケーションを終了します。では、セマフォはどうなると獲得できるのでしょうか? k_sem_init()セマフォの初期値は0になっているため、そのままではセマフォを取得できません。

答えは、下の関数にあります。

void quit(void)
{
    k_sem_give(&quit_lock);
}

ここで、セマフォを1つ解放しています。このquit()が呼ばれると、セマフォが1つインクリメントされるため、結果として、アプリケーションが終了します。

このquit()を呼ぶのは、TCPパケットの受信処理ハンドラ内です。 エコーに失敗すると、quit()を呼び出し、アプリケーションが終了する仕組みのようです。

static void tcp_received(struct net_app_ctx *ctx,
...
{
...
    ret = net_app_send_pkt(ctx, reply_pkt, NULL, 0, K_NO_WAIT,
                   UINT_TO_POINTER(net_pkt_get_len(reply_pkt)));
    if (ret < 0) {
        NET_ERR("Cannot send data to peer (%d)", ret);
        net_pkt_unref(reply_pkt);

        quit();
    }
}

TCPエコー

start_tcp()で何をしているか解析していきます。

 if (IS_ENABLED(CONFIG_NET_TCP)) {
        start_tcp();
    }

Zephyr Network Application API

本アプリケーションでは、Zephyr doc Network Application APIを主に利用します。

ドキュメントによると、シンプルなTCP serverアプリケーションは次ステップで作ることができます。

  1. net_app_init_tcp_server()IPアドレスTCPポートを設定する
  2. net_app_set_cb()イベントハンドラを登録する
  3. オプションで、net_app_server_tls()TLSを有効にする
  4. net_app_listen()でクライアントからの接続を待つ

すごく普通のTCPサーバアプリケーションですね。

start_tcp

まずは、関数全体を掲載します。

|> tcp.c

static struct net_app_ctx tcp;
...
void start_tcp(void)
{
    int ret;
    // `#define MY_PORT 4242` in common.h
    ret = net_app_init_tcp_server(&tcp, NULL, MY_PORT, NULL);
    if (ret < 0) {
        NET_ERR("Cannot init TCP service at port %d", MY_PORT);
        return;
    }

#if defined(CONFIG_NET_CONTEXT_NET_PKT_POOL)
    net_app_set_net_pkt_pool(&tcp, tx_tcp_slab, data_tcp_pool);
#endif

    ret = net_app_set_cb(&tcp, NULL, tcp_received, NULL, NULL);
    if (ret < 0) {
        NET_ERR("Cannot set callbacks (%d)", ret);
        net_app_release(&tcp);
        return;
    }

#if defined(CONFIG_NET_APP_TLS)
...
#endif

    net_app_server_enable(&tcp);

    ret = net_app_listen(&tcp);
    if (ret < 0) {
        NET_ERR("Cannot wait connection (%d)", ret);
        net_app_release(&tcp);
        return;
    }
}

メモリの設定をします。 これでアプリケーション専用のメモリプールが用意されます。

#if defined(CONFIG_NET_CONTEXT_NET_PKT_POOL)
    net_app_set_net_pkt_pool(&tcp, tx_tcp_slab, data_tcp_pool);
#endif

イベントハンドラを登録します。 connect, receive, send, closeのハンドラが登録できます。 ここでは、receiveだけ登録しています。

 ret = net_app_set_cb(&tcp, NULL, tcp_received, NULL, NULL);

後は、サーバは有効化して、listenするだけです。

 // サーバを有効化します
    net_app_server_enable(&tcp);

    // コネクションを待つ状態になります。
    ret = net_app_listen(&tcp);

これで、コネクションが確立して、データを受信すると、tcp_receivedのコールバックが呼ばれます。

tcp_received

パケット受信時の処理を見ていきます。受信したパケットからreply_pktを作成して、net_app_send_pkt()でpeerに送信しているだけです。

static void tcp_received(struct net_app_ctx *ctx,
             struct net_pkt *pkt,
             int status,
             void *user_data)
{
    struct net_pkt *reply_pkt;
...
    reply_pkt = build_reply_pkt(dbg, ctx, pkt, NET_TCPH_LEN);

    net_pkt_unref(pkt);
...
    ret = net_app_send_pkt(ctx, reply_pkt, NULL, 0, K_NO_WAIT,
                   UINT_TO_POINTER(net_pkt_get_len(reply_pkt)));
    if (ret < 0) {
        NET_ERR("Cannot send data to peer (%d)", ret);
        net_pkt_unref(reply_pkt);

        quit();
    }
}

build_reply_pkt()はアプリケーション内で定義している関数です。こちらは後で内容を見ます。 受信したパケットから、エコーバックするパケットを作成した後、net_pkt_unref(pkt)しています。

Zephyr doc Networking API / Network and application librariesによると、コールバックで渡されるpktの解放は、コールバック側がやらないといけないようです。

net_app_send_pkt()では、送信成功時はnet_app_send_pkt()reply_pktが消費されて、失敗時は自分で解放しています。 APIドキュメントを見ると、これが正しい使い方です。

If the function return < 0, then it is caller responsibility to unref the pkt.

C言語は、こういうところきついですね…。

build_reply_pkt()を見ていきます。受信したpktからデータをコピーして、reply_pktを返しています。

struct net_pkt *build_reply_pkt(const char *name,
                struct net_app_ctx *ctx,
                struct net_pkt *pkt,
                u8_t proto_len)
{
    struct net_pkt *reply_pkt;
    struct net_buf *frag;
...
    reply_pkt = net_app_get_net_pkt(ctx, net_pkt_family(pkt), BUF_TIMEOUT);
...
    net_pkt_set_appdatalen(reply_pkt, net_pkt_appdatalen(pkt));

    frag = net_pkt_copy_all(pkt, 0, BUF_TIMEOUT);
...
    net_pkt_frag_add(reply_pkt, frag);

    return reply_pkt;
}

大体、アプリケーションとしての使い方は理解できました。