以下内容全部在 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";
文章评论