linux fork进程请谨慎多个进程/线程共享一个 socket连接,会出现多个进程响应串联的情况。
昨天组内同学在使用php父子进程模式的时候遇到了一个比较诡异的问题
简单说来就是:因为fork,父子进程共享了一个redis连接、然后父子进程在发送了各自的redis请求分别获取到了对方的响应体。
复现示例代码:
testFork.php
<?php
require_once("./PowerSpawn.php"); $ps = new Forkutil_PowerSpawn();
$ps->maxChildren = 10 ;
$ps->timeLimit = 86400; $redisObj = new Redis();
$redisObj->connect('127.0.0.1','6379'); // 主进程 -- 查询任务列表并新建子进程
while ($ps->runParentCode()) {
echo "parent:".$redisObj->get("parent")."\n" ;
// 产生一个子进程
if ($ps->spawnReady()) {
$ps->spawnChild();
} else {
// 队列已满,等待
$ps->Tick();
}
} // 子进程 -- 处理具体的任务
if ($ps->runChildCode()) {
echo "chlidren:".$redisObj->get("children")."\n" ;
}
PowerSpawn.php 主要用户进程fork管理工作
<?php
/*
* PowerSpawn
*
* Object wrapper for handling process forking within PHP
* Depends on PCNTL package
* Depends on POSIX package
*
* Author: Don Bauer
* E-Mail: lordgnu@me.com
*
* Date: 2011-11-04
*/
declare(ticks = 1); class Forkutil_PowerSpawn
{
private $myChildren;
private $parentPID;
private $shutdownCallback = null;
private $killCallback = null; public $maxChildren = 10; // Max number of children allowed to Spawn
public $timeLimit = 0; // Time limit in seconds (0 to disable)
public $sleepCount = 100; // Number of uSeconds to sleep on Tick() public $childData; // Variable for storage of data to be passed to the next spawned child
public $complete; public function __construct() {
if (function_exists('pcntl_fork') && function_exists('posix_getpid')) {
// Everything is good
$this->parentPID = $this->myPID();
$this->myChildren = array();
$this->complete = false; // Install the signal handler
pcntl_signal(SIGCHLD, array($this, 'sigHandler'));
} else {
die("You must have POSIX and PCNTL functions to use PowerSpawn\n");
}
} public function __destruct() { } public function sigHandler($signo) {
switch ($signo) {
case SIGCHLD:
$this->checkChildren();
break;
}
} public function getChildStatus($name = false) {
if ($name === false) return false;
if (isset($this->myChildren[$name])) {
return $this->myChildren[$name];
} else {
return false;
}
} public function checkChildren() {
foreach ($this->myChildren as $i => $child) {
// Check for time running and if still running
if ($this->pidDead($child['pid']) != 0) {
// Child is dead
unset($this->myChildren[$i]);
} elseif ($this->timeLimit > 0) {
// Check the time limit
if (time() - $child['time'] >= $this->timeLimit) {
// Child had exceeded time limit
$this->killChild($child['pid']);
unset($this->myChildren[$i]);
}
}
}
} /**
* 获取当前进程pid
* @return int
*/
public function myPID() {
return posix_getpid();
} /**
* 获取父进程pid
* @return int
*/
public function myParent() {
return posix_getppid();
} /**
* 创建子进程 并记录到myChildren中
* @param bool $name
*/
public function spawnChild($name = false) {
$time = time();
$pid = pcntl_fork();
if ($pid) {
if ($name !== false) {
$this->myChildren[$name] = array('time'=>$time,'pid'=>$pid);
} else {
$this->myChildren[] = array('time'=>$time,'pid'=>$pid);
}
}
} /**
* 杀死子进程
* @param int $pid
*/
public function killChild($pid = 0) {
if ($pid > 0) {
posix_kill($pid, SIGTERM);
if ($this->killCallback !== null) call_user_func($this->killCallback);
}
} /**
* 该进程是否主进程 是返回true 不是返回false
* @return bool
*/
public function parentCheck() {
if ($this->myPID() == $this->parentPID) {
return true;
} else {
return false;
}
} public function pidDead($pid = 0) {
if ($pid > 0) {
return pcntl_waitpid($pid, $status, WUNTRACED OR WNOHANG);
} else {
return 0;
}
} public function setCallback($callback = null) {
$this->shutdownCallback = $callback;
} public function setKillCallback($callback = null) {
$this->killCallback = $callback;
} /**
* 返回子进程个数
* @return int
*/
public function childCount() {
return count($this->myChildren);
} public function runParentCode() {
if (!$this->complete) {
return $this->parentCheck();
} else {
if ($this->shutdownCallback !== null)
call_user_func($this->shutdownCallback);
return false;
}
} public function runChildCode() {
return !$this->parentCheck();
} /**
* 进程池是否已满
* @return bool
*/
public function spawnReady() {
if (count($this->myChildren) < $this->maxChildren) {
return true;
} else {
return false;
}
} public function shutdown() {
while($this->childCount()) {
$this->checkChildren();
$this->tick();
}
$this->complete = true;
} public function tick() {
usleep($this->sleepCount);
} public function exec($proc, $args = null) {
if ($args == null) {
pcntl_exec($proc);
} else {
pcntl_exec($proc, $args);
}
}
}
解释一下testFork.php做的事情:子进程从父进程fork出来之后,父子进程各自从redis中取数据,父进程取parent这个key的数据。子进程取child这个key的数据
终端的输出结果是:
parent:parent
parent:parent
parent:children
chlidren:parent
很显然,在偶然的情况下:子进程读到了父进程的结果、父进程读到了子进程该读的结果。
先说结论,再看原因。
linux fork进程请谨慎多个进程/线程共享一个 socket连接,会出现多个进程响应串联的情况。
有经验的朋友应该会想起unix网络编程中在写并发server代码的时候,fork子进程之后立马关闭了子进程的listenfd,原因也是类似的。
昨天,写这份代码的同学,自己闷头查了很长时间,其实还是对于fork没有重分了解,匆忙的写下这份代码。
使用父子进程模式之前,得先问一下自己几个问题:
1.你的代码真的需要父子进程来做吗?(当然这不是今天讨论的话题,对于php业务场景而言、我觉得基本不需要)
2.fork产生的子进程到底与父进程有什么关系?复制的变量相互间的更改是否受影响?
《UNIX系统编程》第24章进程的创建 中对上面的两个问题给出了完美的回答、下面我摘抄几个知识点:
1.fork之后父子进程将共享代码文本段,但是各自拥有不同的栈段、数据段及堆段拷贝。子进程的栈、数据从fork一瞬间开始是对于父进程的完全拷贝、每个进程可以更改自己的数据,而不要担心相互影响!
2.fork之后父子进程同时开始从fork点向下执行代码,具体fork之后CPU会调度到谁?不一定!
3.执行fork之后,子进程将拷贝父进程的文件描述符副本,指向同一个文件句柄(包含了当前文件读写的偏移量等信息)。对于socket而言,其实是复用了同一个socket,这也是文章开头提到的问题所在。

