主进程被杀死时,如何保证子进程同时退出,而不变为孤儿进程(一)

  在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时,主进程中需要对子进程进行等待,否则子进程会随着主进程的退出而退出:

 1 import threading
 2 import time
 3 import multiprocessing
 4
 5
 6 def fun(args):
 7     for i in range(100):
 8         print args
 9         time.sleep(1)
10
11
12 if __name__ == ‘__main__‘:
13     threads = []
14     for i in range(4):
15         # t = threading.Thread(target=fun, args=(str(i),))
16         # t.setDaemon(True)
17         t = multiprocessing.Process(target=fun, args=(str(i),))
18         t.daemon = True
19         t.start()
20         threads.append(t)
21
22     for i in threads:
23         i.join()

  运行上面的代码,主进程会等待子进程执行结束后退出,整个程序结束。line15、16为多线程模式,运行效果和多进程相似。注意,这里说的相似表示的是程序运行正常的情况下,而当有人为的干扰时,例如在进程启动之后,通过kill -9将进程杀死时,情况就不同了,我们知道多线程模型再复杂,也只是在同一个进程中,杀死主进程,所有的线程都会随着主进程的退出而退出,而多进程模型中,每个进程都是独立的,在杀死主进程之后,其他子进程并不会受到影响,还会继续运行,上面的代码中进程的targe函数很简单,只进行了有限次数的循环输出,而在真是的场景,子进程可能会始终在loop处理业务,而如果在子进程被杀死后,没有有效回收子进程,需要人工的杀死,这样的话就比较麻烦。

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

 1 processes = []
 2 def fun(x):
 3     print ‘current sub-process pid is %s‘ % os.getpid()
 4     while True:
 5         print ‘args is %s ‘ % x
 6         time.sleep(1)
 7
 8
 9 def term(sig_num, addtion):
10     print ‘terminate process %d‘ % os.getpid()
11     try:
12         print ‘the processes is %s‘ % processes
13         for p in processes:
14             print ‘process %d terminate‘ % p.pid
15             p.terminate()
16             # os.kill(p.pid, signal.SIGKILL)
17     except Exception as e:
18         print str(e)
19
20
21 if __name__ == ‘__main__‘:
22     print ‘current pid is %s‘ % os.getpid()
23     for i in range(3):
24         t = Process(target=fun, args=(str(i),))
25         t.daemon = True
26         t.start()
27         processes.append(t)
28     signal.signal(signal.SIGTERM, term)
29     try:
30         for p in processes:
31             p.join()
32     except Exception as e:
33         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函数,仔细观察上面的代码示例,注意到在注册信号处理函数时,子进程已经启动,所以这里子进程并没有注册信号处理函数,接着,我们对主进程进行修改,确保子进程启动前,进行信号处理函数的注册:

 1 if __name__ == ‘__main__‘:
 2     signal.signal(signal.SIGTERM, term)
 3     print ‘current main-process pid is %s‘ % os.getpid()
 4     for i in range(3):
 5         t = Process(target=fun, args=(str(i),))
 6         t.daemon = True
 7         t.start()
 8         processes.append(t)
 9
10     try:
11         for p in processes:
12             p.join()
13     except Exception as e:
14         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发送信号,现在对上面的代码进行简单修改:

 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信号时,都会将进程组主进程和子进程全部杀死:

时间: 02-08

主进程被杀死时,如何保证子进程同时退出,而不变为孤儿进程(一)的相关文章

开启子进程的两种方式,孤儿进程与僵尸进程,守护进程,互斥锁,IPC机制,生产者与消费者模型

开启子进程的两种方式 # # # 方式一: # from multiprocessing import Process # import time # # def task(x): # print('%s is running' %x) # time.sleep(3) # print('%s is done' %x) # # if __name__ == '__main__': # # Process(target=task,kwargs={'x':'子进程'}) # p=Process(tar

孤儿进程、僵尸进程和守护进程

维基百科的解释中: 在操作系统领域中,孤儿进程指的是在其父进程执行完成或被终止 后仍继续运行的一类进程. 在类UNIX系统中,僵尸进程是指完成执行(通过 exit 系统调用,或运行时发生致命错误或收到终止信号所致)但在操作系统的进程表中仍然有一个表项(进程控制块PCB),处于"终止状态 "的进程. 在一個多工的電腦作業系統中,守护进程(英语:daemon,英语发音:/?di?m?n/或英语发音:/?de?m?n/)是一種在后台执行的电脑程序. 此类程序会被以进程的形式初始化. 守护进程

