IO多路复用

同步io和异步io,阻塞io和非阻塞io分别是什么,有什么样的区别?

io模式

对于一次io 访问(以read为例),数据会先拷贝到操作系统内核的缓冲区,然后才会从操作系统内核的缓冲区拷贝到应用程序的地址空间。所以说,当一个read操作发生时,它会经历两个阶段:

1.等待数据准备

2.将数据从内核拷贝到用户的进程中

在linux中系统产生了下面五种网络模式的方案:

1.阻塞i/o   2.非阻塞i/o  3.i/o多路复用    4.信号驱动i/o(实际应用中不多)   5.异步i/o

阻塞i/o

在linux中,默认情况下所有的socket都是阻塞的(blocking),一个经典的读操作流程大概是这个样的:

当用户进程调用recvfrom是,kernel就开始了第一阶段的io,准备数据,这个过程需要等待,也就是说数据被拷贝到操作系统内核的缓冲区中是需要一个过程的,而在用户进程那,整个进程会被阻塞。当kernel一直等待数据准备好了,就会将数据从kernel中拷贝到用户内存,然后kernel返回结果,用户进程才解除阻塞状态,重新运行起来

即它的特点就是:在io执行的两个阶段都被block了

非阻塞I/O

linux下,可以通过设置socket使其变成了非阻塞,对一个非阻塞的socket执行读操作流程如下:

在用户进程发出read操作的时候,如果kernel中的数据还没有准备好的话,它不会阻塞用户进程,而是立刻返回一个error,从用户进程角度来说,它发起一个read操作之后,并不需要等待,而是马上得到了一个结果。用户进程判断结果如果是一个error,就知道了数据没有准备好,于是再次发送read操作,一旦kernel中数据准备好了,并且又再次收到了用户进程的system call,那么它马上就将数据拷贝到用户内存,然后返回

即它的特点就是:用户进程需要不断的主动的询问kernel数据准备好了没有

相当于我们第一次调用recv时,如果没有数据,我们可以去干其他的事情,从这个层面上来看,它比阻塞io要好,但仅仅只是非阻塞,下面我们看另外的模式:

I/O多路复用

io多路复用就是我们所说的select,poll,epoll,有些地方也称这种io方式为event driven io(事件驱动),select/epoll的好处就是在于单个process就可以同时处理多个网络连接的io。它的基本原理就是select ,poll,epoll这个function会不断的轮询所负责的所有的socket,当socket有数据的时候,就通知用户进程。

在单线程下,如果我们采取阻塞io,一收数据就卡住,我们就没有办法实现在一个单线程下操作多个socket连接,即单线程下阻塞io模式下是没有办法实现操作多路io

如果变成单线程下,非阻塞模式,建立100个连接,我们不知道哪个socket会发送数据,我们循环收取,100连接可能有5个发送了数据,95个没有发数据,但是没有关系,没有数据就直接走下一个循环不会卡住,最终把这5个数据收到,这种情况下我们已经实现了操作多个socket,在用户看来就已经是多并发了。不过有一个小问题,在从内核态到用户态发送的时候还是会卡住,等待一个copy时间,如果数据量小的话没有什么大问题,如果数据量大的话就会卡很久。虽然多并发,但是还是会卡

对于I/O多路复用:当用户进程调用了select,那么整个进程就会被block,同时,kernel会“监视”所有select负责所有的socket,当有一个socket中数据准备好,selct就会返回,这个时候用户进程再调用read,将数据从kernel拷贝到用户进程

即它的特点就是:通过一种机制一个进程能同时等待多个文件描述符(可以理解为socket连接),而这些文件描述符其中的任意一个进入读就绪状态,select()函数局可以返回

和阻塞io的区别就是:一开始是一个socket调用recv卡住了,而io多路复用是,一开始可能有很多个socket调用select()

异步I/O

