CUBE SUGAR CONTAINER

技術系のこと書きます。

C: glibc のバージョンをライブラリ関数・定数から取得する

今回は glibc のバージョンをライブラリ関数と定数から取得する方法について。 結論から先に述べると gnu_get_libc_version(3) か、定数の __GLIBC____GLIBC_MINOR__ から得られる。

使った環境は次のとおり。

$ lsb_release -a
No LSB modules are available.
Distributor ID: Ubuntu
Description:    Ubuntu 20.04.3 LTS
Release:    20.04
Codename:   focal
$ uname -rm
5.4.0-92-generic aarch64
$ /lib/aarch64-linux-gnu/libc.so.6
GNU C Library (Ubuntu GLIBC 2.31-0ubuntu9.2) stable release version 2.31.
Copyright (C) 2020 Free Software Foundation, Inc.
This is free software; see the source for copying conditions.
There is NO warranty; not even for MERCHANTABILITY or FITNESS FOR A
PARTICULAR PURPOSE.
Compiled by GNU CC version 9.3.0.
libc ABIs: UNIQUE ABSOLUTE
For bug reporting instructions, please see:
<https://bugs.launchpad.net/ubuntu/+source/glibc/+bugs>.

もくじ

下準備

あらかじめ C のソースコードをビルドするのに必要なパッケージをインストールしておく。

$ sudo apt-get update
$ sudo apt-get -y install build-essential

ソースコード

早速だけど以下にサンプルコードを示す。 ライブラリ関数であれば gnu_get_libc_version(3) から、定数だと __GLIBC____GLIBC_MINOR__ でバージョンが得られる。 また、gnu_get_libc_release(3) にはリリース情報が入っている。

#include <stdio.h>
#include <stdlib.h>
#include <gnu/libc-version.h>

int main(int argc, char *argv[]) {
    printf("GNU libc version: %s\n", gnu_get_libc_version());
    printf("GNU libc release: %s\n", gnu_get_libc_release());
    printf("GNU libc version (constant): %d.%d\n", __GLIBC__, __GLIBC_MINOR__);
    return EXIT_SUCCESS;
}

上記に適当な名前をつけてビルドする。

$ gcc -std=c11 -Wall example.c

実行すると、次のとおりちゃんとバージョンが取得できている。

$ ./a.out 
GNU libc version: 2.31
GNU libc release: stable
GNU libc version (constant): 2.31

いじょう。

Linux の UTS Namespace について

Linux のコンテナ仮想化を構成する要素の 1 つに、カーネルの Namespace (名前空間) という機能がある。 Namespace には色々とあるけど、今回はホスト名と NIS (Network Information Service) 1 ドメイン名を隔離する仕組みを提供している UTS Namespace について扱ってみる。

具体的には、unshare(1) と unshare(2) を使ってホスト名が隔離される様子を観察する。 unshare(2) というのは Namespace を操作するためのシステムコール。 そして、unshare(1) は同名のシステムコールを利用したコマンドラインツールになっている。

使った環境は次のとおり。

$ cat /etc/lsb-release 
DISTRIB_ID=Ubuntu
DISTRIB_RELEASE=20.04
DISTRIB_CODENAME=focal
DISTRIB_DESCRIPTION="Ubuntu 20.04.3 LTS"
$ uname -rm
5.4.0-92-generic aarch64
$ gcc --version
gcc (Ubuntu 9.3.0-17ubuntu1~20.04) 9.3.0
Copyright (C) 2019 Free Software Foundation, Inc.
This is free software; see the source for copying conditions.  There is NO
warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.

もくじ

下準備

まずは、事前に必要なパッケージをインストールしておく。 unshare(1) を使うために util-linux を、C のコードをビルドするために build-essential を入れておく。

$ sudo apt-get update
$ sudo apt-get install -y util-linux build-essential

unshare(1) を使って UTS Namespace を使ってみる

まずは unshare(1) から UTS Namespace を使ってみよう。 あらかじめ、システムのホスト名と NIS ドメイン名を確認しておく。

$ hostname
focal
$ domainname
(none)

上記から、ホスト名は focal で、NIS ドメイン名は設定されていないことが分かる。

現在の Namespace の情報も確認しておこう。 procfs の内容から、今は 4026531838 という識別子の UTS Namespace であることが確認できる。

$ file /proc/$$/ns/uts
/proc/949/ns/uts: symbolic link to uts:[4026531838]

続いて、unshare(1) を使って新しく UTS Namespace を作って bash を立ち上げる。

$ sudo unshare --uts bash

procfs の内容から、UTS Namespace の識別子が 4026532128 に変わったことが分かる。

# file /proc/$$/ns/uts
/proc/1068/ns/uts: symbolic link to uts:[4026532128]

ここで hostname(1) と domainname(1) を使ってホスト名と NIS ドメイン名を変更してみよう。

# hostname host.example.com
# domainname example

次のとおり、ちゃんと変更された。

# hostname
host.example.com
# domainname
example

しかし、上記の変更はあくまで新しく作られた UTS Namespace 上での操作に過ぎないはず。 exit して、元のシェルに戻ってみよう。

# exit
exit

UTS Namespace の識別子は、もちろん元に戻る。

