AIXのkshでflockを使って二重起動制御

移植性のあるやり方としてpsやmv,ln,mkdirの結果で二重起動を判断する方法がありますが、Linuxのbash環境の場合はflockを用いたアドバイザリロックで二重起動が実現できます。

プロセスが停止するとkernelがロックを片付けてくれるのでロックの残存を心配する必要がありません。

Linuxのbash環境での方法はこちらがとても参考になりました：https://qiita.com/knaka/items/582289c5b98ca5f55506

これと同じことをAIXのksh環境でやれないか色々試してみました。

# ダメだったやり方

AIXにflock(1)なコマンドは無いので、perlによる方法でそのまま真似すると、openが失敗します。

```sh
#!/bin/ksh
exec 9< "$0"
perl -e "open(LOCK,'<&=9');exit(!flock(LOCK,6))" || exit 0
```

`exec 9< "$0"`とすると、kshプロセスにfd:9の読み取り可能なファイルディスクリプタはできますが、perlのプロセスにコピーされません。存在しないfd:9をopenしようとするため失敗します。

execの代わりに入力リダイレクトすればperlプロセスにfd:9がコピーされるようになりますが、今度はflockが失敗します。

```sh
#!/bin/ksh
{
perl -e "open(LOCK,'<&=9');exit(!flock(LOCK,6))" || exit 0
} 9< "$0"
```

AIXでは書き込みモードで開かれたfdでしか`LOCK_EX`が取れませんでした。



書き込みモードで開けばようやくflockも上手くいきます。が、perlプロセスが終了するとロックが外れてしまいます。

```sh
#!/bin/ksh
{
perl -e "open(LOCK,'>>&=9');exit(!flock(LOCK,6))" || exit 0
} 9>> "$0"
```

このアプローチは万策尽きました。

Linuxのbash環境で実現しているflockを実行したperlプロセスが終了しても、呼び出し元のプロセスが稼働している間はロックが残ってくれるのはなかなか巧妙な仕掛けになっているようです。

あと、書き込みモードでopenしているプロセスがあるスクリプトファイルだと、shbangを経由して起動しようとしたときに「テキストファイルがビジー状態」でエラーになるため気持ち悪いです。



# ダメなりに分かったこと

- ロックを残すにはロックを保持するプロセスを終了してはならない
- 書き込みモードで開いたファイルはflockの`LOCK_EX`を取ることができる
- 「テキストファイルがビジー状態」を避けるために $0 自体にロックを掛けてはいけない



# 孫プロセスにロックを持たせて排他制御

分かったことを踏まえて、可能な方法を考えるとこんな感じになりました。

二重起動をチェックするときはシンプルにif文で判断したいので、ロックを試みた結果はすぐに呼び出し元に制御を戻したい。ただし、ロックを取ったプロセスを終了してしまうとロックが外れてしまうので孫プロセスでロックを取ったままにして結果を子（孫の親）へシグナルで通知して、呼び出し元には戻り値で通知することにします。

孫プロセスはロックを保持する間常駐するので、ロックが不要になったら呼び出し元からkillします。ただし、不意に呼び出し元が終了した場合に孫プロセスが残存してロックが外れなくなってしまうので、呼び出し元のプロセスが存在するのかをポーリングしていなくなっていたら停止するようにします。

本体はperlスクリプトなので別ファイルに分けるのが健全ですが、シェルスクリプト1ファイルだけで実現する方法として考えます。

```sh
# Usage: gclock pid lockfile
#   pid      呼び出し元のPID
#            $$ はサブシェル内で参照するとサブシェルのPIDになってしまうので明示的に受け取る。
#   lockfile flockする対象のファイルパス
#            存在しない場合は空ファイルが作成される。
# 概要
#   孫プロセスでflock(LOCK_EX)を保持します。
# 標準出力
#   孫プロセスのPIDを出力する。
# 戻り値
#   0 ロックを取得した。
#   1 ロックに失敗した。
# 備考
#   孫プロセスをkillするか、pidのプロセスを停止するかlockfileを削除するとロックが解放される。
function gcflock {
    typeset pid="${1}"
    typeset lockfile="${2}"
    typeset -i rc
    perl -e '
        my $ppid = $ARGV[0];
        my $lockfile = $ARGV[1];
        $0 = "gcflock " . $ppid . " " . $lockfile;
        $SIG{USR1} = sub {
            exit(0);
        };
        $SIG{USR2} = sub {
            exit(1);
        };
        $pid = fork();
        if ($pid) {
            print $pid . "\n";
            wait();
        } elsif ($pid == 0) {
            close(STDOUT);
            close(STDERR);
            if (!open(FH, ">>" . $lockfile)) {
                kill "USR2", getppid();
                while (kill(0, getppid())) {
                    sleep(1);
                }
                exit(0);
            }
            if (!flock(FH, 6)) {
                kill "USR2", getppid();
                while (kill(0, getppid())) {
                    sleep(1);
                }
                exit(0);
            }
            kill "USR1", getppid();
            while (kill(0, $ppid)) {
                sleep(1);
            }
            exit(0);
        } else {
            exit(1);
        }
    ' "${pid}" "${lockfile}"; rc="${?}"
    return "${rc}"
}

LOCK_FILE="${0}.lock"
gcpid="$(gcflock "${$}" "${LOCK_FILE}")"; rc="${?}"
if [[ $rc != 0 ]]
then
    echo "二重起動禁止" >&2
    exit 1
fi
# 何かやる
rm -f "${LOCK_FILE}"
kill -9 "${gcpid}"
```

## 良い点

- シェルスクリプトのif文で二重起動制御が実現する

## 悪い点

- ロックを保持する間プロセスが常駐＆呼び出し元をポーリングするのでメモリとCPUに優しくない
- 残存しても問題無いが、書き込み可能なロックファイルが必要
- 実現することの割に長い