异步通知

Posted wanjianjun777

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了异步通知相关的知识,希望对你有一定的参考价值。

更新记录

version status description date author
V1.0 C Create Document 2019.1.13 John Wan

status:
C―― Create,
A—— Add,
M—— Modify,
D—— Delete。

注:内核版本 3.0.15

1、异步通知概念

??异步通知:一旦设备就绪,则主动通知应用程序,这样应用程序根本就不需要查询设备状态(前面的轮询、中断、poll方式都需要在应用程序汇总主动去查询设备状态),非常类似于硬件上“中断”的概念,比较准确的称谓是“信号驱动的异步 I/O”。信号是异步的,一个进程不必通过任何操作来等待信号的到达。

??在Linux中,异步通知是使用信号来实现的,而在Linux,大概有30种信号,比如大家熟悉的ctrl+c的SIGINT信号,进程能够忽略或者捕获除过 SIGSTOP 和 SIGKILL 的全部信号,当信号背捕获以后,有相应的 signal() 函数来捕获信号,函数原型:

sighandler_t signal(int signum, sighandler_t handler);

参数:
    第一个参数就是指定的信号的值;
    第二个参数便是此信号的信号处理函数,当为SIG_IGN,表示信号被忽略,当为SIG_DFL时,表示采用系统的默认方 式来处理该信号。
    当然,信号处理函数也可以自己定义。当signal()调用成功后,返回处理函数handler值,调用失败后返回SIG_ERR。

整个异步通知机制需要解决4个问题:

  1. 注册信号处理函数;
  2. 信号由谁发?
  3. 信号发给谁?
  4. 信号怎么发?

采用异步通知机制的目的:是由驱动程序主动通过进程间的信号通知应用程序。那么回答上面的问题就是:

  1. 应用程序注册信号处理函数,通过 signal() 函数;
  2. 由驱动程序来发送信号;
  3. 将信号发送给应用程序,那么应用程序应该事先告知驱动程序它的 PID 是多少,这样驱动程序才能正确的发送;
  4. 驱动程序通过 kill_fasyn() 函数发送。

2、异步通知机制

??从两个方面来分析异步通知机制:

??1)从应用程序的角度考虑,需要解决信号处理函数的注册以及告知驱动程序信号发送的PID。

??2)从驱动程序的角度考虑,需要解决异步通知机制的初始化以及信号的发送。

2.1 从应用程序的角度考虑

2.1.1 信号处理函数的注册

??函数原型 ``

2.1.1.1 信号的本质

??信号本质(计算机):软中断信号(signal,又简称为信号)用来通知进程发生了异步事件。在软件层次上是对中断机制的一种模拟,在原理上,一个进程收到一个信号与处理器收到一个中断请求可以说是一样的。信号是进程间通信机制中唯一的异步通信机制,一个进程不必通过任何操作来等待信号的到达,事实上,进程也不知道信号到底什么时候到达。进程之间可以互相通过系统调用kill发送软中断信号。内核也可以因为内部事件而给进程发送信号,通知进程发生了某个事件。信号机制除了基本通知功能外,还可以传递附加信息。

2.1.1.2 信号的来源

??信号来自内核, 生成信号的请求来自以下3个地方:

??1)用户:用户可以通过输入Ctrl-C, Ctrl-等命令,或终端驱动程序分配给信号控制字符的其他任何键来请求内核产生信号。

??2)内核:当进程执行出错时, 内核给进程发送一个信号。 例如,非法段存取,浮点数溢出,亦或是一个非法指令,内核也利用信号通知进程特定事件发生。

??3)进程:一个进程可以通过系统调用kill给另外一个进程发送信号, 一个进程可以和另一个进程通过信号通信。

2.1.1.3 信号类型

??文件 /include/asm-generic/signal.h 中有说明:

技术图片

2.1.1.4 信号捕获

??进程能够通过系统调用 signal() 告诉内核, 它要如何处理信号, 进程有3个选择。

??1)接收默认处理

    signal(SIGINT, SIG_DFL);

    SIGINT 的 SIG_DFL(默认处理) 是消亡,进程并不一定要使用 signal 接收默认处理,但是进程能够通过该调用来恢复默认处理。

