从php语言理解多进程编程

从php语言理解多进程编程

以下内容全部在 linux 下执行,过程中会穿插少量的 windows 下的介绍,如果有时间建议阅读《深入理解计算机系统》《Unix环境高级编程》

多进程简介

本节内容转载自文章 对于 windows 来说,进程和线程的概念都是有着明确定义的,进程的概念对应于一个程序的运行实例(instance),而线程则是程序代码执行的最小单元。也就是说 windows 对于进程和线程的定义是与经典OS课程中所教授的进程、线程概念相一致的。

提供 API,CreateThread() 用于建立一个新的线程,传递线程函数的入口地址和调用参数给新建的线程,然后新线程就开始执行了。

windows下,一个典型的线程拥有自己的堆栈、寄存器(包括程序计数器PC,用于指向下一条应该执行的指令在内存中的位置),而代码段、数据段、打开文件这些进程级资源是同一进程内多个线程所共享的。因此同一进程的不同线程可以很方便的通过全局变量(数据段)进行通信,大家都可以对数据段进行读写,这很方便,也被在安全性方面诟病,因为它要求程序员时刻意识到这些数据不是线程独立的。

对于 linux 来说,则没有很明确的进程、线程概念。首先linux只有进程而没有线程,然而它的进程又可以表现得像 windows 下的线程。 linux 利用 fork() 和 exec 函数族来操作多线程。 fork() 函数可以在进程执行的任何阶段被调用,一旦调用,当前进程就被分叉成两个进程——父进程和子进程,两者拥有相同的代码段和暂时相同的数据段(虽然暂时相同,但从分叉开的时刻就是逻辑上的两个数据段了,之所以说是逻辑上的,是因为这里是“写时复制”机制,也就是,除非万不得已有一个进程对数据段进行了写操作,否则系统不去复制数据段,这样达到了负担最小),两者的区别在于fork()函数返回值,对于子进程来说返回为0,对于父进程来说返回的是子进程id,因此可以通过

if(fork()==0)
    …
else
    …

来让父子进程执行不同的代码段,从而实现“分叉”。

exec 函数族的函数的作用则是启动另一个程序的新进程,然后完全用那个进程来代替自己(代码段被替换,数据段和堆栈被废弃,只保留原有进程id)。这样,如果在 fork() 之后,在子进程代码段里用 exec 启动另一个进程,就相当于 windows 下的 CreateThread() 的用处了,所以说 linux 下的进程可以表现得像 windows 下的线程。

然而 linux 下的进程不能像 windows 下线程那样方便地通信,因为他们没有共享数据段、地址空间等。它们之间的通信是通过所谓 IPC(InterProcess Communication) 来进行的。具体有管道(无名管道用于父子进程间通信,命名管道可以用于任意两个进程间的通信)、共享内存(一个进程向系统申请一块可以被共享的内存,其它进程通过标识符取得这块内存,并将其连接到自己的地址空间中,效果上类似于 windows 下的多线程间的共享数据段),信号量,套接字。

php 多进程

PHP多进程已经十分成熟,可以用户

基础知识

根据前一节的介绍可以知道, windows 下是不能使用 fork 来开辟新进程的,php中提供了 proc_open 等函数来进行进程控制,相关内容本文并不展开,如有精力单独写篇文章。

下面单说 *nix 下的多进程,开始之前先提供两个基础学习的点posix(可移植操作系统接口)相关函数

示例代码

为了了解 php 在 linux 下如何创建多进程,我们先展示一下一个父进程,创建两个子进程的代码;

for ($i=0;$i<2;$i++) {
    $pid = pcntl_fork(); // 程序在这里分了叉,子进程从这里开始,并且会将到此之前的所有变量等都继承。
    if ($pid > 0) {
        // 分支进入父进程
        line("当前创建了第{$i}个子进程,PID是{$pid}");
    } else {
        // 分支进入子进程
        line("我是一个子进程");
        exit(0);    // 子进程代码运行完毕后要执行退出
    }
}
function line($msg){
    echo $msg.PHP_EOL;
}

