Just one thing about Linux asynchronous I/O-AIO

Posted by suneel on Fri, 17 May 2019 08:29:12 +0200

Article directory


Generally speaking, asynchrony can be divided into POSIX asynchrony and kernel asynchrony.

  • POSIX asynchrony is a poor version of asynchronous implementation. The main method is to clone the current process with library function aio_read/aio_write, and let the child process call synchronous read/write. Then the parent process ends the aio_read/aio_write function and continues to execute. Therefore, there is no need to wait for the synchronization operation of the child process to complete. The kernel achieves real asynchrony. POSIX is much slower than the kernel asynchrony.
  • Direct IO, or O_DIRECT, is supported only, and system caching is not used. If you want to use it, only programmers can implement it themselves! It really takes advantage of the asynchronous working characteristics of CPU and IO devices (IO request submission process is mainly completed synchronously on the caller thread, after the request submission, because the CPU and IO device can work in parallel, so the call process can be returned, and the caller can continue to do other things)

API function

struct aiocb

/* Asynchronous I/O control block.  */
struct aiocb
{
  int aio_fildes;		/* File desriptor.  */
  int aio_lio_opcode;		/* The operation to be performed.  */
  int aio_reqprio;		/* Request priority offset.  */
  volatile void *aio_buf;	/* Location of buffer.  */
  size_t aio_nbytes;		/* Length of transfer.  */
  struct sigevent aio_sigevent;	/* Signal number and value.  */
   // The sigevent structure tells AIO what to do when I/O operations are complete 
   
  /* Internal members.  */
  struct aiocb *__next_prio;
  int __abs_prio;
  int __policy;
  int __error_code;
  __ssize_t __return_value;

#ifndef __USE_FILE_OFFSET64
  __off_t aio_offset;		/* File offset.  */
  char __pad[sizeof (__off64_t) - sizeof (__off_t)];
#else
  __off64_t aio_offset;		/* File offset.  */
#endif
  char __glibc_reserved[32];
};

Use examples of aio_read (similar to aio_write)

#include <iostream>
#include <aio.h>
#include <bits/stdc++.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>

const int BUFSIZE = 1024;

using namespace std;
int main(void)
{
    struct aiocb my_aiocb;
    int fd = open("file.txt", O_RDONLY);
    if (fd < 0)
        perror("open");

    bzero((char *)&my_aiocb, sizeof(my_aiocb));

    my_aiocb.aio_buf = malloc(BUFSIZE);
    if (!my_aiocb.aio_buf)
        perror("my_aiocb.aio_buf");

    my_aiocb.aio_fildes = fd;
    my_aiocb.aio_nbytes = BUFSIZE;
    my_aiocb.aio_offset = 0;

    int ret = aio_read(&my_aiocb);
    if (ret < 0)
        perror("aio_read");

    //See if the transmission is started correctly, and if it fails, it may need to be called again to wait for processing. (After the connection is established, the getsockerror principle is the same.)
    //EINPROGRESS indicates processing
    //If you remove it, you will find that no data has been read, indicating that the reading is asynchronous!!!
    for (int i = 1; aio_error(&my_aiocb) == EINPROGRESS; i++)
    {
        cout << i << endl;
    }
    /*A more general way of writing should be
    
    if(If so, go to the following steps
    
    */
    
	
    ret = aio_return(&my_aiocb);
    if (ret > 0)
    {
        cout << ret << endl;
        cout << (char *)my_aiocb.aio_buf << endl; //Note that there has to be a change here.
    }
    else
        perror("aio_return");
    close(fd);
    free((void *)my_aiocb.aio_buf);
    return 0;
}


The essential implementation of aio_read:

Create a thread, and then the thread actually calls the synchronous read/write function, and the thread exits after processing is completed.

aio_suspend asynchronous blocking IO

The aio_suspend function suspends (or blocks) the calling process until the asynchronous request is completed, which generates a signal or other timeout operation. The caller provides a list of aiocb references, any completion of which results in aio_suspend returning

I don't think it's really helpful because I use asynchronous IO to avoid blocking, but it's strange that you can cause process blocking. I'll talk about it later.

