Web高阶课堂笔记8(并发编程-IO模型)

并发编程-IO模型

基本概念和术语

Unix体系结构

关于Unix操作系统的体系结构:

  • 内核(Kernel)
    主要用于控制硬件以及提供运行环境,位于核心部分。
  • 系统调用(systemcall)
    是内核提供的接口。
  • Shell和公共函数库
    是系统调用之上,应用程序可以使用公共函数库,在部分情况下也可以使用系统调用。Shell是一个命令行解释器程序,可以按照用户的输入执行相关操作,也可以运行其他程序。
  • 应用程序
    大多数情况使用公共函数库,部分情况也可以使用系统调用。
    Web高阶课堂笔记8(并发编程-IO模型)_第1张图片

一般运维人员使用到Shell脚本。

Unix与Linux

Linux是可以称为Unix系统的一种实现,或者说是一类Unix操作系统,可以提供Unix编程环境,除了Linux外还有BSD、Mac OS X】Solaris等Unix系统实现。但同时Linux也有自身的特点,比如支持更多的系统调用,具有更多新的特性。

文件描述符与套接字

I/O设备可以被抽象为文件(正如Linux遵循的“一切皆文件”的理念),在I/O设备上的输入和输出被处理为对应的文件读与写操作。

文件描述符(file descriptor)简称fd:
当打开一个文件时,内核会返回一个非负整数,用来标识该文件。在此后的读写等处理过程中,应用程序即可以通过这个描述符来访问,而不需要记录有关文件的其他信息。

套接字(socket):
在网络编程中常用的一种文件类型,一个套接字便是一个有着对应描述符的打开文件,用于和另外一个进程进行网络通信。

用户空间与内核空间

从内核安全和可靠性考虑,用户程序不能直接运行内核代码或操作内核数据,为此操作系统有内核空间和用户空间划分。运行在用户空间的应用程序(比如图形以及文本编辑器、音乐播放器等)想要执行某些系统调用时,需要通过特定的机制来告诉内核。

还比如TCP发送原理中,send和recv原理:
收发的数据通过套接字后并不是直接通过网卡进行服务端客户端传送,而是在网卡之前有个数据缓冲区,先将发送或者接收的数据放在缓冲区再通过网卡传送。
这就是用户空间和内核空间

Web高阶课堂笔记8(并发编程-IO模型)_第2张图片

Unix I/O模型

在Unix中,有5种I/O模型:

  • 阻塞式I/O (blocking I/O)
  • 非阻塞式I/O (nonblocking I/O)
  • I/O复用 (I/O multiplexing)
  • 信号驱动式I/O (signal-driven I/O)
  • 异步I/O (asynchronous I/O)

网络编程为背景,分别来了解这几种模型。

I/O中一个输入操作在操作系统层面通常包括两个过程:

  1. 等待数据准备
  2. 由内核(可以看成是操作系统)向对应进程(看成应用程序)中进行数据拷贝
    Web高阶课堂笔记8(并发编程-IO模型)_第3张图片

对应在网络套接字上:首先 等待网络中的数据到达,数据到达后,先被拷贝至内核缓冲区,接着再由内核缓冲区拷贝至进程中。

阻塞式I/O (blocking I/O)

是一种思想,而不是一种写法,比如当代码如何如何写就是阻塞的,而不写这样的代码就不是阻塞,基本代码的思想就是阻塞的,只有协程不是阻塞的,协程的思想就是利用阻塞等待的时间去做其他任务。

阻塞式I/O是最常见的,并且也是最常用的I/O模型。阻塞式I/O会因为无法立即完成某个操作而被挂起。对于一个套接字来说,其默认情况下便是阻塞的。当相应的操作系统调用操作阻塞时(比如send、recv、accept、connent等操作),对应的进程则会进入睡眠,直至操作完成后才恢复进行。

如下图所示:当应用进程调用recvfrom时,其对应的系统调用会阻塞,等待至数据报到达,并复制到应用进程的缓冲区后才会返回,对应上述的两个过程,即等待数据和数据拷贝,阻塞式I/O在这两个过程中都是阻塞的
Web高阶课堂笔记8(并发编程-IO模型)_第4张图片
比如TCP的客户端与服务端之间数据传送代码,数据虽然能传输但是并没有解决阻塞式I/O思想。也就出现了非阻塞式I/O

非阻塞式I/O (nonblocking I/O)

非阻塞I/O相关的系统调用无论操作是否完成,总会立即返回。

为嘛可以将套接字设置为非阻塞式,当进程调用recvfrom时,若没有数据到达,应用进程无需等待,内核会立即返回一个EWOULDBLOCK(‘期望阻塞’)(在一些系统实现下,也可能会返回EAGAIN【‘再来一次’】)。在非阻塞式I/O中,应用进程可以以这种形式不断轮询(polling)内核,通过循环调用recvfrom以查看数据报是否准备好,在每一次轮训内核返回后,应用进程可以选择进行一些其他任务的处理后在此发起轮询。
Web高阶课堂笔记8(并发编程-IO模型)_第5张图片

