NetBSD Advent Calendar 2022 21日目の記事です。今日はNPF(NetBSD Packet Filter)のソースコード中に出てきた、 libnv
の紹介を使用と思います。
libnvとは?
20日目の記事でNPFのソースコードを調べた際に、 nvlist_add_string()
という関数が出てきました。例えば、/usr/src/lib/libnpf/npf.cにある以下のような処理です。
629 nl_ext_t *
630 npf_ext_construct(const char *name)
631 {
...
639 nvlist_add_string(ext->ext_dict, "name", name);
640 return ext;
641 }
関数名に含まれている"nvlist"とは、そもそも一体何なのでしょうか?
libnv
のソースコードは /usr/src/sys/external/bsd/libnv/dist 以下に置かれています。
$ ls
CVS/ dnv.9 nv.9 nvlist.c
cnv.9 dnv.h nv.h nvlist_impl.h
cnv.h dnvlist.c nv_compat.h nvpair.c
cnvlist.c msgio.c nv_impl.h nvpair_impl.h
common_impl.h msgio.h nv_kern_netbsd.c
nv.9
が man 9 nv
で参照されるファイルのようです。なぜか man 9 nv
でマニュアルが引けないのですが、 mandoc
コマンドで整形したマニュアルにしてみます。
$ mandoc nv.9
NV(9) Kernel Developer's Manual NV(9)
NAME
nvlist_create, nvlist_destroy, nvlist_error, nvlist_set_error,
nvlist_empty, nvlist_flags, nvlist_exists, nvlist_free, nvlist_clone,
nvlist_dump, nvlist_fdump, nvlist_size, nvlist_pack, nvlist_unpack,
nvlist_send, nvlist_recv, nvlist_xfer, nvlist_in_array, nvlist_next,
nvlist_add, nvlist_move, nvlist_get, nvlist_take, nvlist_append - library
for name/value pairs
LIBRARY
Name/value pairs library (libnv, -lnv)
...
DESCRIPTION
The libnv library allows to easily manage name value pairs as well as
send and receive them over sockets. A group (list) of name value pairs
is called an nvlist. The API supports the following data types:
なるほど、どうやら libnv
は Name/value
のペアになったデータを扱うためのライブラリで、さらにソケット経由でデータをやりとりすることも可能なようですね。
libnvはBSD系のOSなら利用できる?
NetBSDでは man 9 nv
は存在していませんが、FreeBSDではlibnv(9)でマニュアルを引くことができます。少なくとも、FreeBSDとNetBSDでは libnv
が利用できるようです。
FreeBSDでは /lib/libnv.so.0
でライブラリが提供されていますが、NetBSDにはこのライブラリが存在していません…。 libnv
のソースコードは提供されているため、 libnv
のソースコードを一緒にコンパイルする(アプリに直接組み込む)ことで利用できるはずです。
が、後述する libnv
を使用したサンプルをコンパイルしようとすると、以下のエラーが発生します。
$ gcc \
/usr/src/sys/external/bsd/libnv/dist/*.c \
sample.c \
-o sample \
-I/usr/src/sys/external/bsd/libnv/dist/
ld: /tmp//cc8QVbnj.o: in function `main':
sample.c:(.text+0xc3): undefined reference to `nvlist_send'
ld: sample.c:(.text+0x11d): undefined reference to `nvlist_recv'
nvlist_send()
という関数が見つからないというエラーになっています。ソースコードをgrepしてみると、この関数は nvlist.c
で定義されています。
$ pushd /usr/src/sys/external/bsd/libnv/dist/
$ grep nvlist_send *.c
nv_kern_netbsd.c:nvlist_send_ioctl(int fd, unsigned long cmd, const nvlist_t *nvl)
nvlist.c:nvlist_send(int sock, const nvlist_t *nvl)
nvlist.c: if (nvlist_send(sock, nvl) < 0) {
nvlist_send()
がコンパイルされるための条件を見ると、 _KERNEL
と _STANDALONE
が「定義されていない」状態で、かつ WITH_MSGIO
マクロが定義されている場合にコンパイルされます。
grep -B3 ^nvlist_send nvlist.c
#if !defined(_KERNEL) && !defined(_STANDALONE) && defined(WITH_MSGIO)
int
nvlist_send(int sock, const nvlist_t *nvl)
さて、この WITH_MSGIO
マクロですが、NetBSDのソースツリーの中では nvlist.c
にしか出てきません。つまり、そのままだと常に nvlist_send()
が未定義になるという構成になっています。FreeBSDの libnv
では、このマクロ自体が存在していないため、NetBSDで独自に追加されているようですが、現状だと意味をなしていない(むしろ libnv
をユーザランドで利用する際の妨げになっている?)ように見えます。とりあえず、これ以上の深掘りは止めて、コンパイル時に -DWITH_MSGIO
を指定する方法で対応してみます。
libnvの簡単なサンプルを動かしてみる
libnv(9)のマニュアルを参照しながら libnv
の簡単なサンプルを作成してみます。基本的には以下の手順によりプロセス間でのデータ通信が行えます。
- データ送出側での処理
-
nvlist_create()
でname/valueリストを作成する。 -
nvlist_add_string()
nvlist_add_number()
等で、任意のデータ型のname/valueアイテムを設定する。 -
nvlist_send()
で相手側のプロセスにデータを送出する。 -
nvlist_destroy()
で確保したリソースを開放する。
-
- データ受信側での処理
-
nvlist_recv()
でname/valueリストを受信する。 -
nvlist_get_string()
nvlist_get_number()
等で値を取得する。値取得時には設定されているnameをキーに指定する。 -
nvlist_destroy()
で確保したリソースを開放する。
-
さっそくこの手順をコードに起こしてみます。具体的な実装は以下になります。
// libnvを使用して親・子プロセス間でデータをやり取りするサンプル。
#include <sys/socket.h>
#include <sys/types.h>
#include <sys/wait.h>
// 以下の場所にあるnv.hを参照する。
// /usr/src/sys/external/bsd/libnv/dist/
#include <nv.h>
#include <stdio.h>
#include <stdlib.h>
#include <fcntl.h>
#include <unistd.h>
#include <string.h>
int main(int argc, char *argv[])
{
int r;
int socks[2];
int status;
pid_t pid;
r = socketpair(PF_UNIX, SOCK_STREAM, 0, socks);
if (r == -1) {
fprintf(stderr, "Error: sockpair()\n");
exit(1);
}
pid = fork();
if (pid == -1) {
} else if (pid != 0) {
// 親プロセス(データ送信側)
nvlist_t *nvl;
close(socks[0]);
nvl = nvlist_create(0);
// name/valueの形式で値をセットする。
nvlist_add_string(nvl, "message", "hello,world");
nvlist_add_number(nvl, "value", (uint64_t)123456);
// 文字列と数値を送出する。
nvlist_send(socks[1], nvl);
nvlist_destroy(nvl);
waitpid(pid, &status, 0);
fprintf(stderr, "waitpid status= %d\n", WIFEXITED(status));
} else {
// 子プロセス(データ受信側)
nvlist_t *nvl;
const char *message;
uint64_t value;
nvl = nvlist_recv(socks[0], 0);
if (nvl == NULL) {
fprintf(stderr, "Error: nvlist_recv()\n");
exit(1);
}
// 受信したデータの取り出し。
message = nvlist_get_string(nvl, "message");
value = nvlist_get_number(nvl, "value");
printf("message= '%s', value= %llu\n", message, value);
nvlist_destroy(nvl);
fprintf(stderr, "quit child process.\n");
}
return 0;
}
コンパイルとプログラムの実行は以下の手順で行います。親プロセスと子プロセスの間で、文字列("hello,world")と値( 123456
)が送受信されていることが確認できます。
$ gcc \
-DWITH_MSGIO \
/usr/src/sys/external/bsd/libnv/dist/*.c \
sample.c \
-o sample \
-I/usr/src/sys/external/bsd/libnv/dist/
$
$ ./sample
message= 'hello,world', value= 123456
quit child process.
waitpid status= 1
まとめ
libnv
の紹介と簡単なサンプルを作成してみました。C言語ではプロセス間で任意の型のデータを送受信するのは地味に手間なので、こういったライブラリで手順が一般化されているのは助かります。また、今回の記事では触れませんでしたが、NPF(NetBSD Packet Filter)では、ユーザランドとカーネル空間でのデータ送受信にも利用されており、だいぶ汎用性のある機能となっています。