tshell_blog

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

Docker for WindowsでNeo4Jを動かす

グラフデータベースNeo4JをDockerで動かしてみました。 SQLなどの一般的にイメージされるデータベースと異なり,グラフ構造のデータを格納することができるデータベースです。ノードとノードのつながりを表現することができるので,要求と仕様を対応させられるツールが作れそうだな~なんて夢が広がります。

環境

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

  • Windows 10 Professional version 1903
  • Docker CE 19.03
Client: Docker Engine - Community
 Version:           19.03.5
 API version:       1.40
 Go version:        go1.12.12
 Git commit:        633a0ea
 Built:             Wed Nov 13 07:22:37 2019
 OS/Arch:           windows/amd64
 Experimental:      false

Server: Docker Engine - Community
 Engine:
  Version:          19.03.5
  API version:      1.40 (minimum version 1.12)
  Go version:       go1.12.12
  Git commit:       633a0ea
  Built:            Wed Nov 13 07:29:19 2019
  OS/Arch:          linux/amd64
  Experimental:     false
 containerd:
  Version:          v1.2.10
  GitCommit:        b34a5c8af56e510852c35414db4c1f4fa6172339
 runc:
  Version:          1.0.0-rc8+dev
  GitCommit:        3e425f80a8c931f88e6d94a8c831b9d5aa481657
 docker-init:
  Version:          0.18.0
  GitCommit:        fec3683
  • Neo4j:4.0

インストール

コマンドプロンプトまたはPowershellで以下のコマンドを実行してNeo4JのDockerイメージを取得しておきます。

> docker image pull neo4j:4.0

コンテナの起動

以下のコマンドでコンテナを起動します。コマンドを実行するディレクトリがコンテナの/dataにマウントされます。

コマンドプロンプトの場合

> docker run -p 7474:7474 -p 7687:7687 -v "$pwd:/data" neo4j:4.0

Powershellの場合

> docker run -p 7474:7474 -p 7687:7687 -v "$(Get-Location):/data" neo4j:4.0

コンテナが起動すると以下のようなメッセージが表示されます。

Directories in use:
  home:         /var/lib/neo4j
  config:       /var/lib/neo4j/conf
  logs:         /logs
  plugins:      /var/lib/neo4j/plugins
  import:       /var/lib/neo4j/import
  data:         /var/lib/neo4j/data
  certificates: /var/lib/neo4j/certificates
  run:          /var/lib/neo4j/run
Starting Neo4j.
2020-02-13 04:39:51.067+0000 INFO  ======== Neo4j 4.0.0 ========
2020-02-13 04:39:51.077+0000 INFO  Starting...

Webインターフェースへのアクセス

http://localhost:7474へアクセスすると以下のような画面が表示されます。

f:id:tshell:20200217211712p:plain

以下を入力してConnectをクリックします。

  • Connect URL: bold://localhost:7687
  • Authentication type: Username / Password
  • Username: neo4j
  • Password: neo4j

f:id:tshell:20200217211748p:plain

ログインに成功すると以下のようなページが表示されます。

f:id:tshell:20200217211820p:plain

サンプルデータベースの作成

サンプルの映画データベースを作成してみます。
Webインターフェース上部のコマンド入力部分に:play movie graphを入力し,右側の▷をクリックします。

f:id:tshell:20200217211915p:plain

下部にある>をクリックして2ページ目に移動するとcypherスクリプトが表示されます。枠の中をクリックします。

f:id:tshell:20200217211954p:plain

上部のコマンド入力部分に空くrぷ都がコピーされるので,右側の▷をクリックします。

f:id:tshell:20200217212042p:plain

以下のようにグラフが表示されます。

f:id:tshell:20200217212112p:plain

データの取得

キアヌリーヴスの出演映画を取得してみましょう。
以下のコマンドをコマンド入力部に入れ,右側の▷をクリックします。
コマンド入力部ではShift + Enterで改行できます。

MATCH (actor:Person)-[r:ACTED_IN]->(movie)
WHERE actor.name = "Keanu Reeves"
RETURN actor,movie

実行すると以下のようなグラフが表示されます。
(ジョンウィックがない・・)

f:id:tshell:20200217212258p:plain

