IT数码 购物 网址 头条 软件 日历 阅读 图书馆
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
图片批量下载器
↓批量下载图片,美女图库↓
图片自动播放器
↓图片自动播放器↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁
 
   -> Python知识库 -> 【python】通过信号机制对子进程进行控制 -> 正文阅读

[Python知识库]【python】通过信号机制对子进程进行控制

在python中,使用多进程可以绕过GIL的限制从而充分利用多核CPU来加速任务。但如果对进程处理不当,就容易造成僵尸进程孤儿进程,从而造成系统资源的浪费。

本文分析了在python中可能会生成这两种进程的写法以及如何正确地处理代码以避免它们的产生。

本文所有的代码均在CentOS下测试通过,其他的版本与操作系统不做保证。

1. 僵尸进程的产生与处理

1.1 产生一个僵尸进程

当父进程分配给一个子进程去某项任务后不再管理该子进程(无论该子进程是否完成、是否报错),那么当子进程完成任务后,就会变成僵尸进程。

利用python制造一个僵尸进程非常简单,假设有两个脚本,分别名为main.pyworker.py,其内容如下:

# main.py
import subprocess
import os
import time

if __name__ == '__main__':
    print(f"I am the parent process, with pid: {os.getpid()}")
    subprocess.Popen(['python', 'worker.py'])
    time.sleep(100)  
    print(f"I finish my work.")
# worker.py
import time
import os

if __name__ == '__main__':
	print(f"I am the child process, with pid: {os.getpid()} and I'm going to sleep.")
	time.sleep(5)
	print("Now I wake up")

现在,在控制台执行python main.py,首先会打印以下内容:

I am the parent process, with pid: 29947
I am the child process, with pid: 29968 and I'm going to sleep.

然后马上利用ps查看进程,那么将会打印出如下内容:
在这里插入图片描述
由以上可知,主进程(29947)产生了一个子进程(29968)来执行任务;5秒钟之后,控制台会继续打印:

Now I wake up

这说明子进程的任务已经完成,由于主进程在生成子进程后就没有再进行管理,此时会发现子进程已经变成了僵尸进程:
在这里插入图片描述
如果再等待95秒钟,主进程完成并退出,那么再通过ps查看会发现僵尸进程已经消失。这说明主进程退出后,僵尸进程转由init进程管理,它会周期性地回收僵尸进程。

所以,main.py的最后一行是为了保证主进程持续足够的时间,这样子进程就不会交由init进程管理,僵尸进程就可以通过ps观察到。

1.2 僵尸进程的处理

尽管生成一个子进程便不再管理是一个「不负责任」的做法,但如果子进程可以完成所有的工作(例如,能将计算结果存储到指定的数据库中而不必汇总到主进程),那这种写法确实是便利的,我们只需要考虑将僵尸进行回收即可。

python通过signal模块提供了这样的方法。修改main.py的代码如下:

# main.py
import subprocess
import os
import time
import signal
signal.signal(signal.SIGCHLD, signal.SIG_IGN)

if __name__ == '__main__':
    print(f"I am the parent process, with pid: {os.getpid()}")
    subprocess.Popen(['python', 'worker.py'])
    time.sleep(100)  
    print(f"I finish my work.")

重复1.1中的过程,会发现子进程在5秒钟后执行完成会直接退出,而没有变成僵尸进程。显然,起作用的就是这一行代码:

signal.signal(signal.SIGCHLD, signal.SIG_IGN)

signal.signal()接受两个参数,第一个是收到的信号值,第二是所采取的行为。在这里例子里,signal.signal(signal.SIGCHLD, signal.SIG_IGN)表示主进程在收到子进程被终止的信号(signal.SIGCHLD)时,对其进行忽略(signal.SIG_IGN),从而子进程的资源得到了释放。

2. 孤儿进程的产生与处理

当父进程在子进程完成任务之前终止时,由于子进程还会继续完成任务,这时就变成了一个孤儿进程。

2.1 产生一个孤儿进程

稍微修改上述代码,就可以产生一个孤儿进程:

# main.py
import subprocess
import time
import os

if __name__ == '__main__':
    print(f"I am the parent process, with pid: {os.getpid()}")
    subprocess.Popen(['python', 'worker.py'])
    time.sleep(5)  
    print(f"I finish my work.")
# worker.py
import time
import os

if __name__ == '__main__':
	print(f"I am the child process, with pid: {os.getpid()} and I'm going to sleep.")
	time.sleep(10)
	print("Now I wake up")

仍然在控制台执行python main.py,则首先会打印如下内容:

I am the parent process, with pid: 12994
I am the child process, with pid: 13015 and I'm going to sleep.

这是利用ps查看进程,内容如下:
在这里插入图片描述
5秒钟之后,父进程退出了。这时,ps的结果如下:
在这里插入图片描述
注意,1631号进程已经退出,而1641号进程的父进程已经变成了init进程。这说明它已经变成了孤儿进程了,交由init来管理。

10秒钟之后,子进程也完成了任务并退出了,用ps可以验证这一点。

由于孤儿进程有init接管,因此它不会变成僵尸进程。

2.2 孤儿进程的处理

尽管孤儿进程不同于僵尸进程,但在某些情况下(例如任务运行起来后发现在程序的最后有一个错误,为了避免执行前置代码浪费时间,想要终止这些进程)我们希望主进程退出后,自动终止所有的子进程。

