IO多路复用

什么是IO, 同步IO, 异步IO,阻塞,非阻塞

简单的理解: IO在计算机中指Input/Output,也就是输入和输出。

由于CPU和内存的速度远远高于外设的速度,所以,在IO编程中,就存在速度严重不匹配的问题。举个例子来说,比如要把100M的数据写入磁盘,CPU输出100M的数据只需要0.01秒,可是磁盘要接收这100M数据可能需要10秒,怎么办呢?有两种办法:

同步IO:第一种是CPU等着,也就是程序暂停执行后续代码,等100M的数据在10秒后写入磁盘,再接着往下执行,这种模式称为同步IO;

异步IO:另一种方法是CPU不等待,只是告诉磁盘,“您老慢慢写,不着急,我接着干别的事去了”,于是,后续代码可以立刻接着执行,这种模式称为异步IO。

阻塞 、非阻塞 : 指程序执行中的运行状态

详情文章: 连接

Socket中的IO

Socket是应用层与TCP/IP协议族通信的中间软件抽象层,它是一组接口。在设计模式中,Socket其实就是一个门面模式,它把复杂的TCP/IP协议族隐藏在Socket接口后面

详情文章: 连接

服务端:
import socket
sk = socket.socket()
# sk 是套接字对象,用来与客户端建连接的
sk.bind(('127.0.0.1',8000)) #把地址绑定到套接字
sk.listen() #监听链接
conn,addr = sk.accept() #等待接受客户端链接
# conn 是套接字(双向通道通信)对象, addr是ip和port
ret = conn.recv(1024) #等待接收客户端信息  此处等待客户端操作,是一次IO
print(ret) #打印客户端信息
conn.send(b'hi') #向客户端发送信息
conn.close() #关闭客户端套接字
sk.close() #关闭服务器套接字(可选)

客户端与服务端建立连接后, 服务端等待接收客户端传来的数据, 而此时服务端就是IO状态(read)

那么就会出现一下问题:

由于进程的执行过程是线性的(也就是顺序执行),当我们调用低速系统I/O(read,write,accept等等),进程可能阻塞,此时进程就阻塞 在这个调用上,不能执行其他操作.阻塞很正常.

接下来考虑这么一个问题:一个服务器进程和一个客户端进程通信,服务器端read(sockfd1,bud,bufsize),此时客户端进程没有发送数据,那么read(阻塞调用)将 阻塞直到客户端调用write(sockfd,but,size)发来数据.

在一个客户和服务器通信时这没什么问题,当多个客户与服务器通信时,若服 务器阻塞于其中一个客户sockfd1,当另一个客户的数据到达套接字sockfd2时,服务器不 能处理,仍然阻塞在read(sockfd1,…)上;此时问题就出现了,不能及时处理另一个客户的服务,咋么办?

详情文章: 连接

场景:

有100W个客户端同时与一个服务器保持着TCP连接。而每一时刻只有几百上千个TCP连接是活跃着的(事实上大部分场景都是这样的情况)。如何实现这样的高并发?

解决方案: 

1. 多线程并发处理:

针对每一个客户端进程都新建一个新的服务器端线程,即一对一地应付客户端的通信需求

import multiprocessing
import threading
 
import socket
s=socket.socket(socket.AF_INET,socket.SOCK_STREAM)
s.bind(('127.0.0.1',8080))
s.listen(5)
 
def action(conn):
    while True:
        data=conn.recv(1024)
        print(data)
        conn.send(data.upper())
 
if __name__ == '__main__':
 
    while True:
        conn,addr=s.accept()
 
        p=threading.Thread(target=action,args=(conn,))  # 将每个连接的通信交给不同的线程取处理
        p.start() 

出现的问题:

如果通信的客户端很多,那么服务器就需要开很多线程来处理IO,服务器这边的压力就会比较大,除开开启线程与维持线程本身需要的资源以外,服务器CPU在线程之间切换也要耗时,导致效率低下 

2.单线程IO多路复用

作用:由操作系统(从用户态复制句柄数据结构到内核态)检测多个socket是否已经发生变化(是否已经连接成功/是否已经获取数据)(可写/可读),能够通知程序进行相应的操作

对于一个network IO (这里我们以read举例),它会涉及到两个系统对象:

  • 一个是调用这个IO的process (or thread)
  • 一个就是系统内核(kernel)

当一个read操作发生时,它会经历两个阶段:

  • 等待数据准备,比如accept(), recv()等待数据 (Waiting for the data to be ready)
  • 将数据从内核拷贝到进程中, 比如 accept()接受到请求,recv()接收连接发送的数据后需要复制到内核,再从内核复制到进程用户空间(Copying the data from the kernel to the process)

