Five major IO models in Linux and three IO models in Java

The five major IO models in Linux and the three IO models in Java

  • IO model
  • background
    • 1. Kernel state, user state
    • 2. The general process of the application receiving data from the network
    • 3. Synchronous/asynchronous, blocking/non-blocking
  • Five major Linux IO models
    • Blocking IO
    • Non-blocking IO
    • Multiplexed IO
    • Signal driven IO
    • Asynchronous IO
  • Three IO models of JAVA
    • BIO (synchronous blocking)
    • NIO (synchronous non-blocking)
      • NIO without the introduction of multiplexers:
      • NIO with multiplexers introduced:
    • AIO (asynchronous non-blocking)

IO model

I have often heard the terms five IO models and three IO models before, and it is often easy to get confused. Here is a detailed explanation.
Five IO models: The five IO models in Linux systems are: blocking IO, non-blocking IO, multiplexed IO, signal-driven IO and asynchronous IO.
Three IO models: The three IO models in JAVA are: synchronous blocking (BIO), synchronous non-blocking (NIO) and asynchronous non-blocking model (AIO).

Background

IO refers to Input/Output in computers, that is, input and output. Since programs and runtime data reside in memory and are executed by the ultra-fast computing core of the CPU, IO interfaces are required wherever data exchange is involved, usually disks, networks, etc.

I/O operations are relative to memory. Entering the memory from an external device is called Input, and conversely, outputting from the memory to an external device is called Output.
In LINUX, processes cannot directly operate I/O devices and must make system calls to request the kernel to assist in completing I/O operations. The kernel maintains a buffer for each I/O device.
For an input operation, after the process IO system call, the kernel will first check whether there is corresponding cache data in the buffer. If there is data, it will be copied directly to the process space. If not, it will be read from the device, because the IO device is generally fast. Slower, need to wait.

Usually a complete IO in a user process is divided into two stages: user process space <–> kernel space, kernel space <- -> device space (disk, network, etc.). There are three types of IO: memory IO, network IO and disk IO. Usually when we talk about IO, we refer to the latter two.

I/O is divided into two types according to the device: one is network I/O, which is the pulling and output of data through the network. One is disk I/O, which mainly reads and writes to the disk.

1. Kernel state, user state

To understand the IO model, you need to understand the concepts of kernel mode and user mode. In order to protect itself, the operating system has designed two states: user mode and kernel mode. Applications generally work in user mode. When calling some underlying operations (such as IO operations), they need to switch to kernel mode. Switching between user mode and kernel mode consumes some resources. Zero-copy technology improves performance by reducing the switching between user mode and kernel mode.

Virtual memory is divided into two parts by the operating system: kernel space and user space. For safety, they are isolated so that even if the user’s program crashes, the kernel is not affected.
Kernel space is where kernel code runs, and user space is where user program code runs.
When a process is running in kernel space, it is in kernel mode, and when a process is running in user space, it is in user mode.

2. The general process of the application receiving data from the network

The general process of the server receiving from the network is as follows:

1. Data comes to the network card through the computer network
2. Read the network card data into the socket buffer
3. Read the socket buffer into the user buffer, and then the application can use it.

The core is two read operations. The difference between the five major IO models lies in how these two read operations interact.

3. Synchronous/asynchronous, blocking/non-blocking

Synchronization/asynchronous: This is an application-level concept, which refers to calling a function. Should we wait for the function to finish executing before continuing to the next step, or should we finish adjusting the function and continue to the next step, starting from scratch? A thread to execute the called function. The focus is on collaboration between threads.
Synchronization and asynchronous focus on the message communication mechanism.
The so-called synchronization means that when issuing a call, you need to participate in the process of waiting for the result, which is synchronization. The first four IOs are all involved, so it is also called synchronous IO.
Asynchronous IO means that after the call is issued, until the data preparation is completed, you are not involved, so it is asynchronous.

Blocking/non-blocking, this is a concept at the hardware level. Blocking means that the CPU is “rested” to handle other processes, such as IO operations, while non-blocking means that the CPU will still execute, and will not Switch to other process. The concern is whether the CPU will be “rested”, which is manifested at the application level as whether the thread will be “suspended”.

As for the difference between synchronization and blocking, and the difference between asynchronous and non-blocking, in fact, these are things at different levels and cannot be compared with each other.

Linux’s five major IO models

As mentioned before, the general process for an application to receive data from the network is two steps:

  1. Data preparation: wait for network data and read the network card data into the socket buffer
  2. Data copy: Read the data from the socket buffer to the user mode Buffer for use by the application program

