1 阻塞和非阻塞

 对于阻塞和非阻塞,网上有一个很形象的比喻,就是说好比你在等快递,阻塞模式就是快递如果不到,你就不能做其他事情。非阻塞模式就是在这段时间里面,你可以做其他事情,比如上网、打游戏、睡觉等,很显然非阻塞的模式会效率更高。
 非阻塞的模式也分两种,第一种就是忙轮询,因为你不知道快递什么时候来,所以你每5分钟就跟快递打一次电话进行询问,另外一种就是我们这篇文章讲的 epoll 模型,在等待快递到达的时间内,你尽可以做其他任何事情,包括睡觉,当快递到达时,你就会被告知。

 那么阻塞在操作系统中到底是如何进行的呢?假设有一个管道,进程A为管道的写入方,B为管道的读出方。

管道示意图

 假设一开始内核缓冲区是空的,B作为读出方,被阻塞着。然后首先A往管道写入,这时候内核缓冲区由空的状态变到非空状态,内核就会产生一个事件告诉B该醒来了,这个事件姑且称之为“缓冲区非空”。
 但是“缓冲区非空”事件通知B后,B却还没有读出数据;且内核许诺了不能把写入管道中的数据丢掉这个时候,A写入的数据会滞留在内核缓冲区中,如果内核也缓冲区满了,B仍未开始读数据,最终内核缓冲区会被填满,这个时候会产生一个I/O事件,告诉进程A,你该等等(阻塞)了,我们把这个事件定义为“缓冲区满”。
 假设后来B终于开始读数据了,于是内核的缓冲区空了出来,这时候内核会告诉A,内核缓冲区有空位了,你可以从长眠中醒来了,继续写数据了,我们把这个事件叫做“缓冲区非满”
 也许事件Y1已经通知了A,但是A也没有数据写入了,而B继续读出数据,知道内核缓冲区空了。这个时候内核就告诉B,你需要阻塞了!,我们把这个时间定为“缓冲区空”。

 这四个情形涵盖了四个I/O事件,缓冲区满,缓冲区空,缓冲区非空,缓冲区非满,这四个I/O事件是进行阻塞同步的根本。那么在我们的 client-server 模型是怎样发生阻塞的呢?
 socket 之间的通信就像这个管道,两端的 socket 会进入读取和写入。但请注意的是,写入仅仅表示数据被复制到了内核中的 TCP 发送缓冲区,至于什么时候发送到网络,什么时候被对方主机接收,什么时候被对方进程读取,系统调用层面不会给与任何通知。由于缓冲区的大小是有限的,当该socket的写入缓冲区满时会发生阻塞。所以,如果接收端进程从socket读数据的速度跟不上发送端进程向socket写数据的速度,会导致发送端write调用阻塞。
 读取阻塞则相对来说非常容易理解,就是该 socket 的读取缓冲区中没有数据时发生阻塞,通常是因为发送端的数据没有到达。如果想对缓冲区有一个感性的了解,可以在 Linux 下执行如下命令,查看本机 socket 的发送和读取缓冲区大小。如下图所示:

查看socket缓冲区

 既然 socket 在读写的过程中会存在阻塞,那么如何进行非阻塞的socket 读写呢?很简单,我们可以记录所有这些流,通过写一个 for 循环把所有socket流从头到尾问一遍。但这样的做法显然不好,因为某些 socket 没有数据,则只会浪费 CPU 的时间。那怎么解决这个问题呢?答案就是引进一个 代理,通过代理来观察许多流的I/O 事件,在空闲的时候把当前线程阻塞掉,当一个或多个流有 I/O 事件时,就从阻塞态醒来,这个代理就是 select, poll 和 epoll 模型。

2 select, poll, epoll 代理

select 和 poll

  select最早于1983年出现在4.2BSD中,它通过一个select()系统调用来监视多个文件描述符的数组,当select()返回后,该数组中就绪的文件描述符便会被内核修改标志位,使得进程可以获得这些文件描述符从而进行后续的读写操作。

