Mr Dk.'s BlogMr Dk.'s Blog
  • 🦆 About Me
  • ⛏️ Technology Stack
  • 🔗 Links
  • 🗒️ About Blog
  • Algorithm
  • C++
  • Compiler
  • Cryptography
  • DevOps
  • Docker
  • Git
  • Java
  • Linux
  • MS Office
  • MySQL
  • Network
  • Operating System
  • Performance
  • PostgreSQL
  • Productivity
  • Solidity
  • Vue.js
  • Web
  • Wireless
  • 🐧 How Linux Works (notes)
  • 🐧 Linux Kernel Comments (notes)
  • 🐧 Linux Kernel Development (notes)
  • 🐤 μc/OS-II Source Code (notes)
  • ☕ Understanding the JVM (notes)
  • ⛸️ Redis Implementation (notes)
  • 🗜️ Understanding Nginx (notes)
  • ⚙️ Netty in Action (notes)
  • ☁️ Spring Microservices (notes)
  • ⚒️ The Annotated STL Sources (notes)
  • ☕ Java Development Kit 8
GitHub
  • 🦆 About Me
  • ⛏️ Technology Stack
  • 🔗 Links
  • 🗒️ About Blog
  • Algorithm
  • C++
  • Compiler
  • Cryptography
  • DevOps
  • Docker
  • Git
  • Java
  • Linux
  • MS Office
  • MySQL
  • Network
  • Operating System
  • Performance
  • PostgreSQL
  • Productivity
  • Solidity
  • Vue.js
  • Web
  • Wireless
  • 🐧 How Linux Works (notes)
  • 🐧 Linux Kernel Comments (notes)
  • 🐧 Linux Kernel Development (notes)
  • 🐤 μc/OS-II Source Code (notes)
  • ☕ Understanding the JVM (notes)
  • ⛸️ Redis Implementation (notes)
  • 🗜️ Understanding Nginx (notes)
  • ⚙️ Netty in Action (notes)
  • ☁️ Spring Microservices (notes)
  • ⚒️ The Annotated STL Sources (notes)
  • ☕ Java Development Kit 8
