写点什么

一文弄懂 Linux 下五种 IO 模型

  • 2022 年 3 月 25 日
  • 本文字数:3600 字

    阅读完需:约 12 分钟

Linux 下主要的 IO 主要分为:阻塞 IO(Blocking IO),非阻塞 IO(Non-blocking IO),同步 IO(Sync IO)和异步 IO(Async IO)。 同步:调用端会一直等待服务端响应,直到返回结果。 异步:调用端发起调用之后不会立刻返回,不会等待服务端响应。服务端通过通知机制或者回调函数来通知客户端。 阻塞:服务端返回结果之前,客户端线程会被挂起,此时线程不可被 CPU 调度,线程暂停运行。 非阻塞:在服务端返回前,函数不会阻塞调用端线程,而会立刻返回。

同步异步的区别在于:服务端在拷贝数据时是否阻塞调用端线程;阻塞和非阻塞的区别在于:调用端线程在调用 function 后是否立刻返回。要理解这些 I/O,需要先理解一些基本的概念。

用户态和核心态

Linux 系统中分为核心态(Kernel model)和用户态(User model),CPU 会在两个 model 之间切换。

  1. 核心态代码拥有完全的底层资源控制权限,可以执行任何 CPU 指令,访问任何内存地址,其占有的处理机是不允许被抢占的。内核态的指令包括:启动 I/O,内存清零,修改程序状态字,设置时钟,允许/终止中断和停机。内核态的程序崩溃会导致 PC 停机。

  2. 用户态是用户程序能够使用的指令,不能直接访问底层硬件和内存地址。用户态运行的程序必须委托系统调用来访问硬件和内存。用户态的指令包括:控制转移,算数运算,取数指令,访管指令(使用户程序从用户态陷入内核态)。

用户态和核心态的切换

用户态切换到核心态有三种方式: a.系统调用

这是用户态进程主动要求切换到内核态的一种方式,用户态进程通过系统调用申请使用操作系统提供的服务程序完成工作,比如前例中 fork()实际上就是执行了一个创建新进程的系统调用。而系统调用的机制其核心还是使用了操作系统为用户特别开放的一个中断来实现,例如 Linux 的 int 80h 中断。 b.异常

当 CPU 在执行运行在用户态下的程序时,发生了某些事先不可知的异常,这时会触发由当前运行进程切换到处理此异常的内核相关程序中,也就转到了内核态,比如缺页异常。 c.外围设备的中断当外围设备完成用户请求的操作后,会向 CPU 发出相应的中断信号,这时 CPU 会暂停执行下一条即将要执行的指令转而去执行与中断信号对应的处理程序,如果先前执行的指令是用户态下的程序,那么这个转换的过程自然也就发生了由用户态到内核态的切换。比如硬盘读写操作完成,系统会切换到硬盘读写的中断处理程序中执行后续操作等。

进程切换

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

  1. 保存处理机上下文,包括程序计数器和其他寄存器。

  2. 更新 PCB 信息。

  3. 把进程的 PCB 移入相应的队列,如就绪、在某事件阻塞等队列。

  4. 选择另一个进程执行,并更新其 PCB。

  5. 更新内存管理的数据结构。

  6. 恢复处理机上下文。

进程阻塞

正在执行的进程由于一些事情发生,如请求资源失败、等待某种操作完成、新数据尚未达到或者没有新工作做等,由系统自动执行阻塞原语,使进程状态变为阻塞状态。因此,进程阻塞是进程自身的一种主动行为,只有处于运行中的进程才可以将自身转化为阻塞状态。当进程被阻塞,它是不占用 CPU 资源的。

文件描述符(fd, File Descriptor)

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

缓存 I/O

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

缓存 IO 的缺点:

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

Linux 下的五种 I/O 模型

Linux 下主要有以下五种 I/O 模型:

  1. 阻塞 I/O(blocking IO)

  2. 非阻塞 I/O (nonblocking I/O)

  3. I/O 复用 (I/O multiplexing)

  4. 信号驱动 I/O (signal driven I/O (SIGIO))

  5. 异步 I/O (asynchronous I/O)

C/C++Linux 后端开发高级架构师学习视频 点击 视频学习资料 获取,内容知识点包括 Linux,Nginx,ZeroMQ,MySQL,Redis,线程池,MongoDB,ZK,Linux 内核,CDN,P2P,epoll,Docker,TCP/IP,协程,DPDK 等等。免费学习地址:C/C++Linux服务器开发高级架构师/Linux后台架构师​

​阻塞 IO 模型

