"Principle and Application of Asynchronous IO" of Linux Network I/O Model

1. Why have asynchronous I/O

Compared with synchronous IO, asynchronous IO will not block the execution of the current program and can continue to execute downward. That is, when the application initiates an IO operation, the caller will not get the result immediately, but will notify the caller through a signal or callback after the kernel completes the IO operation.

2. Signal-driven I/O

Signal-driven IO is an implementation of asynchronous IO. In asynchronous IO, when an I/O operation can be performed on a file descriptor, a process can request the kernel to send a signal for itself. The process can then perform any other tasks until the file descriptor becomes available for I/O, at which point the kernel sends a signal to the process.

To use the signal driver, the program needs to be executed according to the following steps:

  • Enable non-blocking I/O by specifying the O_NONBLOCK flag
  • Enable asynchronous I/O by specifying the O_ASYNC flag
  • By setting the receiving process for asynchronous I/O time. A signal is sent to the process when an I/O operation can be performed on the file descriptor.
  • Register a signal handler for notification signals sent by the kernel. Asynchronous signal I/O defaults to SIGIO, so the kernel sends the signal SIGIO to the process.

After the above steps are completed, the process can perform other tasks. When the I/O is ready, the kernel will send a SIGIO signal to the process. When the process receives the signal, it will execute the signal processing function with the pre-registered number. The I/O operation is performed in the signal processing function

1. Enable O_ASYNC

Asynchronous I/O cannot be enabled by specifying the O_ASYNC flag when calling open, but I/O can be enabled by adding the O_ASYNC flag through the fcntl() function:

int flag;

flag = fcntl(fd,F_GETFL);     // First get the original flag from the open file descriptor
flag |= O_ASYNC;              // Add O_ASYNC flag to flags
fcntl(fd,F_SETFL,flag);       // Reset the flag

2. Set the receiving process of asynchronous I/O time

Set the receiving process of the asynchronous I/O time for the file descriptor, that is, set the owner of the asynchronous I/O:

fcntl(fd,F_SETOWN,getpid());   // You can also pass in the pid of other processes

3. Register the processing function of the SIGIO signal

Register a signal processing function for the SIGIO signal through the signal() or sigaction() function. When the process receives the SIGIO signal sent by the kernel, the function will be executed.

Code example:

#define _GNU_SOURCE // F_SETSIG
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <signal.h>

#define MOUSE "/dev/input/mouse0"

static int fd;

static void sigio_handler(int sig)
{
    static int loops = 5;
    char buf[100] = {0};
    int ret;

    if(SIGIO != sig)
    {
        return ;
    }

    ret = read(fd,buf,sizeof(buf));
    if(0 < ret)
        printf("mouse : read %d bytes\n",ret);

    loops--;
    if(0>=loops)
    {
        close(fd);
        exit(0);
    }
}

int main(void)
{
    int flag;

    // Open the device and enable non-blocking IO
    fd = open(MOUSE,O_RDONLY|O_NONBLOCK);
    if(-1 == fd)
    {
        perror("open mouse error");
        exit(-1);
    }

    // Enable asynchronous IO
    flag = fcntl(fd,F_GETFL);
    flag |= O_ASYNC;
    fcntl(fd,F_SETFL,flag);

    // Set the owner of asynchronous IO
    fcntl(fd,F_SETOWN,getpid());

    // Register signal callback function
    signal(SIGIO,sigio_handler);
    
    for(;;)
    {
        sleep(1);
    }
}

operation result:

But using the default signal SIGIO will have some problems. SIGIO is a standard signal, unreliable signal, non-real-time signal, does not support the signal queuing mechanism, does not know what happened to the file descriptor, and does not judge whether the file descriptor is readable or not. state, so further optimization is required (real-time signal replacement).

1. Replace the default signal SIGIO with a real-time signal

For example, use the SIGRTMIN signal instead of SIGIO, such as:

fcntl(fd,F_SETSIG,SIGRTMIN);

2. Use the sigaction() function to register the signal processing function

In the application, the signal processing function needs to be registered for the real-time signal, and the sigaction function is used for registration. The sigaction prototype:

#include <signal.h>

int sigaction(int signum,const struct sigaction *act,struct sigaction *oldact);

Example of use:

#define _GNU_SOURCE // F_SETSIG
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <signal.h>

#define MOUSE "/dev/input/mouse0"

static int fd;

static void io_handler(int sig,siginfo_t *info,void *context)
{
    static int loops = 5;
    char buf[100] = {0};
    int ret;

    if(SIGRTMIN != sig)
    {
        return ;
    }

    // Determine whether the mouse is readable
    if(POLL_IN == info->si_code)
    {
        ret = read(fd,buf,sizeof(buf));
        if(0 < ret)
        {
            printf("mouse : read %d bytes\n",ret);
        }

        loops--;
        if(0>=loops)
        {
            close(fd);
            exit(0);
        }
    }
}

