经验首页 前端设计 程序设计 Java相关 移动开发 数据库/运维 软件/图像 大数据/云计算 其他经验
当前位置:技术经验 » 程序设计 » PHP » 查看文章
linux fork进程请谨慎多个进程/线程共享一个 socket连接,会出现多个进程响应串联的情况。
来源:cnblogs  作者:隔壁老赵的博客  时间:2019/5/13 9:03:07  对本文有异议

昨天组内同学在使用php父子进程模式的时候遇到了一个比较诡异的问题

简单说来就是:因为fork,父子进程共享了一个redis连接、然后父子进程在发送了各自的redis请求分别获取到了对方的响应体。

 

复现示例代码:

testFork.php

  1. 1 <?php
  2. 2 require_once("./PowerSpawn.php");
  3. 3
  4. 4 $ps = new Forkutil_PowerSpawn();
  5. 5 $ps->maxChildren = 10 ;
  6. 6 $ps->timeLimit = 86400;
  7. 7
  8. 8 $redisObj = new Redis();
  9. 9 $redisObj->connect('127.0.0.1','6379');
  10. 10
  11. 11 // 主进程 -- 查询任务列表并新建子进程
  12. 12 while ($ps->runParentCode()) {
  13. 13 echo "parent:".$redisObj->get("parent")."\n" ;
  14. 14 // 产生一个子进程
  15. 15 if ($ps->spawnReady()) {
  16. 16 $ps->spawnChild();
  17. 17 } else {
  18. 18 // 队列已满,等待
  19. 19 $ps->Tick();
  20. 20 }
  21. 21 }
  22. 22
  23. 23 // 子进程 -- 处理具体的任务
  24. 24 if ($ps->runChildCode()) {
  25. 25 echo "chlidren:".$redisObj->get("children")."\n" ;
  26. 26 }

 

