title: IO多路复用
top: 43
date: 2022-07-07 14:06:39
tags:
- io多路复用
categories: - IO
IO多路复用
能够阻塞等待,非忙轮询装填,不浪费CPU等资源,也能同一时刻监听多个IO请求的机制。
- IO 多路复用是一种同步IO模型,实现一个线程可以监视多个文件句柄;
- 一旦某个文件句柄就绪,就能够通知应用程序进行相应的读写操作;
- 没有文件句柄就绪就会阻塞应用程序,交出CPU。
多路是指网络连接,复用指的是同一个线程
select机制
客户端操作服务器时就会产生这三种文件描述符(简称fd):writefds(写)、readfds(读)、和exceptfds(异常)。select会阻塞住监视3类文件描述符,等有数据、可读、可写、出异常 或超时、就会返回;返回后通过遍历fdset整个数组来找到就绪的描述符fd,然后进行对应的IO操作。
- 优点:
几乎在所有的平台上支持,跨平台支持性好 - 缺点:
- 由于是采用轮询方式全盘扫描,会随着文件描述符FD数量增多而性能下降。
- 每次调用 select(),需要把 fd 集合从用户态拷贝到内核态,并进行遍历(消息传递都是从内核到用户空间)
- 默认单个进程打开的FD有限制是1024个,可修改宏定义,但是效率仍然慢。
poll机制
- 基本原理与select一致,只是没有最大文件描述符限制,因为采用的是链表存储fd。
epoll机制
-
epoll之所以高性能是得益于它的三个函数
- epoll_create()系统启动时,在Linux内核里面申请一个B+树结构文件系统,返回epoll对象,也是一个fd
- epoll_ctl() 每新建一个连接,都通过该函数操作epoll对象,在这个对象里面修改添加删除对应的链接fd, 绑定一个callback函数。
- epoll_wait() 轮训所有的callback集合,并完成对应的IO操作
-
优点:
- 没fd这个限制,所支持的FD上限是操作系统的最大文件句柄数,1G内存大概支持10万个句柄
- 效率提高,使用回调通知而不是轮询的方式,不会随着FD数目的增加效率下降
- 内核和用户空间mmap同一块内存实现
同步&异步
同步和异步描述的是消息通信的机制。
-
同步
**代码调用IO操作时,必须等待IO操作完成才返回的调用方式。**当一个request发送出去以后,会得到一个response,这整个过程就是一个同步调用的过程。哪怕response为空,或者response的返回特别快,但是针对这一次请求而言就是一个同步的调用。
-
异步
**代码调用IO操作时,不必等待IO操作完成才返回的调用方式。**当一个request发送出去以后,没有得到想要的response,而是通过后面的callback、状态或者通知的方式获得结果。可以这么理解,对于异步请求分两步:
1.
调用方发送request没有返回对应的response(可能是一个空的response);2.
服务提供方将response处理完成以后通过callback的方式通知调用方。对于1. 而言是同步操作(调用方请求服务方),对于2. 而言也是同步操作(服务方回调调用方)。从请求的目的(调用方发送一个request,希望获得对应的response)来看,这两个步骤拆分开来没有任何意义;而需要结合起来看,这整个过程就是一次异步请求。
异步请求有一个最典型的特点:需要callback、状态或者通知的方式来告知调用方结果。
阻塞&非阻塞
阻塞和非阻塞描述的是程序在等待调用结果(消息,返回值)时的状态。函数调用的机制。
-
阻塞
阻塞调用是指调用方发出request的线程因为某种原因(如:等待系统资源)被服务方挂起,当服务方得到response后就唤醒挂起线程,并将response返回给调用方。
-
非阻塞
非阻塞调用是指调用方发出request的线程在没有等到结果时不会被挂起,直到得到response后才返回。
阻塞和非阻塞最大的区别是在调用方线程是否会被挂起。
Unix系统下的五种I/O模型
-
阻塞式I/O
-
非阻塞式I/O
-
I/O复用
-
信号驱动式I/O
-
异步I/O
select+回调+事件循环实现IO多路复用
import socket
from urllib.parse import urlparse
from selectors import DefaultSelector, EVENT_WRITE, EVENT_READ
import time
selector = DefaultSelector()
urls = []
STOP = False
class Fetcher(object):
def __init__(self):
self.data = b""
self.host = None
self.path = None
self.client = None
self.spider_url = None
def connected(self, key):
selector.unregister(key.fd)
self.client.send("GET {} HTTP/1.1\r\nHost:{}\r\nConnection:close\r\n\r\n".format(self.path, self.host).encode("utf8"))
selector.register(self.client.fileno(), EVENT_READ, self.readable)
def readable(self, key):
d = self.client.recv(1024)
if d:
self.data += d
else:
selector.unregister(key.fd)
data = self.data.decode("utf8")
html_data = data.split("\r\n\r\n")[1]
print(html_data)
self.client.close()
urls.remove(self.spider_url)
if not urls:
global STOP
STOP = True
def get_url(self, url):
self.spider_url = url
url = urlparse(url)
self.host = url.netloc
self.path = url.path
if self.path == "":
self.path = "/"
# 建立socket连接
self.client = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
self.client.setblocking(False)
try:
self.client.connect((self.host, 80))
except BlockingIOError as e:
pass
# 注册
selector.register(self.client.fileno(), EVENT_WRITE, self.connected)
def loop():
"""
事件循环,不停的请求socket的状态并调用对应的回调函数
模式: 回调+事件循环+select/poll/epoll
"""
# 1. selector本身是不支持register模式的
# 2. socket状态变化后的回调是由程序员完成的
# while True:
while not STOP:
ready = selector.select()
for key, mask in ready:
call_back = key.data
call_back(key)
if __name__ == "__main__":
# fetcher = Fetcher()
start = time.time()
for seq in range(20):
url = "http://shop.projectsedu.com/goods/{}/".format(seq)
urls.append(url)
fetcher = Fetcher()
fetcher.get_url(url)
loop()
print(time.time()-start)
参考链接
[1] IO多路复用机制
[2] Java IO多路复用机制详解
[3] Python高级核心技术97讲
[4] 同步异步/阻塞非阻塞