python中的守护进程、僵尸进程、孤儿进程

继续上一篇文章的探讨:https://blog.csdn.net/weixin_39743356/article/details/137885419

守护进程

守护进程(Daemon Process)是一种在后台运行的特殊类型的进程,它独立于控制终端,并且周期性地执行某种任务或等待处理某些事件。在Unix-like系统中,守护进程通常在系统启动时启动,并持续运行直到系统关闭。由于它们不与任何用户交互,因此被认为是“服务”型的进程,通常用于执行系统任务,如日志记录、网络服务、系统监控等。

守护进程的特点包括:

  1. 通常在系统启动时自动运行。
  2. 在后台运行,不与任何终端会话相关联。
  3. 通常具有较高的权限,以便能够执行一些需要特权的操作。
  4. 经常被设计为长时间运行的服务,而不是执行一次性任务。
  5. 通常没有用户界面,而是通过命令行或配置文件进行交互和配置。
  6. 在系统关闭时通常会被优雅地终止,以确保数据的完整性。

步骤

  1. 从命令行或终端会话中分离出来,使得守护进程不依赖于任何终端。
  2. 改变工作目录,通常是到/或者创建一个专用的目录。
  3. 改变文件创建掩码(umask),以确保新创建的文件对其他用户是可读的。
  4. 关闭所有非必需的文件描述符,以避免文件泄露。
  5. 调用daemon()函数(如果使用C语言)或者使用Python的subprocess模块中的相关功能来将进程转变为守护进程。
  6. 启动主服务循环,执行守护进程的主要职责。

os.fork()示例

Python中创建守护进程可以通过os.fork()方法来实现,在子进程中再次使用os.fork()确保该子进程不会成为会话领导(session leader),从而避免获取控制终端。同时设置新的工作目录、文件权限掩码和关闭所有打开的文件描述符来与控制终端彻底脱离关系。

import os
import time
from multiprocessing import current_process


def daemonize():
    # 第一次fork,生成子进程,脱离父进程
    try:
        if os.fork() > 0:
            raise SystemExit(0)  # 父进程退出
    except OSError as e:
        raise RuntimeError('fork #1 failed.')

    print("子进程执行了", current_process().name, current_process().pid)
    os.chdir('/')  # 修改工作目录
    os.umask(0)  # 重新设置文件创建权限
    os.setsid()  # 设置新的会话连接

    # 第二次fork,防止子程序获取控制终端。
    try:
        if os.fork() > 0:
            raise SystemExit(0)
    except OSError as e:
        raise RuntimeError('fork #2 failed.')

    # 关闭打开的文件描述符。
    print("子进程执行了", current_process().name, current_process().pid)
    with open('/dev/null', 'r+b') as f_null:
        for fd in range(3):  # STDIN, STDOUT, STDERR.
            try:
                os.dup2(f_null.fileno(), fd)
            except OSError as e:
                pass


def main():
    while True:
        print("Daemon process running.")
        time.sleep(5)


if __name__ == '__main__':
    print("父进程执行了", current_process().name, current_process().pid)
    daemonize()
    main()
  1. 程序开始执行,并且当前只有一个进程,即主进程。
  2. 主进程调用 os.fork() 创建了一个子进程。这时候存在两个几乎完全相同的进程:父(主)进程和子进程。
  3. 在父(主)程序中,os.fork() 返回新创建子程序的PID(大于0),然后父程序通过调用 exit(0) 退出。这时候父程序结束了自己的生命周期。
  4. 在子程序中,os.fork() 返回0,表示这是在子程序内部执行。
    1. 子进程首先会打印自己的名称和PID,然后执行一系列操作来确保它成为一个守护进程。
    2. os.chdir('/') 改变子进程的工作目录到根目录/
    3. os.umask(0) 设置文件创建掩码为0,这样新创建的文件对其他用户是可读的。
    4. os.setsid() 创建一个新的会话ID(SID),这个操作确保子进程不会成为任何终端的会话领导者,从而避免了控制终端的影响。
  5. 第二次 fork() 调用后:
    • 子程序再次成为父亲并创建了另一个新的子程序。
    • 新创建出来的子进程会继续运行代码剩余部分。
    • 第一次被创建出来的进程则退出。因为第一次被创建出来的进程已经完成了其使命:确保最终运行代码剩余部分的那个子进程不会成为会话领导者(session leader),从而避免获得控制终端。
  6. 最后的子进程将标准输入、标准输出和标准错误(即文件描述符0、1和2)重定向到了 /dev/null,因此main方法打印的内容不会显示在任何控制台或终端上。这是守护进程常见的做法,因为它们通常不应该与用户交互或产生输出到控制台。确保了守护进程不会占用任何终端资源,也不会产生僵尸进程。
  7. 这个最后的进程就是我们想要实现功能逻辑上真正意义上“守护”的过程;它没有控制终端、独立于其他用户交互式过场景,并且可以持续在背景里面执行任务直至系统关闭或者任务完成自我结束。

输出:

