章节目录
第十六章 七层协议
推荐书籍:tcp-ip协议
两种架构:C/S:客户端-服务端(打印机,qq,微信等应用软件);B/S:浏览器-服务端(各种网站系统),严格来讲也是C/S架构中的一种。
服务端肯定是在公司的服务器上运行的,消息的传递是先由客户端请求到服务端再发送到另一个客户端(可以是原来那个也可以是另一个)的。
网络编程的实质就是编写一个客户端和服务端实现基于网络的通信。
计算机之间通过物理连接介质(通信载体)根据互联网协议标准(统一的语言才能通信)来通信。
物理层:
发送端网卡(发送帧)-网线/无线电波(0/1高低电平)-交换机/路由器/防火墙等等-网线/无线电波(0/1高低电平)-接收端网
数据链路层:
在物理层的上一层,负责将二进制数据进行分组打包成帧或将帧重新组合成数据。具体操作如下:
二进制数据分组统一用以太网协议(数据分组协议),协议内容如下:
一组电信号构成一个数据报叫做帧。每一帧分报头(head-头部长度固定)和数据(data-最短46、最长1500)两部分。总长度区间(64-1518字节)
源/目标地址使用的是mac地址(一个mac地址有6个字节,一个字节有8个二进制位),每个网卡都有一个全球唯一的mac地址(48位二进制通常转成12位的16进制,前六位表示产商编号,后六位表示流水线号)
计算机的通信要靠广播,但是全世界的计算机太多了,所以只能在局域网广播,否则网络数据量太大了。因此世界是由一个个小网(彼此隔离的局域网-广播域)构成的一张大网(互联网)。
局域网里根据以太网协议通过mac地址来定位,互联网中通过网络层的ip地址来定位局域网
网络层:
网络层中传输的是ip数据,也分为head(存放源ip与目标ip 20-60字节)和data(最长65515字
节)两部分。
通过ip定位在哪个局域网,通过mac来定位来自局域网内的哪一台机器。(ip+mac可以定位世界上任一一台计算机)
传输层:
网络通信其实是应用软件之间的通信,传输层就是用来标识软件位置的。
传输层有两个协议TCP/UDP都是基于端口工作的协议。基于网络通信的软件都有一个端口
应用层:
应用软件有无协议标准或用什么协议可以有自己来定,如:http,mail,ftp。
数据由应用软件产生后通过传输层在网络层打包成大块ip数据报,传输到指定的局域网后,在局域网内ip就没用了,转成用转换子网ip为mac,通过mac地址在局域内广播小跨数据帧。
发送即一层一层打包,接收则一层一层解包
第十七章 通信原理
服务器硬件配置
服务器用来提供访问数据,最基本的要求就是稳定,通过以下两个操作来保证
配置备份:两套同样的服务器硬件配置,两个网卡,两套硬盘,两套内存。
双路电源:一路是国家电网提供的正常交流电,一路是备用电源。
服务器时刻处于运转状态,声音非常大,对周边环境温度和湿度都是有要求的,因此服务器的运行环境造价是比较昂贵的。所以大部分中小企业都是用联通或电信的IDC机房(专业级机房环境/互联网数据中心)来托管服务器。
通信原理
公网ip(分A,B,C类网络,A类公网ip只有'2^7-1'
个最少,但每个子网段10.xxx可以有16777214个,B类网络只有 '2^14'
,子网段172.16.xxx-172.32.xxx有65534个,C类公网最多有2^21
个,但每个子网段只有有254台,不同网络的子网数时不一样的)通过与子网掩码(不同网络有不同的掩码,掩码是用来屏蔽部分ip地址来区分网络标识还是主机标识)的二进制与计算可以获得子网ip地址(每类网络中都有一部分区间被割出来作为子网ip段公用-子网ip段中的ip为子网ip,通过公网与子网掩码计算得来的),在局域网内通过ARP协议广播可以获取mac地址,(ARP是操作系统自动执行的,发报时交换机中记录mac地址与网口)知道公网ip和mac地址可以定位全球唯一一台计算机。
局域网间通信:
每个局域网(子网)都有一个网关,通常会配在路由器上,当源子网ip地址与目标子网ip地址不在同一个局域网(子网)内时,就会把包给网关,网关之间通过路由协议找到对应的网关后通过以太网协议把包给相应的主机。
局域网通信:
根据子网ip(动态可变的)通过ARP协议(ip地址解析协议)来广播获取接收方的Mac地址,记录在交换机的mac地址表中。
一开始加入局域网的计算机在交换机的mac地址表中并没有记录,所有局域网中的计算机只有自己的(公网ip和子网掩码)子网ip地址和自己的mac地址。计算机之间相互不知道对方的mac,只知道接收方的IP地址,这时候发送方在局域网内广播一个含有目标ip的数据包(专门用来获取目标主机的mac地址的,包含发送方的子网ip及mac还有接收方的子网ip及待填写的mac)。接收方根据目标子网ip确定是发给自己的之后,会将自己的mac地址写入数据包返回给发送方(此时不用广播,因为已经发送方的mac通过广播时交换机已经记录了他的网口和对应的mac地址)。
第十八章 TCP/UDP传输层协议 传输数据实现流程
传输层负责传输来自应用层产生后传给操作系统的数据,传输层及传输层以下的数据时有操作系统来控制的,而应用层数据是由应用软件控制的。并不是每次通信数据都要走一遍(客户端/服务器应用程序产生数据再由操作系统把数据按照传输层,网络层,数据链路层和物理层的数据发送给目标机器),而是通过三次握手建立好两条来往通路之后,沿着两条通路下载上传就可以了
TCP协议数据传输
参数意义:syn=1(此消息为请求);ack=1(确认收到请求)+x(请求消息编号id);seq=x(请求消息编号id)
TCP协议三次握手建连接
- 定义:建立TCP连接的方式,建立的连接其实是客户端和服务器的内存中保存了一份关于对方的信息,如ip地址、端口号等。TCP可以看成是一种字节流,会处理ip层或以下层的丢包、重复以及错误问题。在建立谅解的时候会把syn,seq,ack等参数放在tcp的头部。tcp提供了一种可靠、面向连接、字节流、传输层的服务,是采用三次握手建立的。
- 形象过程:一次消息发送为一次握手,c客户端发送“我喜欢你”给b服务端,b接受到后发送给“我知道了,其实我也喜欢你”,c收到后发送回复给b‘嗯嗯,我知道了,那我们在一起吧’,然后就建立了恋爱关系。
- 实际过程:上述通讯消息中可以既是请求消息也是确认消息,请求消息中 syn=1,seq=x(syn=1表示这是一个请求消息,x为请求的编号),确认消息中会有个 ack=1+x (1 表示确认收到请求了,x表示确认的是编号为x的请求)
TCP协议存在的漏洞
- 原因:每当有一个客户端请求 (syn=1,seq=x)过来服务器,无论客户端好坏,服务器都会给其请求返回确定信息,所以TCP协议也称好人协议。这是设计层面的漏洞。
- 实例:洪水攻击(syn flood)-客户端可以用假ip模拟大量的
syn请求
向服务器发包,因为ip是假的,所以服务器收不到客户端发回来的确认消息会隔几秒(由linux设置间隔)一直发送确认请求消息给假ip。因此会一直占用服务器操作系统的资源。 - 解决方法及仍未解决的问题:通过调整linux操作系统内核参数来防范洪水攻击。虽然syn flood是黑客发起的,但是当有大量的真实请求是服务器也是会瘫痪的。微博能号称支撑八个明星同时出轨却承受不住一对明星结婚的访问量。还有天猫的双11,12306网的并发量是facebook难以望其项背的,其论技术水平是世界级的和资金都是必不可少的。
半连接池
- 定义:如果服务器来一个
syn请求
信息就立马接收会导致内存被无限占用直至瘫痪,所以服务器有了半连接池(backlog限制)的保护机制,来一个syn请求
就将其放在半连接池中,池中的数量(服务器操作系统内存所能承受的数量)是固定的,当池满了之后就会将后来的syn请求
挡在外面,服务器会在自己可承受范围内从半连接池中取一个连接来做回应来建立真正的连接。 - 总结:半连接池就是一个
syn请求
缓冲池,供服务器提取处理。里面的缓冲的syn请求并不多,但是服务器处理请求速度极快,避免服务器奔溃,缓冲后一个一个取去响应。
连接请求状态
- 定义:服务器和客户端在建立TCP连接的时候都有相应的状态。服务器开机后会一直处于(1)
LISTEN监听状态
,等待客户端请求包;客户端发送了syn请求包后会处于(2)SYN_SENT状态
,等待服务器的响应(确认请求集成包);服务端收到请求(syn请求包)后会发送响应(确认请求集成包)并变成(3)SYN_RCVD状态
;客户端接收到服务器的响应(确认请求集成包)后(4)ESTABLISHED状态
表示-服务器—》客户端-线路连接成功并返回响应(确认请求集成包)给服务器;服务器收到响应后会改变状态为ESTABLISHED状态
表示-客户端—》服务器-线路连接成功。 - 服务端会一直在(1)(3)(5)状态之间迅速切换,如果受到syn flood攻击服务端会出现大量的(3)
SYN_RCVD状态
。
四次挥手断连接
- 定义:连接占用系统资源,不再使用时回收连接的方法。先断开客户端到服务器的连接再断开服务器到客户端的连接。
- 具体流程:客户端向服务端发送断连接的请求,客户端向服务器发送断开连接请求(fin=1,seq=x),服务器接收到后响应请求并断开客户端到服务器的连接(ack=1+x);服务器再向客户端发送断开连接请求(fin=1,seq=y)请求断开连接,客户端响应(ack=1+y)并断开服务器到客户端的连接。
- 注意:断开要四次是因为服务器响应断开请求和请求断开不能同时,而建立可以。断连接过程对称,由于服务端同时服务于许多用户,所以特别想断开连接,断开连接一般是由服务端发起的,而建立一般是由客户端发起的。
- 过程中的状态:服务器先进入FIN_WAIT_1状态发送断连接请求给客户端;客户端接收并响应请求进入CLOSE_WAIT状态;服务器接收到断开响应后(断开一条路线)进入FIN_WAIT_2状态;然后客户端再发送一个断连接请求到服务器并进入LAST_ACK状态;服务端接收到断连接请求后进入TIME_WAIT并响应。
- 注意:服务器最有可能停滞在准备响应客户端断连接请求的TIME_WAIT状态,当服务器中出现大量的TIME_WAIT状态,说明服务器处于高并发状态,及时监控服务器内存,cpu是否够用,如果内存占用70%以上要警惕继续下去可能机器会瘫痪。
TCP协议是可靠协议
用接收方的响应来保证数据不丢失,错误,无响应就一直自动发包。
UDP协议数据传输
- 定义:不可靠传输,不建立连接,基于ip和端口发送,只要知道对方的ip和端口就会对应发送,发过去就完事了,数据丢失,出错什么都不理,不需要确认信息。
TCP与UDP的比较
为了确保数据安全,用的比较多的是TCP,而一些查询相关少量数据信息会使用UDP。扯淡闲聊什么的可以用UDP,但是转账就要用TCP。
第十九章 套接编程
套接字层的由来
- 背景及原始编程图:按照七层网络协议把数据组织好沿着网络协议发送出去再按照网络协议解包。传输层及以下的工作(封装传输层的head,网络层的head,数据链路层的head,再转成二进制调网卡发送数据)已经封装好了。网络编程只需要调用类似封装好操作系统硬件接口的open函数就可以操作传输层所需要的硬件及固件编程。只需要工作在应用层即可。
套接字层(Socket层)
- 定义:Socket层就是套接字层,在应用层与传输层之间,是中间的软件抽象层,把传输层及以下的协议都封装好,写应用层软件只要遵循套接字层的标准写出来的程序自然就是遵循TCP/UDP协议和ip协议和以太网协议的标准。门面模式,把复杂的协议隐藏在接口后面,一组接口就是全部,让Socket去组织数据以符合指定的协议。
- 参数要求:客户端和服务器的通信必不可少的就是ip和端口,所以Socket编程就是一堆ip和端口。
- 套接字种族:基于文件类型(AF_UNIXunix一切接文件,一台主机上同一文件的多个应用之间的通讯)和基于网络类型(AF_INET)
- 编写代码工作流程:服务器实例Socket对象绑定(band)自身ip地址和端口(公示位置)提供服务并进入监听(listen)状态监听客户端的syn请求挂起进半连接池并取之一接受(accept)请求;客户端则是请求连接和通信最后请求断开。如图:
- 注意:客户端的connect对应服务端的accept,建立一个通讯循环可以多次通讯(请求响应),建立一个连接循环(建立连接,断开连接)可以多次处理不同客户端连接,由于现在还没加入并发所以只能同时处理一个客户端。
Socket编程实现(网络通信)
- 客户端代码
# coding=utf-8
# @Time : 2019/10/7 14:12
# @Author : 铝合金
# @File : Socket_client1.py
import socket
#选择电话类型买电话
phone=socket.socket(socket.AF_INET,socket.SOCK_STREAM)
#拨电话,地址为服务器的ip和端口
phone.connect(('127.0.0.1',8082))
#通讯循环,不输入结束命令可以一直通讯
while True:
#客户端输入发送消息并指定编码格式
msg_C=input('发送>>')
phone.send(msg_C.encode('utf-8'))
#接收服务端消息并指定解码格式
msg_S=phone.recv(1024).decode('utf-8')
print('服务端:',msg_S)
#如果是结束命令则结束通讯循环
if msg_C in ['结束','断开']:
break
#关闭套接字服务电话
phone.close()
- 服务端代码
# coding=utf-8
# @Time : 2019/10/7 14:12
# @Author : 铝合金
# @File : Socket_service.py
import socket
#选择电话类型买电话
phone=socket.socket(socket.AF_INET,socket.SOCK_STREAM)
#插手机卡,应用端口范围(0-65535),其中(0-1024)是给系统用的
phone.bind(('127.0.0.1',8082))
#开启监听并设置半连接池挂起数,同一时间可以来5个请求
phone.listen(5)
print('服务器以开启,等待客户端接入!!')
#连接循环
while True:
# 建立连接,conn是三次握手的产物一个双向连接
conn, client_address = phone.accept()
print(f'客户端{client_address}接入')
#通讯循环,客户端不输入结束命令可以一直通讯
while True:
try:
msg_C=conn.recv(1024).decode('utf-8')
print(f"客户端消息:{msg_C}")
if msg_C in ['结束','断开']:
conn.send(f"好的,已断开".encode('utf-8'))
break
msg_S=input("回复>>:")
conn.send(f"{msg_S}".encode('utf-8'))
except Exception as e:
print('出错',e)
break
#四次挥手断连接对象
conn.close()
#关闭套接字服务电话
phone.close()
第二十章 套接服务
远程控制
- 背景:服务器存放在机房(一般建在山区或者郊区,成本低)中并没有显示器和键盘,需要运维人员远程控制查看服务器状态,基于网络通信
subprocess模块实现远程控制程序
- 作用:用于在程序中执行系统命令
- 代码如下
客户端:
# coding=utf-8
# @Time : 2019/10/7 14:12
# @Author : 铝合金
# @File : Socket_client1.py
import socket
#选择电话类型买电话
phone=socket.socket(socket.AF_INET,socket.SOCK_STREAM)
#拨电话,地址为服务器的ip和端口
phone.connect(('127.0.0.1',8080))
#通讯循环,不输入结束命令可以一直通讯
while True:
#客户端输入发送消息并指定编码格式
msg_C=input('发送>>')
phone.send(msg_C.encode('utf-8'))
#接收服务端消息并指定解码格式
msg_S=phone.recv(1024).decode('utf-8')
print('服务端:',msg_S)
#如果是结束命令则结束通讯循环
if msg_C in ['结束','断开']:
break
#关闭套接字服务电话
phone.close()
服务端:
# coding=utf-8
# @Time : 2019/10/7 15:04
# @Author : 铝合金
# @File : Socket_remove_service.py
from socket import *
import subprocess #用于在服务器中执行系统命令的模块
#实例化套接服务并公示服务器地址,打开设置监听
server=socket(AF_INET,SOCK_STREAM)
server.bind(('127.0.0.1',8080))
server.listen(5) #半连接池缓冲请求
#连接循环
while True:
#连接开始
conn,client_address=server.accept()
print(f'客户端{client_address}接入')
#通信循环
while True:
try:
# 客户端发送过来的cmd命令,最大不超1024个字节
cmd=conn.recv(1024)
obj=subprocess.Popen(cmd.decode('utf-8'),#cmd命令
shell=True, #通过shell执行
stdout=subprocess.PIPE,#命令正确信息
stderr=subprocess.PIPE#命令错误信息
)
conn.send(obj.stdout.read())
conn.send(obj.stderr.read())
except ConnectionResetError:
break
conn.close()
server.close()
粘包现象
- 产生原因及定义:由于限制了接收大小为1024字节,所以超过大小的信息会残留在管道中。应用程序执行了send只是把信息复制一份到操作系统的内存中,发送方把所有数据发给接收方操作系统,由于recv有限制,超过的1024部分会缓存在接受方的操作系统中,下次从操作系统recv取时会取到上次的信息 。如图(windows先执行dir在执行ipconfig):
- 注意:接收和发送不是一一对应的关系,因为不是直接对接,而是通过自己的操作系统来完成的。
- 解决方法:接收方必须清楚发送方发了多少数据,进而再进行循环接收(一瓢瓢取一桶已知大小的水——都取一瓢残留缓存,直取一桶内存崩)。
UDP协议通信
UDP与TCP比较说明:
UDP通信不需要ack=1的确认信息,也不用建立连接。发送了数据后就清理自己的缓存,而TCP要接收方收到确认信息ack=1才清理自身的缓存。UDP相比TCP少了个确认和建立连接就少了网络延迟当然快。
UDP套接字
- 编程实现:不用建立连接,直接指定服务器ip与端口来发送和接收。
- 特点:UDP通信一个发送对应一个接收,不会有粘包问题。因为操作系统监测到是用数据报协议时会自定义报头规定大小,保证一个发送对应一个接收。
- 客户端:
# coding=utf-8
# @Time : 2019/10/7 16:50
# @Author : 铝合金
# @File : UDP_client.py
from socket import *
client=socket(AF_INET,SOCK_DGRAM) #数据报协议
while True:
msg=input('127.0.0.1:8082 >>>:').strip() #输入
client.sendto(msg.encode('utf-8'),('127.0.0.1',8082)) #指定发送到的服务端
res,server_addr=client.recvfrom(1024) #接收消息还有发消息的服务端ip及端口
print("127.0.0.1:8082 >>> :",res.decode('utf-8'))
- 服务端:
# coding=utf-8
# @Time : 2019/10/7 16:50
# @Author : 铝合金
# @File : UDP_server.py
from socket import *
server=socket(AF_INET,SOCK_DGRAM) #数据报协议
server.bind(('127.0.0.1',8082)) #公示服务器位置
while True:
client_data,client_addr=server.recvfrom(1024) #接收消息
print(f"{client_addr[0]}:{client_addr[1]} >>> {client_data.decode('utf-8')}")
msg=input("127.0.0.1:8082 >>> ")
server.sendto(msg.encode('utf-8'),client_addr) #指定客户端地址发送
- 并发与并行:并发看起来像同时运行的,并行是真的在同时并行。
网络通信基本常识
- 本机获取(开机后机器会自动从DHCP服务端发送请求获取本机IP,子网掩码,网关IP,DNS服务器IP)
- 打开浏览器(输入域名)
- 基于UDP协议的DNS服务查找公网IP。UDP协议的有效传输数据量是512bytes。
第二十一章 进程对象
进程的理解
- 定义:一个正在进程(运行)的程序,一个程序的运行过程,一个状态。(相对于线程是一个大任务,涉及整个程序,其实执行进程是执行进程下的主线程),主线程其实就代表了这个进程。
- 进程产生过程:执行一段程序代码后,操作系统申请一块内存空间将代码&数据放进去就起了一个进程。程序(代码/文件)最开始就是放在硬盘里的一个文件,当启动程序时应用程序发指令给操作系统调用硬件将程序从硬盘读入内存,目的是为了让CPU在内存直接处理这个程序,再由(应用程序发指令请求调度)操作系统调用CPU执行这个程序。
- 进程创建的主要方式:系统初始化(开机,从硬盘取程序数据),进程调用子进程(从内存或硬盘取),用户交互请求(打开应用,从硬盘取程序数据),批处理作业初始化(准备批处理程序所需的资源)。
- 不同系统进程调用子进程的区别:UNIX系统(linux系统和macOS系统都是UNIX内核)的子进程直接复制父进程的所有内存空间信息作为程序的初始状态;而windows系统会在父内存空间信息上初始化一些特有的子进程数据。基于进程调用子进程(打开pycharm是在硬盘取数据,在pycharm进程中执行py程序是在内存中取程序数据)。
- 进程运行的三种状态:运行态(正在被CPU执行),阻塞态(遇到io或资源准备,要想重新获得CPU执行权限要完成io并通过就绪态),就绪态(万事具备只欠CPU,供CPU调度选择要运行的进程-切换根据
‘io操作,占用时间,优先级‘
,改为运行或就绪)。 - 注意:进程(程序的执行过程)是由操作系统(调度)控制的,并发编程难在理解而不是在代码。
- 进程属性查看:每个进程都有唯一的PID。一个程序运行多次是多个进程,登录多个qq多个进程甚至会更多,一个程序可能开启多个进程。
- 具体命令:查看进程-linux/MacOS执行
ps aux
命令,window执行tasklist
可以看到操作系统上的所有进程的信息。在这些命令后面加上指定的程序名,就可以查看指定的进程。如查看python过滤掉其他,linux/MacOS执行ps aux|grep python
命令,window执行tasklist |findstr python
。结束进程-linux/MacOS下使用kill -9 PID
,windows使用taskkill /F /PID PID
- 进程命令操作实例:在pycharm中执行一个代码创建一个进程,过滤出进程得到进程PID后,每次执行代码的PID都不一样,但是其父进程PPID都是一样的,因为PPID是pycharm的进程。因为python解释器是pycharm调用的,如果在终端中调用解释器执行刚才编写好的python则父进程PPID变化了。
操作系统
- 定义:操作系统是位于计算机硬件和应用软件之间的,用于协调,管理和控制硬件与软件资源的一种控制程序。
- 操作系统两大作用:1、把复杂的硬件操作封装成简单的接口供应用程序使用,例如 open(),socket()。2、把进程对硬件的竞争变得有序化,例如word和excel同时执行了打印操作,共享带来竞争,竞争带来错乱。
OS发展史
- 第一代:真空管(易烧毁)和穿孔卡片——程序员直接控制硬件,同一时间段只有一个人使用。统一时刻只有一个程序在内存中执行,一个一个来。串行工作方式。
- 第二代:晶体管和批处理系统——硬件小升级,要人参与还是串行。
- 第三代:集成电路芯片和多道程序设计——一个CPU不可能实现并行,但是可以让一个CPU在多个进程之间来回的切换工作来实现并发(多道程序技术),好像同时在做。
- 多道技术:在单核cpu的情况下实现多个进程并发执行。1、内存空间复用(将内存分为几个部分分别放入一个程序,让cpu在内存的进程间高速切换。) 2、CPU时间复用(当其中一个进程在等待是切换到另一个进程)。
- 程序执行效率:写的程序时减少IO操作(因为进程放入阻塞态后会少分配CPU的执行权限即cpu时间)可以占用更多的CPU运行时间,提高程序执行效率。
- 注意:串行执行等待其他进程与阻塞没有任何关系,因为串行程序一直在占用CPU,而这里的阻塞是不占用CPU的。每个状态分配CPU执行权限的概率不同。
- 分时操作系统:标志着计算机进入一个新的时代。
- 概念总结:并发-一个人做雨露均沾做四份工作;并行-四个人分别负责四份工作;串行-一个人做四份工作,一份一份来;阻塞-任务遇到需要其他人帮助的时候停下;就绪-别人帮助后等待你继续完成剩下的;运行-正在做的工作;非阻塞-等待做的任务和正在做的任务来回切换,都没做完,就绪态和运行态切换。
第二十二章 进程并发
进程并发的原理
- 并发的本质:切换进程+保存进程状态。运行态发生切换的条件有:高优先级进程切入,当前进程执行时间片完到了,当前进程需要io操作。
进程并发编程实践
- 两种开启子进程的方式:父进程会等待子进程结束后再结束。
- python调用子进程函数:
# coding=utf-8
# @Time : 2019/10/9 17:58
# @Author : 铝合金
# @File : Create_Sub_Process.py
from multiprocessing import Process
import time
def task(name):
print('2 %s进程开始'%name)
time.sleep(3)
print('3 %s进程结束'%name)
if __name__ == '__main__':
#实例化子进程类(子进程函数,及函数所需参数:args=数据类型为元组,要按顺序;
# kwargs={'name':'lhj'}可用字典传参,不依赖顺序)
# p=Process(target=task,args=('铝合金的子进程',))
p=Process(target=task,kwargs={'name':'铝合金的子进程'})
#应用程序向操作系统发送指令申请硬件资源
# (申请内存空间;拷贝父进程内存空间数据;创建子进程)
p.start()
#此时已经在并发执行了
time.sleep(2)
print('1 这是主进程')
#windows系统开启子进程一定要在main里面
# 不然会无限创建自身函数进程
- python重写多进程类并引用实例化:
# coding = utf-8
# @Time : 2019/10/9 19:10
# @Author : 铝合金
# @File : Refactor_Process.py
from multiprocessing import Process
import time
class MyProcess(Process):
def __init__(self,name):
#父类初始化(执行类,参数)
super (Process,self).__init__()
self.name=name
def run(self):
print('2 %s进程开始'%self.name)
time.sleep(3)
print('3 %s进程开始' % self.name)
if __name__ == '__main__':
p=MyProcess('铝合金')
p.start() #start最后调用的是run这个方法
print('1 这是主进程')
- 父进程与子进程内存空间相互隔离:
# coding = utf-8
# @Time : 2019/10/9 19:23
# @Author : 铝合金
# @File : Process_RAM.py
from multiprocessing import Process
import time
x=10 #父内存空间的全局变量x
def task():
time.sleep(3)
global x #调用的是子内存空间,由于是从父内存空间复制的,所以值一样,但是地址不一样
print('子进程值x', x, '子进程开始')
x=5 #改变了子内存空间中的x全局变量
print('子进程值x',x,'子进程结束')
if __name__ == '__main__':
p=Process(target=task,)
p.start() #改变子进程内存空间中的x全局变量
time.sleep(5)
print(x) #父进程的x全局变量
- 父进程等待子进程结束:在通信场景中用到,如果子进程tcp没有建好的话不能让父进程进行到下一步。这里使用动态监测方法join()来监测子进程是否结束(join的内部原理是每隔一段时间就询问一下子进程是否结束)。
# coding = utf-8
# @Time : 2019/10/9 19:32
# @Author : 铝合金
# @File : Super_Wait_Sub_Down.py
from multiprocessing import Process
import time
x=10
def task(id):
print(id,'号子进程开始',x)
time.sleep(id)
if __name__ == '__main__':
start_time=time.time()
list_process=[]
for i in range(10):
p=Process(target=task,kwargs={'id':i})
list_process.append(p)
p.start()#发指令给os分配硬件资源创建子进程
for p in list_process:
p.join() #动态监测进程是否结束才会继续运行进入下一个迭代
end_time = time.time()
#输出的id不一定是0-9,是根据操作系统创建进程哪个快哪个先
print('监测到所有子进程结束,结束父进程,总耗时',end_time-start_time,
'最长子进程耗时9s,进程切换耗费',end_time-start_time-9)
- 进程对象属性:
- 注意:linux系统刚启动时会启动一个init进程,是所有进程的祖宗,会在这个进程的基础上再开很多的子进程,子孙进程。
僵尸进程与孤儿进程(linux系统)
- 注意:以下的例子是根据linux系统为标准的,与windows系统视完全不同的进程机制。所有子进程都会进入僵尸状态然后由父进程来完成收尸。
- 僵尸进程:在linux中除了init外都有父进程,进程结束后(清理占用的内存空间,关闭打开文件,回收资源),会遗留一些东西(保留进程的PID,占用CPU的时间和退出状态)在操作系统供父进程来查看最后在父进程要结束时调用linux专有接口waitpid(python中的join封装了waitpid)来回收子进程的数据(收尸)。
- 孤儿进程:如果父进程结束但是子进程没结束,则此子进程则成了孤儿进程,执行完后进入僵尸状态等政府(init)回收。
- 僵尸进程与孤儿进程的危害:孤儿进程没什么危害,因为有init一直在后面回收;而大量僵尸进程则会,如果一个父进程调用了大量的子进程且自身没有结束,则一直不会处理大量的僵尸进程,由于操作系统的pid数量有限,所以大量为关闭的僵尸进程会影响开启新的进程。
守护进程
- 定义:守护主程序运行代码。守护进程(子进程)守护一个主进程,如果被守护的主进程结束了,那守护进程也要在父进程结束前结束。
# coding = utf-8
# @Time : 2019/10/9 20:31
# @Author : 铝合金
# @File : Guard_Process.py
from multiprocessing import Process
import time,os
def task(name):
print('%s is running'%name)
time.sleep(10)
if __name__ == '__main__':
p=Process(target=task,kwargs={'name':'铝合金'})
p.daemon=True #将此子进程设为守护进程,父进程一结束子进程也要结束
p.start()
print('主进程结束')
互斥锁
- 场景:三个任务同时需要打印数据到控制台,但是控制台只有一个。要想保住数据不交叉错乱,必须把并发执行变成串行执行,一个一个来。这时就需要互斥锁。
- 互斥锁:所有要串行的进程公用一个互斥锁对象。这个锁是公用的被抢了没解锁下一次抢是没有的,连续抢两次,第二次肯定没抢到,因为第一次没解锁程序会卡住。
# coding = utf-8
# @Time : 2019/10/9 21:23
# @Author : 铝合金
# @File : Lock_Process.py
'''
进程互斥锁:把并发程序变成串行执行,
通过join()方法来控制要人为决定资源使用顺序
通过互斥锁lock就不用
'''
from multiprocessing import Process,Lock
import time,random
mutex=Lock() #实例化一个锁对象
def task1(lock):
lock.acquire() #上锁
print('word之打印机任务1')
time.sleep(random.randint(1,3))
print('word之打印机任务2')
time.sleep(random.randint(1,3))
print('word之打印机任务3')
lock.release() #解锁
def task2(lock):
lock.acquire() # 上锁
print('wps之打印机任务1')
time.sleep(random.randint(1,3))
print('wps之打印机任务2')
time.sleep(random.randint(1,3))
print('wps之打印机任务3')
lock.release() #解锁
def task3(lock):
lock.acquire() # 上锁
print('notepad之打印机任务1')
time.sleep(random.randint(1,3))
print('notepad之打印机任务2')
time.sleep(random.randint(1,3))
print('notepad之打印机任务3')
lock.release() #解锁
if __name__ == '__main__':
#传入锁对象,进程一执行就上锁一直占用打印机资源至进程结束解锁
p1=Process(target=task1,args=(mutex,))
p2 = Process(target=task2,args=(mutex,))
p3 = Process(target=task3,args=(mutex,))
#当上一个子进程结束时再进行下一个,改并发为串行
p1.start()
p2.start()
p3.start()
print('主进程')
- 注意:每个进程公用一把锁,但是子进程之间的内存空间完全隔离,所以通过参数的形式将锁的地址传给了子进程。
IPC机制(进程间的通信-Queue机制是重点)
- 进程通信存在的问题:中间介质硬盘共享速度低,可以用某块共享内存空间来共享。IPC机制需要的东西(能自动处理好锁问题的进程共享的内存空间,共享就存在竞争,需要互斥)。
- 缺陷方法-Manager类:Manager类有设计上的缺陷,(Manager类)没有处理好锁的问题,极有可能出现bug,不建议使用。数据库开发比较有难度是因为数据库软件就是要处理很多锁的问题。
- 常用方法(Queue机制):Queue即先进先出的队列,符合IPC机制所需要的全部条件。
- 注意:Queue队列(队列之间的通信也称消息队列)用来存储进程间沟通的消息,数据量不应过大;通信参数为队列大小,设置范围局限于内存的限制。Queue是在进程间是共享的。
- 编程实践:能进队列里的block的值无所谓,因为用不到,而大于队列的在队列外面如果设置了阻塞没有设置超时时间(会一直等下去)或者block直接不阻塞会直接报错,因为队列已经满了。
# coding = utf-8
# @Time : 2019/10/9 22:15
# @Author : 铝合金
# @File : IPC_Queue_Process.py
from multiprocessing import Process,Queue
import random
def task1(q):
print(q.get())
if __name__ == '__main__':
q=Queue(3)
p=Process(target=task1,kwargs={'q':q})
#前三个阻塞不阻塞都无所谓,因为,队列有三个空位,不用排队,直接进。
q.put('1',block=True)
q.put('2',block=True)
q.put('3',block=True)
# p.start()#分配资源,创建进程
# p.join()#等待子进程结束,取出一个后队列就有一个空位
q.put('4', block=False) #输入不阻塞,不管满不满直接塞,如果队列满直接报错
生产者消费者模型
- 生产者与消费者定义:生产者(负责生成数据的任务);消费者(负责处理生产者造出来的数据)。生产者进程和消费者进程模型实现了程序的解耦合。
- 形象比喻:生产者(调酒师);队列(吧台);消费者(顾客)
- 应用场景:当程序中出现明显的两类任务(一类生产数据,一类处理数据)时。如爬虫(生产者)和数据分析(消费者)组合。
- 好处:实现了生产者和消费者的解耦和;通过队列沟通而不是直接沟通平衡了生产力和消费力,两种进程可以一直工作。
- 程序实现:
# coding = utf-8
# @Time : 2019/10/10 12:49
# @Author : 铝合金
# @File : Producer_Consumer_Process.py
import time
import random
from multiprocessing import Process,Queue
def consumer(name,q):
while True:
res=q.get()
time.sleep(random.randint(1,3)) #随机睡眠比生产长,避免生产队列已为空。
print(f'{name}饮用{res}')
def producer(name,q,food):
for i in range(5):
time.sleep(random.randint(1,2))
res=f'{i}号饮品:{food}'
q.put(res)
print(f'{name}调出了{res}')
if __name__ == '__main__':
#共享吧台
q=Queue()
#生产者进程们
p1=Process(target=producer,kwargs={'name':'调酒师1号','q':q,'food':'红牛'})
p2=Process(target=producer,kwargs={'name':'调酒师2号','q':q,'food':'白兰地'})
p3=Process(target=producer,kwargs={'name':'调酒师3号','q':q,'food':'绿魔'})
#消费者进程们
c1=Process(target=consumer,kwargs={'name':'消费者1号','q':q})
c2=Process(target=consumer,kwargs={'name':'消费者2号','q':q})
p1.start();p2.start();p3.start();
c1.start();c2.start()
守护进程的应用
- 应用场景:上一个实现的生产者消费者模型有一个问题就是程序没有结束。由于当队列为空时消费者子进程不知道生产是否结束,所以消费者队列会一直阻塞等待。
- 问题:需要通知消费者进程生产结束使之结束。1(在所有生产者结束后-用
join()
判断,有几个生产者加入几个None使消费者get到None就break)。2(使用JoinalbeQueue对象-Queue对象,在所有生产者结束后,调用q.join()
来阻塞父进程继续,如果结束阻塞则是队列为空,且被子进程取出去的任务已完成q.task_done()
判断。) - 总结:父进程加入
q.join()
来阻塞等待生产完的队列处理完,消费子进程加入q.task_done()
来告知q.join()
结束阻塞。这样之后就可以将消费子进程设为守护进程了,因为父进程结束时肯定没得吃了,所以消费子进程不必瞎等了。 - 代码实现:
# coding = utf-8
# @Time : 2019/10/10 12:49
# @Author : 铝合金
# @File : Producer_Consumer_Process.py
import time
import random
from multiprocessing import Process,JoinableQueue
def consumer(name,q):
while True:
res=q.get()
time.sleep(random.randint(1,3)) #随机睡眠比生产长(吃不做慢),q.get()不到数据会阻塞,不结束进程。
print(f'{name}饮用{res}')
q.task_done()
def producer(name,q,food):
for i in range(2):
time.sleep(random.randint(1,2))
res=f'饮品:{food}'
q.put(res)
print(f'{name}调出了{res}')
if __name__ == '__main__':
#共享吧台
q=JoinableQueue()
#生产者进程们
p1=Process(target=producer,kwargs={'name':'调酒师1号','q':q,'food':'红牛'})
p2=Process(target=producer,kwargs={'name':'调酒师2号','q':q,'food':'白兰地'})
p3=Process(target=producer,kwargs={'name':'调酒师3号','q':q,'food':'绿魔'})
#消费者进程们
c1=Process(target=consumer,kwargs={'name':'消费者1号','q':q})
c2=Process(target=consumer,kwargs={'name':'消费者2号','q':q})
c1.daemon=True;c2.daemon = True
p1.start();p2.start();p3.start();
c1.start();c2.start()
p1.join();p2.join();p3.join();
print('生产结束,阻塞等待消费者消费完队列产品')
q.join()
print('消费者消费完毕,结束守护进程(消费者进程),父进程结束')
- 总结:Queue用法是进程通信的核心是一个重点,生产者消费者模型设计模式(解决问题的思路)源自于java,也叫工厂模式,也很重要。‘工厂对应队列吧,从工厂容器里取bean出来用’
第二十三章 线程并发
线程
- 定义:线程才是真正的执行单位,而进程只是一个资源单位,一个进程内都自带了一个主线程。只有当所有子线程结束后,主线程才会结束并清理内存空间,主线程其实就代表了这个进程。
- 代入比喻:进程是一个车间,而线程是车间里的的一条条流水线,线程才是真正用于生产执行的东西。
- 线程优点:一个进程内的多个线程公用一个进程内的资源,不同进程的线程资源相互隔离。创建开销小于进程100倍以上,因为创建线程不用在此申请内存空间。
- 应用场景:如果我们写的程序个部分组件之间都要共享一些数据,推荐使用多线程。例如写一个文本处理工具。考虑的子功能至少有四个(获取用户输入信息;把用户输入信息打印到屏幕上;把用户输入信息保存到硬盘上),如果用进程并发的话进程间的通信很漫长复杂,而线程不一样,公用进程内存空间中的数据。
开启线程的方式
- 函数创建线程:把函数定义为线程对象。线程没有父子线程说法,但是这里为了好定位先把主进程创建的进程成为子线程。
- 注意:这里的主线程等待子线程与父进程等待子进程不一样。主线程是等待子线程运行完清理内存空间,而主进程等待子进程是为了给子进程收尸(清理进程结束信息PID等)。除了主进程的主线程,主线程结束相当于子进程结束,而子进程结束后会留下PID的遗留信息。
- 类创建线程:同一个进程内的多个线程属于一个进程,并共享该进程内的资源。
# coding = utf-8
# @Time : 2019/10/11 16:18
# @Author : 铝合金
# @File : Create_Thread.py
from threading import Thread
import time
def task(name):
print(f'创建子线程{name}')
class MyThread(Thread):
def run(self) -> None:
print(f'{self.name}进程正在运行')
time.sleep(3)
if __name__ == '__main__':
t1=Thread(target=task,kwargs={'name':'线程1'})
t2=MyThread()
t1.start()
t2.start()
print('主线程运行')
线程对象的其他方法
- 进程对象的方法:判断是否存活;活跃线程数;获取当前线程的名字等。
守护线程
- 注意:主线程运行完毕指的是非守护线程运行完毕就代表了主线程(进程)结束。基于linux(主进程的运行完毕与子进程是没有关系的。只是在其结束时会清理已结束的子进程的遗留信息。)。
- 守护进程与守护线程的区别:**1、线程创建开销小先于主线程运行。2、主线程必须等待非守护进程运行结束才算结束。**守护进程还没运行父进程(主线程)已经运行完了,守护线程开启很快,所以可以在主线程结束前(非守护进程结束)运行到。
线程互斥锁(不是做数据库开发很少接触锁处理,了解即可,其他都重要)
- 问题:并发执行带来了数据不安全的问题,需要对多个线程进行加锁处理。如果多个线程之间共用一个变量,且线程处理变量之间相互依赖,需要互斥。多个线程取了内存里的同一个变量做临时变量后,再根据临时变量修改了内存变量,由于延迟了0.1秒,所以所有temp都等于100。
x=100
def task():
global x
temp=x
time.sleep(0.1) #模拟线程处理任务过程中的延迟。
x=temp-1
#输出是99,因为sleep的那0.1使每个子线程的temp都变成一样的了
if __name__ == '__main__':
start_time=time.time()
thread_list=[]
for i in range(100):
t=Thread(target=task)
thread_list.append(t)
t.start()
for t in thread_list:
t.join()
print('主',x)
print(time.time()-start_time)
- 加锁处理:串行执行100个0.1秒的线程,进程(主线程)运行时间大概10秒出头。
# coding = utf-8
# @Time : 2019/10/11 16:38
# @Author : 铝合金
# @File : Lock_Thread.py
from threading import Thread,Lock
import time
#并发执行上锁成串行执行
mutex=Lock()
x=100
def task():
global x
mutex.acquire() #上锁
temp=x
time.sleep(0.1)
x=temp-1
mutex.release() #解锁
if __name__ == '__main__':
start_time=time.time()
thread_list=[]
for i in range(100):
t=Thread(target=task)
#不用传锁,因为与进程不同,线程共享内存空间
thread_list.append(t)
t.start()
for t in thread_list:
t.join()
print('主',x)
print(time.time()-start_time)
死锁与锁递归
- 死锁定义:死锁互不放锁。线程1-锁1锁2,开2开1;线程2-锁2锁1,开1开2;线程1拿到锁1,线程2拿到锁2,互不放手==死锁。
- 解决死锁之锁递归:互斥锁打包,连续拿锁(acquire),没拿完锁包中的锁不能抢。
信号量
- 定义:信号量就是特殊锁。把互斥锁来解决独卫问题,那用信号量来解决公厕问题。跟锁递归类似,都是锁包,一个要全空才可以进,另一个有空就可以进。
GIL全局解释器锁(重要的概念)
- 定义:为了使线程争抢解释器变得有序CPython就通过给解释器加上GIL锁来控制。GIL的作用是保证一个进程内(你的py和py解释器)同一时间只有一个线程运行。
- python代码执行过程:执行进程后你编写的py文件代码和py解释器代码在同一个内存空间,先运行python解释器代码然后将你的py文件全部当做字符串来解释运行。当你的字符串代码里出现了一些python的关键字解释器就会调用自己的代码来帮你完成。py文件的会多个线程争抢解释器
- 背景知识:GIL不是python的特性而是默认的编译环境CPython特有的,Pthon不完全依赖于GIL。 实现python解释器有多种方法,即有多种用汇编语言编写的编译器(例如c语言是一套语法标准,有visual C++等编译器,又如同样一段python代码可以通过CPython,PyPy,Psyco等执行环境来执行)。线程安全-由于python中有一个垃圾回收功能的线程,垃圾回收线程在后台由解释器定期启动与你写的程序并发执行,如果没有GIL的存在,在多核cpu的情况下就会出现并行处理数据,有些数据还没有绑定线程就被垃圾回收线程给删掉了。由于CPython解释器的内存管理不是线程安全的,需要一把阻止多个本机的线程
- 注意:这里的GIL是解释器层级的数据安全,而线程层级的数据安全是要靠自己加的互斥锁来保证。
- GIL的优缺点:优点-保证了CPython解释器内存管理的线程安全。缺点-同一进程内的线程无法实现并行(无法利用cpu多核的优势),某个线程遇到io还是得把并发没有任何问题
单核多线程并发和多核多线程并行执行(重要、重要)
- 应用选择:如果程序是IO密集型的