【4.1】僵尸进程和孤儿进程

【一】引入

  • 我们知道在unix/linux中,正常情况下,子进程是通过父进程创建的,子进程在创建新的进程。
  • 子进程的结束和父进程的运行是一个异步过程,即父进程永远无法预测子进程 到底什么时候结束。
  • 当一个 进程完成它的工作终止之后,它的父进程需要调用wait()或者waitpid()系统调用取得子进程的终止状态。

【二】僵尸进程(有害)

【1】什么是僵尸进程

  • 僵尸进程是指完成了自己的任务,但父进程没有正确地释放它所占用的系统资源
  • 导致它仍然存在于进程列表中
  • 但已经停止了运行。
  • 这些僵尸进程会占据一定的系统内存,并在一定程度上影响系统的性能。

【2】解决办法(UNⅨ系统)

  • 因此,UNⅨ提供了一种机制可以保证父进程可以在任意时刻获取子进程结束时的状态信息

    • 在每个进程退出的时候,内核释放该进程所有的资源,包括打开的文件,占用的内存等。
      • 但是仍然为其保留一定的信息(包括进程号the process ID,退出状态the termination status of the process,运行时间the amount of CPU time taken by the process等)
    • 直到父进程通过wait / waitpid来取时才释放.
      • 但这样就导致了问题,如果进程不调用wait / waitpid的话,那么保留的那段信息就不会释放,其进程号就会一直被占用,但是系统所能使用的进程号是有限的
      • 如果大量的产生僵死进程,将因为没有可用的进程号而导致系统不能产生新的进程. 此即为僵尸进程的危害,应当避免。
  • 任何一个子进程(init除外)在exit()之后,并非马上就消失掉,而是留下一个称为僵尸进程(Zombie)的数据结构,等待父进程处理。

    • 这是每个子进程在结束时都要经过的阶段。
    • 如果子进程在exit()之后,父进程没有来得及处理,这时用ps命令就能看到子进程的状态是“Z”。
    • 如果父进程能及时 处理,可能用ps命令就来不及看到子进程的僵尸状态,但这并不等于子进程不经过僵尸状态。
    • 如果父进程在子进程结束之前退出,则子进程将由init接管。
    • init将会以父进程的身份对僵尸状态的子进程进行处理。

【3】Process示例

  • 当子进程开设后,该进程死后不会立刻释放占用的进程号
    • 因为要让父进程能够查看到开设的子进程的一些基本信息
      • 占用的 PID 号,运行时间等
  • 所有的进程都会步入僵尸进程
    • 父进程不死并且在无限制的创建子进程并且子进程也不结束
  • 如何回收子进程占用的 PID 号
    • 父进程等待子进程运行结束
    • 父进程调用 join 方法
from multiprocessing import Process
import time


def run():
    print(f'the run is beginning:>>>>')
    time.sleep(1)
    print(f'the run is ending:>>>>')


if __name__ == '__main__':
    p = Process(target=run)

    p.start()

    print(f'这是主程序的执行方法:>>>>')
    # 这是主程序的执行方法:>>>>
    # the run is beginning:>>>>
    # the run is ending:>>>>

【三】孤儿进程(无害)

【1】什么是孤儿进程

  • 孤儿进程则是指父进程在子进程终止之前就已经退出了
  • 导致子进程失去了与父进程通信的能力。
  • 这些孤儿进程将被init进程接管
  • init进程会等待它的状态信息并释放它的系统资源。

【2】示例

  • 子进程存活,父进程意外死亡
    • 没有父进程来帮助回收 PID 号
  • 解决办法
    • 操作系统会开设一个儿童福利院(init 进程)专门管理孤儿进程回收相关资源
import os
import sys
import time

if __name__ == '__main__':
    pid = os.getpid()
    ppid = os.getppid()
    print('我是父进程 :>>> ', 'pid', pid, 'ppid', ppid)
    pid = os.fork()
    # 执行pid=os.fork()则会生成一个子进程
    # 返回值pid有两种值:
    #    如果返回的pid值为0,表示在子进程当中
    #    如果返回的pid值>0,表示在父进程当中
    if pid > 0:
        print('父进程终止')
        sys.exit(0)

    # 保证主线程退出完毕
    time.sleep(1)
    print('我是子进程 :>>>> ', os.getpid(), os.getppid())

    # 子进程已经被pid为1的init进程接收了
    # 所以僵尸进程在这种情况下是不存在的,存在只有孤儿进程而已
    # 孤儿进程声明周期结束自然会被init来销毁。
    # 我是父进程 :>>>  pid 86270 ppid 61564
    # 父进程终止

【四】僵尸进程的危害场景

