2

I have a multiprocessing.Process subclass that ignores SIGINT:

# inside the run method
signal.signal(signal.SIGINT, signal.SIG_IGN)

I don't want this process to terminate when pressing CTRL + C, so I'm trying to simulate this terminal event in my unittests by sending a SIGINT signal to this process ID:

os.kill(PID, signal.SIGINT)

But even without ignoring this signal the process is not terminating, so this test is useless, I found out from other questions that on a CTRL + C event the terminal sends SIGINT to the process group ID, but I can't do this in my case because it will also terminate the unittest process.

So why the process doesn't terminate when it receives a SIGINT from os.kill ? and should I be doing this in another way ?

Pierre
  • 12,468
  • 6
  • 44
  • 63
  • 1
    Can you paste an example of code? I tried to do it naively but it works as expected: https://bpaste.net/show/f2c5fc34b8b6 – Armin Rigo Aug 15 '15 at 11:25
  • Sorry friend i founf now : http://stackoverflow.com/questions/13341870/signals-and-interrupts-a-comparison – dsgdfg Aug 15 '15 at 11:47
  • @ArminRigo thank you very much, the sleep before and after sending the signal is solving the problem, it seems that it's being done very fast, can you please post it as an answer so I can accept it. – Pierre Aug 15 '15 at 12:14
  • @Pierre: can you confirm that the behavior is that the child is _not terminated_ by SIGINT in both cases, i.e. whether or not it is ignoring SIGINT? I find the opposite: child _is_ terminated in both cases. The timing issue that you mentioned seems to confirm my experience because the signal is received by the child _before_ it has ignored the signal. Adding a small delay in the parent fixes it. – mhawke Aug 15 '15 at 12:59
  • @mhawke yes the child wasn't terminated by SIGINT in both cases, I also tried setting a signal handler to check if the signal is being received by the child and it didn't fire at all, the sleep before and after sending the signal solved this issue. – Pierre Aug 15 '15 at 13:18
  • How did the sleep solve the issue? That doesn't make sense. Is your parent process already ignoring SIGINT before starting the child? If it is, then the child will inherit the same signal disposition and will also ignore SIGINT. However, adding a sleep would not affect that. What OS are you using? Which Python version? – mhawke Aug 15 '15 at 13:56
  • @Pierre: I have added an answer that demonstrates the problem and illustrates fixes for it. I'd be interested to know whether you get the same results with my demo. – mhawke Aug 15 '15 at 14:00
  • @mhawke thank you very much for posting a detailed answer, my code is using `Celery` which adds signal handlers, but I checked in my code the installed handler for SIGINT and it gave me ``, so sending a SIGINT didn't terminate the process, but adding 0.1 delay before and after sending the signal did the job, also I tried setting a custom handler and it didn't execute without these delays. – Pierre Aug 15 '15 at 15:52

2 Answers2

3

The child process should terminate on receipt of SIGINT, unless it is ignoring that signal or has its own handler installed. If you are not explicitly ignoring SIGINT in the child, then it is possible that SIGINT is being ignored in the parent, and therefore in the child, because the signal disposition is inherited.

However, I have not been able to replicate your issue, in fact, I find the opposite problem: the child process terminates regardless of its signal disposition.

If the signal is sent too soon, before the child process has ignored SIGINT (in its run() method), it will be terminated. Here is some code that demonstrates the problem:

import os, time, signal
from multiprocessing import Process

class P(Process):
    def run(self):
        signal.signal(signal.SIGINT, signal.SIG_IGN)
        return super(P, self).run()

def f():
    print 'Child sleeping...'
    time.sleep(10)
    print 'Child done'

p = P(target=f)
p.start()    
print 'Child started with PID', p.pid

print 'Killing child'
os.kill(p.pid, signal.SIGINT)
print 'Joining child'
p.join()

Output

Child started with PID 1515
Killing child
Joining child
Traceback (most recent call last):
  File "p1.py", line 15, in 
    p.start()    
  File "/usr/lib64/python2.7/multiprocessing/process.py", line 130, in start
    self._popen = Popen(self)
  File "/usr/lib64/python2.7/multiprocessing/forking.py", line 126, in __init__
    code = process_obj._bootstrap()
  File "/usr/lib64/python2.7/multiprocessing/process.py", line 242, in _bootstrap
    from . import util
KeyboardInterrupt

Adding a small delay with time.sleep(0.1) in the parent just before sending the SIGINT signal to the child will fix the problem. This will give the child enough time to execute the run() method in which SIGINT is ignored. Now the signal will be ignored by the child:

Child started with PID 1589
Killing child
Child sleeping...
Joining child
Child done

An alternative that requires no delays nor custom run() method is to set the parent to ignore SIGINT, start the child, then restore the parent's original SIGINT handler. Because the signal disposition is inherited, the child will ignore SIGINT from the moment it starts:

import os, time, signal
from multiprocessing import Process

def f():
    print 'Child sleeping...'
    time.sleep(10)
    print 'Child done'

p = Process(target=f)
old_sigint = signal.signal(signal.SIGINT, signal.SIG_IGN)
p.start()    
signal.signal(signal.SIGINT, old_sigint)    # restore parent's handler
print 'Child started with PID', p.pid

print 'Killing child'
os.kill(p.pid, signal.SIGINT)
print 'Joining child'
p.join()

Output

Child started with PID 1660
Killing child
Joining child
Child sleeping...
Child done
mhawke
  • 84,695
  • 9
  • 117
  • 138
3

A simplified version of the issue is:

import os, time, signal

childpid = os.fork()

if childpid == 0:
    # in the child
    time.sleep(5)      # will be interrupted by KeyboardInterrupt
    print "stop child"
else:
    # in the parent
    #time.sleep(1)
    os.kill(childpid, signal.SIGINT)

If the parent does sleep(1) before sending the signal, everything works as expected: the child (and only the child) receives a Python KeyboardInterrupt exception, which interrupts the sleep(5). However, if we comment out sleep(1) as in the example above, the kill() appears to be completely ignored: the child runs, sleeps 5 seconds, and finally prints "stop child". So a simple workaround is possible for your test suite: simply add a small sleep().

As far as I understand it, this occurs for the following (bad) reason: looking at the CPython source code, after the system call fork(), the child process explicitly clears the list of pending signals. But the following situation seems to occur often: the parent continues slightly ahead of the child, and sends the SIGINT signal. The child receives it, but at that point it is still only shortly after the system call fork(), and before the _clear_pending_signals(). As a result, the signal is lost.

This could be regarded as a CPython bug, if you feel like filing an issue on http://bugs.python.org . See PyOS_AfterFork() in signalmodule.c.

Armin Rigo
  • 12,048
  • 37
  • 48