Blocking IO

The academic language is: when an application calls recvfrom to read data, its system call does not return until the data packet arrives and is copied to the application buffer or an error is sent. The process will be waiting during this period. The process is blocked from the time it is called to the time it returns, which is called Blocking IO; The system call will wait until the kernel prepares the data. All Sockets are blocked by default

Process description:
1. The application process initiates recfrom to read data from the kernel.
2. The kernel prepares the datagram (the application process is blocked at this time)
3. The kernel negatively copies data from the kernel to the application space.
4. After the copy is completed, a success message will be returned.

Non-blocking IO

Non-blocking IO means that when application B initiates a request to read data, if the kernel data is not ready, it will immediately tell application B and will not let B wait here. If the kernel has not prepared the data, the system call will still return directly. And return EWOULDBLOCK error code;
Non-blocking IO often requires programmers to repeatedly try to read and write file descriptors in a loop. This process is called polling. This is a huge waste of CPU and is generally only used in specific scenarios.

Processes and Flowcharts:
1. The application process initiates recvfrom to read data from the kernel.
2. The kernel datagram is not ready and the EWOULDBLOCK error code is returned immediately.
3. The application process initiates recvfrom to read data from the kernel again.
4. If the kernel has a data packet ready, proceed to the next step. Otherwise, it will still return an error code and execute the third step.
5. The kernel copies the data to user space.
6. After completion, a success message will be returned.

Multiplexed IO

Multiplexing model: A thread monitors multiple network requests (fd file descriptor, the Linux system identifies all network requests with an fd) to complete the data status query operation. When there is After the data is ready, the corresponding thread is assigned to read the data, so that a large amount of thread resources can be saved.


As can be seen from the above figure,multiplexing means that the system provides a function that can monitor the operations of multiple FDs at the same time. This function is the select, poll, and epoll functions we often talk about, through which multiple FDs can be monitored at the same time. fdAs long as any data state is ready, it returns to the readable state. At this time, the query thread notifies the thread processing the data, and the corresponding thread then initiates a recvfrom request to read the data.

Although it looks similar to blocking IO from the flow chart, in fact the core point is that IO multiplexing can wait for the ready status of multiple file descriptors at the same time, so that there is no need to create a corresponding monitoring thread for each fd. , thereby reducing the purpose of thread resource creation.

flow chart:

Signal driver IO

Multi-channel switching solves the problem of one thread being able to monitor multiple FDs, but choosing to use brainless polling is a bit violent, because polling in most cases is invalid, so some people think, don’t let me I always ask if the data is ready, but wait for you to proactively notify me when it is ready. This is signal-driven IO.

Signal-driven IO establishes a SIGIO signal connection when calling sigaction. When the kernel prepares the data, it notifies the thread through the SIGIO signal that the fd is ready. When the thread receives the readable signal, it initiates a recvfrom read to the kernel. For data retrieval requests, because under the signal-driven IO model, the application thread can return after sending the signal monitoring without blocking, so one application thread can also monitor multiple FDs at the same time.


When the kernel prepares the data, it uses the SIGIO signal to notify the application to perform IO operations.

Although select in multiplexed IO can monitor multiple fds, the essence of select implementation is to monitor the data status by continuously polling fds, because most polling requests are actually invalid, soSignal-driven IO aims to establish signal association in this way, so that after issuing a request, you only need to wait for the notification that the data is ready, so that a large number of invalid data status polling operations can be avoided.

Asynchronous IO

Asynchronous IO is different from the above four IO models. It is completely asynchronous, and the two-step operation will not block. After the application initiates a read call, it can use the data in the user-mode Buffer after receiving the callback notification.

The application only needs to send a read request to the kernel, telling the kernel that it wants to read the data and return immediately; the kernel will establish a signal connection after receiving the request. When the data is ready, the kernel will actively copy the data from the kernel to user space. After all operations are completed, the kernel will initiate a notification to tell the application. We call this model an asynchronous IO model.

The optimization idea of asynchronous IO is to solve the problem that the application needs to send an inquiry request and receive a data request in two stages. In the asynchronous IO mode, all operations of status inquiry and data copy can be completed by sending only one request to the kernel.

Three IO models of JAVA

BIO (synchronous blocking)

Synchronous blocking model, one client connection corresponds to one processing thread

Each new network connection is assigned to a thread, and each thread independently handles its own input and output, also known as the Connection Per Thread mode
shortcoming:

