目录
2、几种常见的IO模型(我理解IO模型的实现都是操作系统做的工作,对应用层而言,只是选择使用什么模型实现的系统调用函数而已)
一、操作系统内核
- 操作系统是管理计算机硬件与软件资源,并提供与用户交互的程序,它的核心是操作系统内核。内核,就是计算机学科意义上的操作系统,直接与硬件交互,提供CPU时间片管理、中断、内存管理、IO管理等等;一般意义上的操作系统包含的东西要更多一些,至少要有用户交互的基本程序,比如一个命令行界面和基本的指令(文件遍历、进程管理等等),或者图形界面的桌面和文件浏览器。
- 内核程序一直占据内存中的一段内存,这样处理器可以随时调用这些内核程序(Linux进程的4GB地址空间,3G-4G部 分大家是共享的,是内核态的地址空间,这里存放在整个内核的代码和所有的内核模块,以及内核所维护的数据。);
- 总之一句话,内核就是操作系统中的一个子程序(提供最基础的功能),提供对软件层面的抽象(例如对进程、文件系统、同步、内存、网络协议等对象的操作和权限控制)和对硬件访问的抽象(例如磁盘的IO,显示,网卡(网络通信)等)。而操作系统是在内核的基础上进行延申,包括了提供基础服务和系统组件。
二、内核态与用户态
操作系统作为计算机资源与应用程序间的中间层,有两个基本功能:
- 防止硬件被失控的应用程序滥用;
- 向应用程序提供简单一致的机制,从而控制复杂而又通常大相径庭的低级硬件设备。
为了防止系统程序、硬件不被应用程序有意或者无意的滥用破坏(试想一下所有程序都能清理内存、设置始终),计算机系统为计算机设置了两种运行级别:
- 内核态(系统态):操作系统在内核态运行,能够访问所有的内存空间(用户存储空间和系统存储空间),包括外围设备如硬盘、网卡等,所占用的CPU不可以被抢占,CPU也可以将自己从一个程序切换到另一个程序;
- 用户态:应用程序只能在用户态运行,进程访问的内存空间和对象受限(不能对系统中的软件和硬件进行直接访问,对内存的访问范围只局限于用户空间),它所占用的CPU可以被抢占。
进程在实际运行过程中会在内核态和用户态之间来回切换,相应的现代多数操作系统将CPU指令分为特权指令和非特权指令(Intel的CPU将特权等级分成四级)两类:特权指令在内核态时使用,非特权指令可以在用户态使用。
两种状态之间如何切换?
1、系统调用(陷阱指令):这是用户态应用进程主动要求切换到内核态的一种方式,用户态进程通过系统调用申请使用操作系统提供的服务程序(软件或者硬件),比如磁盘IO,网络通信(socket操作、accept()等操作),redis中的fork()、epoll、select()等操作。当进程执行系统调用而陷入内核代码执行时,我们就称进程在内核态运行,当进程运行自己的程序时(应用程序)我们称进程在用户态运行;
2、异常:当处理器在用户态执行应用程序时,如果此时发生异常,处理器会切换到处理此异常的内核程序中,也就进入内核态;
3、外围设备的中断:当外围设备完成用户请求的操作后,会向CPU发出相应的中断信号,这时CPU会暂停执行下一条即将要执行的指令转而去执行与中断信号对应的处理程序,如果先前执行的指令是用户态下的程序,那么这个转换的过程自然也就发生了由用户态到内核态的切换。比如硬盘读写操作完成,系统会切换到硬盘读写的中断处理程序中执行后续操作等。
这3种方式是系统在运行时由用户态转到内核态的最主要方式,其中系统调用可以认为是用户进程主动发起的,异常和外围设备中断则是被动的,但他们终归都是中断响应的过程。
为什么要有用户态和内核态?
由于需要限制不同的程序之间的访问能力, 防止他们获取别的程序的内存数据, 或者获取外围设备的数据, 并发送到网络, CPU划分出两个权限等级 – 用户态和内核态。
三、用户态和内核态的应用
1、IO过程:通常情况下一个进程中的IO过程分为两个阶段:
- 用户空间(工作内存)<----->内核空间(主内存)
- 内核空间<----->设备空间(硬盘、网卡)
当应用进程发起系统调用后,处理器会从用户态转为内核态,处理器执行调用程序过程:先去内核空间中维护的设备缓冲区查看数据是否准备好,如果没有数据,会去设备空间中读取。IO读取的过程很慢,需要等待,直到将设备区的数据全部读取到内核空间对应的设备缓冲区,然后再复制到用户内存空间;如果内核缓冲区有就直接复制到用户内存空间。因为在此过程中CPU在内核中执行调用的内核程序,应用程序会被阻塞(阻塞IO)。所以网络输入过程也分为两个阶段:
- 等待网络数据到达网卡<------->读取网卡数据到内核空间对应的设备缓存区
- 从内核缓存区读取数据到用户空间
2、几种常见的IO模型(我理解IO模型的实现都是操作系统做的工作,对应用层而言,只是选择使用什么模型实现的系统调用函数而已)
- 同步阻塞IO:整个IO过程用户进程一直被阻塞,在发起系统调用后就被挂起了,知道数据返回到用户空间时线程才被再次唤醒;
应用进程发起IO读取recvfrom()系统调用后,处理器切换到内核态,执行内核程序,如果内核缓冲区无数据,进程被阻塞(等待待数据到达设备空间,再从设备空间复制数据到内核缓冲区),整个IO处理完毕后返回进程。操作成功则进程获取数据。
特点:线程阻塞挂起,不消耗CPU资源,但如果阻塞线程太多会消耗线程资源。及时响应。实现并发量少的网络应用开发。
- 同步非阻塞IO:用户进程发起调用后,用户线程不会被挂起,而是会继续执行下面的程序,但是会反复调用系统函数查看内核空间缓冲区数据是否准备好。直到准备好拷贝到用户空间。
进程发起IO系统调用后,内核缓冲区如果没有数据需要到IO设备中读取,处理器会返回一个错误而不会阻塞。如果有数据,直接返回。
特点:进程轮询(重复)调用,消耗CPU资源,使用并发量小且不需要及时响应的网络应用开发。
- IO多路复用(实现IO多路复用模型的系统函数有select、poll、epoll):当进程发起epoll函数调用后,会一致阻塞,直到收到数据准备好的通知,才被唤醒。与同步阻塞IO不同的是,系统函数epoll的实现模型是IO多路复用,其实就是epoll()同时监听多个io对象,如果有一个io对象发生变化就唤醒用户进程发起获取数据的调用。
用户进程将客户端连接成功的Socket注册到Selector中,一个进程发起系统调用selector(),selector负责监听注册进来的所有Socket(IO)中是否存在内核缓冲区中数据准备好的,如果没有,该进程阻塞,直到内核缓冲区中存在准备好的数据,唤醒该进程(或通知注册进程)重新进行轮询读取数据到用户进程空间。以上阻塞的是selector调用进程,不会阻塞负责接收客户端连接的进程。
特点:通过一个监听线程解决多个线程IO阻塞的问题,性能好,节约线程资源。
结:多路复用I/O比较复杂,它整个过程也是阻塞的,但不同的是它可以阻塞多个i/o,同时阻塞多个socket连接,有epoll,,poll,select等,epoll是linux最高效的,多路复用的特点是通过一种机制一个进程能同时等待多个IO文件描述符,内核监视这些文件描述符(套接字描述符),其中的任意一个进入读就绪状态,select, poll,epoll函数就可以返回。select,poll,epoll都是内核状态的函数调用。用户进程发起系统调用后,处于挂起状态,同时监听多个socket连接,只要有其中有一个数据到达内核,用户进程就被唤醒工作,然后将数据从内核读取到用户进程,其实就是由epoll,select同时监听多个io对象,当io对象发生变化的时候,就通知用户进程读写数据,进行操作。即多个io对象复用一个进程,这样可以很充分的利用阻塞的这段时间。