tshell_blog

ソフトウェアと車輪がついた乗り物のはなし

Dockerコンテナでのcyclictest実行結果

前回はDockerコンテナでcyclictestを動作させるところまでやってみました。

tshell.hatenablog.com

今回はcyclictestの結果をもう少し詳しく見てみます。

環境

以下の環境で試しました。

docker version
Client:
 Version:           18.09.7
 API version:       1.39
 Go version:        go1.10.1
 Git commit:        2d0083d
 Built:             Fri Aug 16 14:20:06 2019
 OS/Arch:           linux/amd64
 Experimental:      false

Server:
 Engine:
  Version:          18.09.9
  API version:      1.39 (minimum version 1.12)
  Go version:       go1.13.4
  Git commit:       9552f2b
  Built:            Sat Nov 16 01:07:48 2019
  OS/Arch:          linux/amd64
  Experimental:     false

cyclictestについて

cyclictestは以下の図のように一定時間ごとにリアルタイムスレッドを起動して,理想とするスレッドの起動時刻と実際にスレッドが起動した時刻との差(レイテンシ)を計測するテストです。

f:id:tshell:20200211221903p:plain

当然レイテンシは低ければ低いほどよいと言えます。

CPU負荷の与え方

CPU負荷が高い場合にもリアルタイムスレッドが定周期実行されることを確認したいので,以下のようなスクリプトを実行しながらcyclictestを実行します。
実行するとCPU使用率がほぼ100%になります。

while true; do dd if=/dev/zero of=bigfile bs=1024000 count=1024; done &
 while true; do killall hackbench; sleep 5; done &
 while true; do ./hackbench 20; done &
 ( cd ltp-full-20040707; su mingo -c ./run40; ) &
 ping -l 100000 -q -s 10 -f v &
 du / &
 ./dortc &

cyclictestの実行

前回の記事と同じ手順でrttestコンテナを起動し,以下のコマンドを実行します。
100ms周期で1時間テストを実行し,レイテンシ3000[us]までのヒストグラムを表示します。

# cyclictest --smp --priority=99 --interval=100000 --distance=0 -l36000 -h3000

結果

以下のような結果が得られました。

T: 0 (   31) P:99 I:100000 C:  36000 Min:      5 Act:   11 Avg:   10 Max:    3417
T: 1 (   32) P:99 I:100000 C:  36000 Min:      7 Act:   21 Avg:   14 Max:    4488
T: 2 (   33) P:99 I:100000 C:  36000 Min:      5 Act:   14 Avg:   14 Max:    4390
T: 3 (   34) P:99 I:100000 C:  36000 Min:      5 Act:   11 Avg:   10 Max:    4498
# Histogram
000000 000000   000000  000000  000000
000001 000000   000000  000000  000000
000002 000000   000000  000000  000000
000003 000000   000000  000000  000000
000004 000000   000000  000000  000000
000005 000001   000000  000007  000017
000006 000058   000000  000498  000537
000007 001017   000003  004293  005340
000008 006554   000034  008831  009400
000009 009585   000258  007380  008309
000010 007967   001102  005890  005945
000011 005389   003659  003540  003257
000012 002894   007126  001574  001387
000013 001071   006705  000727  000624
000014 000442   005549  000406  000293
000015 000200   004313  000270  000208
000016 000136   003121  000205  000124
000017 000098   001697  000159  000081
000018 000053   000799  000113  000058
000019 000038   000385  000096  000033
000020 000035   000205  000076  000029
000021 000027   000176  000086  000017
000022 000018   000111  000073  000026
000023 000009   000087  000087  000011
000024 000021   000064  000102  000010
000025 000039   000058  000065  000022
...
002996 000000   000000  000000  000000
002997 000000   000000  000000  000000
002998 000000   000000  000000  000000
002999 000000   000000  000000  000000
# Total: 000035997 000035996 000035993 000035998
# Min Latencies: 00005 00007 00005 00005
# Avg Latencies: 00010 00014 00014 00010
# Max Latencies: 03417 04488 04390 04498
# Histogram Overflows: 00003 00004 00007 00002
# Histogram Overflow at cycle number:
# Thread 0: 19250 24091 34394
# Thread 1: 24091 30057 34394 34806
# Thread 2: 16096 17984 24091 28377 30057 32070 34394
# Thread 3: 19250 28377

最大レイテンシは4498[us]ですが,平均は約10[us]です。
これくらいの精度で十分な用途であればコンテナでのリアルタイムアプリケーションも使えるのではないでしょうか。

ヒストグラムを25[us]までグラフ化すると以下のようになります。

f:id:tshell:20200211222737p:plain

結構10us付近にまとまっています。CPU1はどうしたのか・・・

Dockerでリアルタイム処理(cyclictest)を実行する

Dockerコンテナでリアルタイムタスクは動かせないの?と思ったので試してみました。

docker docs | Runtime options with Memory, CPUs, and GPUsにはConfigure the realtime schedulerという項目があり,リアルタイム処理を実現できそうな感じがします。

以下の手順でDockerコンテナ上でcyclictestを実行するところまでやってみます。

  1. カーネルのビルド
  2. リアルタイムスケジューラを使ったコンテナの起動
  3. cyclictestの実行

