基础概念

正式开始之前,需要铺垫一些基本概念,以免接下来看到一脸懵逼。

我们都知道,在操作系统中,CPU负责执行指令,这些指令有些来自应用程序,有些是底层系统的自调用。有些指令是非常危险的,如清除内存,网络连接等等,如果错误调用的话有可能导致系统崩溃。因而CPU将指令分为特权指令和非特权指令,对于某些特定的指令,只需要操作系统及其相关模块进行调用。因而,根据这个特点,操作系统内部也划分出了内核态用户态

内核态

内核态拥有完全的底层资源控制权限,可以执行任何的CPU指令,访问任何内存地址,其占有的处理机是不允许被抢占的。

用户态

用户程序是运行在操作系统之上,这些程序运行时称之为用户态,用户态下不能直接访问底层硬件和内存地址,只能通过委托系统调用的方式来访问底层硬件和内存。

用户态到内核态如何切换

从用户态切换到内核态有三种方式:

  • 系统调用:这是用户态主动要求切换到内核态的一种方式。用户进程通过系统调用申请使用操作系统提供的某些服务以便完成工作,比如,调用fork()指令实际上就是执行了一个创建新进程的系统调用。系统调用的机制其核心在于**使用了操作系统为用户特别开放的一个中断来实现的,例如Linuxint 80h中断;
  •  
  • 外设中断:当外围设备完成用户请求的操作后,会向CPU发出相应的中断信号。这时CPU会暂停执行下一条即将要执行的指令转而去执行与中断信号对应的处理程序。如果先前执行的是用户态下的指令,那么这个切换过程就是用户态转为内核态。比如硬盘读写操作完成,系统会切换到硬盘读写的中断处理程序中执行后续操作;
  • 异常:当CPU在执行运行处于用户态的程序时,发生了一些不可知的异常,这个时候就会触发由当前运行进行切换到处理此异常的内核相关程序中,也就是转到了内核态,比如缺页异常;

这三种是用户态切换到内核态的主要方式,系统调用是主动的,后面两种是被动的。

Linux的整体架构图如下所示:

java channel io 内核态 用户态 java 用户态与内核态_内核态

同步/异步

同步/异步关注的是消息通信机制。

同步:所谓同步,就是在发出一个功能调用时,在没有得到结果之前,该调用就不返回。等前一件做完了才能做下一件事。

异步:异步的概念和同步相对。当一个异步过程调用发出后,调用者若不能立刻得到结果,此时可以直接返回然后执行其他任务,等到获得了结果之后通过状态、通知或者回调等手段通知调用者。

同步、异步一般发生在不同的线程/进程之间,如Thread1Thread2是同步执行还是异步执行的。

阻塞和非阻塞

阻塞和非阻塞关注的是程序在等待调用结果时的状态。

阻塞: 阻塞调用是指调用返回之前,当前线程会被挂起,只有当调用得到结果后才返回。

非阻塞:与阻塞相反,非阻塞调用是指在不能立即得到结果之前,该函数不会将当前线程阻塞,而是立即返回。

五种 IO 模型

IO一般分为磁盘IO网络IO,这里我们主要关注网络IO。一次完整的网络IO过程如下所示:

java channel io 内核态 用户态 java 用户态与内核态_用户态_02

从上图可以看出,数据无论从网卡到用户空间还是从用户空间到网卡都需要经过内核。

阻塞IO模型

当应用程序调用一个 IO 函数,其底层会委托操作系统的recvfrom()去完成,当数据还没有准备好时,revfrom会一直阻塞,等待数据准备好。当数据准备好后,从内核拷贝到用户空间,recvfrom 返回成功,IO函数调用完成。过程如下所示:

java channel io 内核态 用户态 java 用户态与内核态_用户态_03

阻塞IO模型的优点是编程简单,但缺点是需要配合大量线程使用。应用进程没接收一个连接,就需要为此连接创建一个线程来处理该连接上的读写任务。

非阻塞IO模型

调用进程在等待数据的过程中不会被阻塞,而是会不断地轮询查看数据有没有准备好。当数据准备好后,将数据从内核空间拷贝到用户空间,完成IO函数的调用。等待数据的过程是非阻塞的,但数据拷贝时仍是阻塞的。过程如下所示:

java channel io 内核态 用户态 java 用户态与内核态_数据_04

非阻塞io的优点在于可以实现使用一个线程同时处理多个连接的需求,减少线程的大量使用。缺点在于要不断地去轮询检查数据是否准备好,比较耗费CPU

IO复用模型

为了解决非阻塞IO不断轮询导致CPU占用升高的问题,出现了IO复用模型。IO复用中,使用其他线程帮助去检查多个线程数据的完成情况,提高效率。

Linux中提供了selectpollepoll三种方式来实现IO复用。一个线程可以对多个IO端口进行监听,当有读写事件产生时会分发到具体的线程进行处理。过程如下所示:

java channel io 内核态 用户态 java 用户态与内核态_内核_05

IO复用只需要阻塞在selectpoll或者epoll,可以同时处理和管理多个连接。缺点是当selectpoll或者epoll 管理的连接数过少时,这种模型将退化成阻塞IO 模型。并且还多了一次系统调用:一次selectpoll或者epoll 一次recvfrom

信号驱动IO模型

应用程序可以创建一个信号驱动程序SIGIO,当数据没有处理好时,应用程序继续运行,不会被阻塞。当数据准备好之后,操作系统向应用程序发送信号,之后信号驱动程序就会执行,在信号处理函数中调用 IO函数处理数据。过程如下所示:

java channel io 内核态 用户态 java 用户态与内核态_数据_06

信号驱动IO模型的优点在于非阻塞,缺点在于串行处理信号驱动程序,当前一个SIGIO没有被处理的情况下,后一个信号也不能被处理。在信号量大的时候会导致后面的信号不能被及时感知。

异步IO模型

相比于同步IO,异步IO不是顺序执行的。应用进程在执行aio_read系统调用之后,无论数据是否准备好,都会直接返回给用户进程,然后应用进程可以去做别的事情。当数据准备好之后,内核直接复制数据给用户进程,然后内核向进程发送通知。过程如下:

信号驱动IO模型中内核通知应用进程数据何时准备好,而在异步IO模型中内核将数据复制完成之后告知应用进程IO操作已完成。

在异步IO模型中,应用进程调用aio_read以及数据被拷贝到用户空间这两个过程都是非阻塞的。

总结

IO模型公有五种,前四种模型区别在于第一部分,即系统调用,但是第二部分都是一样的,即将数据从内核空间拷贝到用户空间这个过程,进程阻塞于redvfrom的调用。而最后一种,异步IO模型,在系统调用和数据拷贝过程都是非阻塞的。

java channel io 内核态 用户态 java 用户态与内核态_数据_07

参考

Redis的IO多路复用机制

IO模型简介

理解Linux用户态和内核态

线程,进程,协程, 并发,并行,同步,异步概念解析

服务器网络编程之 IO 模型

图解Java IO模型(一)