6.4异步通知

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/shenwanjiang111/article/details/81480971

6.4 异步通知

尽管使用 select 方法, 结合阻塞和非阻塞操作, 对于设备的查询来说, 大部分时候足够了, 但是有些情况, 以我们目前所知的技术还不能有效处理。

假设一个进行长计算循环的进程, 需要尽可能快地处理接收到的数据。 对于某种数据采集的外设, 一旦有数据就绪, 进程必须能够立马响应。 这当然可以调用 poll 检查数据, 但是, 更多情况下, 有更好的办法。 通过异步通知, 如果数据就绪, 发送信号给应用程序, 而不需要轮询。

使能文件的异步通知, 需要执行两步。 首先, 指定文件的所有者进程。 当进程通过fcntl系统调用, 而去调用F_SETOWN命令时, 所有者进程的进程ID将被保存在 filp->f_owner 中以供以后使用。 此步骤是内核知道要通知的对象所必需的。 其次, 为了实际启用异步通知, 用户程序必须通过 fcntlF_SETFL命令在设备中设置 FASYNC 标志。

执行完这2个调用后, 输入文件可以在新数据到达时请求传送SIGIO信号。 该信号被发送给所属进程(或者进程组, 如果值是负的话), 该进程存储在 filp->f_owner

例如, 下面的用户程序代码为 stdin 输入文件设置了异步通知, 通知对象是当前进程:

signal(SIGIO, &input_handler); /* dummy sample; sigaction( ) is better */
fcntl(STDIN_FILENO, F_SETOWN, getpid( ));
oflags = fcntl(STDIN_FILENO, F_GETFL);
fcntl(STDIN_FILENO, F_SETFL, oflags | FASYNC);

示例源代码中, asynctest 就是读取 stdin 的简单示例程序。 可以用来测试 scullpipe 的异步能力。 该程序与 cat 相似, 但是不会在文件结尾处终止; 只响应输入, 没有输入时不响应。

但是, 请注意, 并不是所有的设备都支持异步, 你可以选择不提供这个功能。 通常情况下, 默认支持异步的是 sockettty 设备文件。

对于输入文件的异步通知还有一个问题。 那就是, 当进程接收到 SIGIO 信号时, 它并不知道哪个输入文件有输入。 如果有不只一个文件异步通知该进程, 引用程序必须借助 pollselect 去查找到底哪个文件有了新的输入。

6.4.1 从驱动程序的角度考虑

对于我们来说, 更关心的是驱动程序怎样实现异步通知的。 从内核的视角看, 分为三步:

  1. 当 F_SETOWN 被调用时, 给 filp->f_owner 赋值, 不做其余操作;
  2. 当执行 F_SETFL 命令设置 FASYNC 标志时, 驱动程序的 fasync 方法被调用。 当 filp->f_flags 中的 FASYNC 标志发生变化, 该方法就会被调用, 以便把这个变化通知驱动程序, 使其能做出正确响应。 文件打开时, FASYNC 标志默认是被清除的;
  3. 当数据到达时, 所有注册为异步通知的进程都会被发送一个SIGIO信号。

第一步的实现很简单,在驱动程序部分没有什么可做的。其余两步则要涉及维护一个动态数据结构,以跟踪不同的异步读取进程,这种进程可能有好几个。不过,这个动态数据结构并不依赖于特定设备,内核已经提供了一套通用的实现方法,没有必要为每个驱动程序重写这部分代码。

Linux 提供的通用实现是基于一个数据结构和两个函数(都是在第2步和第3步中调用)。 该数据结构为 struct fasync_struct , 相关声明位于头文件 <linux/fs.h> 中, 内容如下:

扫描二维码关注公众号,回复: 3186510 查看本文章
struct fasync_struct {
    int magic;
    int fa_fd;
    struct  fasync_struct   *fa_next;   /* 单向链表 */
    struct  file            *fa_file;
};

和处理等待队列的方式类似, 我们需要把一个指向该数据结构的指针插入到设备特定的数据结构中去。 回忆一下 scullpipe 设备结构体的定义:

struct scull_pipe {
    wait_queue_head_t       inq, outq;          /* read 和 write队列 */
    char                    *buffer, *end;      /* 缓冲区的头尾 */
    int                     buffersize;         /* 缓冲区大小 */
    char                    *rp, *wp;           /* 读写位置指针 */
    int                     nreaders, nwriters; /* r/w操作的数量 */
    struct fasync_struct    *async_queue;       /* 异步读取者 */
    struct semaphore        sem;                /* 互斥信号量 */
    struct cdev             cdev;               /* 字符设备结构 */
};

驱动程序调用的两个相关函数原型如下:

int fasync_helper(int fd, struct file *filp, int mode, struct fasync_struct **fa);
void kill_fasync(struct fasync_struct **fa, int sig, int band);

