工作と競馬2

電子工作、プログラミング、木工といった工作の記録記事、競馬に関する考察記事を掲載するブログ

システムシャットダウンの際、Pythonアプリのデーモンを安全に終了する

概要

Pythonアプリのデーモンをシャットダウン時に安全に終了するためのメモ。

背景と目的

Pythonアプリのデーモンを、システムシャットダウン時に安全に終了するための基本的な実装方法を確認する。


詳細

0. 環境


1. 実装

Python標準モジュールのsignalを使うことで、システムシャットダウン時に、デーモンにSIGNALが送られたのを、アプリが受信できる。

以下、具体的な実装をメモ。

1.1 Pythonアプリ

  • SIGTERMを受け取ったら、handlerという関数が呼ばれるようにする。
  • SIGTERM受信したら、フラグを立てる。
  • while ループでフラグを調べて、立っていたら抜ける。→アプリが終了

test_signal.py

# coding: utf-8

import signal
import time

x = {
    "end": False,
    "signum": None,
    "frame": None
}

def handler(signum, frame):
    x["end"] = True # 終了させる
    x["signum"] = signum
    x["frame"] = frame

print("test_signal start!")

signal.signal(signal.SIGTERM, handler)

print("test_signal loop!")

while True:
    time.sleep(1)
    if x["end"]:
        print("test_signal handler! {},{}".format(x["signum"], x["frame"]))
        break

1.2 呼び出し用シェルスクリプト

Pythonアプリを直接デーモンとして実行してもいいが、起動用シェルスクリプトをかませることが多いので、試しにシェルスクリプトから呼ぶ。

test_signal.sh

#!/bin/bash

HERE=$(cd $(dirname $0); pwd)

echo "test_signal sh start"

python3 $HERE/test_signal.py

echo "test_signal sh end"

1.3 サービスファイル

test_signal.shが呼ばれるようにしておく。

test_signal.service

[Unit]
Description=test_signal
After=multi-user.target

[Service]
ExecStart=/bin/bash /path/to/test_signal.sh
Restart=on-failure
Type=simple

[Install]
WantedBy=multi-user.target

配置してデーモン有効化、起動。

cp ./test_signal.service /etc/systemd/system
systemctl daemon-reload
systemctl enable test_signal
systemctl start test_signal


2. 動作確認

2.1 手動で止めてみる

systemctl stop test_signal

ログを見ると、以下の感じでちゃんと止まった。 shから呼ばれたPythonアプリでも、SIGTERMを受信して、Pythonアプリで処理できている。

Mar 10 07:14:27 ****** bash[2048]: start!
Mar 10 07:14:27 ****** bash[2048]: handler! 15,<frame at 0x7690fc30, file '/r
Mar 10 07:14:27 ****** bash[2048]: terminate!
Mar 10 07:14:27 ****** systemd[1]: Stopped test_signal Application.

2.2 システムシャットダウン

シャットダウンしてみた。再起動後、syslogを確認したところ、以下。ちゃんと止まった模様。 ただし、なぜかハンドラ関数が2回呼ばれる(SIGTERMを2回受信?)ようなので、念のため、ハンドラ関数内で2回実行されると困るものはやらないようにするほうがいいかもしれない。今回のように、メインループを抜けるためのフラグを立てるだけみたいな単純なものであれば問題ないだろう。

Mar 10 07:40:07 ********** systemd[1]: Started test_signal Application.
Mar 10 07:40:07 ********** bash[2550]: test_signal sh start
Mar 10 07:40:13 ********** systemd[1]: Stopping test_signal Application...
Mar 10 07:40:14 ********** bash[2550]: test_signal start!
Mar 10 07:40:14 ********** bash[2550]: test_signal loop!
Mar 10 07:40:14 ********** bash[2550]: test_signal handler! 15,<frame at 0x769bbc30, file '/root/test/test_signal.py', line 28, code <module>>
Mar 10 07:40:14 ********** bash[2550]: test_signal end!
Mar 10 07:40:14 ********** systemd[1]: Stopped test_signal Application.


まとめと今後の課題

とりあえず、安全にデーモンを終了させられそう。

ESP32のRAM周りの理解のなさを反省、カメラを使うならRAM増強が必要

背景

ここ最近、ESP32とカメラモジュールを繋いでをいじっていたのだが、RAM周りについて理解が深まったというか、そもそも理解が足らなかったと痛感した。


わかったこと

1. ユーザープログラム上の変数データで使えるRAMは120数kB

私の使っているESP-WROOM-32 DevKitCには、ESP-WROOM-32が載っていて、SRAMが520kBとあり、この分がすべて使えそうである。しかし、実際にユーザープログラム上の変数データで使えるのは、マニュアルにある通り、SRAM2の領域200kBのうちの一部のようだ。(ただし、あらゆる方法を試したわけではない。下記の方法で調べた限りで。)

以下のような全く何もしないArduinoプログラムを書き込んだ場合、

void setup(){

}

void loop (){

}

Arduino IDEの書き込みログで、約307kB使えると表示される。

最大327680バイトのRAMのうち、グローバル変数が13228バイト(4%)を使っていて、ローカル変数で314452バイト使うことができます。

一方、こちらを使って残りメモリを求めると、setup関数スタート時点で、

127168

となる。結局使えるのは124kB程度。背景で動いているFree RTOSなのか、Arduinoのせいなのか細かいことはわからないが。

以下のように、各変数の配置先アドレスを調べてみる。