??2)忽略信号

    signal(SIGINT, SIG_IGN);
    
    程序可以通过该调用来告诉内核, 它需要忽略SIGINT。

??3)自定义信号处理函数

    signal(signum, functionname);
    
    程序能够告诉内核,当信号到来时应该调用哪个函数。
    signum:指定信号的类型
    functionname:指定信号处理函数
2.1.1.5 测试

??源码:

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

void function_name(int signum)
{
    static unsigned int cnt = 0;

    printf("SIGUSR1: %x %x !
", signum, ++cnt);
}

int main(void)
{
    signal(SIGUSR1, function_name);

    while (1) {
        sleep(1000);
    }

    return 0;
}

操作:

① 编译

② 在开发板上后台运行 "./signal &"

③ 查看进程,命令"ps"

④ 发送信号 "kill -USR1 PID"

⑤ 干掉进程 kill -9 PID

[[email protected]]# ./signal &
[[email protected]]# ps
PID   USER     TIME   COMMAND
    1 root       0:06 {linuxrc} init
......
 6629 root       0:00 ./signal
 6642 root       0:00 ps
[[email protected]]# kill -USR1 6629
[[email protected]]# SIGUSR1: a 1 !
[[email protected]]# kill -USR1 6629
[[email protected]]# SIGUSR1: a 2 !
[[email protected]]# kill -USR1 6629
[[email protected]]# SIGUSR1: a 3 !
[[email protected]]# kill -USR1 6629
[[email protected]]# SIGUSR1: a 4 !

关于 "SIGINT" 的测试可参考Linux信号来源和捕获处理以及signal函数简介

2.1.2 告知驱动程序信号发送的PID

??为开启文件的异步通知机制,应用程序必须执行两个步骤。

??1)指定一个进程作为文件的 “属主(owner)”。通过系统调用 fcntl() 执行 F_SETOWN命令,将属主进程的 ID 号保存在 file->f_owner 中。

??2)启动异步通知机制。通过系统调用 fcntl() 执行 F_SETFL 命令,设置 FASYNC 标志。

int Oflags;

fcntl(fd, F_SETOWN, getpid());  //获取进程PID,并告知内核
Oflags = fcntl(fd, F_GETFL);   
fcntl(fd, F_SETFL, Oflags | FASYNC);    //改变fasync标记,最终会调用到驱动的faync > fasync_helper:初始化/释放fasync_struct

??执行完这两个步骤之后,输入文件就可以在新数据到达时请求发送一个SIGIO信号。该信号被发送到存放在filp->f_owner 中的进程(如果是负值就是进程组)。

2.1.3 fcntl 机制:

??分析 fcntl 与前面分析 openreadpoll 类似,可参考 “poll 机制”那篇文章。

??应用层的fcntl 调用:fcntl() -> sys_fcntl() -> do_fcntl() -> 根据cmd调用相应操作函数

函数原型:

??/fs/fcntl.c 文件中的 do_fcntl()函数:

//只保留与异步通知机制相关的部分代码
static long do_fcntl(int fd, unsigned int cmd, unsigned long arg,
        struct file *filp)
{
    long err = -EINVAL;

    switch (cmd) {
......
    case F_GETFL:
        err = filp->f_flags;
        break;
    case F_SETFL:
        err = setfl(fd, filp, arg);
        break;
......
    case F_GETOWN:
        /*
         * XXX If f_owner is a process group, the
         * negative return value will get converted
         * into an error.  Oops.  If we keep the
         * current syscall conventions, the only way
         * to fix this will be in libc.
         */
        err = f_getown(filp);
        force_successful_syscall_return();
        break;
    case F_SETOWN:
        err = f_setown(filp, arg, 1);
        break;
......
    default:
        break;
    }
    return err;
}

??f_setown() 函数分析:f_setown -> __f_setown -> f_modown

