PHP多进程开发[快问快答系列]

PHP技术
368
0
0
2022-12-23

介绍一些简单命令

echo $$ //输出当前bash进程
strace -s 65500 -p 进程号    //打印进程系统调用
kill -s 10 pid //发送信号
kill -s SIGUSR2 pid //发送信号
pstree -ap //查看进程树
ps -ajx //查看进程信息
ps 命令字段解析:
PPID:父进程ID
PID:进程ID
PGID:进程组ID
SID:会话ID
TTY:所在终端
STAT:进程状态 R运行 Z僵尸 S睡眠 T停止 D睡眠无法被唤醒
UID:unix用户ID
COMMAND:启动命令

什么是程序?

一般指可执行文件,在Linux系统中它按ELF格式进行存储,没有后缀可言, file命令可以查看elf文件的具体类型

ELF全程Executable Linkable Format 可执行可链接格式

ELF分为四大种类

  • EXEC 可执行文件
  • REL 可重定位文件,也称为静态库文件,链接器链接之后成为动态库文件,比如event.so sockets.so curl.so
  • Shared Object File 共享目标文件
  • core dump 存储进程产生的异常信息
  • 可通过objdump/readelf 命令查看ELF文件相关信息

什么是终端?

  • tty 物理终端
  • tty是最令人熟悉的了,在Linux中,/dev/ttyX代表的都是上述的物理终端,其中,/dev/tty1~/dev/tty63代表的是本地终端,也就是接到本机的键盘显示器可以操作的终端
  • /dev/console 当前焦点终端
  • pts 伪终端
  • 通过tcp/ip协议实现的终端,比如用SSH进行的登录,或者telnet, 那么你将得到一个叫做/dev/pts/X的伪终端同时在
  • /proc/bash pid/fd 生成三个标识符指向当前的/dev/pts/X
  • 0 标准输入 鼠标,键盘
  • 1 标准输出 显示器
  • 2 标准错误 显示器
  • PHPMultiple processesNOTE

什么是进程?

进程退出

  1. 运行到最后一行语句
  2. 运行时遇到return 时
  3. 运行时遇到exit()函数的时候
  4. 程序异常的时候
  5. 进程接收到中断信号

进程结束时并不会真的退出,还会驻留在内在中,pcntl_wait(pcntl_waitpid)函数来获取进程的终止状态码同时该函数会释放终止进程的内存空间,如果不这么做,会产生很多僵尸进程占用大量的内存空间

孤儿进程

父进程运行完,子进程在运行,则子进程会被头号进程init接管,这类型的进程成为孤儿进程

僵尸进程

子进程运行完,父进程没有调用pcntl_wait()回收,进程状态变成Z+

守护进程

父进程是init进程,一般在系统启动时开始运行,除非强行终止,否则直到系统关机都保持运行。守护进程经常以超级用户(root)权限运行,因为它们要使用特殊的端口(1-1024)或访问某些特殊的资源。

什么是进程组?

多个进程组成一个进程组,每个进程组只有一个组长,组长的PID就是进程组的ID;组内所有进程退出时,进程组才会消失,可以通过ps -ajx 命令查看 pgid

PHP多进程开发[快问快答系列]

什么是会话?

多个进程组组成一个会话,每个会话都有一个会话首进程。会话的特点

1) 使用setsid()函数可以创建一个新的会话

2) 会话首进程无法调用setsid,会报错

3) 非会话首进程进程可调用setsid创建出一个新的会话,这个行为会导致该进程会创建一个新的进程组且自身为该进程组组长,该进程会创建出一个新的会话组且自身为该会话组组长,该进程会脱离当前命令行控制终端

现实上的比喻就是除了老板之后,员工都可以调用 我上我也行() 这个函数变成老板且不受原公司的控制

什么是信号?

信号是进程间通信的其中一种方式,平时用到的kill -9 pid,指的不是用第九种方式杀死进程,而是发送信号值为9的信号给进程,而刚好信号9是SIGKILL,功能是停止进程,查看操作系统支持的信号命令: kill -l

PHP多进程开发[快问快答系列]

一般使用1-31, 注意看没有32,33这两个信号

