当前位置: 首页 > 文档资料 > PHP 进阶教程 >

7. php 多进程初探 - 再次谈 daemon 进程

优质
小牛编辑
129浏览
2023-12-01

其实前面是谈过一次 daemon 进程的,但是并涉及过多原理,但是并不影响使用。今天打算说说关于 daemon 进程更多的二三事,本质上说,如果你仅仅是简单实现利用一下 daemon 进程,这个不看也是可以的。

杠真,*NIX 真是波大精深,越是深入看越是发现它的 diao。原理往往都是枯燥的,大家都不爱看,但这并不影响我坚持写自己对这些东西的理解。

三个概念

进程组

一坨相关的进程可以组成一个进程组,每个进程组都会有一个组ID(正整数),每个进程组都会有一个组长进程,组长进程的ID等于进程组ID。组长进程可以创建新的进程组以及该进程组中的其他进程。一个进程组的是有生命周期的,即便是组长进程挂了,只有组里还有其他的活口,那就就算该进程组依然存活,只有到组里最后一个活口也挂了,那真的就是彻底没了。

会话

一坨相关的进程组组成了一个会话。在 *NIX 下,是通过 setsid() 创建一个新的会话。但是值得注意的是,组长进程不能创建会话,简单理解就是在组长进程中,执行setsid函数会报错,这点很重要。所以一般都是组长进程执行 fork,然后主进程退出,因为子进程的进程 ID 是新分配的,而子进程的进程组ID是继承父进程的,所以子进程就注定不可能是组长进程,从而可以确保子进程中一定可以执行 setsid 函数。在执行 setsid 函数时候,一般会发生下面三个比较重要的事情:

  • 该进程会创建一个新的进程组,该进程为进程组组长(或者你可以认为这是一种提升)
  • 该进程会创建一个会话组并成为该会话的会话首进程(会话首进程就是创建该会话的进程)
  • 该进程会失去控制终端。如果该进程本来就没有控制终端,则罢了(liao)。如果有,那么该进程也将脱离该控制终端,与之失去联系。

控制终端

每个会话可能会拥有一个控制终端(看着比较玄学,你可以暂时理解为就一个那种黑乎乎的命令行窗口),建立与控制终端连接的会话首进程叫做控制进程。

结合 Linux 命令 ps 来查看一下上述几个概念的恩怨情仇,我们看下我们常用的 ps -o pid,ppid,pgid,sid,comm | less 执行结果:

第一行分别是 PID,PPID,PGID,SID,COMMAND,依次分别是进程 ID,该进程父进程ID,进程组ID,会话ID,命令。

通过最后一列,我们知道第二行就是 bash 也就是 bash shell 进程,其进程 ID 为 15793,其父进程为 13291,进程组 ID 为 15793,会话 ID 也会 15793,结合前面的概念,我们可以知道bash shell就是该进程组组长。

第三行则是 ps 命令的进程,其进程 ID 为 15816,他是由于bash进程fork出来的,所以他的父进程 ID 为 15793,然后是他所属的组 ID 为 15816,所属的会话ID依然是15793。

最后一行是 less 命令的进程,其进程 ID 为 15817,他也是由bash进程fork出来的,所以他的父进程 ID 也为 15793,然后是他所属的组 ID 为 15816,所属的会话ID依然是15793。

简单总结一下:

  • 上述三个进程一共形成了两个进程组,bash自己为一组,组 ID 为 15793,组长进程为 bash 自己 ; ps 和 less 为一组,组 ID 为 15816,组长进程为ps进程
  • 上述三个进程属于同一个会话,会话 ID 为 15793,会话首进程为 bash 进程(待定)
  • 控制终端则为打开的 terminal 窗口,与之关联的控制进程则为 bash 进程

通过这么一顿分析,是不是感觉可以接受点儿了?然后是,叨逼叨了半天这个,跟 daemon 进程有啥子关系?

下面通过引入代码直接分析:

$pid = pcntl_fork();
if( $pid < 0 ){
  exit('fork error.');
} else if( $pid > 0 ) {
  // 主进程退出
  exit();
}
// 子进程继续执行

// 最关键的一步来了,执行setsid函数!
if( !posix_setsid() ){
  exit('setsid error.');
}

// 理论上一次fork就可以了
// 但是,二次fork,这里的历史渊源是这样的:在基于system V的系统中,通过再次fork,父进程退出,子进程继续,保证形成的daemon进程绝对不会成为会话首进程,不会拥有控制终端。

