IO模型

 
#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期间也可能发生在IO之前。
#同步与异步是针对IO操作来说的,同步是用户线程一直盯着IO直到完成,异步是用户线程在IO完成时会收到通知。

同步

#什么是用户空间(线程)IO操作和内核空间(线程)IO操作
    空间本质上就是内存,用户空间是指用户进程所占用的内存,内核空间指的是系统进程所占用的空间,你玩的windows开机的时候就已经划分好了,这块内存归你,那块内存归系统,你不能在系统占用的内存上瞎搞。

#同步体现在哪?
    同步就是说你内核线程IO读写的时候,我用户线程得等着你完成IO,干不了旁的事儿,这里的重点就在于必须等待对方把事情做完我才能做别的事

异步

#内核线程在IO操作的时候,我用户线程不跟着你,而是我想干啥就干啥,等你IO完成之后你可以通知我你IO完事了,得到通知之后我再去选择对这些数据做操作。
#举例:
    1. multiprocessing.Pool().apply_async() #发起异步调用后,并不会等待任务结束才返回,相反,会立即获取一个临时结果(并不是最终的结果,可能是封装好的一个对象)。
    2. concurrent.futures.ProcessPoolExecutor(3).submit(func,)
    3. concurrent.futures.ThreadPoolExecutor(3).submit(func,)

阻塞

#什么是阻塞?
    答:阻塞就是把线程堵住了,线程不能去干别的事。阻塞情况下用户线程读取内核空间数据,如果此时还没有数据就会被堵住,一直到有数据才返回。
#阻塞与IO有啥关系?
    答:当内核空间没有发生IO读写之前,用户线程就等待操作内核空间IO好的数据。
#阻塞与同步是一回事儿吗?
    答:显然不是,同步针对的是IO操作,阻塞针对的是线程对象。即便内核空间没有IO操作,用户线程同样会发生阻塞。

非阻塞

#非阻塞情况,用户线程读取内核空间数据,不管此时有没有数据,用户线程都直接返回。

Linux下,可以通过设置socket使其变为non-blocking
在非阻塞式IO中,用户进程其实是需要不断的主动询问kernel数据准备好了没有
#但是非阻塞IO模型绝不被推荐。
    1. 循环调用recv()将大幅度推高CPU占用率
    2. 任务完成的响应延迟增大了,因为每过一段时间才去轮询一次read操作,而任务可能在两次轮询之间的任意时间完成。这会导致整体数据吞吐量的降低。
import socket
import time
s = socket.socket()
s.bind(("127.0.0.1",9999))
s.listen()
# 设置socket 是否阻塞 默认为True
s.setblocking(False)

# 所有的客户端socket
cs = []
# 所有需要返回数据的客户端
send_cs = []

while True:
    # time.sleep(0.2)
    try:
        c,addr = s.accept() # 三次握手
        print("run accept")
        cs.append(c) #存储已经连接成功的客户端
    except BlockingIOError:
        # 没有数据准备 可以作别的事情
        # print("收数据")
        for c in cs[:]:
            try:
                data = c.recv(1024)
                if not data:
                    c.close()
                    cs.remove(c)
                print(data.decode("utf-8"))
                # 把数据和连接放进去
                send_cs.append((c, data))
                #c.send(data.upper()) # io
                # send也是io操作  在一些极端情况下 例如系统缓存满了 放不进去 那肯定抛出
                # 非阻塞异常  这时候必须把发送数据 单独拿出来处理 因为recv和send都有可能抛出相同异常
                # 就无法判断如何处理


            except BlockingIOError:
                continue

            except ConnectionResetError:
                c.close()
                # 从所有客户端列表中删除这个连接
                cs.remove(c)

        # print("发数据")
        for item in send_cs[:]:
            c,data = item
            try:
                c.send(data.upper())
                # 如果发送成功就把数据从列表中删除
                send_cs.remove(item)
            except BlockingIOError: # 如果缓冲区慢了 那就下次再发
                continue
            except ConnectionResetError:
                c.close() # 关闭连接
                send_cs.remove(item) # 删除数据
                # 从所有客户端中删除这个已经断开的连接
                cs.remove(c)
非阻塞IO、套接字服务端

 补充:

li = [1,2,3,4,5,6]

for i in li[:]:                     #
    li.remove(i)
print(li)
迭代取值不修改元素

组合概念