PowerSpawn.php 主要用户进程fork管理工作

  1. <?php
  2. /*
  3. * PowerSpawn
  4. *
  5. * Object wrapper for handling process forking within PHP
  6. * Depends on PCNTL package
  7. * Depends on POSIX package
  8. *
  9. * Author: Don Bauer
  10. * E-Mail: lordgnu@me.com
  11. *
  12. * Date: 2011-11-04
  13. */
  14. declare(ticks = 1);
  15. class Forkutil_PowerSpawn
  16. {
  17. private $myChildren;
  18. private $parentPID;
  19. private $shutdownCallback = null;
  20. private $killCallback = null;
  21. public $maxChildren = 10; // Max number of children allowed to Spawn
  22. public $timeLimit = 0; // Time limit in seconds (0 to disable)
  23. public $sleepCount = 100; // Number of uSeconds to sleep on Tick()
  24.  
  25. public $childData; // Variable for storage of data to be passed to the next spawned child
  26. public $complete;
  27. public function __construct() {
  28. if (function_exists('pcntl_fork') && function_exists('posix_getpid')) {
  29. // Everything is good
  30. $this->parentPID = $this->myPID();
  31. $this->myChildren = array();
  32. $this->complete = false;
  33. // Install the signal handler
  34. pcntl_signal(SIGCHLD, array($this, 'sigHandler'));
  35. } else {
  36. die("You must have POSIX and PCNTL functions to use PowerSpawn\n");
  37. }
  38. }
  39. public function __destruct() {
  40. }
  41. public function sigHandler($signo) {
  42. switch ($signo) {
  43. case SIGCHLD:
  44. $this->checkChildren();
  45. break;
  46. }
  47. }
  48. public function getChildStatus($name = false) {
  49. if ($name === false) return false;
  50. if (isset($this->myChildren[$name])) {
  51. return $this->myChildren[$name];
  52. } else {
  53. return false;
  54. }
  55. }
  56. public function checkChildren() {
  57. foreach ($this->myChildren as $i => $child) {
  58. // Check for time running and if still running
  59. if ($this->pidDead($child['pid']) != 0) {
  60. // Child is dead
  61. unset($this->myChildren[$i]);
  62. } elseif ($this->timeLimit > 0) {
  63. // Check the time limit
  64. if (time() - $child['time'] >= $this->timeLimit) {
  65. // Child had exceeded time limit
  66. $this->killChild($child['pid']);
  67. unset($this->myChildren[$i]);
  68. }
  69. }
  70. }
  71. }
  72. /**
  73. * 获取当前进程pid
  74. * @return int
  75. */
  76. public function myPID() {
  77. return posix_getpid();
  78. }
  79. /**
  80. * 获取父进程pid
  81. * @return int
  82. */
  83. public function myParent() {
  84. return posix_getppid();
  85. }
  86. /**
  87. * 创建子进程 并记录到myChildren中
  88. * @param bool $name
  89. */
  90. public function spawnChild($name = false) {
  91. $time = time();
  92. $pid = pcntl_fork();
  93. if ($pid) {
  94. if ($name !== false) {
  95. $this->myChildren[$name] = array('time'=>$time,'pid'=>$pid);
  96. } else {
  97. $this->myChildren[] = array('time'=>$time,'pid'=>$pid);
  98. }
  99. }
  100. }
  101. /**
  102. * 杀死子进程
  103. * @param int $pid
  104. */
  105. public function killChild($pid = 0) {
  106. if ($pid > 0) {
  107. posix_kill($pid, SIGTERM);
  108. if ($this->killCallback !== null) call_user_func($this->killCallback);
  109. }
  110. }
  111. /**
  112. * 该进程是否主进程 是返回true 不是返回false
  113. * @return bool
  114. */
  115. public function parentCheck() {
  116. if ($this->myPID() == $this->parentPID) {
  117. return true;
  118. } else {
  119. return false;
  120. }
  121. }
  122. public function pidDead($pid = 0) {
  123. if ($pid > 0) {
  124. return pcntl_waitpid($pid, $status, WUNTRACED OR WNOHANG);
  125. } else {
  126. return 0;
  127. }
  128. }
  129. public function setCallback($callback = null) {
  130. $this->shutdownCallback = $callback;
  131. }
  132. public function setKillCallback($callback = null) {
  133. $this->killCallback = $callback;
  134. }
  135. /**
  136. * 返回子进程个数
  137. * @return int
  138. */
  139. public function childCount() {
  140. return count($this->myChildren);
  141. }
  142. public function runParentCode() {
  143. if (!$this->complete) {
  144. return $this->parentCheck();
  145. } else {
  146. if ($this->shutdownCallback !== null)
  147. call_user_func($this->shutdownCallback);
  148. return false;
  149. }
  150. }
  151. public function runChildCode() {
  152. return !$this->parentCheck();
  153. }
  154. /**
  155. * 进程池是否已满
  156. * @return bool
  157. */
  158. public function spawnReady() {
  159. if (count($this->myChildren) < $this->maxChildren) {
  160. return true;
  161. } else {
  162. return false;
  163. }
  164. }
  165. public function shutdown() {
  166. while($this->childCount()) {
  167. $this->checkChildren();
  168. $this->tick();
  169. }
  170. $this->complete = true;
  171. }
  172. public function tick() {
  173. usleep($this->sleepCount);
  174. }
  175. public function exec($proc, $args = null) {
  176. if ($args == null) {
  177. pcntl_exec($proc);
  178. } else {
  179. pcntl_exec($proc, $args);
  180. }
  181. }
  182. }
View Code

 

解释一下testFork.php做的事情:子进程从父进程fork出来之后,父子进程各自从redis中取数据,父进程取parent这个key的数据。子进程取child这个key的数据

终端的输出结果是:

  1. parent:parent
  2. parent:parent
  3. parent:children
  4. 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版本

原文链接:http://www.cnblogs.com/zhaoyixing/p/10847300.html

 友情链接:直通硅谷  点职佳  北美留学生论坛

本站QQ群:前端 618073944 | Java 606181507 | Python 626812652 | C/C++ 612253063 | 微信 634508462 | 苹果 692586424 | C#/.net 182808419 | PHP 305140648 | 运维 608723728

W3xue 的所有内容仅供测试,对任何法律问题及风险不承担任何责任。通过使用本站内容随之而来的风险与本站无关。
关于我们  |  意见建议  |  捐助我们  |  报错有奖  |  广告合作、友情链接(目前9元/月)请联系QQ:27243702 沸活量
皖ICP备17017327号-2 皖公网安备34020702000426号