有免费搭建app的网站吗,345诛仙网站是谁做的,怎么用qq邮箱做网站,wordpress工具箱主题事件驱动模型 上节的问题#xff1a; 协程#xff1a;遇到IO操作就切换。 但什么时候切回去呢#xff1f;怎么确定IO操作完了#xff1f; 很多程序员可能会考虑使用“线程池”或“连接池”。“线程池”旨在减少创建和销毁线程的频率#xff0c;其维持一定合理数量的线程 协程遇到IO操作就切换。 但什么时候切回去呢怎么确定IO操作完了 很多程序员可能会考虑使用“线程池”或“连接池”。“线程池”旨在减少创建和销毁线程的频率其维持一定合理数量的线程并让空闲的线程重新承担新的执行任务。“连接池”维持连接的缓存池尽量重用已有的连接、减少创建和关闭连接的频率。这两种技术都可以很好的降低系统开销都被广泛应用很多大型系统如websphere、tomcat和各种数据库等。但是“线程池”和“连接池”技术也只是在一定程度上缓解了频繁调用IO接口带来的资源占用。而且所谓“池”始终有其上限当请求大大超过上限时“池”构成的系统对外界的响应并不比没有池的时候效果好多少。所以使用“池”必须考虑其面临的响应规模并根据响应规模调整“池”的大小。
对应上例中的所面临的可能同时出现的上千甚至上万次的客户端请求“线程池”或“连接池”或许可以缓解部分压力但是不能解决所有问题。总之多线程模型可以方便高效的解决小规模的服务请求但面对大规模的服务请求多线程模型也会遇到瓶颈可以用非阻塞接口来尝试解决这个问题 传统的编程是如下线性模式的 开始---代码块A---代码块B---代码块C---代码块D---......---结束 每一个代码块里是完成各种各样事情的代码但编程者知道代码块A,B,C,D...的执行顺序唯一能够改变这个流程的是数据。输入不同的数据根据条件语句判断流程或许就改为A---C---E...---结束。每一次程序运行顺序或许都不同但它的控制流程是由输入数据和你编写的程序决定的。如果你知道这个程序当前的运行状态包括输入数据和程序本身那你就知道接下来甚至一直到结束它的运行流程。 对于事件驱动型程序模型它的流程大致如下 开始---初始化---等待 与上面传统编程模式不同事件驱动程序在启动之后就在那等待等待什么呢等待被事件触发。传统编程下也有“等待”的时候比如在代码块D中你定义了一个input()需要用户输入数据。但这与下面的等待不同传统编程的“等待”比如input()你作为程序编写者是知道或者强制用户输入某个东西的或许是数字或许是文件名称如果用户输入错误你还需要提醒他并请他重新输入。事件驱动程序的等待则是完全不知道也不强制用户输入或者干什么。只要某一事件发生那程序就会做出相应的“反应”。这些事件包括输入信息、鼠标、敲击键盘上某个键还有系统内部定时器触发。 一、事件驱动模型介绍 通常我们写服务器处理模型的程序时有以下几种模型 1每收到一个请求创建一个新的进程来处理该请求
2每收到一个请求创建一个新的线程来处理该请求
3每收到一个请求放入一个事件列表让主进程通过非阻塞I/O方式来处理请求 第三种就是协程、事件驱动的方式一般普遍认为第3种方式是大多数网络服务器采用的方式 论事件驱动模型 !DOCTYPE html
html langen
headmeta charsetUTF-8titleTitle/title/head
bodyp οnclickfun()点我呀/pscript typetext/javascriptfunction fun() {alert(约吗?)}
/script
/body/html 在UI编程中常常要对鼠标点击进行相应首先如何获得鼠标点击呢 两种方式 1创建一个线程循环检测是否有鼠标点击 那么这个方式有以下几个缺点 CPU资源浪费可能鼠标点击的频率非常小但是扫描线程还是会一直循环检测这会造成很多的CPU资源浪费如果扫描鼠标点击的接口是阻塞的呢如果是堵塞的又会出现下面这样的问题如果我们不但要扫描鼠标点击还要扫描键盘是否按下由于扫描鼠标时被堵塞了那么可能永远不会去扫描键盘如果一个循环需要扫描的设备非常多这又会引来响应时间的问题 所以该方式是非常不好的。2 就是事件驱动模型 目前大部分的UI编程都是事件驱动模型如很多UI平台都会提供onClick()事件这个事件就代表鼠标按下事件。事件驱动模型大体思路如下 有一个事件消息队列鼠标按下时往这个队列中增加一个点击事件消息有个循环不断从队列取出事件根据不同的事件调用不同的函数如onClick()、onKeyDown()等事件消息一般都各自保存各自的处理函数指针这样每个消息都有独立的处理函数 事件驱动编程是一种编程范式这里程序的执行流由外部事件来决定。它的特点是包含一个事件循环当外部事件发生时使用回调机制来触发相应的处理。另外两种常见的编程范式是单线程同步以及多线程编程。 让我们用例子来比较和对比一下单线程、多线程以及事件驱动编程模型。下图展示了随着时间的推移这三种模式下程序所做的工作。这个程序有3个任务需要完成每个任务都在等待I/O操作时阻塞自身。阻塞在I/O操作上所花费的时间已经用灰色框标示出来了。 最初的问题怎么确定IO操作完了切回去呢通过回调函数 1.要理解事件驱动和程序就需要与非事件驱动的程序进行比较。实际上现代的程序大多是事件驱动的比如多线程的程序肯定是事件驱动的。早期则存在许多非事件驱动的程序这样的程序在需要等待某个条件触发时会不断地检查这个条件直到条件满足这是很浪费cpu时间的。而事件驱动的程序则有机会释放cpu从而进入睡眠态注意是有机会当然程序也可自行决定不释放cpu当事件触发时被操作系统唤醒这样就能更加有效地使用cpu.
2.再说什么是事件驱动的程序。一个典型的事件驱动的程序就是一个死循环并以一个线程的形式存在这个死循环包括两个部分第一个部分是按照一定的条件接收并选择一个要处理的事件第二个部分就是事件的处理过程。程序的执行过程就是选择事件和处理事件而当没有任何事件触发时程序会因查询事件队列失败而进入睡眠状态从而释放cpu。
3.事件驱动的程序必定会直接或者间接拥有一个事件队列用于存储未能及时处理的事件。
4.事件驱动的程序的行为完全受外部输入的事件控制所以事件驱动的系统中存在大量这种程序并以事件作为主要的通信方式。
5.事件驱动的程序还有一个最大的好处就是可以按照一定的顺序处理队列中的事件而这个顺序则是由事件的触发顺序决定的这一特性往往被用于保证某些过程的原子化。
6.目前windows,linux,nucleus,vxworks都是事件驱动的只有一些单片机可能是非事件驱动的。 注意事件驱动的监听事件是由操作系统调用的cpu来完成的 IO多路复用 前面是用协程实现的IO阻塞自动切换那么协程又是怎么实现的在原理是是怎么实现的。如何去实现事件驱动的情况下IO的自动阻塞的切换这个学名叫什么呢 IO多路复用 比如socketserver多个客户端连接单线程下实现并发效果就叫多路复用。 同步IO和异步IO阻塞IO和非阻塞IO分别是什么到底有什么区别不同的人在不同的上下文下给出的答案是不同的。所以先限定一下本文的上下文。 本文讨论的背景是Linux环境下的network IO。 1 IO模型前戏准备 在进行解释之前首先要说明几个概念 用户空间和内核空间进程切换进程的阻塞文件描述符缓存 I/O用户空间与内核空间 现在操作系统都是采用虚拟存储器那么对32位操作系统而言它的寻址空间虚拟存储空间为4G2的32次方。 操作系统的核心是内核独立于普通的应用程序可以访问受保护的内存空间也有访问底层硬件设备的所有权限。 为了保证用户进程不能直接操作内核kernel保证内核的安全操心系统将虚拟空间划分为两部分一部分为内核空间一部分为用户空间。 针对linux操作系统而言将最高的1G字节从虚拟地址0xC0000000到0xFFFFFFFF供内核使用称为内核空间而将较低的3G字节从虚拟地址0x00000000到0xBFFFFFFF供各个进程使用称为用户空间。 进程切换 为了控制进程的执行内核必须有能力挂起正在CPU上运行的进程并恢复以前挂起的某个进程的执行。这种行为被称为进程切换这种切换是由操作系统来完成的。因此可以说任何进程都是在操作系统内核的支持下运行的是与内核紧密相关的。 从一个进程的运行转到另一个进程上运行这个过程中经过下面这些变化 保存处理机上下文包括程序计数器和其他寄存器。 更新PCB信息。 把进程的PCB移入相应的队列如就绪、在某事件阻塞等队列。 选择另一个进程执行并更新其PCB。 更新内存管理的数据结构。 恢复处理机上下文。 注总而言之就是很耗资源的 进程的阻塞 正在执行的进程由于期待的某些事件未发生如请求系统资源失败、等待某种操作的完成、新数据尚未到达或无新工作做等则由系统自动执行阻塞原语(Block)使自己由运行状态变为阻塞状态。可见进程的阻塞是进程自身的一种主动行为也因此只有处于运行态的进程获得CPU才可能将其转为阻塞状态。当进程进入阻塞状态是不占用CPU资源的。 文件描述符fd 文件描述符File descriptor是计算机科学中的一个术语是一个用于表述指向文件的引用的抽象化概念。 文件描述符在形式上是一个非负整数。实际上它是一个索引值指向内核为每一个进程所维护的该进程打开文件的记录表。当程序打开一个现有文件或者创建一个新文件时内核向进程返回一个文件描述符。在程序设计中一些涉及底层的程序编写往往会围绕着文件描述符展开。但是文件描述符这一概念往往只适用于UNIX、Linux这样的操作系统。 缓存 I/O 缓存 I/O 又被称作标准 I/O大多数文件系统的默认 I/O 操作都是缓存 I/O。在 Linux 的缓存 I/O 机制中操作系统会将 I/O 的数据缓存在文件系统的页缓存 page cache 中也就是说数据会先被拷贝到操作系统内核的缓冲区中然后才会从操作系统内核的缓冲区拷贝到应用程序的地址空间。用户空间没法直接访问内核空间的内核态到用户态的数据拷贝 思考为什么数据一定要先到内核区直接到用户内存不是更直接吗缓存 I/O 的缺点 数据在传输过程中需要在应用程序地址空间和内核进行多次数据拷贝操作这些数据拷贝操作所带来的 CPU 以及内存开销是非常大的。 同步synchronous IO和异步asynchronous IO阻塞blocking IO和非阻塞non-blockingIO分别是什么到底有什么区别这个问题其实不同的人给出的答案都可能不同比如wiki就认为asynchronous IO和non-blocking IO是一个东西。这其实是因为不同的人的知识背景不同并且在讨论这个问题的时候上下文(context)也不相同。所以为了更好的回答这个问题我先限定一下本文的上下文。本文讨论的背景是Linux环境下的network IO。 Stevens在文章中一共比较了五种IO Model blocking IO nonblocking IO IO multiplexing signal driven IO asynchronous IO由于signal driven IO在实际中并不常用所以我这只提及剩下的四种IO Model。再说一下IO发生时涉及的对象和步骤。 对于一个network IO (这里我们以read举例)它会涉及到两个系统对象一个是调用这个IO的process (or thread)另一个就是系统内核(kernel)。当一个read操作发生时它会经历两个阶段 1 等待数据准备 (Waiting for the data to be ready) 2 将数据从内核拷贝到进程中 (Copying the data from the kernel to the process)记住这两点很重要因为这些IO Model的区别就是在两个阶段上各有不同的情况。 2 blocking IO 阻塞IO 在linux中默认情况下所有的socket都是blocking一个典型的读操作流程大概是这样 当用户进程调用了recvfrom这个系统调用kernel就开始了IO的第一个阶段准备数据。对于network io来说很多时候数据在一开始还没有到达比如还没有收到一个完整的UDP包这个时候kernel就要等待足够的数据到来。而在用户进程这边整个进程会被阻塞。当kernel一直等到数据准备好了它就会将数据从kernel中拷贝到用户内存然后kernel返回结果用户进程才解除block的状态重新运行起来。所以blocking IO的特点就是在IO执行的两个阶段都被block了。 3 non-blocking IO非阻塞IO linux下可以通过设置socket使其变为non-blocking。当对一个non-blocking socket执行读操作时流程是这个样子 从图中可以看出当用户进程发出read操作时如果kernel中的数据还没有准备好那么它并不会block用户进程而是立刻返回一个error。从用户进程角度讲 它发起一个read操作后并不需要等待而是马上就得到了一个结果。用户进程判断结果是一个error时它就知道数据还没有准备好于是它可以再次发送read操作。一旦kernel中的数据准备好了并且又再次收到了用户进程的system call那么它马上就将数据拷贝到了用户内存然后返回。所以用户进程其实是需要不断的主动询问kernel数据好了没有。 注意 在网络IO时候非阻塞IO也会进行recvform系统调用检查数据是否准备好与阻塞IO不一样”非阻塞将大的整片时间的阻塞分成N多的小的阻塞, 所以进程不断地有机会 ‘被’ CPU光顾”。即每次recvform系统调用之间cpu的权限还在进程手中这段时间是可以做其他事情的 也就是说非阻塞的recvform系统调用调用之后进程并没有被阻塞内核马上返回给进程如果数据还没准备好此时会返回一个error。进程在返回之后可以干点别的事情然后再发起recvform系统调用。重复上面的过程循环往复的进行recvform系统调用。这个过程通常被称之为轮询。轮询检查内核数据直到数据准备好再拷贝数据到进程进行数据处理。需要注意拷贝数据整个过程进程仍然是属于阻塞的状态。 4 IO multiplexingIO多路复用 IO multiplexing这个词可能有点陌生但是如果我说selectepoll大概就都能明白了。有些地方也称这种IO方式为event driven IO。我们都知道select/epoll的好处就在于单个process就可以同时处理多个网络连接的IO。它的基本原理就是select/epoll这个function会不断的轮询所负责的所有socket当某个socket有数据到达了就通知用户进程。它的流程如图 当用户进程调用了select那么整个进程会被block而同时kernel会“监视”所有select负责的socket当任何一个socket中的数据准备好了select就会返回。这个时候用户进程再调用read操作将数据从kernel拷贝到用户进程。这个图和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。 注意1select函数返回结果中如果有文件可读了那么进程就可以通过调用accept()或recv()来让kernel将位于内核中准备到的数据copy到用户区。 注意2: select的优势在于可以处理多个连接不适用于单个连接 5 Asynchronous I/O异步IO linux下的asynchronous IO其实用得很少。先看一下它的流程 用户进程发起read操作之后立刻就可以开始去做其它的事。而另一方面从kernel的角度当它受到一个asynchronous read之后首先它会立刻返回所以不会对用户进程产生任何block。然后kernel会等待数据准备完成然后将数据拷贝到用户内存当这一切都完成之后kernel会给用户进程发送一个signal告诉它read操作完成了。 到目前为止已经将四个IO Model都介绍完了。现在回过头来回答最初的那几个问题blocking和non-blocking的区别在哪synchronous IO和asynchronous IO的区别在哪。先回答最简单的这个blocking vs non-blocking。前面的介绍中其实已经很明确的说明了这两者的区别。调用blocking IO会一直block住对应的进程直到操作完成而non-blocking IO在kernel还准备数据的情况下会立刻返回。 在说明synchronous IO和asynchronous IO的区别之前需要先给出两者的定义。Stevens给出的定义其实是POSIX的定义是这样子的 A synchronous I/O operation causes the requesting process to be blocked until that I/O operationcompletes; An asynchronous I/O operation does not cause the requesting process to be blocked; 两者的区别就在于synchronous IO做”IO operation”的时候会将process阻塞。按照这个定义之前所述的blocking IOnon-blocking IOIO multiplexing都属于synchronous IO。有人可能会说non-blocking IO并没有被block啊。这里有个非常“狡猾”的地方定义中所指的”IO operation”是指真实的IO操作就是例子中的recvfrom这个system call。non-blocking IO在执行recvfrom这个system call的时候如果kernel的数据没有准备好这时候不会block进程。但是当kernel中数据准备好的时候recvfrom会将数据从kernel拷贝到用户内存中这个时候进程是被block了在这段时间内进程是被block的。而asynchronous IO则不一样当进程发起IO 操作之后就直接返回再也不理睬了直到kernel发送一个信号告诉进程说IO完成。在这整个过程中进程完全没有被block。 注意由于咱们接下来要讲的selectpollepoll都属于IO多路复用而IO多路复用又属于同步的范畴故epoll只是一个伪异步而已。 各个IO Model的比较如图所示 经过上面的介绍会发现non-blocking IO和asynchronous IO的区别还是很明显的。在non-blocking IO中虽然进程大部分时间都不会被block但是它仍然要求进程去主动的check并且当数据准备完成以后也需要进程主动的再次调用recvfrom来将数据拷贝到用户内存。而asynchronous IO则完全不同。它就像是用户进程将整个IO操作交给了他人kernel完成然后他人做完后发信号通知。在此期间用户进程不需要去检查IO操作的状态也不需要主动的去拷贝数据。 五种IO模型比较 6 select poll epoll IO多路复用介绍 首先列一下sellect、poll、epoll三者的区别 select select最早于1983年出现在4.2BSD中它通过一个select()系统调用来监视多个文件描述符的数组当select()返回后该数组中就绪的文件描述符便会被内核修改标志位使得进程可以获得这些文件描述符从而进行后续的读写操作。 select目前几乎在所有的平台上支持 select的一个缺点在于单个进程能够监视的文件描述符的数量存在最大限制在Linux上一般为1024不过可以通过修改宏定义甚至重新编译内核的方式提升这一限制。 另外select()所维护的存储大量文件描述符的数据结构随着文件描述符数量的增大其复制的开销也线性增长。同时由于网络响应时间的延迟使得大量TCP连接处于非活跃状态但调用select()会对所有socket进行一次线性扫描所以这也浪费了一定的开销。 poll 它和select在本质上没有多大差别但是poll没有最大文件描述符数量的限制。 一般也不用它相当于过渡阶段 epoll 直到Linux2.6才出现了由内核直接支持的实现方法那就是epoll。被公认为Linux2.6下性能最好的多路I/O就绪通知方法。windows不支持 没有最大文件描述符数量的限制。 比如100个连接有两个活跃了epoll会告诉用户这两个两个活跃了直接取就ok了而select是循环一遍。 了解epoll可以同时支持水平触发和边缘触发Edge Triggered只告诉进程哪些文件描述符刚刚变为就绪状态它只说一遍如果我们没有采取行动那么它将不会再次告知这种方式称为边缘触发理论上边缘触发的性能要更高一些但是代码实现相当复杂。 另一个本质的改进在于epoll采用基于事件的就绪通知方式。在select/poll中进程只有在调用一定的方法后内核才对所有监视的文件描述符进行扫描而epoll事先通过epoll_ctl()来注册一个文件描述符一旦基于某个文件描述符就绪时内核会采用类似callback的回调机制迅速激活这个文件描述符当进程调用epoll_wait()时便得到通知。 所以市面上上见到的所谓的异步IO比如nginx、Tornado、等我们叫它异步IO实际上是IO多路复用。 select与epoll # 首先我们来定义流的概念一个流可以是文件socketpipe等等可以进行I/O操作的内核对象。
# 不管是文件还是套接字还是管道我们都可以把他们看作流。
# 之后我们来讨论I/O的操作通过read我们可以从流中读入数据通过write我们可以往流写入数据。现在假
# 定一个情形我们需要从流中读数据但是流中还没有数据典型的例子为客户端要从socket读如数据但是
# 服务器还没有把数据传回来这时候该怎么办
# 阻塞。阻塞是个什么概念呢比如某个时候你在等快递但是你不知道快递什么时候过来而且你没有别的事可以干
# 或者说接下来的事要等快递来了才能做那么你可以去睡觉了因为你知道快递把货送来时一定会给你打个电话
# 假定一定能叫醒你。
# 非阻塞忙轮询。接着上面等快递的例子如果用忙轮询的方法那么你需要知道快递员的手机号然后每分钟给他挂
# 个电话“你到了没”
# 很明显一般人不会用第二种做法不仅显很无脑浪费话费不说还占用了快递员大量的时间。
# 大部分程序也不会用第二种做法因为第一种方法经济而简单经济是指消耗很少的CPU时间如果线程睡眠了
# 就掉出了系统的调度队列暂时不会去瓜分CPU宝贵的时间片了。
#
# 为了了解阻塞是如何进行的我们来讨论缓冲区以及内核缓冲区最终把I/O事件解释清楚。缓冲区的引入是为
# 了减少频繁I/O操作而引起频繁的系统调用你知道它很慢的当你操作一个流时更多的是以缓冲区为单位进
# 行操作这是相对于用户空间而言。对于内核来说也需要缓冲区。
# 假设有一个管道进程A为管道的写入方为管道的读出方。
# 假设一开始内核缓冲区是空的B作为读出方被阻塞着。然后首先A往管道写入这时候内核缓冲区由空的状态变
# 到非空状态内核就会产生一个事件告诉该醒来了这个事件姑且称之为“缓冲区非空”。
# 但是“缓冲区非空”事件通知B后B却还没有读出数据且内核许诺了不能把写入管道中的数据丢掉这个时候写
# 入的数据会滞留在内核缓冲区中如果内核也缓冲区满了B仍未开始读数据最终内核缓冲区会被填满这个时候
# 会产生一个I/O事件告诉进程A你该等等阻塞了我们把这个事件定义为“缓冲区满”。
# 假设后来终于开始读数据了于是内核的缓冲区空了出来这时候内核会告诉A内核缓冲区有空位了你可以从
# 长眠中醒来了继续写数据了我们把这个事件叫做“缓冲区非满”
# 也许事件Y1已经通知了A但是A也没有数据写入了而继续读出数据知道内核缓冲区空了。这个时候内核就告
# 诉B你需要阻塞了我们把这个时间定为“缓冲区空”。
# 这四个情形涵盖了四个I/O事件缓冲区满缓冲区空缓冲区非空缓冲区非满注都是说的内核缓冲区且这四
# 个术语都是我生造的仅为解释其原理而造。这四个I/O事件是进行阻塞同步的根本。如果不能理解“同步”是
# 什么概念请学习操作系统的锁信号量条件变量等任务同步方面的相关知识。
#
# 然后我们来说说阻塞I/O的缺点。但是阻塞I/O模式下一个线程只能处理一个流的I/O事件。如果想要同时处理多
# 个流要么多进程(fork)要么多线程(pthread_create)很不幸这两种方法效率都不高。
# 于是再来考虑非阻塞忙轮询的I/O方式我们发现我们可以同时处理多个流了把一个流从阻塞模式切换到非阻塞
# 模式再此不予讨论
# while true {
# for i in stream[]; {
# if i has data
# read until unavailable
# }
# }
# 我们只要不停的把所有流从头到尾问一遍又从头开始。这样就可以处理多个流了但这样的做法显然不好因为
# 如果所有的流都没有数据那么只会白白浪费CPU。这里要补充一点阻塞模式下内核对于I/O事件的处理是阻
# 塞或者唤醒而非阻塞模式下则把I/O事件交给其他对象后文介绍的select以及epoll处理甚至直接忽略。
#
# 为了避免CPU空转可以引进了一个代理一开始有一位叫做select的代理后来又有一位叫做poll的代理不
# 过两者的本质是一样的。这个代理比较厉害可以同时观察许多流的I/O事件在空闲的时候会把当前线程阻
# 塞掉当有一个或多个流有I/O事件时就从阻塞态中醒来于是我们的程序就会轮询一遍所有的流于是我们可
# 以把“忙”字去掉了。代码长这样:
# while true {
# select(streams[])
# for i in streams[] {
# if i has data
# read until unavailable
# }
# }
# 于是如果没有I/O事件产生我们的程序就会阻塞在select处。但是依然有个问题我们从select那里仅仅知
# 道了有I/O事件发生了但却并不知道是那几个流可能有一个多个甚至全部我们只能无差别轮询所有流
# 找出能读出数据或者写入数据的流对他们进行操作。
# 但是使用select我们有O(n)的无差别轮询复杂度同时处理的流越多每一次无差别轮询时间就越长。再次
# 说了这么多终于能好好解释epoll了
# epoll可以理解为event poll不同于忙轮询和无差别轮询epoll之会把哪个流发生了怎样的I/O事件通知我
# 们。此时我们对这些流的操作都是有意义的。
# 在讨论epoll的实现细节之前先把epoll的相关操作列出
# epoll_create 创建一个epoll对象一般epollfd epoll_create()
# epoll_ctl epoll_add/epoll_del的合体往epoll对象中增加/删除某一个流的某一个事件
# 比如
# epoll_ctl(epollfd, EPOLL_CTL_ADD, socket, EPOLLIN);//有缓冲区内有数据时epoll_wait返回
# epoll_ctl(epollfd, EPOLL_CTL_DEL, socket, EPOLLOUT);//缓冲区可写入时epoll_wait返回
# epoll_wait(epollfd,...)等待直到注册的事件发生
# 注当对一个非阻塞流的读写发生缓冲区满或缓冲区空write/read会返回-1并设置errnoEAGAIN。
# 而epoll只关心缓冲区非满和缓冲区非空事件。
# 一个epoll模式的代码大概的样子是
# while true {
# active_stream[] epoll_wait(epollfd)
# for i in active_stream[] {
# read or write till unavailable
# }
# }# 举个例子:
# select:
# 班里三十个同学在考试,谁先做完想交卷都要通过按钮来活动,他按按钮作为老师的我桌子上的灯就会变红.
# 一旦灯变红,我(select)我就可以知道有人交卷了,但是我并不知道谁交的,所以,我必须跟个傻子似的轮询
# 地去问:嘿,是你要交卷吗?然后我就可以以这种效率极低地方式找到要交卷的学生,然后把它的卷子收上来.
#
#
# epoll:
# 这次再有人按按钮,我这不光灯会亮,上面还会显示要交卷学生的名字.这样我就可以直接去对应学生那收卷就
# 好了.当然,同时可以有多人交卷. IO多路复用的触发方式 # 在linux的IO多路复用中有水平触发,边缘触发两种模式,这两种模式的区别如下:
#
# 水平触发:如果文件描述符已经就绪可以非阻塞的执行IO操作了,此时会触发通知.允许在任意时刻重复检测IO的状态,
# 没有必要每次描述符就绪后尽可能多的执行IO.select,poll就属于水平触发.
#
# 边缘触发:如果文件描述符自上次状态改变后有新的IO活动到来,此时会触发通知.在收到一个IO事件通知后要尽可能
# 多的执行IO操作,因为如果在一次通知中没有执行完IO那么就需要等到下一次新的IO活动到来才能获取到就绪的描述
# 符.信号驱动式IO就属于边缘触发.
#
# epoll既可以采用水平触发,也可以采用边缘触发.
#
# 大家可能还不能完全了解这两种模式的区别,我们可以举例说明:一个管道收到了1kb的数据,epoll会立即返回,此时
# 读了512字节数据,然后再次调用epoll.这时如果是水平触发的,epoll会立即返回,因为有数据准备好了.如果是边
# 缘触发的不会立即返回,因为此时虽然有数据可读但是已经触发了一次通知,在这次通知到现在还没有新的数据到来,
# 直到有新的数据到来epoll才会返回,此时老的数据和新的数据都可以读取到(当然是需要这次你尽可能的多读取).# 下面我们还从电子的角度来解释一下:
#
# 水平触发:也就是只有高电平(1)或低电平(0)时才触发通知,只要在这两种状态就能得到通知.上面提到的只要
# 有数据可读(描述符就绪)那么水平触发的epoll就立即返回.
#
# 边缘触发:只有电平发生变化(高电平到低电平,或者低电平到高电平)的时候才触发通知.上面提到即使有数据
# 可读,但是没有新的IO活动到来,epoll也不会立即返回. 简单实例 实例1(non-blocking IO) import time
import socket
sk socket.socket(socket.AF_INET,socket.SOCK_STREAM)
sk.setsockopt
sk.bind((127.0.0.1,6667))
sk.listen(5)
sk.setblocking(False)
while True:try:print (waiting client connection .......)connection,address sk.accept() # 进程主动轮询print(,address)client_messge connection.recv(1024)print(str(client_messge,utf8))connection.close()except Exception as e:print (e)time.sleep(4)#############################clientimport time
import socket
sk socket.socket(socket.AF_INET,socket.SOCK_STREAM)while True:sk.connect((127.0.0.1,6667))print(hello)sk.sendall(bytes(hello,utf8))time.sleep(2)break 优点能够在等待任务完成的时间里干其他活了包括提交其他任务也就是 “后台” 可以有多个任务在同时执行。 缺点任务完成的响应延迟增大了因为每过一段时间才去轮询一次read操作而任务可能在两次轮询之间的任意时间完成。这会导致整体数据吞吐量的降低。 实例2(IO multiplexing): 在非阻塞实例中轮询的主语是进程而“后台” 可能有多个任务在同时进行人们就想到了循环查询多个任务的完成状态只要有任何一个任务完成就去处理它。不过这个监听的重任通过调用select等函数交给了内核去做。IO多路复用有两个特别的系统调用select、poll、epoll函数。select调用是内核级别的select轮询相对非阻塞的轮询的区别在于—前者可以等待多个socket能实现同时对多个IO端口进行监听当其中任何一个socket的数据准好了就能返回进行可读然后进程再进行recvfrom系统调用将数据由内核拷贝到用户进程当然这个过程是阻塞的。 实例2: import socket
import select
sksocket.socket()
sk.bind((127.0.0.1,9904))
sk.listen(5)while True:r,w,eselect.select([sk,],[],[],5)for i in r:# conn,addi.accept()#print(conn)print(hello)print()#*************************client.py
import socketsksocket.socket()sk.connect((127.0.0.1,9904))while 1:inpinput().strip()sk.send(inp.encode(utf8))datask.recv(1024)print(data.decode(utf8)) 请思考为什么不调用accept会反复print select属于水平触发 实例3(server端并发聊天): #***********************server.py
import socket
import select
sksocket.socket()
sk.bind((127.0.0.1,8801))
sk.listen(5)
inputs[sk,]
while True:r,w,eselect.select(inputs,[],[],5)print(len(r))for obj in r:if objsk:conn,addobj.accept()print(conn)inputs.append(conn)else:data_byteobj.recv(1024)print(str(data_byte,utf8))inpinput(回答%s号客户%inputs.index(obj))obj.sendall(bytes(inp,utf8))print(,r)#***********************client.pyimport socket
sksocket.socket()
sk.connect((127.0.0.1,8801))while True:inpinput()sk.sendall(bytes(inp,utf8))datask.recv(1024)print(str(data,utf8)) 文件描述符其实就是咱们平时说的句柄只不过文件描述符是linux中的概念。注意我们的accept或recv调用时即向系统发出recvfrom请求 (1) 如果内核缓冲区没有数据等待数据到了内核缓冲区转到用户进程缓冲区 (2) 如果先用select监听到某个文件描述符对应的内核缓冲区有了数据当我们再调用accept或recv时直接将数据转到用户缓冲区。 思考1开启5个client分别按54321的顺序发送消息那么server端是按什么顺序回消息的呢 思考2: 如何在某一个client端退出后不影响server端和其它客户端正常交流 linux if not data_byte:inputs.remove(obj)continue win try:data_byteobj.recv(1024)print(str(data_byte,utf8))inpinput(回答%s号客户%inputs.index(obj))obj.sendall(bytes(inp,utf8))
except Exception:inputs.remove(obj) 延伸 实例4: #_*_coding:utf-8_*_
__author__ Alex Liimport select
import socket
import sys
import queue# Create a TCP/IP socket
server socket.socket(socket.AF_INET, socket.SOCK_STREAM)
server.setblocking(False)# Bind the socket to the port
server_address (localhost, 10000)
print(sys.stderr, starting up on %s port %s % server_address)
server.bind(server_address)# Listen for incoming connections
server.listen(5)# Sockets from which we expect to read
inputs [ server ]# Sockets to which we expect to write
outputs [ ]message_queues {}
while inputs:# Wait for at least one of the sockets to be ready for processingprint( \nwaiting for the next event)readable, writable, exceptional select.select(inputs, outputs, inputs)# Handle inputsfor s in readable:if s is server:# A readable server socket is ready to accept a connectionconnection, client_address s.accept()print(new connection from, client_address)connection.setblocking(False)inputs.append(connection)# Give the connection a queue for data we want to sendmessage_queues[connection] queue.Queue()else:data s.recv(1024)if data:# A readable client socket has dataprint(sys.stderr, received %s from %s % (data, s.getpeername()) )message_queues[s].put(data)# Add output channel for responseif s not in outputs:outputs.append(s)else:# Interpret empty result as closed connectionprint(closing, client_address, after reading no data)# Stop listening for input on the connectionif s in outputs:outputs.remove(s) #既然客户端都断开了我就不用再给它返回数据了所以这时候如果这个客户端的连接对象还在outputs列表中就把它删掉inputs.remove(s) #inputs中也删除掉s.close() #把这个连接关闭掉# Remove message queuedel message_queues[s]# Handle outputsfor s in writable:try:next_msg message_queues[s].get_nowait()except queue.Empty:# No messages waiting so stop checking for writability.print(output queue for, s.getpeername(), is empty)outputs.remove(s)else:print( sending %s to %s % (next_msg, s.getpeername()))s.send(next_msg)# Handle exceptional conditionsfor s in exceptional:print(handling exceptional condition for, s.getpeername() )# Stop listening for input on the connectioninputs.remove(s)if s in outputs:outputs.remove(s)s.close()# Remove message queuedel message_queues[s] 实例5: # select 模拟一个socket server注意socket必须在非阻塞情况下才能实现IO多路复用。
# 接下来通过例子了解select 是如何通过单进程实现同时处理多个非阻塞的socket连接的。
#server端import select
import socket
import queueserver socket.socket()
server.bind((localhost,9000))
server.listen(1000)server.setblocking(False) # 设置成非阻塞模式accept和recv都非阻塞
# 这里如果直接 server.accept() 如果没有连接会报错所以有数据才调他们
# BlockIOError[WinError 10035] 无法立即完成一个非阻塞性套接字操作。
msg_dic {}
inputs [server,] # 交给内核、select检测的列表。
# 必须有一个值让select检测否则报错提供无效参数。
# 没有其他连接之前自己就是个socket自己就是个连接检测自己。活动了说明有链接
outputs [] # 你往里面放什么下一次就出来了while True:readable, writeable, exceptional select.select(inputs, outputs, inputs) # 定义检测#新来连接 检测列表 异常断开# 异常的也是inputs是 检测那些连接的存在异常print(readable,writeable,exceptional)for r in readable:if r is server: # 有数据代表来了一个新连接conn, addr server.accept()print(来了个新连接,addr)inputs.append(conn) # 把连接加到检测列表里如果这个连接活动了就说明数据来了# inputs [server.conn] # 【conn】只返回活动的连接但怎么确定是谁活动了# 如果server活动则来了新连接conn活动则来数据msg_dic[conn] queue.Queue() # 初始化一个队列后面存要返回给这个客户端的数据else:try :data r.recv(1024) # 注意这里是r而不是conn多个连接的情况print(收到数据,data)# r.send(data) # 不能直接发如果客户端不收数据就没了msg_dic[r].put(data) # 往里面放数据outputs.append(r) # 放入返回的连接队列里except ConnectionResetError as e:print(客户端断开了,r)if r in outputs:outputs.remove(r) #清理已断开的连接inputs.remove(r) #清理已断开的连接del msg_dic[r] ##清理已断开的连接for w in writeable: # 要返回给客户端的连接列表data_to_client msg_dic[w].get() # 在字典里取数据w.send(data_to_client) # 返回给客户端outputs.remove(w) # 删除这个数据确保下次循环的时候不返回这个已经处理完的连接了。for e in exceptional: # 如果连接断开删除连接相关数据if e in outputs:outputs.remove(e)inputs.remove(e)del msg_dic[e]#*************************client
import socket
client socket.socket()client.connect((localhost, 9000))while True:cmd input( ).strip()if len(cmd) 0 : continueclient.send(cmd.encode(utf-8))data client.recv(1024)print(data.decode())client.close() 实例6: import selectors
import socketsel selectors.DefaultSelector()def accept(sock, mask):conn, addr sock.accept() # Should be readyprint(accepted, conn, from, addr)conn.setblocking(False)sel.register(conn, selectors.EVENT_READ, read)def read(conn, mask):data conn.recv(1000) # Should be readyif data:print(echoing, repr(data), to, conn)conn.send(data) # Hope it wont blockelse:print(closing, conn)sel.unregister(conn)conn.close()sock socket.socket()
sock.bind((localhost, 1234))
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.datacallback(key.fileobj, mask) 注本文最重要的参考文献是Richard Stevens的“UNIX® Network Programming Volume 1, Third Edition: The Sockets Networking ” http://mp.weixin.qq.com/s__bizMzA4MjEyNTA5Mwmid2652563599idx1sn9781747e54d906c0c140228376e671edscene21#wecha t_redirect https://pymotw.com/2/select/#module-select http://blog.csdn.net/lingfengtengfei/article/details/12392449 http://www.jb51.net/article/37416.htm https://pymotw.com/2/select/#module-select 转载于:https://www.cnblogs.com/zxfprogram/p/8295540.html