进程管理

当子进程创建之后将由父进程对子进程进行管理,父进程不对子进程进行回收会产生孤儿进程和僵尸进程。(建议阅读深入理解计算机系统进行补充)

  • 孤儿进程的产生是因为:子进程的活还没干完,父进程就自己圆寂了,等到子进程干完活时发现自己成为了孤儿,自己所占用的资源没有得到释放。这个时候会被init进程收养,并适时回收。
  • 僵尸进程产生的原因是:子进程活干完了,父进程还在忙别的,年幼的子进程虽然没有成为孤儿,但是因为没有老子的管教成为了行尸走肉,就是传说中的僵尸进程,这个时候只要老爹还健在,这个子进程就不会被送福利院接收教育,init不能回收资源,导致子进程一直占用资源。这种情况对计算机资源产生了浪费。

父进程等待子进程返回的状态两个函数 [pcntl_wait]() pcntl_waitpid,第二个参数可以决定是否挂起父进程。

其他参见

  • pcntl_fork() - 在当前进程当前位置产生分支(子进程)。译注:fork是创建了一个子进程,父进程和子进程 都从fork的位置开始向下继续执行,不同的是父进程执行过程中,得到的fork返回值为子进程 号,而子进程得到的是0。
  • pcntl_signal() - 安装一个信号处理器
  • pcntl_wifexited() - 检查状态代码是否代表一个正常的退出。
  • pcntl_wifstopped() - 检查子进程当前是否已经停止
  • pcntl_wifsignaled() - 检查子进程状态码是否代表由于某个信号而中断
  • pcntl_wexitstatus() - 返回一个中断的子进程的返回代码
  • pcntl_wtermsig() - 返回导致子进程中断的信号
  • pcntl_wstopsig() - 返回导致子进程停止的信号
  • pcntl_waitpid() - 等待或返回fork的子进程状态

进程间通信

常见的进程间通信的方式有以下几种。

  • 管道pipe:管道是一种半双工的通信方式,数据只能单向流动,而且只能在具有亲缘关系的进程间使用。进程的亲缘关系通常是指父子进程关系。

  • 命名管道FIFO:有名管道也是半双工的通信方式,但是它允许无亲缘关系进程间的通信。

  • 消息队列MessageQueue:消息队列是由消息的链表,存放在内核中并由消息队列标识符标识。消息队列克服了信号传递信息少、管道只能承载无格式字节流以及缓冲区大小受限等缺点。

  • 共享存储SharedMemory:共享内存就是映射一段能被其他进程所访问的内存,这段共享内存由一个进程创建,但多个进程都可以访问。共享内存是最快的 IPC 方式,它是针对其他进程间通信方式运行效率低而专门设计的。它往往与其他通信机制,如信号两,配合使用,来实现进程间的同步和通信。

  • 信号量Semaphore:信号量是一个计数器,可以用来控制多个进程对共享资源的访问。它常作为一种锁机制,防止某进程正在访问共享资源时,其他进程也访问该资源。因此,主要作为进程间以及同一进程内不同线程之间的同步手段。

  • 套接字Socket:套解口也是一种进程间通信机制,与其他通信机制不同的是,它可用于不同及其间的进程通信。

  • 信号 ( sinal ) : 信号是一种比较复杂的通信方式,用于通知接收进程某个事件已经发生。

先演示使用信号进行通信的代码

declare(ticks=1);

pcntl_signal(SIGHUP,  "sig_handler",false);

$child_pid = [];
for ($i=0;$i<2;$i++) {
    $pid = pcntl_fork(); // 程序在这里分了叉,子进程从这里开始,并且会将到此之前的所有变量等都继承。
    if ($pid > 0) {
        // 分支进入父进程
        line("当前创建了第{$i}个子进程,PID是{$pid}");
        $child_pid[$i] = $pid;
    } else {
        // 分支进入子进程
        line("我是一个子进程{$i}");
        while (true){
            pcntl_signal_dispatch();
        }
        exit($i+1);    // 子进程代码运行完毕后要执行退出
    }
}

