缺陷的背后(四)---for
导语
业务模块为实现高并发时的更快的处理速度,经常会采用多进程的方式去处理业务。本章就是站在多进程的角度下,理解多进程模式下常见的两种bug:僵尸进程,窜包返回。理解问题出现的原因以及如何避免,如何
有效的测试出这类缺陷。
目录 一:线上缺陷分析 二:多进程概念理解 三:僵尸进程理解 3.1 产生原因 3.2 如何避免 3.3 问题场景 四:窜包 4.1 产生原因 4.2 如何避免 4.3 问题场景
二:多进程概念理解
概念2.1: fork子进程
进程(Process)是计算机中已运行程序的实体,是系统的基本运作单位,是资源分配的最小单位,fork子进程后,子进程会复制父进程的状态(内存空间数据等)。fork 出来的进程和父进程拥有同样的上下文信息、内存数据、与进程关联的文件描述符。
下面结合demo来看看有没有理解这句话。
问题1 :全局变量list1,fork子进程后,在子进程内:打印list1的虚拟地址,修改list1[0]的值,打印list1值,打印list1虚拟地址。 主进程内:打印list1的虚拟地址,待子进程修改后,打印list1值。
子进程和主进程打印的虚拟地址值是一样的吗?打印的list1的值是一样的吗?
import os
import time
list1 =[1,2,3,4]
print("list1的地址为{0}".format(id(list1)))
mainpid = os.getpid()
print(os.getpid())
pid = os.fork()
if pid<0:
print('创建进程失败')
elif pid == 0:
print("子进程执行的代码,子进程的pid为{0},主进程pid为{1}".format(os.getpid(),os.getppid()))
print("子进程修改前list1的地址为{0}".format(id(list1)))
list1[0]=10
print("子进程修改后list1的地址为{0}".format(id(list1)))
print("子进程list1为{0}".format(list1))
else:
print("主进程执行的代码,当前pid为{0},我真实的pid为{1}".format(pid,os.getpid()))
print("主进程list1的地址为{0}".format(id(list1)))
time.sleep(1)
print("主进程list1为{0}".format(list1))
print("主进程最后打印list1的地址为{0}".format(id(list1)))
print(list1)
print('end')
运行结果:
list1的地址为4349698528 10157 主进程执行的代码,当前pid为10158,我真实的pid为10157 主进程list1的地址为4349698528 子进程执行的代码,子进程的pid为10158,主进程pid为10157 子进程修改前list1的地址为4349698528 子进程修改后list1的地址为4349698528 子进程list1为[10, 2, 3, 4] [10, 2, 3, 4] end 主进程list1为[1, 2, 3, 4] 主进程最后打印list1的地址为4349698528 [1, 2, 3, 4] end Process finished with exit code 0
结果:
全局变量在子进程的虚拟地址值 = 主进程的虚拟地址值;子进程内的list1的值 不等于主进程list1的值。
分析:
fork创建一个新进程。系统调用复制当前进程,在进程表中新建一个新的表项,新表项中的许多属性与当前进程是相同的。新进程几乎与主进程一模一样,执行的代码也完全相同,但是新进程有自己的数据空间、环境和文件描述符。但是新的进程只是拥有自己的虚拟内存空间,而没有自己的物理内存空间。新进程共享源进程的物理内存空间。而且新内存的虚拟内存空间几乎就是源进程虚拟内存空间的一个复制。所以父子进程都打印list1的虚拟地址时,都是同一个地址值。
进程空间可以简单地分为程序段(正文段)、数据段、堆和栈四部分(简单这样理解)。fork函数,当执行完fork后的一定时间内,新的进程(p2)和主进程(p1)的进程空间关系如下图:
fork执行时,Linux内核会为新的进程P2创建一个虚拟内存空间,而新的虚拟空间中的内容是对P1虚拟内存空间中的内容的一个拷贝。而P2和P1共享原来P1的物理内存空间。但是当父子两个进程中任意一个进程对数据段、栈区、堆区进行写操作时,上图中的状态就会被打破,这个时候就会发生物理内存的复制,这也就是叫“写时复制”的原因。发生的状态转变如下:
P2有了属于自己的物理内存空间。如果只有数据段发生了写操作那么就只有数据段进行写时复制。这就解释了为啥修改子进程的全局变量,不影响父进程list1值的情况。而堆、栈区域依然是父子进程共享。还有一个需要注意的是,正文段(程序段)不会发生写时复制,这是因为通常情况下程序段是只读的。子进程和父进程从fork之后,基本上就是独立运行,互不影响了。
此外需要特别注意的是,父子进程的文件描述符表也会发生写时复制。
#!/usr/bin/python # -*- coding: UTF-8 -*- import pymysql import os import time def init_db(): # 打开数据库连接 db_conn = pymysql.connect("localhost","root","root1234","mysql" ) # 使用 cursor() 方法创建一个游标对象 cursor cursor = db_conn.cursor() return db_conn,cursor db_conn,db_curdsor = init_db() pid = os.fork() if pid<0: print('创建进程失败') elif pid == 0: print('子进程db_curdsor的地址是:{0}'.format(id(db_curdsor))) for i in range(1000): time.sleep(1) db_curdsor.execute("SELECT VERSION()") version_data = db_curdsor.fetchone() print(version_data) else: print("主进程执行的代码,当前pid为{0},我真实的pid为{1}".format(pid,os.getpid())) print('主进程db_curdsor的地址是:{0}'.format(id(db_curdsor))) time.sleep(2) db_conn.close()
运行结果:
主进程执行的代码,当前pid为13237,我真实的pid为13224 主进程db_curdsor的地址是:4672554320 子进程db_curdsor的地址是:4672554320 ('8.0.18',) Traceback (most recent call last): File "/Users/leiliao/Downloads/loleina_excise/process/test2.py", line 30, in <module> db_curdsor.execute("SELECT VERSION()") File "/opt/anaconda3/lib/python3.7/site-packages/pymysql/cursors.py", line 170, in execute result = self._query(query) File "/opt/anaconda3/lib/python3.7/site-packages/pymysql/cursors.py", line 328, in _query conn.query(q) File "/opt/anaconda3/lib/python3.7/site-packages/pymysql/connections.py", line 517, in query self._affected_rows = self._read_query_result(unbuffered=unbuffered) File "/opt/anaconda3/lib/python3.7/site-packages/pymysql/connections.py", line 732, in _read_query_result result.read() File "/opt/anaconda3/lib/python3.7/site-packages/pymysql/connections.py", line 1075, in read first_packet = self.connection._read_packet() File "/opt/anaconda3/lib/python3.7/site-packages/pymysql/connections.py", line 657, in _read_packet packet_header = self._read_bytes(4) File "/opt/anaconda3/lib/python3.7/site-packages/pymysql/connections.py", line 707, in _read_bytes CR.CR_SERVER_LOST, "Lost connection to MySQL server during query") pymysql.err.OperationalError: (2013, 'Lost connection to MySQL server during query') Process finished with exit code 0
此时如果子进程运行中,发现断连执行重连操作,则重连后的句柄属于子进程独有资源。
问题2:下面代码会创建几个子进程呢?
import os import time n = 2 # 期望设置5个进程 for i in range(n): pid = os.fork() if pid<0: print('创建进程失败') elif pid == 0: print("子进程执行的代码,子进程的pid为{0},主进程pid为{1}".format(os.getpid(),os.getppid())) else: pass #主进程什么都不做
运行结果:
子进程执行的代码,子进程的pid为13668,主进程pid为13667
子进程执行的代码,子进程的pid为13669,主进程pid为13667
子进程执行的代码,子进程的pid为13670,主进程pid为13668
结果:3个
分析:fork是UNIX或类UNIX中的分叉函数,fork函数将运行着的程序分成2个(几乎)完全一样的进程,每个进程都启动一个从代码的同一位置开始执行的线程。这两个进程中的线程继续执行,就像是两个用户同时启动了该应用程序的两个副本。如果把n设置成3,则实际会产生7个子进程。
1.i=0时,父进程进入for循环,此时由于fork的作用,产生父子两个进程(分别记为F0/S0),分别输出father和child,然后,二者分别执行后续的代码,子进程由于for循环的存在,没有退出当前循环,因此,父子进程都将进入i=1的情况;
2.i=1时,父进程继续分成父子两个进程(分别记为F1/S1),而i=0时fork出的子进程也将分成两个进程(分别记为FS01/SS01),然后所有这些进程进入i=2;
3.....过程于上面类似,已经不用多说了,相信一切都已经明了了,依照上面的标记方法,i=2时将产生
对应的数学公式如下:
1 + 2 + 4 + ... + 2^(n - 1) = 2^n - 1
原文地址:https://www.cnblogs.com/loleina/p/11937782.html
- Activity数据回传
- 引入Fragment原来是这么回事
- Fragment显示和隐藏、绑定和解绑
- 一网打进Linux下那些查找命令
- 高颜值可定制在线绘图工具-第三版
- network3D 交互式网络生成
- 如何用六点教会老婆写 Python ?
- 连高晓松都想学的区块链江湖切口,「HODL」是什么意思?
- Spring Data REST 与 Spring RestTemplate 实战详解
- 程序员炒股,如何计算股票投资组合的风险和收益
- Docker 容器化部署运维 OpenStack 和 Ceph
- 关于设计模式的思考
- Spring 框架之 AOP 原理剖析
- Java 平台反应式编程(Reactive Programming)入门
- JavaScript 教程
- JavaScript 编辑工具
- JavaScript 与HTML
- JavaScript 与Java
- JavaScript 数据结构
- JavaScript 基本数据类型
- JavaScript 特殊数据类型
- JavaScript 运算符
- JavaScript typeof 运算符
- JavaScript 表达式
- JavaScript 类型转换
- JavaScript 基本语法
- JavaScript 注释
- Javascript 基本处理流程
- Javascript 选择结构
- Javascript if 语句
- Javascript if 语句的嵌套
- Javascript switch 语句
- Javascript 循环结构
- Javascript 循环结构实例
- Javascript 跳转语句
- Javascript 控制语句总结
- Javascript 函数介绍
- Javascript 函数的定义
- Javascript 函数调用
- Javascript 几种特殊的函数
- JavaScript 内置函数简介
- Javascript eval() 函数
- Javascript isFinite() 函数
- Javascript isNaN() 函数
- parseInt() 与 parseFloat()
- escape() 与 unescape()
- Javascript 字符串介绍
- Javascript length属性
- javascript 字符串函数
- Javascript 日期对象简介
- Javascript 日期对象用途
- Date 对象属性和方法
- Javascript 数组是什么
- Javascript 创建数组
- Javascript 数组赋值与取值
- Javascript 数组属性和方法
- 一文搞定pandas的透视表
- Spring 日志输出错误字符 -e[0;39m e[2m[
- linux不支持所有命令的解决办法
- linux系列之常用运维命令整理笔录(小结)
- 轻松掌握Git开发(五)远程库的基本操作
- GitHub竟然还有这些骚操作,赶紧学起来
- leetcode之单词规律
- 内存溢出及解决方案
- 3分钟短文:Laravel控制器用法光速入门
- Linux查看ip的实例方法
- Ubuntu18.04通过源码安装Odoo14的教程
- Linux系统中SSH服务基于key认证实践的过程
- linux防墙iptables详细介绍、配置方法与案例
- Linux unlink函数和删除文件的操作方法
- Linux seq命令的使用详解