用户进程发起read操作之后,立刻就可以去干其他的事情,另一方面,从kernel角度,他们收到一个异步的read操作之后,首先会立刻返回(用户就可以在这个时候去操作其他的东西),所以不会对用户进程产生任何的block。然后kernel会等待数据准备完成,再将数据拷贝到用户内存,当这一切完成之后,kernel会给用户进程发送一个signal,告诉它read操作完成了

就相当于在最后从内核态拷贝到用户态那个环节也不会卡住

下面我们就详细了解一下多路复用的select,poll,epoll:

select:

select目前支持所有的平台,其良好的跨平台是是他的一个优点,缺点就是单个进程能够监视的文件描述符的数量是存在最大限制的,在linux上一般为1024,不过可以通过修改宏定义甚至重新编译内核的方式提升,

另外,select()所维护的存储大量的文件描述符的数据结构,随着文件miaoshu描述符的数量增大,其复制的开销也就不断的增大。同时,由于网络响应时间的延迟使得大量tcp连接处于非活跃状态,但调用select()会对所有的socket进行一次线性扫描,所以在一定程度上造成了浪费

poll:

和select没有多大的区别,但是poll没有最大文件描述符数量的限制,缺点也差不多,

epoll:

epoll是由内核直接支持的实现方法,几乎具备了之前的一切优点。windows不支持epoll,linux支持

简单的说, 如果有100socket交给内核去检测,只不过当100里面有一个链接活跃了,这个内核会告诉用户哪一个链接有数据,所以用户直接取到这个链接去读数据就行了,这就是epoll最主要的有点。如果我们有6万个链接里面有两个活跃,只要循环去这两个就可以了。

epoll也可以同时支持水平触发和边缘触发(水平触发就是说100个连接中有两个有数据,活跃了,内核返回给用户的程序,用户去取数据,但是这个时候用户有其他的事情,没有来得及去处理,没有去取数据,那数据还在内核态,除非用户主动的去取数据,这个数据才会从内核态拷贝到用户态。下一次可以继续监测再通知这个有数据的连接里;边缘触发就是说如果数据来了,告诉用户数据准备好了可以来取了,如果用户没有去取数据,那就数据就一直存储在内核中,但是下次不会再次通知,所以数据对于用户来说就没有了)

epoll没有最大socket数的限制,依然是io多路复用,不是异步io

上面讲了这么多,我们用代码来看看这个selec,poll,epoll,看看实现出来是什么样子的,

select:

python的select()方法直接调用操作系统的io调用,它监控sockets,open files,and pipes何时变成readable,writeable,或者通信错误,select()使得同时监控多个连接变的简单,并且这个比写一个长循环来等待和监控客户端连接更加的高效,因为select直接通过操作系统提供的c的网络接口进行操作的。而不是python解释器

下面我们就用echo server例子来了解select是如何通过单进程实现同时处理多个非阻塞的socket连接的

import select
import socket
import queue server = socket.socket()
server.bind(('localhost',9999))
server.listen(1000) server.setblocking(False)#设置成非阻塞 inputs = [server,]
outputs = []
msg_dic = {}
while True:
readable,writeable,exceptional = select.select(inputs,outputs,inputs)
print(readable,writeable,exceptional)
for r in readable:
if r is server:#代表来了一个新连接
conn,addr = server.accept()
print("来了一个新连接",addr)
#是因为这个新建立的连接还没有发送数据过来,现在接受数据的话程序就会报错,所以要想实现这个客户端发数据来时,server端能够知道,就需要让
#select再检测这个conn
inputs.append(conn)
msg_dic[conn] = queue.Queue()#初始化一个队列,后面存要返回给这个客户端的数据
else:
data = r.recv(1024)
print("recv: ",data)
msg_dic[r].put(data)
#在下一次循环的时候发送数据
outputs.append(r)#放入返回的连接队列中 # r.send(data)
# print("send done....")
for w in writeable:#要返回给客户端的连接列表
data_to_client = msg_dic[w].get()
w.send(data_to_client)#返回给客户端原数据
outputs.remove(w)#确保下次循环的时候writeable不返回已经处理完的连接
for e in exceptional:
if e in outputs:
outputs.remove(e)
inputs.remove(e)
del msg_dic[e]