GitHub
  • 📝 Notes
    • Algorithm
      • Algorithm - Bloom Filter
      • Algorithm - Disjoint Set
      • Algorithm - Fast Power
      • Algorithm - KMP
      • Algorithm - Monotonic Stack
      • Algorithm - RB-Tree
      • Algorithm - Regular Expression
      • Algorithm - Sliding Window
      • Online Judge - I/O
    • C++
      • C++ - Const
      • C++ File I/O
      • C++ - Object Layout
      • C++ - Operator Overload
      • C++ - Polymorphism
      • C++ STL algorithm
      • C++ STL map
      • C++ STL multimap
      • C++ STL priority_queue
      • C++ STL set
      • C++ STL string
      • C++ STL unordered_map
      • C++ STL vector
      • C++ - Smart Pointer
      • C++ - Template & Genericity
    • Compiler
      • ANTLR - Basic
      • Compiler - LLVM Architecture
      • Compiler - Multi-version GCC
    • Cryptography
      • Cryptography - Certbot
      • Cryptography - Digital Signature & PKCS #7
      • Cryptography - GPG
      • Cryptography - JWT
      • Cryptography - Keystore & Certificates
      • Cryptography - OAuth 2.0
      • Cryptography - Java 实现对称与非对称加密算法
      • Cryptography - TLS
    • DevOps
      • DevOps - Travis CI
    • Docker
      • Docker - Image & Storage Management
      • Docker - Image
      • Docker - Libcontainer
      • Docker - Multi-Arch Image
      • Docker - Multi-Stage Build
      • Docker - Network
      • Docker - Orchestration & Deployment
      • Docker - Overview
      • Docker - Service Building
      • Docker - Volume & Network Usage
      • Docker - Volume
      • Linux - Control Group
      • Linux - Namespace
    • Git
      • Git - Branch & Merge
      • Git - Cached
      • Git - Cherry Pick
      • Git - Commit
      • Git - Patch
      • Git - Proxy
      • Git - Rebase
      • Git - Reset
      • Git - Stash
      • Git - Theme for Git-Bash
    • Java
      • JVM - Synchronized
      • JVM - Volatile
      • Java - Annotation 注解
      • Java - BIO & NIO
      • Java - Class Path
      • Java - Condition and LockSupport
      • Java - Current Timestamp
      • Java - Deep Copy
      • Java - 运行环境配置
      • Java - Equals
      • Java - Exporting JAR
      • Java - Javadoc
      • Java - Lock
      • Java - Maven 项目构建工具
      • Java - References
      • Java - Reflection Mechanism
      • Java - String Split
      • Java - Thread Pool
      • Java - Thread
      • Tomcat - Class Loader
      • Tomcat - Container
    • Linux
      • addr2line
      • cut
      • df
      • du
      • fallocate
      • find
      • fio
      • grep
      • groupadd
      • gzip
      • head / tail
      • hexdump
      • iostat
      • iotop
      • kill
      • ldd
      • lsof
      • ltrace / strace
      • mpstat
      • netstat
      • nm
      • pidstat
      • pmap
      • readlink
      • readlink
      • rpm2cpio / rpm2archive
      • sort
      • tee
      • uniq
      • useradd
      • usermod
      • watch
      • wc
      • which
      • xargs
    • MS Office
      • MS Office - Add-in Dev
      • MS Office - Application
    • MySQL
      • InnoDB - Architecture
      • InnoDB - Backup
      • InnoDB - Checkpoint
      • InnoDB - Critical Features
      • InnoDB - Files
      • InnoDB - Index
      • InnoDB - Insert Buffer
      • InnoDB - Lock
      • InnoDB - Partition Table
      • InnoDB - Table Storage
      • MySQL - Server Configuration
      • MySQL - Storage Engine
    • Network
      • Network - ARP
      • Network - FTP
      • Network - GitHub Accelerating
      • HTTP - Message Format
      • HTTP - POST 提交表单的两种方式
      • Network - Proxy Server
      • Network - SCP
      • Network - SSH
      • Network - TCP Congestion Control
      • Network - TCP Connection Management
      • Network - TCP Flow Control
      • Network - TCP Retransmission
      • Network - Traceroute
      • Network - V2Ray
      • Network - WebSocket
      • Network - Windows 10 Mail APP
      • Network - frp
    • Operating System
      • Linux - Kernel Compilation
      • Linux - Multi-OS
      • Linux - Mutex & Condition
      • Linux - Operations
      • Linux: Package Manager
      • Linux - Process Manipulation
      • Linux - User ID
      • Linux - Execve
      • OS - Compile and Link
      • OS - Dynamic Linking
      • OS - ELF
      • Linux - Image
      • OS - Loading
      • OS - Shared Library Organization
      • OS - Static Linking
      • Syzkaller - Architecture
      • Syzkaller - Description Syntax
      • Syzkaller - Usage
      • Ubuntu - Desktop Recover (Python)
      • WSL: CentOS 8
    • Performance
      • Linux Performance - Perf Event
      • Linux Performance - Perf Record
      • Linux Performance - Perf Report
      • Linux Performance - Flame Graphs
      • Linux Performance - Off CPU Analyze
    • PostgreSQL
      • PostgreSQL - ANALYZE
      • PostgreSQL - Atomics
      • PostgreSQL - CREATE INDEX CONCURRENTLY
      • PostgreSQL - COPY FROM
      • PostgreSQL - COPY TO
      • PostgreSQL - Executor: Append
      • PostgreSQL - Executor: Group
      • PostgreSQL - Executor: Limit
      • PostgreSQL - Executor: Material
      • PostgreSQL - Executor: Nest Loop Join
      • PostgreSQL - Executor: Result
      • PostgreSQL - Executor: Sequential Scan
      • PostgreSQL - Executor: Sort
      • PostgreSQL - Executor: Unique
      • PostgreSQL - FDW Asynchronous Execution
      • PostgreSQL - GUC
      • PostgreSQL - Locking
      • PostgreSQL - LWLock
      • PostgreSQL - Multi Insert
      • PostgreSQL - Plan Hint GUC
      • PostgreSQL - Process Activity
      • PostgreSQL - Query Execution
      • PostgreSQL - Spinlock
      • PostgreSQL - Storage Management
      • PostgreSQL - VFD
      • PostgreSQL - WAL Insert
      • PostgreSQL - WAL Prefetch
    • Productivity
      • LaTeX
      • Venn Diagram
      • VuePress
    • Solidity
      • Solidity - ABI Specification
      • Solidity - Contracts
      • Solidity - Expressions and Control Structures
      • Solidity - Layout and Structure
      • Solidity - Remix IDE
      • Solidity - Slither
      • Solidity - Types
      • Solidity - Units and Globally Available Variables
    • Vue.js
      • Vue.js - Environment Variable
    • Web
      • Web - CORS
      • Web - OpenAPI Specification
    • Wireless
      • Wireless - WEP Cracking by Aircrack-ng
      • Wireless - WPS Cracking by Reaver
      • Wireless - wifiphisher

