Search code examples
pythonsignals

The difference of placing signal.signal before or after child process is created and started?


I'm encountering an odd problem when using the signal module to manage process behavior.

I want to send a SIGTERM signal from b.py after a.py is running to terminate the main and child processes in a.py.

Now I find when signal.signal(signal.SIGTERM, handle_signal) is placed before __main__ entrypoint then the child processes are not terminated as expected. They still run.

But if I placed signal.signal(signal.SIGTERM, handle_signal) after that child process starts then the child process can be terminated as expected when a.py receives the SIGTERM signal from b.py.

a.py
import multiprocessing
import os
import signal
import time


def process1():
    while True:
        print("the child process is running")
        time.sleep(1)


def handle_signal(signum, frame):
    print("signal received:", signum, "pid:", os.getpid())


# register signal
signal.signal(signal.SIGTERM, handle_signal)  # place here 1
if "__main__" == __name__:
    a = multiprocessing.Process(target=process1)
    a.daemon = True
    a.start()

    # signal.signal(signal.SIGTERM, handle_signal)  # place here 2

    child_pid = a.pid
    parent_pid = os.getpid()

    parent_pid_group = os.getpgid(parent_pid)

    with open("./crawler.pid", "w") as f:
        f.write(str(parent_pid_group))

    a.join()

    print("Parent id:", parent_pid)
    print("Child id", child_pid)
    print("all terminated!")
b.py
import os
import signal

with open("./crawler.pid", "r") as f:
    try:
        pid = int(f.read())
        os.killpg(pid, signal.SIGTERM)
        print("Signal sent successfully to process", pid)
    except Exception as e:
        print("Error sending signal:", e)

The wrong output - signal is registered before the child process starts:

╰─ python a.py
the child process is running
the child process is running
the child process is running
the child process is running
the child process is running
signal received: 15 pid: 1379
signal received: 15 pid: 1380
the child process is running
the child process is running
the child process is running
the child process is running
^CTraceback (most recent call last):
  File "a.py", line 34, in <module>
    a.join()
  File "/usr/lib/python3.8/multiprocessing/process.py", line 149, in join
    res = self._popen.wait(timeout)
  File "/usr/lib/python3.8/multiprocessing/popen_fork.py", line 47, in wait
    return self.poll(os.WNOHANG if timeout == 0.0 else 0)
  File "/usr/lib/python3.8/multiprocessing/popen_fork.py", line 27, in poll
    pid, sts = os.waitpid(self.pid, flag)
KeyboardInterrupt
signal received: 15 pid: 1380
Process Process-1:
Traceback (most recent call last):
  File "/usr/lib/python3.8/multiprocessing/process.py", line 315, in _bootstrap
    self.run()
  File "/usr/lib/python3.8/multiprocessing/process.py", line 108, in run
    self._target(*self._args, **self._kwargs)
  File "a.py", line 10, in process1
    time.sleep(1)
KeyboardInterrupt

----------------------
╰─ python b.py 
Signal sent successfully to process 1379

And the right output - signal is registered after the child process starts:

╰─ python a.py
the child process is running
the child process is running
the child process is running
signal received: 15 pid: 1961
Parent id: 1961
Child id 1962
all terminated!

---------------------
╰─ python b.py 
Signal sent successfully to process 1961

Is this a python mechanism or an operating system design?


Solution

  • Is this a python mechanism or an operating system design?

    It is OS design. The same would happen in a similar C program, for instance.

    In each case, the child multiprocessing.Process inherits its parent's signal disposition at the time it is forked.

    In the first case, that disposition is your chatty signal handler. That's why both child and parent report receiving signal 15. Each runs the handler when the SIGTERM is delivered, and then resumes its loop (child) or its blocking join() (parent).

    In the second case, that disposition is the default, SIG_DFL. The parent installs the chatty handler after the child is forked, and the child is TERMinated by the signal sent to the process group while the parent merely reports it.