Linux内核的IO模型详解


本篇文章已同步更新至Github仓库JavaSummary,欢迎star!

一、什么是IO

在Linux的世界,一切皆文件。而文件是什么呢,文件就是一串二进制流。不管socket、FIFO、管道还是终端,对我们来说,一切都是流。在信息的交换过程中,都是对这些流进行数据收发操作,简称为I/O操作(input and output)。往流中读取数据,系统调用read,写入数据,系统调用write

通常用户进程的一个完整的IO分为两个阶段:

1.以磁盘IO为例:

fUYKR.jpg

2.以网络IO为例:

fUnjd.jpg

操作系统和驱动程序运行在内核空间,应用程序运行在用户空间,两者不能使用指针传递数据,因为Linux使用的虚拟内存机制,必须通过系统调用请求内核来完成IO动作。

IO有内存IO、网络IO和磁盘IO三种,通常我们说的IO指的是后两者,本文重点讲解网络IO,由于Java应用程序的IO操作依赖操作系统内核才能完成,因此选了Linux内核的IO模型进行详细讲解,目的是为了更好的理解Java的网络IO模型。

二、Linux内核的IO模型

2.1 概念说明

2.1.1 用户空间和内核空间

现在操作系统都是采用虚拟存储器,那么对32位操作系统而言,它的寻址空间(虚拟存储空间)为4G(2的32次方)。操作系统的核心是内核,独立于普通的应用程序,可以访问受保护的内存空间,也有访问底层硬件设备的所有权限。为了保证内核的安全,用户进程不能直接操作内核(kernel),操作系统将虚拟空间划分为两部分,一部分为内核空间,一部分为用户空间。针对Linux操作系统而言,将最高的1G字节(从虚拟地址0xC0000000到0xFFFFFFFF),供内核使用,称为内核空间,而将较低的3G字节(从虚拟地址0x00000000到0xBFFFFFFF),供各个进程使用,称为用户空间

2.1.2 进程切换

为了控制进程的执行,内核必须有能力挂起正在CPU上运行的进程,并恢复以前挂起的某个进程的执行。这种行为被称为进程切换。因此可以说,任何进程都是在操作系统内核的支持下运行的,是与内核紧密相关的。

从一个进程的运行转到另一个进程上运行,这个过程中经过下面这些变化:

  • 保存处理机上下文,包括程序计数器和其他寄存器
  • 更新PCB(进程控制块)信息
  • 把进程的PCB(进程控制块)移入相应的队列,如就绪、在某事件阻塞等队列
  • 选择另一个进程执行,并更新其PCB(进程控制块)
  • 更新内存管理的数据结构
  • 恢复处理机上下文

2.1.3 进程的阻塞

正在执行的进程,由于期待的某些事件未发生,如请求系统资源失败、等待某种操作的完成、新数据尚未到达或无新工作做等,则由系统自动执行阻塞原语(Block),使自己由运行状态变为阻塞状态。可见,进程的阻塞是进程自身的一种主动行为,也因此只有处于运行态的进程(获得CPU),才可能将其转为阻塞状态。当进程进入阻塞状态,是不占用CPU资源的

2.1.4 文件描述符

文件描述符(File descriptor)是计算机科学中的一个术语,是一个用于表述指向文件的引用的抽象化概念。文件描述符在形式上是一个非负整数。实际上,它是一个索引值,指向内核为每一个进程所维护的该进程打开文件的记录表。当程序打开一个现有文件或者创建一个新文件时,内核向进程返回一个文件描述符。在程序设计中,一些涉及底层的程序编写往往会围绕着文件描述符展开。但是文件描述符这一概念往往只适用于UNIX、Linux这样的操作系统。

2.1.5 缓存 IO

缓存 IO 又被称作标准 IO,大多数文件系统的默认 IO 操作都是缓存 IO。在 Linux 的缓存 IO 机制中,操作系统会将 IO 的数据缓存在文件系统的页缓存( page cache )中,也就是说,数据会先被拷贝到操作系统内核的缓冲区中,然后才会从操作系统内核的缓冲区拷贝到应用程序的地址空间。

缓存 IO 的缺点:数据在传输过程中需要在应用程序地址空间和内核空间进行多次数据拷贝操作,这些数据拷贝操作所带来的 CPU 以及内存开销是非常大的。

2.2 同步阻塞

用户空间的应用程序执行一个系统调用(recvform),这会导致应用程序阻塞,什么也不干,直到数据准备好,并且将数据从内核复制到用户进程,最后进程再处理数据,在等待数据到处理数据的两个阶段,整个进程都被阻塞,不能处理别的网络IO。调用应用程序处于一种不再消费 CPU 而只是简单等待响应的状态,因此从处理的角度来看,这是非常有效的。

这也是最简单的IO模型,在通常fd较少、就绪很快的情况下使用是没有问题的。

fUIvW.jpg

2.3 同步非阻塞

非阻塞的recvfrom系统调用之后,进程并没有被阻塞,内核马上返回给进程,如果数据还没准备好,此时会返回一个error。进程在返回之后,可以干点别的事情,然后再发起recvform系统调用。重复上面的过程,循环往复的进行recvfrom系统调用。这个过程通常被称之为轮询。轮询检查内核数据,直到数据准备好,再拷贝数据到进程,进行数据处理。需要注意,拷贝数据整个过程,进程仍然是属于阻塞的状态

