Day13 PythonWeb全栈课程课堂内容

1. 并发编程-IO模型

Unix体系结构

  • 关于 Unix 操作系统的体系结构可以参考下图,其中内核( kernel )主要用于控制硬件以及提供运行环境,位于操作系统的核心部分。内核提供的接口则称为系统调用( system call ),在系统调用之上分别存在 shell 和公共函数库,应用程序可以使用公共函数库,在部分情况下也可以使用系统调用。shell 是一个命令行解释器程序,可以按照用户的输入执行相关的操作,也可以运行其他程序。

5fbb23000916ad1006210621.png

Unix 与 Linux

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

文件描述符与套接字

  • I/O 设备可以被抽象为文件(正如 Linux 遵循的 “一切皆文件” 的理念),在 I/O 设备上的输入和输出被处理为对相应的文件的读写操作。当打开一个文件时,内核会返回一个非负整数,用来标识该文件,称为文件描述符( file descriptor ),简称 fd 。在此后的读、写等处理过程中,应用程序即可通过这个描述符来访问文件,而不需要记录有关文件的其他信息。

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

用户空间与内核空间

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

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 中一个输入操作在操作系统层面通常包括两个过程,首先需要等待数据准备就绪,接着由内核向对应进程中进行数据拷贝。对应到网络套接字上,首先则是等待网络中的数据到达,数据到达后,先被拷贝至内核缓冲区,接着再由内核缓冲区拷贝至进程中。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-SNkHncwM-1611039506665)(/home/fyccheng/Desktop/IO.png)]

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

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

  • 如下图所示,当应用进程调用 recvfrom 时,其对应的系统调用会阻塞,等待至数据报到达,并复制到应用进程对应的缓冲区后才会返回。对应上述的两个过程,即等待数据和数据拷贝,阻塞式 I/O 在这两个过程中都是阻塞的。

  • 在内核实现层面上这种 I/O 模型实现简单,并且能够在数据报准备好后无延迟的返回数据以进行后续处理,但对于用户进程来说往往需要耗费时间来等待操作完成。

5fbb232009a787af14150835.png

常见的网络阻塞

  • recv,recvfrom网络I/O阻塞。

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-qWygVtFC-1611039506667)(/home/fyccheng/Desktop/IO2.png)]

# 客户端
import socket


client = socket.socket()
client.connent(('127.0.0.1', 8080))


while True:
	client.send(b'hello world')
	data = client.recv(1024)
	print(data)
# 服务端 为单个客户端服务
import socket


server = socket.socket()
server.bind(('127.0.0.1', 8080)) # IP可以不写

server.listen(128) # 必须得写
while True:
	conn, addr = server.accept()
	while True:
		try:
			data = conn.recv(1024)
			if len(data) == 0; break # 当客户端发送结束。
			print(data)
			conn.send(data.upper())
		except ConnectionError as e:
			break
            
    conn.close()
# 服务端 为多个客户端服务
'''
1.多线程或者多进程,创建多个线程或多个进程。
'''

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

  • 非阻塞 I/O 相关的系统调用无论操作是否完成,总会立即返回。如下图所示,我们可以将套接字设置为非阻塞模式,当应用进程调用 recvfrom 时,若没有数据到达,应用进程无需等待,内核会立即返回一个 EWOULDBLOCK(“期望阻塞”)(在一些系统实现下,也有可能会返回 EAGAIN(“再来一次”))。在非阻塞式 I/O 中,应用进程可以以这种形式不断轮询( polling )内核,通过循环调用 recvfrom 以查看数据报是否准备好,在每次轮询内核返回后,应用进程可以选择进行一些其他任务的处理后再次发起轮询。

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

# 服务端 
import socket


server = socket.socket()
server.bind(('127.0.0.1', 8080)) # IP可以不写

server.listen(128) # 必须得写

# 将所有的网络阻塞变成非阻塞
server.setblocking(False)

c_list = []
d_list = []

while True:
	try:
		conn, addr = server.accept()
		c_list.append(cpnn)
	except BlockigIOError as e:
		# 可以做其他事情
		# print('hello')
		for conn in  c_list:
			# 创建try来让recv变成飞阻塞
			try:
				data = conn.recv(1024)
				# print(data)

				if len(data) == 0:
					conn.close()
					d_list.append(conn)
					continue
				conn.send(data.upper())

			except BlockigIOError as e:
				continue

			except ConectionResetError as e:
				conn.close()
				d_list.append(conn)
	
	for conn in d_list:
		c_list.remove(conn)

