ホームページ >ウェブフロントエンド >jsチュートリアル >PHP はデーモンを実装します - バックエンド
PHP 実装デーモンは、pcntl
および posix
拡張機能を通じて実装できます。
プログラミングで注意する必要があることは次のとおりです。
pcntl_fork()
および posix_setsid# を介してターミナルから離れるようにします。
SIGHUP シグナルを無視または処理します
または
pcntl_signal()
SIGCHLD シグナルを無視して、子プロセスがゾンビ プロセスになるのを防ぎます
は、実行中のプロセスの/dev/null
または他のストリームにリダイレクトします
#root から開始する場合は、実行時に低い特権のユーザー ID に変更してください
メモリ リークを防ぐためにマルチプロセス プログラムを定期的に再起動することを検討してください
マルチタスクのコンピューター オペレーティング システムでは、デーモン (英語: daemon、/ˈdiːmən/ または /ˈdeɪmən/) は次のとおりです。バックグラウンドで実行されるコンピューター プログラム。このようなプログラムはプロセスとして初期化されます。デーモン プログラムの名前は通常、文字「d」で終わります。たとえば、syslogd はシステム ログを管理するデーモンを指します。
通常、デーモン プロセスには既存の親プロセス (つまり、PPID=1) がなく、UNIX システム プロセス階層の init の直下にあります。デーモン プログラムは通常、子プロセスで fork を実行し、その後親プロセスを直ちに終了して、子プロセスが init で実行できるようにすることで、自分自身をデーモンにします。この方法は、「シェル処理」と呼ばれることがよくあります。
UNIX 環境での高度なプログラミング (第 2 版) (以下、APUE と呼びます) 第 13 章にクラウドがあります:
デーモン プロセスはエルフ プロセスにもなります(デーモン) はライフサイクルの長いプロセスです。多くの場合、これらはシステムの起動時に開始され、システムがシャットダウンされたときにのみ終了します。制御端末がないため、バックグラウンドで実行されると言われています。
ここで、デーモンには次の特徴があることに注意してください:
ターミナルなし
ps -ef を使用して表示できます。ここで
-x はリストに表示されることを意味します 端末を制御するプロセスはありません。
実装に関する懸念事項
pcntl
拡張機能は、PHP で新しいプロセスをフォークするために使用されるpcntl_fork() 関数を実装します。
setsid システム コール
セッション
、プロセス グループといういくつかの概念があります。
Linux では、ユーザーのログインによりセッションが生成されます。セッションには 1 つ以上のプロセス グループが含まれ、プロセス グループには複数のプロセスが含まれます。各プロセス グループにはセッション リーダーがあり、その pid はプロセス グループのグループ ID です。プロセス リーダーがターミナルを開くと、このターミナルは制御ターミナルと呼ばれます。制御端末で例外(切断、ハードウェアエラーなど)が発生すると、プロセスグループリーダーに信号が送信されます。
&
で終わるシェル実行命令など) も、端末が閉じられた後、つまり制御端末が切断されたときに発行されたSIGHUP 後に強制終了されます。 シグナルは適切に処理されず、プロセスの
SIGHUP シグナルのデフォルトの動作はプロセスを終了します。
Call
setsid
PHP の
posix
posix_setsid() 関数を実装します。
孤立プロセス
まず、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('process_a'); $pidA = pcntl_fork(); if ($pidA > 0) { exit(0); } else if ($pidA < 0) { exit(1); } cli_set_process_title('process_b'); if (-1 === posix_setsid()) { exit(2); } while(true) { sleep(1); }
プログラム実行後:
➜ ~ php56 2fork1.php ➜ ~ ps ax | grep -v grep | grep -E 'process_|PID' 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
の誤受信によるプロセスの終了を防ぐため、デーモン プログラミングの規則に従うためには、このシグナルも処理する必要があります。
簡単に言うと、子プロセスは親プロセスよりも先に終了し、親プロセスは wait を呼び出しません。
システムコールが処理され、プロセスはゾンビプロセスになります。
子プロセスが親プロセスより先に終了すると、SIGCHLD
シグナルが親プロセスに送信されます。親プロセスが処理しない場合、子プロセスもゾンビになります。プロセス。
ゾンビ プロセスは、フォークできるプロセスの数を占有します。ゾンビ プロセスが多すぎると、新しいプロセスをフォークできなくなります。
また、Linux システムでは、ppid が init プロセスであるプロセスは、Zombie になった後、init プロセスによって再利用され、管理されます。
ゾンビ プロセスの特性から、マルチプロセス デーモンの場合、この問題は 2 つの方法で解決できます。
SIGCHLD
Signal親プロセスの信号処理について詳しく説明する必要はありません。信号処理を登録するだけです。コールバック関数を呼び出して、リサイクルメソッドを呼び出します。
子プロセスを init によって引き継ぐには、fork メソッドを 2 回使用できます。これにより、最初の fork からの子プロセス a が実際に作業しているプロセス b をフォークアウトし、a を終了させることができます。まず、b が孤立プロセスになり、init プロセスでホストできるようにします。
umask は親プロセスから継承され、ファイルを作成する権限に影響します。
PHP マニュアルには次のように記載されています:
umask() は PHP の umask をマスク & 0777 に設定し、元の umask を返します。 PHP がサーバー モジュールとして使用されている場合、umask はリクエストのたびに復元されます。
親プロセスの umask が適切に設定されていない場合、一部のファイル操作を実行するときに予期しない影響が発生します。
➜ ~ cat test_umask.php <?php chdir('/tmp'); umask(0066); mkdir('test_umask', 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 は、それぞれ 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 13.3中提到过一条编程规则(第6条):
某些守护进程打开
/dev/null
时期具有文件描述符0、1和2,这样,任何一个视图读标准输入、写标准输出或者标准错误的库例程都不会产生任何效果。因为守护进程并不与终端设备相关联,所以不能在终端设备上显示器输出,也无从从交互式用户那里接受输入。及时守护进程是从交互式会话启动的,但因为守护进程是在后台运行的,所以登录会话的终止并不影响守护进程。如果其他用户在同一终端设备上登录,我们也不会在该终端上见到守护进程的输出,用户也不可期望他们在终端上的输入会由守护进程读取。
简单来说:
例程中使用:
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 中的 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 サイトの他の関連記事を参照してください。