这种方式在编程中对socket设置O_NONBLOCK即可。但此方式仅仅针对网络IO有效,对磁盘IO并没有作用。因为本地文件IO就没有被认为是阻塞,我们所说的网络IO的阻塞是因为网路IO有无限阻塞的可能,而本地文件除非是被锁住,否则是不可能无限阻塞的,因此只有锁这种情况下,O_NONBLOCK才会有作用。而且,磁盘IO时要么数据在内核缓冲区中直接可以返回,要么需要调用物理设备去读取,这时候进程的其他工作都需要等待。因此,后续的IO复用和信号驱动IO对文件IO也是没有意义的。

fU7il.jpg

2.4 IO复用

IO复用,也叫多路IO就绪通知。这是一种进程预先告知内核的能力,让内核发现进程指定的一个或多个IO条件就绪了,就通知进程。使得一个进程能在一连串的事件上等待

IO复用的实现方式目前主要有selectpollepoll

selectpoll的原理基本相同:

  • 注册待侦听的fd(文件描述符),这里的fd(文件描述符)创建时最好使用非阻塞
  • 每次调用都去检查这些fd(文件描述符)的状态,当有一个或者多个fd(文件描述符)就绪的时候返回
  • 返回结果中包括已就绪和未就绪的fd

相比select,poll解决了单个进程能够打开的文件描述符数量有限制这个问题:select受限于FD_SIZE的限制,如果修改则需要修改这个宏重新编译内核;而poll通过一个pollfd数组向内核传递需要关注的事件,避开了文件描述符数量限制。

此外,select和poll共同具有的一个很大的缺点就是包含大量fd的数组被整体复制于用户态和内核态地址空间之间,开销会随着fd(文件描述符)数量增多而线性增大。

epoll的出现,解决了select、poll的缺点:

  • 基于事件驱动的方式,避免了每次都要把所有fd(文件描述符)都扫描一遍
  • epoll_wait只返回就绪的fd(文件描述符)
  • epoll使用nmap内存映射技术避免了内存复制的开销
  • epoll的fd(文件描述符)数量上限是操作系统的最大文件句柄数目,这个数目一般和内存有关,通常远大于1024

目前,epoll是Linux2.6下最高效的IO复用方式,也是Nginx、Node的IO实现方式。而在freeBSD下,kqueue是另一种类似于epoll的IO复用方式。

此外,对于IO复用还有一个水平触发和边缘触发的概念:

  • 水平触发:当就绪的fd(文件描述符)未被用户进程处理,下一次查询依旧会返回,这是selectpoll的触发方式
  • 边缘触发:无论就绪的fd(文件描述符)是否被处理,下一次不再返回。理论上性能更高,但是实现相当复杂,并且任何意外的丢失事件都会造成请求处理错误。epoll默认使用水平触发,通过相应选项可以使用边缘触发
fUOF7.jpg

2.5 信号驱动

首先我们允许Socket进行信号驱动IO,并安装一个信号处理函数,进程继续运行并不阻塞。当数据准备好时,进程会收到一个SIGIO信号,可以在信号处理函数中调用I/O操作函数处理数据。流程如下:

  • 开启套接字信号驱动IO功能
  • 系统调用sigaction执行信号处理函数(非阻塞,立刻返回)
  • 数据就绪,生成sigio信号,通过信号回调通知应用来读取数据

此种io方式存在的一个很大的问题:Linux中信号队列是有限制的,如果超过这个数字就无法读取数据

fU8y6.jpg

2.6 异步非阻塞

异步io流程如下所示:

  • 当用户线程调用了aio_read系统调用,立刻就可以开始去做其它的事,用户线程不阻塞
  • 内核(kernel)就开始了IO的第一个阶段:准备数据。当kernel一直等到数据准备好了,它就会将数据从kernel内核缓冲区,拷贝到用户缓冲区(用户内存)
  • kernel会给用户线程发送一个信号(signal),或者回调用户线程注册的回调接口,告诉用户线程read操作完成了
  • 用户线程读取用户缓冲区的数据,完成后续的业务操作

相对于同步IO,异步IO不是顺序执行。用户进程进行aio_read系统调用之后,无论内核数据是否准备好,都会直接返回给用户进程,然后用户态进程可以去做别的事情。等到数据准备好了,内核直接复制数据给进程,然后从内核向进程发送通知。IO两个阶段,进程都是非阻塞的。

对比信号驱动IO,异步IO的主要区别在于:信号驱动由内核告诉我们何时可以开始一个IO操作(数据在内核缓冲区中),而异步IO则由内核通知IO操作何时已经完成(数据已经在用户空间中)。

异步IO又叫做事件驱动IO,在Unix中,POSIX1003.1标准为异步方式访问文件定义了一套库函数,定义了AIO的一系列接口。使用aio_read或者aio_write发起异步IO操作,使用aio_error检查正在运行的IO操作的状态。但是其实现没有通过内核而是使用了多线程阻塞。此外,还有Linux自己实现的Native AIO,依赖两个函数:io_submitio_getevents,虽然io是非阻塞的,但仍需要主动去获取读写的状态。

目前Linux中AIO的内核实现只对文件IO有效,如果要实现真正的AIO,需要用户自己来实现。目前有很多开源的异步IO库,例如libevent、libev、libuv。

fUAR8.jpg

Reference

  • 1
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

xylitolz

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值