表示形式はグラフのほかにもレスポンスのテキスト形式などに切り替えることができます。

f:id:tshell:20200217212350p:plain

まとめ

Neo4JをDocker for Windowsで起動し,チュートリアルを実行できました。
グラフデータベース,何かにつかえそうですね,なにかに。

ROS2 Pub/Subサンプル

ROSやROS2ではPub/Sub方式で他のノードと通信することができます。
PublisherノードとSubscriberノードを作成して,Pub/Sub通信をしてみます。

プロジェクトの作成

ROS2開発環境のDockerコンテナを起動します。
Dockerイメージの作成方法,使用方法は以下の記事にまとめています。

tshell.hatenablog.com

tshell.hatenablog.com

コンテナを起動したら以下のコマンドを実行し,pubsubプロジェクトを作成していきます。

$ source /opt/ros/dashing/setup.bash
$ cd /home
$ mkdir src
$ cd src
$ ros2 pkg create --build-type ament_cmake pubsub --dependencies rclcpp

作成されたpubsubディレクトリ内のsrc ディレクトリに移動し,publisher.cppとsubscriber.cppを作成していきます。

ソースの記述

publisher.cppの作成

以下の内容でpublisher.cppを作成します。

#include <memory>
#include <chrono>
#include "rclcpp/rclcpp.hpp"
#include "std_msgs/msg/string.hpp"

using namespace std::chrono_literals;

class Publisher : public rclcpp::Node
{
    public:
    Publisher() : Node("publisher_node")
    {
        timer_ = create_wall_timer(1s, std::bind(&Publisher::timer_callback, this));
        publisher_ = this->create_publisher<std_msgs::msg::String>("topic", 10);

    }

    private:
    void timer_callback()
    {
        auto message = std_msgs::msg::String();
        message.data = "Hello World";
        RCLCPP_INFO(this->get_logger(), "Publishing: '%s'", message.data.c_str());
        publisher_->publish(message);
    }
    rclcpp::TimerBase::SharedPtr timer_;
    rclcpp::Publisher<std_msgs::msg::String>::SharedPtr publisher_;
};

int main(int argc, char *argv[])
{
    rclcpp::init(argc, argv);
    rclcpp::spin(std::make_shared<Publisher>());
    rclcpp::shutdown();
    return 0;
}

subscriber.cppの作成

以下の内容でsubscriber.cppを作成します。

#include <memory>
#include "rclcpp/rclcpp.hpp"
#include "std_msgs/msg/string.hpp"

using std::placeholders::_1;

class Subscriber : public rclcpp::Node
{
    public:
    Subscriber() : Node("subscriber_node")
    {
        subscriber_ = this->create_subscription<std_msgs::msg::String>("topic", 10, std::bind(&Subscriber::topic_callback, this, _1));
    }

    private:
    void topic_callback(const std_msgs::msg::String::SharedPtr msg) const
    {
        RCLCPP_INFO(this->get_logger(), "Subscribing: '%s'", msg->data.c_str());
    }
    rclcpp::Subscription<std_msgs::msg::String>::SharedPtr subscriber_;
};


int main(int argc, char *argv[])
{
    rclcpp::init(argc, argv);
    rclcpp::spin(std::make_shared<Subscriber>());
    rclcpp::shutdown();
    return 0;
}

CMakeLists.txtの編集

ros2 pkg createコマンドで自動生成されたCMakeLists.txtを編集し,以下のような内容にします。

cmake_minimum_required(VERSION 3.5)
project(pubsub)

# Default to C99
if(NOT CMAKE_C_STANDARD)
  set(CMAKE_C_STANDARD 99)
endif()

# Default to C++14
if(NOT CMAKE_CXX_STANDARD)
  set(CMAKE_CXX_STANDARD 14)
endif()

if(CMAKE_COMPILER_IS_GNUCXX OR CMAKE_CXX_COMPILER_ID MATCHES "Clang")
  add_compile_options(-Wall -Wextra -Wpedantic)
endif()

# find dependencies
find_package(ament_cmake REQUIRED)
find_package(rclcpp REQUIRED)
find_package(std_msgs REQUIRED)