CONFIG_RT_GROUP_SCHEDを有効にしたカーネルをビルドする

docker docsを見るとCONFIG_RT_GROUP_SCHEDが有効になっているカーネルでないとコンテナがリアルタイムスケジューラを使用できないようです。RT PREEMPTIONパッチを当てがてら,CONFIG_RT_GROUP_SCHEDを有効にしてカーネルをビルドします。

パッチとカーネルソースのダウンロード

実はPreemption ModelをLowlatency DesktopとしないとCONFIG_RT_GROUP_SCHEDを有効にできないので,RT PREEMTTIONパッチは必要はなさそうですが一応当てておきます。

まずは以下からRT PREEMPTIONパッチを探します。

https://mirrors.edge.kernel.org/pub/linux/kernel/projects/rt/5.4/

5.4.13-rt7が最新版のようなので,以下から5.4.13のソースをダウンロードします。

https://mirrors.edge.kernel.org/pub/linux/kernel/v5.x/

以下の.tar.gzファイルをダウンロードしました。 https://mirrors.edge.kernel.org/pub/linux/kernel/v5.x/linux-5.4.13.tar.gz

RT PREEMPTIONパッチの適用

カーネルソースとパッチを同じディレクトリに保存し,以下を実行します。

$ tar xf linux-5.4.13.tar.gz
$ cd linux-5.4.13
$ gzip -cd ../patch-5.4.13-rt7.patch.gz | patch -p1 --verbose

Preemption Modelの変更

カーネルソースが展開されたディレクトリで以下を実行します。

$ make menuconfig

以下のように General setup > Preemption Model > Preemptible Kernel(Low-Latency Desktop)を選択します。
f:id:tshell:20200127211016p:plain

以下のようにGeneral setup > Control Group support > CPUControllerのGroup scheduling for SCHED_RR/FIFOにチェックを入れます。
上記のPreemption ModelでFully Preemptible Kernel(Real-Time)を選択するとGroup scheduling for SCHED_RR/FIFOが選択できなくなります。 f:id:tshell:20200127211110p:plain

上記2つを設定したら下部メニューのExitを選択し,menuconfigを終了します。終了時に.configに内容を保存するか聞かれるのでYesを選択します。

カーネルイメージの作成

以下のコマンドを実行し,カーネルイメージとヘッダイメージを作成します。

$ make-kpkg -j 8 --rootcmd fakeroot --initrd kernel_image kernel_headers

ソースが展開されているディレクトリの一つ上の階層に.debファイルが作成されるので以下のコマンドを実行して.debファイルをインストールします。

$ cd ..
$ sudo dpkg -i linux-headers-5.4.13-rt7_5.4.13-rt7-10.00.Custom_amd64.deb linux-image-5.4.13-rt7_5.4.13-rt7-10.00.Custom_amd64.deb

GRUBメニューが表示されなくなることの対策

ビルドしたカーネルをインストールして再起動しGRUBメニューでカーネルを選択するわけですが,起動時にGRUBメニューが表示されない場合があります。

そのような場合は,/etc/default/grubファイルを編集してGRUBメニューが表示されるようにします。

$ sudo vi /etc/default/grub

以下のように先頭に#をつけて,2行をコメントアウトする
# GRUB_TIMEOUT_STYLE=hidden
# GRUB_TIMEOUT=0

編集後はupdate-grubを実行しておきます。

Invalid Signatureによりカーネルがロードできない場合の対策

ビルドしたカーネルで起動しようとした際にInvalid Signatureエラーによりカーネルがロードできない場合があります。
以下のようにしてSecure Bootを無効にすることで回避できます。

PCをシャットダウンする前に以下のコマンドを実行します。

$ sudo mokutil --disable-validation

再起動後にmokutilを実行するか確認するダイアログが表示されます。mokutilを実行すると上記のmokutilコマンド実行時に設定したパスワード入力を求められます。
入力したパスワードをすべて入力するのではなく,パスワードの何文字目は何かという形式で1文字ずつ入力します。

リアルタイムスケジューラを使うコンテナの起動

起動前の確認

以下のファイルがあることを確認します。

/sys/fs/cgroup/cpu.rt_runtime_us

Dockerデーモンが動作していることを確認します。

$ docker ps

起動していない場合,以下コマンドにより再起動します。

$ sudo service docker restart

Dockerfileの作成とイメージのビルド

以下のようなDockerfileを作成します。ubuntu18.04にRT-Testsをインストールしています。

FROM ubuntu:18.04

RUN apt-get update && apt-get upgrade -y && apt-get install -y build-essential libnuma-dev git python
RUN git clone git://git.kernel.org/pub/scm/utils/rt-tests/rt-tests.git && \
    cd rt-tests && \
    git checkout stable/v1.0 && \
    make all && \
    make install

Dockerファイルをビルドします。

$ docker build -t rttest:latest .

コンテナの起動

以下のコマンドによりrttestイメージを起動します。

$ docker run -it --cpu-rt-runtime=950000 --ulimit rtprio=99 --cap-add=sys_nice rttest:latest

