大家好,我是码农先森。

守护进程顾名思义就是能够在后台一直运行的进程,不会霸占用户的会话终端,脱离了终端的控制。相信朋友们对这东西都不陌生了吧?如果连这个概念都还不能理解的话,建议回炉重造多看看 Linux 进程管理相关的基础知识。在我们日常的编程中常见有类似 php think ...php artisan ...php yii ... 等命令启动需要一直执行的任务,都会通过 nohup 挂载到后台保持长期运行的状态。同样在 Workerman 中也是使用类似 php index.php start 的命令来启动进程,但不同的是它不需要利用 nohup 便可以挂载到后台运行。那有些朋友就会好奇它是怎么实现的呢?为了解决朋友们的疑惑,我们今天就重点深入分析一下 Workerman 守护进程的实现原理。

我们先了解一些进程相关的知识:

  • 父进程:父进程是生成其他进程的进程。当一个进程创建了另一个进程时,创建者被称为父进程,而被创建的进程则成为子进程。父进程可以通过进程标识符(PID)来识别它所创建的子进程。
  • 子进程:子进程是由父进程创建的新进程。子进程继承了父进程的一些属性,例如环境变量、文件描述符等。子进程独立于父进程运行,它可以执行自己的代码,并且具有自己的资源和内存空间。
  • 进程组:进程组是一组相关联的进程的集合。每个进程组都有一个唯一的进程组ID(PGID),用于标识该进程组。进程组通常由一个父进程创建,并且包含了与父进程具有相同会话ID(SID)的所有子进程。
  • 会话:会话是一组关联进程的集合,通常由用户登录到系统开始,直至用户注销或关闭终端会话结束,一个会话中的进程共享相同的控制终端。每个会话都有一个唯一的会话ID(SID),用于标识该会话。会话通常包含一个或多个进程组,其中第一个进程组成为会话的主进程组。

这些概念俗称八股文,向来都不怎么好理解,那我们来看个例子。执行了命令 php index.php 便产生了进程 61052「该进程的父进程是 Bash 进程 8243,这里不用管它」,然后通过 Fork 创建了子进程 61053 且其父进程就是 61052,这两个进程拥有共同的进程组 61052 和会话 8243。调用 posix_setsid 函数,将会为子进程 61053 开启新的进程组 61053 和新的会话 61053,这里的会话可以理解为一个新的命令窗口终端。最后子进程 61053 通过 Fork 创建了子进程 61054,进程 61053 升级成了父进程,这里再次 Fork 的原因是要避免被终端控制进程所关联,这个进程 61052 是在终端的模式下创建的,自此进程 61054 就形成了守护进程。

[manongsen@root phpwork]$ php index.php
[parent] 进程ID: 61052, 父进程ID: 8243, 进程组ID: 61052, 会话ID: 8243
[parent1] 进程ID: 61052, 父进程ID: 8243, 进程组ID: 61052, 会话ID: 8243 退出了该进程
[child1] 进程ID: 61053, 父进程ID: 61052, 进程组ID: 61052, 会话ID: 8243
[child1] 进程ID: 61053, 父进程ID: 61052, 进程组ID: 61053, 会话ID: 61053
[parent2] 进程ID: 61053, 父进程ID: 61052, 进程组ID: 61053, 会话ID: 61053 退出了该进程
[child2] 进程ID: 61054, 父进程ID: 61053, 进程组ID: 61053, 会话ID: 61053 保留了该进程 [manongsen@root phpwork]$ ps aux | grep index.php
root 66064 0.0 0.0 408105040 1472 s080 S+ 10:00下午 0:00.00 grep index.php
root 61054 0.0 0.0 438073488 280 ?? S 10:00下午 0:00.00 php index.php

上面举例的进程信息,正是这段代码运行所产生的。如果看了这段代码且细心的朋友,会发现为什么 posix_setsid 这个函数不放在第一次 Fork 前调用,而在第二次 Fork 前调用呢,这样的话就不用 Fork 两次了?原因是组长进程是不能创建会话的,进程组ID 61052 和进程ID 61052 相同「即当前进程则为组长进程」,所以需要子进程来创建新的会话,这一点需要特别注意一下。

<?php