$pid = pcntl_fork();
if( $pid  < 0 ){
  exit('fork error');
} else if( $pid > 0 ) {
  // 主进程退出
  exit;
}

// 子进程继续执行

// 啦啦啦,啦啦啦,啦啦啦,已经变成daemon啦,开心
cli_set_process_title('testtesttest');
// 睡眠1000000,防止进程执行完毕挂了
sleep( 1000000 );

将上述文件保存为 daemon.php,然后 php daemon.php 执行,使用 ps -aux | grep testte ,如果没有什么大问题你应该就可以看到这个进程在后台跑了。

所以为什么第一步要先 fork 呢?因为调用 setsid 的进程不可以是组长进程(篇头的枯燥知识需要了吧?),所以必须 fork 一次,然后将主进程直接退出,保留子进程。因为子进程一定不会是组长进程,所以子进程可以调用 setsid。调用 setsid 则会产生三个现象:创建一个新会话并成为会话首进程,创建一个进程组并成为组长进程,脱离控制终端。

啦啦啦,明白为啥篇头那一坨枯燥的知识是为了什么吧?

然而,实际上,上述代码仅仅完成了一个标准 daemon 的 80%,还有 20% 需要我们进一步完善。那么,需要完善什么呢?我们修改一下上述代码,让程序在最终的代码段中执行一些文本输出:

$pid = pcntl_fork();
if( $pid < 0 ){
  exit('fork error.');
} else if( $pid > 0 ) {
  // 主进程退出
  exit();
}
// 子进程继续执行

// 最关键的一步来了,执行setsid函数!
if( !posix_setsid() ){
  exit('setsid error.');
}

// 理论上一次fork就可以了
// 但是,二次fork,这里的历史渊源是这样的:在基于system V的系统中,通过再次fork,父进程退出,子进程继续,保证形成的daemon进程绝对不会成为会话首进程,不会拥有控制终端。

$pid = pcntl_fork();
if( $pid  < 0 ){
  exit('fork error');
} else if( $pid > 0 ) {
  // 主进程退出
  exit;
}

// 子进程继续执行

// 啦啦啦,啦啦啦,啦啦啦,已经变成daemon啦,开心
cli_set_process_title('testtesttest');
// 循环1000次,每次睡眠1s,输出一个字符test
for( $i = 1; $i <= 1000; $i++ ){
  sleep( 1 );
  echo "test".PHP_EOL;
}

将文件保存为 daemon.php,然后 php daemon.php 执行文件,是不是有怪怪的现象,大概类似于下图:

即便你按 Ctrl+C 都没用,终端在不断输出 test,唯一办法就是关闭当前终端窗口然后重新开一个,然而,这并不符合社会主义主流价值观。所以,我们还要解决标准输出和错误输出,我们的 daemon 程序不可以再将终端窗口当作默认的标准输出了。

其次是将当前工作目录修改更改为根目录。不然可能就会出现下面这样一个问题,就是如果父进程是的工作目录是一个挂载的目录,那么子进程会继承父进程的工作目录,当子进程已经 daemon 化后就会出现一个悲剧:那就是虽然原来挂载的目录已经不用了,但是却无法用 umount 卸载,非常悲剧。

最后一个问题是,要在第一次 fork 后设置 umask(0),避免权限上的一些问题。所以较为完整的代码如下:

// 设置umask为0,这样,当前进程创建的文件权限则为777
umask( 0 );

$pid = pcntl_fork();
if( $pid < 0 ){
  exit('fork error.');
} else if( $pid > 0 ) {
  // 主进程退出
  exit();
}
// 子进程继续执行

// 最关键的一步来了,执行setsid函数!
if( !posix_setsid() ){
  exit('setsid error.');
}

// 理论上一次fork就可以了
// 但是,二次fork,这里的历史渊源是这样的:在基于system V的系统中,通过再次fork,父进程退出,子进程继续,保证形成的daemon进程绝对不会成为会话首进程,不会拥有控制终端。

$pid = pcntl_fork();
if( $pid  < 0 ){
  exit('fork error');
} else if( $pid > 0 ) {
  // 主进程退出
  exit;
}

子进程继续执行

已经变成 daemon 啦

cli_set_process_title('testtesttest');

一般服务器软件都有写配置项,比如以 debug 模式运行还是以 daemon 模式运行。如果以 debug 模式运行,那么标准输出和错误输出大多数都是直接输出到当前终端上,如果是 daemon 形式运行,那么错误输出和标准输出可能会被分别输出到两个不同的配置文件中去

连工作目录都是一个配置项目,通过 php 函数 chdir 可以修改当前工作目录

chdir( $dir );