首先我们要用到的是server.setblocking(False)把他设置成非阻塞模式,

然后我们设置了inputs,outputs两个列表,我们想要内核帮我们监测100个连接,我们要传一个列表,有多少个连接就把他都放在inputs中,

使用select.select(),里面有3个参数,rlist,wlist,xlist,分别是你想要监测的连接,想要输出的数据,100个中如果有5个断开了操作系统就会把这些放到xlist

如果运行出现了这个错误的话,这个就是说在inputs这个列表中没有放入你要监测的连接,这里我们自己server本身就是一个监测对象,所以可以写入inputs,监测自己,然后运行就没有问题了:inputs[server,]

select.select()返回三个数据:readable,writeable,exceptional

如果我们在select.select()直接server.accept(),这样连接客户端之后server端就直接结束运行,所以我们要在前面用for循环,循环readable

在接受数据的时候我们可能会遇到这样的错误:(直接用recv方法接受数据,就是说在accept()之后直接用conn.recv(1024)接受数据)

这个是由于连接过来之后客户端没有发送数据,server端没有收到数据但是不阻塞,所以就会报错,那么我们怎么知道他发数据的时候知道,那我们就可以把这个连接加入到inputs列表,这样在下一次检测的时候,如果活动了,说明有数据过来了:inputs.append(conn),这个时候相当于inputs这个列表里有server,conn两个连接,如果是返回活动的是server说明是又来了一个新连接,加入到inputs中,如果返回活动的是conn,那我们就可以直接接收数据,所以要用if判断。

在接收数据和发送数据的时候,我们接受数据可以放在一个队列字典中,等一会发送给客户端。所以我们在建立连接 的时候就可以给这个连接建立一个queue,然后在接受的数据之后可以先存到这个连接的字典中,那我们什么时候发送呢?

我们是在下一次循环发送数据,怎么实现呢,需要用我们刚才的outputs列表,你往outputs中放什么数据,你就会输出什么数据。所以就用到上面代码中的循环等,在用完之后要删除。

当然上述代码还不是很完善,当我们在关闭一个客户端运行的时候,我们server端也就停止运行了:

服务器端:

 import queue
import select
import socket server = socket.socket()
server.bind(("localhost",9999))
server.setblocking(False)
server.listen(1000) inputs = [server,]
outputs = []
msg_dic = {} while True:
print("waiting for next event....")
readable,writeable,exceptional = select.select(inputs,outputs,inputs)
#print(readable,writeable,exceptional)
for r in readable:#每个r就是一个新的连接
if r is server:#这里判断如果是server就表明有新的连接进入,之前我们就已经把我们自己server放在了inputs中
conn,addr = r.accept()
conn.setblocking(False)
inputs.append(conn)#为了不阻塞整个程序,我们不会立刻在这里开始接收客户端发来的数据, 把它放到inputs里, 下一次loop时,这个新连接
#就会被交给select去监听,如果这个连接的客户端发来了数据 ,那这个连接的fd在server端就会变成就续的,select就会把这个连接返回,返回到
#readable 列表里,然后你就可以loop readable列表,取出这个连接,开始接收数据了, 下面就是这么干的
msg_dic[conn] = queue.Queue()#接受到的客户端的数据之后,不立刻返回,而是存在消息队列中,以后发送
else:#如果r不是server的话,那就说明没有新的连接进入,我们需要接受客户端传过来的数据,
data = r.recv(1024)
if data:
print("收到来自[%s]发过来的数据:%s" %(r.getpeername()[0],data))
msg_dic[r].put(data)
if r not in outputs:
outputs.append(r)#为了不影响处理与其他客户端的连接,这里不立刻返回数据
else:#如果收不到数据,代表客户端断开
print("客户端断开",r)
#清理已经断开的连接
if r in outputs:
outputs.remove(r)
inputs.remove(r)
del msg_dic[r]
for w in writeable:
try:
next_msg = msg_dic[w].get_nowait() except queue.Empty:
print("client [%s]" % w.getpeername()[0], "queue is empty..")
outputs.remove(w) else:
print("sending msg to [%s]" % w.getpeername()[0], next_msg)
w.send(next_msg.upper())
for e in exceptional:
print("handling exception for ",e.getpeername())
inputs.remove(e)
if e in outputs:
outputs.remove(e)
e.close() del msg_dic[e]