function echoMsg($prefix, $suffix="") {
// 进程ID
$pid = getmypid();
// 进程组ID
$pgid = posix_getpgid($pid);
// 会话ID
$sid = posix_getsid($pid);
// 父进程ID
$ppid = posix_getppid(); echo "[{$prefix}] 进程ID: {$pid}, 父进程ID: {$ppid}, 进程组ID: {$pgid}, 会话ID: {$sid} {$suffix}" . PHP_EOL;
} // [parent] 进程ID: 61052, 父进程ID: 8243, 进程组ID: 61052, 会话ID: 8243
echoMsg("parent"); // 第一次 Fork 进程
$pid = pcntl_fork();
if ( $pid < 0 ) {
exit('fork error');
} else if( $pid > 0 ) {
// [parent1] 进程ID: 61052, 父进程ID: 8243, 进程组ID: 61052, 会话ID: 8243 退出了该进程
echoMsg("parent1", "退出了该进程");
exit;
} // 创建的 子进程ID 为 61053 但 进程组、会话 还是和父进程是同一个
// [child1] 进程ID: 61053, 父进程ID: 61052, 进程组ID: 61052, 会话ID: 8243
echoMsg("child1"); // 调用 posix_setsid 函数,会创建一个新的会话和进程组,并设置 进程组ID 和 会话ID 为该 进程ID
if (-1 === \posix_setsid()) {
throw new Exception("Setsid fail");
} // 现在会发现 进程组ID 和 会话ID 都变成了 61053 在这里相当于启动了一个类似 Linux 终端下的会话窗口
// [child1] 进程ID: 61053, 父进程ID: 61052, 进程组ID: 61053, 会话ID: 61053
echoMsg("child1"); // 第二次 Fork 进程
// 这里需要二次 Fork 进程的原因是避免被终端控制进程所关联,这个进程 61052 是在终端的模式下创建的
// 需要脱离这个进程 61052 以确保守护进程的稳定
$pid = pcntl_fork();
if ( $pid < 0 ){
exit('fork error');
} else if( $pid > 0 ) {
// [parent2] 进程ID: 61053, 父进程ID: 61052, 进程组ID: 61053, 会话ID: 61053 退出了该进程
echoMsg("parent2", "退出了该进程");
exit;
} // 到这里该进程已经脱离了终端进程的控制,形成了守护进程
// [child2] 进程ID: 61054, 父进程ID: 61053, 进程组ID: 61053, 会话ID: 61053 保留了该进程
echoMsg("child2", "保留了该进程"); sleep(100);

有时间的朋友最好自行执行代码并分析一遍,会有不一样的收获。这里假装你已经实践过了,这下我们来看 Workerman 的 Worker.php 文件中 554 行的 runAll 方法中的 static::daemonize() 这个函数,实现的流程逻辑和上面的例子几乎一样。不过这里还使用了 umask 这个函数,其主要的作用是为该进程所创建的文件或目录赋予相应的权限,保证有权限操作文件或目录。

// workerman/Worker.php:554
/**
* Run all worker instances.
* 运行进程
* @return void
*/
public static function runAll()
{
static::checkSapiEnv();
static::init();
static::parseCommand();
static::lock();
// 创建进程并形成守护进程
static::daemonize();
static::initWorkers();
static::installSignal();
static::saveMasterPid();
static::lock(\LOCK_UN);
static::displayUI();
static::forkWorkers();
static::resetStd();
static::monitorWorkers();
} // workerman/Worker.php:1262
/**
* Run as daemon mode.
* 使用守护进程模式运行
* @throws Exception
*/
protected static function daemonize()
{
// 判断是否已经是守护状态、以及当前系统是否是 Linux 环境
if (!static::$daemonize || static::$_OS !== \OS_TYPE_LINUX) {
return;
} // 设置 umask 为 0 则当前进程创建的文件权限都为 777 拥有最高权限
\umask(0); // 第一次创建进程
$pid = \pcntl_fork();
if (-1 === $pid) {
// 创建进程失败
throw new Exception('Fork fail');
} elseif ($pid > 0) {
// 主进程退出
exit(0);
} // 子进程继续执行...
// 调用 posix_setsid 函数,可以让进程脱离父进程,转变为守护进程
if (-1 === \posix_setsid()) {
throw new Exception("Setsid fail");
} // 第二次创建进程,在基于 System V 的系统中,通过再次 Fork 父进程退出
// 保证形成的守护进程,不会成为会话首进程,不会拥有控制终端
$pid = \pcntl_fork();
if (-1 === $pid) {
// 创建进程失败
throw new Exception("Fork fail");
} elseif (0 !== $pid) {
// 主进程退出
exit(0);
} // 子进程继续执行...
}

守护进程也是 Workerman 中重要的一部分,它保障了 Workerman 进程的稳定性。不像我们通过 nohup 启动的命令,挂起到后台之后,有时还神不知鬼不觉的就挂了,朋友们或许都有这样的经历吧。当然在市面上也有一些开源的守护进程管理软件,比如 supervisor 等,其次还有人利用会话终端 screen、tmux 等工具来实现。其实守护进程的实现方式有多种多样,我们这里只是为了分析 Workerman 中守护进程的实现原理,而引出了在 PHP 中实现守护进程模式的例子,希望本次的内容能对你有所帮助。

感谢大家阅读,个人观点仅供参考,欢迎在评论区发表不同观点。


欢迎关注、分享、点赞、收藏、在看,我是微信公众号「码农先森」作者。