信号的产生来源可能是:

  • 键盘上按了Ctrl+C会产生SIGINT信号,关闭终端会产生SIGHUP信号
  • 硬件也能产生信号
  • 使用kill命令
  • 软件产生,比如在管道里当一侧准备写管道时可能会产生SIGPIPE信号

当一个进程收到一个信号时,三个可选操作

  • 操作系统默认的方式,比如SIGKILL就是杀死进程
  • 忽略掉这个信号,pcntl_signal(SIGKILL, SIG_IGN, false);进程收到SIGKILL命令时将不为所动
  • 有自己的想法,pcntl_signal(SIGKILL, function($signal){//自己的想法}, false); 这样将会触发自定义回调

pcntl_signal() 信号处理器是会被子进程继承的,所以fork()之前最后先行处理信号处理器

posix 命令

//需要安装posix扩展
posix_getpid();    //获取进程ID
posix_getppid();//获取父进程ID
posix_getpgid(posix_getppid());//获取进程组ID
posix_getpgrp());//同上
posix_getsid(posix_getpid()));//获取会话ID
posix_getuid();//获取当前登录用户UID
posix_getgid();//获取当前登录用户组UID
posix_geteuid();//获取当前有效用户UID
posix_getguid();//获取当前有效用户组UID
posix_kill();//发送信号

pcntl 命令

//创建一个计时器,在指定的秒数后向进程发送一个SIGALRM信号。每次对 pcntl_alarm()的调用都会取消之前设置的alarm信号。如果seconds设置为0,将不会创建alarm信号。 
pcntl_alarm(int $seconds);
//在当前进程当前位置产生子进程,子进程会复制父进程的代码段和数据段(Copy on write 写时复制,当子进程要修改内存空间时,操作系统会分配新的内存给子进程),ELF文件的结构,如果父进程先退出,子进程变成孤儿进程,被pid=1进程接管
pcntl_fork();
//安装一个信号处理器
pcntl_signal(int $signo, callback $handler);
//调用等待信号的处理器,触发全部未执行的信号回调
pcntl_signal_dispatch()
//设置或检索阻塞信号
pcntl_sigprocmask(int $how, array $set[, array &$oldset])
//等待或返回fork的子进程状态,wait函数挂起当前进程的执行直到一个子进程退出或接收到一个信号要求中断当前进程或调用一个信号处理函数。用此函数时已经退出(俗称僵尸进程),此函数立刻返回。子进程使用的所有系统资源将被释放。
pcntl_wait($status)
//加个WNOHANG参数,不挂起父进程,如果没有子进程退出返回0,如果有子进程退出返回子进程pid,如果返回-1表示父进程已经没有子进程
pcntl_wait($status, WNOHANG)
//基本同pcntl_wait,waitpid可以指定子进程id
pcntl_waitpid ($pid ,$status)
pcntl_waitpid ($pid ,$status, WNOHANG)
//检查状态代码是否代表一个正常的退出。参数 status 是提供给成功调用 pcntl_waitpid() 时的状态参数。
pcntl_wifexited($status)
//返回一个中断的子进程的返回代码  当php exit(10)时,这个函数返回10,这个函数仅在函数pcntl_wifexited()返回 TRUE.时有效
pcntl_wexitstatus($status)
//检查子进程状态码是否代表由于某个信号而中断。参数 status 是提供给成功调用 pcntl_waitpid() 时的状态参数。
pcntl_wifsignaled($status)
//返回导致子进程中断的信号
pcntl_wtermsig($status)
//检查子进程当前是否已经停止,此函数只有作用于pcntl_wait使用了WUNTRACED作为 option的时候
pcntl_wifstopped($status)
//返回导致子进程停止的信号
pcntl_wstopsig($status)
//检索由最后一个失败的pcntl函数设置的错误数
pcntl_errno() 
pcntl_get_last_error()
//检索与给定errno关联的系统错误消息
pcntl_strerror(pcntl_errno())

pcntl_fork()执行之前先与Redis建立一个连接,然后再开3个子进程之后多少个Redis连接?

<?php
$o_redis = new Redis();
$o_redis->connect( '127.0.0.1', 6379 );
// 使用for循环搞出3个子进程来
for ( $i = 1; $i <= 3; $i++ ) {
  $i_pid = pcntl_fork();
  if ( 0 == $i_pid ) {
    // 使用while保证三个子进程不会退出... 
    while( true ) {
      sleep( 1 );
    }
  }
}
// 使用while保证主进程不会退出...
while( true ) { 
  sleep( 1 );
}
netstat -ant |grep 6379