那么再回头看开始提到的问题,当fork之后,父子进程同时共享同一条redis连接。
一条tcp连接唯一标识的办法是那个四元组:clientip + clientport + serverip + serverport
那当两个进程同时指向了一个socket,socket改把响应体给谁呢?我的理解是CPU片分到谁谁会去读取,当然这个理解也可能是错误的,在评论区给出你的理解,谢谢
文章的最后谈几点我的想法:
1.php业务场景下需要使用多进程模式的并不多,如果你觉得真的需要使用fork来完成业务,可以先思考一下,真的需要吗?
2.当遇到问题的时候,最先看的还应该是你所使用技术的到底做了啥,主动与身边人沟通
3.《UNIX系统编程》是一本好书,英文名是《The Linux Programming Interface》,简称《TLPI》,我在这本书里找到了很多我想找到的答案。作为一个写php的、读C的程序员来说,简单易懂。
比如进程的创建、IO相关主题、select&poll&信号驱动IO&epoll,特别是事件驱动这块非常推荐阅读,后面我也会在我弄明白网络请求到达网卡之后、linux内核做了啥?然后结合事件驱动再记一篇我的理解。
我把《UNIX系统编程》电子版书籍放到了我的公众号,如果需要可以扫码关注我的公众号&回复 "TLPI",即可下载 《UNIX系统编程》《The Linux Programming Interface》的pdf版本