#1、同步阻塞IO
同步体现在IO完成之前用户线程不能做别的事情。
阻塞体现在用户线程从发送read请求开始一直到内核线程完成IO读写和数据拷贝都是堵住的。

#2、同步非阻塞IO
同步体现在IO完成之前用户线程不能做别的事情。
非阻塞体现在用户线程发送read请求之后没有被堵住而是立刻返回。
这里体现了同步与阻塞的区别,即虽然线程返回了,但是线程在没拿到结果之前干不了别的事情。


#3、IO多路复用应该算作异步阻塞
异步体现在,用户线程在内核线程IO操作完成之前没有强制其不可以做别的事情。
阻塞体现在,用户线程一直等待,直到内核线程通知有可处理的事件。

多路复用IO

#IO多路复用
    用一个线程来并发处理所有的客户端
    IO多路复用不用一个线程去轮询所有连接,而是该线程阻塞在一个阻塞对象上等待通知一般这个阻塞对象可以是select epoll poll等,这些对象会把IO封装成对应事件,

    筛选出已经准备就绪的socket,会将可读和可写的分别放到不同的列表中
    既然是已经就绪 那么执行recv或是send 就不会在阻塞

#select模块只有一个函数就是select
    参数1:r_list 需要被select检测是否是可读的客户端  把所有socket放到该列表中,select会负责从中找出可以读取数据的socket
    参数2:w_lirt 需要被select检测是否是可写的客户端  把所有socket放到该列表中,select会负责从中找出可以写入数据的socket
    参数3:x_list 存储要检测异常条件 ....忽略即可

    返回一个元组 包含三个列表
        readables 已经处于可读状态的socket  即数据已经到达缓冲区
        writeables 已经处于可写状态的socket  即缓冲区没满 可以发送...
        x_list:忽略

    从可读或写列表中拿出所有的socket 依次处理它们即可
import socket
import time
import select
s = socket.socket()
s.bind(("127.0.0.1",9999))
s.listen()
# 在多路复用中 一旦select交给你一个socket 一定意味着 该socket已经准备就绪 可读或是可写
# s.setblocking(False)

r_list = [s]
w_list = []
# 存储需要发送的数据 已及对应的socket  把socket作为key 数据作为value
data_dic = {}

while True:
    readables,writeables,_ = select.select(r_list,w_list,[])

    # 接收数据 以及服务器建立连接
    for i in readables:
        if i == s:# 如果是服务器  就执行accept
            c,_ = i.accept()
            r_list.append(c)
        else: # 是一个客户端端 那就recv收数据
            try:
                data = i.recv(1024)
                if not data: #linux 对方强行下线或是 windows正常下线
                    i.close()
                    r_list.remove(i)
                    continue
                print(data)
                # 发送数据 不清楚 目前是不是可以发 所以交给select来检测
                w_list.append(i)
                data_dic[i] = data # 把要发送的数据先存在 等select告诉你这个连接可以发送时再发送
            except ConnectionResetError:# windows强行下线
                i.close()
                r_list.remove(i) # 从检测列表中删除

    # 发送数据
    for i in writeables:
        try:
            i.send(data_dic[i].upper()) # 返回数据
            #data_dic.pop(i)
            #w_list.remove(i)
        except ConnectionResetError:
            i.close()
        finally:
            data_dic.pop(i) # 删除已经发送成功的数
            w_list.remove(i) # 从检测列表中删除这个连接  如果不删除 将一直处于可写状态
IO多路复用,套接字服务端

三种I/O多路复用模型优缺点

1、select

#优点:
(1)select的可移植性好,在某些unix下不支持poll。
(2)select对超时值提供了很好的精度,精确到微秒,而poll式毫秒。

#缺点:
(1)单个进程可监视的fd数量被限制,默认是1024。
(2)需要维护一个用来存放大量fd的数据结构,这样会使得用户空间和内核空间在传递该结构时复制开销大。
(3)对fd进行扫描时是线性扫描,fd剧增后,IO效率降低,每次调用都对fd进行线性扫描遍历,随着fd的增加会造成遍历速度慢的问题。
(4)select函数超时参数在返回时也是未定义的,考虑到可移植性,每次超时之后进入下一个select之前都要重新设置超时参数。

2、poll

#优点:
(1)不要求计算最大文件描述符+1的大小。
(2)应付大数量的文件描述符时比select要快。
(3)没有最大连接数的限制是基于链表存储的。