$ file /proc/$$/ns/uts
/proc/949/ns/uts: symbolic link to uts:[4026531838]

ホスト名と NIS ドメイン名も元に戻っている。 つまり、ホスト名と NIS ドメイン名は、ちゃんと UTS Namespace によって隔離されていた。

$ hostname
focal
$ domainname
(none)

unshare(2) を使って UTS Namespace を使ってみる

続いては unshare(2) を使って UTS Namespace を使ってみよう。 今度は C のコードを使うことになるけど、長くなるのでホスト名とドメイン名で分けることにする。 といっても、両者は呼び出すシステムコールが {set,get}hostname(2) になるか {set,get}domainname(2) になるか位しか違いはない。

ホスト名を変更する

まずはホスト名から。 コメントに処理の説明は書いてあるけど、unshare(1) で UTS Namespace を新しく作ってから sethostname(2) と gethostname(2) を発行している。 そして、最終的には bash を起動している。

#define _GNU_SOURCE
  
#include <sched.h>
#include <stdlib.h>
#include <stdio.h>
#include <errno.h>
#include <string.h>
#include <unistd.h>

int main(int argc, char *argv[]) {
    // UTS Namespace を作成する
    if (unshare(CLONE_NEWUTS) != 0) {
        fprintf(stderr, "Failed to create a new UTS namespace: %s\n", strerror(errno));
        exit(EXIT_FAILURE);
    }

    // sethostname(2) でホスト名を変更する
    char const hostname[] = "host.example.com";
    if (sethostname(hostname, strlen(hostname)) != 0) {
        fprintf(stderr, "Failed to set hostname: %s\n", strerror(errno));
        exit(EXIT_FAILURE);
    }

    // gethostname(2) でホスト名を確認する
    char hostname_buf[254];
    if (gethostname(hostname_buf, sizeof(hostname_buf) / sizeof(hostname_buf[0])) != 0) {
        fprintf(stderr, "Failed to get hostname: %s\n", strerror(errno));
        exit(EXIT_FAILURE);
    }
    printf("gethostname: %s\n", hostname_buf);

    // execvp(3) でシェルを起動する
    char* const args[] = {"bash", NULL};
    if (execvp(args[0], args) != 0) {
        fprintf(stderr, "Failed to exec \"%s\": %s\n", args[0], strerror(errno));
        exit(EXIT_FAILURE);
    }

    return EXIT_SUCCESS;
}

上記をコンパイルする。

$ gcc -Wall example.c 

できたバイナリを実行する。

$ sudo ./a.out 
gethostname: host.example.com

sethostname(2) で変更されたホスト名が gethostname(2) で取得できている。

また、立ち上がったシェルを hostname(1) を発行しても、ちゃんとホスト名が変更されていることがわかる。

# hostname
host.example.com

procfs で確認できる UTS Namespace の識別子もちゃんと変わっている。 どうも識別子は使い回されてるっぽいけど。

# file /proc/$$/ns/uts
/proc/1288/ns/uts: symbolic link to uts:[4026532128]

もちろん、シェルから抜ければホスト名は元に戻る。

# exit
exit
$ hostname
focal

NIS ドメイン名を変更する

同様に NIS ドメイン名でも試してみる。 基本的にさっきのコードで発行するシステムコールが setdomainname(2) と getdomainname(2) に変わっただけ。

#define _GNU_SOURCE
  
#include <sched.h>
#include <stdlib.h>
#include <stdio.h>
#include <errno.h>
#include <string.h>
#include <unistd.h>

int main(int argc, char *argv[]) {
    // UTS Namespace を作成する
    if (unshare(CLONE_NEWUTS) != 0) {
        fprintf(stderr, "Failed to create a new UTS namespace: %s\n", strerror(errno));
        exit(EXIT_FAILURE);
    }

    // setdomainname(2) でNIS ドメイン名を変更する
    char const hostname[] = "example";
    if (setdomainname(hostname, strlen(hostname)) != 0) {
        fprintf(stderr, "Failed to set NIS domain name: %s\n", strerror(errno));
        exit(EXIT_FAILURE);
    }

    // getdomainname(2) で NIS ドメイン名を確認する
    char hostname_buf[64];
    if (getdomainname(hostname_buf, sizeof(hostname_buf) / sizeof(hostname_buf[0])) != 0) {
        fprintf(stderr, "Failed to get NIS domain name: %s\n", strerror(errno));
        exit(EXIT_FAILURE);
    }
    printf("getdomainname: %s\n", hostname_buf);

    // execvp(3) でシェルを起動する
    char* const args[] = {"bash", NULL};
    if (execvp(args[0], args) != 0) {
        fprintf(stderr, "Failed to exec \"%s\": %s\n", args[0], strerror(errno));
        exit(EXIT_FAILURE);
    }

    return EXIT_SUCCESS;
}

上記をコンパイルする。

$ gcc -Wall example.c 

できたバイナリを実行する。

$ sudo ./a.out 
getdomainname: example

ちゃんと NIS ドメイン名が変更されている。

立ち上がったシェルで domainnname(1) を実行しても、変更されていることがわかる。

# domainname
example

