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
.
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!")
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?
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.