PHP多进程开发[快问快答系列]

说明父进程和三个子进程一共四个进程,实际上共享了一个Redis长连接

上面这种写法会有什么问题?

因为Redis是一个单进程单线程的服务器,所以接收到的命令都是顺序执行顺序返回的,所以当客户端多个进程共享一个redis连接时,当有四个进程向Redis服务端发起请求,返回四个结果,谁先抢到就是谁的,正确的做法是每个子进程创建一个Redis连接,或者用连接池

孤儿进程怎么产生?

$i_pid = pcntl_fork();
if (0 == $i_pid) {
    // 子进程10秒钟后退出.
    for ($i = 1; $i <= 10; $i++) {
        sleep(1);
        echo "我的父进程是:" . posix_getppid() . PHP_EOL;
    }
} else if ($i_pid > 0) {
    // 父进程休眠2s后退出.
    sleep(2);
}

僵尸进程怎么产生?

$i_pid = pcntl_fork();
if (0 == $i_pid) {
    // 子进程10s后退出,变成僵尸进程
    sleep(10);
} else if ($i_pid > 0) {
    // 父进程休眠1000s后退出.
    sleep(1000);
}

子进程怎么回收?

$i_pid = pcntl_fork();
if (0 == $i_pid) {
    // 在子进程中
    for ($i = 1; $i <= 10; $i++) {
        sleep(1);
        echo "子进程PID " . posix_getpid() . "倒计时 : " . $i . PHP_EOL;
    }
} else if ($i_pid > 0) {
    $i_ret = pcntl_wait($status);
    echo $i_ret . ' : ' . $status . PHP_EOL;
    // while保持父进程不退出
    while (true) {
        sleep(1);
    }
}

子进程怎么回收? 非阻塞版本

<?php
// fork出十个子进程
for ($i = 1; $i <= 10; $i++) {
    $i_pid = pcntl_fork();
    // 每个子进程随机运行1-5秒钟 
    if (0 == $i_pid) {
        $i_rand_time = mt_rand(1, 5);
        sleep($i_rand_time);
        exit;
    } // 父进程收集所有子进程PID 
    else if ($i_pid > 0) {

    }
}
while (true) {
    // sleep使父进程不会因while导致CPU爆炸. 
    sleep(1);
    //设置WNOHANG参数不会阻塞,就是需要外层包个循环 
    $pid = pcntl_wait($status, WNOHANG);
    if ($pid == 0) {   //目前还没有结束的子进程 
        continue;
    }
    if ($pid == -1) { //已经结束啦 很蓝的啦 
        exit("所有进程均已终止" . PHP_EOL);
    }
    // 如果子进程是正常结束 
    if (pcntl_wifexited($status)) {
        // 获取子进程结束时候的 返回错误码 
        $i_code = pcntl_wexitstatus($status);
        echo $pid . "正常结束,最终返回:" . $i_code . PHP_EOL;
    }
    // 如果子进程是被信号终止 
    if (pcntl_wifsignaled($status)) {
        // 获取是哪个信号终止的该进程 
        $i_signal = pcntl_wtermsig($status);
        echo $pid . "由信号结束,信号为:" . $i_signal . PHP_EOL;
    }
    // 如果子进程是[临时挂起] 
    if (pcntl_wifstopped($status)) {
        // 获取是哪个信号让他挂起 
        $i_signal = pcntl_wstopsig($status);
        echo $pid . "被挂起,挂起信号为:" . $i_signal . PHP_EOL;
    }
}

如何创建守护进程?

$pid = pcntl_fork();
if ($pid > 0) { //1)在父进程中执行forkexit推出
    exit();
} elseif ($pid == 0) {
    if (posix_setsid() < 0) {   //2)在子进程中调用setsid函数创建新的会话
        exit();
    }
    chdir('/'); //3)在子进程中调用chdir函数,让根目录 ” / ” 成为子进程的工作目录
    umask(0);   //4)在子进程中调用umask函数,设置进程的umask0
    echo "create success, pid = " . posix_getpid();
    //5)在子进程中关闭任何不需要的文件描述符
    fclose(STDIN);
    fclose(STDOUT);
    fclose(STDERR);
}
//可以把上面封装成函数daemon();
while (true) {} //具体业务