进程会一直阻塞,直到数据拷贝完成 应用程序调用一个 IO 函数,导致应用程序阻塞,等待数据准备好。数据准备好后,从内核拷贝到用户空间,IO 函数返回成功指示。阻塞 IO 模型图如下所示:

​非阻塞 IO 模型

通过进程反复调用 IO 函数,在数据拷贝过程中,进程是阻塞的。模型图如下所示:

IO 复用模型

主要是 select 和 epoll。一个线程可以对多个 IO 端口进行监听,当 socket 有读写事件时分发到具体的线程进行处理。模型如下所示:

信号驱动 IO 模型

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

异步 IO 模型

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

​五种 IO 模型比较

阻塞 IO 和非阻塞 IO 的区别调用阻塞 IO 后进程会一直等待对应的进程完成,而非阻塞 IO 不会等待对应的进程完成,在 kernel 还在准备数据的情况下直接返回。 同步 IO 和异步 IO 的区别首先看一下 POSIX 中对这两个 IO 的定义:

A synchronous I/O operation causes the requesting process to be blocked until that I/O operation completes;An asynchronous I/O operation does not cause the requesting process to be blocked;
复制代码

**两者的区别就在于 synchronous IO 做”IO operation”的时候会将 process 阻塞。**按照这个定义,之前所述的 blocking IO,non-blocking IO,IO multiplexing 都属于 synchronous IO。注意到 non-blocking IO 会一直轮询(polling),这个过程是没有阻塞的,但是 recvfrom 阶段 blocking IO,non-blocking IO 和 IO multiplexing 都是阻塞的。 而 asynchronous IO 则不一样,当进程发起 IO 操作之后,就直接返回再也不理睬了,直到 kernel 发送一个信号,告诉进程说 IO 完成。在这整个过程中,进程完全没有被 block。

IO 复用之 select、poll、epoll 简介

epoll 是 linux 所特有,而 select 是 POSIX 所规定,一般操作系统均有实现。

select

select 本质是通过设置或检查存放 fd 标志位的数据结构来进行下一步处理。缺点是:

  1. 单个进程可监视的 fd 数量被限制,即能监听端口的大小有限。一般来说和系统内存有关,具体数目可以 cat /proc/sys/fs/file-max 察看。32 位默认是 1024 个,64 位默认为 2048 个

  2. 对 socket 进行扫描时是线性扫描,即采用轮询方法,效率低。当套接字比较多的时候,每次 select()都要遍历 FD_SETSIZE 个 socket 来完成调度,不管 socket 是否活跃都遍历一遍。会浪费很多 CPU 时间。如果能给套接字注册某个回调函数,当他们活跃时,自动完成相关操作,就避免了轮询,这正是 epoll 与 kqueue 做的

  3. 需要维护一个用来存放大量 fd 的数据结构,会使得用户空间和内核空间在传递该结构时复制开销大

poll

poll 本质和 select 相同,将用户传入的数据拷贝到内核空间,然后查询每个 fd 对应的设备状态,如果设备就绪则在设备等待队列中加入一项并继续遍历,如果遍历所有 fd 后没有发现就绪设备,则挂起当前进程,直到设备就绪或主动超时,被唤醒后又要再次遍历 fd。它没有最大连接数的限制,原因是它是基于链表来存储的,但缺点是:

  1. 大量的 fd 的数组被整体复制到用户态和内核空间之间,不管有无意义。

  2. poll 还有一个特点“水平触发”,如果报告了 fd 后,没有被处理,那么下次 poll 时再次报告该 ffd。

epoll

epoll 支持水平触发和边缘触发,最大特点在于边缘触发,只告诉哪些 fd 刚刚变为就绪态,并且只通知一次。还有一特点是,epoll 使用“事件”的就绪通知方式,通过 epoll_ctl 注册 fd,一量该 fd 就绪,内核就会采用类似 callback 的回调机制来激活该 fd,epoll_wait 便可以收到通知。epoll 的优点:

  1. 没有最大并发连接的限制。

  2. 效率提升,只有活跃可用的 FD 才会调用 callback 函数。

  3. 内存拷贝,利用 mmap()文件映射内存加速与内核空间的消息传递。

select、poll、epoll 区别总结:

参考资料:

《Unix 网络编程》

用户头像

Linux服务器开发qun720209036,欢迎来交流 2020.11.26 加入

专注C/C++ Linux后台服务器开发。

评论

发布
暂无评论
一文弄懂Linux下五种IO模型_epoll_Linux服务器开发_InfoQ写作平台