事件驱动----IO多路复用

1、事件驱动

一种编程范式,程序的执行流由外部事件来决定。它的一个特点是包含一个事件循环,当外部事件发生时使用回调机制来触发相应的处理。

2、IO多路复用

(1)用户空间与内核空间:

供操作系统使用的是内核空间,供用户使用的是用户空间。

假设一个操作系统4G内存,用户态占3G,操作系统1G。当运作起来时,会通过一个指令集来判断是什么状态的命令,在内核态时,操作系统是可以直接操纵4G内存并非1G,用户态只能操控3G内存。

(2)进程切换:
1、保存处理机的上下文,包括程序计数器和其他寄存器。2、更新PCB信息。3、把进程的PCB移入相应的队列,如就绪、在某事件阻塞等队列。4、选择另一个进程执行,并更新其PCB。5、更新内存管理的数据结构。6、恢复处理机上下文。由此可见进程切换很耗资源。

(3)进程阻塞:   是进程的一种主动行为,阻塞状态不占用cpu资源。

(4)文件描述符fd:

文件描述符是一个用于表述指向文件的引用的抽象化概念,可以理解为文件的句柄。

(5)缓存 I/O:

缓存 I/O 又被称作标准 I/O,大多数文件系统的默认 I/O 操作都是缓存 I/O。在 Linux 的缓存 I/O 机制中,操作系统会将 I/O 的数据缓存在文件系统的页缓存( page cache )中,也就是说,数据会先被拷贝到操作系统内核的缓冲区中,然后才会从操作系统内核的缓冲区拷贝到应用程序的地址空间。用户空间没法直接访问内核空间的,内核态到用户态的数据拷贝 。
缓存 I/O 的缺点: 数据在传输过程中需要在应用程序地址空间和内核进行多次数据拷贝操作,这些数据拷贝操作所带来的 CPU 以及内存开销是非常大的。

3、blocking IO (阻塞IO)

阻塞IO:先等待数据,数据到后,从内核copy到用户区。接收到数据继续向下执行。好处是只发送了一次系统调用,但缺点是全程阻塞。

4、non-blocking IO(非阻塞IO) 

非阻塞IO:发送一次请求发现没有数据,就去干其他事,过一段时间后回来再查看数据有没有到,如果没有就会去做其他事情,如此往复,直到数据到达,在多次recvfrom的时候cpu还是在自己手中,处于非阻塞状态,在复制数据的时候却处于阻塞状态。其缺点是:数据不及时、发送太多系统调用。 

5、  IO multiplexing(IO多路复用) 

 当用户进程调用了select,那么整个进程会被block。直到有数据到达后再次进行系统调用,从内核太复制出数据。

6、Asynchronous I/O(异步IO)

用户进程发起read操作之后,立刻就可以开始去做其它的事。而另一方面,从内核的角度,当它受到一个读命令之后,首先它会立刻返回,所以不会对用户进程产生任何block。然后,内核会等待数据准备完成,然后将数据拷贝到用户内存,当这一切都完成之后,内核会给用户进程发送一个信号,告诉它读操作完成了。

7、sellect、epoll二者的区别

epoll:没有最大文件描述符数量的限制。 水平触发、边缘触发(当外部条件发生变化就触发,切换的瞬间触发)。当有进程活跃时,直接去拿,不用再轮询。伪异步。

select:可以同时监听多个连接。只要数据没有接收到用户态(一直在内核态)就会一直监听。水平触发(只有一直处于某种状态才能触发)。时间大部分消耗在轮询上所以效率低,而且最大连接数为1024,但是跨平台性好。windows没有poll和epoll。Linux全有。

select模块:

select.select([input] , [output] , [error put] , step_time)。监听input列表内的事件。

from socket import *
import select

s = socket(AF_INET, SOCK_STREAM)
s.bind(("127.0.0.1",8080))
# s.setblocking(False)   设置为非阻塞
s.listen(5)
inp = [s, ]
while 1:
    r, w, e = select.select(inp,[],[])  # 这里将会卡住
    for i in r:
        if i == s:
            conn, addr = i.accept()
            inp.append(conn)  #  将新的事件添加进列表,下次一同监听
        else:
            data = i.recv(1024).decode('utf8')
            print("来自客户端的消息",data)
            i.send("hello".encode('utf8'))
        # print('2333')  没有把内核态的数据放到用户态中所以每次都触发select

如果不接收数据(执行accept方法),让数据一直保存在内核态,就会一直走for循环。如果接收了数据,就走一次for循环。

from socket import *
import select

s = socket(AF_INET, SOCK_STREAM)
s.bind(("127.0.0.1",8080))
# s.setblocking(False)
s.listen(5)
inp = [s, ]
while 1:
    print("卡在了这里")
    r, w, e = select.select(inp, [], [])
    for i in r:
        conn, addr = i.accept()
        data = conn.recv(1024).decode('utf8')
        print("来自客户端的消息", data)
        conn.send("hello".encode('utf8'))

这种情况只能接收一次数据,除非把conn添加到列表中,因为文件描述符 s 没有任何变化,除非有第二个用户进行连接。

selectors 模块:
selectors.Defaultselector() 根据操作系统默认选择io多路复用的模式。register(s,selectors event,accept)  文件描述符法 s 和 回调函数绑定。unregister(文件描述符)  解注册。select 开始监听文件描述符。

import selectors
from socket import *


def eat(sock,mask):
    conn, addr = sock.accept()
    sel.register(conn, selectors.EVENT_READ, speak)   # 将conn和speak函数进行绑定
def speak(conn,mask):
    try:
        data = conn.recv(1024).decode('utf8')   # 在Windows如果远程强行关闭data不为空,Linux为空
        print(data)
        conn.send('hello'.encode('utf8'))
    except Exception as e:
        sel.unregister(conn)  #  删除一个监听事件

sk = socket(AF_INET, SOCK_STREAM)
sk.bind(("127.0.0.1", 8080))
sk.listen(5)
sk.setblocking(False)
sel = selectors.DefaultSelector()  #  
sel.register(sk, selectors.EVENT_READ, eat)  # 将sk和eat函数进行绑定
print('running...')
while 1:
    l = sel.select()  # 在这个位置开始监听
    for key, mask in l:
        callback = key.data  # 绑定的函数名
        callback(key.fileobj, mask)  # mask 是掩码  fileobj 是当前服务端的套接字

多路复用就是在单进程、线程实现并发的操作。

猜你喜欢

转载自blog.csdn.net/weixin_41678001/article/details/84703334
今日推荐