Java - BIO & NIO

Created by : Mr Dk.

2020 / 06 / 14 21:42

Nanjing, Jiangsu, China


BIO 和 NIO 的概念很早之前就听说过,但没有机会深挖。最近下定决心弄清楚,有机缘巧合买了一节相关的网课听了听,觉得有些收获。把几个常用到的名词和含义搞懂了:

  • BIO / NIO / AIO
  • SELECT / POLL / EPOLL
  • 同步 / 异步
  • 阻塞 / 非阻塞

本文大致以服务端网络 I/O 操作的发展演进过程为逻辑。


BIO

BIO 即 blocking I/O,这是操作系统中最简单的 I/O 模型。无论是在网络课还是在操作系统课上,无论是 Java 还是 C/C++,都肯定演示过一个最简单的网络程序 (伪代码):

  • 监听一个服务器端口
  • 在一个死循环中,不断接收客户端连接并处理
  • 如果不想在处理连接的同时错过新的客户端连接,就把连接的处理交给一个子进程/子线程
ServerSocket server = new ServerSocket(8080);
while (true) {
    Socket client = server.accept();

    new Thread() {
        // client...
    }
}

ServerSocket 的实例化过程有三步:

  • 调用 OS 的 socket() 系统调用,拿到一个文件描述符 fd
  • 调用 OS 的 bind() 将这个 fd 关联到服务器要监听的端口上
  • 调用 OS 的 listen() 开始监听 fd (即监听端口)
NAME
       socket - create an endpoint for communication

SYNOPSIS
       #include <sys/types.h>          /* See NOTES */
       #include <sys/socket.h>

       int socket(int domain, int type, int protocol);

DESCRIPTION
       socket()  creates  an  endpoint  for communication and returns a file descriptor that refers to that endpoint.
       The file descriptor returned by a successful call will be the lowest-numbered file  descriptor  not  currently
       open for the process.
NAME
       bind - bind a name to a socket

SYNOPSIS
       #include <sys/types.h>          /* See NOTES */
       #include <sys/socket.h>

       int bind(int sockfd, const struct sockaddr *addr,
                socklen_t addrlen);

DESCRIPTION
       When  a  socket  is  created  with  socket(2),  it  exists in a name space (address family) but has no address
       assigned to it.  bind() assigns the address specified by addr to the socket referred to by the file descriptor
       sockfd.   addrlen  specifies  the size, in bytes, of the address structure pointed to by addr.  Traditionally,
       this operation is called “assigning a name to a socket”.

       It is normally necessary to assign a local address using bind() before a SOCK_STREAM socket may  receive  con‐
       nections (see accept(2)).
NAME
       listen - listen for connections on a socket

SYNOPSIS
       #include <sys/types.h>          /* See NOTES */
       #include <sys/socket.h>

       int listen(int sockfd, int backlog);

DESCRIPTION
       listen() marks the socket referred to by sockfd as a passive socket, that is, as a socket that will be used to
       accept incoming connection requests using accept(2).

在死循环中,accept() 用于接受客户端连接,将会一直阻塞 (即不再向下执行代码,进程处于休眠),直到有新的连接到来,进程被 OS 唤醒,代码继续向下执行。该系统调用返回了客户端对应的 socket fd。

另外,在子线程处理连接的过程中,如果调用了 recv() 读取客户端发来的数据,OS 只有在接收到数据后才会使 recv() 返回。因此,accept() 和 recv() 这两个系统调用都是阻塞的。

NAME
       accept, accept4 - accept a connection on a socket

SYNOPSIS
       #include <sys/types.h>          /* See NOTES */
       #include <sys/socket.h>

       int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);

       #define _GNU_SOURCE             /* See feature_test_macros(7) */
       #include <sys/socket.h>

       int accept4(int sockfd, struct sockaddr *addr,
                   socklen_t *addrlen, int flags);

DESCRIPTION
       The  accept()  system  call  is  used  with  connection-based  socket types (SOCK_STREAM, SOCK_SEQPACKET).  It
       extracts the first connection request on the queue of pending connections for the  listening  socket,  sockfd,
       creates a new connected socket, and returns a new file descriptor referring to that socket.  The newly created
       socket is not in the listening state.  The original socket sockfd is unaffected by this call.