如何修改进程名?

for ($i = 1; $i <= 4; $i++) {
    $i_pid = pcntl_fork();
    if (0 == $i_pid) { //子进程
        cli_set_process_title("Worker Process"); //修改子进程的名字
        while (true) {
            sleep(1);
        }
    }
}
cli_set_process_title("Master Process");    //修改父进程的名字
while (true) {
    sleep(1);
}

PHP多进程开发[快问快答系列]

进程怎么接收信号?

// 信号处理回调
function signal_handler($signal)
{
    switch ($signal) {
        case SIGTERM:
            echo "sigterm信号." . PHP_EOL;
            break;
        case SIGUSR2:
            echo "sigusr2信号." . PHP_EOL;
            break;
        case SIGUSR1:
            echo "sigusr1信号." . PHP_EOL;
            break;
        default:
            echo "其他信号." . PHP_EOL;
    }
}
// 给进程安装3个信号处理回调
pcntl_signal(SIGTERM, "signal_handler");
pcntl_signal(SIGUSR1, "signal_handler");
pcntl_signal(SIGUSR2, "signal_handler");
while (true) {
    posix_kill(posix_getpid(), SIGUSR1);//发送一个信号给当前进程 
    posix_kill(posix_getpid(), SIGUSR1);
    pcntl_signal_dispatch(); //调一次分发一次信号,调用之前,信号累积在队列里 
    posix_kill(posix_getpid(), SIGUSR2);
    posix_kill(posix_getpid(), SIGUSR2);
    sleep(1);   //稍微休息一下
}

PHP多进程开发[快问快答系列]

其中第1,2行与第3,4,5,6行中间隔了一秒,体会一下pcntl_signal_dispatch这个函数

进程怎么接收信号(不阻塞版本)?

//php7.1及以上才能用这个函数
pcntl_async_signals(true);
// 信号处理回调
function signal_handler($signal)
{
    switch ($signal) {
        case SIGTERM:
            echo "sigterm信号." . PHP_EOL;
            break;
        case SIGUSR2:
            echo "sigusr2信号." . PHP_EOL;
            break;
        case SIGUSR1:
            echo "sigusr1信号." . PHP_EOL;
            break;
        default:
            echo "其他信号." . PHP_EOL;
    }
}
// 给进程安装信号...
pcntl_signal(SIGTERM, "signal_handler");
pcntl_signal(SIGUSR1, "signal_handler");
pcntl_signal(SIGUSR2, "signal_handler");
while (true) {
    posix_kill(posix_getpid(), SIGUSR1);//发送一个信号给当前进程 
    posix_kill(posix_getpid(), SIGUSR2);
    sleep(1);   //稍微休息一下
}

进程怎么阻塞信号

pcntl_async_signals(true);
// 信号处理回调
function signal_handler($signal)
{
    switch ($signal) {
        case SIGTERM:
            echo "sigterm信号." . PHP_EOL;
            break;
        case SIGUSR2:
            echo "sigusr2信号." . PHP_EOL;
            break;
        case SIGUSR1:
            echo "sigusr1信号." . PHP_EOL;
            break;
        default:
            echo "其他信号." . PHP_EOL;
    }
}
// 给进程安装信号...
pcntl_signal(SIGTERM, "signal_handler");
pcntl_signal(SIGUSR1, "signal_handler");
pcntl_signal(SIGUSR2, "signal_handler");
//把SIGUSR1阻塞,收到这个信号先不处理
pcntl_sigprocmask(SIG_BLOCK, [SIGUSR1], $a_oldset);
$counter = 0;
while (true) {
    posix_kill(posix_getpid(), SIGUSR1);//发送一个信号给当前进程 
    posix_kill(posix_getpid(), SIGUSR2);
    sleep(1);   //稍微休息一下 
    if ($counter++ == 5) {
        //解除SIGUSR1信号阻塞,并立刻执行SIGUSR1处理回调函数 
        pcntl_sigprocmask(SIG_UNBLOCK, [SIGUSR1], $a_oldset);
    }
}