sleep(1);

posix_kill($child_pid[0],SIGHUP);

while ($rpid = pcntl_wait($status,WUNTRACED)){
    if (-1 !== $rpid){
        line($rpid.'进程结束');
    }else{
        line("没有子进程");
        exit(0);
    }
}

function line($msg){
    echo $msg.PHP_EOL;
}

function sig_handler($signo)
{
    line("收到信号");
    var_dump(SIGHUP,$signo);
    exit(0);
}

再来演示管道通信

说道管道 用法和读写文件一样,按照官方说法,管道是一种特殊的文件。

declare(ticks=1);

$pid = pcntl_fork(); // 程序在这里分了叉,子进程从这里开始,并且会将到此之前的所有变量等都继承。

$pipe_file = './test.pipe';
// 创建命名管道
if( !file_exists( $pipe_file ) ){
    if( !posix_mkfifo( $pipe_file, 0666 ) ){
        line('管道创建失败!');
        exit();
    }
}

if ($pid > 0) {
    // 分支进入父进程
    line("当前创建了子进程,PID是{$pid}");
    $fb = fopen( $pipe_file, "r" );
    // 管道的读取和写入都会阻塞
    $message = fread( $fb, 1024 );
    line($message);
} else {
    // 分支进入子进程
    sleep(1);
    $fb = fopen( $pipe_file, "w" );
    fwrite( $fb, "我是一个子进程{$i}");
    exit(0);    // 子进程代码运行完毕后要执行退出
}

pcntl_wait( $status );

function line($msg){
    echo $msg.PHP_EOL;
}

使用共享内存实现通信

php提供了两种实现共享内存的扩展

  • shmop 系类函数
    • 是基于字符串偏移量的方式进行查找和写入的
  • Semaphore 扩展中的 sem 类函数
    • 是以key-value形式进行查找和写入的

以下是shmop示例代码

$pid = pcntl_fork(); // 程序在这里分了叉,子进程从这里开始,并且会将到此之前的所有变量等都继承。

$pipe_file = './test.pipe';
// 创建共享内存
$shm_key = ftok(__FILE__, 't');
$shm_id = shmop_open($shm_key, "c", 0666, 1024);

if ($pid > 0) {
    // 分支进入父进程
    line("当前创建了子进程,PID是{$pid}");
    sleep(3);
    $message = shmop_read($shm_id, 0, 100);
    line($message);
} else {
    // 分支进入子进程
    sleep(1);
    $size = shmop_write($shm_id, "我是一个子进程{$i}".PHP_EOL, 0);
    $size2 = shmop_write($shm_id, '追加'.PHP_EOL, $size+1);
    var_dump($size,$size2);
    shmop_delete($shm_id);
    exit(0);    // 子进程代码运行完毕后要执行退出
}

pcntl_wait( $status );

function line($msg){
    echo $msg.PHP_EOL;
}

shmop_close($shm_id);

sem 函数示例代码

$pid = pcntl_fork(); // 程序在这里分了叉,子进程从这里开始,并且会将到此之前的所有变量等都继承。

$pipe_file = './test.pipe';
// 创建共享内存
$key = ftok(__FILE__, 'a');
$share_key = 1;
$shm_id = shm_attach($key, 1024, 0666);

if ($pid > 0) {
    // 分支进入父进程
    line("当前创建了子进程,PID是{$pid}");
    sleep(3);
    $message = shm_get_var($shm_id, $share_key);
    line($message);
} else {
    // 分支进入子进程
    sleep(1);
    $message1 = "我是一个子进程{$i}";
    shm_put_var($shm_id, $share_key, $message1);
    shm_remove($shm_id);
    exit(0);    // 子进程代码运行完毕后要执行退出
}

pcntl_wait( $status );

function line($msg){
    echo $msg.PHP_EOL;
}

shm_detach($shm_id);

信号量

通常信号量是和共享内存联合使用的;信号量提供了一种所机制,防止多个进程对于内存资源的争抢。实现原子操作。