起動時に以下のようなエラーが出る場合があります。

docker: Error response from daemon: OCI runtime create failed: container_linux.go:345: starting container process caused "process_linux.go:281: applying cgroup configuration for process caused \"failed to write 950000 to cpu.rt_runtime_us: write /sys/fs/cgroup/cpu,cpuacct/docker/e2a68990c554b501acfdee02a2d6be94240edd8860335c6ce5b64f559e82eba3/cpu.rt_runtime_us: invalid argument\"": unknown.

上記のようなエラーが出るのは/sys/fs/cgroup/cpu/docker/cpu.rt_runtime_usに設定されているリアルタイムタスクの実行時間が0になっているためのようなので,以下のコマンドにより0以上の値を設定します。

$ sudo -s
# echo 950000 > /sys/fs/cgroup/cpu/docker/cpu.rt_runtime_us

/sys/fs/cgroup/cpu/docker/cpu.rt_period_usには1000000が設定されており,これとの比率で割り当てられたCPU処理時間の何%をリアルタイム処理に回すかを設定できます。上記の場合はcpu.rt_runtime_usが950000なので,95%がリアルタイム処理に回されます。

上記を実行後,再度docker runコマンドを実行し,rttestコンテナのコンソールに接続します。

cyclictestの実行

起動したコンテナ上でcyclictestを実行します。

# cyclictest --smp --priority=80 --interval=200 --distance=0

以下のように表示され,cyclictestが実行できました。

WARN: stat /dev/cpu_dma_latency failed: No such file or directory
policy: fifo: loadavg: 6.18 3.00 1.65 6/1057 23           

T: 0 (   20) P:80 I:200 C: 618500 Min:      2 Act:    7 Avg:    8 Max:    2419
T: 1 (   21) P:80 I:200 C: 617407 Min:      2 Act:    6 Avg:    8 Max:    2513
T: 2 (   22) P:80 I:200 C: 618459 Min:      2 Act:   12 Avg:    7 Max:    2540
T: 3 (   23) P:80 I:200 C: 616966 Min:      2 Act:    6 Avg:    9 Max:    2431

しかし,スレッドの起動周期200us(cyclictestの引数--inteval=200)に対して最大のレイテンシが2540usとなっています。
平均は約10usなので概ね大丈夫そうですが,たまに遅れるようです。

まとめ

Dockerコンテナ上でcyclictestを実行するところまで試しました。
次回はcyclictestの結果をヒストグラム表示してもう少し詳しく見てみます。

yoctoでNano Pi NEO 2用イメージをビルドする

tshell.hatenablog.com

公式イメージもいいですが,meta-allwinner-hxレイヤーというものがあるのでyoctoでNano Pi NEO 2用イメージをビルドすることができます。

環境

以下の環境を使いました。

  • ホストPC:ubuntu 18.04 LTS
  • poky warrior 2.7.1
  • meta-allwinner-hx (warrior)
  • meta-openembedded(warrior)

ビルド環境のセットアップ

meta-allwinner-hxのREADMEを見ながら ホームディレクトリの下に以下のような構成のbuild_nanopiディレクトリを作っていきます。(名前は何でもいいです)

build_nanopi/
├── flash_sd.sh
├── setup-environment.sh
└── sources
    ├── meta-allwinner-hx 
    ├── meta-openembedded
    └── poky

yocto環境は通常~/poky-warriorなどに作成されているかと思います。上記のようなフォルダ構成を実現するためにまた同じものをダウンロードしてくるのも無駄なので,sourcesディレクトリ以下はシンボリックリンクを作成します。

以下コマンドを実行していきます。

$ mkdir ~/build_nanopi
$ cd ~/build_nanopi
$ mkdir sources
$ cd sources
$ ln -s ~/poky-warrior/meta-allwinner-hx meta-allwinner-hx
$ ln -s ~/poky-warrior poky
$ ln -s ~/poky-warrior/meta-openembedded meta-openembedded

セットアップスクリプトをコピーします。

$ cd ~build_nanopi
$ cp sources/meta-allwinner-hx/scripts/setup-environment.sh .
$ cp sources/meta-allwinner-hx/scripts/flash_sd.sh .

環境変数MACHINEにnanopi-neo2を設定します。

$ export MACHINE=nanopi-neo2

setup-environment.shを実行します。

$ source ./setup-environment.sh build

以下のようにsetup-environment-append.shが見つからないというメッセージが表示されますが,buildディレクトリは正常に生成されます。

Configuring for nanopi-neo2
Searching for append setup scripts...

ls: '/home/***/build_nanopi/sources/*/setup-environment-append.sh' にアクセスできません: そのようなファイルやディレクトリはありません
These are the default supported images:
    allwinner-image

You can now build your image. To build the allwinner-image then run this:
$ bitbake allwinner-image

buildディレクトリが生成され,confディレクトリ内にlocal.confとbblayers.confが自動生成されます。
私の環境ではbitbakeのdo_fetchでダウンロードするファイルが~/poky-warrior/downloadsに入っているのでlocal.confのDL_DIRは以下のように変更しました。

DL_DIR = "${HOME}/poky-warrior/downloads"