深入理解 PHP 高性能框架 Workerman 守护进程原理的更多相关文章

  1. Linux 守护进程原理及实例(Redis、Nginx)

    1. 什么是守护进程 守护进程daemon,是指没有控制终端,运行在后台的进程,通常伴随着系统启动产生,系统关机结束.可以使用命令ps -axj查看系统的守护进程,输出如下所示: 父ID PID 组I ...

  2. linux守护进程、SIGHUP与nohup详解

    前端时间帮忙定位个问题.docker容器故障恢复后,其中的keepalived进程始终无法启动,也看不到Keepalived的日志. strace 查看系统调用之后,发现了原因所在 socket(PF ...

  3. 基于Laravel框架下使用守护进程supervisor实现定时任务(毫秒)

    本篇文章给大家带来的内容是关于基于Laravel框架下使用守护进程supervisor实现定时任务(毫秒),有一定的参考价值,有需要的朋友可以参考一下,希望对你有所帮助. 公司需要实现X分钟内每隔Y秒 ...

  4. Linux进程理解与实践(五)细谈守护进程

    一. 守护进程及其特性      守护进程最重要的特性是后台运行.在这一点上DOS下的常驻内存程序TSR与之相似.其次,守护进程必须与其运行前的环境隔离开来.这些环境包括未关闭的文件描述符,控制终端, ...

  5. 深入理解Linux操作系统守护进程的意义

    Linux服务器在启动时需要启动很多系统服务,它们向本地和网络用户提供了Linux的系统功能接口,直接面向应用程序和用户.提供这些服务的程序是由运行在后台的守护进程(daemons)来执行的.守护进程 ...

  6. 守护进程与Supervisor

    博客链接:http://www.cnblogs.com/zhenghongxin/p/8676565.html 消息队列处理后台任务带来的问题 在系统稍微大些的时候,我们经常会用到消息队列(实现的方式 ...

  7. Android 通过JNI实现守护进程,使得Service服务不被杀死

    来自: http://finalshares.com/read-7306 转载请注明出处: http://blog.csdn.net/yyh352091626/article/details/5054 ...

  8. .NET跨平台实践:用C#开发Linux守护进程

    Linux守护进程(Daemon)是Linux的后台服务进程,它脱离了与控制终端的关联,直接由Linux init进程管理其生命周期,即使你关闭了控制台,daemon也能在后台正常工作. 一句话,为L ...

  9. .NET跨平台实践:用C#开发Linux守护进程(转)

    Linux守护进程(Daemon)是Linux的后台服务进程,它脱离了与控制终端的关联,直接由Linux init进程管理其生命周期,即使你关闭了控制台,daemon也能在后台正常工作. 一句话,为L ...

  10. [转]❲阮一峰❳Linux 守护进程的启动方法

    ❲阮一峰❳Linux 守护进程的启动方法 "守护进程"(daemon)就是一直在后台运行的进程(daemon). 本文介绍如何将一个 Web 应用,启动为守护进程. 一.问题的由来 ...

随机推荐

  1. 如何使用csproj构建C#源代码组件NuGet包?

    一般我们构建传统的NuGet包,都是打包和分发dll程序集文件. 至于打包和分发C#源代码文件的做法,比较少见. 那么这种打包源代码文件的做法,有什么优点和缺点呢? 优点: 方便阅读源代码. 方便断点 ...

  2. textFieldShouldReturn: 方法无效化!

    问题描述 不管如何在键盘上点击return,textFieldShouldReturn:方法一直没有调用. 问题代码 @interface ViewController : UIViewControl ...

  3. java+SpringCloud开发的性能和环保问题

    对于大部分商业应用开发程序员而言,使用java+spring是一件幸福的事情. 一般情况下,我们使用cloud开发不是那么重要.精密的应用,这些应用包括例如大型的商业交易,社区等等. 因为这些应用天然 ...

  4. 我又学会了使用Range实现网络文件下载的断点续传

    目录 前言 1.Range请求头 1.1.概述 1.2.使用限制 1.3.范围请求 1.4.预防资源变更 2.断点续传下载实现 2.1.流程设计 2.2.代码实现 2.3.运行结果 3.RandomA ...

  5. Fedora升级33->34

    Fedora升级33->34   1. dnf --refresh upgrade   2. dnf install dnf-plugin-system-upgrade --best   3. ...

  6. 两个防火墙的IPsec

    首先要保证双面都能通信到对面! 拓扑图: P1 没有要求默认就行 P2 没有要求默认就行 对流体 对流体细节问题 预共享密钥要一致 IPsec 创建隧道 添加路由 Ping对方激活ipsec Ping ...

  7. 『vulnhub系列』Dripping-Blues-1

    『vulnhub系列』Dripping-Blues-1 下载地址: https://www.vulnhub.com/entry/dripping-blues-1,744/ 信息搜集: 使用nmap进行 ...

  8. Nuxt3 的生命周期和钩子函数(十)

    title: Nuxt3 的生命周期和钩子函数(十) date: 2024/6/30 updated: 2024/6/30 author: cmdragon excerpt: 摘要:本文详细介绍了Nu ...

  9. 学习笔记--Java 运算符

    Java 运算符 算术运算符 关系运算符 逻辑运算符 位运算[略] 赋值运算符 字符串连接符 三元运算符 Java 运算符 按照功能划分: 功能 运算符 算术运算符 +.-.*./.++.--.% 关 ...

  10. 01-初识springboot

    目录 01,什么是springboot 02,如何使用springboot 01,什么是springboot springboot是一个基于spring框架开发出来的一个新的框架,目的是为了简化spr ...