while true {
select(streams[])
for i in streams[] {
if i has data
read until unavailable
}
}
select的优点是支持目前几乎所有的平台,缺点主要有如下2个:
 1)单个进程能够监视的文件描述符的数量存在最大限制,在Linux上一般为1024,不过可以通过修改宏定义甚至重新编译内核的方式提升这一限制。
 2)select 所维护的存储大量文件描述符的数据结构,随着文件描述符数量的增大,其复制的开销也线性增长。同时,由于网络响应时间的延迟使得大量TCP连接处于非活跃状态,但调用select()会对所有socket进行一次线性扫描,所以这也浪费了一定的开销。
 poll则在1986年诞生于System V Release 3,它和select在本质上没有多大差别,但是poll没有最大文件描述符数量的限制。

epoll

 epoll是Linux 2.6 开始出现的为处理大批量文件描述符而作了改进的poll,是Linux下多路复用IO接口select/poll的增强版本,它能显著提高程序在大量并发连接中只有少量活跃的情况下的系统CPU利用率。另一点原因就是获取事件的时候,它无须遍历整个被侦听的描述符集,只要遍历那些被内核IO事件异步唤醒而加入Ready队列的描述符集合就行了。
 在select/poll中,进程只有在调用一定的方法后,内核才对所有监视的文件描述符进行扫描,而epoll事先通过epoll_ctl()来注册一个文件描述符,一旦基于某个文件描述符就绪时,内核会采用类似callback的回调机制,迅速激活这个文件描述符,当进程调用epoll_wait()时便得到通知。

伪代码如下:

while true {
active_stream[] = epoll_wait(epollfd)
for i in active_stream[] {
read or write till
}
}

3 采用 select 和 epoll 代理重构网络并发模型

 通过上面的分析,我们知道采用代理可以一次处理多个连接,那么到底是如何实现的呢,我们以上节课的代码为基础,分别使用 select 和 epoll 代理进行重构,并比较它们之间的区别。先给出代码如下:
1) select 代理实现的 server代码:server_select.py

#coding:utf-8
import socket
from time import ctime
import select
import Queue HOST = ''
PORT = 21567
BUFSIZE = 1024
ADDR = ('127.0.0.1', PORT) # 服务器端创建 socket
serverSock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
serverSock.bind(ADDR)
serverSock.listen(5) inputs = [serverSock] outputs = [] timeout = 20 message_queues = {} while inputs:
print "doing select ..." readable, writable, exceptional = select.select(inputs, outputs, inputs, timeout) for s in readable: if s is serverSock:
server2client_Sock, addr = serverSock.accept()
print " Connection from "
server2client_Sock.setblocking(0)
inputs.append(server2client_Sock) message_queues[server2client_Sock] = Queue.Queue() else:
server2client_Sock = s data = server2client_Sock.recv(BUFSIZE) # 如果数据接收完,则退出 recv, 进入到下一个连接
if data:
# server2client_Sock.send('[%s] %s' % (ctime(), data))
print "Received data from ", server2client_Sock.getpeername()
data = '[%s] %s' % (ctime(), data) message_queues[server2client_Sock].put(data) # 将建立连接的 socket 放入到可以写的 socket 列表中
if server2client_Sock not in outputs:
outputs.append(server2client_Sock)
else:
if server2client_Sock in outputs:
outputs.remove(server2client_Sock) inputs.remove(server2client_Sock) server2client_Sock.close() del message_queues[server2client_Sock] if s in writable:
try:
next_msg = message_queues[s].get_nowait()
except Queue.Empty:
print " " , s.getpeername() , 'queue empty'
outputs.remove(s)
else:
print " sending " , next_msg , " to ", s.getpeername()
s.send(next_msg) serverSock.close()

2) epoll代理实现的 server代码:server_epoll.py