NAME
       recv, recvfrom, recvmsg - receive a message from a socket

SYNOPSIS
       #include <sys/types.h>
       #include <sys/socket.h>

       ssize_t recv(int sockfd, void *buf, size_t len, int flags);

       ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags,
                        struct sockaddr *src_addr, socklen_t *addrlen);

       ssize_t recvmsg(int sockfd, struct msghdr *msg, int flags);

DESCRIPTION
       The  recv(),  recvfrom(), and recvmsg() calls are used to receive messages from a socket.  They may be used to
       receive data on both connectionless and connection-oriented sockets.  This page first  describes  common  fea‐
       tures of all three system calls, and then describes the differences between the calls.

因此,BIO 的特性是:一个线程对应一个连接,能够解决的问题:

  • 可以接收大量的连接

但是弊端在于,OS 中将会同时存在大量的线程:

  • 线程内存开销较大
  • 大量的 CPU 资源被浪费在线程上下文切换上

造成 BIO 特性的根本原因是,系统调用的阻塞性。如果系统调用能够支持非阻塞,那么就能够使用新的 I/O 模型。

NIO

OS 中的 NIO 指的是 non-blocking I/O,而 Java JDK 中的 nio 指的是 new I/O。在 Linux 2.6.27 起对 socket() 系统调用的非阻塞提供了支持:

Since Linux 2.6.27, the type argument serves a second purpose: in addition to specifying a socket type, it
may include the bitwise OR of any of the following values, to modify the behavior of socket():

       SOCK_NONBLOCK   Set the O_NONBLOCK file status flag on the new open file  description.   Using  this  flag
                       saves extra calls to fcntl(2) to achieve the same result.

而较新的 Java 版本使用内核提供的非阻塞系统调用实现了 JDK 中的 NIO。可以看一个用 Java NIO API 实现的例子:

ServerSocketChannel server = ServerSocketChannel.open();
server.bind(new InetSocketAddress(9090));
server.configureBlocking(false);

// 所有客户端连接
LinkedList<SocketChannel> clients = new LinkedList<>();

while (true) {
    SocketChannel client = server.accept();

    if (client == null) {
        // ..
    } else {
        client.configureBlocking(false);
        clients.add(client); // 维护客户端
    }

    ByteBuffer buffer = ByteBuffer.allocateDirect(4096);
    for (SocketChannel c : clients) {
        int num = c.read(buffer);
        if (num > 0) {
            // ...
        }
    }
}

每一轮循环中,accept() 不会阻塞,立刻返回:

  • 如果有连接,就返回连接
  • 如果没有连接,就返回 null

另外,在每轮循环中,对于已经建立的每一个连接调用一次 read() 来接收数据,read() 的调用也是非阻塞的,如果有数据,就返回一个大于 0 的字节数。这样一来,只使用了一个线程,就实现了 BIO 模型中的功能。在并发量较大时 (C10K),相比 BIO 有优势。

它的局限性在于,假设 clients 中维护着大量的客户端连接。在每一轮循环中,都要对整个 clients 链表中的每一个客户端调用一次 read(),但只有极个别的客户端有数据可以被读取。那么相当于绝大部分 read() 的调用是无意义的。这一过程中,由于进行了大量的系统调用,大量的 CPU 时间被浪费在内核态-用户态切换上。

这个问题的实质是,用户空间程序用一个 for 循环来遍历内核中的数据结构 (fd),大量系统调用带来了大量堆栈切换的开销。如果用户空间程序可以通过 一次 系统调用,一次性把所有要查询的 fd 告诉内核,由内核来进行循环遍历,那么就可以节省堆栈切换的开销。这种通过一次系统调用同时查询多个 fd 的 I/O 状态的方式被称为 多路复用。


SELECT and POLL

这两个系统调用是 POSIX 标准中定义的多路复用功能,Linux 很早就实现了它们。用户空间程序告诉内核自己想要查询哪些 fd 的 I/O 已经处于就绪状态,内核代为查询之后,返回给用户空间。

关于 SELECT 在 Linux 0.12 中的实现可以参考 [这里](../../linux-kernel-comments-notes/Chapter 12 - 文件系统/Chapter 12.19 - select.c 程序.md)。用户空间程序可以以 bitmap 的形式,将想要查询 I/O 状态的 fd 对应的 bit 置为 1。其中的限制在于 bitmap 的长度是确定的,因此查询的 fd 范围受到了限制。而 POLL 的参数是链表,从而突破了这个局限。