父进程执行了 MainProcess 75046
子进程执行了 MainProcess 75047
子进程执行了 MainProcess 75048

执行以下命令可以看到子进程还在后台

ps -p 75046,75047,75048 -o pid,ppid,comm,state,user,vsz,command

请添加图片描述

记得手动kill掉

kill -9 75048

补充

os.dup2(f_null.fileno(), fd) 这行代码是在进行文件描述符的重定向操作。在Unix和类Unix系统中,每个进程都有一组文件描述符,它们是非负整数,用于引用打开的文件、管道或网络连接。其中,标准输入(STDIN)、标准输出(STDOUT)和标准错误(STDERR)分别被分配了固定的文件描述符编号:0、1 和 2。

具体来说:

  • f_null.fileno() 返回 /dev/null 文件对象 f_null 的文件描述符。
  • fd 是要被替换的目标文件描述符编号,在这里指的是 0、1 、 2。

调用 os.dup2(x, y) 将会:

  1. 关闭当前进程中编号为 y 的文件描述符(如果它已经打开)。
  2. 复制编号为 x 的文件描述符,并将其复制到编号为 y 的位置上。

因此,在这种情况下,该操作将关闭子进程中原本与标准输入、输出和错误相关联的文件描述符,并将它们全部重定向到 /dev/null。这意味着任何尝试从STDIN读取数据的操作都会立即返回EOF(表示没有数据可读),任何尝试写入STDOUT或STDERR的输出都会被丢弃并不会显示在任何地方。

这样做通常是出于以下原因:

  • 防止守护进程产生任何终端I/O操作,因为守护进程应该独立于控制终端运行。
  • 避免由于未处理输出导致资源泄露或其他潜在问题。
  • 确保即使程序尝试读取输入或写入日志信息也不会对程序执行产生影响。

简而言之,通过重定向到 /dev/null, 守护进程可以无声无息地运行其任务而不干扰其他系统活动。

multiprocessing示例

multiprocessing中,每个Process对象都有一个属性叫做daemon,当这个属性被设置为True时,这个进程就会成为守护进程。守护进程在其父(主)进程终止时会自动终止,并且通常不用于执行需要长时间运行的任务。

import multiprocessing
import time


def daemon_process():
    print('Starting my daemon process')
    while True:
        time.sleep(1)
        print('Daemon process is running...')


if __name__ == '__main__':
    # 创建一个守护子程序
    d = multiprocessing.Process(name='Daemon', target=daemon_process)
    d.daemon = True  # 设置为True表示这是一个守护程序

    # 启动守护子程序
    d.start()

    # 主程序将等待一段时间然后退出
    print('Main process is running...')
    time.sleep(5)
    print('Main process is exiting.')

  • 定义了一个名为 daemon_process() 的函数作为我们想要以守护方式运行的任务。
  • if __name__ == '__main__': 块中,我们创建了 Process() 对象,并将其 daemon 属性设置为True。
  • 然后启动该子程序并让主程序休眠5秒钟。
  • 当主程序完成休眠并退出时,由于子程序是以守护方式运行的,所以也会自动停止。

os.fork实现的例子中为什么在pycharm中跑完了进程还在,而用multiprocessing实现的确直接结束了?

  1. 使用os.fork() 的情况下:
    • PyCharm可能没有检测到父进程退出后子进程仍然在运行。因此,在父进程结束执行后,IDE界面可能显示程序已经“完成”,但实际上孤立的子进程(守护进程)仍然在系统中运行。
    • 这个孤立的子程序并没有被PyCharm所管理,所以即使IDE认为程序已经结束了,实际上该子程序还是会持续运行直至自身结束或者被外部强制杀死。
  2. 使用 multiprocessing 的情况下:
    • 当主程序创建一个设置了 daemon=True 的多处理子程序时,并且主程序退出后,默认情况下所有守护式多处理子程序也会自动退出。
    • PyCharm能够正确地管理和追踪通过 multiprocessing 模块创建的所有多处理任务,并且当主任务结束时也能够确保所有守护式多处理任务都被正确地关闭。

僵尸进程

在操作系统中,僵尸进程(Zombie Process)是指已经完成执行(终止)但仍然有一个记录存在进程表中的进程。这个记录包含了进程的一些信息,如退出状态、运行时间等,以便父进程查询。僵尸进程本身不占用任何系统资源,除了在进程表中的一个位置。

在Unix和类Unix系统(比如Linux)中,当一个子进程结束运行时,并不会立即从系统中完全清除。如果父进程还在运行,它需要通过调用wait()waitpid()函数来读取子进程的退出状态。在父进程读取了子程序的结束状态之前,子程序会保留为僵尸状态。