【1】引入

  • 例如有个进程,它定期的产 生一个子进程,这个子进程需要做的事情很少,做完它该做的事情之后就退出了,因此这个子进程的生命周期很短
  • 但是,父进程只管生成新的子进程,至于子进程 退出之后的事情,则一概不闻不问
  • 这样,系统运行上一段时间之后,系统中就会存在很多的僵死进程,倘若用ps命令查看的话,就会看到很多状态为Z的进程。
  • 严格地来说,僵死进程并不是问题的根源,罪魁祸首是产生出大量僵死进程的那个父进程。
  • 因此,当我们寻求如何消灭系统中大量的僵死进程时,答案就是把产生大 量僵死进程的那个元凶枪毙掉(也就是通过kill发送SIGTERM或者SIGKILL信号啦)。
  • 枪毙了元凶进程之后,它产生的僵死进程就变成了孤儿进 程,这些孤儿进程会被init进程接管,init进程会wait()这些孤儿进程,释放它们占用的系统进程表中的资源
  • 这样,这些已经僵死的孤儿进程 就能瞑目而去了。

【2】测试

  • test.py
from multiprocessing import Process
import time, os


def run():
    print('子', os.getpid())


if __name__ == '__main__':
    p = Process(target=run)
    p.start()

    print('主', os.getpid())
    time.sleep(1000)
  • 在unix或linux系统上执行
[root@vm172-31-0-19 ~]# python3  test.py &
[1] 18652
[root@vm172-31-0-19 ~]# 主 18652
子 18653

[root@vm172-31-0-19 ~]# ps aux |grep Z
USER       PID %CPU %MEM    VSZ   RSS TTY      STAT START   TIME COMMAND
root     18653  0.0  0.0      0     0 pts/0    Z    20:02   0:00 [python3] <defunct> #出现僵尸进程
root     18656  0.0  0.0 112648   952 pts/0    S+   20:02   0:00 grep --color=auto Z

[root@vm172-31-0-19 ~]# top #执行top命令发现1zombie
top - 20:03:42 up 31 min,  3 users,  load average: 0.01, 0.06, 0.12
Tasks:  93 total,   2 running,  90 sleeping,   0 stopped,   1 zombie
%Cpu(s):  0.0 us,  0.3 sy,  0.0 ni, 99.7 id,  0.0 wa,  0.0 hi,  0.0 si,  0.0 st
KiB Mem :  1016884 total,    97184 free,    70848 used,   848852 buff/cache
KiB Swap:        0 total,        0 free,        0 used.   782540 avail Mem 

  PID USER      PR  NI    VIRT    RES    SHR S %CPU %MEM     TIME+ COMMAND                                                                                                                                        
root      20   0   29788   1256    988 S  0.3  0.1   0:01.50 elfin   

【3】解决办法

(1)解决方法一:杀死父进程

(2)解决方法二:对开启的子进程应该记得使用join,join会回收僵尸进程

# 参考python2源码注释
class Process(object):
    def join(self, timeout=None):
        '''
        Wait until child process terminates
        '''
        assert self._parent_pid == os.getpid(), 'can only join a child process'
        assert self._popen is not None, 'can only join a started process'
        res = self._popen.wait(timeout)
        if res is not None:
            _current_process._children.discard(self)

# join方法中调用了wait,告诉系统释放僵尸进程。discard为从自己的children中剔除

[3]解决办法三:信号机制

# 解决方法三:http://blog.csdn.net/u010571844/article/details/50419798
# 思考:
from multiprocessing import Process
import time,os

def task():
    print('%s is running' %os.getpid())
    time.sleep(3)

if __name__ == '__main__':
    p=Process(target=task)
    p.start()
    p.join() # 等待进程p结束后,join函数内部会发送系统调用wait,去告诉操作系统回收掉进程p的id号

    print(p.pid) #???此时能否看到子进程p的id号
    print('主')
  • 分析:
    • p.join()是向操作系统发送请求,告知操作系统p的id号不需要再占用了,回收就可以,
    • 此时在父进程内还可以看到p.pid,但此时的p.pid是一个无意义的id号,因为操作系统已经将该编号回收
  • 打个比方:
    • 我党相当于操作系统,控制着整个中国的硬件,每个人相当于一个进程,每个人都需要跟我党申请一个身份证号
    • 该号码就相当于进程的pid,人死后应该到我党那里注销身份证号,
    • p.join()就相当于要求我党回收身份证号,但p的家人(相当于主进程)
      仍然持有p的身份证,但此刻的身份证已经没有意义

posted @ 2024-01-23 14:23  Chimengmeng  阅读(53)  评论(0编辑  收藏  举报