剖析IO原理和零拷贝机制

目录

  • [1 Linux的五种IO模型](#1 Linux的五种IO模型)
    • [1.1 模型调用的函数](#1.1 模型调用的函数)
      • [1.1.1 recv函数](#1.1.1 recv函数)
      • [1.1.2 select函数](#1.1.2 select函数)
      • [1.1.3 poll函数](#1.1.3 poll函数)
      • [1.1.4 epoll函数](#1.1.4 epoll函数)
      • [1.1.5 sigaction函数](#1.1.5 sigaction函数)
    • [1.2 IO模型](#1.2 IO模型)
      • [1.2.1 阻塞IO模型](#1.2.1 阻塞IO模型)
      • [1.2.2 非阻塞IO模型](#1.2.2 非阻塞IO模型)
      • [1.2.3 IO复用模型](#1.2.3 IO复用模型)
      • [1.2.4 信号驱动IO模型](#1.2.4 信号驱动IO模型)
      • [1.2.5 异步IO模型](#1.2.5 异步IO模型)
      • [1.2.6 IO模型比较](#1.2.6 IO模型比较)
  • [2 Java的BIO、NIO、AIO](#2 Java的BIO、NIO、AIO)
    • [2.1 BIO(Blocking IO,同步阻塞式IO模型)](#2.1 BIO(Blocking IO,同步阻塞式IO模型))
    • [2.2 NIO(Non-blocking IO,同步非阻塞式IO模型)](#2.2 NIO(Non-blocking IO,同步非阻塞式IO模型))
    • [2.3 AIO(Asynchronous IO,异步非阻塞式IO模型)](#2.3 AIO(Asynchronous IO,异步非阻塞式IO模型))

1 Linux的五种IO模型

五种IO模型包括:阻塞IO、非阻塞IO、IO多路复用、信号驱动IO、异步IO

1.1 模型调用的函数

使用到的部分函数

1.1.1 recv函数

recv函数用于从套接字(socket)接收数据。在TCP/IP协议栈中,当数据从网络到达时,它首先被放入内核的接收缓冲区中。然后,应用程序可以通过调用recv函数将这些数据从内核缓冲区复制到用户空间的缓冲区中,以便进一步处理。

参数和返回值

  1. 参数:
    套接字文件描述符 :指定要接收数据的套接字。
    缓冲区指针 :指向用户空间缓冲区的指针,用于存放接收到的数据。
    缓冲区大小 :指定用户空间缓冲区的大小(以字节为单位)。
    标志位:用于设置接收数据的特定方式(如是否阻塞、是否立即返回等)。
  2. 返回值:
    成功时 :返回实际接收到的字节数。
    失败时:返回-1,并设置全局变量errno以指示错误类型。

1.1.2 select函数

在IO模型中,select函数是一个非常重要的系统调用,它允许一个程序同时监视多个文件描述符(通常是套接字描述符),以查看它们中的任何一个是否可以进行I/O操作(例如读、写或异常条件)。select函数在多种编程场景中都非常有用,特别是在需要处理多个并发连接的网络服务器中。

select函数通过三个文件描述符集(读集、写集和异常集)来监视多个文件描述符。调用select时,程序指定这三个集合,select函数将阻塞(除非设置了非阻塞标志),直到以下情况之一发生:

  • 指定的读集合中的一个或多个文件描述符变为可读(例如,有数据可以读取)。
  • 指定的写集合中的一个或多个文件描述符变为可写。
  • 指定的异常集合中的一个或多个文件描述符发生异常条件。

当select返回时,它将更新这三个集合,以反映哪些文件描述符已准备好进行I/O操作。

select系统调用允许程序同时在多个底层文件描述符上,等待输入的到达或输出的完成。以数组形式存储文件描述符,64位机器默认2048个。当有数据准备好时,无法感知具体是哪个流OK了,所以需要一个一个的遍历,函数的时间复杂度为O(n)。

参数和返回值

  1. 参数:
    nfds :要监视的文件描述符集合中的最大文件描述符加1。
    readfds :指向文件描述符集合的指针,这些文件描述符被监视以查看它们是否可读。
    writefds :指向文件描述符集合的指针,这些文件描述符被监视以查看它们是否可写。
    exceptfds :指向文件描述符集合的指针,这些文件描述符被监视以查看是否发生异常条件。
    timeout:指定select函数等待I/O操作发生的最长时间。如果为NULL,则select将无限期地等待。
  2. 返回值
    返回值是准备好的文件描述符的总数。如果返回-1,则表示发生了错误。

1.1.3 poll函数

poll函数通过轮询的方式检测输入源(文件描述符)是否有数据到达或是否准备好进行I/O操作。它会遍历一个由pollfd结构体组成的数组,每个结构体包含了一个文件描述符、要监视的事件类型以及实际发生的事件类型。当调用poll函数时,它会阻塞(除非设置了非阻塞标志),直到指定的文件描述符之一准备好进行I/O操作或超时。

poll函数使用pollfd结构体来指定要监视的文件描述符和事件类型。pollfd结构体的定义通常如下:

java 复制代码
struct pollfd {
    int fd;         // 文件描述符
    short events;   // 要监视的事件类型(如读、写、异常等)
    short revents;  // 实际发生的事件类型,由内核设置
};

以链表形式存储文件描述符,没有长度限制。本质与select相同,函数的时间复杂度也为O(n)。

参数和返回值

  1. 函数
bash 复制代码
int poll(struct pollfd *fds, unsigned long nfds, int timeout);
  1. 参数
    fds :指向pollfd结构体数组的指针。
    nfds :数组中pollfd结构体的数量。
    timeout:指定poll函数等待I/O操作发生的最长时间(以毫秒为单位)。如果为-1,则poll将无限期地等待;如果为0,则poll将立即返回,不阻塞。

  2. 返回值
    返回-1 :表示发生错误。
    返回0 :表示在指定的时间内没有任何事件发生。
    返回正数:表示有事件发生,返回值是准备好进行I/O操作的文件描述符的数量。

1.1.4 epoll函数

epoll的工作原理主要基于Linux内核中的高效数据结构和事件驱动机制。事件驱动的,即如果某个流准备好了,会以事件通知,知道具体是哪个流,因此不需要遍历,函数的时间复杂度为O(1)。

数据结构:

  1. 红黑树:epoll在内核中使用红黑树来管理所有注册的文件描述符(通常是socket)。红黑树是一种平衡二叉搜索树,它的查找、插入和删除操作的时间复杂度都是O(log n),这使得epoll能够高效地管理大量的文件描述符。
  2. 就绪列表:epoll还维护了一个就绪列表,用于存储那些已经就绪、有事件发生的文件描述符。就绪列表通常使用双向链表来实现,因为双向链表支持快速的插入和删除操作。当有事件发生时,内核会将相应的文件描述符从红黑树中取出,并加入到就绪列表中。

工作原理:

  1. 创建epoll实例:
    使用epoll_create函数创建一个epoll实例,该函数返回一个文件描述符,用于后续操作。
  2. 注册事件:
    使用epoll_ctl函数将感兴趣的文件描述符(通常是socket)及其事件(如读就绪、写就绪等)注册到epoll实例中。这一步可以添加新的事件、修改已注册的事件或删除事件。
  3. 等待事件:
    使用epoll_wait或epoll_pwait函数等待事件的发生。这些函数会阻塞调用线程,直到有注册的事件发生或超时。当事件发生时,函数会返回发生事件的文件描述符数量,并将事件信息存储在提供的epoll_event结构体数组中。

1.1.5 sigaction函数

在I/O模型中,特别是在信号驱动I/O模型中,sigaction函数扮演着重要的角色。信号驱动I/O模型是Unix/Linux系统中一种处理I/O操作的方式,它使用信号来通知应用程序数据已经准备好可以进行处理。在这个过程中,sigaction函数被用来设置信号处理函数,以便在接收到特定信号(如SIGIO)时执行相应的操作。

sigaction在信号驱动I/O模型中的作用

  1. 设置信号处理函数:
    在信号驱动I/O模型中,应用程序首先需要通过sigaction函数设置一个信号处理函数。这个处理函数将在接收到SIGIO信号时被调用。SIGIO信号通常用于通知应用程序某个文件描述符(如套接字)上有数据到达或可以进行I/O操作。
  2. 配置文件描述符:
    除了设置信号处理函数外,应用程序还需要将相关的文件描述符(如套接字)配置为非阻塞模式,并使其能够接收SIGIO信号。这通常通过fcntl函数来实现,设置文件描述符的O_ASYNC和O_NONBLOCK标志。
  3. 接收并处理信号:
    当数据到达配置为信号驱动I/O模式的文件描述符时,内核会发送SIGIO信号给应用程序。应用程序的信号处理函数随后被调用,可以在该函数中执行读取数据或其他相关操作。

1.2 IO模型

1.2.1 阻塞IO模型

进程发起IO系统调用后,进程被阻塞,转到内核空间处理,整个IO处理完毕后返回进程。操作成功则进程获取到数据。

1.2.2 非阻塞IO模型

进程发起IO系统调用后,如果内核缓冲区没有数据,需要到IO设备中读取,进程返回一个错误而不会被阻塞;进程发起IO系统调用后,如果内核缓冲区有数据,内核就会把数据返回进程。

对于上面的阻塞IO模型来说,内核数据没准备好需要进程阻塞的时候,就返回一个错误,以使得进程不被阻塞。

1.2.3 IO复用模型

多个的进程的IO可以注册到一个复用器(select)上,然后用一个进程调用该select, select会监听所有注册进来的IO;

如果select没有监听的IO在内核缓冲区都没有可读数据,select调用进程会被阻塞;而当任一IO在内核缓冲区中有可数据时,select调用就会返回;

相比于阻塞IO模型,多路复用只是多了一个select/poll/epoll函数。select函数会不断地轮询自己所负责的文件描述符/套接字的到达状态,当某个套接字就绪时,就对这个套接字进行处理。select负责轮询等待,recvfrom负责拷贝。当用户进程调用该select,select会监听所有注册好的IO,如果所有IO都没注册好,调用进程就阻塞。

1.2.4 信号驱动IO模型

模型也分为两个阶段:
数据准备阶段 :未阻塞,当数据准备完成之后,会主动的通知用户进程数据已经准备完成,对用户进程做一个回调。
数据拷贝阶段:阻塞用户进程,等待数据拷贝。

1.2.5 异步IO模型

当进程发起一个IO操作,进程返回(不阻塞),但也不能返回果结;内核把整个IO处理完后,会通知进程结果。如果IO操作成功则进程直接获取到数据。

1.2.6 IO模型比较

2 Java的BIO、NIO、AIO

2.1 BIO(Blocking IO,同步阻塞式IO模型)

工作机制 :在BIO模型中,每个客户端连接都会在一个独立的线程中处理。这个线程在处理IO操作时会阻塞,直到操作完成。因此,每个连接都需要一个独立的线程。当连接数较多时,会消耗大量的内存和CPU资源。
应用场景:适用于连接数少的场景。在这种情况下,程序编写相对简单,但对服务器的资源要求较高。在JDK 1.4之前,BIO是Java中唯一的IO模型选择。

2.2 NIO(Non-blocking IO,同步非阻塞式IO模型)

核心组件 :NIO包含三大核心组件,即通道(Channel)、缓冲区(Buffer)和选择器(Selector)。
工作机制 :在NIO模型中,一个线程可以处理多个连接。客户端连接请求会注册到多路复用器(Selector)上。多路复用器检测到某个连接有IO事件(如读、写、连接等)时,就会处理该事件。这种非阻塞模式使得主线程在未发生数据读写事件时无需阻塞,可以继续执行其他任务,从而增强了服务器的并发处理能力。

应用场景:适用于连接数多的场景,如聊天服务器、服务器间通讯等。然而,由于NIO的编程模型相对复杂,因此程序编写难度较高。NIO从JDK 1.4版本开始被支持。

2.3 AIO(Asynchronous IO,异步非阻塞式IO模型)

核心组件 :AIO引入了异步通道的概念,并使用Future或CompletionHandler来处理异步操作的结果。

工作机制:在AIO模型中,读写异步通道会立刻返回,而不需要等待IO操作完成。读写的数据由Future或CompletionHandler进一步处理。当操作系统完成IO操作后,会主动通知应用程序,或者调用应用程序注册的回调函数来处理结果。
应用场景:也适用于连接数多的场景,但更加偏向于异步操作多的场景。AIO作为NIO的改进和增强,随JDK 1.7版本更新被集成在JDK的nio包中,因此也被称为NIO 2.0。

细化文章推荐:Java的BIO、NIO、AIO

相关推荐
李少兄37 分钟前
Unirest:优雅的Java HTTP客户端库
java·开发语言·http
宁zz38 分钟前
乌班图安装jenkins
运维·jenkins
此木|西贝43 分钟前
【设计模式】原型模式
java·设计模式·原型模式
可乐加.糖1 小时前
一篇关于Netty相关的梳理总结
java·后端·网络协议·netty·信息与通信
s9123601011 小时前
rust 同时处理多个异步任务
java·数据库·rust
9号达人1 小时前
java9新特性详解与实践
java·后端·面试
大丈夫立于天地间1 小时前
ISIS协议中的数据库同步
运维·网络·信息与通信
cg50171 小时前
Spring Boot 的配置文件
java·linux·spring boot
Dream Algorithm1 小时前
路由器的 WAN(广域网)口 和 LAN(局域网)口
网络·智能路由器
啊喜拔牙1 小时前
1. hadoop 集群的常用命令
java·大数据·开发语言·python·scala