local.confを設定したら以下のコマンドを実行するとNano Pi NEO 2用のイメージを作成できます。(bitbakeへのパスが通っている必要があります)

$ bitbake allwinner-image

microSDカードへの書き込み

イメージが作成できたらmicroSDカードへ書き込みます。
~/build_nanopiにあるflash_sd.shを使用します。flasah_sd.shからbmaptoolが呼ばれるので,まずは必要なパッケージをインストールします。

$ sudo apt-get install bmap-tools

MACHINEと書き込み先を指定して./flash_sd.shを実行します。
デフォルトでは13.8GBのイメージを書き込むようになっているため,16GB以上のmicroSDカードを用意する必要があります。

$ sudo MACHINE=nanopi-neo2 ./flash_sd.sh /dev/sdb

meta-allwinner-hxのREADMEには「デフォルトだと13.8GBのイメージを作るが,本当に必要なのは60MB弱だから中身スカスカで,bmaptoolを使えば14GBを数秒で書き込めるんだ」みたいなことが書いてありますが,ならばもっと低容量のSDに入るようにしたいですね。

作成されるイメージを小さくするにはmeta-allwinner-hx/classes/allwinner-wks-defs.incを編集し,ROOT_EXTRA_SPACEを変更してルートディレクトリの領域を小さくします。デフォルトでは10240となっていますが,1024に変更すると1.2GBくらいのイメージが作成されます。

meta-allwinner-hx/classes/allwinner-wks-defs.incのROOT_EXTRA_SPACEを以下のように変更します。

ROOT_EXTRA_SPACE ?= "1024"

起動確認

イメージを書き込んだmicroSDをNano Pi NEO 2に挿入し,LANケーブルも接続して電源を入れます。
シリアルコンソールを繋いでいない場合,Nano Pi NEO 2のIPアドレスを知るにはNmapを使ってスキャンする必要があります。
IPアドレスがわかったらSSHで接続します。

$ sudo nmap -sP 192.168.0.*
・・・
Nmap scan report for nanopi-neo2 (192.168.0.32)
・・・

$ ssh root@192.168.0.32

なんとパスワード無しでログインできました。
早めにパスワードを変えておきましょう。

Raspberry Piに増設したSDカードの速度を測る 1bitモードと4bitモードの比較

Raspberry Piに増設したSDカードのパフォーマンスをfioで測ってみました。
増設方法は以下で紹介しています。 tshell.hatenablog.com

実は前の記事のようにdtboファイルを作らなくても以下の設定を/boot/config.txtに追記するだけで使えました。

dtoverlay=sdio,poll_once=off,sdio_overclock=50

前回はbus_width=1を指定して動作させ,とりあえず動いて喜んでいましたがこれを指定すると1bitモードで動作するようです。せっかくDAT0〜3まで結線しているのにDAT0しか使っていません。
今回はbus_width=1の指定を外してデフォルトの4bitモードで動作させ,どれくらい速度が違うのか測ってみます。

環境

以下のものを使用しました。

測り方

SDカードをマウントしたあと,fioを使用してSDカードへの書き込み速度を測ってみます。
以下のコマンドを実行します。

$ sudo mount -t ext4 /dev/mmcblk1 /mnt/sdcard
$ fio -filename=/mnt/sdcard/test1g -direct=1 -rw=write -bs=4k -size=1G -name=file1

/mnt/sdcardに1GBのファイルをブロックサイズ4kBで書き込んでアクセス速度を測ります。

1bitモード

まずは/boot/config.txtへの追記内容を以下のようにしたときの速度を測ってみます。

dtoverlay=sdio,poll_once=off,bus_width=1,sdio_overclock=50

結果は以下のようになりました。

file1: (g=0): rw=write, bs=(R) 4096B-4096B, (W) 4096B-4096B, (T) 4096B-4096B, ioengine=psync, iodepth=1
fio-3.12
Starting 1 process
file1: Laying out IO file (1 file / 1024MiB)
Jobs: 1 (f=1): [W(1)][100.0%][w=1628KiB/s][w=407 IOPS][eta 00m:00s]
file1: (groupid=0, jobs=1): err= 0: pid=667: Mon Dec  2 04:37:06 2019
  write: IOPS=406, BW=1626KiB/s (1665kB/s)(1024MiB/644787msec); 0 zone resets
    clat (usec): min=1602, max=20014, avg=2442.16, stdev=763.70
     lat (usec): min=1603, max=20019, avg=2444.70, stdev=763.72
    clat percentiles (usec):
     |  1.00th=[ 1926],  5.00th=[ 1975], 10.00th=[ 2040], 20.00th=[ 2073],
     | 30.00th=[ 2089], 40.00th=[ 2114], 50.00th=[ 2147], 60.00th=[ 2311],
     | 70.00th=[ 2507], 80.00th=[ 2900], 90.00th=[ 3064], 95.00th=[ 3294],
     | 99.00th=[ 3851], 99.50th=[ 9110], 99.90th=[10814], 99.95th=[11338],
     | 99.99th=[13042]
   bw (  KiB/s): min= 1552, max= 1736, per=100.00%, avg=1626.02, stdev=20.09, samples=1289
   iops        : min=  388, max=  434, avg=406.50, stdev= 5.02, samples=1289
  lat (msec)   : 2=8.00%, 4=91.17%, 10=0.61%, 20=0.22%, 50=0.01%
  cpu          : usr=0.59%, sys=7.93%, ctx=265811, majf=0, minf=23
  IO depths    : 1=100.0%, 2=0.0%, 4=0.0%, 8=0.0%, 16=0.0%, 32=0.0%, >=64=0.0%
     submit    : 0=0.0%, 4=100.0%, 8=0.0%, 16=0.0%, 32=0.0%, 64=0.0%, >=64=0.0%
     complete  : 0=0.0%, 4=100.0%, 8=0.0%, 16=0.0%, 32=0.0%, 64=0.0%, >=64=0.0%
     issued rwts: total=0,262144,0,0 short=0,0,0,0 dropped=0,0,0,0
     latency   : target=0, window=0, percentile=100.00%, depth=1

