主进程被杀死时,如何保证子进程同时退出(一)

在Python中,由于全局解释器锁GIL的存在,使得Python中的多线程并不能大大提高程序的运行效率(这里单指CPU密集型),那么在处理CPU密集型计算时,多用多进程模型来处理,而Python标准库中提供了multiprocessing库来支持多进程模型的编程。multiprocessing中提供了的Process类用于开发人员编写创建子进程,接口类似于标准库提供的threading.Thread类,还提供了进程池Pool类,减少进程创建和销毁带来开销,用以提高复用。

在多线程模型中,默认情况下(sub-Thread.daemon=False)主线程会等待子线程退出后再退出,而如果sub-Thread.setDaemon(True)时,主线程不会等待子线程,直接退出,而此时子线程会随着主线程的退出而退出,避免这种情况,主线程中需要对子线程进行join,等待子线程执行完毕后再退出。对应的,在多进程模型中,Process类也有daemon属性,而它表示的含义与Thread.daemon类似,当设置sub-Process.daemon=True时,主进程中需要对子进程进行等待,否则子进程会随着主进程的退出而退出:

import threading
import time
import multiprocessing


def fun(args):
    for i in range(100):
        print args
        time.sleep(1)


if __name__ == '__main__':
    threads = []
    for i in range(4):
        # t = threading.Thread(target=fun, args=(str(i),))
        # t.setDaemon(True)
        t = multiprocessing.Process(target=fun, args=(str(i),))
        t.daemon = True
        t.start()
        threads.append(t)

    for i in threads:
        i.join()

运行上面的代码,主进程会等待子进程执行结束后退出,整个程序结束。line15、16为多线程模式,运行效果和多进程相似。这里说的相似表示的是程序运行正常的情况下,而当有人为的干扰时,例如在进程启动之后,通过kill -9将进程杀死时,情况就不同了,我们知道多线程模型再复杂,也只是在同一个进程中,杀死主进程,所有的线程都会随着主进程的退出而退出,而多进程模型中,每个进程都是独立的,在杀死主进程之后,其他子进程并不会受到影响,还会继续运行,上面的代码中进程的target函数很简单,只进行了有限次数的循环输出,而在真实的场景,子进程可能会始终在loop处理业务,而如果在子进程被杀死后,没有有效回收子进程,需要人工的杀死,这样的话就比较麻烦。注意,在python官方文档中有声明,对于deamon=True的子进程:When a process exits, it attempts to terminate all of its daemonic child processes,这里的exits表示的是进程正常结束,而如果父进程在运行中非正常退出,比如前面提到的kill -9命令直接杀死,它并没有机会去回收子进程。

对于这种情况,首先想到的是用信号signal来处理,这样一来,在杀死主进程时就不能再用kill -9命令了,因为kill -9命令表示向进程发送SIGKILL命令,而在系统中,SIGKILL和SIGSTOP两种信号,进程是无法捕获的,收到后会立即退出。在linux下执行kill -l,可以看到全部的信号量,这里使用SIGTERM信号,SIGTERM表示终止信号,是kill命令传送的系统默认信号,它与SIGKIIL的区别是,SIGTERM更为友好,进程能捕捉SIGTERM信号,进而根据需要来做一些清理工作,明确了这点之后,对上面的代码进行一些修改:

processes = []
def fun(x):
    print 'current sub-process pid is %s' % os.getpid()
    while True:
        print 'args is %s ' % x
        time.sleep(1)


def term(sig_num, addtion):
    print 'terminate process %d' % os.getpid()
    try:
        print 'the processes is %s' % processes
        for p in processes:
            print 'process %d terminate' % p.pid
            p.terminate()
            # os.kill(p.pid, signal.SIGKILL)
    except Exception as e:
        print str(e)


if __name__ == '__main__':
    print 'current pid is %s' % os.getpid()
    for i in range(3):
        t = Process(target=fun, args=(str(i),))
        t.daemon = True
        t.start()
        processes.append(t)
    signal.signal(signal.SIGTERM, term)
    try:
        for p in processes:
            p.join()
    except Exception as e:
        print str(e)

