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中,当一个父进程退出时,其子进程并不会成为孤儿进程。相反,子进程继续运行,并且它们的生命周期与父进程的结束是独立的。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mfbz.cn/a/556129.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

本地部署 Meta Llama3-8b 和 Llama3-70b

本地部署 Meta Llama3-8b 和 Llama3-70b 0. 引言1. Meta对Llama 3的目标2. Llama 3的性能3. 下载和安装 Ollama4. 使用 Ollama 运行 Llama3 0. 引言 今天,Meta 正式介绍Meta Llama 3,Meta 开源大型语言模型的下一代产品。 这次发布包括具有80亿&#xf…

数据可视化(四):Pandas技术的高级操作案例,豆瓣电影数据也能轻松分析!

Tips:"分享是快乐的源泉💧,在我的博客里,不仅有知识的海洋🌊,还有满满的正能量加持💪,快来和我一起分享这份快乐吧😊! 喜欢我的博客的话,记得…

typecho博客的相对地址实现

typecho其中的博客地址,必须写上绝对地址,否则在迁移网址的时候会出现问题,例如页面记载异常 修改其中的 typecho\var\Widget\Options\General.php 中的165行左右, /** 站点地址 */if (!defined(__TYPECHO_SITE_URL__)) {$siteUrl new Form\Element\Text(siteUrl,null,$this-…

Tomcat和Spring Boot配置https

生成测试证书 生成证书前,先验证本地是否正确配置jdk环境变量,如果jdk环境变量配置正确,在命令行程序输入生成证书的命令。 keytool -genkey -alias tomcat -keyalg RSA -keystore "F:\job\apache-tomcat-8.5.29\key\freeHttps.keysto…

MySQL模糊查询

一、MySQL通配符模糊查询(%,_) 1.1.通配符的分类 1.“%”百分号通配符:表示任何字符出现任意次数(可以是0次) 2.“_”下划线通配符:表示只能匹配单个字符,不能多也不能少,就是一个字符。当然…

Yoshua Bengio独家专访:我不想把大模型未来押注在Scaling Law上,AGI路上要“注意安全”...

导读 漫长的30年间,数度从主流方向的超然出走,是Bengio的制胜秘诀。这种不盲从主流的风格体现在他研究生涯的方方面面。 90年代末期,神经网络被打入冷宫,Bengio的论文多次遭拒,连学生们也开始担心,和他一起…

EPSON晶振应用到汽车电子产品上的型号有哪些?

EPSON品牌应用在汽车电子产品上的晶振.,当然也少不了晶振可能最熟悉的就是32.768K系列和26MHZGPS晶振用的多。 在汽车里每一个部件都应有的不一样,甚至多次使用到同一尺寸,不同频率的晶振.爱普生品牌晶振型号就有几百种,很容易混淆,要想记住汽车里所应用到的不是件…

⑥【Shiro】使多个自定义Realm规则生效。

个人简介:Java领域新星创作者;阿里云技术博主、星级博主、专家博主;正在Java学习的路上摸爬滚打,记录学习的过程~ 个人主页:.29.的博客 学习社区:进去逛一逛~ ⑥【Shiro】Shiro中,如何使多个自定…

DevOps(七)Jenkins发布第一个流水线任务

Jenkins的流水线(Pipeline)是一种强大的工具,用于定义和管理持续集成和持续交付(CI/CD)过程。它允许你以代码的形式(即"Pipeline as Code")定义整个构建、测试和部署流程,…

UE4 拍摄、保存并浏览相册

效果: 1.新建CameraActor类 2.修改截图保存路径 3.编写BP_Camera蓝图 注意路径 Save Image函数要在执行拍照和BeginPlay事件执行一次 按钮执行拍摄事件 3.编写UMG蓝图 技巧:让Index加1、减1循环赋值 4.把BP_Camera挂在玩家上

《QT实用小工具·三十》基于QT开发的访客管理平台demo

1、概述 源码放在文章末尾 该项目为访客管理平台demo,包含主界面、系统设置、警情查询、调试帮助、用户退出功能。 项目部分代码如下: #ifndef QTHELPER_H #define QTHELPER_H#include "head.h"class QtHelper { public://获取所有屏幕区域…

从Linux角度具体理解程序翻译过程-----预处理、编译、汇编、链接

前言: 在C语言中,我们知道程序从我们所写的代码到可执行执行的过程中经历了以下过程 1.预处理 2.编译 3.汇编 4.链接 可以通过下图来理解 翻译过程 1.预处理 该过程主要进行以下操作: (1)头文件的包含 (2)define定义符号的替换&#xff…

怎样实现opc采集数据后传给web后端

现在很多老工厂要进行数字化改造,现场生产的各种数据需要传到web后端,很多工厂现场原有的自动监控系统已经采集了现场的各种数据,只是没有形成联网。如果前端自动化系统全部废除,重新做数字化控制系统,成本投入太大&am…

docker服务无法启动

背景:断电重启经常会导致磁盘io错误,甚至出现磁盘坏块 这时可以使用xfs_repair来修复磁盘,但是修复过程可能会导致部分数据丢失 xfs_repair -f -L /dev/sdc问题一: Apr 15 19:27:15 Centos7.6 systemd[1]: Unit docker.service e…

Java入门(JDK安装)

安装 JDK 下载 Java Downloads | Oracle 安装 下一步直接安装安装过程中,需要确定自己的安装位置 参考:D:\Java\jdk1.8.0_281_x64 演示位置 校验 终端输入 java -version 配置 1)删除默认 javapath 默认情况下,可以在cm…

【C++题解】1607. 两位数运算

问题:1607. 两位数运算 类型:基本运算、拆位求解 题目描述: 小丽在编程课上学会了拆位运算,她已经可以拆出一个两位整数的十位和个位了,她想知道这个整数的十位 / 个位的结果是多少,请编程帮她实现&#…

VulnHub靶机 DC-5 打靶 渗透测试详情过程

VulnHub靶机 DC-5 打靶 详细渗透测试过程 目录 VulnHub靶机 DC-5 打靶 详细渗透测试过程一、将靶机导入到虚拟机当中二、渗透流程主机发现端口扫描目录爆破文件包含getshell反弹shell提权 一、将靶机导入到虚拟机当中 靶机地址: https://download.vulnhub.com/dc/…

【论文阅读】YOLO-World | 开集目标检测

Date:2024.02.22,Tencent AI Lab,华中科技大学Paper:https://arxiv.org/pdf/2401.17270.pdfGithub:https://github.com/AILab-CVC/YOLO-World 论文解决的问题: 通过视觉语言建模和大规模数据集上的预训练来…

HTTP/HTTPS详解

HTTP/HTTPS详解 1. HTTP1.1 HTTP基础知识1.2 HTTP建立和断开连接 2. HTTPS 1. HTTP 1.1 HTTP基础知识 HTTP是互联网上应用最为广泛的一种网络协议,是一个客户端和服务器端请求和应答的标准(TCP),用 于从WWW服务器传输超文本到本…

学习一下选择排序,快速排序

1.选择排序 我们可以根据这动图看到,就是在这个数组里面我们选出最小的放进第一个位置,然后再选除了第一个位置最小的,剩下的数里面最小的放到第二个位置,是不是非常简单呢。 void SelectSort2(int* arr, int n) {int begin 0;…