Run status group 0 (all jobs):
  WRITE: bw=1626KiB/s (1665kB/s), 1626KiB/s-1626KiB/s (1665kB/s-1665kB/s), io=1024MiB (1074MB), run=644787-644787msec

Disk stats (read/write):
  mmcblk1: ios=0/262310, merge=0/279, ticks=0/592877, in_queue=592690, util=91.91%

IOPS(1秒あたりのIO数)が406,アクセス速度は1665kB/sでした。

4bitモード

次に/boot/config.txtの追記内容を以下のようにして4bitモードでの速度を測ってみます。

dtoverlay=sdio,poll_once=off,sdio_overclock=50

結果は以下のようになりました。

file1: (g=0): rw=write, bs=(R) 4096B-4096B, (W) 4096B-4096B, (T) 4096B-4096B, ioengine=psync, iodepth=1
fio-3.12
Starting 1 process
Jobs: 1 (f=1): [W(1)][100.0%][w=2312KiB/s][w=578 IOPS][eta 00m:00s]
file1: (groupid=0, jobs=1): err= 0: pid=530: Mon Dec  2 04:47:50 2019
  write: IOPS=577, BW=2310KiB/s (2366kB/s)(1024MiB/453837msec); 0 zone resets
    clat (usec): min=921, max=21609, avg=1715.19, stdev=743.28
     lat (usec): min=922, max=21611, avg=1717.64, stdev=743.29
    clat percentiles (usec):
     |  1.00th=[ 1205],  5.00th=[ 1254], 10.00th=[ 1319], 20.00th=[ 1352],
     | 30.00th=[ 1369], 40.00th=[ 1385], 50.00th=[ 1418], 60.00th=[ 1582],
     | 70.00th=[ 1795], 80.00th=[ 2114], 90.00th=[ 2311], 95.00th=[ 2376],
     | 99.00th=[ 3228], 99.50th=[ 8029], 99.90th=[ 9503], 99.95th=[10159],
     | 99.99th=[11207]
   bw (  KiB/s): min= 2160, max= 2504, per=100.00%, avg=2310.03, stdev=34.79, samples=907
   iops        : min=  540, max=  626, avg=577.46, stdev= 8.71, samples=907
  lat (usec)   : 1000=0.01%
  lat (msec)   : 2=75.77%, 4=23.49%, 10=0.68%, 20=0.06%, 50=0.01%
  cpu          : usr=0.77%, sys=3.67%, ctx=263680, majf=0, minf=26
  IO depths    : 1=100.0%, 2=0.0%, 4=0.0%, 8=0.0%, 16=0.0%, 32=0.0%, >=64=0.0%
     submit    : 0=0.0%, 4=100.0%, 8=0.0%, 16=0.0%, 32=0.0%, 64=0.0%, >=64=0.0%
     complete  : 0=0.0%, 4=100.0%, 8=0.0%, 16=0.0%, 32=0.0%, 64=0.0%, >=64=0.0%
     issued rwts: total=0,262144,0,0 short=0,0,0,0 dropped=0,0,0,0
     latency   : target=0, window=0, percentile=100.00%, depth=1

Run status group 0 (all jobs):
  WRITE: bw=2310KiB/s (2366kB/s), 2310KiB/s-2310KiB/s (2366kB/s-2366kB/s), io=1024MiB (1074MB), run=453837-453837msec

Disk stats (read/write):
  mmcblk1: ios=1/262264, merge=0/91, ticks=1/435935, in_queue=435340, util=95.99%

IOPSは577,アクセス速度は2366kB/sです。

まとめ

アクセス速度を比較すると1bitモードのとき1665kB/s,4bitモードのとき2366kB/sで,4bitモードにすると1.4倍ほど速くなります。
データシートにはシーケンシャル書き込みは最大50MB/sと書いてありますが,ブロックサイズを大きくすればこれくらい出るということかもしれません。

Fuegoにテストを追加する(スクリプト編)

前回はホストPCにFuegoをインストールし,サンプルを実行しました。 tshell.hatenablog.com

今回はシェルスクリプトとして記述したテストを追加する方法です。

環境

