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

2024-04-19 12:36

本文主要是介绍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_processdef 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:passdef 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 timedef 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_processdef 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 Processdef 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 = Truep.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中,当一个父进程退出时,其子进程并不会成为孤儿进程。相反,子进程继续运行,并且它们的生命周期与父进程的结束是独立的。

这篇关于python中的守护进程、僵尸进程、孤儿进程的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/917555

相关文章

使用Python绘制蛇年春节祝福艺术图

《使用Python绘制蛇年春节祝福艺术图》:本文主要介绍如何使用Python的Matplotlib库绘制一幅富有创意的“蛇年有福”艺术图,这幅图结合了数字,蛇形,花朵等装饰,需要的可以参考下... 目录1. 绘图的基本概念2. 准备工作3. 实现代码解析3.1 设置绘图画布3.2 绘制数字“2025”3.3

python使用watchdog实现文件资源监控

《python使用watchdog实现文件资源监控》watchdog支持跨平台文件资源监控,可以检测指定文件夹下文件及文件夹变动,下面我们来看看Python如何使用watchdog实现文件资源监控吧... python文件监控库watchdogs简介随着Python在各种应用领域中的广泛使用,其生态环境也

Python中构建终端应用界面利器Blessed模块的使用

《Python中构建终端应用界面利器Blessed模块的使用》Blessed库作为一个轻量级且功能强大的解决方案,开始在开发者中赢得口碑,今天,我们就一起来探索一下它是如何让终端UI开发变得轻松而高... 目录一、安装与配置:简单、快速、无障碍二、基本功能:从彩色文本到动态交互1. 显示基本内容2. 创建链

Java调用Python代码的几种方法小结

《Java调用Python代码的几种方法小结》Python语言有丰富的系统管理、数据处理、统计类软件包,因此从java应用中调用Python代码的需求很常见、实用,本文介绍几种方法从java调用Pyt... 目录引言Java core使用ProcessBuilder使用Java脚本引擎总结引言python

python 字典d[k]中key不存在的解决方案

《python字典d[k]中key不存在的解决方案》本文主要介绍了在Python中处理字典键不存在时获取默认值的两种方法,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,... 目录defaultdict:处理找不到的键的一个选择特殊方法__missing__有时候为了方便起见,

使用Python绘制可爱的招财猫

《使用Python绘制可爱的招财猫》招财猫,也被称为“幸运猫”,是一种象征财富和好运的吉祥物,经常出现在亚洲文化的商店、餐厅和家庭中,今天,我将带你用Python和matplotlib库从零开始绘制一... 目录1. 为什么选择用 python 绘制?2. 绘图的基本概念3. 实现代码解析3.1 设置绘图画

Python pyinstaller实现图形化打包工具

《Pythonpyinstaller实现图形化打包工具》:本文主要介绍一个使用PythonPYQT5制作的关于pyinstaller打包工具,代替传统的cmd黑窗口模式打包页面,实现更快捷方便的... 目录1.简介2.运行效果3.相关源码1.简介一个使用python PYQT5制作的关于pyinstall

使用Python实现大文件切片上传及断点续传的方法

《使用Python实现大文件切片上传及断点续传的方法》本文介绍了使用Python实现大文件切片上传及断点续传的方法,包括功能模块划分(获取上传文件接口状态、临时文件夹状态信息、切片上传、切片合并)、整... 目录概要整体架构流程技术细节获取上传文件状态接口获取临时文件夹状态信息接口切片上传功能文件合并功能小

python实现自动登录12306自动抢票功能

《python实现自动登录12306自动抢票功能》随着互联网技术的发展,越来越多的人选择通过网络平台购票,特别是在中国,12306作为官方火车票预订平台,承担了巨大的访问量,对于热门线路或者节假日出行... 目录一、遇到的问题?二、改进三、进阶–展望总结一、遇到的问题?1.url-正确的表头:就是首先ur

基于Python实现PDF动画翻页效果的阅读器

《基于Python实现PDF动画翻页效果的阅读器》在这篇博客中,我们将深入分析一个基于wxPython实现的PDF阅读器程序,该程序支持加载PDF文件并显示页面内容,同时支持页面切换动画效果,文中有详... 目录全部代码代码结构初始化 UI 界面加载 PDF 文件显示 PDF 页面页面切换动画运行效果总结主