add_executable(publisher src/publisher.cpp)
ament_target_dependencies(publisher rclcpp std_msgs)
install(TARGETS
publisher
DESTINATION lib/${PROJECT_NAME}
)

add_executable(subscriber src/subscriber.cpp)
ament_target_dependencies(subscriber rclcpp std_msgs)
install(TARGETS
subscriber
DESTINATION lib/${PROJECT_NAME}
)

if(BUILD_TESTING)
  find_package(ament_lint_auto REQUIRED)
  # the following line skips the linter which checks for copyrights
  # uncomment the line when a copyright and license is not present in all source files
  #set(ament_cmake_copyright_FOUND TRUE)
  # the following line skips cpplint (only works in a git repo)
  # uncomment the line when this package is not in a git repo
  #set(ament_cmake_cpplint_FOUND TRUE)
  ament_lint_auto_find_test_dependencies()
endif()

ament_package()

ビルド

上記の3ファイルを編集したら以下のコマンドを実行してビルドします。

$ cd /home
$ colcon build

動作確認

Publisherの起動

ROS2開発環境のDockerコンテナ内で以下のコマンドを実行し,Publisherを起動します。

$ cd /home/src
$ source install/setup.bash
$ ros2 run pubsub publisher

以下のように1秒毎に文字列がPublishされます。
f:id:tshell:20200216193717p:plain

Subscriberの起動

ROS2開発環境のDockerコンテナをもう一つ起動し,以下のコマンドを実行してSubscriberを起動します。

$ source /opt/ros/dashing/setup.bash
$ cd /home/src
$ source install/setup.bash
$ ros2 run pubsub publisher

以下のようにSubscribeした文字列を表示します。
publisher.cppとsubscriber.cppで同じトピックを指定しているので上記で起動したPublisherが出力した文字がSubscriberに表示されます。
f:id:tshell:20200216193920p:plain

Docker環境でROS2プログラム開発

前回作成したROS2開発環境のDockerイメージを使ってROS2プログラムを作っていきます。

tshell.hatenablog.com

以下のような流れです。

  1. パッケージの作成
  2. ソースの記述
  3. CMakeLists.txtの編集
  4. package.xmlの編集
  5. ビルド
  6. 動作確認

コンテナの起動

以下のようにして/etc/group/etc/passwdをマウントし,Dockerコンテナのシェルを一般ユーザーで起動するようにするとソースの記述やCMakeLists.txtを編集するときにsudoを使う手間がなくなります。

以降の手順ではros2 pkg createによってディレクトリやファイルが自動生成されるので,空のディレクトリ内で以下を実行します。

$ docker run --rm -it -v `pwd`:/home -v /etc/group:/etc/group:ro -v /etc/passwd:/etc/passwd:ro -u $(id -u $USER):$(id -g $USER) ros2:dashing 

コンテナ起動後に以下を実行します。

$ source /opt/ros2/dashing/setup.bash

パッケージの作成

パッケージはsrcディレクトリ内に作成する必要があります。まずはsrcディレクトリを作成し,その中でros2 pkg createコマンドを実行します。

ビルド時にはここで作成したsrcディレクトリの一つ上(ここでは/home)でcolcon buildコマンドを実行する必要があります。

$ cd /home
$ mkdir src
$ cd src
$ ros2 pkg create --build-type ament_cmake sample --dependencies rclcpp

sample/src内にsample.cppを作成

上のようなros2 pkg createコマンドを実行するとsampleディレクトリが作成され,その中にsrcディレクトリが作成されます。
以下のようなsample.cppをsrcディレクトリ内に作成します。

#include <memory>
#include <chrono>
#include "rclcpp/rclcpp.hpp"

using namespace std::chrono_literals;

class SampleClass : public rclcpp::Node
{
    public:
    SampleClass() : Node("sample_node")
    {
        timer_ = create_wall_timer(1s, std::bind(&SampleClass::timer_callback, this));
    }
    private:
    void timer_callback()
    {
        RCLCPP_INFO(this->get_logger(), "Hello World");
    }
    rclcpp::TimerBase::SharedPtr timer_;
};

int main(int argc, char *argv[])
{
    rclcpp::init(argc, argv);
    rclcpp::spin(std::make_shared<SampleClass>());
    rclcpp::shutdown();
    return 0;
}