1. The read operation in the IO code is a blocking operation. If the connection does not perform data reading and writing operations, it will cause thread blocking and waste resources.

2. If there are many threads, the server will have too many threads and be under too much pressure, such as the C10K problem.

The so-called c10k problem refers to the problem of the server supporting thousands of clients at the same time, that is, concurrent 10 000 connections

Application scenarios: The BIO method is suitable for architectures with a relatively small and fixed number of connections. This method has relatively high requirements on server resources, but the program is simple and easy to understand.

The sample code is as follows:

/**
 * @Title: BIO server
 */
public class SocketServer {<!-- -->

    public static void main(String[] args) throws IOException {<!-- -->
        ServerSocket serverSocket = new ServerSocket(9000);
        while (true){<!-- -->
            System.out.println("Waiting for connection...");
            Socket clientSocket = serverSocket.accept();
            System.out.println("Client" + clientSocket.getRemoteSocketAddress() + "Connected!");

            handle(clientSocket);
        }
    }

    private static void handle(Socket clientSocket) throws IOException{<!-- -->
        byte[] bytes = new byte[1024];
        int read = clientSocket.getInputStream().read(bytes);
        System.out.println("read client" + clientSocket.getRemoteSocketAddress() + "data completed");
        if(read != -1){<!-- -->
            System.out.println("Received data from client: " + new String(bytes, 0, read));
        }
        clientSocket.getOutputStream().write("HelloClient".getBytes());
        clientSocket.getOutputStream().flush();
    }
}


/**
 * @Title: BIO client
 */
public class SocketClient {<!-- -->

    public static void main(String[] args) throws IOException {<!-- -->

        Socket socket = new Socket("localhost", 9000);
        //Send data to the server
        socket.getOutputStream().write("HelloServer".getBytes());
        socket.getOutputStream().flush();
        System.out.println("End of sending data to server");

        byte[] bytes = new byte[1024];
        //Receive data returned by the server
        socket.getInputStream().read(bytes);

        System.out.println("Data received from server: " + new String(bytes));
        socket.close();
    }
}

NIO (synchronous non-blocking)

Synchronous and non-blocking, the server implementation mode is that one thread can handle multiple connection requests (connections). The connection requests sent by the client will be registered on the multiplexer selector. The multiplexer polls when there is an IO request on the connection. Processing was introduced in JDK1.4.

Application scenarios: The NIO method is suitable for architectures with a large number of connections and relatively short connections (light operations), such as chat servers, barrage systems, and communication between servers, and the programming is relatively complex. netty is the nio model.

NIO has three core components: Channel, Buffer, Selector

1. Channel is similar to a stream. Each channel corresponds to a buffer. The bottom layer of the buffer is an array.

2. The channel will be registered to the selector, and the selector will hand it over to an idle thread for processing according to the occurrence of channel read and write events.

3.NIO’s Buffer and Channel are both readable and writable.

There are two code examples for NIO:

NIO without the introduction of multiplexers:

/**
 * @Title: Nio server
 */
public class NioServer {<!-- -->

    /**
     * Save client connection
     */
    static List<SocketChannel> channelList = new ArrayList<>();

    public static void main(String[] args) throws IOException {<!-- -->
        //Create Nio ServerSocketChannel
        ServerSocketChannel serverSocket = ServerSocketChannel.open();
        serverSocket.socket().bind(new InetSocketAddress(9000));
        //Set ServerSocketChannel to non-blocking
        serverSocket.configureBlocking(false);
        System.out.println("Nio service started successfully");

        while(true){<!-- -->
            //Non-blocking mode accept method will not block
            /// NIO's non-blocking is implemented internally by the operating system, and the accept function of the Linux kernel is called at the bottom
            SocketChannel socketChannel = serverSocket.accept();
            if(socketChannel != null){<!-- -->
                System.out.println("Connection successful");
                socketChannel.configureBlocking(false);
                channelList.add(socketChannel);
            }

            Iterator<SocketChannel> iterator = channelList.iterator();
            while(iterator.hasNext()){<!-- -->
                SocketChannel sc = iterator.next();
                ByteBuffer byteBuffer = ByteBuffer.allocate(128);
                //Non-blocking mode read method will not block
                int len = sc.read(byteBuffer);

                if(len > 0){<!-- -->
                    System.out.println("Message received:" + new String(byteBuffer.array()));
                }else if(len == -1){<!-- -->
                    iterator.remove();
                    System.out.println("Client disconnected");
                }
            }
        }
    }
}

/**
 * @Title: Nio client
 */