static void f_modown(struct file *filp, struct pid *pid, enum pid_type type,
                     int force)
{
    //设置对应 fd 的 f_owner 的 pid、uid、euid
    write_lock_irq(&filp->f_owner.lock);
    if (force || !filp->f_owner.pid) {
        put_pid(filp->f_owner.pid);
        filp->f_owner.pid = get_pid(pid);
        filp->f_owner.pid_type = type;

        if (pid) {
            const struct cred *cred = current_cred();
            filp->f_owner.uid = cred->uid;
            filp->f_owner.euid = cred->euid;
        }
    }
    write_unlock_irq(&filp->f_owner.lock);
}

??setfl() 函数分析:

//只保留异步相关代码
static int setfl(int fd, struct file * filp, unsigned long arg)
{
......
    /*
     * ->fasync() is responsible for setting the FASYNC bit.
     */
    if (((arg ^ filp->f_flags) & FASYNC) && filp->f_op &&
            filp->f_op->fasync) {
        error = filp->f_op->fasync(fd, filp, (arg & FASYNC) != 0);
        if (error < 0)
            goto out;
        if (error > 0)
            error = 0;
    }
    spin_lock(&filp->f_lock);
    filp->f_flags = (arg & SETFL_MASK) | (filp->f_flags & ~SETFL_MASK);
    spin_unlock(&filp->f_lock);

 out:
    return error;
}
  1. 上面的 if 语句,类似边沿触发,当检测到 FASYNC 标志位从 0 到 1 的跳变才为真。
  2. 进入 if 语句中,与接触的其它内核驱动接口类似,filp->f_op->fasync(),即调用驱动中向 fiel_operation 结构注册的 .fasync 成员。

2.2 从驱动程序的角度考虑

2.2.1 异步通知机制的初始化

??在上面已经介绍了,借助系统调用 fcntlF_SETFL 命令,可以调用到驱动层 file_operation 中的 .fasync 成员,那么异步通知机制的初始化,就在该函数注册的过程中进行。

??那么就在驱动中注册 fasync 函数,来调用 fasync_helper() 函数进行初始化。

?? fasync_helper() 函数原型:

/*
 * fasync_helper() is used by almost all character device drivers
 * to set up the fasync queue, and for regular files by the file
 * lease code. It returns negative on error, 0 if it did no changes
 * and positive if it added/deleted the entry.
 */
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);
}

??从上面可看出,根据 on 参数决定是移除一个东西还是添加一个东西,那么这个 on 参数就是在上面 setfl() 函数中传入的 “(arg & FASYNC) != 0)”,也就是 arg 参数有没有设置 FASYNC 标志位。

??fasync_remove_entry(): 根据文件句柄对应的struct file 遍历 fasync 列表,如果在列表中找到指定文件,则从列表中移除,清除相关标志,并返回1,否则返回0。

??fasync_add_entry(): 申请一个 fasync_cache 结构的内存, 根据文件句柄对应的struct file 遍历 fasync 列表,如果在列表中找到指定文件,则销毁申请的内存,否则设置新申请的 struct fasync_struct对应的参数,并添加到 fasync 列表中。

??fasync 列表成员 struct fasync_struct

struct fasync_struct {
    spinlock_t      fa_lock;
    int         magic;
    int         fa_fd;
    struct fasync_struct    *fa_next; /* singly linked list */
    struct file     *fa_file;
    struct rcu_head     fa_rcu;
};

2.2.2 信号的发送

??信号的发送:通过 kill_fasync() 函数。

??调用:kill_fasync() -> kill_fasync_rcu() -> send_sigio()

/*
 * rcu_read_lock() is held
 */
