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

时间:2023-12-30 13:12:20

1 基本概述

1.1 孤儿进程和僵尸进程

父进程创建子进程后,较为理想状态是子进程结束,父进程回收子进程并释放子进程占有的资源;而实际上,父子进程是异步过程,两者谁先结束是无顺的,一般可以通过父进程调用wait()或waitpid()语句来等待子进程结束再退出。

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

孤儿进程:父进程结束后还有基于该父进程创建的子进程(一个或多个)尚没有结束,此时的子进程称之为孤儿进程;孤儿进程将被init进程(进程树中除了init都有父进程)接受,也就意味着init进程负责孤儿进程完成状态收集工作。一般而言,init进程的pid为1,有资料显示init有三种形式,其pid并不为1。

僵尸进程:在Linux进程状态及转换关系中有一种进程状态是僵尸状态(zombie),此时该进程称之为僵尸进程;当使用fork创建子进程后,子进程退出,而父进程并没有调用wait或waitpid获取子进程的状态信息,那么子进程中的进程描述仍然保存在系统中,这种进程称之为僵尸进程,有时也称为僵死进程。

1.2 孤儿进程和僵尸进程危害

孤儿进程:孤儿进程不会占用系统资源,系统会将父进程回收处理孤儿进程,所以孤儿进程不占用系统资源;有时还会利用孤儿进程的这一原理进程程序逻辑设计。

僵尸进程:子进程先于父进程退出,父进程没有处理子进程的退出状态,此时子进程就会成为僵尸进程;进程已经结束,但是会占有一定的计算机资源。所以,我们应该尽量避免僵尸进程的产生。

Lunix提供了父进程获取子进程状态信息的机制;在每一个进程退出的时候,内核会释放该进程所有的资源,包括打开的文件、占用的内存等;但是仍然为其保留一定的信息(这些信息涵盖有进程号the process ID、退出状态the termination status of the process、运行时间the amount of CPU time taken by the process等),直到父进程调用wait() / waitpid()时才释放。

一个进程在调用exit命令结束自己的生命时,其实它并没有真正被销毁,而是留下一个称之为僵尸进程(Zombie)的数据结构(系统调用exit,它的作用是使进程退出,但也仅仅限于讲一个正常进程变成了一个僵尸进程,并不能将其完全销毁)。在Linux进程的状态中,僵尸进程是非常特殊的一种,它已经放弃了几乎所有内存空间,没有任何可执行代码,也不能被调度,仅仅在进程列表中保留一个位置,记载该进程的退出状态等信息供其他进程收集,除此之外,僵尸进程不再占有任何内存空间。它需要它的父进程来为它收尸,如果他的父进程没有安装SIGCHLD信号处理函数调用wait或waitpid等待子进程结束,又没有显式忽略该信号,那么它就一直保持僵尸状态,如果这时父进程结束了,那么init进程自动会接手这个子进程,为它收尸,它还是能被清除的。但是如果父进程是一个循环而不结束,那么子进程就会一直保持僵尸状态,如果如此进行下去,系统中就会遗留大量僵尸进程。

如何避免僵尸进程的产生?

1. 合理安排进程任务,尽量让父进程先结束

2. 父进程使用os.wait() os.waitpid() 来处理僵尸进程

3. 创建二级子进程,让一级子进程退出,从而形成两个完全独立的进程

4. 使用信号处理的方式

2 阻塞函数os.wait 与 os.waitpid 的基本语法

阻塞函数:当程序运行到该函数则进程处于阻塞状态不再继续运行 。一般在达到某些条件后结束阻塞

有大量IO操作或者可以给子进程操作,尽量让父进程先结束

阻塞函数不是不运行,而是等待某种事件的发生,如sleep就是阻塞等待函数

内核判断子进程是否结束,当子进程结束时,会通知应用层进行反馈

详情参考Python::OS 模块 -- 进程管理,里面详细阐述了os模块的属性

2.1 os.wait()

功能 : 阻塞等待子进程的退出,只要该父进程的任意子进程退出则终止阻塞。

参数:无

返回值:包含两个元素的元组,第一个 元素为退出的子进程的PID,第二个元素为子进程的退出码。

等待任何一个子进程结束,返回一个tuple,包括子进程的进程ID退出状态信息:一个16位的数字,低8位是杀死该子进程的信号编号,而高8位是退出状态(如果信号编号是0),其中低8位的最高位如果被置位,则表示产生了一个core文件。

import os
import sys
from time import sleep

pid = os.fork()

if pid<0:
    print("create process failed")
elif pid==0:
    print("this is chaild process:",os.getpid())
    sleep(2)
    sys.exit(2)
else:
    p,status = os.wait()
    print("this is parena process")
    print("p=",p,"status=",status)
    print(os.WEXITSTATUS(status))

运行

this is chaild process: 4088
this is parena process
p= 4088 status= 512
2

这里status=512 而sys.exit(2)中的值是2,其实他们是256倍数关系,这是计算机本身的一种算法;可以使用os.WEXITSTATUS(status)函数来将其计算成为想要的数值,也可以使用256倍数关系进行直接运算;但是不同系统存在有不同的算法规则,所以尽量采用函数调用来计算数值

2.2 os.waitpid(pid, options)

功能: 阻塞等待子进程的退出

参数: pid -1 表示任意子进程退出都可以处理

>0 表示指定pid的子进程退出才能处理

options : 0 表示一直阻塞等待

WNOHANG: 非阻塞等待

返回值 : 同wait

os.wait() ====> os.waitpid(-1,0)

等待进程id为pid的进程结束,返回一个tuple,包括进程的进程ID和退出信息(和os.wait()一样),参数options会影响该函数的行为。在默认情况下,options的值为0。

如果pid是一个正数,waitpid()请求获取一个pid指定的进程的退出信息;

如果pid为0,则等待并获取当前进程组中的任何子进程的值;

如果pid为-1,则等待当前进程的任何子进程;

如果pid小于-1,则获取进程组id为pid的绝对值的任何一个进程;

当系统调用返回-1时,抛出一个OSError异常。

import os
import sys
from time import sleep

pid = os.fork()

if pid<0:
    print("create process failed")
elif pid==0:
    print("this is chaild process:",os.getpid())
    sleep(2)
    sys.exit(2)
else:
    p,status = os.waitpid(-1,0)#与刚才等价
    print("this is parena process")
    print("p=",p,"status=",status)
    print(os.WEXITSTATUS(status))

运行

this is chaild process: 4296
this is parena process
p= 4296 status= 512
2

创建子子进程

import os

pid = os.fork()

if pid<0:
    print("create process failed")
elif pid==0:
    p = os.fork()
    if p<0:
        pass
    elif p==0:
        print("child--->child")
    else:
        os._exit(0)
else:
    os.wait()
    print("this is parena process")

运行

this is parena process
child--->child

用的相关资源。