n_hachiのメモ

メモです。

子プロセスの生成・終了を管理するサンプルアプリ

背景

アプリが保持する状態が変化したときに、それを音声でつたえる仕組みが欲しくなった(青色になったら音が鳴る歩行者信号のような機能をイメージしてください)。
本記事では、上記機能を実現したときのメモを記す。

機能と方針

実現したい機能を示す。

  • 前もって複数の音声ファイルを用意しておき、状態変化に応じて適切な音声ファイルを再生する
  • ある状態Aから別の状態Bに遷移したら「状態Bに遷移しました」と音声で通知する。
  • 状態Bに遷移を示す音声再生中(例えば「状態Bに」と発話された直後など)に状態Cへ遷移したら直前の音声ファイル再生を止め、新たな状態遷移を示す音声を再生する

上記実現のために今回選んだ方針を以下に示す。

  • 状態遷移時に子プロセスを生成し、音声再生アプリ(例:aplay)を起動する
  • ワーカスレッドを立て、上記子プロセスの終了を待ち受ける
  • 音声再生中に新たな状態遷移が起きた場合はkillを使い、音声再生中の子プロセスを終了したあと、新しい状態への遷移をしめすための子プロセスを生成する。

ソースコード

以下に今回作成したリポジトリのリンクである。
n-hachi/sound-switcher
ここでは、本記事作成時の最新コミットn-hachi/sound-switcher at 4c2f8275880be9592c58fb074f8d2631c118d9bdを前提に記載する。

今回重要なコードはsrc/lib.cppである。

#include "sound/lib.hpp"

#include <fcntl.h>
#include <signal.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <unistd.h>

#include <iostream>
#include <thread>
#include <vector>

void SoundSwitcher::Stop() {
    // If pid_ is less equal than 0, there is no child process.
    if (pid_ <= 0) {
        return;
    }

    // If pid is more than 0, stop child process.
    kill(pid_, SIGTERM);

    // If thread is joinable
    if (is_running_ && th_.joinable()) {
        th_.join();
    }

    // Set false value to is_running
    is_running_ = false;
}

void SoundSwitcher::ThreadFunc() {
    // Setup flag
    is_running_ = true;

    waitpid(pid_, NULL, 0);
}

void SoundSwitcher::Start(const int num) {
    // If child process has already executed, stop it at first.
    Stop();

    pid_ = vfork();
    if (pid_ == -1) {
        std::cerr << "Error: vfork" << std::endl;
        return;
    }
    if (pid_ == 0) {
        if (Size() <= 0) {
            return;
        }
        int fd = open("/dev/null", O_WRONLY | O_CREAT, 0666);
        dup2(fd, 1);
        dup2(fd, 2);

        const char **argv = new const char *[3];
        argv[0] = "/usr/bin/aplay";
        argv[1] = map_.at(num).c_str();
        argv[2] = NULL;

        const char *cmd = argv[0];
        execvp(cmd, (char **)argv);
    }
    if (pid_ > 0) {
        // Wait for child process exit.
        std::thread th(&SoundSwitcher::ThreadFunc, this);
        th_ = std::move(th);
    }
}

void SoundSwitcher::Insert(const std::string &path) {
    int size = (int)map_.size();
    map_.insert(std::pair<int, std::string>(size, path));
}

size_t SoundSwitcher::Size() const { return map_.size(); }

std::map<int, std::string> SoundSwitcher::map() { return map_; }

ここから各種メソッドを説明する

Insertメソッド

ファイル(この例では音声ファイル)のパスを引数に取りマップに格納する。
マップには、現状のマップのサイズをkey、ファイルパスをvalueとして値を格納する。
key値は後述するstartメソッドで音声ファイルを指定するために使う。

Stopメソッド

子プロセスのPIDが有効な値の場合=すでに子プロセスを生成している場合、子プロセスに対してシグナルを発行し子プロセスを停止する。
子プロセスの終了がきっかけでワーカスレッドが終了する(ワーカスレッド内で実行したwaitpidによる停止が解除される)ためthread::joinでスレッド完了を待つ。

Startメソッド

はじめにStopメソッドを使い、管理中の子プロセスが存在するなら停止する。
vforkを使い子プロセスを生成する。ここから親プロセスと子プロセスで処理が分かれる。
子プロセスはStartメソッドの引数として与えられた値とmap型の変数から再生するファイルパスを取得し、aplayを実行(exec)する。 親プロセスは子プロセスの終了を検知するためのワーカスレッドを作成する

以上が概要である。

終わりに

多分、好ましくない書き方など多くあると思うのでアドバイスなどあればコメントしていただけると嬉しいです。