七.孤儿进程
1.什么是孤儿进程
- 当一个父进程创建了多个子进程, 子进程再创建子子进程等等
- 父进程因正常运行完毕或其他情况被干掉的时候, 它的子进程就变成了孤儿进程
- 为了避免孤儿进程完成任务后没有父亲通知操作系统回收资源
- 于是 PID 为 "1"的顶级进程 systemd 就接手了这个孤儿进程
- systemd 相当于一个孤儿院, 但凡是孤儿进程都会成为它的子进程
2.孤儿进程演示
- 先在一个虚拟终端里开启一个 Bash 进程,把他当做父进程
- 紧接着开启一个 “sleep 1000 &” 进程, 把它当做子进程
- 然后在另一个虚拟终端查看这两个进程信息
- 再杀掉 sleep 的父进程 Bash 看看结果如何
- 图示
八.僵尸进程
1.什么是僵尸进程
- 这是Linux出于好心的设计
- 一个父进程开启了一堆子进程, 当子进程比父进程先运行完(死掉)
- 操作系统会释放子进程占用的重型资源(内存空间, CPU资源, 打开的文件)
- 但会保留子进程的关键信息(PID, 退出状态, 运行时间等)
- 目的是为了让父进程能随时查看自己的子进程信息(不管该子进程有没有死掉)
- 这种已经死掉的子进程都会进入僵尸状态, '‘僵尸进程’'是Linux系统的一种数据结构
ps : 任何正常结束的子进程都会进入到僵尸状态, 而被强制终止的进程的所有信息将会被清除
2.僵尸进程回收----概念
- 操作系统保留子进程信息供父进程查看
- 当父进程觉得不再需要查看的时候, 会向操作系统发送一个 wait / waitpid 系统调用
- 于是操作系统再次清理僵尸进程的残余信息
3.僵尸进程回收----实际
- 优秀的开源软件
这些软件在开启子进程时, 父进程内部会及时调用"wait" / "waitpid" 通知操作系统来回收僵尸进程
- 水平良好的开发者
功底深厚,知道父进程要对子进程负责
会在父进程内部考虑到调用 "wait" / "waitpid" 通知操作系统回收僵尸进程
但是发起系统调用时间可能慢了一点
于是我们就可以使用 "ps aux | grep [z]+" 命令查看到僵尸进程
- 水平非常低的开发者
技术半吊子,只知道开子进程,父进程也不结束,并在那一直开子进程,不知道什么是僵尸进程
系统调用 "wait" / "waitpid" 也没有听说过
于是计算机会堆积许多的僵尸进程,占用着大量的"pid",(每启动一个进程就会分配一个"pid号")
计算机进入一个奇怪的现象: 内存够用,硬盘充足,CPU空闲,但新的程序无法启动
这就是因为"PID"不够用了
4.如何清理僵尸进程
- 针对良好的开发者
我们可以手动发信号给父进程: "# kill -CHLD [父进程的PID]"
通知父进程快点向操作系统发起系统调用 "wait" / "waitpid" 来清理变成僵尸的儿子们
- 针对半吊子水平的开发者
这种情况子下,我们只能将父进程终结,因为你发给它的信号不会得到回应
父进程被杀死,"僵尸进程"将会变成"僵尸孤儿进程"
但凡是"孤儿进程"都会被Linux系统中"PID"为"1"的顶级进程"systemd"回收
"systemd"会发起系统调用 "wait" / "waitpid" 来通知操作系统清理僵尸进程
# Centos7 的顶级进程为 systemd
# Centos6 的顶级进程为 init
5.使用Process类制造僵尸进程
原本 multiprocessing 模块在你发起系统调用 start()
开启子进程的时候会自动检测当前状态下是否存在僵尸进程, 并将其回收, join()
调用也是一样, 我们可以查看这两个调用的源码进行查看 :
- 我们可以让父进程创建子进程后暂停在原地什么事情都不做, 于是 multiprocessing 模块的底层机制都没有运行, 也就没法清除运行完毕并变成僵尸态的子进程, 下面再 Linux 上进行演示 :
# coding:utf-8
from multiprocessing import Process
import os,time
def task():
print("子进程:%s"%os.getpid())
time.sleep(4) # 子进程 4 秒后结束变成僵尸进程
if __name__ == "__main__":
for i in range(400):
print("父进程:%s"%os.getpid())
p = Process(target=task)
p.start()
time.sleep(100000) # 让父进程停在原地什么也不做
使用 top 命令查看系统状态信息, 可以发现已经出现了 400 个僵尸进程
我们可以通过 kill 刚运行的 py 文件将这些僵尸进程变成孤儿进程, 从而被 systemd 接管, systemd 再发起系统调用将其清除
九.守护进程
1.什么是守护进程
由主进程创建, 并会随着主进程的结束而结束
2.守护进程的生命周期
-
进程之间是相互独立的, 守护进程会在主进程代码执行结束后就终止
-
守护进程内无法再次开启子进程, 否则会抛出异常 : AssertionError: daemonic processes are not allowed to have children
from multiprocessing import Process
import os,time
class MyProcess(Process):
def __init__(self,n):
super().__init__()
self.n = n
def run(self) -> None:
print(f'子进程:{os.getpid()}开始')
time.sleep(2)
print(f"子进程:{os.getpid()}结束")
if __name__ == '__main__':
p = MyProcess(2)
p.daemon = True # 需要在 strat() 之前设置
p.start()
print(f"主进程:{os.getpid()}结束")
# 在当前主进程的代码已经运行完毕, 守护进程就会终止, 甚至守护进程还没来的急启动
'''输出
主进程:16924结束
'''
我们使用 sleep 让主进程简单延时一下好让子进程启动起来
from multiprocessing import Process
import os,time
class MyProcess(Process):
def __init__(self,n):
super().__init__()
self.n = n
def run(self) -> None:
print(f'子进程:{os.getpid()}开始')
time.sleep(2)
print(f"子进程:{os.getpid()}结束")
if __name__ == '__main__':
p = MyProcess(2)
p.daemon = True
p.start()
time.sleep(1) # 延时一秒, 足够操作系统将子进程开起来
print(f"主进程:{os.getpid()}结束")
'''输出
子进程:8620开始
主进程:10480结束
'''
再次强调, 守护进程是在主进程的代码执行完毕终止
from multiprocessing import Process
import os,time
def Foo():
print(f"Foo:{os.getpid()}-->111")
time.sleep(1)
print(f"Foo--->222")
def Bar():
print(f"Bar:{os.getpid()}-->333")
time.sleep(2)
print(f"Bar--->444")
if __name__ == '__main__':
p1 = Process(target=Foo)
p2 = Process(target=Bar)
p1.daemon = True # 将 p1 设置守护进程
p1.start()
p2.start()
print("------>end")
# 当运行到这一行的时候主进程代码已经运行完了, 那么守护进程也已经终止了, 与主进程在等着 p2 运行无关, 这时操作系统还没来的急启动 p1 这个子进程
'''输出
------>end
Bar:18124-->333
Bar--->444
'''