1 introduction

As we all know, in order to achieve a high performance communication server, BIO will suffer from rapid performance degradation in the case of high concurrency, and even lead to OOM due to the creation of multiple threads. Therefore, in the Java industry, BIO performance has been criticized by developers. Fortunately, JDK1.4 introduced NIO, which basically solves BIO performance problems and is the basic framework for implementing Java high-performance servers. NIO is officially called New IO, but it is also non-blocking IO at the operating system level.

The well-known Netty framework is NIO, and many open source frameworks such as Dubbo, RocketMQ,Seata, Spark, and Flink use Netty as their basic communication component. So it’s important to learn Netty well, but NIO is the foundation of Netty, so is learning NIO well!

To learn NIO well, you must first understand the five network IO models at the operating system level.

5 IO models

2.1 Blocking IO model

The blocking IO model is shown as follows:As you can see from the figure above, the process (thread) is blocked atrecvfromSystem call. What does that mean? To put it bluntly, if we want to read data from a socket, we must callreadMethod, this right herereadMethod triggers the operating system kernel oncerecvfromSystem call, where there are two cases:

  1. The kernel has not received the remote data and the datagram is not ready, so the thread reading the data will continue to block until the remote sends the datagram. This blocking process corresponds to the process of number 1 in the figure above. While the datagram is copied from the kernel to user space, the thread blocks again until the copy is complete, which corresponds to sequence number 2 in the figure above.
  2. The kernel has received the remote data, the datagram is ready, and the datagram is copied from the kernel to user space. This process is blocked, as shown in number 2 above.

The user process (thread) is blocked when the kernel’s datagrams are not ready. The recvFROM system call is blocked when the kernel’s datagrams are not ready. When the kernel datagram is ready, the datagram is copied from the kernel to user space, and the user process (thread) is blocked. It is not until the datagrams are copied into user space that the user process (thread) wakes up and processes the datagrams to perform some of the user’s business logic. Of course, if the user process (thread) is blocking, if the recvFROM system call is interrupted by a signal, then the blocking will also be woken up.

Consider: what happens when the recvFROM system call here is interrupted by a signal? Does this signal refer to thread.interrupt ()? Think for yourself.

2.2 Non-blocking IO model

The non-blocking IO model is shown below:As shown above, depending on whether datagrams are ready in the kernel, there are two scenarios:

  1. When the datagrams in the kernel are not readyrecvfromThe system call returns one immediatelyEWOULDBLOCKError, that is, the user process (thread) will not be put in a blocked state. Let’s take Java NIO for example, when we configureServerSocketChannel.configureBlocking(false);orSocketChannel.. configureBlocking(false);When, we callServerSocketChannel.accept()thenullorSocketChannel.read(buffer)It will not block. If no new connection is connected or datagrams are not ready in the kernel, the return will be understoodnullor0The return result of theEWOULDBLOCKError;
  2. When a datagram is ready in the kernelrecvfromSystem calls, the user process (thread) will still block until the kernel datagrams have been copied into user space, at which point the user process (thread) will wake up to process the received datagrams.

Non-blocking IO The recvFROM system call does not block until the user’s datagram is ready, and then the recvFROM system call continues to see if the datagram is ready. The process (thread) rotates over and over again, so this is very CPU expensive. This model is not very common and is suitable for use when a PARTICULAR CPU is reserved for certain functions.

2.3 IO multiplexing model

IO multiplexing model is shown as follows:From the above IO multiplexing model, this is not the same as the IO blocking model? When the kernel is not reported to be ready,selectSystem calls block; When kernel data is copied to user spacerecvfromSystem calls will still block. What’s the difference from the IO blocking model? The difference is that the IO reuse model has one more time than the blocking IO modelrecvfromSystem call, isn’t that another obvious waste of CPU resources?

If we think this way, then why is the IO reuse model so widely used? The real advantage of the IO reuse model lies in the SELECT operation, which can select multiple file descriptors corresponding to the OP_CONNECT,OP_ACCEPT,OP_READ, and OP_WRITE ready events in Java NIO. Thanks to the ability to select multiple file descriptors from a single thread on a recvFROM system call, we now have a single user thread listening for ready events like OP_CONNECT,OP_ACCEPT,OP_READ and OP_WRITE on different channels. Then get the corresponding channel according to a ready event to do the corresponding operation. Instead of having to select only one file descriptor per thread in a recvFROM system call, as in the blocking or non-blocking IO models, scalability is severely limited. For example, in the blocking IO model, since the user process (thread) blocks each recvFROM system call and corresponds to only one file descriptor, if the server thread blocks the read operation of client A, if another client B needs to access the server, In this case, the server thread blocks the read operation of client A and cannot process the connection operation of client B. In this case, a file descriptor must be created per thread. That is, every time the server thread accepts a client connection, a new thread must be created to handle the reading and writing of the client connection. As we all know, threads are a very expensive CPU resource. When tens of thousands of threads are enabled, the cost of thread switching is very high, and the CPU performance will definitely decrease, perhaps even OOM in high concurrency. For the blocking IO model, we don’t use one socket per thread, we use a thread pool instead. Of course, this is an optimization point, but it doesn’t address the root of the blocking IO model. How can I put it? When all threads in the thread pool block the client’s read or write operations, other newly added threads will be stuck in the queue of the thread pool.

2.4 Signal-driven IO model

The signal-driven IO model is shown as follows:As you can see, the signal-driven IO model does not block while waiting for datagrams, i.e. the user process (thread) sends onesigactionAfter the system call, it returns immediately, without blocking, and the user process (thread) continues execution; When the datagrams are ready, the kernel generates one for the process (thread)SIGIOSignal, at which point the process (thread) occurs oncerecvfromThe system call copies the datagrams from the kernel to user space. Note that this phase is blocked.

PS: I found the Java code of signal-driven IO model on the Internet, but I did not find it. The next partners of signal-driven IO model code can teach me.

2.5 Asynchronous I/O model

The asynchronous I/O model is shown as follows:The asynchronous IO model is also well understood, in which both the user process (thread) waits for datagrams and copies datagrams from the kernel to user space are non-blocking, that is, the user process (thread) makes a system call, immediately returns, and then the user process (thread) continues to execute. After the kernel has received and copied the datagrams to user space, it notifies the user process (thread) to process the datagrams in user space. That is, the sequence of IO operations is handed over to the kernel, and the user process does not block synchronously, so it is asynchronous and non-blocking.

Extension: The difference between the asynchronous IO model and the signal-driven IO model is that when the kernel has the datagram ready, in the signal-driven IO model, the kernel notifies the user process that the datagram is ready, and you need to make a system call to copy the datagram from the kernel to user space. This process is synchronous blocking. In the asynchronous IO model, instead of notifying the user process when the datagram is ready, the kernel silently copies the datagram from the kernel to user space and then notifies the user process that the datagram has been copied to user space and you can just do your business logic.

3 Different I/O models

Through the comparison of 5 IO models, it can be found that the first 4 IO models are synchronous blocking IO models, because the second phase of the copy from the kernel to user space are synchronous blocking, but the first phase waiting datagram processing is different; The last IO model (asynchronous IO) is the true asynchronous non-blocking IO model, where the kernel does everything (kernel: I’m really tired).

4 summarizes

Well, five IO models have been summed up, basic is based on their own “UNIX network programming _ volume 1_ socket” reading summary, and then through Java code to achieve these IO models again.

Reference: UNIX Network Programming volume 1Socket

If you feel good, please mercilessly forward and like it!

Github address:

Github.com/yuanmabiji/…