[Linux Programming] Five IO models of Linux: blocking IO, non-blocking IO, IO multiplexing, signal-driven IO and asynchronous IO

The Linux IO model refers to the way in which user processes and the kernel perform input and output operations in a Linux system. Input and output operations usually involve the transmission and copying of data. These operations consume system resources and time. Therefore, choosing an appropriate IO model is very important to improve the performance and efficiency of the program.

The Linux system provides us with five available IO models, namely blocking IO, non-blocking IO, IO multiplexing, signal-driven IO and asynchronous IO. The purpose of these models is to enable applications to better manage and process input and output operations. Below I will briefly introduce the concepts, features, advantages and disadvantages, and sample code of these five models.

Blocking IO

Blocking IO is the most traditional IO model. Its characteristic is that blocking occurs during the process of reading and writing data. When the user process issues an IO request, the kernel will check whether the data is ready. If not, it will wait for the data to be ready, and the user process will be blocked and the user process will hand over the CPU. When the data is ready, the kernel will copy the data to the user process and return the result to the user process, and then the user process will unblock the state.

The advantages of blocking IO are simple implementation, convenient programming, and no additional processing logic is required. The disadvantage of blocking IO is low efficiency. The user process cannot perform other tasks while waiting for data, which wastes CPU resources. Blocking IO is more suitable for scenarios with small data volume and short response time.

Here is an example code using blocking IO, which reads a line of data from standard input and prints it:

#include <stdio.h>
#include <unistd.h>

