-
io操作不占用CPU
计算占用CPU
Python的多线程不适合CPU密集操作型的任务,适合io操作密集型任务 -
事件驱动与异步io
通常我们写服务器处理模型的程序时,有以下几种模型:
(1)每收到一个请求,创建一个新的进程,来处理该请求;
(2)每收到一个请求,创建一个新的线程,来处理该请求;
(3)每收到一个请求,放入一个事件列表,让主进程通过非阻塞io方式来处理该请求。
上面的几种方式各有千秋:
第一种方法,由于创建新的进程开销大,所以会导致服务器性能比较差,但实现比较简单
第二种方法,由于涉及到线程的同步,可能会导致死锁等问题
第三种方法,在写应用程序代码时,比上面两种方式都复杂
综合考虑大多因素,一般认为第三种方式比较适合网络服务器事件驱动模型:
在UI编程时,常常要对鼠标点击进行相应,首先如何获得鼠标点击?
方式一:创建一个线程,该线程一直循环检测是否有鼠标点击,那么这个方式有以下几个缺点:- CPU资源浪费,可能鼠标点击的频率非常小,但是扫描线程还是会一直检测,这会造成很多CPU资源的浪费,如果扫描鼠标点击的接口是阻塞的呢?
- 如果是阻塞的,又会出现下面的问题,如果我们不但要扫描鼠标点击,还要扫描键盘是否按下,由于扫描鼠标时被阻塞了,那么可能永远不会扫描键盘
- 如果一个循环需要扫描的设备很多,这又会引来响应时间的问题
所以,该方式是非常不好的。
方式二:事件驱动模型
目前大部分的UI编程都是事件驱动模型,如很多UI平台都会提供onClick()事件,这个事件就代表鼠标按下事件。事件驱动模型大体思路如下: - 有一个事件(消息)队列;
- 鼠标按下时,往这个队列中增加一个点击事件(消息);
- 有个循环,不断从队列中取出事件,根据不同的事件,调用不同的函数,如onClick()、onKeyDown()等;
- 事件(消息)一般都各自保存各自的处理函数指针,这样,每个消息都有独立的处理函数;
事件驱动编程是一种编程范式,这里程序的执行流由外部事件来决定。它的特点是包含一个事件循环,当外部事件发生时,使用
回调机制来触发相应的处理。另外两种常见的编程范式是(单线程)同步以及多线程编程。 -
io多路复用
(1)用户空间和内存空间
现在操作系统都是采用虚拟存储器,对32位操作系统,虚拟存储空间位4G。
操作系统的核心是内核,独立于普通的应用程序,可以访问受保护的内存空间,也有访问底层硬件设备的所有权限
为了保护用户进程不能直接操作内核,保证内核的安全,保证内核的安全,操作系统将虚拟空间划分为两部分,一部分为用户空间
一部分为内核空间。
(2)进程切换
保存上下文进行切换
(3)进程阻塞
正在执行的进程,由于期待的某些事件未发生,如请求系统资源失败,等待某种操作完成,新数据尚未到达或或无新工作等,
则由系统自动执行阻塞原语(Block),是自己由运行状态转变为阻塞状态。可见,进程的阻塞是进程自身的一种主动行为,也因此只有处于运行
状态的进程(获得CPU),才能将其转换为阻塞状态。当进程进入阻塞状态时,是不占用CPU资源的。
(4)文件描述符fd
文件描述符是计算机科学中的一个术语,是一个用于表述指向文件的引用的抽象化概念。
文件描述符在形式上是一个非负整数。实际上,它是一个索引值,指向内核为每一个进程所维护的该进程所打开文件的记录表。
当程序打开一个现有文件或者创建一个新文件时,内核向进程返回一个文件描述符。在程序设计中,一些设计底层的程序的编写往往会围绕
文件描述符展开。但是文件描述符这一概念往往只适用于UNIX,Linux这也的操作系统。
(5)缓存I/O
缓存io又被称作标准io,大多数文件系统默认io操作都是缓存io。在Linux的缓存io机制中,操作系统会将io的数据缓存在文件系统的
页缓存中(page cache), 也就是说,数据会先被拷贝到操作系统的缓存区中,然后才会从操作系统内核的缓冲区拷贝到应用程序的地址空间。
缓存io的缺点:
数据在传输过程中需要在应用程序的地址空间和内核进行多次数据拷贝,这些数据拷贝所带来的CPU以及内存开销是非常大的。 -
io模式
当一个read模式发生时,他会发生以下两个步骤:
(1)等待数据准备
(2)将数据从内核拷贝到进程中
正是因为这两个阶段,Linux系统产生了以下五种网络模式的方案:
-阻塞io
-非阻塞io
-io多路复用
-信号驱动io
-异步io
由于signal driven io在实际中并不常用,所以下面只介绍其他四种1)阻塞IO
在Linux系统中,默认所有的socket都是blocking
当用户进程调用RECVfrom这个系统调用,kernel就开始了IO的第一个阶段:准备数据(对于网络IO来说,很多时候数据在一开始
还没有到达,比如,还没有一个完整的UDP包。这个时候kernel就要等待足够的数据到来)。这个过程需要等待,也就是说数据被拷贝到
操作系统内核的缓存区中是需要一个过程的。而在用户进程这边,整个进程会被阻塞(当然,是进程自己选择的阻塞)。当kernel一直
等待数据准备好了,他就会将数据从内核缓存拷贝到用户进程中,然后kernel返回结果,用户进程才解除blocking状态,重新运行起来。
所以blocking IO的特点就是在IO执行的两个阶段都被block。2)非阻塞IO
Linux下,可以通过设置socket设置成nonblocking
这时如果数据没准备好,不会进行阻塞,直接返回一个error,用户进程通过error判断数据是否准备好。
从用户进程角度讲,发起一个read操作后,不需要等待,而是马上得到一个结果。只有当数据准备好时,并且kernel再次收到system call
,那么就可以进行数据的拷贝。
所以,nonblocking的特点是用户进程需要不断地主动询问kernel数据是否准备好。但是收大数据时还会卡。3)IO多路复用
就是我们说的select,poll,epoll,有些地方也成这种IO方式为event driven IO,
select/epoll 的好处在于单个process可以同时处理多个网络IO,它的基本原理是select,poll,epoll这个function
会不断的询问所负责的所有socket,当某个socket数据到达了,就通知用户进程。
当用户进程调用了select,那么整个进程就会被block。
同时,kernel会监视所有的select负责的socket,当任何一个socket数据准备好,select就会返回。这个时候用户进程在调用read操作
,将数据从kernel拷贝到用户进程。select默认最多只能同时监控1024个socket进程,不能精确定位哪一个链接准备好;
poll去除了对文件描述符个数的限制;
epoll集合上两种操作的优点,能够精确定位准备好的socket的位置,epoll可以同时支持水平触发(当数据准备好,
每次循环都发送一遍通知,直到数据操作完成)与边缘触发(当数据准备好,只发送一次通知,不管用户操作是否完成都不在通知)。所以,IO多路复用的特点是通过一种机制使一个进程能同时等待多个文件描述符,而这些文件描述符其中任何一个进入读就绪状态,select()
函数就可以返回。4)异步IO
用户进程发起read操作后,立刻就可以开始去做其他事。另一方面,从kernel角度讲,当他收到一个asynchronous read后,会立刻返回,
所以不会对用户进程造成block。然后,kernel会等待数据准备完成并将数据拷贝,当这一切完成,kernel会给用户进程发一个signal,
告诉它read操作完成了。 -
IO多路复用
(1). 用select实现服务器端,使之具有自动监视多个客户端连接并实现对应的IO操作
# Author : Xuefeng
import socket
import queue
import select
# 创建服务器端并设置端口地址与监听
server = socket.socket()
server.bind(("localhost", 9000))
server.listen(1000)
# no blocking
server.setblocking(False)
# 定义需要监视的read事件列表
inputs = [server, ]
# 定义需要监视的write事件列表
outputs = []
msg_dic = {}
while True:
# select返回read事件,write事件与异常三个元素
readable,writeable,exceptional = select.select(inputs, outputs, inputs)
print(readable,writeable,exceptional)
for r in readable:
# 如果来的read事件是一个服务链接,创建该链接并创建对应链接的队列,同时将该链接添加到需要监视的read列表
if r is server:
conn,addr = server.accept()
print("来了一个新连接:", conn,addr)
# 新建立的链接还没有发数据过来
inputs.append(conn)
# 创建对应的队列
msg_dic[conn] = queue.Queue()
else:
data = r.recv(1024)
print("收到新数据:", data)
# r.send(data)
# 将接收到的数据放入创建好的队列中
msg_dic[r].put(data)
# 将read操作放入write事件列表
outputs.append(r)
for w in writeable:
# 获取队列中的数据并发送
msg_W = msg_dic[w].get()
w.send(msg_W)
# 将该回合write操作移除
outputs.remove(w)
for e in exceptional:
if e in outputs:
outputs.remove(e)
e.close()
del msg_dic[e]
(2). 多进程客户端
# Author : Xuefeng
import socket
import sys
# 设置连接端口
addr = ("localhost", 9000)
# 创建1000个客户端
sock = [socket.socket(socket.AF_INET,socket.SOCK_STREAM) for i in range(1000)]
# 定义需要发送的数据列表
info = [
b"1111111111",
b"2222222222222",
b"333333333333333333"
]
# 循环创建连接,发送数据,接收数据
for s in sock:
s.connect(addr)
for i in info:
s.send(i)
data = s.recv(1024)
print("Recv:", data)
(3) . selector 更简便的实现IO多路复用
# Author : Xuefeng
import selectors
import socket
# 实例化selector
sel = selectors.DefaultSelector()
def accept(sock, mask):
'''
定义创建连接函数
:param sock: 客户端socket
:param mask:
:return:
'''
# 创建连接
conn, addr = sock.accept()
print(conn, addr)
# 设置非阻塞
conn.setblocking(False)
# 对连接注册read操作函数
sel.register(conn, selectors.EVENT_READ, read)
def read(conn, mask):
'''
定义read操作函数,包括收发数据功能,并实现关闭连接解除注册的功能
:param conn: 连接
:param mask:
:return:
'''
data = conn.recv(1024)
if data:
print("Recv:", data)
conn.send(data)
else:
print("closing:", conn)
sel.unregister(conn)
conn.close()
# 创建服务器端socket,并设置端口号和非阻塞,同时进行监听
sock = socket.socket()
sock.bind(("localhost", 9000))
sock.listen(100)
sock.setblocking(False)
# 将accept创建连接函数注册到服务器端sock
sel.register(sock, selectors.EVENT_READ, accept)
while True:
# 循环监视服务器端
events = sel.select()
for key, mask in events:
# print(mask)
# 如果服务器端有响应,通过上面的注册操作返回accept函数,也就是key.data
callback = key.data
callback(key.fileobj, mask)
本文深入探讨了IO操作的不同模型,包括阻塞IO、非阻塞IO、IO多路复用(select、poll、epoll)及异步IO,解析了它们在服务器编程中的应用与优劣。同时,介绍了事件驱动模型在UI编程中的作用,以及Linux环境下如何使用select、epoll实现服务器端的多客户端连接监控。
2799

被折叠的 条评论
为什么被折叠?