procfs で確認できる UTS Namespace の識別子もちゃんと変わっている。

# file /proc/$$/ns/uts
/proc/1338/ns/uts: symbolic link to uts:[4026532128]

もちろん、シェルから抜ければホスト名は元に戻る。

# exit
exit
$ domainname
(none)

ばっちり。

まとめ

今回は UTS Namespace を使ってホスト名と NIS ドメイン名の変更が隔離される様子を観察した。


  1. 使ったことがないけどディレクトリサービスの一種らしい

Linux: fork(2) で子プロセスの終了理由を判定する

今回は fork(2) で子プロセスの終了理由を判定してみる。 結論から先に述べると、子プロセスの終了を待つとき wait(2) に int のポインタを渡すと終了理由をセットしてくれる。 それをマクロで判定していけば良い。

linuxjm.osdn.jp

使った環境は次のとおり。

$ cat /etc/lsb-release 
DISTRIB_ID=Ubuntu
DISTRIB_RELEASE=20.04
DISTRIB_CODENAME=focal
DISTRIB_DESCRIPTION="Ubuntu 20.04.3 LTS"
$ uname -rm
5.4.0-91-generic aarch64

もくじ

下準備

あらかじめ C のビルドに必要な build-essential をインストールしておく。

$ sudo apt-get update
$ sudo apt-get install -y build-essential

子プロセスの終了理由を判定してみる

以下に、子プロセスの終了理由を「exit(3)」「シグナル」「その他」に判定するサンプルコードを示す。 前述したとおり、親プロセスが wait(2) で子プロセスの終了を待つときに、引数として int のポインタを渡してやる。 すると、終了理由を示す数値がセットされるので、それをマクロで判定してやる。 子プロセスでは bash を起動している。

#define _GNU_SOURCE

#include <sched.h>
#include <stdlib.h>
#include <stdio.h>
#include <errno.h>
#include <string.h>
#include <unistd.h>
#include <sys/wait.h>
#include <sys/types.h>

int main(int argc, char *argv[]) {
    // プロセスを fork(2) でフォークする
    pid_t pid = fork();
    if (pid < 0) {
        fprintf(stderr, "Failed to fork a new process: %s\n", strerror(errno));
        exit(-1);
    }

    if (pid != 0) {
        // PID が非ゼロは親プロセス
        // wait(2) で子プロセスの完了を待つ
        int status;
        wait(&status);

        // 子プロセスの終了の仕方を出力する
        if (WIFEXITED(status)) {
            // WIFEXITED() が非ゼロなら exit(3) による終了
            // WEXITSTATUS() で終了コードが得られる 
            printf("exit(3), status=%d\n", WEXITSTATUS(status));
        } else if (WIFSIGNALED(status)) {
            // WIFSIGNALED() が非ゼロならシグナルによる終了
            // WTERMSIG() でシグナル番号が得られる
            printf("signal, sig=%d\n", WTERMSIG(status));
        } else {
            // それ以外の終了
            printf("aborted");
        }

        exit(EXIT_SUCCESS);
    }

    // 以降は子プロセスでの処理

    // 子プロセスでシェル (Bash) を起動する
    char* const exec_argv[] = {"bash", NULL};
    if (execvp(exec_argv[0], exec_argv) < 0) {
        fprintf(stderr, "Failed to exec \"%s\": %s\n", exec_argv[0], strerror(errno));
        exit(EXIT_FAILURE);
    }

    return EXIT_SUCCESS;
}

ちなみに、理由を知る必要がないときは wait(2) の引数に NULL を指定すれば良い。

上記をコンパイルする。

$ gcc -Wall example.c 

現在の PID は 927 だった。

$ echo $$
927

先ほどコンパイルしたバイナリを実行する。

$ ./a.out

すると、fork(2) した子プロセスで新たに bash が起動する。

$ echo $$
1135

試しに組み込みの exit コマンドでプロセスを終了してみよう。 ステータスコードには適当に 10 を指定する。

$ exit 10
exit
exit(3), status=10

上記のとおり、ちゃんと WIFEXITED のブロックに入ってステータスコードが得られた。

続いてはシグナルで終了させてみよう。

$ echo $$
927
$ ./a.out
$ echo $$
1536

子プロセスに KILL シグナルを送る。

$ kill -KILL 1536
signal, sig=9

上記のとおり、ちゃんと WIFSIGNALED のブロックに入ってシグナルによる終了と判定された。

いじょう。 それ以外のパターンは、どう確認すれば良いのかな。

Linux: util-linux を gdb でデバッグする

util-linux に含まれるコマンドの振る舞いを動的に解析したい場面があったので、手順を書き残しておく。

使った環境は次のとおり。

$ cat /etc/lsb-release 
DISTRIB_ID=Ubuntu
DISTRIB_RELEASE=20.04
DISTRIB_CODENAME=focal
DISTRIB_DESCRIPTION="Ubuntu 20.04.3 LTS"
$ uname -rm
5.4.0-91-generic aarch64

下準備

まずは、システムにインストールされている util-linux のバージョンを調べておく。 今回使った環境では 2.34 らしい。

$ dpkg -l | grep util-linux
ii  util-linux                     2.34-0.1ubuntu9.1                     arm64        miscellaneous system utilities