#缺点:
(1)大量的fd数组被整体复制于内核态和用户态之间,而不管这样的复制是不是有意义。
(2)同select相同的是调用结束后需要轮询来获取就绪描述符。

3、epoll

(1)支持一个进程打开大数目的socket描述符(FD)
select 最不能忍受的是一个进程所打开的FD是有一定限制的,由FD_SETSIZE设置,默认值是2048。对于那些需要支持的上万连接数目的IM服务器来说显 然太少了。这时候你一是可以选择修改这个宏然后重新编译内核,不过资料也同时指出这样会带来网络效率的下降,二是可以选择多进程的解决方案(传统的 Apache方案),不过虽然linux上面创建进程的代价比较小,但仍旧是不可忽视的,加上进程间数据同步远比不上线程间同步的高效,所以也不是一种完 美的方案。不过 epoll则没有这个限制,它所支持的FD上限是最大可以打开文件的数目,这个数字一般远大于2048,举个例子,在1GB内存的机器上大约是10万左 右,具体数目可以cat /proc/sys/fs/file-max察看,一般来说这个数目和系统内存关系很大。

(2)IO效率不随FD数目增加而线性下降
传统的select/poll另一个致命弱点就是当你拥有一个很大的socket集合,不过由于网络延时,任一时间只有部分的socket是”活跃”的, 但是select/poll每次调用都会线性扫描全部的集合,导致效率呈现线性下降。但是epoll不存在这个问题,它只会对”活跃”的socket进行 操作—这是因为在内核实现中epoll是根据每个fd上面的callback函数实现的。那么,只有”活跃”的socket才会主动的去调用 callback函数,其他idle状态socket则不会,在这点上,epoll实现了一个”伪”AIO,因为这时候推动力在os内核。在一些 benchmark中,如果所有的socket基本上都是活跃的—比如一个高速LAN环境,epoll并不比select/poll有什么效率,相 反,如果过多使用epoll_ctl,效率相比还有稍微的下降。但是一旦使用idle connections模拟WAN环境,epoll的效率就远在select/poll之上了。

(3)epoll工作的两种模式
EPOLL事件分发系统可以运转在两种模式下:边缘触发Edge Triggered (ET)、水平触发Level Triggered (LT)。
LT 是缺省的工作方式:同时支持block和no-block socket;在这种做法中,内核告诉你一个文件描述符是否就绪了,然后你可以对这个就绪的fd进行IO操作。如果你不作任何操作,内核还是会继续通知你的,所以,这种模式编程出错误可能性要小一点。传统的select/poll都是这种模型的代表。
ET是高速工作方式:它只支持no-block socket。在这种模式下,当描述符从未就绪变为就绪时,内核通过epoll告诉你。然后它会假设你知道文件描述符已经就绪,并且不会再为那个文件描述 符发送更多的就绪通知,直到你做了某些操作导致那个文件描述符不再为就绪状态了。但是请注意,如果一直不对这个fd作IO操作(从而导致它再次变成未就绪),内核不会发送更多的通知。
对于LT模式epoll_wait清空就绪链表之后会检查该文件描述符是哪一种模式,如果为LT模式,且必须该节点确实有事件未处理,那么就会把该节点重新放入到刚刚删除掉的且刚准备好的就绪链表,epoll_wait马上返回。而ET模式不会检查,只会调用一次,只通知就绪通知一次 。

(4)epoll函数底层实现过程
首先epoll_create创建一个epoll文件描述符,底层同时创建一个红黑树,和一个就绪链表;红黑树存储所监控的文件描述符的节点数据,就绪链表存储就绪的文件描述符的节点数据;epoll_ctl将会添加新的描述符,首先判断是红黑树上是否有此文件描述符节点,如果有,则立即返回。如果没有, 则在树干上插入新的节点,并且告知内核注册回调函数。当接收到某个文件描述符过来数据时,那么内核将该节点插入到就绪链表里面。epoll_wait将会接收到消息,并且将数据拷贝到用户空间,清空链表。对于LT模式epoll_wait清空就绪链表之后会检查该文件描述符是哪一种模式,如果为LT模式,且必须该节点确实有事件未处理,那么就会把该节点重新放入到刚刚删除掉的且刚准备好的就绪链表,epoll_wait马上返回。ET模式不会检查,只会调用一次
View Code
原文地址:https://www.cnblogs.com/pdun/p/10516467.html