Zephyrのecho_serverサンプルを理解する
echo_server
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.c
とvlan.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アプリケーションは次ステップで作ることができます。
net_app_init_tcp_server()
でIPアドレスとTCPポートを設定するnet_app_set_cb()
でイベントハンドラを登録する- オプションで、
net_app_server_tls()
でTLSを有効にする 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; }
大体、アプリケーションとしての使い方は理解できました。