前回と同様以下の環境を使用しました。

  • ホストPC : ubuntu 18.04 LTS
  • ターゲット : Raspberry Pi 3(Raspbian Buster Lite)
  • fuego (Commit eeb3c08c...)

作業手順

以下の作業を行ってテストを追加します。

  1. テストディレクトリの作成
  2. テストスクリプトの作成
  3. spec.jsonの作成
  4. fuego_test.shの作成
  5. テストの登録

今回はmytestという名前のテストを作成します。

ディレクトリ作成

Fuegoのインストールディレクトリにテストを作成します。Fuegoは/home/ユーザー名/fuegoにインストールされているとします。
ホストPCのターミナルで以下のコマンドを実行します。

cd /home/ユーザー名/fuego/fuego-core/tests
mkdir Functional.mytest
cd Functional.mytest

テストスクリプトの作成

作成したFunctional.mytestディレクトリの中にmytest-target.shを以下の内容で作成します。
与えられた引数をそのまま表示するスクリプトです。

#!/bin/bash

echo $1

spec.jsonの作成

以下の内容でテストスペックを設定するファイルspec.jsonを作成します。このファイルは必ずspec.jsonという名称で作成する必要があります。

{
        "testName": "Functional.mytest",
        "specs":{
                "fail":{
                        "ARG":"FAIL"
                },
                "success":{
                        "ARG":"SUCCESS"
                },
                "default":{
                        "ARG":"SUCCESS"
                }
        }
}

ここで設定したARGはあとで述べるfuego_test.shの中でFUNCTIONAL_MYTEST_ARGとして使用できます。spec.jsonの中でARGとだけ記述したものに自動的にテスト名が付加されます。

fuego_test.shの作成

以下の内容でfuego_test.shを作成します。このファイルもfuego_test.shという名前で作成する必要があります。

function test_deploy {
        put $TEST_HOME/mytest-target.sh $BOARD_TESTDIR/fuego.$TESTDIR/
}

function test_run {
        report "cd $BOARD_TESTDIR/fuego.$TESTDIR; \
                ./mytest-target.sh $FUNCTIONAL_MYTEST_ARG"
}

function test_processing {
        log_compare "$TESTDIR" "1" "SUCCESS" "p"
}

function test_deployではテスト実行前にターゲットに対して何を転送するか記述しています。

function test_runではテストを実行する際に何をするか記述しています。 Fuegoのreportコマンドは引数で指定されたコマンドを実行して結果をファイルに保存するコマンドです。

Fuego wiki - function report

test_runの中で,spec.jsonで設定したARGを$FUNCTIONAL_MYTEST_ARGとして使っています。

function test_processingはテスト結果を判定する設定です。log_compareコマンドで結果を判定します。

Fuego wiki - function log_compare

各引数の意味は以下のようになっています。

$1 = reflog_prefix = prefix of reference parsed log filename
$2 = match_count = number of matching results
$3 = results_expression
$4 = results_category (either 'p', 'n', or something else)

log_compare "$TESTDIR" "1" "SUCCESS" "p"と記述した場合は,$TESTDIRからログを読み取ってSUCCESSが含まれれば成功とする,という意味になります。

テストの登録

前回のサンプルと同様にFuegoコンテナのコンソールで以下のコマンドを実行して,作成したテストを登録します。

ftc add-jobs -b myboard -t Functional.mytest

上記のコマンドで登録するとspec.jsonのdefaultに記述した内容が有効になります。別のspecを指定したい場合は以下のようにコマンドを実行します。

ftc add-jobs -b mytest -t Functional.mytest -s fail

以上のようにしてシェルスクリプトで作成したテストをFuegoで実行することができます。

FuegoでRaspberry Piをテストする

Fuegoとは

Fuegoとは組み込みLinux用テストフレームワークです。ターゲットにSSH接続してターゲット上でテストを実行し,Jenkinsでテストを管理することができます。
ターゲット上で動作するソフトの単体テストを実行するものではなく,組み込みシステムとしてのテストを実行するものです。
スペイン語で炎という意味です。(炎上しそうですね)

Fuego wikiArchitectureのページに詳しく載っていますが,

Fuego = (Jenkins + abstraction scripts + pre-packed tests) inside a container

ということでJenkinsとテスト実行のためのスクリプト群,よく使いそうなテストの記述がDockerコンテナとして提供されています。

今回はFuegoのインストールとサンプルのテストの実行までやってみます。

構成

f:id:tshell:20191201091340p:plain

Fuegoの構成は上のような感じになっています。ホストPCからターゲット(ここではRaspberry Pi)に対してSSH接続できるようになっている必要があります。
ホストPCではJenkinsとFuegoのスクリプトが含まれるDockerコンテナが起動し,Jenkinsからテストを実行できます。

テストプログラムはホストPC上でクロスコンパイルされ,Raspberry Piに転送されます。Raspberry Pi上で実行されたテストが標準出力上に結果を返し,この結果によってテストの成功・失敗を判定します。なにをもって成功とするかはテストの設定ファイルに記述します。

テストの設定ファイルはFuegoのインストールディレクトリ内に含まれ,Fuegoのコンテナが起動するときにReadOnlyでコンテナにマウントされます。