CMakeLists.txtの編集

CMakeLists.txtを以下のように編集します。
Dockerコンテナを立ち上げたディレクトリ内にあるファイルを適当なエディタで編集すればDockerコンテナ内にも反映されます(当たり前ですが)

cmake_minimum_required(VERSION 3.5)
project(sample)

# Default to C99
if(NOT CMAKE_C_STANDARD)
  set(CMAKE_C_STANDARD 99)
endif()

# Default to C++14
if(NOT CMAKE_CXX_STANDARD)
  set(CMAKE_CXX_STANDARD 14)
endif()

if(CMAKE_COMPILER_IS_GNUCXX OR CMAKE_CXX_COMPILER_ID MATCHES "Clang")
  add_compile_options(-Wall -Wextra -Wpedantic)
endif()

# find dependencies
find_package(ament_cmake REQUIRED)
find_package(rclcpp REQUIRED)

add_executable(sample_package src/sample.cpp)
ament_target_dependencies(sample_package rclcpp)
install(TARGETS
sample_package
DESTINATION lib/${PROJECT_NAME}
)

if(BUILD_TESTING)
  find_package(ament_lint_auto REQUIRED)
  # the following line skips the linter which checks for copyrights
  # uncomment the line when a copyright and license is not present in all source files
  #set(ament_cmake_copyright_FOUND TRUE)
  # the following line skips cpplint (only works in a git repo)
  # uncomment the line when this package is not in a git repo
  #set(ament_cmake_cpplint_FOUND TRUE)
  ament_lint_auto_find_test_dependencies()
endif()

ament_package()

package.xmlの編集

package.xmlを以下のように編集します。

<?xml version="1.0"?>
<?xml-model href="http://download.ros.org/schema/package_format3.xsd" schematypens="http://www.w3.org/2001/XMLSchema"?>
<package format="3">
  <name>sample</name>
  <version>0.0.0</version>
  <description>TODO: Package description</description>
  <maintainer email="root@todo.todo">root</maintainer>
  <license>TODO: License declaration</license>

  <buildtool_depend>ament_cmake</buildtool_depend>

  <test_depend>ament_lint_auto</test_depend>
  <test_depend>ament_lint_common</test_depend>

  <build_depend>rclcpp</build_depend>
  <exec_depend>rclcpp</exec_depend>

  <export>
    <build_type>ament_cmake</build_type>
  </export>
</package>

ビルド

ros2 pkg createコマンドを実行したディレクトリの一つ上(ここでは/home)に移動し,colcon buildを実行します。

$ cd /home
$ colcon build

動作確認

ビルドに成功したら以下のコマンドを実行し,動作確認を行います。

$ source install/setup.bash
$ ros2 run sample sample_package

ここでsampleros2 pkg createコマンド実行時に与えたパッケージ名,sample_packageはCMakeLists.txtで指定したexecutable名です。
これらを変えれば好きな名前にできます。

上記のコマンドを実行すると以下のように1秒毎に文字列が表示されます。

[INFO] [sample_node]: Hello World
[INFO] [sample_node]: Hello World
[INFO] [sample_node]: Hello World
[INFO] [sample_node]: Hello World

開発環境コンテナでROS2プログラムを作成することができました。
VSCode拡張機能か何かを使ってラクしたいですね〜

ROS2開発環境をDockerで構築する

ROS2の勉強始めよっかな〜と思い立ったので ROS2ではじめよう 次世代ロボットプログラミングの第2章を参考にROS2環境をDockerで構築してみました。

セットアップ方法を見ていると「ROS1環境があるところにROS2をセットアップすると環境変数とか依存関係で大変なことになる場合があります」というようなことが書いてあり,そのままインストールするとあとで大変になりそうなのでDockerイメージとして構築してみます。

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

以下のようなDockerfileを作成します。

# docker build -t ros2:dashing .
FROM ubuntu:18.04
ENV DEBIAN_FRONTEND noninteractive

ENV ROS_DISTRO dashing