对于socket流而言,数据的流向经历两个阶段:

  • 第一步通常涉及等待网络上的数据分组到达,然后被复制到内核的某个缓冲区。
  • 第二步把数据从内核缓冲区复制到应用进程缓冲区。

记住这两点很重要,因为这些IO Model的区别就是在两个阶段上各有不同的情况。详情文章: 连接

IO多路复用的三种模式:

select:循环去检测

服务器进程每次都把这100万个连接告诉操作系统,让操作系统内核去查询这些套接字上是否有事件发生,轮询完后,再将句柄数据复制到用户态,让服务器应用程序轮询处理已发生的网络事件,这一过程资源消耗较大,因此,select一般只能处理1024的并发连接。
如果没有I/O事件产生,我们的程序就会阻塞在select处。但是依然有个问题,我们从select那里仅仅知道了,有I/O事件发生了,但却并不知道是那几个流(可能有一个,多个,甚至全部),我们只能无差别轮询所有流,找出能读出数据,或者写入数据的流,对他们进行操作。

缺点:

  • 最多1024个socket(36位1024个, 64位2048个)
  • 每次调用select/poll都需要把fd集合从用户态拷贝到内核态,这个开销在fd很多时会很大;
  • 使用select,我们有O(n)的无差别轮询复杂度,同时处理的流越多,每一次无差别轮询时间就越长
  • select返回的是含有整个句柄的数组,应用程序需要遍历整个数组才能发现哪些句柄发生了事件;
  • select的触发方式是水平触发,应用程序如果没有完成对一个已经就绪的文件描述符进行IO操作,那么之后每次select调用还是会将这些文件描述符通知给进程;
  • select 不是线程安全的(即: 只能在一个线程里面处理一组I/O流)

poll:不限制监听socket个数,循环去检测;

  • poll与select一样,只是不限制socket的个数, poll使用链表保存文件描述符,因此没有了监视文件数量的限制,但其他问题依然存在

epoll:不限制监听socket个数,回调方式(边缘触发);

一个epoll场景:一个酒吧服务员(一个线程),前面趴了一群醉汉,突然一个吼一声“倒酒”(事件),你小跑过去给他倒一杯,然后随他去吧,突然又一个要倒酒,你又过去倒上,就这样一个服务员服务好多人,有时没人喝酒,服务员处于空闲状态,可以干点别的玩玩手机。至于epoll与select,poll的区别在于后两者的场景中醉汉不说话,你要挨个问要不要酒,没时间玩手机了。io多路复用大概就是指这几个醉汉共用一个服务员。

epoll是poll的一种优化,返回后不需要对所有的fd进行遍历,它在内核中维护了fd列表select和poll是将这个内核列表维持在用户态,然后复制到内核态。与select/poll不同,epoll不在是一个单独的调度系统

epoll通过在Linux内核中申请一个简易的文件系统(文件系统一般用什么数据结构?B+树)。

把原先的select/poll调用分成三个部分:

  1. 调用epoll_create() 建立一个epoll对象(在epoll文件系统中为这个句柄对象分配资源)
  2. 调用epoll_ctl 向epoll对象中添加这100万个连接的套接字, 或减少套接字,或改变事件的监听方式
  3. 调用epoll_wait 收集发生的事件的连接

如此一来,要实现上面说是的场景,只需要在进程启动时建立一个epoll对象,然后在需要的时候向这个epoll对象中添加或者删除连接。同时,epoll_wait的效率也非常高,因为调用epoll_wait时,并没有一股脑的向操作系统复制这100万个连接的句柄数据,内核也不需要去遍历全部的连接。

优点: 

由于epoll的实现机制与select/poll机制完全不同,上面所说的 select的缺点在epoll上不复存在

epoll 没有最大并发连接的限制,上限是最大可以打开文件的数目,这个数字一般远大于 2048, 一般来说这个数目和系统内存关系很大  ,具体数目可以 cat /proc/sys/fs/file-max 察看。

效率提升, Epoll 最大的优点就在于它只管你“活跃”的连接 ,而跟连接总数无关,因此在实际的网络环境中, Epoll 的效率就会远远高于 select 和 poll 。

缺点:

只有linux支持

IO多路服用跟多线程相比较

线程切换需要切换到内核进行线程切换,需要消耗时间和资源. 而I/O多路复用不需要切换线/进程,效率相对较高,特别是对高并发的应用nginx就是用I/O多路复用,故而性能极佳.但多线程编程逻辑和处理上比I/O多路复用简单.而I/O多路复用处理起来较为复杂.

推荐文章: IO多路复用, 协程,

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值