Python同步与异步IO一直是新手难以理解的问题,现在就来一探究竟吧。先说容易理解的吧。同步,就是你去麦当劳点了一个汉堡包,你一直在服务台等着汉堡包送到你手上。异步就是你去麦当劳点了一个汉堡包,然后你去隔壁的商场购物,不等汉堡做好,直到麦当劳的服务员叫你去拿汉堡包。一句话,有等待同步,没有等待异步!Linux操作系统基础知识用户空间和内核空间操作系统的核心是内核,内核独立于普通应用程序,可以访问受保护的内存空间和底层硬件设备的所有权限。为了保证用户进程不能直接操作内核以保证内核的安全,堪忧系统将虚拟空间分为两部分,一部分是内核空间,一部分是用户空间。对于32位操作系统,其寻址空间(虚拟存储空间)为4G)。对于Linux操作系统,最大的1G字节(从虚拟地址0xC0000000到0xFFFFFFFF)被内核使用,称为内核空间,低3G字节(从虚拟地址0x00000000到0xBFFFFFFF),被各个进程使用并称为用户空间。文件描述符文件描述符用于表达对文件的引用的抽象概念。文件描述符形式上是一个非负整数。其实就是一个索引值,指向内核为每个进程维护的进程打开文件的记录表。当程序打开现有文件或创建新文件时,内核会向进程返回一个文件描述符。在编程中,一些低级编程往往围绕文件描述符展开。然而,文件描述符的概念往往只适用于UNIX和Linux等操作系统。该进程阻塞正在执行的进程。由于一些预期的事件没有发生,例如请求系统资源失败、等待某些操作完成、新数据未到达或没有新的工作要做等,系统自动执行阻塞原语(Block),将自身从运行状态变为阻塞状态。可见,进程的阻塞是进程自身的主动行为,因此只有处于运行状态(获取CPU)的进程才能转为阻塞状态。当进程进入阻塞状态时,不占用CPU资源。进程切换为了控制一个进程的执行,内核必须有能力挂起运行在CPU上的进程,并恢复之前挂起的进程的执行。这种行为称为进程切换。因此可以说,任何进程都是在操作系统内核的支持下运行的,都与内核息息相关。从一个进程上运行到另一个进程上运行,这个进程有以下变化:1.保存处理器上下文,包括程序计数器和其他寄存器。上下文是内核再次唤醒当前进程时所需要的状态,由一些对象(程序计数器、状态寄存器、用户栈等内核数据结构)的值组成。这些值包括划定地址空间的页表、包含进程信息的进程表、文件表等等。2.更新PCB信息。3.将进程的PCB移动到相应的队列中,比如就绪、阻塞在某个事件等队列中。4.选择另一个进程执行并更新其PCB。5.更新内存管理数据结构。6.恢复处理器上下文。总而言之,它会消耗大量资源。具体可以参考这篇文章:进程切换直接IO和缓存IO。缓存I/O也称为标准I/O。大多数文件系统的默认I/O操作是缓存I/O。.在Linux的缓存I/O机制中,操作系统会将I/O数据缓存在文件系统的页缓存(pagecache)中,即先将数据复制到操作系统内核的缓冲区中,然后从操作系统内核的缓冲区复制到应用程序的地址空间。以write为例,数据会先拷贝到进程缓冲区,再拷贝到操作系统内核的缓冲区,再写入存储设备。直接I/O的写:(少了复制到进程缓冲区的步骤)缓存I/O的缺点:在数据传输过程中,需要在应用程序地址空间和内核中进行多次数据复制操作。它带来的CPU和内存开销非常大。IO模式对于一次IO访问(以read为例),数据会先被复制到操作系统内核的缓冲区中,然后再从操作系统内核的缓冲区中复制到应用程序的地址空间中。所以,当一个读操作发生时,会经历两个阶段:1.等待数据准备好(Waitingforthedatatobeready)2.将数据从内核复制到进程(Copyingthedatafromthekerneltoprocess)正式因为这两个阶段,Linux系统产生了以下五种网络模式解决方案。-阻塞I/O(阻塞IO)-非阻塞I/O(非阻塞IO)-I/O多路复用(IOmultiplexing)-信号驱动I/O(信号驱动IO)在实践中不常用-异步I/O(异步IO)阻塞IO在linux中,默认情况下所有套接字都是阻塞的。一个典型的read操作流程大概是这样的:以read为例:流程发起read,执行recvfrom系统调用;内核启动***阶段,准备数据(从磁盘复制到内核缓冲区),进程请求的数据并没有立即准备好;准备数据需要时间;在这个过程中,整个用户进程会被阻塞(进程选择Blocking),等待数据;直到数据从内核拷贝到用户空间,内核返回结果,进程解除阻塞,重新运行。因此,内核准备数据和从内核复制数据到进程内存地址的过程都被阻塞了。非阻塞IO模型可以通过设置socket使之成为非阻塞的。当对非阻塞套接字执行读操作时,流程是这样的:当用户进程发出读操作时,如果内核中的数据还没有准备好;那么它不会阻塞用户进程,而是立即返回错误,从用户进程的角度来看,它发起读操作后,不需要等待,而是立即得到结果;当用户进程判断结果为错误时,就知道数据还没有准备好,所以可以重新发送读操作。一旦内核中的数据准备好,再次接收到用户进程的系统调用;然后它立即将数据复制到用户内存,然后返回。所以,非阻塞IO的特点就是在内核准备数据的时候,用户进程需要不断的主动询问数据是否就绪。IO多路复用I/O多路复用使用select、poll、epoll来监控多个io对象,当io对象发生变化(有数据)时通知用户进程。优点是单个进程可以处理多个套接字。当然,具体的区别我们会在后面讨论。现在来看I/O多路复用的过程:当用户进程调用select时,整个进程会被阻塞;同时,内核会“监控”所有select负责的socket。;当任意一个socket中的数据准备好后,select将返回;此时用户进程调用read操作将数据从内核拷贝到用户进程。因此,I/O多路复用的特点是一个进程可以通过一种机制同时等待多个文件描述符,其中任意一个文件描述符(套接字描述符)进入read-ready状态,select()函数可以返回。与阻塞IO相比,这里需要两次系统调用(select和recvfrom),而阻塞IO只调用一次系统调用(recvfrom)。但是,使用select的好处是它可以同时处理多个连接。所以,如果处理的连接数不是很高,使用select/epoll的webserver不一定比使用多线程+阻塞IO的webserver好,延迟可能会更大。select/epoll的优势不在于处理单个连接速度快,而在于可以处理更多的连接。在IOmultiplexingModel中,在实践中,每个socket一般都设置为非阻塞的。但是,如上图所示,实际上整个用户进程一直处于阻塞状态。只是进程是由functionblock选择的,而不是socketIO给block的。异步IO用户进程发起读操作后,就可以马上开始做其他事情了。另一方面,从内核的角度来看,当它接收到一个异步读取时,它会先立即返回,因此它不会为用户进程产生任何块。然后,内核会等待数据准备完成,然后将数据拷贝到用户内存中。当这一切完成后,内核会向用户进程发送一个信号,告诉它读操作已经完成。总结在非阻塞IO中,虽然进程大部分时间不会被阻塞,但还是需要进程主动检查,当数据准备完成后,进程也需要主动再次调用recvfrom将数据复制到用户记忆。而异步IO则完全不同。就好比用户进程把整个IO操作交给别人(kernel)去完成,别人做完了再发信号。在此期间,用户进程不需要检查IO操作的状态,也不需要主动拷贝数据。