# ロケールのセットアップ
RUN apt-get update && apt-get install -y locales && \
    dpkg-reconfigure locales && \
    locale-gen ja_JP ja_JP.UTF-8 && \
    update-locale LC_ALL=ja_JP.UTF-8 LANG=ja_JP.UTF-8
ENV LC_ALL   ja_JP.UTF-8
ENV LANG     ja_JP.UTF-8
ENV LANGUAGE ja_JP.UTF-8

# APTソースリストの設定
RUN apt-get update && \
    apt-get install -y curl gnupg2 lsb-release && \
    curl http://repo.ros2.org/repos.key | apt-key add - && \
    sh -c 'echo "deb [arch=amd64,arm64] http://packages.ros.org/ros2/ubuntu \
    `lsb_release -cs` main" > /etc/apt/sources.list.d/ros2-latest.list' && \
    apt-get update

# ROS2パッケージのインストール
RUN export ROS_DISTRO=dashing && \
    apt-get install -y ros-$ROS_DISTRO-desktop \
    python3-colcon-common-extensions python3-rosdep python3-argcomplete && \
    rosdep init && \
    rosdep update

## 環境設定
RUN    echo "source /opt/ros/$ROS_DISTRO/setup.bash" >> ~/.bashrc

Dockerfileを保存したディレクトリの中で以下を実行します。

$ docker build --rm -t ros2:dashing .

30分以上かかるので気長に待ちます。

動作確認

demo_nodes_cppに含まれるTalkerとListenerを使って動作確認してみます。
うまく行けばTalkerで出力された内容がListenerで受信されることを確認できます。

ターミナルを2つ開き,それぞれで以下のコマンドを実行してコンテナを起動します。

$ docker run --rm -it ros2:dashing

Listenerの起動

一方のターミナルで以下のコマンドを実行し,Listenerを起動します。

# ros2 run demo_nodes_cpp listener

Talkerの起動

もう一方のターミナルで以下のコマンドを実行し,Talkerを起動します。

# ros2 run demo_nodes_cpp talker

表示例

Talkerの表示例
f:id:tshell:20200213174147p:plain

Listenerの表示例
f:id:tshell:20200213174223p:plain

以上のようにTalkerの内容がListenerに反映されれば正常に動作しています。

あとは以下のような感じでROS2ノードを作って遊びましょう。

tshell.hatenablog.com

tshell.hatenablog.com

RealTime処理のサンプルプログラム

tshell.hatenablog.com

前回の記事ではDockerコンテナ上でcyclictestを実行しました。
今回は HOWTO build a simple RT applicationを参考にリアルタイムアプリケーションを作成してみます。

Exampleに載っている内容をsample.cとして保存し,以下のコマンドでコンパイルします。

$ gcc -pthread sample.c

Dockerコンテナを起動して実行してみます。生成されたa.outがあるディレクトリ内で以下を実行します。

$ docker run --rm -it --cpu-rt-runtime=950000 --ulimit rtprio=99 --cap-add=sys_nice -v `pwd`:/home ubuntu

コンテナが起動したら/homeに移動し,./a.outを実行します。
エラーがあった場合にはエラー箇所が表示されますが,正常に動作した場合は何も表示されません。あまり面白くないですね...

周期実行サンプル

HOWTO build a basic cyclic applicationを参考に以下のようなファイルをsample_cyclic.cとして作成します。

/*                                                                  
 * POSIX Real Time Example
 * using a single pthread as RT thread
 */
 
#include <limits.h>
#include <pthread.h>
#include <sched.h>
#include <stdio.h>
#include <stdlib.h>
#include <sys/mman.h>
#include <time.h>
 
struct period_info {
        struct timespec next_period;
        long period_ns;
};

static void inc_period(struct period_info *pinfo) 
{
        pinfo->next_period.tv_nsec += pinfo->period_ns;
 
        while (pinfo->next_period.tv_nsec >= 1000000000) {
                /* timespec nsec overflow */
                pinfo->next_period.tv_sec++;
                pinfo->next_period.tv_nsec -= 1000000000;
        }
}
 
static void periodic_task_init(struct period_info *pinfo)
{
        /* for simplicity, hardcoding a 1ms period */
        pinfo->period_ns = 1000000;
 
        clock_gettime(CLOCK_MONOTONIC, &(pinfo->next_period));
}
 