正常情况下代码走到了recv或者recvfrom会等待会阻塞
即下图红框部分代码会进行阻塞等待
Web高阶课堂笔记8(并发编程-IO模型)_第6张图片

要使用非阻塞需要在服务端写入代码: server.setblocking(False):这个参数默认是True,需要写成False。当参数是True会进行等待,如果是False则不会等待。
Web高阶课堂笔记8(并发编程-IO模型)_第7张图片

非阻塞I/O出现的异常基本都需要自己手动进行异常处理。

import socket

sever = socket.socket()
sever.bind(('127.0.0.1', 8080))
sever.listen(128)

# 将所有网络阻塞变为非阻塞
sever.setblocking(False)

list = []
d_list = []

while True:
    try:
        c, b = sever.accept()
        list.append(c)
    except BlockingIOError as e:
        # 以下代码可以做其他事而不再是等待的阻塞状态
        for i in list:
            try:
                data = i.recv(1024)
                print(data)

                # 如果接收到的数据长度为0代表服务器断开连接:
                if len(data) == 0:
                    i.close()
                    # 断开连接就关闭套接字,并把套接字端口数据让入垃圾列表中
                    d_list.append(i)
                    continue
                # 没断开连接的话就发送数据并转为大写
                i.send(data.upper())
            except BlockingIOError as e:
                continue

            # 出现停止服务端的报错解决:
            except ConnectionResetError as e:
                i.close()  # 关闭连接的套接字
                d_list.append(i)

    # 统一删除数据
    for i in d_list:
        list.remove(i)

非堵塞I/O模型弊端:
在等待数据准备过程中不被阻塞(但在数据从内核复制到用户空间的过程中任是阻塞的),从而可以在等待过程中执行其他任务,但同时由于按照一定频率进行轮询,数据准备好的时间点可能位于两次轮询之间,从而导致数据到达后不能及时的被后续过程处理,存在一定延迟。同时这种通过应用进程主动不断轮询内核数据是否就绪,往往存在多次轮询并没有数据就绪,就会造成CPU资源多余的消耗(通常非阻塞I/O需要结合另外的I/O通知机制一起发挥作用,比如结合I/O复用等)。

I/O复用 (I/O multiplexing)

在不考虑多线程的情况下,如果要想在单个进程内处理多个文件描述符的话,显然在阻塞式I/O下是无法同时在多个文件描述符对于的阻塞调用上同时进行阻塞。比如。当打开多个套接字时候,当某个套接字用上了recvfrom但无数据准备好时,进程会阻塞等待数据到达,那么这个时候就无法处理已经准备好数据的描述符,整个程序执行率会比较低。

那么非阻塞I/O,在应用进程的系统调用不会阻塞,而是返回某种特定的错误,但是就像之前代码示例一样,应用进程需要不断轮询内核以期对应的文件描述符准备好,这种效率低下会消耗大量的CPU时间。这时候在应用进程中循环的查询多个非阻塞模式的描述符状态,然后在任意一个描述符数据就绪时进行处理,就可以处理多个文件描述符。
Web高阶课堂笔记8(并发编程-IO模型)_第8张图片

I/O复用等待部分是系统执行的监管sock是需要等待的。
与阻塞式I/O相比还多了返回可读条件 这一步,单看模型或者当监管一个对象的时候,I/O复在效率方面用可能比不上阻塞式I/O。但是I/O复用可以一次性监管多个sock对象,如果对象有人触发了那么返回可执行对象。所以这个优势在量上。

I/O复用的使用:
先导入select模块
将阻塞变为非阻塞:xx.setblocking(False)
再调用select.select(rlist, wlist, xlist, timeout=None)

select的参数都需要列表形式,如果没有内容可传入空列表
rlist:等待直到有返回可读对象。
wlist:等待到可以输入。
xlist:等待有特殊情况(异常)。
wlist、xlist暂时用不到,参数可直接写空列表。
注意:select监听的对象是xx=socket.socket()和aa, bb = xx.accept()两部分。

如下图所示:
Web高阶课堂笔记8(并发编程-IO模型)_第9张图片

Web高阶课堂笔记8(并发编程-IO模型)_第10张图片通过上图打印res得到了参数中的内容,即:套接字创建的变量server以及两个空列表。所以可以通过拆包接收内容。即:aa,bb,cc =select.select(rlist, wlist, xlist),此处为:rlist, wlist, xlist = select.select(read_list,[],[])。那么rlist接收read_list。 wlist和xlist分别接收[ ]。

服务端代码:

import socket
import select

"""
服务端:
select监听对象:
1、xx = socket.socket()
2、aa, bb = xx.accpet()
"""
# 创建套接字
sever = socket.socket()

# 绑定套接字
sever.bind(('127.0.0.1', 8080))

# 监听套接字主动为被动
sever.listen(128)

# 非阻塞式I/O创建
sever.setblocking(False)

# 创建I/O复用,传入参数. sever_list当前指向创建套接字sever,打印结果就是sever赋值的内容即套接字的信息
# sever_list=[sever]

sever_list = [sever]  # sever_list本质是selecy需要监听的对象,即文本解释中的2种对象
# res = select.select(sever_list, [], [])

