Python-IO模式介绍
事件驱动模型:有个事件队列,把事件放到队列里,然后循环这个队列,取出事件执行
5种IO模式:
阻塞 I/O(blocking IO)
非阻塞 I/O(nonblocking IO)
I/O 多路复用(IO multiplexing)
信号驱动 I/O(signal driven IO)
异步 I/O(asynchronous IO)
其中信号驱动 I/O 并不常用
如果进行一次读取数据,数据会先被拷贝到操作系统内核的缓冲区中,然后会从操作系统内核的缓冲区拷贝到应用程序的地址空间
阻塞 I/O (blocking IO)
默认情况下所有的socket都是阻塞的,即一直等到有连接为止
阻塞 IO的特点就是在IO执行的阶段都被阻塞了
当用户要进行数据读取时,操作系统内核就开始准备数据,也就是说数据被拷贝到操作系统内核的缓冲区中的这个需要等待
而用户读取数据的进程也会被阻塞
当操作系统内核一直等到数据准备好了,它就会将数据从操作系统内核中拷贝到用户内存,这个也需要一定的时间,然后操作系统内核返回结果,用户进程才解除阻塞的状态,重新运行起来。
非阻塞 I/O (nonblocking IO)
非阻塞 IO的特点是用户进程不断地询问操作系统内核数据是否准备好
当用户要进行数据读取时,如果操作系统内核中的数据还没有准备好,它不会阻塞用户进程,而是立刻返回一个错误
用户进程发起一个读取数据操作后,并不需要等待,而是马上就得到了一个结果
用户进程判断结果是一个错误时,就知道操作系统内核中的数据还没有准备好,于是它再次发送读取数据操作
当操作系统内核中的数据准备好了,并且又收到了用户进程的询问,它就马上将数据拷贝到了用户内存,然后返回结果
I/O 多路复用(IO multiplexing)
IO 多路复用就是select,poll,epoll,这种IO模式就是事件驱动模型
select/epoll的好处就在于单个进程就可以同时处理多个网络连接的IO
select,poll,epoll方法会不断地轮询所负责的所有socket,当某个socket有数据到达了,就通知用户进程
当用户进程调用了select,整个进程就会被阻塞,而同时,操作系统内核会“监视”所有select负责的socket,当任何一个socket中的数据准备好了,select就会返回
这个时候用户进程再调用读取数据操作,将数据从操作系统内核拷贝到用户进程
异步 I/O(asynchronous IO)
用户进程发起读取数据操作之后,就可以去做其它的事
因为操作系统内核收到指令后,首先它会立刻返回,不会对用户进程产生任何阻塞,然后,操作系统内核会等待数据准备完成,然后将数据拷贝到用户内存,拷贝完成后,操作系统内核会给用户进程发送一个操作完成的结果
I/O 多路复用-select,poll,epoll
select
select支持多平台,select在单个进程中能够监视的文件描述符的数量存在限制,在Linux上一般为1024,可以通过修改宏定义甚至重新编译内核的方式提升这一限制
select()函数监视多个文件描述符的数组(writefds、readfds、exceptfds),调用后select()函数会阻塞,直到有描述符就绪(有数据 可读、可写、或者有错误),或者超时(timeout指定等待时间,如果立即返回设为null即可),函数返回。当select()函数返回后,该数组中就绪的文件描述符便会被内核修改标志位,使得进程可以获得这些文件描述符从而进行后续的读写操作
select()所维护的存储大量文件描述符的数据结构,随着文件描述符数量的增大,其复制的开销也线性增长。同时,由于网络响应时间的延迟使得大量TCP连接处于非活跃状态,但调用select()会对所有socket进行一次线性扫描,所以这也浪费了一定的开销
poll
poll和select在本质上没有多大差别,但是poll没有最大文件描述符数量的限制,select和poll都需要在返回后,通过遍历文件描述符来获取已经就绪的socket
poll和select同样存在一个缺点就是,包含大量文件描述符的数组被整体复制于用户态和内核的地址空间之间,而不论这些文件描述符是否就绪,它的开销随着文件描述符数量的增加而线性增大
select()和poll()将就绪的文件描述符告诉进程后,如果进程没有对其进行IO操作,那么下次调用select()或poll()的时候将再次报告这些文件描述符,所以它们一般不会丢失就绪的消息,这种方式称为水平触发(Level Triggered)
epoll
epoll可以同时支持水平触发和边缘触发(Edge Triggered,只告诉进程哪些文件描述符刚刚变为就绪状态,它只说一遍,如果我们没有采取行动,那么它将不会再次告知,这种方式称为边缘触发),理论上边缘触发的性能要更高一些,但是代码实现相当复杂。
当调用epoll_wait()获得就绪文件描述符时,返回的不是实际的描述符,而是一个代表就绪描述符数量的值,只需要去epoll指定的一个数组中依次取得相应数量的文件描述符即可,这里也使用了内存映射(mmap)技术,这样便彻底省掉了这些文件描述符在系统调用时复制的开销。
epoll事先通过epoll_ctl()来注册一个文件描述符,一旦基于某个文件描述符就绪时,内核会采用类似callback的回调机制,迅速激活这个文件描述符,当进程调用epoll_wait()时便得到通知
参考:https://segmentfault.com/a/1190000003063859