ホームページ  >  記事  >  バックエンド開発  >  デーモンとは何ですか? PHPでデーモンを実装するにはどうすればよいですか?

デーモンとは何ですか? PHPでデーモンを実装するにはどうすればよいですか?

青灯夜游
青灯夜游転載
2021-06-23 20:30:302748ブラウズ

デーモンはバックグラウンドで実行される特別なプロセスであり、特定のシステム タスクを実行するために使用されます。この記事では、PHPでデーモンを実装する方法と、プログラミングで注意すべき点を紹介します。

デーモンとは何ですか? PHPでデーモンを実装するにはどうすればよいですか?

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

システムコールは、親プロセスとほぼ同一のプロセスをコピーするために使用されます。新しく生成された子プロセスと親プロセスの違いは、親プロセスが異なることです。 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(&#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 になりましたか? 、つまり、対応する制御端子が存在しません。

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

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

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

これは、実際の作業プロセスがアクティブに関連付けられたり、コントロールに関連付けられたりするのを防ぐためです。端末が誤って関連付けられました。再度フォークして生成された新しいプロセスは、プロセス グループ リーダーではないため、制御端末との関連付けを申請できません。

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

SIGHUP シグナルの処理

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

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

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

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

ゾンビ プロセスの処理

ゾンビ プロセスとは

簡単に言えば、子プロセスです。 first 親プロセスが終了すると、親プロセスは wait システムコール処理を呼び出さず、ゾンビプロセスとなります。

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

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

また、Linux システムでは、ppid が init プロセスであるプロセスは、Zombie になった後、init プロセスによってリサイクルされ、管理されます。

ゾンビ プロセスの処理

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

親プロセスの処理
    SIGCHLD
  • シグナル init に子プロセスを引き継がせる
  • 親プロセスの処理シグナル シグナル処理を登録するのは言うまでもありませんコールバック関数とRecyclingメソッドの呼び出しだけで十分です。

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

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

PHP

Manual

で言及されている:

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

所以,为了保证每一次都能按照预期的权限操作文件,需要置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,这样,任何一个视图读标准输入、写标准输出或者标准错误的库例程都不会产生任何效果。因为守护进程并不与终端设备相关联,所以不能在终端设备上显示器输出,也无从从交互式用户那里接受输入。及时守护进程是从交互式会话启动的,但因为守护进程是在后台运行的,所以登录会话的终止并不影响守护进程。如果其他用户在同一终端设备上登录,我们也不会在该终端上见到守护进程的输出,用户也不可期望他们在终端上的输入会由守护进程读取。

简单来说:

  • 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(&#39;can not open stdoutFile &#39; . self::$stdoutFile);
   }
}

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

推荐学习:《PHP视频教程

以上がデーモンとは何ですか? PHPでデーモンを実装するにはどうすればよいですか?の詳細内容です。詳細については、PHP 中国語 Web サイトの他の関連記事を参照してください。

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