lio_listio initiates multiple asynchronous IO requests at the same time

This means that we can start a large number of I/O operations in a system call (a kernel context switch). From a performance point of view, this is very important.

int lio_listio(int mode, struct aiocb *const aiocb_list[],
                      int nitems, struct sigevent *sevp);
  • The first parameter mode: LIO_WAIT (blocking until all IOs are completed) or LIO_NOWAIT (not blocking, returning after entering the queue)
  • The second parameter list: Asynchronous IO request queue.
  • Third parameter nitems: Asynchronous IO request queue length
  • The fourth parameter, sevp, defines the method of generating signals when all I/O operations are completed.
#include <iostream>
#include <aio.h>
#include <bits/stdc++.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
using namespace std;

const int BUFSIZE = 1024;

//Asynchronous reading
void aio_read_file(struct aiocb *cbp, int fd, int size)
{
    bzero(cbp, sizeof(cbp));

    cbp->aio_buf = malloc(size + 1);
    cbp->aio_nbytes = size;
    cbp->aio_offset = 0;
    cbp->aio_fildes = fd;
    cbp->aio_lio_opcode = LIO_READ;
}
int main(void)
{
    //Asynchronous request list
    struct aiocb *io_list[2];
    struct aiocb cbp1, cbp2;

    int fd1 = open("test.txt", O_RDONLY);
    if (fd1 < 0)
    {
        perror("open error\n");
    }
    aio_read_file(&cbp1, fd1, BUFSIZE);

    int fd2 = open("test.txt", O_RDONLY);
    if (fd2 < 0)
    {
        perror("open error\n");
    }
    aio_read_file(&cbp2, fd2, BUFSIZE * 4);

    io_list[0] = &cbp1;
    io_list[1] = &cbp2;

    lio_listio(LIO_NOWAIT, io_list, 2, NULL);

    //sleep(1); If not, asynchronous IO may not be completed, resulting in no output

    cout << (char *)cbp1.aio_buf << endl;
    cout << "1111111111111111111111111111" << endl
         << endl
         << endl;
    cout << (char *)cbp1.aio_buf << endl;
}

For read operations, the value of aio_lio_opcode field is LIO_READ.
For write operations, we use LIO_WRITE.
However, LIO_NOP is also effective for not performing operations.

Asynchronous IO notification mechanism

The difference between asynchronization and synchronization is that we can continue to do other things without waiting for the asynchronization operation to return. When the asynchronization operation is completed, we can notify us to process it.

There are two main forms of notification:

  • signal
  • Function Callback

(1) Signaling

When making an asynchronous request, you can specify what signal to send to the calling process when the asynchronous operation is completed, so that the call will execute the corresponding signal processing function when it receives the signal. Obviously, this is not good, because there may be many signals in the system, and it must be noted that there must be no blocking operation in signal processing, otherwise it will be. Causing blockage (interruption) of the entire process

#include <iostream>
#include <aio.h>
#include <bits/stdc++.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <signal.h>

const int BUFSIZE = 1024;

using namespace std;
void aio_completion_handler(int signo, siginfo_t *info, void *text)
{
    struct aiocb *req;

    if (info->si_signo == SIGIO)
    {
        req = (struct aiocb *)info->si_value.sival_ptr; //Pay attention here.
        if (aio_error(req) == 0)
        {
            int ret = aio_return(req);
            cout << "ret == " << ret << endl;
            cout << (char *)req->aio_buf << endl;
        }
    }
    close(req->aio_fildes);
    free((void *)req->aio_buf);
    while (1)
    {
        printf("The callback function is being executed...\n");
        sleep(1);
    }
}
int main(void)
{
    struct aiocb my_aiocb;
    struct sigaction sig_act;
    /*set signal handler */
    sigemptyset(&sig_act.sa_mask);
    /*sa_flags The addition of the option SA_SIGINFO simply means that a siginfo_t* type parameter is attached to the signal processing.*/
    sig_act.sa_flags = SA_SIGINFO;
    sig_act.sa_sigaction = aio_completion_handler;

    int fd = open("file.txt", O_RDONLY);
    if (fd < 0)
        perror("open");
    bzero((char *)&my_aiocb, sizeof(my_aiocb));

    my_aiocb.aio_buf = malloc(BUFSIZE);
    if (!my_aiocb.aio_buf)
        perror("my_aiocb.aio_buf");

    my_aiocb.aio_fildes = fd;
    my_aiocb.aio_nbytes = BUFSIZE;
    my_aiocb.aio_offset = 0;
    //Loading signal information
    my_aiocb.aio_sigevent.sigev_notify = SIGEV_SIGNAL;
    my_aiocb.aio_sigevent.sigev_signo = SIGIO;
    my_aiocb.aio_sigevent.sigev_value.sival_ptr = &my_aiocb;

    /*Map the Signal to the Signal Handler*/
    int ret = sigaction(SIGIO, &sig_act, NULL);

    ret = aio_read(&my_aiocb);
    if (ret < 0)
        perror("aio_read");

    //The calling process continues to execute
    while (1)
    {
        printf("The main thread continues to execute...\n");
        sleep(1);
    }

    return 0;
}