NAME
       select, pselect, FD_CLR, FD_ISSET, FD_SET, FD_ZERO - synchronous I/O multiplexing

SYNOPSIS
       /* According to POSIX.1-2001, POSIX.1-2008 */
       #include <sys/select.h>

       /* According to earlier standards */
       #include <sys/time.h>
       #include <sys/types.h>
       #include <unistd.h>

       int select(int nfds, fd_set *readfds, fd_set *writefds,
                  fd_set *exceptfds, struct timeval *timeout);

       void FD_CLR(int fd, fd_set *set);
       int  FD_ISSET(int fd, fd_set *set);
       void FD_SET(int fd, fd_set *set);
       void FD_ZERO(fd_set *set);

       #include <sys/select.h>

       int pselect(int nfds, fd_set *readfds, fd_set *writefds,
                   fd_set *exceptfds, const struct timespec *timeout,
                   const sigset_t *sigmask);
   Feature Test Macro Requirements for glibc (see feature_test_macros(7)):

       pselect(): _POSIX_C_SOURCE >= 200112L

DESCRIPTION
       select()  and pselect() allow a program to monitor multiple file descriptors, waiting until one or more of
       the file descriptors become "ready" for some class of  I/O  operation  (e.g.,  input  possible).   A  file
       descriptor  is  considered ready if it is possible to perform a corresponding I/O operation (e.g., read(2)
       without blocking, or a sufficiently small write(2)).

       select() can monitor only file descriptors numbers that are less than FD_SETSIZE; poll(2)  does  not  have
       this limitation.  See BUGS.
NAME
       poll, ppoll - wait for some event on a file descriptor

SYNOPSIS
       #include <poll.h>

       int poll(struct pollfd *fds, nfds_t nfds, int timeout);

       #define _GNU_SOURCE         /* See feature_test_macros(7) */
       #include <signal.h>
       #include <poll.h>

       int ppoll(struct pollfd *fds, nfds_t nfds,
               const struct timespec *tmo_p, const sigset_t *sigmask);

DESCRIPTION
       poll()  performs a similar task to select(2): it waits for one of a set of file descriptors to become ready
       to perform I/O.

这种多路复用显著减少了系统调用的调用次数,但是还存在一个问题:

  • 每次调用都要传递大量重复的 fd (用户空间向内核空间复制参数是需要开销的)
  • 内核每次都要遍历内核中所有的 fd 来确定哪些 fd 就绪 (类似轮询,需要通过中断解决)

如果能够在内核中维护用户空间要查询的所有 fd 的状态,那么就能够消除这种参数复制开销。这就是 EPOLL 多路复用器出现的意义。


EPOLL

EPOLL 如何实现高效查询多个 fd 的状态呢?

EPOLL 包含三个系统调用:

int epoll_create(int size);
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
int epoll_wait(int epfd, struct epoll_event *events,
                      int maxevents, int timeout);

这三个系统调用的使用方式:

首先使用 epoll_create(),内核会开辟一块内存,然后返回一个代表这个内存区的 epoll fd。这块内存用于保存所有要监视的 fd。

然后,用户空间通过调用 epoll_ctl(),将 想要监视的 fd 以及 事件 注册到 epoll fd 对应的内存中。注意,对于某一个被监视的 fd,一般只需要调用一次 epoll_ctl() 即可。epoll_ctl() 支持的注册事件包括:

  • EPOLL_CTL_ADD - 注册 fd
  • EPOLL_CTL_MOD - 修改已注册 fd 的事件
  • EPOLL_CTL_DEL - 解除注册 fd

支持监视的事件很多,比如:

  • EPOLLIN - fd 可被调用 read()
  • EPOLLOUT - fd 可被调用 write()
  • ...

用户空间程序只需要调用 epoll_wait() (并可指定一个超时参数),内核就会返回目前已经就绪的 fd。

可以看到,上述步骤解决了 SELECT 与 POLL 中需要反复向内核传递 fd 集合的问题,内核已经在 epoll fd 对应的内存中维护了所有已注册的被监控 fd。另外,EPOLL 中具体做了哪些工作来提高性能呢?

/*
 * This structure is stored inside the "private_data" member of the file
 * structure and represents the main data structure for the eventpoll
 * interface.
 */