当一个打开的文件的 FASYNC 标志被修改时, 调用 fasync_helper 以便从相关的进程列表中增加或删除文件。 除了最后一个参数外, fasync_helper 的其它参数与驱动程序的fasync函数相同, 可以直接传递。

下面是 scullpipe 实现 fasync 方法的代码:

static int scull_p_fasync(int fd, struct file *filp, int mode)
{
    struct scull_pipe *dev = filp->private_data;
    return fasync_helper(fd, filp, mode, &dev->async_queue);
}

很明显, 所有的工作都是由 fasync_helper 实现的。 但是, 如果不在驱动程序中封装这个函数, 提供给它指向 struct fasync_struct 结构体的指针(在这儿, &dev->async_queue ), 它是无法实现这个功能的。 因为只有驱动程序才能提供这个信息。

当数据到达时, 应调用 kill_fasync 通知所有的相关进程, 该函数的第2个参数是要发送的信号(通常是 SIGIO ), 第3个参数是带宽(通常是 POLL_IN , 但是有时候在网络代码中, 被用来发送 urgent 或带外数据)。 由于提供给 scullpipe 的读取进程的新数据是由某个进程调用 write 产生的,所以 kill_fasync 函数在 scullpipewrite 函数中调用,代码片段如下所示:

if (dev->async_queue)
    kill_fasync(&dev->async_queue, SIGIO, POLL_IN);

如果是针对写入的异步通知, kill_fasync 的第三个参数必须为 POLL_OUT

最后, 要做的是, 当文件关闭时, 必须调用 fasync 方法, 以便从活动的异步读取进程列表中删除该文件。 虽然只有在 filp->f_flags 设置了 FASYNC 时才需要进行此调用, 但是强制调用该函数不会造成什么破坏, 所以, 通常都会这么实现。 在 scullpipeclose 函数中, 有如下代码

/* remove this filp from the asynchronously notified filp's */
scull_p_fasync(-1, filp, 0);

异步通知的数据结构和等待队列 struct wait_queue 几乎相同, 因为这两种情况都涉及等待事件。 它们的区别就是, 使用 struct file 取代了 struct task_struct 。 等待队列中的 struct file 是用来检索 f_owner, 为了发送信号通知进程。

6.4.2 测试scullpipe的异步通知机制

LDD3提供了一个异步通知机制的测试程序 asynctest.c 。 我们把 asynctest 的标准输入重定向到 /dev/scullp, 所以当 /dev/scullp 有数据可读时, 会向 asynctest 发信号 SIGIO , 唤醒 asynctest , 执行信号处理函数, 然后读取并打印信息到临时文件 tmp.txt , 再进入下次循环。 如下图所示:其代码如下:

/*
 * asynctest.c: use async notification to read stdin
 *
 * Copyright (C) 2001 Alessandro Rubini and Jonathan Corbet
 * Copyright (C) 2001 O'Reilly & Associates
 *
 * The source code in this file can be freely used, adapted,
 * and redistributed in source or binary form, so long as an
 * acknowledgment appears in derived source files.  The citation
 * should list that the code comes from the book "Linux Device
 * Drivers" by Alessandro Rubini and Jonathan Corbet, published
 * by O'Reilly & Associates.   No warranty is attached;
 * we cannot take responsibility for errors or fitness for use.
 */

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

int gotdata=0;
void sighandler(int signo)
{
    if (signo==SIGIO)
        gotdata++;
    return;
}

char buffer[4096];

int main(int argc, char **argv)
{
    int fd, count;
    struct sigaction action;

    memset(&action, 0, sizeof(action));
    action.sa_handler = sighandler;
    action.sa_flags = 0;

    sigaction(SIGIO, &action, NULL);

    fcntl(STDIN_FILENO, F_SETOWN, getpid());
    fcntl(STDIN_FILENO, F_SETFL, fcntl(STDIN_FILENO, F_GETFL) | FASYNC);

    fd = open("tmp.txt", O_RDWR);
    if (fd < 0)
    {
        return -1;
    }
    while(1) {
        /* this only returns if a signal arrives */
        sleep(86400); /* one day */
        if (!gotdata)
            continue;
        count=read(0, buffer, 4096);
        /* buggy: if avail data is more than 4kbytes... */
        write(fd,buffer,count);
        close(fd);
        gotdata=0;
    }
}

操作过程如下

首先, 执行:
    ./asynctest < /dev/scullp &  // 放到后台执行
然后,
    ls > /dev/scullp
再执行,
    cat tmp.txt
显示结果,当前目录下只有这两个文件,
    asynctest
    tmp.txt

猜你喜欢

转载自blog.csdn.net/shenwanjiang111/article/details/81480971
6.4