Asynchronous IO + asynchronous notification

Hits: 0

Linux Kernel Notes (4) Advanced I/O Operations (2)

Analyze the first code:
Line 50 of the code struct aiocb aiow, aior;
defines two asynchronous I/O control blocks for reading and writing, respectively

Lines 56 to 76 of the code initialize these two control blocks.

memset (&aiow, 0 , sizeof (aiow));
    memset (&greater, 0 , sizeof (greater));

    aiow.aio_fildes = fd;
    aiow.aio_buf = malloc(32);
    strcpy((char *)aiow.aio_buf, "aio test");
    aiow.aio_nbytes = strlen((char *)aiow.aio_buf) + 1;
    aiow.aio_offset = 0 ;
    aiow.aio_sigevent.sigev_notify = SIGEV_THREAD;
    aiow.aio_sigevent.sigev_notify_function = aiow_completion_handler;
    aiow.aio_sigevent.sigev_notify_attributes = NULL;
    aiow.aio_sigevent.sigev_value.sival_ptr = &aiow;

    aior.aio_fildes = fd;
    aior.aio_buf = malloc ( 32 );
    aior.aio_nbytes = 32 ;
    aior.aio_offset = 0 ;
    aior.aio_sigevent.sigev_notify = SIGEV_THREAD;
    aior.aio_sigevent.sigev_notify_function = aior_completion_handler;
    aior.aio_sigevent.sigev_notify_attributes = NULL;
    aior.aio_sigevent.sigev_value.sival_ptr = &aior;

Mainly file descriptors, buffers used for reading and writing, the number of bytes read and written, and the callback function after asynchronous I/O is completed.

Line 79 of the code initiates an asynchronous write operation, the function will return immediately, and the specific write operation is completed in the underlying driver.