运行上面的代码,输出主进程id,然后通过kill -15 pid向主进程发送SIGTERM信号,主进程退出之前,会将子进程也terminate掉。但是退出时,line32会捕获到异常信息OSError: [Errno 4] Interrupted system call,表示主进程在对子进程进行join时,被信号中断并退出。程序得到了预期的结果,在向主进程发送SIGTERM信号时,首先结束所有子进程,之后主进程退出。
接着使用kill -15加上子进程的进程id,向子进程发送SIGTERM信号,看看子进程是否能够得到同样的效果,然而在向进程发送信号之后,并未进入term函数,通过ps可以看到,子进程收到SIGTERM信号之后,自行退出,而主进程和其他子进程没有受到影响,依然正常运行,这里并没有得到相同的效果。我们知道,子进程会继承父进程的信号处理机制,但是这里子进程在收到SIGTERM信号后,没有运行term函数,仔细观察上面的代码示例,注意到在注册信号处理函数时,子进程已经启动,所以这里子进程并没有注册信号处理函数,接着,我们对主进程进行修改,确保子进程启动前,进行信号处理函数的注册:

if __name__ == '__main__':
    signal.signal(signal.SIGTERM, term)
    print 'current main-process pid is %s' % os.getpid()
    for i in range(3):
        t = Process(target=fun, args=(str(i),))
        t.daemon = True
        t.start()
        processes.append(t)

    try:
        for p in processes:
            p.join()
    except Exception as e:
        print str(e)

再次运行程序,通过kill -15向父进程发送SIGTERM信号,这时进程收到了信号,但是程序仍然继续运行,观察下面的输出信息,主进程收到信号后,执行了term函数,并且通过调用子进程的p.terminate(),注意在linux系统下terminate的实现方式有: Terminate the process. On Unix this is done using the SIGTERM signal,也就是说,当对子进程调用p.terminate()时,实际上仍是向子进程发送SIGTERM信号,之前我们已经将信号处理函数的注册放置子进程启动前,使得子进程也能够执行信号处理函数。从输出的processes信息可以看到,由于启动顺序,全局的processes变量并没有对子进程信息进行很好地共享。在收到由p.terminate()发送的信号量之后,子进程执行term函数,会再次通过调用p.terminate()来试图杀死子进程,这样就会进入一个无限的循环,kill -15向子进程发送SIGTERM信号,会得到相同的结果。


至此,基本了解了如何向主进程发送信号量来结束主进程和其子进程的方法,那么有没有什么方式可以通过向子进程发送信号,取得同样的效果呢?答案是肯定的,当我们在主进程中创建子进程时,主进程与其创建的子进程隶属于同一个分组里,这个分组的概念在linux中成为进程组,它是一个或多个进程的组成的集合,同一个进程组中的进程,它们的进程组ID是一致的。
利用python标准库中os.getpgid方法,通过进程的ID来获取进程对应的组ID,接着调用os.killpg方法,向进程的组ID发送信号,现在对上面的代码进行简单修改:

def fun(x):
    print 'current pid is %s, group id is %s' % (os.getpid(), os.getpgrp())
    while True:
        print 'args is %s ' % x
        time.sleep(1)


def term(sig_num, addtion):
    print 'current pid is %s, group id is %s' % (os.getpid(), os.getpgrp())
    os.killpg(os.getpgid(os.getpid()), signal.SIGKILL)


if __name__ == '__main__':
    signal.signal(signal.SIGTERM, term)
    print 'current pid is %s' % os.getpid()
    for i in range(3):
        t = Process(target=fun, args=(str(i),))
        t.daemon = True
        t.start()
        processes.append(t)

    try:
        for p in processes:
            p.join()
    except Exception as e:
        print str(e)

注意在代码中,为了防止之前出现的无限循环,在term函数中,我们通过os.killpg,直接向进程组发送SIGKILL信号。运行代码,通过输出我们可以看出,进程组中,主进程和子进程的进程组id相同,都是主进程的pid。通过kill -15向主进程或者子进程发送SIGTERM信号时,都会将进程组主进程和子进程全部杀死

总结:
进程意外退出时,如何将主进程创建的子进程终止,有两种做法。

  • 一种是将是将主进程中创建的子进程信息保存,使用信号处理机制,在主进程收到终止信号SIGTERM时,保存的子进程信息terminate,之后主进程退出;
  • 另一种是更加直接,通过进程组id将整个进程组中的进程杀死。

这两种方式主要的差别是,第一种方式是通过向主进程发送SIGTERM请求来杀死主进程和其子进程的,而第二种方式主进程和其子进程都可以收到SIGTERM,将进程组中的所有进程杀死。

你可能感兴趣的:(主进程被杀死时,如何保证子进程同时退出(一))