static void do_rt_task()
{
        struct timespec ts;
        clock_gettime(CLOCK_REALTIME, &ts);
        printf("time: %10ld.%09ld\n", ts.tv_sec, ts.tv_nsec);
        
        /* Do RT stuff here. */
}
 
static void wait_rest_of_period(struct period_info *pinfo)
{
        inc_period(pinfo);
 
        /* for simplicity, ignoring possibilities of signal wakes */
        clock_nanosleep(CLOCK_MONOTONIC, TIMER_ABSTIME, &pinfo->next_period, NULL);
}

void *thread_func(void *data)
{
        /* Do RT specific stuff here */
        
        struct period_info pinfo;
 
        periodic_task_init(&pinfo);
 
        while (1) {
                do_rt_task();
                wait_rest_of_period(&pinfo);
        }
 
        return NULL;
}
 
int main(int argc, char* argv[])
{
        struct sched_param param;
        pthread_attr_t attr;
        pthread_t thread;
        int ret;
 
        /* Lock memory */
        if(mlockall(MCL_CURRENT|MCL_FUTURE) == -1) {
                printf("mlockall failed: %m\n");
                exit(-2);
        }
 
        /* Initialize pthread attributes (default values) */
        ret = pthread_attr_init(&attr);
        if (ret) {
                printf("init pthread attributes failed\n");
                goto out;
        }
 
        /* Set a specific stack size  */
        ret = pthread_attr_setstacksize(&attr, PTHREAD_STACK_MIN);
        if (ret) {
            printf("pthread setstacksize failed\n");
            goto out;
        }
 
        /* Set scheduler policy and priority of pthread */
        ret = pthread_attr_setschedpolicy(&attr, SCHED_FIFO);
        if (ret) {
                printf("pthread setschedpolicy failed\n");
                goto out;
        }
        param.sched_priority = 80;
        ret = pthread_attr_setschedparam(&attr, &param);
        if (ret) {
                printf("pthread setschedparam failed\n");
                goto out;
        }
        /* Use scheduling parameters of attr */
        ret = pthread_attr_setinheritsched(&attr, PTHREAD_EXPLICIT_SCHED);
        if (ret) {
                printf("pthread setinheritsched failed\n");
                goto out;
        }
 
        /* Create a pthread with specified attributes */
        ret = pthread_create(&thread, &attr, thread_func, NULL);
        if (ret) {
                printf("create pthread failed\n");
                goto out;
        }
 
        /* Join the thread and wait until it is done */
        ret = pthread_join(thread, NULL);
        if (ret)
                printf("join pthread failed: %m\n");
 
out:
        return ret;
}

最初のsample.cと同様に以下のようにしてコンパイルします。

$ gcc -pthread -o cyclic sample_cyclic.c

Dockerコンテナ上で./cyclicを実行すると以下のように1msごとに時刻が表示されます。

time: 1580301619.999005823
time: 1580301620.000007537
time: 1580301620.001007728
time: 1580301620.002007485
time: 1580301620.003007308
time: 1580301620.004011275
time: 1580301620.005009829
time: 1580301620.006012556
time: 1580301620.007010412
time: 1580301620.008012001
time: 1580301620.009012342

リアルタイム処理の記述

以上でとりあえずサンプルは動きました。
自分の好きなことを好きな間隔で実行させるには以下の箇所を修正します。

static void periodic_task_init(struct period_info *pinfo)
{
        /* for simplicity, hardcoding a 1ms period */
        pinfo->period_ns = 1000000;
 
        clock_gettime(CLOCK_MONOTONIC, &(pinfo->next_period));
}
 
static void do_rt_task()
{
        struct timespec ts;
        clock_gettime(CLOCK_REALTIME, &ts);
        printf("time: %10ld.%09ld\n", ts.tv_sec, ts.tv_nsec);
        
        /* Do RT stuff here. */
}

periodic_task_init内のpinfo->period_nsを変更すれば実行周期を変えられます。

do_rt_task()の中には周期ごとに実行したい処理を記述しましょう。

以上で最低限のリアルタイムは実行できるようになります。

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の結果をヒストグラム表示してもう少し詳しく見てみます。