如何将主进程创建的子进程终止,避免形成孤儿进程

一,linux中,kill -9和kill -15的区别

1,kill -l(查看Linux/Unix的信号变量)

用来查看kill命令中可以带哪些 “信号编号”

在这里插入图片描述

2,常使用的kill -9、kill -15的区别
1)kill -15

系统会发送一个SIGTERM的信号给对应的程序。当程序接收到该signal后,将会发生以下的事情:

  • 程序立刻停止
  • 当程序释放相应资源后再停止
  • 程序可能仍然继续运行

大部分程序接收到SIGTERM信号后,会先释放自己的资源,然后在停止。但是也有程序可以在接受到信号量后,做一些其他的事情,并且这些事情是可以配置的。如果程序正在等待IO,可能就不会立马做出相应。也就是说,SIGTERM多半是会被阻塞的、忽略。

2)kill -9

你不是可以不响应 SIGTERM吗?那好,我给你下一道必杀令,我看你还不乖乖的。多半admin会用这个命令不过,也不是所有的程序都会乖乖听话,总有那些状态下的程序无法立刻相应。

二,在多进程中杀掉父进程可能出现的问题

        在Python中,由于全局解释器锁GIL的存在,使得Python中的多线程并不能大大提高程序的运行效率,那么在处理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()

        运行上面的代码,主进程会等待子进程执行结束后退出,整个程序结束。 而当有人为的干扰时,例如在进程启动之后,通过kill -9将进程杀死时,情况就不同了,我们知道多线程模型再复杂,也只是在同一个进程中,杀死主进程,所有的线程都会随着主进程的退出而退出,而多进程模型中,每个进程都是独立的,在杀死主进程之后,其他子进程并不会受到影响,还会继续运行。如果在父进程被杀死后,没有有效回收子进程,这样的话就比较麻烦,需要人工的杀死。
  对于这种情况,首先想到的是用信号signal来处理,这样一来,在杀死主进程时就不能再用kill -9命令了,因为kill -9命令表示向进程发送SIGKILL命令,而 在系统中,SIGKILL(kill -9)和SIGSTOP两种信号,进程是无法捕获的,收到后会立即退出。 在linux下执行kill -l,可以看到全部的信号量,这里使用SIGTERM信号(kill -15),SIGTERM表示终止信号,是kill命令传送的系统默认信号,它与SIGKIIL的区别是,SIGTERM更为友好,进程能捕捉SIGTERM信号,进而根据需要来做一些清理工作。

三,如何将主进程创建的子进程终止,避免形成孤儿进程,两种做法

1,通过进程组id将整个进程组中的进程杀死。

当我们在主进程中创建子进程时,主进程与其创建的子进程隶属于同一个分组里,这个分组的概念在linux中成为进程组,它是一个或多个进程的组成的集合,同一个进程组中的进程,它们的进程组ID是一致的。利用python标准库中os.getpgid方法,通过进程的ID来获取进程对应的组ID,接着调用os.killpg方法,向进程的组ID发送信号。

1 def fun(x):
 2     print 'current pid is %s, group id is %s' % (os.getpid(), os.getpgrp())
 3     while True:
 4         print 'args is %s ' % x
 5         time.sleep(1)
 6 
 7 
 8 def term(sig_num, addtion):
 9     print 'current pid is %s, group id is %s' % (os.getpid(), os.getpgrp())
10     os.killpg(os.getpgid(os.getpid()), signal.SIGKILL)
11 
12 
13 if __name__ == '__main__':
14     signal.signal(signal.SIGTERM, term)
15     print 'current pid is %s' % os.getpid()
16     for i in range(3):
17         t = Process(target=fun, args=(str(i),))
18         t.daemon = True
19         t.start()
20         processes.append(t)
21     
22     try:
23         for p in processes:
24             p.join() 
25     except Exception as e:
26         print str(e)

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

2,使用信号处理机制,在主进程收到终止信号SIGTERM时,保存的子进程信息terminate,之后主进程退出
(1)示例:

能够将processes通过函数调用,传递给回调函数,避免使用全局变量。python标准库functools向我们提供了partial偏函数,它的用途是让一些参数在函数被调用之前提前获知其值,位置参数和关键字参数均可应用,我们来看个例子:

from functools import partial
def add(a, b):
    return a + b

add_with_hundred = partial(add, 100)
result = add_with_hundred(10)
print result
110

