缺陷的背后(四)---for

时间:2019-11-27
本文章向大家介绍缺陷的背后(四)---for,主要包括缺陷的背后(四)---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