CPUを全力で回して綺麗なXmasツリーを描いてみた

2022年12月23日 金曜日


【この記事を書いた人】
安井 裕亮

IIJで主に法人向けブロードバンド接続サービスのシステム開発・運用を担当しています。趣味はコーディング、海外旅行、音楽鑑賞、そして宇宙に想いを馳せること。Python / VS Code / Emacs / HHKB / Network

「CPUを全力で回して綺麗なXmasツリーを描いてみた」のイメージ

IIJ 2022 TECHアドベントカレンダー 12/24(土)の記事です】

皆さん、Merry Christmas です!

IIJ の安井(ysk)です。前回の 【多次元対応!?】TUIで動くマインスイーパ作ってみた に引き続き、今回は2022年のアドベントカレンダーの枠をいただきました。毎度お遊びネタで恐縮ですが最後までお付き合いいただければ幸いです!

私の自己紹介については 前回の記事 に書いておりますので、ぜひそちらもご覧ください。

CPUを全力で回して綺麗なXmasツリーを描いてみた

README.md に実行手順を書いていますが、せっかくなのでスクリーンショット付きで実行手順をご説明いたします。Ansible, VirtualBox, Vagrant が必要になりますので予めインストールをお願いします。

まずは Repository をお好みの場所に clone しディレクトリに移動してください。

http / https の通信に proxy が必要な場合は proxy の設定をする必要があります。proxy を設定する場合は Vagrantfile の1行目の http_proxy に proxy のURLを記述してください。

http_proxy = "<URL of your proxy>" # ここにproxyのURLを記載する

Vagrant.configure("2") do |config|
  config.vm.box = "centos/7"
  config.vm.synced_folder ".", "/vagrant", type: "virtualbox"
  ...

準備が出来たら build_and_start_tree.sh を実行します。Vagrant のプラグインのインストールが要求されますのでYを入力しEnterを押してください。

proxy を設定している場合は以下のように2つのプラグインのインストールが要求されます。同様にYを入力しEnterを押してください。

プラグインのインストールが完了したら、再度 build_and_start_tree.sh を実行します。Vagrant のboxイメージのダウンロードが始まります。ダウンロードには少々時間がかかります。

boxのダウンロードが完了するとVMのセットアップが始まります。

お使いの VirtualBox のバージョンに寄るかと思いますが、VirtualBox の Guest Additions のインストールが始まります。これも完了するまで少々時間がかかります。

Guest Additions のインストールが完了すると、ansibleのplaybookの実行が始まります。ここまできたらXmasツリーを拝めるまであと一歩です!

ansibleのplaybookの実行が完了すると、VMにssh接続され htop が立ち上がります。いよいよXmasツリーの登場です!いかがでしょう?綺麗に輝いております!見事に全CPUがフル回転です。これを動かしている皆さんのマシンのCPUファンもきっとフル回転していることでしょう。

どうやってXmasツリーを描いているのか

htop ではユーザプロセスの実行時間である user time が緑で、カーネルの実行時間である system time が赤で表現されます。この特性を利用して user time と system time のワークロードをバランス良く発生させることでXmasツリーを描いています。

user time のワークロードは単にforループを回すだけで容易に発生させられますが、system time のワークロードはどのように発生させているのでしょうか。肝は /dev/busy というデバイスドライバになります。こちらのデバイス、少し遊び心を入れていて、読んでも書いても EBUSY (Device or resource busy) を返すユニークなデバイスとなっております。

[vagrant@localhost ~]$ cat /dev/busy
cat: /dev/busy: Device or resource busy
[vagrant@localhost ~]$ echo 1 > /dev/busy
-bash: echo: write error: Device or resource busy
[vagrant@localhost ~]$

しかしこのデバイス、単にEBUSY を返すだけのデバイスではありません。実は read システムコールや write システムコールの引数である count 値で指定された値分だけカーネル内でforループを回してくれます。これを利用することで system time のワークロードを発生させているのです。

system time のワークロードを発生させる方法はデバイスドライバを利用する以外にもあります。初期の実装ではforループを回すだけのシステムコールを実装し、それを呼び出すことで system time のワークロードを発生させていました。しかしシステムコールを用いた実装だと、カーネル全体のrebuildが必要になったり、日々増えてゆくシステムコールとシステムコール番号がconflictするリスクがあります。今回のようにデバイスドライバを用いた実装であれば、カーネル全体のrebuildは不要になりますし、Linuxの LKM (Loadable Kernel Module) を用いて動的に load / unload 可能になります。またLinux以外のカーネルへの移植も容易になると考えられます。