const int a = 1; // constはFlashメモリに配置される
int c[256 * 108 + 185] = {0}; // グローバル変数としてRAMが消費される
int b = 2; // グローバル変数としてRAMが消費される

void setup() {

  int i = 0;

  Serial.begin(115200);
  Serial.printf("a, address=%x\n", &a);
  Serial.printf("i, address=%x\n", &i);
  Serial.printf("b, address=%x\n", &b);
  Serial.printf("c, address=%x - %x\n", &c[0], &c[256 * 108 + 185 - 1]);

}

void loop (){

}

マニュアルと照らし合わせると、aは、constなのでFlashメモリに配置されている。i, b, cこれはSRAM2の領域だ。他にもいろいろやってみたが、SRAM1への配置はされなかった。どうやら、SRAM2の120kB強くらいしか使えないらしい。

a, address=3f400178
i, address=3ffb878c
b, address=3ffbdbb4
c, address=3ffbfdd0 - 3ffdb0b0


2. PSRAMを外付け(External)することでRAMが増強できる

マニュアルを読むと、External Memoryという項目が確認できる。 SPI接続のSRAMを外付けすることで、4MBまでRAMが増強できるらしい。

ESP-IDF Programming Guideには、より詳しくプログラムから利用する方法がかかれている。 やってはいないが、Arduino IDEのメニューバー>ツールの中にPSRAMという項目があってこれを有効化すれば、外付けSRAMが使えそうだ。


3. ESP32-CAMはPSRAMが増強されている

最近、ESP32にカメラを繋いでいじっていたときに、最高解像度設定(UXGA)で撮影したところ、一見ちゃんと取れたかに見えたのに、実は画像の一部が欠けて表示されていなかった。プログラムをいろいろいじってわかったのだが、上述の使用可能メモリ量のせいで、画像全体のデータが保持できていないことがわかった。

じゃあ、Amazonなどで格安で売っているESP32-CAMはどうしてちゃんと撮れるのか?というと、上述のPSRAMが増強されているから、ということがわかった。

ESP32-CAMの基板の写真をよく見ると、IPS6404という8本足のICが載っていて、調べたらこれがまさに外付けSRAMだった。まあ、カメラモジュール基板として作られているので必要なRAM容量が確保されているのは当たり前ではあるが、改めてちゃんと見ると納得。


3. ESP32-CAMが使えないとすると、PSRAMが一緒に載っているモジュールESP32-WROVER-Eを使って、カメラを繋げばいい

とはいえ、Amazonなどで売られているものの写真を見る限り、ESP32-CAMは技適をクリアしていない。(ように見える。)技適マークのあるものは見つからない。米国のFCCなどの認証はクリアしている。 総務省の説明によれば、FCC FCC認証OKのものは持ち込んでから90日は使えるとあるが、主に観光で来た外国人の持ち物を想定した話なので、ESP32-CAMをAmazonで買ったとしてそこから90日という風に適用されるのだろうか? また、技適未取得のものでも申請すれば一定期間使えるというルールの運用が始まった?のか、始まるのかわからないが、申請はしないといけない。

参考1 参考2

なので、結局ESP32-CAMが日本で大っぴらに使える状況なのかどうか、いまいち自信が持てないので、買うのを避けた。

じゃあ、ESP32-CAMが使えないとすると、手持ちのESP32に外付けSRAMをつければいいのだが、上述のIPS6404も、ESP-IDF Programming GuideにあるESP-PSRAM32も、ネットで探しても全く入手ルートがなさそうだった。ニーズがあまりないんだろうなあ。と、悩んでいたが、色々調べた結果、ESP32-WROVER-EというモジュールにはPSRAMが載っていて、技適マークも付いていて、すぐに使える開発ボードの形で秋月に売られてる

akizukidenshi.com

ことがわかった。 なので、これを買えばよい。もともと、ESP32-WROVER-Eというものがあることは知っていたのだが、手持ちのESP-WROOM-32で間に合ってしまったので全然気にしていなかった。やっと存在意義を知ることができた。


ArduinoでRAMの残りを求める関数を高速化する

Arudinoで、RAMの残りを求める関数として、以下のサイトにavailableMemoryというものが紹介されている。

https://playground.arduino.cc/Code/AvailableMemory/

これは、mallocの戻り値がNULLでなくなる=確保できるまで1バイトずつサイズを小さくして試すという仕組み。 原理が単純で分かりやすいのだが、ESP-WROOM-32などRAM容量の大きなものに対してそのまま使うと、非常に動作が遅い。また、残り容量次第で計算時間が大きく変化する。

そこで、高速化をしてみた。

mallocの戻り値がNULLでなくなるごとにstepという変数の値を半分に小さくしていく。つまり、バイナリサーチだ。size=1MB、step=128kBであれば、最悪でも40回程度で探索が終わってくれる。運が良ければ16回。

size_t availableMemory() {
  
  size_t size = 1024 * 1024; // 最大サイズ
  size_t step = 128 * 1024; // 探索ステップ
  
  while (true) {
    byte *buf;
    while ((buf = (byte *) malloc(size)) == NULL) {
      size -= step;
    }
    free(buf);
    //Serial.printf("step=%d, size=%d\n", step, size);
    step /= 2;
    if (step == 0) break;
    size += step * 2;
  }

  return size;
}

実際、使ってみると、非常に素早く終了する。デバッグ用に、プログラムの各所に入れても、それほど動作に影響を与えないだろう。