public class NioClient {<!-- -->

    public static void main(String[] args) throws IOException {<!-- -->
        SocketChannel socketChannel=SocketChannel.open(new InetSocketAddress("localhost", 9000));
        socketChannel.configureBlocking(false);

        ByteBuffer writeBuffer=ByteBuffer.wrap("HelloServer1".getBytes());
        socketChannel.write(writeBuffer);
        System.out.println("Sending data 1 to the server ends");

        writeBuffer = ByteBuffer.wrap("HelloServer2".getBytes());
        socketChannel.write(writeBuffer);
        System.out.println("Send data to server 2 ends");

        writeBuffer = ByteBuffer.wrap("HelloServer3".getBytes());
        socketChannel.write(writeBuffer);
        System.out.println("Send data to server 3 ends");
    }
}

NIO with multiplexer introduced:

/**
 * @Title: NIO server after introducing multiplexer
 * SelectionKey.OP_ACCEPT - Receiving the connection continuation event means that the server has monitored the client connection and the server can accept the connection.
 * SelectionKey.OP_CONNECT - connection ready event, indicating that the connection between the client and the server has been successfully established
 * SelectionKey.OP_READ - read ready event, indicating that there is already readable data in the channel and the read operation can be performed (the channel currently has data and can be read)
 * SelectionKey.OP_WRITE - Write ready event, indicating that data can be written to the channel (the channel can currently be used for write operations)
 *
 * 1. After registering the SelectionKey.OP_READ event in the channel, if the client writes data to the cache, the next time it polls, isReadable()=true;
 *
 * 2. After registering the SelectionKey.OP_WRITE event in the channel, you will find that isWritable() in the current polling thread is always true, if it is not set to other events
 */
 public class NioSelectorServer {<!-- -->

    public static void main(String[] args) throws IOException {<!-- -->
        /**
         * Create the server and register with the multiplexer to let the multiplexer listen for connection events
         */
        //Create ServerSocketChannel
        ServerSocketChannel serverSocket = ServerSocketChannel.open();
        serverSocket.socket().bind(new InetSocketAddress(9000));
        //Set ServerSocketChannel to non-blocking
        serverSocket.configureBlocking(false);
        //Open the selector to process the channel, that is, create epoll
        Selector selector = Selector.open();
        //Register ServerSocketChannel to the selector, and the selector is interested in the client's accept connection operation
        serverSocket.register(selector, SelectionKey.OP_ACCEPT);
        System.out.println("NioSelectorServer service started successfully");
while(true){<!-- -->
            //Block and wait for the event to be processed to occur
            selector.select();

            //Get the SelectionKey instances of all events registered in the selector
            Set<SelectionKey> selectionKeys = selector.selectedKeys();
            Iterator<SelectionKey> iterator = selectionKeys.iterator();

            //Traverse selectionKeys and process events
            while (iterator.hasNext()){<!-- -->
                SelectionKey key = iterator.next();
                //If it is an OP_ACCEPT event, connect and register the event
                if(key.isAcceptable()){<!-- -->
                    ServerSocketChannel serverSocketChannel = (ServerSocketChannel) key.channel();
                    //Accept client connection
                    SocketChannel socketChannel = serverSocketChannel.accept();
                    socketChannel.configureBlocking(false);
                    //Register SocketChannel to the selector, and the selector is interested in the client's read operation (that is, reading messages from the client)
                    socketChannel.register(selector, SelectionKey.OP_READ);
                    System.out.println("Client" + socketChannel.getRemoteAddress() + "Connection successful!");

                }else if(key.isReadable()){<!-- -->
                    SocketChannel socketChannel = (SocketChannel) key.channel();
                    ByteBuffer byteBuffer = ByteBuffer.allocate(128);
                    int len = socketChannel.read(byteBuffer);
                    if(len > 0){<!-- -->
                        System.out.println("Received message from client" + socketChannel.getRemoteAddress() + "Message content is:" + new String(byteBuffer.array()));
                    }else if(len == -1){<!-- -->
                        System.out.println("Client disconnected");
                        //Close the client
                        socketChannel.close();
                    }
                }
                //Delete the key processed this time from the event collection to prevent repeated processing of the select next time
                iterator.remove();
            }
        }
    }
}


/**
 * @Title: NIO client after introducing multiplexer
 */
public class NioSelectorClient {<!-- -->