客户端:

 import socket

 messages = [ b'This is the message. ',
b'It will be sent ',
b'in parts.',
]
server_address = ('localhost', 9999) socks = [ socket.socket(socket.AF_INET, socket.SOCK_STREAM),
socket.socket(socket.AF_INET, socket.SOCK_STREAM),
]
for s in socks:
s.connect(server_address) for message in messages: # Send messages on both sockets
for s in socks:
print('%s: sending "%s"' % (s.getsockname(), message) )
s.send(message) # Read responses on both sockets
for s in socks:
data = s.recv(1024)
print( '%s: received "%s"' % (s.getsockname(), data) )
if not data:
print('closing socket', s.getsockname() )

这样可以在客户端中多并发的执行多个socket

selector模块

直接上代码:

 import selectors
import socket sel = selectors.DefaultSelector() def accept(sock, mask):
conn, addr = sock.accept()
print('accepted', conn, 'from', addr)
conn.setblocking(False)
sel.register(conn, selectors.EVENT_READ, read) def read(conn, mask):
data = conn.recv(1000)
if data:
print('echoing', repr(data), 'to', conn)
conn.send(data)
else:
print('closing', conn)
sel.unregister(conn)
conn.close() sock = socket.socket()
sock.bind(('localhost', 9999))
sock.listen(100)
sock.setblocking(False)
sel.register(sock, selectors.EVENT_READ, accept) while True:
events = sel.select()#默认是阻塞,有活动连接就返回活动的连接列表
for key, mask in events:
callback = key.data#相当于调用accept
callback(key.fileobj, mask)#key.fileobj就是文件句柄,相当于上面程序的r

selector.DefaultSelector()和select.select()差不多,生成一个select对象,然后下面用sel.register(sock,selectors.Event_READ,accept)注册一个事件,让它监听,只要来一个新的连接就调用accept函数

events = sel.select()这句话也有可能调用的是epoll,看你系统支持什么

然后在结合客户端代码,就可以了,速度很快:

初次学习,做做笔记,用于加深印象,如果有错误的话,希望大家积极纠正,在此谢过大家啦