5fbb23440948a0d514170935.png
I/O 多路复用( I/O multiplexing )

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

  • 那么,非阻塞 I/O 是否可以解决这个问题?在非阻塞 I/O 中,应用进程的系统调用不会阻塞,而是返回某种特定的错误,但就像上面的代码示例一样,应用进程需要不断地轮询内核以期对应的文件描述符准备好,这种方式效率低下,会消耗大量的 CPU 时间。但试想,如果我们在应用进程中循环的查询多个非阻塞模式下的描述符状态,然后在任意一个描述符数据就绪时进行处理,这样子便可以处理多个文件描述符。

    import socket
    import select
    '''
    select监听以下两个
    
    server = socket.socket()
    conn, addr = server.accept()
    
    select 机制   windows 和 linux 都有
    
    poll 机制		Linux poll可以监管数量比select更多
    
    epoll 机制	Linux
    	添加回调机制
    	一旦有响应,回调机制立刻触发。
    	
    selectors 模块
    '''
    
    server = socket.socket()
    server.bind(('127.0.0.1', 8080))
    
    server.listen(128)
    server.setblocking(False)
    
    read_list = [server]
    while True:
        # 帮你监管,一旦有人连接,立刻返回对应的监管对象
    	rlist, wlist, xlist = select.select(read_list, [], [])
    	for i in rlist:
            if i is server:
            	conn, addr = i.accpet()
            	read_list.append(conn)
            else:
                res = i.recv(1024)
                if len(res) == 0:
                    i.close()
                    read_list.remove(i)
                print(res)
                i.send(b'hello')
    

5fbb23a309e8411913970816.png

异步 I/O( asynchronous I/O )

  • 我们将下图和上面的几个图示简单对比,便能看出一个显著的区别是在异步 I/O 中不需要应用进程调用 recvfrom 来完成数据的复制过程。这也是异步 I/O 最主要的工作机制:异步 I/O 相关函数通知内核进行 I/O 操作,在内核执行完包括等待数据和将数据复制到用户空间等所有 I/O 操作后再通知我们。下图中所示,比如当我们调用 aio_read 函数时,会将描述符以及相关的数据传递给内核,并将整个操作完成后的通知方式告知内核,该调用会马上返回,进程不会阻塞。

    import asyncio # 用在爬虫中较多
    import threading
    # 单线程实心并发 协程
    # 效率最高
    
    @asyncio.coroutine
    def task():
        print('hello %s' % threading.current_thread())
        yield from asyncio.sleep(1) # 模拟IO操作
        print('hello $s' % threading.current_thread())
        
    loop = asyncio.get_event_loop()
    tasks = [task(), task()]
    loop.run_until_complete(asyncio.wait(tasks))
    loop.close()
    

5fbb23c4093763b514510869.png

  • 可以看到,异步 I/O 和上面的信号驱动式 I/O 都包含内核通知的过程,但这两种 I/O 模型有显著的区别。首先,对于信号驱动式 I/O 来说,内核产生信号通知我们的信息是何时执行 I/O 操作,I/O 操作需要我们自己完成,而异步 I/O 则不同,内核通知我们的信息是何时完成 I/O 操作,I/O 操作由内核来完成。其次,前者在数据准备就绪时便产生信号,后者在数据准备好并复制完成时才产生信号

IO 模型对比

简要对比

  • 下图中的第一阶段是指等待数据过程,第二阶段指将数据从内核复制到用户空间过程。

5fbb23ce09632a9713751049.png

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

  • 上面的内容中,我们多次提到阻塞和非阻塞,一个阻塞操作会将对应的进程挂起直至操作完成才恢复执行。对于一个正在执行的进程,可能会由于资源未到位、操作未完成等原因而阻塞。

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

  • 最后重要的一点,针对于我们在应用层面( user-level ,并非我们上面所说的操作系统层面或内核层面 kernel-level )上常说的异步网络库或异步框架(我们会在后面的小节中深入了解),这些框架可以提供异步调用的接口(意味着某些任务的执行过程可以独立于主程序流程,常用回调或者协程的编码方式),但在操作系统( Unix/Linux )这一层,这些框架所使用的接口通常都是内核中成熟的同步 I/O。结合应用层面来看,阻塞和非阻塞的概念强调的是调用者在调用后的一种运行状态,是挂起还是继续执行;而同步和异步的概念强调的是执行结果返回的通知方式,在同步模型中调用者在调用后等待直至返回结果,而在异步模型中调用者在调用后立即返回,执行结果通常会通过其他机制通知到调用者。

1. 并发编程-IO模型

Unix体系结构

  • 关于 Unix 操作系统的体系结构可以参考下图,其中内核( kernel )主要用于控制硬件以及提供运行环境,位于操作系统的核心部分。内核提供的接口则称为系统调用( system call ),在系统调用之上分别存在 shell 和公共函数库,应用程序可以使用公共函数库,在部分情况下也可以使用系统调用。shell 是一个命令行解释器程序,可以按照用户的输入执行相关的操作,也可以运行其他程序。

5fbb23000916ad1006210621.png

Unix 与 Linux

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