static void kill_fasync_rcu(struct fasync_struct *fa, int sig, int band)
{
    while (fa) {
        struct fown_struct *fown;
        unsigned long flags;
......
        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 send_sigio(struct fown_struct *fown, int fd, int band)
{
    struct task_struct *p;
    enum pid_type type;
    struct pid *pid;
    int group = 1;
    
    read_lock(&fown->lock);

    type = fown->pid_type;
    if (type == PIDTYPE_MAX) {
        group = 0;
        type = PIDTYPE_PID;
    }

    pid = fown->pid;
    if (!pid)
        goto out_unlock_fown;
    
    read_lock(&tasklist_lock);
    do_each_pid_task(pid, type, p) {
        send_sigio_to_task(p, fown, fd, band, group);
    } while_each_pid_task(pid, type, p);
    read_unlock(&tasklist_lock);
 out_unlock_fown:
    read_unlock(&fown->lock);
}

??kill_fasync_rcu()函数中 "fown = &fa->fa_file->f_owner;" 调用了文件对应的 f_owner,并在 send_sigio() 函数中利用 f_owner->pid 来发送信号,这也就是应用程序中系统调用 fcntl() 执行 F_SETOWN命令的作用。

2.3 小结

技术图片

3、案列

??应用程序:

#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <sys/ioctl.h>
#include <unistd.h>
#include <signal.h>
#include <fcntl.h>

int fd;

void my_signal_fun(int signum)
{
    unsigned char key_val;

    read(fd, &key_val, 1);
    printf("key_val: 0x%x
", key_val);
}


int main(int argc, char **argv)
{
    int Oflags;

    signal(SIGIO, my_signal_fun);
    
    fd = open("/dev/buttons_irq_minor", O_RDWR);
    if (fd < 0)
        printf("can't open is!
");

    fcntl(fd, F_SETOWN, getpid());
    Oflags = fcntl(fd, F_GETFL);
    fcntl(fd, F_SETFL, Oflags | FASYNC);
    
    while (1) {
        sleep(1000);
    }

    return 0;
}

??驱动程序:

#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/fs.h>
#include <linux/init.h>
#include <linux/delay.h>
#include <asm/uaccess.h>
#include <asm/irq.h>
#include <asm/io.h>

/*驱动注册的头文件,包含驱动的结构体和注册和卸载的函数*/
#include <linux/platform_device.h>
/*Linux中申请GPIO的头文件*/
#include <linux/gpio.h>
/*三星平台的GPIO配置函数头文件*/
/*三星平台EXYNOS系列平台,GPIO配置参数宏定义头文件*/
#include <plat/gpio-cfg.h>
#include <mach/gpio.h>
/*三星平台4412平台,GPIO宏定义头文件*/
#include <mach/gpio-exynos4.h>

#include <linux/irq.h>
#include <linux/wait.h>
#include <linux/interrupt.h>
#include <linux/sched.h>
#include <linux/poll.h>

#define DEVICE_NAME "buttons_fasync"

static struct class *buttons_irq_class;
static struct device *buttons_irq_class_dev;


struct pin_desc {
    unsigned int pin;
    unsigned int key_val;
};

struct pin_desc pins_desc[5] = {
    {EXYNOS4_GPX1(1), 1},
    {EXYNOS4_GPX1(2), 2},
    {EXYNOS4_GPX3(3), 3},
    {EXYNOS4_GPX2(1), 4},
    {EXYNOS4_GPX2(0), 5},
};

static unsigned char key_val = 0;

static DECLARE_WAIT_QUEUE_HEAD(button_waitq);

static struct fasync_struct *button_fasync;

/* 中断事件标志, 中断服务程序将它置1,third_drv_read将它清0 */
static volatile int ev_press = 0;


static irqreturn_t buttons_irq(int irq, void *dev_id)
{
    struct pin_desc *pindesc = (struct pin_desc *)dev_id;
    unsigned int pinval;

    pinval = gpio_get_value(pindesc->pin);

    if (pinval)
        key_val = 0x80 | pindesc->key_val;
    else
        key_val = pindesc->key_val;

    printk(DEVICE_NAME " key press1
");

    ev_press = 1;   //中断发生
    wake_up_interruptible(&button_waitq);   /* 唤醒休眠的进程 */
  
    printk(DEVICE_NAME " key press2
");

    kill_fasync(&button_fasync, SIGIO, POLL_IN);    //fasync

    return IRQ_RETVAL(IRQ_HANDLED);
}


static int buttons_irq_open(struct inode *pinode, struct file *pfile)
{
    /* 配置各按键引脚为外部中断 */
    request_irq(IRQ_EINT(9), buttons_irq, IRQ_TYPE_EDGE_BOTH, "S1_Home", &pins_desc[0]);
    request_irq(IRQ_EINT(10), buttons_irq, IRQ_TYPE_EDGE_BOTH, "S2_Back", &pins_desc[1]);
    request_irq(IRQ_EINT(27), buttons_irq, IRQ_TYPE_EDGE_BOTH, "S3_Sleep", &pins_desc[2]);
    request_irq(IRQ_EINT(17), buttons_irq, IRQ_TYPE_EDGE_BOTH, "S4_Vol+", &pins_desc[3]);
    request_irq(IRQ_EINT(16), buttons_irq, IRQ_TYPE_EDGE_BOTH, "S5_Vol-", &pins_desc[4]);

    printk(DEVICE_NAME " I'm open!
");

    return 0;
}

static ssize_t buttons_irq_read(struct file *pfile, char __user *pbuf,
                                    size_t count, loff_t *ploff)
{
    if (count != 1)
        return -EINVAL;

    //如果没有按键动作,休眠
    wait_event_interruptible(button_waitq, ev_press);

    //如果有按键动作,返回键值
    copy_to_user(pbuf, &key_val, 1);
    ev_press = 0;

    printk(DEVICE_NAME " I'm read key_val %d!
", key_val);

    return 1;
}

static int buttons_irq_release(struct inode *pinode, struct file *pfile)
{
    free_irq(IRQ_EINT(9), &pins_desc[0]);
    free_irq(IRQ_EINT(10), &pins_desc[1]);
    free_irq(IRQ_EINT(27), &pins_desc[2]);
    free_irq(IRQ_EINT(17), &pins_desc[3]);
    free_irq(IRQ_EINT(16), &pins_desc[4]);

    printk(DEVICE_NAME " I'm release
");

    return 0;
}

static unsigned int buttons_irq_poll(struct file *pfile, struct poll_table_struct *ptable)
{
    unsigned int mask = 0;
    poll_wait(pfile, &button_waitq, ptable); // 不会立即休眠

    if (ev_press)
        mask |= POLLIN | POLLRDNORM;

    return mask;
}

static int buttons_fasync(int fd, struct file *file, int on)
{
    printk("driver: buttons_fasync
");
    
    return fasync_helper(fd, file, on, &button_fasync);
}

static struct file_operations buttons_irq_fpos = {
    .owner = THIS_MODULE,
    .open = buttons_irq_open,
    .read = buttons_irq_read,
    .release = buttons_irq_release,
    .poll = buttons_irq_poll,
    .fasync = buttons_fasync,
};

int major;
static int __init buttons_irq_init(void)
{
    /*注册主设备号*/
    major = register_chrdev(0, "buttons_irq", &buttons_irq_fpos);

    /*注册次设备号*/
    buttons_irq_class = class_create(THIS_MODULE, "buttons_irq");
    if (IS_ERR(buttons_irq_class))
        return PTR_ERR(buttons_irq_class);

    buttons_irq_class_dev = device_create(buttons_irq_class, NULL,
                                MKDEV(major, 0), NULL, "buttons_irq_minor");

    printk(DEVICE_NAME " initialized
");

    return 0;
}

static void __exit buttons_irq_exit(void)
{
    unregister_chrdev(major, "buttons_irq");

    device_unregister(buttons_irq_class_dev);

    class_destroy(buttons_irq_class);

    //return 0;
}

module_init(buttons_irq_init);
module_exit(buttons_irq_exit);

MODULE_LICENSE("GPL");

测试:

① 编译模块及测试程序;

② 加载驱动模块 insmod buttons_fasync,查看 lsmod;

③ 按键测试,查看打印。

④ 卸载驱动模块 rmmod buttons_fasync

参考

  1. 异步通知机制
  2. Linux设备驱动之阻塞I/O与异步通知
  3. 【韦东山】嵌入式 Linux 视频教程第一期—第12课第6节。
  4. Linux信号来源和捕获处理以及signal函数简介

以上是关于异步通知的主要内容,如果未能解决你的问题,请参考以下文章

字符设备驱动按键异步通知

如何在通知选项卡中单击推送通知消息打开特定片段?

VBA中的异步通知

Firebase 函数无法使用异步 sendMulticast 方法发送一些推送通知

关于 exynos 4412 按键中断 异步通知

在android中按下通知时如何打开片段页面