開発時には以下のものを操作しながら作業することになります。

  • JenkinsのWebUI操作
  • Fuegoコンテナのコンソール操作
  • Fuegoインストールディレクトリ内のファイル編集

参考情報

公式のWikiを読むとだいたい使えるようになります。

環境

以下の環境を使いました。

  • ホストPC : ubuntu 18.04 LTS
  • ターゲット : Raspberry Pi 3 (Raspbian Buster Lite)
  • Fuego (Commit eeb3c08c...)

Fuegoのインストールとテスト実行

Quick Start Guideに記載されているコマンドを実行していきます。

大まかに以下のような流れです。

  1. ホストPCにFuegoをインストールする
  2. Raspberry Piの設定をする
  3. ホストPCでJenkinsにジョブを登録する

Fuegoのインストール

ホストPC上で以下のコマンドを実行し,必要なパッケージをインストールする。

sudo apt-get update
sudo apt-get install git docker.io

ホストPCで以下のコマンドを実行し,Fuegoのリポジトリをクローンする。

git clone https://bitbucket.org/fuegotest/fuego.git

fuegoディレクトリに移動し,インストール用スクリプトを実行する。
(数十分かかります)

cd fuego
./install.sh

インストールが終了したら以下のコマンドを実行する。

./start.sh

start.shを実行すると以下のように,Fuegoコンテナのコンソールにアタッチされる。

f:id:tshell:20191201093726p:plain

http://localhost:8090/fuegoを開き,以下のような画面が表示されればOK

f:id:tshell:20191201093828p:plain

Raspberry Pi側の準備

既存のユーザー(piなど)でテストを実行しても良いと思いますが,ここでは以下のコマンドを実行し,ユーザーfuegoを作成します。

sudo su
mkdir /home/fuego
adduser fuego
(fuegoのパスワード等設定)

rootでの接続が必要な場合

テストによってはrootでの実行が必要かもしれません。
rootでのSSH接続が必要な場合は/etc/ssh/sshd_configのPermitRootLoginをyesに変更し,SSHサーバーを再起動します。

sudo vi /etc/ssh/sshd_config
(PermitRootLogin をyesに変更)
sudo systemctl restart sshd

ボードファイルの作成

テストの実行対象を設定するボードファイルを作成します。 fuego/fuego-ro/boards内にraspberrypi3.boardがすでにあるので,これをmyboard.boardにコピーして編集します。

cd fuego/fuego-ro/boards
cp raspberrypi3.board myboard.board

myboard.boardは以下のように修正しました。(修正箇所のみ載せています)
IPADDRはRaspberry PiIPアドレスSRV_IPはホストPCのIPアドレスを設定します。また,PASSWORDはRaspberry Piに作成したユーザーfuegoのパスワードを指定します。

IPADDR="192.168.0.10
SSH_PORT="22"
SRV_IP="192.168.0.11"
LOGIN="fuego"
BOARD_TESTDIR="/home/fuego/"
PASSWORD="****"
TOOLCHAIN="debian-armhf"

#TARGET_SETUP_LINK="fuego-lava-target-setup"
#TARGET_TEARDOWN_LINK="fuego-lava-target-teardown"

クロス開発用ツールチェインのインストール

Fuegoコンテナのコンソールで以下のコマンドを実行します。

cd /fuego-ro/toolchains
./install_cross_toolchain.sh

Jenkinsにジョブを登録

Fuegoコンテナのコンソールで以下のコマンドを実行し,Jenkinsにノードを登録します。上で作成したmyboard.boardの内容が登録されます。

ftc add-nodes -b myboard

Fuegoコンテナのコンソールで以下のコマンドを実行し,ジョブを登録します。

ftc add-jobs -b myboard -t Functional.hello_world

テストの実行

Jenkinsでビルドの実行をクリックするとテストがビルドされ,Raspberry Piに転送されたあと実行されます。
Functional.hello_worldはデフォルトでは常に成功します。

f:id:tshell:20191201095833p:plain

FuegoからRaspberry Pi上でテストを実行させることができました。次回はテストの記述方法です。

tshell.hatenablog.com

Raspbian buster-liteでCM3+のeMMCをROM化

前回はRaspberry Pi Compute Module 3+(CM3+)にSDカードを増設しました。 tshell.hatenablog.com

産業用途で使う場合であってもなくても,予期しない電源断によってファイルシステムが飛んで二度と起動しなくなるというのは避けたいところです。
CM3+はeMMCからOSが起動するようになっていますが,今回はeMMCをリードオンリーに設定してみます。

環境

  • Raspberry Pi Compute Module 3+
  • Compute Module IO Board v3.0
  • Raspbian Buster-lite

スワップ領域をSDカードに変更する

メモリが1GBもあるのでスワップなしでもよさそうですが一応スワップありの構成で進めます。以降の作業でeMMCへの書き込みがRAMに保持されるようになるのでスワップファイルがデフォルトの/var/swapのままではスワップの意味をなさなくなります。
前回の記事で増設したSDカード上にスワップファイルを作成し,こちらを使用するようにします。