Select/Poll/Epoll异步IO的更多相关文章

  1. Python之路-python(Queue队列、进程、Gevent协程、Select\Poll\Epoll异步IO与事件驱动)

    一.进程: 1.语法 2.进程间通讯 3.进程池 二.Gevent协程 三.Select\Poll\Epoll异步IO与事件驱动 一.进程: 1.语法 简单的启动线程语法 def run(name): ...

  2. Python自动化 【第十篇】:Python进阶-多进程/协程/事件驱动与Select\Poll\Epoll异步IO

    本节内容: 多进程 协程 事件驱动与Select\Poll\Epoll异步IO   1.  多进程 启动多个进程 进程中启进程 父进程与子进程 进程间通信 不同进程间内存是不共享的,要想实现两个进程间 ...

  3. Select\Poll\Epoll异步IO与事件驱动

    事件驱动与异步IO 事件驱动编程是一种编程规范,这里程序的执行流由外部事件来规定.它的特点是包含一个事件循环,但外部事件发生时使用回调机制来触发响应的处理.另外两种常见的编程规范是(单线程)同步以及多 ...

  4. python网络编程-Select\Poll\Epoll异步IO

    首先列一下,sellect.poll.epoll三者的区别 select select最早于1983年出现在4.2BSD中,它通过一个select()系统调用来监视多个文件描述符的数组,当select ...

  5. Python 协程/异步IO/Select\Poll\Epoll异步IO与事件驱动

    1 Gevent 协程 协程,又称微线程,纤程.英文名Coroutine.一句话说明什么是线程:协程是一种用户态的轻量级线程. 协程拥有自己的寄存器上下文和栈.协程调度切换时,将寄存器上下文和栈保存到 ...

  6. python Select\Poll\Epoll异步IO与事件驱动

    参考:http://www.cnblogs.com/alex3714/articles/5248247.html 写服务器处理模型的程序时通常采用的模型: (1)每收到一个请求,创建一个新的进程,来处 ...

  7. 转一贴,今天实在写累了,也看累了--【Python异步非阻塞IO多路复用Select/Poll/Epoll使用】

    下面这篇,原理理解了, 再结合 这一周来的心得体会,整个框架就差不多了... http://www.haiyun.me/archives/1056.html 有许多封装好的异步非阻塞IO多路复用框架, ...

  8. Python异步非阻塞IO多路复用Select/Poll/Epoll使用,线程,进程,协程

    1.使用select模拟socketserver伪并发处理客户端请求,代码如下: import socket import select sk = socket.socket() sk.bind((' ...

  9. 哪5种IO模型?什么是select/poll/epoll?同步异步阻塞非阻塞有啥区别?全在这讲明白了!

    系统中有哪5种IO模型?什么是 select/poll/epoll?同步异步阻塞非阻塞有啥区别? 本文地址http://yangjianyong.cn/?p=84转载无需经过作者本人授权 先解开第一个 ...

随机推荐

  1. Makefile的补充学习

    通配符%和Makefile自动推导(规则)(1)%是Makefile中的通配符,代表一个或几个字母.也就是说%.o就代表所有以.o为结尾的文件.(2)所谓自动推导其实就是Makefile的规则.当Ma ...

  2. 20165210 Java第五次实验报告

    20165210 实验五 网络编程与安全 网络编程与安全一: 实验要求: 两人一组结对编程: 参考http://www.cnblogs.com/rocedu/p/6766748.html#SECDSA ...

  3. 【python】类file文件处理

    [flush方法] 通常由于缓冲,write不将数据写入文件,而是写入内存的缓冲区,需要使用flush写入文件,并清空缓冲区 文件的flush方法的作用是强制清空缓存写入文件.默认每行flush一下? ...

  4. Near Field Communication (NFC) applications

    Near Field Communication (NFC) applications There has been little practical guidance available on NF ...

  5. ubuntu 上的ruby安装

    安装 rbenv git clone git://github.com/sstephenson/rbenv.git ~/.rbenv # 用来编译安装 ruby git clone git://git ...

  6. (十八)js控制台方法

    console.log 以日志的形式打印 console.warn 输出警示信息 console.info 输出提示信息 console.error 输出错误信息 console.debug 输出调试 ...

  7. 【学习】JennyHui学英语 - 生词积累

    (主要是来自于普特英语) 平常积累 20140820 20140826 VOA慢速英语生词积累 2014-10-08 2014-10-11 2014-10-13 2014-10-14 2014-10- ...

  8. python笔记-4(装饰器、生成器、迭代器)

    一.熟练掌握装饰器的原理 (在装饰器学习的过程中,查了看了很多资料,个人感觉走了很多的弯路,这个笔记,分享我的理解,希望能帮助到一些人.本文对装饰器的描述,侧重点是条理与逻辑思路,想通过从无到有的方式 ...

  9. numpy 矩阵相关函数

    我们 知道,矩阵在python里面用的不少,所以记载下关于矩阵的操作 numpy.zeros():可以用来构造全零矩阵 >>> zeros(3) array([ 0.,  0.,   ...

  10. Java中取整和四舍五入

    import java.math.BigDecimal;  import java.text.DecimalFormat; public class TestGetInt{  public stati ...