デーモンはバックグラウンドで実行される特別なプロセスであり、特定のシステム タスクを実行するために使用されます。この記事では、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 で実行できるようにすることで、自分自身をデーモンにします。この方法は、「シェル処理」と呼ばれることがよくあります。(以下、APUE と呼びます) 第 13 章にクラウドがあります:UNIX 環境における高度なプログラミング (第 2 版)
デーモン プロセスデーモンプロセスはライフサイクルの長いプロセスです。多くの場合、これらはシステムの起動時に開始され、システムがシャットダウンされたときにのみ終了します。制御端末がないため、バックグラウンドで実行されると言われています。
ここで、デーモンには次の特徴があることに注意してください:
ターミナルなし
ps -ef を使用して表示できます。ここで
-x はリストに表示されることを意味します 端末を制御するプロセスはありません。
実装に関する問題
fork システム コール
fork
システムコールは、親プロセスとほぼ同一のプロセスをコピーするために使用されます。新しく生成された子プロセスと親プロセスの違いは、親プロセスが異なることです。 pid と異なるメモリ空間のコード ロジック実装に従って、親プロセスと子プロセスは同じ作業または異なるタスクを完了できます。子プロセスは、ファイル記述子などのリソースを親プロセスから継承します。PHP の pcntl
拡張機能は、PHP で新しいプロセスをフォークするために使用されるpcntl_fork() 関数を実装します。
setsid システム コール
setsid
システム コールは、新しいセッションを作成し、プロセス グループ ID を設定するために使用されます。ここには、セッション
、プロセス グループといういくつかの概念があります。
Linux では、ユーザーのログインによりセッションが生成されます。セッションには 1 つ以上のプロセス グループが含まれ、プロセス グループには複数のプロセスが含まれます。各プロセス グループにはセッション リーダーがあり、その pid はプロセス グループのグループ ID です。プロセス リーダーがターミナルを開くと、このターミナルは制御ターミナルと呼ばれます。制御端末で例外(切断、ハードウェアエラーなど)が発生すると、プロセスグループリーダーに信号が送信されます。
&
で終わるシェル実行命令など) も、端末が閉じられた後、つまり制御端末が切断されたときに発行されたSIGHUP 後に強制終了されます。 シグナルは適切に処理されず、プロセスの
SIGHUP シグナルのデフォルトの動作はプロセスを終了します。
Callsetsid
システム コールの後、現在のプロセスは新しいプロセス グループを作成します。現在のプロセスで端末が開かれていない場合、このプロセス グループには制御端末は存在しません。制御端末が存在しないため、端末を閉じるとプロセスが強制終了されるという問題が発生します。
PHP の posix
拡張機能は、PHP で新しいプロセス グループを設定するために使用される posix_setsid()
関数を実装します。
孤立プロセス
親プロセスは子プロセスより先に終了し、子プロセスは孤立プロセスになります。
init プロセスは孤立プロセスを採用します。つまり、孤立プロセスの ppid は 1 になります。
セカンダリフォークの役割
まず、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 になりましたか?
、つまり、対応する制御端子が存在しません。
コードがこの時点に到達すると、関数が完了したように見えます。ターミナルを閉じた後でも process_b は強制終了されていませんが、なぜ 2 回目の fork 操作があるのでしょうか?
StackOverflow の の回答はよく書かれています:
2 番目の fork(2) は、新しいプロセスがセッション リーダーではないことを確認するためにあります。デーモンは制御端末を持つことは想定されていないため、(誤って) 制御端末を割り当てることはできません。
これは、実際の作業プロセスがアクティブに関連付けられたり、コントロールに関連付けられたりするのを防ぐためです。端末が誤って関連付けられました。再度フォークして生成された新しいプロセスは、プロセス グループ リーダーではないため、制御端末との関連付けを申請できません。
要約すると、セカンダリ フォークと setid の機能は、新しいプロセス グループを生成し、作業プロセスが制御端末に関連付けられるのを防ぐことです。
SIGHUP シグナルの処理
SIGHUP
シグナルを受信したプロセスのデフォルトのアクションは、プロセスを終了することです。
そして SIGHUP
は次の状況で発行されます。
実際に作業しているプロセスはフォアグラウンド プロセス グループに含まれておらず、リーダーがプロセスグループのプロセスが終了しており、端末を制御していないため、通常であれば当然処理は行われませんが、問題は、SIGHUP
の誤受信によるプロセスの終了を防ぐため、デーモン プログラミングの規則に従うためには、このシグナルも処理する必要があります。
ゾンビ プロセスの処理
ゾンビ プロセスとは
簡単に言えば、子プロセスです。 first 親プロセスが終了すると、親プロセスは wait
システムコール処理を呼び出さず、ゾンビプロセスとなります。
子プロセスが親プロセスより先に終了すると、SIGCHLD
シグナルが親プロセスに送信されます。親プロセスが処理しない場合、子プロセスもゾンビになります。プロセス。
ゾンビ プロセスは、フォークできるプロセスの数を占有します。ゾンビ プロセスが多すぎると、新しいプロセスをフォークできなくなります。
また、Linux システムでは、ppid が init プロセスであるプロセスは、Zombie になった後、init プロセスによってリサイクルされ、管理されます。
ゾンビ プロセスの処理
ゾンビ プロセスの特性から、マルチプロセス デーモンの場合、この問題は 2 つの方法で解決できます。
親プロセスの処理
init に子プロセスを引き継がせる 子プロセスを init によって引き継ぐには、fork メソッドを 2 回使用できます。これにより、最初の fork からの子プロセス a が実際に作業しているプロセス b をフォークアウトし、a を終了させることができます。まず、b が孤立プロセスになり、init プロセスでホストできるようにします。
umaskumask は親プロセスから継承され、ファイルを作成する権限に影響します。
PHP
Manualumask() は、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
所以,为了保证每一次都能按照预期的权限操作文件,需要置0 umask 值。
重定向0/1/2
这里的0/1/2分别指的是 STDIN/STDOUT/STDERR
,即标准输入/输出/错误三个流。
样例
首先来看一个样例:
ログイン後にコピー
上述代码几乎完成了文章最开始部分提及的各个方面,唯一不同的是没有对标准流做处理。通过 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,这样,任何一个视图读标准输入、写标准输出或者标准错误的库例程都不会产生任何效果。因为守护进程并不与终端设备相关联,所以不能在终端设备上显示器输出,也无从从交互式用户那里接受输入。及时守护进程是从交互式会话启动的,但因为守护进程是在后台运行的,所以登录会话的终止并不影响守护进程。如果其他用户在同一终端设备上登录,我们也不会在该终端上见到守护进程的输出,用户也不可期望他们在终端上的输入会由守护进程读取。
简单来说:
例程中使用:
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 サイトの他の関連記事を参照してください。