#coding:utf-8
import socket
from time import ctime
import select
import Queue HOST = ''
PORT = 21567
BUFSIZE = 1024
ADDR = ('127.0.0.1', PORT) # 服务器端创建 socket
serverSock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
serverSock.bind(ADDR)
serverSock.listen(5) timeout = 1000 # millisecond message_queues = {} # key state of socket io
READ_ONLY = ( select.POLLIN | select.POLLPRI | select.POLLHUP | select.POLLERR)
READ_WRITE = (READ_ONLY|select.POLLOUT) poller = select.poll()
poller.register(serverSock, READ_ONLY) fd_to_sockets = {serverSock.fileno(): serverSock, } while True:
print "Waiting for next event ..."
events = poller.poll(timeout) for fd, flag in events:
s = fd_to_sockets[fd]
if flag & (select.POLLIN | select.POLLPRI):
if s is serverSock:
server2client_Sock, addr = serverSock.accept()
print "Connetion from ", addr
server2client_Sock.setblocking(0) fd_to_sockets[server2client_Sock.fileno()] = server2client_Sock
poller.register(server2client_Sock, READ_ONLY) message_queues[server2client_Sock] = Queue.Queue() else:
server2client_Sock = s data = server2client_Sock.recv(BUFSIZE) if data:
print "Received data from ", server2client_Sock.getpeername()
data = '[%s] %s' % (ctime(), data) message_queues[server2client_Sock].put(data) # 将建立连接的 socket 放入到可以写的 socket 列表中
poller.modify(server2client_Sock, READ_WRITE)
else:
poller.unregister(server2client_Sock)
server2client_Sock.close()
del message_queues[server2client_Sock]
else:
try:
next_msg = message_queues[s].get_nowait()
except Queue.Empty:
print " " , s.getpeername() , 'queue empty'
poller.modify(s, READ_ONLY)
else:
print " sending " , next_msg , " to ", s.getpeername()
s.send(next_msg) serverSock.close()

对于 select 代理最核心的调用就是

readable, writable, exceptional = select.select(inputs, outputs, inputs, timeout)

该调用将可读可写的socket存储到 readable 和 writable 列表中,从而我们可以直接调用这些 socket的 recv 和 send 时不会发生阻塞。注意除了 serverSock 只读以外,其他 socket 都会存在同时存在于 inputs 和 outputs 列表中。

对于 epoll 代理最核心的就是

events = poller.poll(timeout)

该调用不需要输入观察的 socket,它是之前通过 register 来指定的。和 select 模式中的代码一样,这些 socket 都是可读可写的,通过如下代码实现:

poller.modify(server2client_Sock, READ_WRITE)

4 问题分析

  至此使用 epoll 代理重构我们的 server已经做完了,大家可以同时运行多个 client.py 进行交互,我们可以观察到该 server 具备了同时处理多个客户端连接的能力,而且所有的 socket 都是非阻塞的。
  然而,对于这样一个简单的服务(将客户端发送的数据加上时间戳返回)我们的代码结构看起来已经非常复杂,所以如果要处理我们实际业务中的逻辑处理简直是无法想象。
 另外,对于超高的并发请求,仅仅采用 epoll 模型是不够的,我们还必须使用多进程多线程等方式来充分利用系统资源。
 关于后面这个问题会是本系列文章重点讨论的部分,也是tornado 源码中的核心部分,我们会在稍微后面文章中去讨论。接下来的几篇文章中我们会尝试先梳理代码结构的问题,真正进入到 tornado 源码的部分,看看 tornado 的如何进行架构的。与其他torando源码文章不同的是,我们不会直接按模块挨个挨个分析代码,这种方式既晦涩难懂又非常低效,而是尝试从零开始搭建它,也就是说我们会仿造 tornado 的框架结构,从最简单开始,一步一步去实现 tornado,从而可以从内部逐步理清 tornado 这个优秀高并发框架的脉络。

