ホームページ  >  記事  >  ウェブフロントエンド  >  PHP はデーモンを実装します - バックエンド

PHP はデーモンを実装します - バックエンド

coldplay.xixi
coldplay.xixi転載
2020-06-16 17:22:511571ブラウズ

PHP はデーモンを実装します - バックエンド

TL;DR

PHP 実装デーモンは、pcntl および posix 拡張機能を通じて実装できます。

プログラミングで注意する必要があることは次のとおりです。

  • メイン プロセスが 2 番目の pcntl_fork() および posix_setsid# を介してターミナルから離れるようにします。
  • Pass
  • pcntl_signal() SIGHUP シグナルを無視または処理します
  • マルチプロセス プログラムは 2 回渡す必要があります
  • pcntl_fork()または pcntl_signal() SIGCHLD シグナルを無視して、子プロセスがゾンビ プロセスになるのを防ぎます
  • umask()## を通じてファイル許可マスクを設定します# ファイル権限の継承を防ぐため、結果として生じる権限影響関数は、実行中のプロセスの
  • STDIN/STDOUT/STDERR
  • /dev/nullまたは他のストリームにリダイレクトします
  • より良い結果を出したい場合は、次の点にも注意する必要があります。

#root から開始する場合は、実行時に低い特権のユーザー ID に変更してください

    Timely
  • chdir()
  • 操作エラー パスの防止
  • メモリ リークを防ぐためにマルチプロセス プログラムを定期的に再起動することを検討してください
  • デーモンとは
記事の主人公デーモン (Wikipedia) 上記の定義は次のとおりです:

マルチタスクのコンピューター オペレーティング システムでは、デーモン (英語: daemon、/ˈdiːmən/ または /ˈdeɪmən/) は次のとおりです。バックグラウンドで実行されるコンピューター プログラム。このようなプログラムはプロセスとして初期化されます。デーモン プログラムの名前は通常、文字「d」で終わります。たとえば、syslogd はシステム ログを管理するデーモンを指します。

通常、デーモン プロセスには既存の親プロセス (つまり、PPID=1) がなく、UNIX システム プロセス階層の init の直下にあります。デーモン プログラムは通常、子プロセスで fork を実行し、その後親プロセスを直ちに終了して、子プロセスが init で実行できるようにすることで、自分自身をデーモンにします。この方法は、「シェル処理」と呼ばれることがよくあります。


UNIX 環境での高度なプログラミング (第 2 版) (以下、APUE と呼びます) 第 13 章にクラウドがあります:

デーモン プロセスはエルフ プロセスにもなります(デーモン) はライフサイクルの長いプロセスです。多くの場合、これらはシステムの起動時に開始され、システムがシャットダウンされたときにのみ終了します。制御端末がないため、バックグラウンドで実行されると言われています。

ここで、デーモンには次の特徴があることに注意してください:

ターミナルなし

    バックグラウンドで実行
  • 親プロセスpid は 1
  • 実行中のデーモン プロセスを表示したい場合は、
  • ps -ax
または

ps -ef を使用して表示できます。ここで -x はリストに表示されることを意味します 端末を制御するプロセスはありません。 実装に関する懸念事項

2 番目の fork と setid

fork システム コール

fork システム コールは、親プロセスとほぼ同一のプロセスをコピーするために使用されます。 process の場合、新しく生成された子プロセスと親プロセスの違いは、pid とメモリ空間が異なることです。コード ロジックの実装に応じて、親プロセスと子プロセスは同じ作業を完了することも、異なることもできます。 。子プロセスは、ファイル記述子などのリソースを親プロセスから継承します。

PHP の

pcntl

拡張機能は、PHP で新しいプロセスをフォークするために使用される

pcntl_fork() 関数を実装します。 setsid システム コール

setsid システム コールは、新しいセッションを作成し、プロセス グループ ID を設定するために使用されます。

ここには、

セッション

プロセス グループといういくつかの概念があります。 Linux では、ユーザーのログインによりセッションが生成されます。セッションには 1 つ以上のプロセス グループが含まれ、プロセス グループには複数のプロセスが含まれます。各プロセス グループにはセッション リーダーがあり、その pid はプロセス グループのグループ ID です。プロセス リーダーがターミナルを開くと、このターミナルは制御ターミナルと呼ばれます。制御端末で例外(切断、ハードウェアエラーなど)が発生すると、プロセスグループリーダーに信号が送信されます。