顺着1.2中的想法,我们希望当主进程收到终止信号时,它先终止所有的子进程,然后再退出。

要实现这一点,我们需要维护一个列表来存储所有子进程:

# main.py
import subprocess
import time
import os
import signal

child_processes = []

def handler(signum, action):
    for i, p in enumerate(child_processes):
    	print(f'Killing {i+1}/5...')
        p.kill()
    raise

if __name__ == '__main__':
    print(f"I am the parent process, with pid: {os.getpid()}")
    for i in range(5):
        p = subprocess.Popen(['python', 'worker.py'])
        child_processes.append(p)
    signal.signal(signal.SIGTERM, handler)
    time.sleep(10)  
    print(f"I finish my work.")

signal.SIGTERM表示终止信号,也就是在使用kill <pid>时向目标进程发送的信号。通过定义handler函数,我们已经修改了主进程在收到终止信号时的行为:找到所有的子进程,然后杀死它们。

注意,你不能发送SIGKILL(即kill -9 <pid>发送的信号)或SIGSTOP,这两个信号进程是无法捕获的,会被立即终止而不进行任何操作。

如果你执行了python main.py,那么控制台会马上打印以下内容:

I am the parent process, with pid: 12162
I am the child process, with pid: 12194 and I'm going to sleep.
I am the child process, with pid: 12207 and I'm going to sleep.
I am the child process, with pid: 12219 and I'm going to sleep.
I am the child process, with pid: 12234 and I'm going to sleep.
I am the child process, with pid: 12245 and I'm going to sleep.

然后在另外的shell中执行kill 12162,这样就向主进程发送了一个signal.SIGTERM信号。主进程在收到这个信号时,转入handler函数进行相应的动作,于是,你会在控制台上看到如下内容:

Killing 1/5...
Killing 2/5...
Killing 3/5...
Killing 4/5...
Killing 5/5...
Traceback (most recent call last):
  File "main.py", line 21, in <module>
    time.sleep(10)  
  File "main.py", line 12, in handler
    raise
RuntimeError: No active exception to reraise

通过ps查看,发现所有主进程和子进程都被终止了。

看到这里,你应该已经可以想到,在handler函数中的raise其实是用来终止主进程的。因为我们修改了主进程收到SIGTERM信号时的行为,为了能让kill仍能杀死主进程,所以可以通过杀死所有子进程后抛出一个异常的方式来终止主进程。

2.3 一种更加pythonic的方式

虽然通过2.2的方式可以实现我们的想法,但通过抛出异常的方式来终止进程总显得不够pythonic;另外,在主进程中记录所有的子进程也显得不够完美(姑且不提如果你使用了进程池时该怎么办)。那么,有没有一种更好的方法呢?

答案是肯定的。

linux中有一个进程组的概念,即主进程以及它创建的子进程都属于同一个进程组,它有唯一的编号。于是,通过os.killpg()方法,可以批量杀死进程组中的所有进程。

于是,修改上述代码如下:

# main.py
import subprocess
import time
import os
import signal

def handler(signum, action):
    os.killpg(os.getpgid(os.getpid()), signal.SIGKILL)

if __name__ == '__main__':
    print(f"I am the parent process, with pid: {os.getpid()}")
    for i in range(5):
        p = subprocess.Popen(['python', 'worker.py'])
    signal.signal(signal.SIGTERM, handler)
    time.sleep(10)  
    print(f"I finish my work.")

执行python main.pykill掉该主进程后,控制台打印的内容应该如下:

I am the parent process, with pid: 20416
I am the child process, with pid: 20438 and I'm going to sleep.
I am the child process, with pid: 20445 and I'm going to sleep.
I am the child process, with pid: 20453 and I'm going to sleep.
I am the child process, with pid: 20472 and I'm going to sleep.
I am the child process, with pid: 20463 and I'm going to sleep.
已杀死

3 总结

本文简要分析了linux操作系统中的信号处理机制以及它们在python中的实现。重点讨论了如何利用python进行多进程编程时通过signal来避免产生僵尸进程和孤儿进程。

想要了解更多的内容,可以查看本文的参考内容。

4 参考内容

  1. signal — 设置异步事件处理程序
  2. Linux信号基础
  3. Python模块之信号(signal)
  4. 主进程被杀死时,如何保证子进程同时退出,而不变为孤儿进程(一)
  Python知识库 最新文章
Python中String模块
【Python】 14-CVS文件操作
python的panda库读写文件
使用Nordic的nrf52840实现蓝牙DFU过程
【Python学习记录】numpy数组用法整理
Python学习笔记
python字符串和列表
python如何从txt文件中解析出有效的数据
Python编程从入门到实践自学/3.1-3.2
python变量
上一篇文章      下一篇文章      查看所有文章
加:2021-11-20 18:21:00  更:2021-11-20 18:23:03 
 
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁

360图书馆 购物 三丰科技 阅读网 日历 万年历 2024年11日历 -2024/11/16 0:48:12-

图片自动播放器
↓图片自动播放器↓
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
图片批量下载器
↓批量下载图片,美女图库↓
  网站联系: qq:121756557 email:121756557@qq.com  IT数码