wait()waitpid() 是 Unix 系统调用,它们被用于父进程中以等待和回收子进程的资源,防止子进程成为僵尸进程。

  1. wait():
    • wait() 系统调用使得一个父进程暂停执行,直到它的一个子进程结束或者该父进程接收到一个指定的信号。
    • 当子进程结束时,wait() 会回收子进程所占用的资源,并清除系统中该子进程的记录。
    • 如果有多个子进程,则 wait() 会等待任一子进程结束,并返回终止了的那个子进程的 PID。
  2. waitpid():
    • waitpid() 是更灵活版本的 wait()。它允许父进程指定要等待哪个具体的子进rocess 或者是某一类特定状态变化(如停止或终止)。
    • 它有几个参数:第一个参数是你想要等待状态改变的特定 PID(如果传入 -1 则与 wait() 相同,表示任何一个),第二个参数是存储状态信息(通常是退出码) 的地址,第三个参数可以设置为不同值来修改函数行为(例如是否立即返回而不阻塞)。
    • 使用这种方式可以实现更精确地控制对哪些子进行管理和如何管理。

在 Python 中使用多处理模块时,默认情况下并不需要直接调用这些系统调用。Python 的 multiprocessing 库提供了自己高层次、跨平台版本的 API 来处理相关问题。例如,在 Python 中使用 Process.join() 方法就能够达到类似于 wait/waitpid 的效果——等待子进程结束并回收其资源。

如果父进程没有调用wait()waitpid()来获取子程序的状态信息,则该僵尸程序将一直存在。如果其父程序先于它终止,则该僵尸程序将被init(PID为1)接管,并由init来负责调用wait()回收资源。

模拟僵尸进程代码:

import multiprocessing
import time
from multiprocessing import current_process


def func():
    print("子进程执行了", current_process().name, current_process().pid)
    exit()


if __name__ == '__main__':
    print("父进程执行了", current_process().name, current_process().pid)
    process = multiprocessing.Process(target=func)
    process.start()  # 创建进程
    time.sleep(300)

代码仅仅只是延迟主进程回收子进程资源的时间而已,而这个时间段内对于操作系统而言,就会认为该子进程是僵尸进程。但是并不会造成真正的僵尸进程的出现。因为主进程结束以后还是会回收子进程的数据的。

Python解释器内部实现了对于进程回收的操作进行高度封装和安全处理,所以python中我们不需要担心僵尸进程的出现。

输出

父进程执行了 MainProcess 59423
子进程执行了 Process-1 59425

在mac系统中查看进程状态:

ps -p 59423,59425 -o pid,ppid,comm,state,user,vsz,command

“R”表示运行中,“S”表示睡眠状态,“Z”表示僵尸进程等。

请添加图片描述

在Windows操作系统中,通常不会出现类似于Unix或Linux系统中的僵尸进程(Zombie Process)。

Windows操作系统采用了不同的机制来处理已经结束运行但未被完全清理的进程。当一个Windows程序完成执行后,它的状态和资源通常由操作系统自动清理。如果父进程没有等待子进程(也就是没有调用WaitForSingleObject或类似函数),那么当子进程结束时,它所占用的所有资源都会被立即释放,并且该过程对用户是透明的。

孤儿进程

孤儿进程(Orphan Process)是指在Unix-like系统中,父进程在其子进程结束之前退出或终止了,而这些子进程还在运行的情况。当父进程终止后,所有未终止的子进程将被init进程(PID为1的特殊系统进程)接管。init进程会自动成为这些孤儿子进程的新父亲,并负责对它们执行wait()调用来回收它们结束时留下的资源和状态信息。

孤儿进程通常不会造成系统资源的浪费,因为它们仍然在执行其任务,只是没有了原来的父进程。当这些进程结束执行后,init进程将确保它们被正确清理,防止它们变成僵尸进程。孤儿进程的存在通常不会影响系统性能,除非有大量未被合理管理的孤儿进程积累。在某些情况下,孤儿进程的数量过多可能会导致系统资源压力。

代码模拟孤儿进程:

import os
import time
from multiprocessing import Process


def func():
    print(f"子进程的pid={os.getpid()}")
    time.sleep(60)
    print("hello")

if __name__ == '__main__':
    print(f"主进程的pid={os.getpid()}")
    p = Process(target=func)
    p.daemon = True
    p.start()
    time.sleep(15)

输出

主进程的pid=62467
子进程的pid=62469

macOS下:

1、执行命令:

ps -p 62467,62469 -o pid,ppid,comm,state,user,vsz,command

输出:

请添加图片描述

2、执行命令kill掉主进程,填入主进程id

kill -9 62467

3、再次查看就可以发现,子进程变成了一个还在运行的孤儿进程,并且父进程编程了init(ppid为1)

ps -p 62467,62469 -o pid,ppid,comm,state,user,vsz,command

请添加图片描述

4、等孤儿进程结束后,会被init回收,再次执行命令可以发现已经没有了

请添加图片描述

同样在Windows操作系统中,进程模型与Unix-like系统不同,因此孤儿进程的概念并不完全适用。在Windows中,当一个父进程退出时,其子进程并不会成为孤儿进程。相反,子进程继续运行,并且它们的生命周期与父进程的结束是独立的。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

程序猿-瑞瑞

打赏不准超过你的一半工资哦~

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值