tornado 采用 epoll 代理构建高并发网络模型的更多相关文章

  1. Microsoft Orleans构建高并发、分布式的大型应用程序框架

    Microsoft Orleans 在.net用简单方法构建高并发.分布式的大型应用程序框架. 原文:http://dotnet.github.io/orleans/ 在线文档:http://dotn ...

  2. nginx+lua+redis构建高并发应用(转)

    nginx+lua+redis构建高并发应用 ngx_lua将lua嵌入到nginx,让nginx执行lua脚本,高并发,非阻塞的处理各种请求. url请求nginx服务器,然后lua查询redis, ...

  3. 基于RTKLIB构建高并发通信测试工具

    1. RTKLIB基础动态库生成 RTKLIB是全球导航卫星系统GNSS(global navigation satellite system)的标准&精密定位开源程序包,由日本东京海洋大学的 ...

  4. 使用ngx_lua构建高并发应用(1)

    转自:http://blog.csdn.net/chosen0ne/article/details/7304192 一. 概述 Nginx是一个高性能,支持高并发的,轻量级的web服务器.目前,Apa ...

  5. 构建高并发&高可用&安全的IT系统-高并发部分

    什么是高并发? 狭义来讲就是你的网站/软件同一时间能承受的用户数量有多少 相关指标有 并发数:对网站/软件同时发起的请求数,一般也可代表实际的用户 每秒响应时间:常指一次请求到系统正确响的时间(以秒为 ...

  6. JAVA构建高并发商城秒杀系统——架构分析

    面试场景 我们打算组织一个并发一万人的秒杀活动,1元秒杀100个二手元牙刷,你给我说说解决方案. 秒杀/抢购业务场景 商品秒杀.商品抢购.群红包.抢优惠劵.抽奖....... 秒杀/抢购业务特点 秒杀 ...

  7. Nginx+Lua+Redis构建高并发应用

    一.  源文来自:http://www.ttlsa.com/nginx/nginx-lua-redis/ 二.  预览如下:

  8. 使用ngx_lua构建高并发应用

    http://blog.csdn.net/chosen0ne/article/details/7304192

  9. 高并发网络编程之epoll详解(转载)

    高并发网络编程之epoll详解(转载) 转载自:https://blog.csdn.net/shenya1314/article/details/73691088 在linux 没有实现epoll事件 ...

随机推荐

  1. Python 爬取 中关村CPU名字和主频

    0.准备工作   1.相关教程         Python 爬虫系列教程:http://cuiqingcai.com/1052.html         Python Web课程:http://ww ...

  2. ViewGroup事件分发机制解析

    最近在看View的事件分发机制,感觉比复杂的地方就是ViewGrop的dispatchTouchEvent函数,便对照着源码研究了一下.故名思意这个函数起到的作用就是分发事件,在具体分析之前还要说明几 ...

  3. 慢慢人生路,学点Jakarta基础-JavaDoc标记

    本文对使用Maven工程构建Jenkinsjob时遇到的问题进行一下分析汇总. JavaDoc标记使用问题 一般Maven项目都有配置产生Java DOC,但是在Jenkins里面产生DOC会有一些严 ...

  4. NOIp2017 滚粗记

    NOIp2017 滚粗记 Day0 早上 早自习的时候,班主任忽然告诉我们, 我们要参加期中考试... 这对于我们真是一个沉重的打击... 但是,管不着了 明天就死去考试了 上午 \(8:10\)到了 ...

  5. vue-过滤器filter

    vue-过滤器filter vue的过滤器一般在JavaScript 表达式的尾部,由"|"符号指示: 过滤器可以让我们的代码更加优美,一般可以用在时间格式化,首字母大写等等. 例 ...

  6. EntityFramework Core 1.1+ Backing Fields(返回字段)

    前言 通过我发表的博文可知最近一段时间会将持续讲解EntityFramework Core特性,在此之前我提到过Backing Fields,回头翻了翻感觉写的还不够好,于是乎再来讲解一番,也是自己再 ...

  7. face landmark 人脸特征点检测

    1.ASM&AAM算法 ASM(Active Shape Model)算法介绍:http://blog.csdn.net/carson2005/article/details/8194317 ...

  8. 在CentOS7中安装.Net Core2.0 SDK

    1.sudo yum install libunwind libicu(安装libicu依赖) 2.curl -sSL -o dotnet.tar.gz https://go.microsoft.co ...

  9. dhcp 的安装和配置文件

    install: yum  - y  install dhcp modify : vim  /etc/dhcp/dhcpd.conf ddns-update-style none;ignore cli ...

  10. linux修改时区

    Linux修改时区 原创作品,允许转载,转载时请务必以超链接形式标明文章 原始出处 .作者信息和本声明.否则将追究法律责任 CentOS6: 查看以前的时区: [root@localhost mysq ...