代码示例中,partial(add, 100)返回一个partial对象,参数add表示要封装的方法,参数100表示位置参数,它表示的位置是add方法中第一个参数,相当于对add方法的第一个参数添加了默认值100,对于返回的add_with_hundred对象,它的第一个参数默认已经是100,那么在使用时只需要传入一个参数即可。再来看一个关键字参数的例子:

from functools import partial
basetwo = partial(int, base=2)
result = basetwo('101')
print result
5
(2):使用partial偏函数,并通过返回partial对象实现传递参数
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(t_processes, sig_num, frame):
    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()
    except Exception as e:
        print str(e)


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

    # handler使用partital处理,用local processes对term方法的第一个参数进行绑定
    handler = functools.partial(term, processes)
    signal.signal(signal.SIGTERM, handler)
    try:
        for p in processes:
            p.join()
    except Exception as e:
        print str(e)

四,使用进程池multiprocessing.Pool时,如何保证主进程意外退出,进程池中的worker进程同时退出,不产生孤儿进程

此处介绍两种方法

1,主进程中使用进程池,kill -15 杀掉进程群组。

在新建worker进程时,默认启动方式为daemon,这种情况下worker进程作为主进程的子进程,会随着主进程的退出而退出。查看源码如下:

def _repopulate_pool(self):
    """Bring the number of pool processes up to the specified number,
    for use after reaping workers which have exited.
    """
    for i in range(self._processes - len(self._pool)):
        w = self.Process(target=worker,
                         args=(self._inqueue, self._outqueue,
                               self._initializer,
                               self._initargs, self._maxtasksperchild,
                               self._wrap_exception)
                        )
        self._pool.append(w)
        w.name = w.name.replace('Process', 'PoolWorker')
        w.daemon = True
        w.start()
        util.debug('added worker')

代码实现如下:

import time
import os
import signal
from multiprocessing import Pool
  
def fun(x):
     print ('current sub-process pid is %s' % os.getpid())
     while True:
         print(x)
         time.sleep(2)

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__':
    print ('current pid is %s' % os.getpid())
    mul_pool = Pool()
    signal.signal(signal.SIGTERM, term)
    
    for i in range(3):
        mul_pool.apply_async(func=fun, args=(i,))

    mul_pool.close()
    mul_pool.join()

这么做是有些武断,如果一些worker进程在运行一些重要的业务逻辑,强制结束可能会使得数据的丢失,或者一些其他难以恢复的后果,那么有没有更合理的处理方式,使worker进程在处理完本轮数据后,再退出呢?答案同样是肯定的

2,主进程中使用进程池,用Event来控制worker进程的退出

python标准库中提供了一些进程间同步的工具,这里我们使用Event对象来做同步。首先我们需要通过multiprocessing.Manager类来获取一个Event对象,用Event来控制worker进程的退出

import time
import os
import signal
import functools
from multiprocessing import Pool
from multiprocessing import Manager

def fun(x,event):
    while True:
        print("%s进程%s开始运行..."%(str(x),str(os.getpid())))
        time.sleep(1)
        print("%s进程%s运行中..." % (str(x), str(os.getpid())))
        time.sleep(1)
        print("%s进程%s运行OK!..." % (str(x), str(os.getpid())))
        time.sleep(1)
        if event.is_set():
            break

def term(pool,event,manager,sig_num, addtion):
    print ('current pid is %s, group id is %s' % (os.getpid(), os.getpgrp()))
    if not event.is_set():
        event.set()
    pool.close()
    pool.join()
    manager.shutdown()
    print('exit ...')
    os._exit(0)
    # os.killpg(os.getpgid(os.getpid()), signal.SIGKILL)

if __name__ == '__main__':
    print ('current pid is %s' % os.getpid())
    mul_pool = Pool()
    manager = Manager()
    event = manager.Event()

    handler = functools.partial(term,mul_pool,event,manager)
    signal.signal(signal.SIGTERM,handler)
    
    for i in range(3):
        mul_pool.apply_async(func=fun, args=(i,event))

    mul_pool.close()
    mul_pool.join()

参考自:
主进程被杀死时,如何保证子进程同时退出,而不变为孤儿进程
《主进程被杀死时,如何保证子进程同时退出,而不变为孤儿进程(一)》
《主进程被杀死时,如何保证子进程同时退出,而不变为孤儿进程(二)》
《主进程被杀死时,如何保证子进程同时退出,而不变为孤儿进程(三)》

猜你喜欢

转载自blog.csdn.net/TFATS/article/details/107663162