sigaction can pass parameters and will be used later, not the signal function

(2) Thread function callback

As the name implies,... Is the callback function we often write (it will open another thread to process, so it will not block the current process)

#include <iostream>
#include <aio.h>
#include <bits/stdc++.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <signal.h>

const int BUFSIZE = 1024;

using namespace std;
void aio_completion_handler(sigval_t sigval)
{
    struct aiocb *req;

    req = (struct aiocb *)sigval.sival_ptr; //Pay attention here.
    /*Check again if the asynchrony is complete?*/
    if (aio_error(req) == 0)
    {
        int ret = aio_return(req);
        cout << "ret == " << ret << endl;
        cout << (char *)req->aio_buf << endl;
    }
    close(req->aio_fildes);
    free((void *)req->aio_buf);
    while (1)
    {
        printf("The callback function is being executed...\n");
        sleep(1);
    }
}
int main(void)
{
    struct aiocb my_aiocb;
    int fd = open("file.txt", O_RDONLY);
    if (fd < 0)
        perror("open");
    bzero((char *)&my_aiocb, sizeof(my_aiocb));

    my_aiocb.aio_buf = malloc(BUFSIZE);
    if (!my_aiocb.aio_buf)
        perror("my_aiocb.aio_buf");

    my_aiocb.aio_fildes = fd;
    my_aiocb.aio_nbytes = BUFSIZE;
    my_aiocb.aio_offset = 0;

    //Fill in callback information
    /*
    Using SIGEV_THREAD to request a thread callback function as a notification method
    */
    my_aiocb.aio_sigevent.sigev_notify = SIGEV_THREAD;
    my_aiocb.aio_sigevent.sigev_notify_function = aio_completion_handler;
    my_aiocb.aio_sigevent.sigev_notify_attributes = NULL;
    /*
    The context to be transmitted is loaded into the handler (in this case, a reference to the aiocb request itself).
    In this handler, we simply refer to the arrived sigval pointer and use the AIO function to verify that the request has been completed.
    */
    my_aiocb.aio_sigevent.sigev_value.sival_ptr = &my_aiocb;

    int ret = aio_read(&my_aiocb);
    if (ret < 0)
        perror("aio_read");

    //The calling process continues to execute
    while (1)
    {
        printf("The main thread continues to execute...\n");
        sleep(1);
    }
    return 0;
}

Asynchronous IO-libio in Linux 2.6

API function

Asynchronous IO Environment and Implementation Principle

Asynchronous IO environment

It is a set of data structures used to track the operation of asynchronous IO operations for process requests.

kioctx object

Each AIO environment is associated with a kioctx object that stores all the information related to the environment.

A process can create multiple AIO environments. All kioctx objects corresponding to a process are stored in a single linked list, which is stored in mm_struct.

#endif
#ifdef CONFIG_AIO
		spinlock_t			ioctx_lock;
		struct kioctx_table __rcu	*ioctx_table;
#endif

AIO ring