struct eventpoll {
	/* Protect the access to this structure */
	spinlock_t lock;

	/*
	 * This mutex is used to ensure that files are not removed
	 * while epoll is using them. This is held during the event
	 * collection loop, the file cleanup path, the epoll file exit
	 * code and the ctl operations.
	 */
	struct mutex mtx;

	/* Wait queue used by sys_epoll_wait() */
	wait_queue_head_t wq;

	/* Wait queue used by file->poll() */
	wait_queue_head_t poll_wait;

	/* List of ready file descriptors */
	struct list_head rdllist;

	/* RB tree root used to store monitored fd structs */
	struct rb_root_cached rbr;

	/*
	 * This is a single linked list that chains all the "struct epitem" that
	 * happened while transferring ready events to userspace w/out
	 * holding ->lock.
	 */
	struct epitem *ovflist;

	/* wakeup_source used when ep_scan_ready_list is running */
	struct wakeup_source *ws;

	/* The user that created the eventpoll descriptor */
	struct user_struct *user;

	struct file *file;

	/* used to optimize loop detection check */
	int visited;
	struct list_head visited_list_link;

#ifdef CONFIG_NET_RX_BUSY_POLL
	/* used to track busy poll napi_id */
	unsigned int napi_id;
#endif
};

以上是 Linux 4.15 内核中的 epoll 结构体。内核在 struct rb_root_cached rbr 中将所有被监控的 fd 维护成一颗红黑树,对这棵树进行增删改查的时间复杂度为 O(log(n))。另外,struct list_head rdllist 维护了所有已经就绪的被监控 fd。

当被监控的 fd 加入红黑树时,内核会为中断处理程序注册一个回调函数。当这个 fd 对应的中断到达时,在中断处理程序的回调函数中,将 fd 自身放入就绪链表。当用户空间程序调用 epoll_wait() 时,直接返回这个链表中的数据即可。

EPOLL 是一个适用于多 CPU core 场景的多路复用器。对于 SELECT 或 POLL 来说,假设一个 CPU 正在运行用户空间的服务器进程,当服务器进程调用 SELECT 或 POLL 时,另一个 CPU 运行内核态代码,而运行服务器进程的 CPU 只能干等,或上下文切换。对于 EPOLL 来说,一个 CPU 可以专门运行服务器进程,一个 CPU 可以专门运行内核中的中断处理代码,两者可以并行。能够并行的根本原因是,内核中维护了 fd 的状态,两个 CPU core 能够共同访问这些状态。

EPOLL 有两种触发方式:

  • Level-triggered (水平触发) (也是 SELECT、POLL 的触发方式)
  • Edge-triggered (边沿触发) (也是 信号驱动 I/O 的触发方式)

顾名思义,水平触发就是,只要 fd 已经就绪,每次调用 epoll_wait() 都能返回该 fd;而边沿触发只会在某个 fd 第一次就绪时通过 epoll_wait() 返回,假设用户代码没有对一个已经就绪的 fd 调用 recv(),那么之后调用 epoll_wait() 时内核将不会返回这个 fd,直到这个 fd 第二次就绪。


同步 & 异步?

这是一个纠缠了很久的问题,但是随着上述知识点的学习,似乎突然就明朗了。

可以看到,上述的 BIO 过程,或是 NIO + 多路复用器的过程,程序都需要主动调用 recv() / accept() 等系统调用读取数据或建立连接。即使使用了 多路复用器,其系统调用也只不过是向内核 查询了 I/O 的状态,对于状态为 ready 的 fd,程序还是需要主动调用 recv() 接收数据。这种 I/O 模型全部被称为 同步 I/O 模型。

而所谓的异步 I/O,是有专门的内核线程负责将 socket 接收到的数据自动拷贝到程序的用户空间缓冲区中,再发送信号通知进程。程序自己不需要主动调用 recv() 从内核拷贝数据了。这种模型被称为 异步 I/O (AIO) 模型。


References

腾讯课堂 马士兵教育公开课:BIO NIO Netty

CSDN - epoll 为什么这么快,epoll 的实现原理

Julia Evans - Async IO on Linux: select, poll, and epoll

[Mr Dk.'s blog - Linux 0.12 内核完全注释 - select.c 程序](../../linux-kernel-comments-notes/Chapter 12 - 文件系统/Chapter 12.19 - select.c 程序.md)

Edit this page on GitHub
Prev
Java - Annotation 注解
Next
Java - Class Path