    public static void main(String[] args) throws IOException {<!-- -->
        SocketChannel socketChannel = SocketChannel.open();
        socketChannel.configureBlocking(false);
        Selector selector = Selector.open();
        //You must first register with the multiplexer before you can connect to the server.
        socketChannel.register(selector, SelectionKey.OP_CONNECT);
        socketChannel.connect(new InetSocketAddress("localhost", 9000));

        while (true){<!-- -->
            selector.select();
            Set<SelectionKey> keys = selector.selectedKeys();
            Iterator<SelectionKey> iterator = keys.iterator();
            while (iterator.hasNext()){<!-- -->
                SelectionKey key = iterator.next();
                iterator.remove();
                if (key.isConnectable()){<!-- -->
                    SocketChannel sc = (SocketChannel) key.channel();
                    if (sc.finishConnect()){<!-- -->
                        System.out.println("Server connection successful");

                        ByteBuffer writeBuffer=ByteBuffer.wrap("HelloServer".getBytes());
                        sc.write(writeBuffer);
                        System.out.println("End of sending data to server");
                    }
                }
            }
        }
    }
}


AIO (asynchronous non-blocking)

Asynchronous and non-blocking, the operating system completes the callback to notify the server program to start a thread for processing. It is generally suitable for applications with a large number of connections and long connection times.

Application scenarios: The AIO method is suitable for architectures (applications) with a large number of connections and long connection times (heavy operations), and is supported by JDK7.

The reason why the famous asynchronous network communication framework netty abandoned AIO is that on Linux systems, the underlying implementation of NIO uses Epoll, while the underlying implementation of AIO still uses Epoll, and AIO is not implemented well, so there is no obvious performance improvement. The advantages of

AIO sample code is as follows:

/**
 * @Title: Aio server
 */
public class AioServer {<!-- -->

    public static void main(String[] args) throws Exception {<!-- -->
        final AsynchronousServerSocketChannel serverChannel = AsynchronousServerSocketChannel.open().bind(new InetSocketAddress(9000));
        serverChannel.accept(null, new CompletionHandler<AsynchronousSocketChannel, Object>() {<!-- -->
            @Override
            public void completed(AsynchronousSocketChannel socketChannel, Object attachment) {<!-- -->
                try{<!-- -->
                    System.out.println("2--" + Thread.currentThread().getName());
                    //Receive client connection
                    serverChannel.accept(attachment,this);
                    System.out.println("Client" + socketChannel.getRemoteAddress() + "Connected");

                    ByteBuffer buffer = ByteBuffer.allocate(128);
                    socketChannel.read(buffer, null, new CompletionHandler<Integer, Object>() {<!-- -->
                        @Override
                        public void completed(Integer result, Object attachment) {<!-- -->
                            System.out.println("3--" + Thread.currentThread().getName());
                            //flip method switches Buffer from write mode to read mode
                            //If not, it will be read from the end of the file. Of course, all the characters read out are the characters when byte=0. Through the buffer.flip(); statement, you can change the current position of the buffer to the first position of the buffer.
                            buffer.flip();
                            System.out.println(new String(buffer.array(), 0, result));
                            socketChannel.write(ByteBuffer.wrap("hello Aio Client!".getBytes()));
                        }

                        @Override
                        public void failed(Throwable exc, Object attachment) {<!-- -->
                            exc.printStackTrace();
                        }
                    });
                }catch(Exception e){<!-- -->
                    e.printStackTrace();
                }
            }

            @Override
            public void failed(Throwable exc, Object attachment) {<!-- -->

            }
        });
        System.out.println("1‐main" + Thread.currentThread().getName());
        Thread.sleep(Integer.MAX_VALUE);
    }
}

/**
 * @Title: Aio client
 */
public class AioClient {<!-- -->

    public static void main(String[] args) throws Exception {<!-- -->
        //Create Aio client
        AsynchronousSocketChannel socketChannel = AsynchronousSocketChannel.open();
        socketChannel.connect(new InetSocketAddress("localhost", 9000)).get();
        //Send a message
        socketChannel.write(ByteBuffer.wrap("hello AIO server!".getBytes()));
        //receive message
        ByteBuffer buffer = ByteBuffer.allocate(128);
        Integer len = socketChannel.read(buffer).get();
        if(len != -1){<!-- -->
            //The client receives the message: hello Aio Client!
            System.out.println("Client received message:" + new String(buffer.array(), 0, len));
        }
    }
}

refer to:
1. https://blog.csdn.net/m0_51723227/article/details/127931476
2. https://www.w3cschool.cn/article/40045541.html

syntaxbug.com © 2021 All Rights Reserved.