基础概念
正式开始之前,需要铺垫一些基本概念,以免接下来看到一脸懵逼。
我们都知道,在操作系统中,CPU
负责执行指令,这些指令有些来自应用程序,有些是底层系统的自调用。有些指令是非常危险的,如清除内存,网络连接等等,如果错误调用的话有可能导致系统崩溃。因而CPU
将指令分为特权指令和非特权指令,对于某些特定的指令,只需要操作系统及其相关模块进行调用。因而,根据这个特点,操作系统内部也划分出了内核态和用户态。
内核态
内核态拥有完全的底层资源控制权限,可以执行任何的CPU
指令,访问任何内存地址,其占有的处理机是不允许被抢占的。
用户态
用户程序是运行在操作系统之上,这些程序运行时称之为用户态,用户态下不能直接访问底层硬件和内存地址,只能通过委托系统调用的方式来访问底层硬件和内存。
用户态到内核态如何切换
从用户态切换到内核态有三种方式:
- 系统调用:这是用户态主动要求切换到内核态的一种方式。用户进程通过系统调用申请使用操作系统提供的某些服务以便完成工作,比如,调用
fork()
指令实际上就是执行了一个创建新进程的系统调用。系统调用的机制其核心在于**使用了操作系统为用户特别开放的一个中断来实现的,例如Linux
的int 80h
中断; - 外设中断:当外围设备完成用户请求的操作后,会向
CPU
发出相应的中断信号。这时CPU
会暂停执行下一条即将要执行的指令转而去执行与中断信号对应的处理程序。如果先前执行的是用户态下的指令,那么这个切换过程就是用户态转为内核态。比如硬盘读写操作完成,系统会切换到硬盘读写的中断处理程序中执行后续操作; - 异常:当
CPU
在执行运行处于用户态的程序时,发生了一些不可知的异常,这个时候就会触发由当前运行进行切换到处理此异常的内核相关程序中,也就是转到了内核态,比如缺页异常;
这三种是用户态切换到内核态的主要方式,系统调用是主动的,后面两种是被动的。
Linux
的整体架构图如下所示:
同步/异步
同步/异步关注的是消息通信机制。
同步:所谓同步,就是在发出一个功能调用时,在没有得到结果之前,该调用就不返回。等前一件做完了才能做下一件事。
异步:异步的概念和同步相对。当一个异步过程调用发出后,调用者若不能立刻得到结果,此时可以直接返回然后执行其他任务,等到获得了结果之后通过状态、通知或者回调等手段通知调用者。
同步、异步一般发生在不同的线程/进程之间,如Thread1
和Thread2
是同步执行还是异步执行的。
阻塞和非阻塞
阻塞和非阻塞关注的是程序在等待调用结果时的状态。
阻塞: 阻塞调用是指调用返回之前,当前线程会被挂起,只有当调用得到结果后才返回。
非阻塞:与阻塞相反,非阻塞调用是指在不能立即得到结果之前,该函数不会将当前线程阻塞,而是立即返回。
五种 IO 模型
IO
一般分为磁盘IO和网络IO,这里我们主要关注网络IO。一次完整的网络IO
过程如下所示:
从上图可以看出,数据无论从网卡到用户空间还是从用户空间到网卡都需要经过内核。
阻塞IO模型
当应用程序调用一个 IO
函数,其底层会委托操作系统的recvfrom()
去完成,当数据还没有准备好时,revfrom
会一直阻塞,等待数据准备好。当数据准备好后,从内核拷贝到用户空间,recvfrom
返回成功,IO
函数调用完成。过程如下所示:
阻塞IO
模型的优点是编程简单,但缺点是需要配合大量线程使用。应用进程没接收一个连接,就需要为此连接创建一个线程来处理该连接上的读写任务。
非阻塞IO模型
调用进程在等待数据的过程中不会被阻塞,而是会不断地轮询查看数据有没有准备好。当数据准备好后,将数据从内核空间拷贝到用户空间,完成IO
函数的调用。等待数据的过程是非阻塞的,但数据拷贝时仍是阻塞的。过程如下所示:
非阻塞io
的优点在于可以实现使用一个线程同时处理多个连接的需求,减少线程的大量使用。缺点在于要不断地去轮询检查数据是否准备好,比较耗费CPU
。
IO复用模型
为了解决非阻塞IO
不断轮询导致CPU
占用升高的问题,出现了IO
复用模型。IO
复用中,使用其他线程帮助去检查多个线程数据的完成情况,提高效率。
Linux
中提供了select
、poll
和epoll
三种方式来实现IO
复用。一个线程可以对多个IO
端口进行监听,当有读写事件产生时会分发到具体的线程进行处理。过程如下所示:
IO
复用只需要阻塞在select
,poll
或者epoll
,可以同时处理和管理多个连接。缺点是当select
、poll
或者epoll
管理的连接数过少时,这种模型将退化成阻塞IO
模型。并且还多了一次系统调用:一次select
、poll
或者epoll
一次recvfrom
。
信号驱动IO模型
应用程序可以创建一个信号驱动程序SIGIO
,当数据没有处理好时,应用程序继续运行,不会被阻塞。当数据准备好之后,操作系统向应用程序发送信号,之后信号驱动程序就会执行,在信号处理函数中调用 IO
函数处理数据。过程如下所示:
信号驱动IO
模型的优点在于非阻塞,缺点在于串行处理信号驱动程序,当前一个SIGIO
没有被处理的情况下,后一个信号也不能被处理。在信号量大的时候会导致后面的信号不能被及时感知。
异步IO模型
相比于同步IO
,异步IO
不是顺序执行的。应用进程在执行aio_read
系统调用之后,无论数据是否准备好,都会直接返回给用户进程,然后应用进程可以去做别的事情。当数据准备好之后,内核直接复制数据给用户进程,然后内核向进程发送通知。过程如下:
信号驱动IO
模型中内核通知应用进程数据何时准备好,而在异步IO
模型中内核将数据复制完成之后告知应用进程IO
操作已完成。
在异步IO
模型中,应用进程调用aio_read
以及数据被拷贝到用户空间这两个过程都是非阻塞的。
总结
IO
模型公有五种,前四种模型区别在于第一部分,即系统调用,但是第二部分都是一样的,即将数据从内核空间拷贝到用户空间这个过程,进程阻塞于redvfrom
的调用。而最后一种,异步IO
模型,在系统调用和数据拷贝过程都是非阻塞的。
参考
Redis的IO多路复用机制