バックグラウンドで実行されているプログラム (

&

で終わるシェル実行命令など) も、端末が閉じられた後、つまり制御端末が切断されたときに発行された

SIGHUP 後に強制終了されます。 シグナルは適切に処理されず、プロセスの SIGHUP シグナルのデフォルトの動作はプロセスを終了します。 Call setsid

システムコール後、現在のプロセスは新しいプロセスグループを作成するように求められます。現在のプロセスでターミナルが開かれていない場合、制御ターミナルは存在しません。このプロセス グループの場合、ターミナルを閉じてプロセスを強制終了しても問題はありません。

PHP の posix

拡張機能は、PHP で新しいプロセス グループを設定するために使用される

posix_setsid() 関数を実装します。 孤立プロセス

親プロセスは子プロセスより先に終了し、子プロセスは孤立プロセスになります。

init プロセスは孤立プロセスを採用します。つまり、孤立プロセスの ppid は 1 になります。

2 番目のフォークの役割

まず、setsid システム コールはプロセス グループ リーダーから呼び出すことができず、-1 を返します。

2 番目のフォーク操作のサンプル コードは次のとおりです。

$pid1 = pcntl_fork();

if ($pid1 > 0) {
    exit(0);
} else if ($pid1 < 0) {
    exit("Failed to fork 1\n");
}

if (-1 == posix_setsid()) {
    exit("Failed to setsid\n");
}

$pid2 = pcntl_fork();

if ($pid2 > 0) {
    exit(0);
} else if ($pid2 < 0) {
    exit("Failed to fork 2\n");
}

ターミナルでアプリケーションを実行すると仮定します。プロセスは a で、最初のフォークは子プロセス b が生成されます。フォークが成功すると、親プロセス a が終了します。 b 孤立プロセスとして、init プロセスによってホストされます。

現時点では、プロセス b はプロセス グループ a に属しており、プロセス b は posix_setsid を呼び出して新しいプロセス グループの生成を要求します。呼び出しが成功すると、現在のプロセス グループは次のようになります。 b.

この時点で、プロセス b は実際には制御端末から切り離されています。ルーチン:

<?php

cli_set_process_title(&#39;process_a&#39;);

$pidA = pcntl_fork();

if ($pidA > 0) {
    exit(0);
} else if ($pidA < 0) {
    exit(1);
}

cli_set_process_title(&#39;process_b&#39;);

if (-1 === posix_setsid()) {
    exit(2);
}

while(true) {
    sleep(1);
}

プログラム実行後:

➜  ~ php56 2fork1.php
➜  ~ ps ax | grep -v grep | grep -E &#39;process_|PID&#39;
  PID TTY      STAT   TIME COMMAND
28203 ?        Ss     0:00 process_b

ps の結果からすると、process_b の TTY は になりましたか? 、つまり、対応する制御端子が存在しません。

コードがこの時点に到達すると、関数が完了したように見えます。ターミナルを閉じた後でも process_b は強制終了されていませんが、なぜ 2 回目の fork 操作があるのでしょうか?

StackOverflow の回答はよく書かれています:

2 番目の fork(2) は、新しいプロセスがセッション リーダーではないことを確認するためにあります。デーモンは制御端末を持つことは想定されていないため、(偶然に) 制御端末を割り当てます。

これは、実際の作業プロセスが制御端末に積極的に関連付けられたり、誤って関連付けられたりすることを防ぐためです。フォーク後に生成された新しいプロセスは、プロセス グループのリーダーではないため、関連付けられた制御端末に適用できません。

要約すると、セカンダリ フォークと setid の機能は、新しいプロセス グループを生成し、作業プロセスが制御端末に関連付けられるのを防ぐことです。

SIGHUP シグナルの処理

SIGHUP シグナルを受信したプロセスのデフォルトのアクションは、プロセスを終了することです。

そして SIGHUP は次の状況で発行されます。

  • 制御端末が切断され、SIGHUP がプロセス グループ リーダーに送信されます
  • プロセス グループ グループ リーダーが終了すると、プロセス グループのフォアグラウンド プロセスに SIGHUP が送信されます。
  • SIGHUP は、プロセスに設定ファイルをリロードするように通知するためによく使用されます (APUE で述べたように、デーモンは考慮されます)制御端子がないため受信する可能性は低い) これは信号であるため、再利用することにします)