ただし、システムに入っているものはリリース版なので、シンボルなどが削除されてしまっている。

$ file $(which unshare)
/usr/bin/unshare: ELF 64-bit LSB shared object, ARM aarch64, version 1 (SYSV), dynamically linked, interpreter /lib/ld-linux-aarch64.so.1, BuildID[sha1]=3c2afa819eb8040c947103fe980e2d88acc92c2b, for GNU/Linux 3.7.0, stripped

そこで、自分でソースコードを落としてきてビルドする。 まずはビルドに必要なパッケージ一式を入れる。 また、後ほど使うデバッガとして gdb も入れておく。

$ sudo apt-get -y install build-essential gdb

続いては肝心の util-linux の tarball をダウンロードする。

$ wget -O - https://mirrors.edge.kernel.org/pub/linux/utils/util-linux/v2.34/util-linux-2.34.tar.gz | tar zxvf -

ビルドする。

$ cd util-linux-2.34/
$ ./configure && make

これで util-linux に含まれている諸々がビルドできた。

$ ls
ABOUT-NLS         addpart          chrt           ctrlaltdel  fsck        ipcs          libmount.la      lscpu       mkfs.minix  readprofile   sfdisk       tools
AUTHORS           agetty           col            delpart     fsck.minix  isosize       libsmartcols     lsipc       mkswap      rename        stamp-h1     umount
COPYING           autogen.sh       colcrt         disk-utils  fsfreeze    kill          libsmartcols.la  lslocks     mount       renice        sulogin      unshare
ChangeLog         bash-completion  colrm          dmesg       fstrim      last          libtcolors.la    lslogins    mountpoint  resizepart    swaplabel    utmpdump
Documentation     blkdiscard       column         eject       getopt      ldattach      libtool          lsmem       namei       rev           swapoff      uuidd
Makefile          blkid            config         fallocate   hardlink    lib           libuuid          lsns        nologin     rfkill        swapon       uuidgen
Makefile.am       blkzone          config.h       fdformat    hexdump     libblkid      libuuid.la       m4          nsenter     rtcwake       switch_root  uuidparse
Makefile.in       blockdev         config.h.in    fdisk       hwclock     libblkid.la   logger           mcookie     partx       schedutils    sys-utils    wall
NEWS              cal              config.log     fincore     include     libcommon.la  login-utils      mesg        pivot_root  script        taskset      wdctl
README            chcpu            config.status  findfs      ionice      libfdisk      look             misc-utils  po          scriptreplay  term-utils   whereis
README.licensing  chmem            configure      findmnt     ipcmk       libfdisk.la   losetup          mkfs        prlimit     setarch       tests        wipefs
aclocal.m4        choom            configure.ac   flock       ipcrm       libmount      lsblk            mkfs.bfs    raw         setsid        text-utils   zramctl

中身を見ると、ちゃんとシンボルなどが残っている。

$ file unshare
unshare: ELF 64-bit LSB shared object, ARM aarch64, version 1 (SYSV), dynamically linked, interpreter /lib/ld-linux-aarch64.so.1, BuildID[sha1]=a70bad2506d9972f359c141b679d33fdfe03e58d, for GNU/Linux 3.7.0, with debug_info, not stripped

動作することも確認しておこう。 試しに unshare(1) を実行する。 以下では、PID Namespace を新たに作っている。

$ sudo ./unshare --fork --pid --mount-proc bash

新たに起動したシェルで確認すると、ちゃんと PID が 1 からリセットされている。

# ps aux
USER         PID %CPU %MEM    VSZ   RSS TTY      STAT START   TIME COMMAND
root           1  0.0  0.3   8592  3528 pts/0    S    16:54   0:00 bash
root           8  0.0  0.2  10188  2816 pts/0    R+   16:54   0:00 ps aux

動作確認が終わったら終了しておこう。

# exit

gdb でデバッグする

動作確認ができたので、続いては gdb を使って動的解析する。 まずは gdb 経由で unshare(1) を起動する。

$ sudo gdb ./unshare

ブレークポイントとして、とりあえず適当にメイン関数を設定する。 ここはまあご自由に。

(gdb) break main
Breakpoint 1 at 0x2940: file sys-utils/unshare.c, line 288.

ちなみに、デバッグ途中で fork(2) する場合には、親プロセスと子プロセスのどちらを追跡するか設定しておく必要がある。 ここでは子プロセスを追跡する場合。

(gdb) set follow-fork-mode child

設定が終わったらコマンドライン引数を渡して実行する。 設定したブレークポイントまで処理が進むはず。

gdb) run --fork --pid --mount-proc bash
Starting program: /home/ubuntu/util-linux-2.34/unshare --fork --pid --mount-proc bash