while True:
    # 将I/O复用的三个参数进行拆包接收
    rlist, wlist, xlist = select.select(sever_list, [], [])
    for i in rlist:
        # 通过判断解决出现无效参数时候的报错方式,如果i是原始套接字那么添加等待套接字,否则进行数据接发
        if i is sever:
            coon, addr = i.accept()  # i指向rlist等于在sever_list中循环。等待连接创建针对性套接字即等于sever.accept()
            # 添加新套接字进入到I/O复用所需用到到监听参数中。
            sever_list.append(coon)
        else:
            res = i.recv(1024)
            # 当接收的数据没有后关闭套接字并且删除列表元素i
            if len(res) == 0:
                i.close()
                sever_list.remove(i)

            # 打印接收到的数据
            print(res)
            # 发送数据
            i.send(b'hehe')

遇到报错点:

创建好I/O复用在使用等待连接锁创建的新套接字连接后会出现一个报错:提供一个无效参数,报错指向等待连接处。
Web高阶课堂笔记8(并发编程-IO模型)_第11张图片
原因:当新的套接字传入监听对象后,表示已经连接好。所以显示无效参数。

解决方法:创建判断。当等待所创建的新套接字没有被连接的时候进行连接,如果已经连接完毕则进行数据传输。

I/O复用与非阻塞I/O区别:

非阻塞I/O:需要手动进行异常处理代码书写
I/O复用:不需要进行异常处理代码书写,由系统帮助监管处理了。

系统监管机制:

I/O复用是通过select机制进行监管的。除此之外系统还有其他的监管机制。

select机制: windoes和Linux都有
poll机制:Linux有

select和poll机制都可以监管多个对象。但是,poll机制可以监管的数量更多。这两个机制都有个缺点都是系统调用,采用轮询方式,会出现轮询的缺陷延迟响应(即比如1-10个对象,第一个对象调用时候没有内容,所以去监管下一个内容,当监管到第三个到时候第一个对象有内容了,但此时无法响应,必须等到10个对象都轮完重新回到第一个到时候才可以响应。)

为了解决这个情况出现了epoll机制。
epoll机制:Linux有。添加回调机制,一旦有响应回调机制立刻触发(即轮到第三个对象的时候第一个对象有内容了就立刻回到第一个对象进行触发)

selectors 模块:根据不同的操作系统,来采用不同的操作机制。

异步I/O (asynchronous I/O)

异步I/O中不需要应用进程调用recvfrom来完成数据的复制过程,这也是异步的主要工作机制。 异步I/O相关函数通知内核进行I/O操作,在内核执行完毕包括等待数据和将数据复制到用户空间等所有I/O操作后再通知。

如下图所示:比如当调用aio_read函数时,会将描述符以及相关数据传送给内核,并将整个操作完成后以通知方式告知内核,该调用会马上返回,进程不会阻塞。
Web高阶课堂笔记8(并发编程-IO模型)_第12张图片
异步I/O模型是所有模型中效率和次数最多的。

asyncio模块(系统内置模块)
@asyncio.coroutine装饰器

import asyncio
# 异步I/O本质是单线程并发 即协程
import threading


@asyncio.coroutine
def task():
    # 打印当前协程号
    print('hello world %s' % threading.current_thread())
    # 等待一秒
    yield from asyncio.sleep(1)  # 模拟I/O操作
    # 再次打印当前协程号
    print('hello world %s' % threading.current_thread())


# 类似协程创建
loop = asyncio.get_event_loop()
# 创建任务(类似协程任务)
tasks = [task(), task()]
# 运行所有任务
loop.run_until_complete(asyncio.wait(tasks))
# 关闭任务
loop.close()

IO模型对比

简要对比

下图中第一阶段是指等待数据过程,第二阶段指数据从内核复制到用户空间过程。
Web高阶课堂笔记8(并发编程-IO模型)_第13张图片

阻塞和非阻塞、同步和异步

阻塞和非阻塞,一个阻塞操作会将对应的进程挂起直至操作完成才恢复执行。对一个正在执行的进程可能会由于资源未到位、操作未完成等原因造成阻塞。

在操作系统层面,同步I/O操作会导致请求进程阻塞,直至I/O操作完成。
异步I/O操作在整个过程中都不会导致请求进程阻塞。对比上述5种I/O模型,其中阻塞式、非阻塞式、I/O复用和信号驱动式都属于同步I/O。因为都存在阻塞进程的I/O操作。
异步I/O模型属于异步操作,整个进程在等待数据和数据复制过程中均不会阻塞。

在应用层面上(就非操作系统和内核层面的)异步网络库和异步框架可以提供异步调用的接口(某些任务执行过程可以独立于主程序流程,常用回调或者协程编码方式),但是在操作系统层面这些框架所使用的接口通常是内核中成熟的同步I/O。

结合应用层面来看,阻塞和非阻塞的概念强调的是调用者在调用后的一种运行状态,是挂起还是继续执行。而同步异步的概念强调的是执行结果返回的通知方式,同步模型中调用者调用后等待直至返回结果。异步中调用后立即返回,执行结果通常会通过其他机制通知调用者。

你可能感兴趣的:(课堂笔记)