実際に作業しているプロセスはフォアグラウンド プロセス グループに含まれておらず、リーダーがプロセスグループのプロセスが終了しており、端末を制御していないため、通常であれば当然処理は行われませんが、問題は、SIGHUPの誤受信によるプロセスの終了を防ぐため、デーモン プログラミングの規則に従うためには、このシグナルも処理する必要があります。

ゾンビ プロセスの処理

ゾンビ プロセスとは

簡単に言うと、子プロセスは親プロセスよりも先に終了し、親プロセスは wait を呼び出しません。 システムコールが処理され、プロセスはゾンビプロセスになります。

子プロセスが親プロセスより先に終了すると、SIGCHLD シグナルが親プロセスに送信されます。親プロセスが処理しない場合、子プロセスもゾンビになります。プロセス。

ゾンビ プロセスは、フォークできるプロセスの数を占有します。ゾンビ プロセスが多すぎると、新しいプロセスをフォークできなくなります。

また、Linux システムでは、ppid が init プロセスであるプロセスは、Zombie になった後、init プロセスによって再利用され、管理されます。

ゾンビ プロセスの処理

ゾンビ プロセスの特性から、マルチプロセス デーモンの場合、この問題は 2 つの方法で解決できます。

  • 親プロセスの処理SIGCHLD Signal
  • 子プロセスを init に引き継がせる

親プロセスの信号処理について詳しく説明する必要はありません。信号処理を登録するだけです。コールバック関数を呼び出して、リサイクルメソッドを呼び出します。

子プロセスを init によって引き継ぐには、fork メソッドを 2 回使用できます。これにより、最初の fork からの子プロセス a が実際に作業しているプロセス b をフォークアウトし、a を終了させることができます。まず、b が孤立プロセスになり、init プロセスでホストできるようにします。

umask

umask は親プロセスから継承され、ファイルを作成する権限に影響します。

PHP マニュアルには次のように記載されています:

umask() は PHP の umask をマスク & 0777 に設定し、元の umask を返します。 PHP がサーバー モジュールとして使用されている場合、umask はリクエストのたびに復元されます。

親プロセスの umask が適切に設定されていない場合、一部のファイル操作を実行するときに予期しない影響が発生します。