まずはSDカードをフォーマットして,起動時に/mnt/sdcardにマウントされるようにします。

フォーマット

sudo mkfs -t ext4 /dev/mmcblk1

マウントポイントを作成する

sudo mkdir /mnt/sdcard

UUIDを含む行をfstabに追加しておく

sudo bash
lsblk -f | grep mmcblk1 >> /etc/fstab

fstabを編集する

sudo vi /etc/fstab

以下の行を追加する。(あらかじめUUIDを含む行を追加しているのでそのまま利用する。)

UUID=...    /mnt/sdcard ext4    defaults    0   2

再起動して/mnt/sdcardにlost+foundがあることを確認します。

スワップファイルの作成とdphys-swapfileの設定

SDカード上に256MBのスワップファイルを作成します。

sudo dd if=/dev/zero of=/mnt/sdcard/swap/swap bs=1M count=256
sudo chmod 600 /mnt/sdcard/swap/swap
sudo mkswap /mnt/sdcard/swap/swap

作成したスワップファイルが使えるかテストする

sudo swapoff --all
sudo swapon /mnt/sdcard/swap/swap

free -hを実行してswapのサイズが256MBになっていることを確認します。

/etc/dphys-swapfileを以下のようにコメントアウトを解除して編集します。

CONF_SWAPFILE=/mnt/sdcard/swap/swap
CONF_SWAPSIZE=256

再起動後にfree -hを実行し,swapのサイズが256MBであることを確認します。

以上でスワップファイルを増設したSDカードに移すことができました。

fsprotectをインストールする

続いてeMMCをリードオンリー化していきます。
まずはfsprotectをインストールします。

sudo apt-get install fsprotect

aufs-dkmsのセットアップでエラーが出た場合は,以下のようにpsotinstとpreermを削除してから再度セットアップします。

sudo rm /var/lib/dpkg/info/aufs-dkms.postinst
sudo rm /var/lib/dpkg/info/aufs-dkms.prerm
sudo apt-get install fsprotect

initramfsイメージの作成

以降のステップで編集するhook-functionsなどのファイルを生成するため,update-initramfsを実行します。

sudo update-initramfs -c -k `uname -r`

上記を実行すると/usr/share/initramfs-tools/hooks/fsprotectの処理でコケます。
/usr/bin/touchを読みに行くところで失敗しており,/usr/bin/touchは/bin/touchへのシンボリックリンクなので,/usr/share/initramfs-tools/hooks/fsprotectの最終行を以下のように変更します。

copy_exec /bin/touch "/bin"

再度update-initramfsを実行します。

sudo update-initramfs -c -k `uname -r`

hook-functionsの修正

以下のページを参考に,hook-functionsを修正します。
https://www.raspberrypi.org/forums/viewtopic.php?t=161416

/usr/share/initramfs-tools/hook-functionsの524行目付近を以下のように編集します。
(modulesの最後にoverlayを追加する)

for arg in "$@" ; do
        case "$arg" in
        base)
            modules="$modules btrfs ext2 ext3 ext4 ext4dev overlay"

新しいbootスクリプトを作成する

既存のスクリプトをコピーしてoverlayファイルシステムを使用するように変更します。

cd /usr/share/initramfs-tools/scripts
sudo cp local overlay
sudo cp -rp local-premount overlay-premount
sudo cp -rp local-bottom overlay-bottom

/usr/share/initramfs-tools/scripts/overlayのlocal_mount_root()の中を以下のように修正します。
ここに示した行でlocal_mount_root()が終わるようにします。

#   if [ "${readonly}" = "y" ]; then
        roflag=-r
#   else
#       roflag=-w
#   fi

    # FIXME This has no error checking
    modprobe ${FSTYPE}

    checkfs ${ROOT} root

    # FIXME This has no error checking
    # Mount root
    mkdir /upper /lower
    if [ "${FSTYPE}" != "unknown" ]; then
        mount ${roflag} -t ${FSTYPE} ${ROOTFLAGS} ${ROOT} /lower
    else
        mount ${roflag} ${ROOTFLAGS} ${ROOT} /lower
    fi
    modprobe overlay
    mount -t tmpfs tmpfs /upper
    mkdir /upper/data /upper/work
    mount -t overlay \
        -olowerdir=/lower,upperdir=/upper/data,workdir=/upper/work \
        overlay ${rootmnt}

initramfsの作成

以下のコマンドを実行し,initramfsを作成してリネームします。

sudo update-initramfs -c -k `uname -r`
sudo mv /boot/initrd.img-4.19.66-v7+ /boot/initrd7.img

/boot/config.txt,/boot/cmdline.txtの編集

/boot/config.txtに以下を追記します。

kernel=kernel7.img
initramfs initrd7.img

/boot/cmdline.txtの先頭にboot=overlayを追加して再起動します。

動作確認

再起動後,mountを実行し,overlay on / type overlayのような行があればルートファイルシステムがoverlay fs でマウントされています。
適当なファイルを作成して,再起動すると消えることを見てみましょう。

参考にしたページはRaspbian Pixelを使っていましたが,Raspbian busterでも以前と同様にして実現できるようです。