$key=ftok(__FILE__,'t');
/**
 * 获取一个信号量资源
 * int $key [, int $max_acquire = 1 [, int $perm = 0666 [, int $auto_release = 1 ]]]
 * $max_acquire:最多可以多少个进程同时获取信号
 * $perm:权限 默认 0666
 * $auto_release:是否自动释放信号量
 */
$sem_id=sem_get($key);
// 获取信号
sem_acquire($seg_id);

/**
 * 原子性操作业务代码
 */

// 释放信号量
sem_release($seg_id);
// 把次信号从系统中移除
sem_remove($sem_id);

deamon 守护进程

1.在后台运行。

为避免挂起控制终端将Daemon放入后台执行。方法是在进程中调用fork使父进程终止,让Daemon在子进程中后台执行。

if($pid=pcntl_fork()) 
    exit(0); // 是父进程,结束父进程,子进程继续

2.脱离控制终端,登录会话和进程组

有必要先介绍一下Linux中的进程与控制终端,登录会话和进程组之间的关系:进程属于一个进程组,进程组号(GID)就是进程组长的进程号(PID)。登录会话可以包含多个进程组。这些进程组共享一个控制终端。这个控制终端通常是创建进程的登录终端。 控制终端,登录会话和进程组通常是从父进程继承下来的。我们的目的就是要摆脱它们,使之不受它们的影响。方法是在第1点的基础上,调用setsid()使进程成为会话组长: posix_setsid(); 说明:当进程是会话组长时setsid()调用失败。但第一点已经保证进程不是会话组长。setsid()调用成功后,进程成为新的会话组长和新的进程组长,并与原来的登录会话和进程组脱离。由于会话过程对控制终端的独占性,进程同时与控制终端脱离。

3.禁止进程重新打开控制终端

现在,进程已经成为无终端的会话组长。但它可以重新申请打开一个控制终端。可以通过使进程不再成为会话组长来禁止进程重新打开控制终端:

if($pid=pcntl_fork())
exit(0); // 结束第一子进程,第二子进程继续(第二子进程不再是会话组长)

4.关闭打开的文件描述符

进程从创建它的父进程那里继承了打开的文件描述符。如不关闭,将会浪费系统资源,造成进程所在的文件系统无法卸下以及引起无法预料的错误。按如下方法关闭它们:

fclose(STDIN),fclose(STDOUT),fclose(STDERR)关闭标准输入输出与错误显示。

5.改变当前工作目录

进程活动时,其工作目录所在的文件系统不能卸下。一般需要将工作目录改变到根目录。对于需要转储核心,写运行日志的进程将工作目录改变到特定目录如chdir("/")

6.重设文件创建掩模

进程从创建它的父进程那里继承了文件创建掩模。它可能修改守护进程所创建的文件的存取位。为防止这一点,将文件创建掩模清除:umask(0);

7.处理SIGCHLD信号

处理SIGCHLD信号并不是必须的。但对于某些进程,特别是服务器进程往往在请求到来时生成子进程处理请求。如果父进程不等待子进程结束,子进程将成为僵尸进程(zombie)从而占用系统资源。如果父进程等待子进程结束,将增加父进程的负担,影响服务器进程的并发性能。在Linux下可以简单地将SIGCHLD信号的操作设为SIG_IGN。 signal(SIGCHLD,SIG_IGN);

这样,内核在子进程结束时不会产生僵尸进程。这一点与BSD4不同,BSD4下必须显式等待子进程结束才能释放僵尸进程。关于信号的问题请参考

umask(0);
$pid = pcntl_fork();
if (-1 === $pid) {
    exit('fork fail');
} elseif ($pid > 0) {
    exit(0);
}
// 第一次fork的子进程设置为回话组长,脱离终端。
if (-1 === posix_setsid()) {
    exit("setsid fail");
}
// 第二次fork子进程 防止重新打开控制终端。
$pid = pcntl_fork();
if (-1 === $pid) {
    exit("fork fail");
} elseif (0 !== $pid) {
    exit(0);
}

chdir("/");

echo "111";
sleep(10);
echo "222";

评论

captcha