➜  ~ cat test_umask.php
<?php
        chdir(&#39;/tmp&#39;);
        umask(0066);
        mkdir(&#39;test_umask&#39;, 0777);
➜  ~ php test_umask.php
➜  ~ ll /tmp | grep umask
drwx--x--x 2 root root 4.0K 8月  22 17:35 test_umask

したがって、毎回、期待される権限に従ってファイルを操作できるようにするには、umask 値を 0 に設定する必要があります。

リダイレクト 0/1/2

ここでの 0/1/2 は、それぞれ STDIN/STDOUT/STDERR、つまり標準入力/出力/エラー 3 を指します。流れ。

サンプル

まずサンプルを見てみましょう:

<?php

// not_redirect_std_stream_daemon.php

$pid1 = pcntl_fork();

if ($pid1 > 0) {
    exit(0);
} else if ($pid1 < 0) {
    exit("Failed to fork 1\n");
}

if (-1 == posix_setsid()) {
    exit("Failed to setsid\n");
}

$pid2 = pcntl_fork();

if ($pid2 > 0) {
    exit(0);
} else if ($pid2 < 0) {
    exit("Failed to fork 2\n");
}

umask(0);
declare(ticks = 1);
pcntl_signal(SIGHUP, SIG_IGN);

echo getmypid() . "\n";

while(true) {
    echo time() . "\n";
    sleep(10);
}

上記のコードは、記事の冒頭で述べたすべての側面をほぼ完成させています。唯一の違いは、標準ストリームが処理されないことです。このプログラムは、php not_redirect_std_stream_daemon.php ディレクティブを使用してバックグラウンドで実行することもできます。

sleep 的间隙,关闭终端,会发现进程退出。

通过 strace 观察系统调用的情况:

➜  ~ strace -p 6723
Process 6723 attached - interrupt to quit
restart_syscall(<... resuming interrupted call ...>) = 0
write(1, "1503417004\n", 11)            = 11
rt_sigprocmask(SIG_BLOCK, [CHLD], [], 8) = 0
rt_sigaction(SIGCHLD, NULL, {SIG_DFL, [], 0}, 8) = 0
rt_sigprocmask(SIG_SETMASK, [], NULL, 8) = 0
nanosleep({10, 0}, 0x7fff71a30ec0)      = 0
write(1, "1503417014\n", 11)            = -1 EIO (Input/output error)
close(2)                                = 0
close(1)                                = 0
munmap(0x7f35abf59000, 4096)            = 0
close(0)                                = 0

   

发现发生了 EIO 错误,导致进程退出。

原因很简单,即我们编写的 daemon 程序使用了当时启动时终端提供的标准流,当终端关闭时,标准流变得不可读不可写,一旦尝试读写,会导致进程退出。

在信海龙的博文《一个echo引起的进程崩溃》中也提到过类似的问题。

解决方案

APUE 样例

APUE 13.3中提到过一条编程规则(第6条):

某些守护进程打开 /dev/null 时期具有文件描述符0、1和2,这样,任何一个视图读标准输入、写标准输出或者标准错误的库例程都不会产生任何效果。因为守护进程并不与终端设备相关联,所以不能在终端设备上显示器输出,也无从从交互式用户那里接受输入。及时守护进程是从交互式会话启动的,但因为守护进程是在后台运行的,所以登录会话的终止并不影响守护进程。如果其他用户在同一终端设备上登录,我们也不会在该终端上见到守护进程的输出,用户也不可期望他们在终端上的输入会由守护进程读取。

简单来说:

  • daemon 不应使用标准流
  • 0/1/2 要设定成 /dev/null

例程中使用:

for (i = 0; i < rl.rlim_max; i++)
	close(i);

fd0 = open("/dev/null", O_RDWR);
fd1 = dup(0);
fd2 = dup(0);

   

实现了这一个功能。dup() (参考手册)系统调用会复制输入参数中的文件描述符,并复制到最小的未分配文件描述符上。所以上述例程可以理解为:

关闭所有可以打开的文件描述符,包括标准输入输出错误;
打开/dev/null并赋值给变量fd0,因为标准输入已经关闭了,所以/dev/null会绑定到0,即标准输入;
因为最小未分配文件描述符为1,复制文件描述符0到文件描述符1,即标准输出也绑定到/dev/null;
因为最小未分配文件描述符为2,复制文件描述符0到文件描述符2,即标准错误也绑定到/dev/null;

   

开源项目实现:Workerman

Workerman 中的 Worker.php 中的 resetStd() 方法实现了类似的操作。

/**
* Redirect standard input and output.
*
* @throws Exception
*/
public static function resetStd()
{
   if (!self::$daemonize) {
       return;
   }
   global $STDOUT, $STDERR;
   $handle = fopen(self::$stdoutFile, "a");
   if ($handle) {
       unset($handle);
       @fclose(STDOUT);
       @fclose(STDERR);
       $STDOUT = fopen(self::$stdoutFile, "a");
       $STDERR = fopen(self::$stdoutFile, "a");
   } else {
       throw new Exception('can not open stdoutFile ' . self::$stdoutFile);
   }
}

   

Workerman 中如此实现,结合博文,可能与 PHP 的 GC 机制有关,对于 fd 0 1 2来说,PHP 会维持对这三个资源的引用计数,在直接 fclose 之后,会使得这几个 fd 对应的资源类型的变量引用计数为0,导致触发回收。所需要做的就是将这些变量变为全局变量,保证引用的存在。

推荐教程:《php教程

以上がPHP はデーモンを実装します - バックエンドの詳細内容です。詳細については、PHP 中国語 Web サイトの他の関連記事を参照してください。

声明:
この記事はjuejin.imで複製されています。侵害がある場合は、admin@php.cn までご連絡ください。