并发编程-IO模型
基本概念和术语
Unix体系结构
关于Unix操作系统的体系结构:
- 内核(Kernel)
主要用于控制硬件以及提供运行环境,位于核心部分。 - 系统调用(systemcall)
是内核提供的接口。 - Shell和公共函数库
是系统调用之上,应用程序可以使用公共函数库,在部分情况下也可以使用系统调用。Shell是一个命令行解释器程序,可以按照用户的输入执行相关操作,也可以运行其他程序。 - 应用程序
大多数情况使用公共函数库,部分情况也可以使用系统调用。
一般运维人员使用到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原理:
收发的数据通过套接字后并不是直接通过网卡进行服务端客户端传送,而是在网卡之前有个数据缓冲区,先将发送或者接收的数据放在缓冲区再通过网卡传送。
这就是用户空间和内核空间
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会因为无法立即完成某个操作而被挂起。对于一个套接字来说,其默认情况下便是阻塞的。当相应的操作系统调用操作阻塞时(比如send、recv、accept、connent等操作),对应的进程则会进入睡眠,直至操作完成后才恢复进行。
如下图所示:当应用进程调用recvfrom时,其对应的系统调用会阻塞,等待至数据报到达,并复制到应用进程的缓冲区后才会返回,对应上述的两个过程,即等待数据和数据拷贝,阻塞式I/O在这两个过程中都是阻塞的。
比如TCP的客户端与服务端之间数据传送代码,数据虽然能传输但是并没有解决阻塞式I/O思想。也就出现了非阻塞式I/O
非阻塞式I/O (nonblocking I/O)
非阻塞I/O相关的系统调用无论操作是否完成,总会立即返回。
为嘛可以将套接字设置为非阻塞式,当进程调用recvfrom时,若没有数据到达,应用进程无需等待,内核会立即返回一个EWOULDBLOCK(‘期望阻塞’)(在一些系统实现下,也可能会返回EAGAIN【‘再来一次’】)。在非阻塞式I/O中,应用进程可以以这种形式不断轮询(polling)内核,通过循环调用recvfrom以查看数据报是否准备好,在每一次轮训内核返回后,应用进程可以选择进行一些其他任务的处理后在此发起轮询。
正常情况下代码走到了recv或者recvfrom会等待会阻塞
即下图红框部分代码会进行阻塞等待
要使用非阻塞需要在服务端写入代码: server.setblocking(False):这个参数默认是True,需要写成False。当参数是True会进行等待,如果是False则不会等待。
非阻塞I/O出现的异常基本都需要自己手动进行异常处理。
import socket
sever = socket.socket