本节内容
- 事件驱动
-
IO多路复用
- 用户空间和内存空间
- 进程切换
- 进程的阻塞
- 文件描述符fd
- 缓存IO
- Select\Poll\Epoll异步IO
- Twsited异步网络框架
一、事件驱动
事件驱动编程是一种编程范式,这里程序的执行流由外部事件来决定。它的特点是包含一个事件循环,当外部事件发生时使用回调机制来触发相应的处理。另外两种常见的编程范式是(单线程)同步以及多线程编程。
当我们面对如下的环境时,事件驱动模型通常是一个好的选择:
- 程序中有许多任务
- 任务之间高度独立(因此它们不需要互相通信,或者等待彼此)
- 在等待事件到来时,某些任务会阻塞。
- 当应用程序需要在任务间共享可变的数据时,这也是一个不错的选择,因为这里不需要采用同步处理。
网络应用程序通常都有上述这些特点,这使得它们能够很好的契合事件驱动编程模型。
二、IO多路复用
具体介绍见:IO多路复用(番外篇)
讨论的背景是Linux环境下的network IO
IO多路复用(epool)和gevent模块(协程)区别与联系:
都是遇到IO就切换,在linux下底层都是通过libevent.so实现。可以认为gevent是对IO多路复用更上层的封装,IO多路复用是其默认设置,其更专注于任务之间的切换。
1.用户空间和内核空间
操作系统的核心是内核,可以访问受保护的内存空间,也有访问底层硬件设备的所有权限。为了保证内核的安全,用户进程不能直接操作内核(kernel),系统将虚拟空间划分为两部分,一部分为内核空间,一部分为用户空间。
2.进程切换
为了控制进程的执行,内核必须有能力挂起正在CPU上运行的进程,并恢复以前挂起的某个进程的执行。这种行为被称为进程切换。
3.进程的阻塞
正在执行的进程,由于期待的某些事件未发生,如请求系统资源失败、等待某种操作的完成、新数据尚未到达或无新工作做等,则由系统自动执行阻塞原语(Block),使自己由运行状态变为阻塞状态。可见,进程的阻塞是进程自身的一种主动行为,也因此只有处于运行态的进程(获得CPU),才可能将其转为阻塞状态。当进程进入阻塞状态,是不占用CPU资源的。
4.文件描述符fd
文件描述符(File descriptor)是计算机科学中的一个术语,是一个用于表述指向文件的引用的抽象化概念。文件描述符在形式上是一个非负整数。实际上,它是一个索引值,指向内核为每一个进程所维护的该进程打开文件的记录表。(文件句柄是真实的文件对象)
注:文件描述符这一概念往往只适用于UNIX、Linux这样的操作系统。
5.缓存 I/O
又被称作标准 I/O,大多数文件系统的默认 I/O 操作都是缓存 I/O。在 Linux 的缓存 I/O 机制中,操作系统会将 I/O 的数据缓存在文件系统的页缓存( page cache )中,也就是说,数据会先被拷贝到操作系统内核的缓冲区中,然后才会从操作系统内核的缓冲区拷贝到应用程序的地址空间。
缺点:
数据在传输过程中需要在应用程序地址空间和内核进行多次数据拷贝操作,这些数据拷贝操作所带来的 CPU 以及内存开销是非常大的。
6.IO模式
对于一次IO访问(以read举例),数据会先被拷贝到操作系统内核的缓冲区中,然后才会从操作系统内核的缓冲区拷贝到应用程序的地址空间。(内核态到用户态数据拷贝)
所以,当一个read操作发生时,它会经历两个阶段:
1. 等待数据准备 (Waiting for the data to be ready)
2. 将数据从内核拷贝到进程中 (Copying the data from the kernel to the process)
正是因为这两个阶段,linux系统产生了下面五种网络模式的方案。
- 阻塞 I/O(blocking IO)
- 非阻塞 I/O(nonblocking IO)
- I/O 多路复用( IO multiplexing)
- 信号驱动 I/O( signal driven IO,不常用)
- 异步 I/O(asynchronous IO,实现复杂用得少)
阻塞IO(bloking IO)
linux下,默认情况下所有的socket都是blocking。其特点就是在IO执行的两个阶段都是阻塞的 。

非阻塞 I/O(nonblocking IO)
linux下,可以通过设置socket使其变为non-blocking。其特点是IO执行的第一个阶段不阻塞,用户进程不断的主动询问kernel数据有没有准备好,kernel做出相应的回应,但IO执行的第二个阶段仍然是阻塞的。

I/O 多路复用( IO multiplexing)
就是我们说的select,poll,epoll,也称这种IO方式为event driven IO。select/epoll的好处就在于单个process就可以同时处理多个网络连接的IO。它的基本原理就是select,poll,epoll这个function会不断的轮询所负责的所有socket,当某个socket有数据到达了,就通知用户进程数据准备好了,但IO执行的第二个阶段仍然是阻塞的。