int main()
{
    char buf[1024];
    int n;
    n = read(STDIN_FILENO, buf, 1024); //Blocking IO, waiting for user input
    if (n < 0)
    {
        perror("read error");
        return -1;
    }
    printf("read %d bytes: %s\\
", n, buf); //Print the read data
    return 0;
}

operation result:

hello
read 6 bytes: hello

Non-blocking IO

Non-blocking IO is an improved IO model, which is characterized by no blocking during the process of reading and writing data. When the user process initiates a read operation, there is no need to wait, but a result is obtained immediately. If the result is an error, it knows that the data is not ready yet, so it can send the read operation again. Once the data in the kernel is ready and a request from the user process is received again, it immediately copies the data to the user process and then returns.

The advantage of non-blocking IO is that the user process will not be blocked and can continue to perform other tasks, improving CPU utilization. The disadvantage of non-blocking IO is that the user process needs to constantly ask the kernel whether the data is ready, which will lead to a very high CPU usage, complicated implementation, and difficult programming. Non-blocking IO is more suitable for scenarios with large amounts of data and long response times.

Here is an example code using non-blocking IO, which reads a line of data from standard input and prints it:

#include <stdio.h>
#include <unistd.h>
#include <fcntl.h>
#include <errno.h>

int main()
{
    char buf[1024];
    int n;
    int flags;
    flags = fcntl(STDIN_FILENO, F_GETFL); //Get the flags of the standard input file descriptor
    flags |= O_NONBLOCK; //Set the non-blocking flag bit
    fcntl(STDIN_FILENO, F_SETFL, flags); //Set the flags of the standard input file descriptor
    while (1)
    {
        n = read(STDIN_FILENO, buf, 1024); //Non-blocking IO, return results immediately
        if (n < 0)
        {
            if (errno == EAGAIN) //If the error code is EAGAIN, it means the data is not ready yet
            {
                printf("no data available\\
"); //Print prompt information
                sleep(1); //Sleep for 1 second and simulate other tasks
                continue; //Continue trying to read data
            }
            else //If the error code is another value, it means that another error has occurred
            {
                perror("read error"); //Print error message
                return -1;
            }
        }
        printf("read %d bytes: %s\\
", n, buf); //Print the read data
        break; //break out of the loop
    }
    return 0;
}

operation result:

no data available
no data available
no data available
hello
read 6 bytes: hello

IO multiplexing

IO multiplexing is an efficient IO model. Its characteristic is that it can monitor multiple file descriptors at the same time, improving the application’s ability to manage input and output operations. When a user process uses system calls such as select, poll, or epoll, it will pass the file descriptor that needs to be monitored to the kernel, and then the kernel will look for ready file descriptors among all file descriptors and return them to the user process. The user process then performs corresponding read and write operations based on the ready file descriptor.

The advantage of IO multiplexing is that one thread or process can be used to process multiple file descriptors, which avoids the overhead of multi-threads or multi-processes and improves the concurrency and scalability of the system. The disadvantage of IO multiplexing is that additional system calls are required to manage file descriptors, and it is still blocked during the data copy phase. IO multiplexing is more suitable for network programming, such as concurrent processing on the server side.

Sample code that uses select to monitor the status of standard input and a TCP socket, and perform read and write operations based on the status. code show as below:

#include <stdio.h>
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <sys/select.h>

#define PORT 8888 //Define server port number
#define MAXLINE 1024 //Define buffer size

int main()
{
    int sockfd; //Define socket file descriptor
    int nready; //Define the number of ready file descriptors
    int n; //Define the number of bytes to read and write
    char buf[MAXLINE]; //Define buffer
    struct sockaddr_in servaddr; //Define server address structure
    fd_set rset; //Define read set
    int maxfd; //Define the largest file descriptor

    sockfd = socket(AF_INET, SOCK_STREAM, 0); //Create a TCP socket
    if (sockfd < 0)
    {
        perror("socket error");
        return -1;
    }

    servaddr.sin_family = AF_INET; //Set the address family to IPv4
    servaddr.sin_port = htons(PORT); //Set the port number to 8888, pay attention to using network byte order
    servaddr.sin_addr.s_addr = inet_addr("127.0.0.1"); //Set the server IP address as the local loopback address, pay attention to using network byte order

    if (connect(sockfd, (struct sockaddr *) & amp;servaddr, sizeof(servaddr)) < 0) //Connect to the server
    {
        perror("connect error");
        return -1;
    }

    printf("connect to server success\\
");

    FD_ZERO( & amp;rset); //Clear the read collection

    while (1)
    {
        FD_SET(STDIN_FILENO, & amp;rset); //Add standard input to the read set
        FD_SET(sockfd, & amp;rset); //Add the socket to the read set
        maxfd = (STDIN_FILENO > sockfd) ? STDIN_FILENO : sockfd; //Calculate the largest file descriptor

        nready = select(maxfd + 1, & amp;rset, NULL, NULL, NULL); //Wait for the file descriptors in the read set to be ready and return the number of ready file descriptors. NULL means waiting indefinitely
        if (nready < 0)
        {
            perror("select error");
            return -1;
        }

        if (FD_ISSET(STDIN_FILENO, & amp;rset)) //If the standard input is ready
        {
            n = read(STDIN_FILENO, buf, MAXLINE); //Read data from standard input
            if (n < 0)
            {
                perror("read error");
                return -1;
            }
            else if (n == 0) //If it reaches the end of the file, it means the user entered Ctrl + D
            {
                printf("exit\\
");
                break; //Jump out of the loop and end the program
            }
            write(sockfd, buf, n); //Write the read data into the socket and send it to the server
        }

        if (FD_ISSET(sockfd, & amp;rset)) //If the socket is ready
        {
            n = read(sockfd, buf, MAXLINE); //Read data from the socket
            if (n < 0)
            {
                perror("read error");
                return -1;
            }
            else if (n == 0) //If the end of the file is read, it means that the server has closed the connection
            {
                printf("server closed\\
");
                break; //Jump out of the loop and end the program
            }
            write(STDOUT_FILENO, buf, n); //Write the read data to the standard output and display it to the user
        }
    }

    close(sockfd); //Close the socket
    return 0;
}

The function of this sample code is to implement a simple echo client, which can perform two-way communication with the server. The user can input data in the standard input and then send it to the server. The server will return the data as it is, and the client will display the data in the standard input. output. At the same time, the client can also receive data actively sent by the server and display it on the standard output. The client exits when the user enters Ctrl+D or the server closes the connection.

The key point of this sample code is to use the select function to monitor the status of standard input and sockets at the same time, and then perform read and write operations based on the status. This can avoid the waiting problem when using blocking IO and improve the efficiency and responsiveness of the program.

Signal-driven IO

Signal-driven IO is a signal-based IO model. Its characteristic is that it allows the user process to receive a signal when the data is ready, and then perform read and write operations. When a user process uses a system call such as sigaction, it tells the kernel to send it a SIGIO signal when the data is ready and specifies a signal handling function. When the kernel detects that the data is ready, it sends a SIGIO signal to the user process and executes the signal processing function specified by the user process. The user process can perform read and write operations in the signal processing function.

The advantage of signal-driven IO is that the user process does not need to actively poll whether the data is ready, but passively receives notifications from the kernel, saving CPU resources. The disadvantage of signal-driven IO is the complexity and unreliability of the signal mechanism itself, and it is still blocked during the data copy phase. Signal-driven IO is more suitable for scenarios with high real-time requirements.

The following is a sample code that uses signal-driven IO. It uses sigaction to register a signal processing function. When the standard input has data ready, it receives the SIGIO signal, reads the data in the signal processing function and prints it out:

#include <stdio.h>
#include <unistd.h>
#include <fcntl.h>
#include <signal.h>

void sigio_handler(int signo) //Signal processing function
{
    char buf[1024];
    int n;
    n = read(STDIN_FILENO, buf, 1024); //Read data from standard input
    if (n < 0)
    {
        perror("read error");
        return;
    }
    printf("read %d bytes: %s\\
", n, buf); //Print the read data
}

int main()
{
    int flags;
    struct sigaction act; //Structure used to set signal processing function
    act.sa_handler = sigio_handler; //Set the signal processing function to sigio_handler
    act.sa_flags = 0; //Set the signal processing flag bit to 0
    sigemptyset( & amp;act.sa_mask); //Clear the signal mask set
    if (sigaction(SIGIO, & amp;act, NULL) < 0) //Register the SIGIO signal processing function
    {
        perror("sigaction error");
        return -1;
    }

    flags = fcntl(STDIN_FILENO, F_GETFL); //Get the flags of the standard input file descriptor
    flags |= O_ASYNC; //Set the asynchronous flag bit
    fcntl(STDIN_FILENO, F_SETFL, flags); //Set the flags of the standard input file descriptor
    fcntl(STDIN_FILENO, F_SETOWN, getpid()); //Set the owner of the standard input file descriptor to the current process

    while (1) //Loop waiting for the signal to arrive
    {
        pause(); //Pause the process until a signal is received
    }
    return 0;
}

operation result:

hello
read 6 bytes: hello

Asynchronous IO

Asynchronous IO is the most advanced IO model. Its characteristic is that it allows the user process to receive a notification when the data copy is completed, and then perform read and write operations. When a user process uses a system call such as aio_read, it will tell the kernel to send it a signal or execute a callback function when the data copy is completed, and specify an asynchronous IO control block. When the kernel detects that the data is ready, it copies the data to the buffer specified by the user process and sends a signal to the user process or executes a callback function. The user process can directly access the data in the buffer in the signal processing function or callback function.

The advantage of asynchronous IO is that the user process does not need to wait for the copy of the data, but is notified only after the copy of the data is completed. This can minimize the blocking time of the user process and improve the efficiency and performance of IO. The disadvantage of asynchronous IO is that the implementation is very complex, programming is difficult, and not all operating systems and file systems support asynchronous IO. Asynchronous IO is more suitable for scenarios that have strict requirements on IO throughput and latency.

The following is a sample code using asynchronous IO. It uses aio_read to initiate an asynchronous read operation. When the standard input data copy is completed, it receives the SIGUSR1 signal, accesses the data in the buffer in the signal processing function and prints it out:

#include <stdio.h>
#include <unistd.h>
#include <fcntl.h>
#include <signal.h>
#include <aio.h>

struct aiocb cb; //Asynchronous IO control block

void aio_handler(int signo, siginfo_t *info, void *context) //Signal processing function
{
    if (info->si_signo == SIGUSR1) //If the signal is SIGUSR1
    {
        printf("read %d bytes: %s\\
", cb.aio_nbytes, (char *)cb.aio_buf); //Print the read data
    }
}

int main()
{
    char buf[1024]; //buffer
    struct sigaction act; //Structure used to set signal processing function
    act.sa_sigaction = aio_handler; //Set the signal processing function to aio_handler
    act.sa_flags = SA_SIGINFO; //Set the signal processing flag bit to SA_SIGINFO, indicating to use sa_sigaction instead of sa_handler
    sigemptyset( & amp;act.sa_mask); //Clear the signal mask set
    if (sigaction(SIGUSR1, & amp;act, NULL) < 0) //Register the processing function of SIGUSR1 signal
    {
        perror("sigaction error");
        return -1;
    }

    cb.aio_fildes = STDIN_FILENO; //Set the file descriptor of asynchronous IO to standard input
    cb.aio_buf = buf; //Set the asynchronous IO buffer to buf
    cb.aio_nbytes = 1024; //Set the number of bytes of asynchronous IO to 1024
    cb.aio_offset = 0; //Set the offset of asynchronous IO to 0
    cb.aio_sigevent.sigev_notify = SIGEV_SIGNAL; //Set the notification method of asynchronous IO to signal
    cb.aio_sigevent.sigev_signo = SIGUSR1; //Set the asynchronous IO notification signal to SIGUSR1
    cb.aio_sigevent.sigev_value.sival_ptr = & amp;cb; //Set the additional information of the asynchronous IO notification signal to the address of the asynchronous IO control block

    if (aio_read( & amp;cb) < 0) //Initiate an asynchronous read operation
    {
        perror("aio_read error");
        return -1;
    }

    while (1) //Loop waiting for the signal to arrive
    {
        pause(); //Pause the process until a signal is received
    }
    return 0;
}

operation result:

hello
read 6 bytes: hello

The knowledge points of the article match the official knowledge files, and you can further learn related knowledge. Algorithm skill tree Home page Overview 57340 people are learning the system