int main(void)
{
    struct sigaction act;
    int flag;

    // Open the device and enable non-blocking IO
    fd = open(MOUSE,O_RDONLY|O_NONBLOCK);
    if(-1 == fd)
    {
        perror("open mouse error");
        exit(-1);
    }

    // Enable asynchronous IO
    flag = fcntl(fd,F_GETFL);
    flag |= O_ASYNC;
    fcntl(fd,F_SETFL,flag);

    // Set the owner of asynchronous IO
    fcntl(fd,F_SETOWN,getpid());

    // Specify the real-time signal SIGRTMIN as an asynchronous I/O notification signal
    fcntl(fd,F_SETSIG,SIGRTMIN);

    // Register a signal handler for the real-time signal SIGRTMIN
    act.sa_sigaction = io_handler;
    act.sa_flags = SA_SIGINFO;
    sigemptyset(&act.sa_mask);
    sigaction(SIGRTMIN,&act,NULL);

    for(;;)
    {
        sleep(1);
    }
}

operation result:

 

3. Linux Asynchronous I/O - Native AIO

Linux Native AIO is the native AIO supported by Linux, and many third-party asynchronous IO libraries, such as libeio and glibc AIO. Many three-party library asynchronous IO libraries are not real asynchronous IO, but simulate asynchronous IO through multithreading, such as libeio.

The aio_* series of calls are provided by glibc, which is simulated by glibc with thread + blocking calls, and the performance is poor. In order to control the io behavior more, you can use a lower-level libaio.

Ubuntu install livaio:

sudo apt install libaio-dev

Linux AIO execution process:

Linux native AIO processing flow:

  • When the application calls the io_submit system call to initiate an asynchronous IO operation, recall that the kernel's IO task queue adds an IO task and returns success.
  • The kernel will process the IO tasks in the IO task queue in the background, and then store the processing results in the IO tasks
  • Applications can call io_getevents

As can be seen from the above process, the Linux asynchronous IO operation mainly consists of two steps:

  • 1) Call the io_submit function to initiate an asynchronous IO operation
  • 2) Call the io_getevents function to get the result of asynchronous IO

Example code:

#define _GNU_SOURCE
#include <stdlib.h>
#include <string.h>
#include <errno.h>
#include <stdio.h>
#include <unistd.h>
#include <fcntl.h>
#include <libaio.h>

#define FILEPATH "./aio.txt"

int main()
{
io_context_t context; // context of asynchronous IO
    struct iocb io[1],*p[1] = {&io[0]};
    struct io_event e[1];
    unsigned nr_events = 10;
    struct timespec timeout;
    char *wbuf;
    int wbuflen = 1024;
    int ret,num=0,i;

    posix_memalign((void **)&wbuf,512,wbuflen);

    memset(wbuf,'@',wbuflen);
    memset(&context,0,sizeof(io_context_t));

    timeout.tv_sec = 0;
    timeout.tv_nsec = 10000000;

// 1. Open the file for asynchronous IO
    int fd = open(FILEPATH,O_CREAT | O_RDWR | O_DIRECT,0644);
    if (fd < 0) {
        printf("open error: %d\n", errno);
        return 0;
    }

// 2. Create an asynchronous IO context
    if(0 != io_setup(nr_events,&context))
    {
        printf("io_setup error: %d\n", errno);
        return 0;
    }

// 3. Create an asynchronous IO task
    io_prep_pwrite(&io[0],fd,wbuf,wbuflen,0);

// 4. Submit an asynchronous IO task
    if((ret = io_submit(context,1,p)) != 1)
    {
        printf("io_submit error: %d\n", ret);
        io_destroy(context);
        return -1;
    }

// 5. Get the result of asynchronous IO
    while(1)
    {
        ret = io_getevents(context,1,1,e,&timeout);
        if (ret < 0) {
            printf("io_getevents error: %d\n", ret);
            break;
        }

        if (ret > 0) {
            printf("result, res2: %d, res: %d\n", e[0].res2, e[0].res);
            break;
        }
    }
    return 0;
}

Compile command:

cc aio_demo.c -laio

operation result:

There will be an aio.txt file in the directory, the content is 1024 @ characters

Program description:

  • Open the file for asynchronous IO by calling the open system call, the AIO operation must set the O_DIRECT direct IO flag bit
  • Call the io_setup system call to create an asynchronous IO context
  • Call the io_prep_pwrite or io_prep_pread function to create an asynchronous write or asynchronous read task
  • Call the io_submit system call to submit asynchronous IO to the kernel
  • Call the io_getevents system call to get the result of asynchronous IO

The above example uses while detection. You can also use epoll combined with eventfd and event-driven to obtain the results of asynchronous IO operations.

Tags: Linux C++ Qt server

Posted by messer on Tue, 28 Feb 2023 04:32:21 +0530