while (1) {
        if (aio_write(&aiow) == -1)
goto fail;

Then the code 81 line asynchronous read operation, the function will return immediately, the specific write operation is completed in the underlying driver.

if (aio_read(&aior) == -1)
            goto fail;

After the writing is completed, the registered aiow_competion_handler write completion function will be automatically called. This function obtains the error code of the I/O operation and the return value of the actual write operation through aio_error and aio_return. Assign a value to sigval.sival_ptr, which points to the I/O control block aiow.

sugval.sival is assigned on line 67

aiow.aio_sigevent.sigev_value.sival_ptr = &aiow;

Similarly, after the read is completed, the registered aior_completion_handler read completion function will be called automatically.

In addition to the completion status of the write completion operation, the read data can also be obtained from aio_buf.
sleep() is to simulate the time consumed by other operations.

In an asynchronous operation, multiple I/O requests can be combined to complete a series of read and write operations. The corresponding interface function is lio_listio.

The compilation of the second code is implemented:
compile and load

Asynchronous notification

Analysis of code 1:

Lines 41 to 46 of the code correspond to step 1 – registering the signal handler (registering the interrupt handler)

//Block SIGIO to prevent nested calls of signal handlers
    sigemptyset(&act.sa_mask);
    sigaddset(&act.sa_mask, SIGIO);
    act.sa_flags = SA_SIGINFO;
    act.sa_sigaction = sigio_handler;
    if (sigaction(SIGIO, &act, &oldact) == -1)
        goto fail;

The signal processing function registered with sigaction has three parameters, and the second parameter act is some information about the signal, which we will use later.

Also, code lines 41 and 42

sigemptyset (& act .sa_mask );
sigaddset (& act .sa_mask , SIGIO );

Blocks SIGIO itself, preventing nested calls to signal handlers.

//Set the file owner
    fd = open("/dev/vser0", O_RDWR | O_NONBLOCK);
    if (fd == -1) 
        goto fail;

    if (fcntl(fd, F_SETOWN, getpid()) == -1)
        goto fail;

Lines 48 to 53 of the code correspond to step (2) – open the device file and set the file owner (the purpose is to drive the driver to find the corresponding process according to the file structure of the open file, so as to send a signal to the process)

That is to say, when setting the file owner driver to send a signal, it is in a so-called arbitrary process context, that is, it does not know the currently running process. To send a signal to a specific process, some additional information is required. The information is stored in the file structure, so that the driver can find the corresponding process according to the file structure.

Lines 54 and 55 of the code correspond to step (3) – set the signal sent by the driver to the process when the device resource is available (not necessary, but required to use the advanced features of sigaction)

//When the current resource is set, send a SIGIO signal to the process 
    if (fcntl(fd, F_SETSIG, SIGIO) == -1 )
         goto fail;

It is set to send the SIGIO signal to the process when the device resources are available. Although this is the default signal sent, in order to use more information about the signal (mainly the reason for sending the signal, or the case of specific resources), it is necessary to explicitly perform this step.

Lines 56 and 59 of the code correspond to step (4) – set the FASYNC flag of the file to enable the asynchronous notification mechanism (equivalent to the interrupt enable bit)

//Open the asynchronous notification mechanism
     if ((flag = fcntl (fd, F_GETFL)) == - 1 )
         goto fail;
     if ( fcntl (fd, F_SETFL, flag | FASYNC) == - 1 )
         goto fail;

The flag of the file is obtained first, and then the FASYNC flag is added, which opens the mechanism of asynchronous notification.

Supplement kernel code
fs/fcnl.c for asynchronous notifications

static int setfl(int fd, struct file * filp, unsigned long arg)
{
    ........
    if (((arg ^ filp->f_flags) & FASYNC) && filp->f_op->fasync) {
        error = filp->f_op->fasync(fd, filp, (arg & FASYNC) != 0);
    ........
}

static long do_fcntl(int fd, unsigned int cmd, unsigned long arg,
        struct file *filp)
{
long err = -EINVAL;

    switch (cmd) {
    .......
    case F_SETFL:
        err = setfl(fd, filp, arg);
        break;
        ......
        }
    .......
SYSCALL_DEFINE3(fcntl, unsigned int, fd, unsigned int, cmd, unsigned long, arg)
{       
.......
    err = do_fcntl(fd, cmd, arg, f.file);
.......
}
.......

static int fasync_add_entry(int fd, struct file *filp, struct fasync_struct **fapp)
{
    struct fasync_struct *new;

    new = fasync_alloc();
    if (!new)
        return -ENOMEM;
    /*
     * fasync_insert_entry() returns the old (update) entry if
     * it existed.
     *
     * So free the (unused) new entry and return 0 to let the
     * caller know that we didn't add any new fasync entries.
     */
    if (fasync_insert_entry(fd, filp, fapp, new)) {
        fasync_free(new);
        return 0;
    }

    return  1 ;
}
.......
int fasync_helper(int fd, struct file * filp, int on, struct fasync_struct **fapp)
{
    if (!on)
        return fasync_remove_entry(filp, fapp);
    return fasync_add_entry(fd, filp, fapp);
}
.......
static void kill_fasync_rcu(struct fasync_struct *fa, int sig, int band)
{
    while (fa) {
        struct fown_struct *fown;
        unsigned long flags;

        if (fa->magic != FASYNC_MAGIC) {
            printk(KERN_ERR "kill_fasync: bad magic number in "
                   "fasync_struct!\n");
            return;
        }
        spin_lock_irqsave(&fa->fa_lock, flags);
        if (fa->fa_file) {
            fown = &fa->fa_file->f_owner;
            /* Don't send SIGURG to processes which have not set a
               queued signum: SIGURG has its own default signalling
               mechanism. */
            if (!(sig == SIGURG && fown->signum == 0))
                send_sigio (fown, fa-> fa_fd, band);
        }
        spin_unlock_irqrestore(&fa->fa_lock, flags);
        fa = rcu_dereference(fa->fa_next);
    }
}

void kill_fasync(struct fasync_struct **fp, int sig, int band)
{
    /* First a quick test without locking: usually
     * the list is empty.
     */
    if (*fp) {
        rcu_read_lock();
        kill_fasync_rcu(rcu_dereference(*fp), sig, band);
        rcu_read_unlock();
    }
}

analyze:

code

SYSCALL_DEFINE3(fcntl, unsigned int, fd, unsigned int, cmd, unsigned long, arg)

It is the code corresponding to the fcntl system call, which calls do_fcntl to complete the specific operation.

code

case F_SETFL:
        err = setfl(fd, filp, arg);
        break;

The code judges that if it is F_ SETFL, it will call the setfl function, and setfl will call the fasync interface function in the driver code (code line 68)error = filp->f_op->fasync(fd, filp, (arg & FASYNC) != 0);

, and pass whether the FASYNC flag is set. The fasync interface function in the driver will call the fasync_ helper function (code line 680)
int fasync_helper(int fd, struct file * filp, int on, struct fasync_struct **fapp)

, The fasync_helper function decides whether to add a [struct] fasync_ struct node or delete a node in the linked list according to whether the FASYNC flag is set, and the most important member of this structure is fa_ file, which is a structure to open a file and also contains process information ( the file owner set earlier). When the resource is available, the driver calls the kill fasync function to send the signal, which traverses the struct fasync_struct linked list to find all the processes that want to receive the signal, and calls send_sigio to send the signal in sequence (code line 692 to line 714). as follows:

static void kill_fasync_rcu(struct fasync_struct *fa, int sig, int band)
{
    while (fa) {
        struct fown_struct *fown;
        unsigned long flags;

        if (fa->magic != FASYNC_MAGIC) {
            printk(KERN_ERR "kill_fasync: bad magic number in "
                   "fasync_struct!\n");
            return;
        }
        spin_lock_irqsave(&fa->fa_lock, flags);
        if (fa->fa_file) {
            fown = &fa->fa_file->f_owner;
            /* Don't send SIGURG to processes which have not set a
               queued signum: SIGURG has its own default signalling
               mechanism. */
            if (!(sig == SIGURG && fown->signum == 0))
                send_sigio (fown, fa-> fa_fd, band);
        }
        spin_unlock_irqrestore(&fa->fa_lock, flags);
        fa = rcu_dereference(fa->fa_next);
    }
}

After understanding from the above, it is concluded that the driver code completes the following operations:

  1. Constructs the head of the struct fasync_ struct linked list.
  2. Implement the fasync interface function, call the fasync_ helper function to construct the struct fasyne_struct node, and add it to the linked list.
  3. When the resource is available, call kill fasync to send a signal and set whether the available type of the resource is readable or writable.
  4. When the file is closed for the last time, that is, in the release interface, it is necessary to explicitly call the fasyne interface function implemented by the driver to delete the node from the linked list, so that the process will no longer receive the signal.

The complete code of the virtual serial port driver that implements many interfaces is as follows:
see the top blog

Analysis:
Line 30 of the code defines the pointer of the linked list and implements step (1); lines 170 to 173 and 185 of the code implement step (2); lines 66 and 90 of the code send signals , to implement step (3), note that the resource status is POLL_ IN and POLL_OUT at this time, which will be converted to POLLIN and POLLOUT in the signal sending function; the 45th line of the code completes step (4).

The commands to compile and test are as follows:
Note: When compiling an application, you need to define GNU_SOURCE by adding -D_GNU_SOURCE to the command line, because F SETSIG is not a POSIX standard.

here it isgcc -o test test.c -D_GNU_SOURCE

You may also like...

Leave a Reply

Your email address will not be published.