Breakpoint 1, main (argc=5, argv=0xfffffffff638) at sys-utils/unshare.c:288
288    {

次のように、ちゃんとメイン関数でブレークしている。

(gdb) l
283    
284        exit(EXIT_SUCCESS);
285    }
286    
287    int main(int argc, char *argv[])
288    {
289        enum {
290            OPT_MOUNTPROC = CHAR_MAX + 1,
291            OPT_PROPAGATION,
292            OPT_SETGROUPS,

あとは通常の gdb のやり方で調べていけば良い。

いじょう。

PostgreSQL のテーブルに CSV でデータを読み込む

今回は PostgreSQL のテーブルに CSV ファイル経由でデータを読み込む方法について。 ちょくちょくやり方を調べている気がするのでメモしておく。

使った環境は次のとおり。

$ sw_vers
ProductName:    macOS
ProductVersion: 12.1
BuildVersion:   21C52
$ uname -rm
21.2.0 arm64
$ psql --version           
psql (PostgreSQL) 14.1

もくじ

下準備

まずは PostgreSQL をインストールする。 ついでに、CSV をダウンロードするために wget も入れる。

$ brew install postgresql wget

PostgreSQL のサービスを開始する。

$ brew services start postgresql
$ brew services list
Name       Status  User    File
postgresql started amedama ~/Library/LaunchAgents/homebrew.mxcl.postgresql.plist

今回は Seaborn に同梱されている Taxis データセットを使う。 あらかじめ CSV ファイルをダウンロードしておく。

$ wget https://raw.githubusercontent.com/mwaskom/seaborn-data/master/taxis.csv -P /tmp
$ head -n 5 /tmp/taxis.csv
pickup,dropoff,passengers,distance,fare,tip,tolls,total,color,payment,pickup_zone,dropoff_zone,pickup_borough,dropoff_borough
2019-03-23 20:21:09,2019-03-23 20:27:24,1,1.6,7.0,2.15,0.0,12.95,yellow,credit card,Lenox Hill West,UN/Turtle Bay South,Manhattan,Manhattan
2019-03-04 16:11:55,2019-03-04 16:19:00,1,0.79,5.0,0.0,0.0,9.3,yellow,cash,Upper West Side South,Upper West Side South,Manhattan,Manhattan
2019-03-27 17:53:01,2019-03-27 18:00:25,1,1.37,7.5,2.36,0.0,14.16,yellow,credit card,Alphabet City,West Village,Manhattan,Manhattan
2019-03-10 01:23:59,2019-03-10 01:49:51,1,7.7,27.0,6.15,0.0,36.95,yellow,credit card,Hudson Sq,Yorkville West,Manhattan,Manhattan

続いて、上記のデータに適合するテーブルを用意する。 データベースは、デフォルトで postgres が作られている。

$ cat << 'EOF' | psql postgres
CREATE TABLE IF NOT EXISTS taxis (
  pickup TIMESTAMP,
  dropoff TIMESTAMP,
  passengers INT,
  distance FLOAT,
  fare FLOAT,
  tip FLOAT,
  tolls FLOAT,
  total FLOAT,
  color TEXT,
  payment TEXT,
  pickup_zone TEXT,
  dropoff_zone TEXT,
  pickup_borough TEXT,
  dropoff_borough TEXT
);
EOF
CREATE TABLE

CSV ファイルを読み込む

CSV ファイルのインポートには COPY を使う。 これは PostgreSQL 独自の文なので、使い方はドキュメントを参照する。

www.postgresql.jp

実際に、COPY を使って taxis テーブルに /tmp/taxis.csv ファイルを読み込む。 CSV ファイルは先頭行がヘッダになっているので、読み飛ばす設定を WITH 以下に入れる。

$ cat << 'EOF' | psql postgres
COPY taxis
FROM '/tmp/taxis.csv'
WITH (
  FORMAT csv,
  HEADER true
)
EOF

以下のとおり、確認するとファイルの内容が読み込まれていることがわかる。

$ cat << 'EOF' | psql postgres                               
\pset pager off
SELECT * FROM taxis LIMIT 5
EOF
Pager usage is off.
       pickup        |       dropoff       | passengers | distance | fare | tip  | tolls | total | color  |   payment   |      pickup_zone      |     dropoff_zone      | pickup_borough | dropoff_borough 
---------------------+---------------------+------------+----------+------+------+-------+-------+--------+-------------+-----------------------+-----------------------+----------------+-----------------
 2019-03-23 20:21:09 | 2019-03-23 20:27:24 |          1 |      1.6 |    7 | 2.15 |     0 | 12.95 | yellow | credit card | Lenox Hill West       | UN/Turtle Bay South   | Manhattan      | Manhattan
 2019-03-04 16:11:55 | 2019-03-04 16:19:00 |          1 |     0.79 |    5 |    0 |     0 |   9.3 | yellow | cash        | Upper West Side South | Upper West Side South | Manhattan      | Manhattan
 2019-03-27 17:53:01 | 2019-03-27 18:00:25 |          1 |     1.37 |  7.5 | 2.36 |     0 | 14.16 | yellow | credit card | Alphabet City         | West Village          | Manhattan      | Manhattan
 2019-03-10 01:23:59 | 2019-03-10 01:49:51 |          1 |      7.7 |   27 | 6.15 |     0 | 36.95 | yellow | credit card | Hudson Sq             | Yorkville West        | Manhattan      | Manhattan
 2019-03-30 13:27:42 | 2019-03-30 13:37:14 |          3 |     2.16 |    9 |  1.1 |     0 |  13.4 | yellow | credit card | Midtown East          | Yorkville West        | Manhattan      | Manhattan
(5 rows)

いじょう。

Network Namespace 内の Linux Bridge では STP が動作しないらしい

どうやら、今のところ Network Namespace 1 内では Linux Bridge の STP (Spanning Tree Protocol) がサポートされていないようだ。 今回は、以下のような実験を通して、それを実際に確かめてみる。

  1. 単一の Linux Bridge 内にループを作ってストームを引き起こす
  2. Linux Bridge の STP 機能を有効にしてストームが止まるか確かめる

使った環境は次のとおり。

$ cat /etc/lsb-release 
DISTRIB_ID=Ubuntu
DISTRIB_RELEASE=20.04
DISTRIB_CODENAME=focal
DISTRIB_DESCRIPTION="Ubuntu 20.04.3 LTS"
$ uname -rm
5.4.0-91-generic aarch64
$ ip -V
ip utility, iproute2-ss200127
$ brctl -V
bridge-utils, 1.6

もくじ

下準備

あらかじめ、必要なパッケージをインストールしておく。

$ sudo apt-get -y install iproute2 tcpdump bridge-utils watch

Network Namespace を使わずに STP が機能することを確かめる

まずは、システムをそのまま使って Linux Bridge の STP が機能することを確かめておく。

はじめに Linux Bridge を br0 という名前で追加する。

$ sudo ip link add dev br0 type bridge

veth (Virtual Ethernet) ペアを br-veth0br-veth1 という名前で用意する。

$ sudo ip link add br-veth0 type veth peer name br-veth1

一応、ドキュメンテーションアドレスの MAC アドレスを付与しておく。

$ sudo ip link set dev br-veth0 address 00:00:5E:00:53:01
$ sudo ip link set dev br-veth1 address 00:00:5E:00:53:02

インターフェイスの状態を UP にする。

$ sudo ip link set br-veth0 up
$ sudo ip link set br-veth1 up

そして、両方のインターフェイスを先ほど作った br0 に所属させる。 これでループができる。

$ sudo ip link set br-veth0 master br0
$ sudo ip link set br-veth1 master br0

ただし、この時点ではブリッジの状態が DOWN のままなので、何も流れていない。

$ ip -s link show br0
3: br0: <BROADCAST,MULTICAST> mtu 1500 qdisc noop state DOWN mode DEFAULT group default qlen 1000
    link/ether 00:00:5e:00:53:02 brd ff:ff:ff:ff:ff:ff
    RX: bytes  packets  errors  dropped overrun mcast   
    0          0        0       0       0       0       
    TX: bytes  packets  errors  dropped carrier collsns 
    0          0        0       0       0       0       

おもむろにブリッジを UP にしてみよう。

$ sudo ip link set br0 up

すると、そのうちにすごい勢いでパケット数のカウンタが増えだすはず。 ストームが生じている。

$ ip -s link show br0
3: br0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue state UP mode DEFAULT group default qlen 1000
    link/ether 00:00:5e:00:53:02 brd ff:ff:ff:ff:ff:ff
    RX: bytes  packets  errors  dropped overrun mcast   
    2734413272 38328896 0       0       0       38328896 
    TX: bytes  packets  errors  dropped carrier collsns 
    2909638    33833    0       0       0       0       

watch コマンドとか使って確認するとわかりやすい。

$ watch -n 1 ip -s link show br0

tcpdump コマンドを使って確認すると IPv6 の NDP (Neighbor Discovery Protocol) が原因になっているはず。 中身は NA (Neighbor Advertisement) だったり RS (Router Solicitation) だったり、最初に流れるパケット次第。

$ sudo tcpdump -tnel -i br0 -c 5
tcpdump: verbose output suppressed, use -v or -vv for full protocol decode
listening on br0, link-type EN10MB (Ethernet), capture size 262144 bytes
00:00:5e:00:53:02 > 33:33:00:00:00:01, ethertype IPv6 (0x86dd), length 86: fe80::200:5eff:fe00:5302 > ff02::1: ICMP6, neighbor advertisement, tgt is fe80::200:5eff:fe00:5302, length 32
00:00:5e:00:53:02 > 33:33:00:00:00:01, ethertype IPv6 (0x86dd), length 86: fe80::200:5eff:fe00:5302 > ff02::1: ICMP6, neighbor advertisement, tgt is fe80::200:5eff:fe00:5302, length 32
00:00:5e:00:53:02 > 33:33:00:00:00:01, ethertype IPv6 (0x86dd), length 86: fe80::200:5eff:fe00:5302 > ff02::1: ICMP6, neighbor advertisement, tgt is fe80::200:5eff:fe00:5302, length 32
00:00:5e:00:53:02 > 33:33:00:00:00:01, ethertype IPv6 (0x86dd), length 86: fe80::200:5eff:fe00:5302 > ff02::1: ICMP6, neighbor advertisement, tgt is fe80::200:5eff:fe00:5302, length 32
00:00:5e:00:53:02 > 33:33:00:00:00:01, ethertype IPv6 (0x86dd), length 86: fe80::200:5eff:fe00:5302 > ff02::1: ICMP6, neighbor advertisement, tgt is fe80::200:5eff:fe00:5302, length 32
5 packets captured
11083 packets received by filter
11051 packets dropped by kernel

ここで、ブリッジの STP を有効にしてみよう。

$ sudo brctl stp br0 on
$ brctl show br0
bridge name bridge id       STP enabled interfaces
br0     8000.00005e005302  yes     br-veth0
                            br-veth1

上記では、STP によってループがちゃんと遮断されてストームがピタっと止まるはず。

確認が終わったら一旦ブリッジは削除しておく。

$ sudo ip link delete br0

Network Namespace で STP が機能しないことを確かめる

では、続いて Network Namespace 内に作った Linux Bridge でも確かめてみよう。

まずは、bridge という名前で Network Namespace を作成する。

$ sudo ip netns add bridge

その中に、先ほどと同じ br0 という名前で Linux Bridge を作る。

$ sudo ip netns exec bridge ip link add dev br0 type bridge

veth インターフェイスは、先ほど作ったものをそのまま流用する。

$ sudo ip link set br-veth0 netns bridge
$ sudo ip link set br-veth1 netns bridge

インターフェイスの状態を UP にする。

$ sudo ip netns exec bridge ip link set br-veth0 up
$ sudo ip netns exec bridge ip link set br-veth1 up

両方のインターフェイスをブリッジに接続する。

$ sudo ip netns exec bridge ip link set br-veth0 master br0
$ sudo ip netns exec bridge ip link set br-veth1 master br0

先ほどと同じように、この時点ではブリッジの状態が DOWN なのでフレームは流れない。

$ sudo ip netns exec bridge ip -s link show br0
2: br0: <BROADCAST,MULTICAST> mtu 1500 qdisc noop state DOWN mode DEFAULT group default qlen 1000
    link/ether 00:00:5e:00:53:01 brd ff:ff:ff:ff:ff:ff
    RX: bytes  packets  errors  dropped overrun mcast   
    0          0        0       0       0       0       
    TX: bytes  packets  errors  dropped carrier collsns 
    0          0        0       0       0       0  

それでは、ブリッジを UP にしよう。

$ sudo ip netns exec bridge ip link set br0 up

すると、ブリッジのカウンタがもりもりと増えだす。 ここまでは想定内。

$ sudo ip netns exec bridge ip -s link show br0
2: br0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue state UP mode DEFAULT group default qlen 1000
    link/ether 00:00:5e:00:53:01 brd ff:ff:ff:ff:ff:ff
    RX: bytes  packets  errors  dropped overrun mcast   
    2430362024 33834240 0       0       0       33834240 
    TX: bytes  packets  errors  dropped carrier collsns 
    2629020    30570    0       0       0       0

では、ブリッジの STP を有効にしてみよう。 表示の上では、ちゃんと STP enabledyes になっている。

$ sudo ip netns exec bridge brctl stp br0 on
$ sudo ip netns exec bridge brctl show br0
bridge name bridge id       STP enabled interfaces
br0     8000.00005e005301  yes     br-veth0
                            br-veth1

しかし、残念ながらストームは止まらず、ブリッジのカウンタは増え続けている。

$ sudo ip netns exec bridge ip -s link show br0
2: br0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue state UP mode DEFAULT group default qlen 1000
    link/ether 00:00:5e:00:53:01 brd ff:ff:ff:ff:ff:ff
    RX: bytes  packets  errors  dropped overrun mcast   
    17477322936 241253904 0       0       0       241253904 
    TX: bytes  packets  errors  dropped carrier collsns 
    20065998   233325   0       0       0       0
$ sudo ip netns exec bridge ip -s link show br0
2: br0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue state UP mode DEFAULT group default qlen 1000
    link/ether 00:00:5e:00:53:01 brd ff:ff:ff:ff:ff:ff
    RX: bytes  packets  errors  dropped overrun mcast   
    23684383864 327386610 0       0       0       327386610 
    TX: bytes  packets  errors  dropped carrier collsns 
    27466110   319373   0       0       0       0

STP が機能していないようだ。

まとめ

残念ながら、今の Linux カーネルでは Network Namespace 内の Linux Bridge では STP が使えないらしい。 もちろん、これはあくまで「現時点では」という話であって、将来的に使えるようになる可能性はあるはずだけど。

参考

lists.linuxfoundation.org


  1. すべてのプロセスはいずれかの Network Namespace に所属するため、厳密に言うとシステムが所属している以外の (=非ルートな) Network Namespace ということになる

MariaDB のテーブルに CSV でデータを読み込む

今回は MariaDB のテーブルに CSV ファイル経由でデータを読み込む方法について。 ちょくちょくやり方を調べている気がするのでメモしておく。

使った環境は次のとおり。

$ sw_vers
ProductName:    macOS
ProductVersion: 12.1
BuildVersion:   21C52
$ uname -rm
21.2.0 arm64
$ mysql --version      
mysql  Ver 15.1 Distrib 10.6.4-MariaDB, for osx10.17 (arm64) using readline 5.1

もくじ

下準備

あらかじめ Homebrew を使って MariaDB をインストールする。 サンプルデータをダウンロードするために wget も入れておく。

$ brew install mariadb wget

インストールできたら MariaDB のサービスを開始する。 本来は mysql_secure_installation を実行して作業した方が望ましいけど、今回は省略する。

$ brew services start mariadb
$ brew services list          
Name    Status  User    File
mariadb started amedama ~/Library/LaunchAgents/homebrew.mxcl.mariadb.plist

サンプルデータとして、Seaborn が利用している taxis データセットを使う。 これはタクシーの利用情報に関するデータセットになっている。

$ wget https://raw.githubusercontent.com/mwaskom/seaborn-data/master/taxis.csv

あらかじめ example という名前でデータベースを作っておく。

$ mysql -vvv -e "CREATE DATABASE IF NOT EXISTS example"
--------------
CREATE DATABASE IF NOT EXISTS example
--------------

Query OK, 1 row affected (0.000 sec)

Bye

そして、taxis データセットに適合する形でテーブルを用意する。

$ cat << 'EOF' | mysql -vvv -D example
CREATE TABLE IF NOT EXISTS taxis (
  pickup DATETIME,
  dropoff DATETIME,
  passengers INT,
  distance FLOAT,
  fare FLOAT,
  tip FLOAT,
  tolls FLOAT,
  total FLOAT,
  color TEXT,
  payment TEXT,
  pickup_zone TEXT,
  dropoff_zone TEXT,
  pickup_borough TEXT,
  dropoff_borough TEXT
);
EOF
--------------
CREATE TABLE IF NOT EXISTS taxis (
  pickup DATETIME,
  dropoff DATETIME,
  passengers INT,
  distance FLOAT,
  fare FLOAT,
  tip FLOAT,
  tolls FLOAT,
  total FLOAT,
  color TEXT,
  payment TEXT,
  pickup_zone TEXT,
  dropoff_zone TEXT,
  pickup_borough TEXT,
  dropoff_borough TEXT
)
--------------

Query OK, 0 rows affected (0.097 sec)

Bye

CSV ファイルからデータを読み込む

さて、ここからが今回の本題になる。 CSV ファイルからデータを読み込むときは LOAD DATA LOCAL INFILE でファイルパスを指定する。 また、フィールドの区切り文字などを同時に指定している。 そして、サンプルファイルは最初にヘッダの行が含まれるので IGNORE 1 LINES で最初の 1 行を読み飛ばしている。

$ cat << 'EOF' | mysql -vvv -D example
LOAD DATA LOCAL INFILE "taxis.csv"
INTO TABLE taxis
FIELDS TERMINATED BY ','
OPTIONALLY ENCLOSED BY '"'
LINES TERMINATED BY '\n'
IGNORE 1 LINES;
EOF
--------------
LOAD DATA LOCAL INFILE "taxis.csv"
INTO TABLE taxis
FIELDS TERMINATED BY ','
OPTIONALLY ENCLOSED BY '"'
LINES TERMINATED BY '\n'
IGNORE 1 LINES
--------------

Query OK, 6433 rows affected (0.054 sec)
Records: 6433  Deleted: 0  Skipped: 0  Warnings: 0

Bye

確認すると、ちゃんとテーブルにデータが読み込まれていることがわかる。

$ mysql -vvv -D example -e "SELECT * FROM taxis LIMIT 5"
--------------
SELECT * FROM taxis LIMIT 5
--------------

+---------------------+---------------------+------------+----------+------+------+-------+-------+--------+-------------+-----------------------+-----------------------+----------------+-----------------+
| pickup              | dropoff             | passengers | distance | fare | tip  | tolls | total | color  | payment     | pickup_zone           | dropoff_zone          | pickup_borough | dropoff_borough |
+---------------------+---------------------+------------+----------+------+------+-------+-------+--------+-------------+-----------------------+-----------------------+----------------+-----------------+
| 2019-03-23 20:21:09 | 2019-03-23 20:27:24 |          1 |      1.6 |    7 | 2.15 |     0 | 12.95 | yellow | credit card | Lenox Hill West       | UN/Turtle Bay South   | Manhattan      | Manhattan       |
| 2019-03-04 16:11:55 | 2019-03-04 16:19:00 |          1 |     0.79 |    5 |    0 |     0 |   9.3 | yellow | cash        | Upper West Side South | Upper West Side South | Manhattan      | Manhattan       |
| 2019-03-27 17:53:01 | 2019-03-27 18:00:25 |          1 |     1.37 |  7.5 | 2.36 |     0 | 14.16 | yellow | credit card | Alphabet City         | West Village          | Manhattan      | Manhattan       |
| 2019-03-10 01:23:59 | 2019-03-10 01:49:51 |          1 |      7.7 |   27 | 6.15 |     0 | 36.95 | yellow | credit card | Hudson Sq             | Yorkville West        | Manhattan      | Manhattan       |
| 2019-03-30 13:27:42 | 2019-03-30 13:37:14 |          3 |     2.16 |    9 |  1.1 |     0 |  13.4 | yellow | credit card | Midtown East          | Yorkville West        | Manhattan      | Manhattan       |
+---------------------+---------------------+------------+----------+------+------+-------+-------+--------+-------------+-----------------------+-----------------------+----------------+-----------------+
5 rows in set (0.001 sec)

Bye

いじょう。