这个图和blocking IO的图其实并没有太大的不同,事实上,其性能还更差一些。因为这里需要使用两个system call (select 和 recvfrom),而blocking IO只调用了一个system call (recvfrom)。但是,用select的优势在于它可以同时处理多个connection。
所以,如果处理的连接数不是很高的话,使用select/epoll的web server不一定比使用multi-threading + blocking IO的web server性能更好,可能延迟还更大。select/epoll的优势并不是对于单个连接能处理得更快,而是在于能处理更多的连接。
在IO multiplexing Model中,实际中,对于每一个socket,一般都设置成为non-blocking,但是,如上图所示,整个用户的process其实是一直被block的。只不过process是被select这个函数block,而不是被socket IO给block。
异步 I/O(asynchronous IO)
linux下的asynchronous IO其实用得很少。

用户进程发起read操作之后,就可以开始去做其它的事。从kernel的角度,当它收到一个asynchronous read之后,首先它会立刻返回信号给用户进程(确认收到请求的信号?),所以不会对用户进程产生任何block。然后,kernel等待数据准备完成后,将数据拷贝到用户内存,拷贝完成之后,kernel会给用户进程发送一个signal,告诉它read操作完成了。
各个IO Model的比较

三、异步IO(Select\Poll\Epoll)
具体介绍见:Python Select 解析
select目前几乎在所有的平台上支持,其良好跨平台支持也是它的一个优点,但是使用select单个进程能够监视的文件描述符的数量存在最大限制,在Linux上一般为1024,不过可以通过修改宏定义甚至重新编译内核的方式提升这一限制。select监视到文件描述符有活跃,只向用户进程返回有活跃信号,没有返回具体活跃的文件描述符,用户进程还需再次循环检查浪费时间和资源。
poll和select在本质上没有多大差别,但是poll没有最大文件描述符数量的限制,可以看作是一个过渡阶段。
epoll直到Linux2.6才出现,它几乎具备了之前所说的一切优点,被公认为Linux2.6下性能最好的多路I/O就绪通知方法。epoll可以同时支持水平触发和边缘触发(Edge Triggered,只告诉进程哪些文件描述符刚刚变为就绪状态,它只说一遍,如果我们没有采取行动,那么它将不会再次告知,这种方式称为边缘触发),理论上边缘触发的性能要更高一些,但是代码实现相当复杂。
注:epoll在linux上的文件描述符数量,可能会受到OS用户最大连接数限制,注意调整。
1.用select实现socket服务端
服务端
-
import select
-
import socket
-
import queue
-
-
server = socket.socket()
-
server.bind(('localhost',9000))
-
server.listen(1000)
-
-
server.setblocking(False) #必须要先设置不阻塞
-
-
msg_dic = {}
-
-
inputs = [server,]
-
#inputs = [server,conn] #[conn,]
-
#inputs = [server,conn,conn2] #[conn2,]
-
outputs = [] #
-
#outputs = [r1,] #
-
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)
-
inputs.append(conn) #是因为这个新建立的连接还没发数据过来,现在就接收的话程序就报错了,
-
#所以要想实现这个客户端发数据来时server端能知道,就需要让select再监测这个conn
-
msg_dic[conn] = queue.Queue() #初始化一个队列,后面存要返回给这个客户端的数据
-
else: #conn2
-
data = r.recv(1024)
-
print("收到数据",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]
客户端
-
import socket
-
-
HOST = 'localhost' # The remote host
-
PORT = 9999 # The same port as used by the server
-
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
-
s.connect((HOST, PORT))
-
while True:
-
msg = bytes(input(">>:"), encoding="utf8")
-
s.sendall(msg)
-
data = s.recv(1024)
-
-
#
-
print('Received', data)
-
s.close()
2.selectors模块
封装好的IO多路复用模块,默认使用epool,当系统不支持时使用select。
服务端
-
import selectors
-
import socket
-
-
sel = selectors.DefaultSelector()
-
-
-
def accept(sock, mask):
-
conn, addr = sock.accept() # Should be ready
-
print('accepted', conn, 'from', addr,mask)
-
conn.setblocking(False)
-
sel.register(conn, selectors.EVENT_READ, read) #新连接注册read回调函数
-
-
-
def read(conn, mask):
-
data = conn.recv(1024) # Should be ready
-
if data:
-
print('echoing', repr(data), 'to', conn)
-
conn.send(data) # Hope it won't block
-
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= 文件句柄
多并发客户端
-
import socket
-
import sys
-
-
messages = [ b'This is the message. ',
-
b'It will be sent ',
-
b'in parts.',
-
]
-
server_address = ('192.168.16.130', 9998)
-
-
# Create a TCP/IP socket
-
socks = [ socket.socket(socket.AF_INET, socket.SOCK_STREAM) for i in range(11000)]
-
print(socks)
-
# Connect the socket to the port where the server is listening
-
print('connecting to %s port %s' % server_address)
-
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() )
四、Twsited异步网络框架
Twisted自己用异步形式重写了SSH、DNS、FTP、HTTP等,代码比较复杂,游戏开发会用到。
http://www.cnblogs.com/alex3714/articles/5248247.html
参考:
http://www.cnblogs.com/alex3714/articles/5248247.html
http://www.cnblogs.com/alex3714/articles/5876749.html
http://www.cnblogs.com/alex3714/p/4372426.html

浙公网安备 33010602011771号