別サーバからの処理の中断の際にゾンビプロセスを誕生させて大苦戦した話（Socket通信の話も少し）

# プロセスIDとは
OSが現在実行中のプロセスを識別するために割り当てられる一意な識別子のことです．WindowsやUNIX系OSなどで利用される仕組みで，一般的には整数の通し番号が与えられます．

# 親と子の関係
プロセスには，親と子の関係があります．特定の処理を中断したい場合は，この関係が非常に重要になってきます．今回，私もこの親と子の関係で非常に悩みました．中断したい処理が全然中断されずに路頭に迷ってました．
最初，私は安直な気持ちで「**親プロセスを指定して中断すれば子プロセスも中断されるだろう**」と思っていました．しかし，これが大きな間違いでした．この安直な考えが私を大苦戦へと導いた**ゾンビプロセス**を誕生させてしまいました．

# 恐怖のゾンビプロセス
プロセスが終了して既にプログラムの実体は消滅したのに、プロセステーブルには存在が残ってしまっている状態のことを言います．
下記のコマンドでプロセスIDの詳細を表示して「defunct」と表示されていたら，私と同じく恐怖のゾンビプロセスを誕生させてしまっている合図です．こうなっていると，思った通りの処理になりません．
「defunct = ゾンビプロセスの誕生」という認識で大丈夫です．
```$ ps aux | grep ＜プロセスID＞```

おまたせしました．ここからは，本題である私がどうやって解決したかを説明します．
先に結論から言うと・・・
**・子プロセスを中断してから親プロセスを中断する（調べた感じ）**
**・親と子の両方を一気に中断する（実体験）**
の2つの方法になります．1つ目の方法は，私はうまくいきませんでした．
そのため，今回は2つ目の方法について詳しく説明していきます．

# 今回の実験環境
今回の私の実験環境は，下の図のようになっています．
![qiita.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2851869/a22201fa-bab8-e9c0-a581-3dd7a2253c6e.png)

流れを説明します．
1．2つのVMでtest.pyを実行する
　・Socket通信で親プロセスID，子プロセスID，比較に使用する値の3つを送信する
　・subprocessでrsyncを実行して，動画ファイルを送信する

2．送り先のVMのsocket.pyで値を比較する
　・Socket通信で送られてきた値を比較する

3．値が小さい方のファイル送信を中断する
　・IPアドレスと親プロセスID，子プロセスIDを使用して中断する


# rsyncの処理を別サーバで中断
ここでは，具体的なコードを説明します．
・test.pyとsocket.pyで共通する部分
``` 
    import subprocess
    import socket
```

・**test.py**
　・ファイルの送信部分
```
remote_host = "送り先のユーザ名＠送り先のIPアドレス"
remote_path = "送り先のディレクトリ" 
rsync_command = f"rsync -av --progress {selected_send_file_path} {remote_host}:{remote_path}/"

rsync_process = subprocess.Popen(rsync_command, shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True, bufsize=1)
```

　・親プロセスIDと子プロセスIDの取得部分
```
import os

# 子プロセスIDを取得
rsync_process_id = rsync_process.pid

# 親プロセスIDを取得
rsync_ppid = os.getppid()　
```

　・Socket通信での送信部分
```
# クライアントソケットを作成
client_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)

# サーバーのホストとポートを指定
server_host = '192.168.100.200'  # サーバーのIPアドレスを指定
server_port = 1234  # サーバーのポート番号を指定

# 接続
client_socket.connect((server_host, server_port))

# 送信する要素
data_to_send = {
    'rsync_parent_id': rsync_parent_id #　親プロセスID
    'rsync_process_id': rsync_process_id　#　子プロセスID
    'ans': ans　#　比較のための値
}

# サーバに送信
client_socket.send(str(data_to_send).encode())
```

・**socket.py**
　・値の比較
```
host = "192.168.100.200"
port = 1234

data_lock = threading.Lock()
received_data_and_process = {}

#　送られてきた値を取得
received_duration = received_data_dict.get('duration')

# 比較
if received_duration is not None:
    with data_lock:
        if received_duration > max_value:
                max_value = received_duration
                max_client_addr = addr

    with data_lock:
        received_data_and_process[addr] = received_data_dict
```

　・値が小さい方の送信を中断
　＜ポイント＞
　　**・pkill -Pで親プロセスIDと子プロセスIDを同時に中断できる**
　　**・-STOPで一時中断できる**

```
for client_addr, data in received_data_and_process.items():
    if client_addr != max_client_addr:

        # 送られてきた親プロセスIDと子プロセスIDを取得
        rsync_parent_id = data.get('rsync_parent_id')
        rsync_process_id = data.get('rsync_process_id')

        if rsync_parent_id:
            remote_server = f"mikami@{client_addr[0]}"  # リモートサーバのユーザー名とホスト名を指定
            remote_command = f"ssh {remote_server} pkill -STOP -P {rsync_parent_id}"

            try:
                subprocess.run(remote_command, shell=True, check=True)
                print(f"親：Stopped process for {client_addr[0]} on remote server.")

            except subprocess.CalledProcessError as e:
                print(f"Error stopping process on remote server: {e}")

        if rsync_process_id:
            remote_server = f"mikami@{client_addr[0]}"  # リモートサーバのユーザー名とホスト名を指定
            remote_command = f"ssh {remote_server} pkill -STOP -P {rsync_process_id}"

            try:
                subprocess.run(remote_command, shell=True, check=True)
                print(f"子：Stopped process for {client_addr[0]} on remote server.")

            except subprocess.CalledProcessError as e:
                print(f"Error stopping process on remote server: {e}")
```

　・監視スタートと接続されたIPアドレスを取得
```
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as server_socket:
    server_socket.bind((host, port))
    server_socket.listen()
    print(f"Listening on {host}:{port}...")

    while True:
        conn, addr = server_socket.accept()
        print(f"Connection from {addr}")

        max_client_addr = None  

        client_thread = threading.Thread(target=handle_client, args=(conn, addr))
        client_thread.start()
```

# pkillコマンド感謝
今まで，中断や中止はkillコマンドしか知らなかったのでずっとkillコマンドを使ってました．ただ，ずっと思った通りにいかず．．．
そんな時にpkillコマンドを知りました．pkillコマンドを使用した瞬間に全てが思った通りの挙動になりました．
実際の中断された方の結果が以下の画像になります．
![スクリーンショット 2023-10-16 18.30.29.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2851869/13a92d4a-e8e3-0d81-8626-47a4f908e81d.png)
![スクリーンショット 2023-10-16 18.32.07.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2851869/a25dc242-10eb-053f-9580-118c279dc91f.png)
上記の2つの画像からもわかる通り，しっかりと中断できています．（tokyo_ninja250sl.movのファイルサイズが一緒）
つまり・・・
書き込みが中断されている！！
1枚目でも中断されていることが目に見えて分かリます．

# 最後に
今回，この問題を解決するために丸3日かかりました．この記事を読んでいただいた皆さんには，同じような問題が起きた時に時間をかけずに解決できる参考になればと思っています．
今回は，送信中断にしましたが他にも別サーバからの処理はたくさんあります．そんな時でも，プロセスIDが関わっていればこの記事は参考になると思います．

この記事で伝えたいことは1つ
**親プロセスと子プロセスがあって，子プロセスの処理を中断したい場合はpkillコマンドで同時に中断するべし！！**