linux fork进程请谨慎多个进程/线程共享一个 socket连接,会出现多个进程响应串联的情况。的更多相关文章
- 知识点查缺补漏贴03:单机最大进程数,线程数和Socket连接数
前言: 参加Unix/Linux相关高级研发职位时,是否经常会被文档,单机允许最大进程数.线程数和Socket连接数,而你却感到束手无措呢?本文给你一个最为详细的答案. 一.最大进程数 运行Linux ...
- Linux fork()一个进程内核态的变化
[前言]用户态的变化,耳熟能详不在赘述.现在支持读时共享,写时复制. 一.内核态的变化 1.fork一个子进程代码 #include <stdio.h> #include <stdl ...
- linux fork 进程后 主进程的全局变量
fork一个进程后,复制出来的task_struct结构与系统的堆栈空间是父进程独立的,但其他资源却是与父进程共享的,比如文件指针,socket描述符等 不同的进程使用不同的地址空间,子进程被创建后, ...
- 【转载】查看Linux进程CPU过高具体的线程堆栈(不中断程序)
具体的命令经常忘记,毕竟用的不是很多.为了避免去找备份一下 1.TOP命令,找到占用CPU最高的进程 $ top top - 20:11:45 up 850 days, 1:18, 3 users ...
- Linux 性能分析调优 (四)——案例篇:系统中出现大量不可中断进程和僵尸进程怎么办
之前讲到 CPU 使用率的类型.除了上一节提到的用户 CPU 之外,它还包括系统 CPU(比如上下文切换).等待 I/O 的 CPU(比如等待磁盘的响应)以及中断 CPU(包括软中断和硬中断)等. 在 ...
- 【Linux 线程】同一个进程中的线程共享哪些资源
进程是具有一定独立功能的程序关于某个数据集合上的一次运行活动,进程是系统进行资源分配和调度的一个独立单位. 线程是进程的一个实体,是CPU调度和分派的基本单位,它是比进程更小的能独立运行的基本单位.线 ...
- 查看Linux进程CPU过高具体的线程堆栈(不中断程序)
转自:http://blog.csdn.net/mergerly/article/details/47731305 1.TOP命令,找到占用CPU最高的进程 $ top top - 20:11:45 ...
- Linux fork()、exec() Hook Risk、Design-Principle In Multi-Threadeed Program
目录 . Linux exec指令执行监控Hook方案 . 在"Multi-Threadeed Program"环境中调用fork存在的风险 . Fork When Multi-T ...
- linux fork函数与vfork函数,exit,_exit区别
man vfork: NAME vfork - create a child process and block parent SYNOPSIS #include <sys/types.h> ...
随机推荐
- springboot连接mongodb进行CRUD
springboot连接mongodb进行CRUD的过程: 在执行以下操作前已安装了mongodb并创建了用户和数据库,使用Robo 3T可成功连接. 1.创建springboot项目,加入以下mav ...
- 新树莓派系统安装ROS记录
树莓派系统更新了,作为版本控的我怎么能忍住不更新系统,为了提高系统的速度,买了张170M的告诉sd卡,我要说的是,这个高速SD卡的钱花的最值得了,千万不要觉得树莓派都买了4,还要什么高速SD卡(自行车 ...
- mysql —日志记录
日志 事务日志: transaction log 中继日志: reley log错误日志: error log 通用日志: general log 慢查询日志: slow query log 二进制日 ...
- Servlet的入门案例
编写入门案例 1)编写java类,继承HttpServlet类 2)重新doGet和doPost方法 3)Servlet程序交给tomcat服务器运行!! 3.1 servlet程序的class码拷贝 ...
- 阶段5 3.微服务项目【学成在线】_day02 CMS前端开发_09-webpack研究-webpack介绍
使用vue.js开发大型应用需要使用webpack打包工具,本节研究webpack的使用方法. 1.3.1 webpack介绍 Webpack 是一个前端资源的打包工具,它可以将js.image.cs ...
- java字符串MD5加密后再转16进制
话不多说上码 pom.xml <!-- MD5 --> <dependency> <groupId>org.apache.commons</groupId&g ...
- JAVA 基础编程练习题41 【程序 41 猴子分桃】
41 [程序 41 猴子分桃] 题目:海滩上有一堆桃子,五只猴子来分.第一只猴子把这堆桃子凭据分为五份,多了一个,这只猴子把 多的一个扔入海中,拿走了一份.第二只猴子把剩下的桃子又平均分成五份,又多了 ...
- python 内置数据结构 字符串
字符串 一个个字符组成的有序的序列,是字符的集合 使用单引号,双引号,三引号引住的字符序列 字符串是不可变对象 Python3起,字符串就是Unicode类型 字符串定义 初始化 s1 = 'stri ...
- HashMap和ConcurrentHashMap 源码关键点解析
第一部分:关键源码讲解 1.HashMap 是如何存储的? a.底层是一个数组 tab b. hash=hash(key) ,然后根据数组长度n和hash值,决定当前需要put的元素对应的数组下标, ...
- Oracle中替换字符中空格、制表符、换行符、回车符
引用:https://blog.csdn.net/linminqin/article/details/7386510 特殊符号ascii定义 制表符 chr(9) 换行符 chr(10) 回车符 c ...