python学习笔记——孤儿进程和僵尸进程

1 基本概述 1.1 孤儿进程和僵尸进程 父进程创建子进程后,较为理想状态是子进程结束,父进程回收子进程并释放子进程占有的资源:而实际上,父子进程是异步过程,两者谁先结束是无顺的,一般可以通过父进程调用wait()或waitpid()语句来等待子进程结束再退出. 孤儿进程:父进程结束后还有基于该父进程创建的子进程(一个或多个)尚没有结束,此时的子进程称之为孤儿进程:孤儿进程将被init进程(进程树中除了init都有父进程)接受,也就意味着init进程负责孤儿进程完成状态收集工作.一般而言,ini

Linux进程学习(孤儿进程和守护进程)

孤儿进程和守护进程 通过前面的学习我们了解了如何通过fork()函数和vfork()函数来创建一个进程.现在 我们继续深入来学习两个特殊的进程:孤儿进程和守护进程 一.孤儿进程 1.什么是 孤儿进程如果一个子进程的父进程先于子进程 结束, 子进程就成为一个孤儿进程,它由 init 进程收养,成为 init 进程的子进程.2.那么如何让一个进程变为一个孤儿进程呢?我们可以先创建一个进程,然后杀死其父进程,则其就变成了孤儿进程.pid =  fork();if(pid > 0) {         

Linux进程学习 - 孤儿进程和守护进程

孤儿进程和守护进程 通过前面的学习我们了解了如何通过fork()函数和vfork()函数来创建一个进程.现在 我们继续深入来学习两个特殊的进程:孤儿进程和守护进程 一.孤儿进程 1.什么是 孤儿进程如果一个子进程的父进程先于子进程 结束, 子进程就成为一个孤儿进程,它由 init 进程收养,成为 init 进程的子进程.2.那么如何让一个进程变为一个孤儿进程呢?我们可以先创建一个进程,然后杀死其父进程,则其就变成了孤儿进程.pid =  fork();if(pid > 0) {         

Linux中的僵尸进程和孤儿进程

在UNIX里,除了进程0(即PID=0的交换进程,Swapper Process)以外的所有进程都是由其他进程使用系统调用fork创建的,这里调用fork创建新进程的进程即为父进程,而相对应的为其创建出的进程则为子进程,因而除了进程0以外的进程都只有一个父进程,但一个进程可以有多个子进程.        操作系统内核以进程标识符(Process Identifier,即PID)来识别进程.进程0是系统引导时创建的一个特殊进程,在其调用fork创建出一个子进程(即PID=1的进程1,又称init)

【APUE】孤儿进程与僵死进程

基本概念: 在unix/linux中,正常情况下,子进程是通过父进程创建的,子进程在创建新的进程.子进程的结束和父进程的运行是一个异步过程,即父进程永远无法预测子进程 到底什么时候结束. 当一个 进程完成它的工作终止之后,它的父进程需要调用wait()或者waitpid()系统调用取得子进程的终止状态. 孤儿进程:一个父进程退出,而它的一个或多个子进程还在运行,那么那些子进程将成为孤儿进程.孤儿进程将被init进程(进程号为1)所收养,并由init进程对它们完成状态收集工作. 僵尸进程:一个进程

fork()、僵死进程和孤儿进程

孤儿进程:一个父进程退出,而它的一个或多个子进程还在运行,那么那些子进程将成为孤儿进程.孤儿进程将被init进程(进程号为1)所收养,并由init进程对它们完成状态收集工作. 僵尸进程:一个进程使用fork创建子进程,如果子进程退出,而父进程并没有调用wait或waitpid获取子进程的状态信息,那么子进程的进程描述符仍然保存在系统中.这种进程称之为僵死进程. #include <stdio.h> #include <iostream> #include "unistd.

linux --&gt; 孤儿进程与僵尸进程

孤儿进程与僵尸进程 一.介绍 孤儿进程:一个父进程退出,而它的一个或多个子进程还在运行,那么那些子进程将成为孤儿进程.孤儿进程将被init进程(进程号为1)所收养,并由init进程对它们完成状态收集工作. 僵尸进程:一个进程使用fork创建子进程,如果子进程退出,而父进程并没有调用wait或waitpid获取子进程的状态信息,那么子进程的进程描述符仍然保存在系统中.这种进程称之为僵死进程. 二.危害 1.僵尸进程的危害 unix提供了一种机制可以保证只要父进程想知道子进程结束时的状态信息,这种机