Python: как убить дочерний процесс (ы), когда родитель умирает?

Детский процесс начинается с

subprocess.Popen(arg)

Есть ли способ гарантировать, что он будет убит, если родительский анонимно закончит? Мне нужно, чтобы это работало как на Windows, так и на Linux. Я знаю об этом решении для Linux.

Edit:

требование запуска дочернего процесса с помощью subprocess.Popen(arg) может быть ослаблено, если решение существует с использованием другого метода запуска процесса.

Ответ 1

Хе-хе, я просто сам это выяснил вчера! Предполагая, что вы не можете изменить дочернюю программу:

В Linux prctl(PR_SET_PDEATHSIG, ...), вероятно, является единственным надежным выбором. (Если абсолютно необходимо, чтобы дочерний процесс был убит, вам может потребоваться установить сигнал смерти SIGKILL вместо SIGTERM, связанный с ним код использует SIGTERM, но у ребенка есть возможность игнорировать SIGTERM, если он захочет. )

В Windows наиболее надежными параметрами являются объект Job. Идея заключается в том, что вы создаете "Job" (своего рода контейнер для процессов), затем вы помещаете дочерний процесс в Job, и вы устанавливаете волшебную опцию, которая гласит: "когда никто не держит" дескриптор "для этого задания, а затем уничтожить процессы, которые в нем". По умолчанию единственным "дескриптором" задания является тот, который выполняется вашим родительским процессом, и когда родительский процесс умирает, ОС будет проходить и закрывать все свои дескрипторы, а затем замечает, что это означает, что нет открытых дескрипторов для работа. Поэтому он убивает ребенка, как просили. (Если у вас несколько дочерних процессов, вы можете назначить их всем на одно и то же задание.) В этом ответе есть пример кода для этого, используя модуль win32api. Этот код использует CreateProcess для запуска дочернего элемента вместо subprocess.Popen. Причина в том, что им нужно получить "дескриптор процесса" для порожденного ребенка, а CreateProcess возвращает это по умолчанию. Если вы предпочитаете использовать subprocess.Popen, то здесь (непроверенная) копия кода из этого ответа использует subprocess.Popen и OpenProcess вместо CreateProcess:

import subprocess
import win32api
import win32con
import win32job

hJob = win32job.CreateJobObject(None, "")
extended_info = win32job.QueryInformationJobObject(hJob, win32job.JobObjectExtendedLimitInformation)
extended_info['BasicLimitInformation']['LimitFlags'] = win32job.JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE
win32job.SetInformationJobObject(hJob, win32job.JobObjectExtendedLimitInformation, extended_info)

child = subprocess.Popen(...)
# Convert process id to process handle:
perms = win32con.PROCESS_TERMINATE | win32con.PROCESS_SET_QUOTA
hProcess = win32api.OpenProcess(perms, False, child.pid)

win32job.AssignProcessToJobObject(hJob, hProcess)

Технически, здесь крошечное условие гонки, если ребенок умирает между вызовами Popen и OpenProcess, вы можете решить, хотите ли вы об этом беспокоиться.

Один недостаток использования объекта задания заключается в том, что при работе в Vista или Win7, если ваша программа запущена из оболочки Windows (то есть, щелкнув значок), тогда, вероятно, будет уже назначается объект задания, и попытка создания нового объекта задания завершится с ошибкой. Win8 исправляет это (разрешая вложение объектов задания), или если ваша программа запускается из командной строки, тогда это должно быть хорошо.

Если вы можете изменить ребенка (например, при использовании multiprocessing), то, вероятно, лучшим вариантом является каким-либо образом передать родительский PID дочернему элементу (например, в качестве аргумента командной строки или в аргументе args= до multiprocessing.Process), а затем:

В POSIX: создайте поток в дочернем элементе, который иногда вызывает os.getppid(), и если возвращаемое значение когда-либо перестает сопоставлять pid, переданное из родителя, тогда вызовите os._exit(). (Этот подход переносим для всех Unix-ов, включая OS X, а трюк prctl - для Linux.)

В Windows: создайте поток в дочернем элементе, который использует OpenProcess и os.waitpid. Пример использования ctypes:

from ctypes import WinDLL, WinError
from ctypes.wintypes import DWORD, BOOL, HANDLE
# Magic value from http://msdn.microsoft.com/en-us/library/ms684880.aspx
SYNCHRONIZE = 0x00100000
kernel32 = WinDLL("kernel32.dll")
kernel32.OpenProcess.argtypes = (DWORD, BOOL, DWORD)
kernel32.OpenProcess.restype = HANDLE
parent_handle = kernel32.OpenProcess(SYNCHRONIZE, False, parent_pid)
# Block until parent exits
os.waitpid(parent_handle, 0)
os._exit(0)

Это позволяет избежать любой из возможных проблем с объектами задания, о которых я говорил.

Если вы хотите быть действительно, действительно уверены, тогда вы можете объединить все эти решения.

Надеюсь, что это поможет!

Ответ 2

Объект Popen предлагает методы завершения и уничтожения.

https://docs.python.org/2/library/subprocess.html#subprocess.Popen.terminate

Они отправляют сигналы SIGTERM и SIGKILL для вас. Вы можете сделать что-то похожее на нижеследующее:

from subprocess import Popen

p = None
try:
    p = Popen(arg)
    # some code here
except Exception as ex:
    print 'Parent program has exited with the below error:\n{0}'.format(ex)
    if p:
        p.terminate()

UPDATE:

Вы правы - вышеуказанный код не защитит вас от жесткого сбоя или кого-то, убивающего ваш процесс. В этом случае вы можете попробовать обернуть дочерний процесс в классе и использовать модель опроса для просмотра родительского процесса. Знать, что psutil является нестандартным.

import os
import psutil

from multiprocessing import Process
from time import sleep


class MyProcessAbstraction(object):
    def __init__(self, parent_pid, command):
        """
        @type parent_pid: int
        @type command: str
        """
        self._child = None
        self._cmd = command
        self._parent = psutil.Process(pid=parent_pid)

    def run_child(self):
        """
        Start a child process by running self._cmd. 
        Wait until the parent process (self._parent) has died, then kill the 
        child.
        """
        print '---- Running command: "%s" ----' % self._cmd
        self._child = psutil.Popen(self._cmd)
        try:
            while self._parent.status == psutil.STATUS_RUNNING:
                sleep(1)
        except psutil.NoSuchProcess:
            pass
        finally:
            print '---- Terminating child PID %s ----' % self._child.pid
            self._child.terminate()


if __name__ == "__main__":
    parent = os.getpid()
    child = MyProcessAbstraction(parent, 'ping -t localhost')
    child_proc = Process(target=child.run_child)
    child_proc.daemon = True
    child_proc.start()

    print '---- Try killing PID: %s ----' % parent
    while True:
        sleep(1)

В этом примере я запускаю "ping -t localhost" b/c, который будет работать вечно. Если вы убьете родительский процесс, дочерний процесс (команда ping) также будет убит.