A memory buffer in the virtual address space of user-mode processes (in fact, in the virtual address Space-On disk) is also accessed by all kernel-mode processes.

  • The ring_info.mmap_base and ring_info.mmap_size in the kioctx object naturally point to the starting address and length of the AIO ring
  • The ring_info.ring_pages field stores an array pointer that points to all page boxes (physical memory) with AIO rings.

The AIO ring is actually a ring buffer that the kernel uses to write a report on the completion of the running asynchronous IO.

The first byte of the AIO ring has a header (struct rio_ring structure), and the rest are io_event data structures, each representing an asynchronous IO operation that has been completed.
 
Pages in AIO rings are mapped to user-mode address spaces, so applications can directly check asynchronous IO operations and avoid system calls.

io_setup creates an AIO environment for the calling process. The kernel allocates a piece of memory in the corresponding user address space through mmap, and then creates and initializes kioctx objects of the AIO environment. The location and size of the mapping are described by mmap_base and mmap_size in aio_ring_info structure, and the physical memory page information actually allocated is described by ring_pages. Asynchronous IO completes the mapping. After that, the kernel writes the results of the asynchronous IO to it

int io_setup(unsigned nr_events, aio_context_t *ctx_idp);
// 1. nr_events: The maximum number of asynchronous IO operations running determines the size of the AIO ring
// 2. A pointer to the storage environment handle, the base address of the AIO ring

Use examples:

Ensure that libaio and libaio-devel/libaio-dev are installed

#include <iostream>
#include <aio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <libaio.h>
#include <errno.h>
#include <stdio.h>
#include <stdlib.h>

const int BUFSIZE = 1024 * 1024;
const int MAX_EVENTS = 32;

using namespace std;

/*
- Or use it this way
inline static int io_submit(aio_context_t ctx, long nr, struct iocb **iocbpp)
{
	return syscall(__NR_io_submit, ctx, nr, iocbpp);
}
*/
int main(void)
{
    io_context_t ctx = {0};
    //1. Initialize io_context_t
    int ret = io_setup(MAX_EVENTS, &ctx);
    if (ret != 0)
        perror("io_setup");
    //2.open file acquisition fd
    int fd = open("file.txt", O_RDWR | O_DIRECT);
    if (fd < 0)
    {
        io_destroy(ctx);
        perror("open");
    }
    struct iocb my_iocb;
    struct iocb *my_iocb_ptr = &my_iocb;

    void *buf = NULL;
    /*For larger boundaries, such as pages, programmers need to align dynamically. Although motivations are varied,
    But the most common is the alignment of direct block I/O caches or other software-to-hardware interactions.*/
    // Note: With O_DIRECT, alignment is required.
    //Get page size
    int pagesize = sysconf(_SC_PAGESIZE);
    //Processing alignment
    posix_memalign(&buf, pagesize, BUFSIZE);
    memset(buf, 'L', BUFSIZE);
    struct io_event e[10];
    //3. Establishing iocb based on fd, buffer offset and other information
    int n = MAX_EVENTS;
    while (n--) //32 cycles
    {
        io_prep_pwrite(&my_iocb, fd, buf, BUFSIZE, 0);
        //The structure of iocb is generated by io_prep_pwrite and io_prep_pread as parameters of io_submit.
        // This structure specifies the read and write type, start sector, length, and device identifier.
        if (io_submit(ctx, 1, &my_iocb_ptr) != 1) //Multiple asynchronous IO s can also be initiated
        {
            io_destroy(ctx);
            perror("io_submit");
        }
        int ret = io_getevents(ctx, 1, 10, e, NULL);
        if (ret < 1)
        {
            perror("io_getevents");
            break;
        }
        cout << "io_getevents :ret  " << ret << endl;
    }
    close(fd);
    io_destroy(ctx);
    return 0;
}

Reference: (Must Read!!!!
Linux Asynchronous IO + Instances (POSIX IO and libaio)
Using asynchronous I/O greatly improves application performance
Introduction and Implementation Principle of Asynchronous IO Implemented by linux AIO-libaio

linux aio

Asynchronous synchronization is in terms of notification mechanism
Blocking non-blocking is a matter of program execution.

Topics: Linux iOS REST