文件描述符与套接字

  • I/O 设备可以被抽象为文件(正如 Linux 遵循的 “一切皆文件” 的理念),在 I/O 设备上的输入和输出被处理为对相应的文件的读写操作。当打开一个文件时,内核会返回一个非负整数,用来标识该文件,称为文件描述符( file descriptor ),简称 fd 。在此后的读、写等处理过程中,应用程序即可通过这个描述符来访问文件,而不需要记录有关文件的其他信息。

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

用户空间与内核空间

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

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 中一个输入操作在操作系统层面通常包括两个过程,首先需要等待数据准备就绪,接着由内核向对应进程中进行数据拷贝。对应到网络套接字上,首先则是等待网络中的数据到达,数据到达后,先被拷贝至内核缓冲区,接着再由内核缓冲区拷贝至进程中。

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

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

  • 如下图所示,当应用进程调用 recvfrom 时,其对应的系统调用会阻塞,等待至数据报到达,并复制到应用进程对应的缓冲区后才会返回。对应上述的两个过程,即等待数据和数据拷贝,阻塞式 I/O 在这两个过程中都是阻塞的。

  • 在内核实现层面上这种 I/O 模型实现简单,并且能够在数据报准备好后无延迟的返回数据以进行后续处理,但对于用户进程来说往往需要耗费时间来等待操作完成。

5fbb232009a787af14150835.png

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

  • 非阻塞 I/O 相关的系统调用无论操作是否完成,总会立即返回。如下图所示,我们可以将套接字设置为非阻塞模式,当应用进程调用 recvfrom 时,若没有数据到达,应用进程无需等待,内核会立即返回一个 EWOULDBLOCK(“期望阻塞”)(在一些系统实现下,也有可能会返回 EAGAIN(“再来一次”))。在非阻塞式 I/O 中,应用进程可以以这种形式不断轮询( polling )内核,通过循环调用 recvfrom 以查看数据报是否准备好,在每次轮询内核返回后,应用进程可以选择进行一些其他任务的处理后再次发起轮询。

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

5fbb23440948a0d514170935.png

I/O 复用( I/O multiplexing )

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

  • 那么,非阻塞 I/O 是否可以解决这个问题?在非阻塞 I/O 中,应用进程的系统调用不会阻塞,而是返回某种特定的错误,但就像上面的代码示例一样,应用进程需要不断地轮询内核以期对应的文件描述符准备好,这种方式效率低下,会消耗大量的 CPU 时间。但试想,如果我们在应用进程中循环的查询多个非阻塞模式下的描述符状态,然后在任意一个描述符数据就绪时进行处理,这样子便可以处理多个文件描述符。

5fbb23a309e8411913970816.png

异步 I/O( asynchronous I/O )

  • 我们将下图和上面的几个图示简单对比,便能看出一个显著的区别是在异步 I/O 中不需要应用进程调用 recvfrom 来完成数据的复制过程。这也是异步 I/O 最主要的工作机制:异步 I/O 相关函数通知内核进行 I/O 操作,在内核执行完包括等待数据和将数据复制到用户空间等所有 I/O 操作后再通知我们。下图中所示,比如当我们调用 aio_read 函数时,会将描述符以及相关的数据传递给内核,并将整个操作完成后的通知方式告知内核,该调用会马上返回,进程不会阻塞。

5fbb23c4093763b514510869.png

  • 可以看到,异步 I/O 和上面的信号驱动式 I/O 都包含内核通知的过程,但这两种 I/O 模型有显著的区别。首先,对于信号驱动式 I/O 来说,内核产生信号通知我们的信息是何时执行 I/O 操作,I/O 操作需要我们自己完成,而异步 I/O 则不同,内核通知我们的信息是何时完成 I/O 操作,I/O 操作由内核来完成。其次,前者在数据准备就绪时便产生信号,后者在数据准备好并复制完成时才产生信号

  • IO 模型对比

简要对比

  • 下图中的第一阶段是指等待数据过程,第二阶段指将数据从内核复制到用户空间过程。

5fbb23ce09632a9713751049.png

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

  • 上面的内容中,我们多次提到阻塞和非阻塞,一个阻塞操作会将对应的进程挂起直至操作完成才恢复执行。对于一个正在执行的进程,可能会由于资源未到位、操作未完成等原因而阻塞。

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

  • 最后重要的一点,针对于我们在应用层面( user-level ,并非我们上面所说的操作系统层面或内核层面 kernel-level )上常说的异步网络库或异步框架(我们会在后面的小节中深入了解),这些框架可以提供异步调用的接口(意味着某些任务的执行过程可以独立于主程序流程,常用回调或者协程的编码方式),但在操作系统( Unix/Linux )这一层,这些框架所使用的接口通常都是内核中成熟的同步 I/O。结合应用层面来看,阻塞和非阻塞的概念强调的是调用者在调用后的一种运行状态,是挂起还是继续执行;而同步和异步的概念强调的是执行结果返回的通知方式,在同步模型中调用者在调用后等待直至返回结果,而在异步模型中调用者在调用后立即返回,执行结果通常会通过其他机制通知到调用者。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值