実際にXmasツリーを描くワークロードを発生しているプログラムのソースコードが以下となります。

#include <busy.h>
#include <pthread.h>
#include <signal.h>
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <utils.h>

#define TREE_WIDTH_MAX 16
#define TREE_RATE_MAX 256U

#define MAX_NUM_THREADS TREE_WIDTH_MAX

struct tree_thread_args
{
    int th_num;
    int tree_width;
};

void do_own_part_of_tree(const int tree_rate)
{
    for (;;)
    {
        do_busy((65536U / TREE_RATE_MAX) * tree_rate);
        do_busy_in_kernel((65536U / TREE_RATE_MAX) * (TREE_RATE_MAX - tree_rate));
    }
}

void *tree_thread(void *arg)
{
    const struct tree_thread_args *const th_args = (struct tree_thread_args *)arg;
    const int th_num = th_args->th_num;
    const int tree_width = th_args->tree_width;

    bind_on_cpu(th_num);

    const int c = (tree_width + 2) / 2;
    const int d = abs(c - (th_num + 1));
    const int tree_rate = (c - d) * TREE_RATE_MAX / c;

    do_own_part_of_tree(tree_rate);

    return NULL;
}

int main(int argc, char *argv[])
{
    if (argc <= 1)
    {
        fputs("Error: tree width not specified\n", stderr);
        exit(1);
    }
    else if (argc > 2)
    {
        fputs("Error: too many arguments\n", stderr);
        exit(1);
    }

    const int tree_width = atoi(argv[1]);

    if (tree_width <= 0 || tree_width > TREE_WIDTH_MAX)
    {
        fputs("Error: illegal tree width specified\n", stderr);
        exit(1);
    }

    const int num_threads = tree_width;

    pthread_t threads[MAX_NUM_THREADS];
    struct tree_thread_args th_args[MAX_NUM_THREADS];

    for (int i = 0; i < num_threads; i++)
    {
        th_args[i].th_num = i;
        th_args[i].tree_width = tree_width;

        pthread_create(&threads[i], NULL, tree_thread, &th_args[i]);
    }

    for (int i = 0; i < num_threads; i++)
    {
        pthread_join(threads[i], NULL);
    }

    return 0;
}

詳しい説明は割愛しますが、コマンドライン引数で与えられた数だけスレッドを生成し、生成した各々のスレッドに対して、実行する user time と system time のワークロードの量をスレッド番号に応じて指定し、全体としてXmasツリーの形状が描かれるよう調整しています。各スレッドはプロセススケジューリングにより実行されるコアが変化しないよう特定のコアにバインドしています。

上記のソースコード上で呼び出している do_busy()do_busy_in_kernel() を定義しているライブラリのソースコードは以下です。

#include <busy.h>
#include <fcntl.h>
#include <stdio.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <unistd.h>

#define BUSY_DEVICE_PATH "/dev/busy"

void do_busy(u_int32_t count)
{
    for (volatile u_int32_t i = 0; i < count; i++)
        ;
}

int do_busy_in_kernel(u_int32_t count)
{
    const int fd = open(BUSY_DEVICE_PATH, O_RDONLY);
    if (fd == -1)
    {
        perror("open");
        return -1;
    }

    read(fd, NULL, (size_t)count);

    close(fd);

    return 0;
}

do_busy() では単にforループを回しているだけです。do_busy_in_kernel() では、/dev/busy を開き count 分だけ read するという処理をしています。これで count の値分だけカーネル内でforループが回ります。

最後に

いかがでしたでしょうか。今回はCPUを全力で回して綺麗なXmasツリーを描いてみました。CPUがフル回転しますので、Xmasツリーを眺めながらCPUの放出する熱で暖も取れます(笑)

皆さんも手元のマシンのCPUを全力で回して綺麗なXmasを眺めつつ、暖かいクリスマスをお過ごしください!

IIJ Engineers blog読者プレゼントキャンペーン

Twitterフォロー&条件付きツイートで、「IoT米」と「バリーくんストラップ」と「バリーくんシール」のセットを抽選でプレゼント!
応募期間は2022/12/01~2022/12/31まで。詳細はこちらをご覧ください。
今すぐツイートするならこちら→ フォローもお忘れなく!

安井 裕亮

2022年12月23日 金曜日

IIJで主に法人向けブロードバンド接続サービスのシステム開発・運用を担当しています。趣味はコーディング、海外旅行、音楽鑑賞、そして宇宙に想いを馳せること。Python / VS Code / Emacs / HHKB / Network

Related
関連記事