Linux驱动开发之异步通知

 

简述

异步通知的意思是:一旦设备就绪,则主动通知应用程序,这样应用程序根本就不需要查询设备状态,这一点非常类似于硬件上“中断”的概念,比较准确的称谓是“信号驱动的异步I/O”。信号是在软件层次上对中断机制的一种模拟,在原理上,一个进程收到一个信号与处理器收到一个中断请求可以说是一样的。信号是异步的,一个进程不必通过任何操作来等待信号的到达,事实上,进程也不知道信号到底什么时候到达。

下图呈现了阻塞I/O,结合轮询的非阻塞I/O及基于SIGIO的异步通知在时间先后顺序上的不同。

butong

信号

信号(signal)是一种异步通知机制,它用于向进程发送通知。内核可以通过向进程发送信号来通知它们发生了某些事件。进程可以通过使用信号处理函数来处理这些信号。在Linux内核中,每个信号都有一个唯一的编号,可以使用kill命令来向进程发送信号。例如,SIGINT信号(编号为2)是由CTRL+C键触发的,它可以用来中断正在运行的进程。

在Linux内核中,共有64个信号,每个信号都有一个唯一的编号和一个名称。以下是所有信号的列表:

信号编号 信号名称 描述
1 SIGHUP 挂起信号
2 SIGINT 中断信号
3 SIGQUIT 退出信号
4 SIGILL 非法指令信号
5 SIGTRAP 跟踪/断点异常信号
6 SIGABRT 异常终止信号
7 SIGBUS 总线错误信号
8 SIGFPE 浮点异常信号
9 SIGKILL 强制终止信号
10 SIGUSR1 用户自定义信号1
11 SIGSEGV 段错误信号
12 SIGUSR2 用户自定义信号2
13 SIGPIPE 管道破裂信号
14 SIGALRM 定时器信号
15 SIGTERM 终止信号
16 SIGSTKFLT 协处理器堆栈错误信号
17 SIGCHLD 子进程状态改变信号
18 SIGCONT 继续执行信号
19 SIGSTOP 停止执行信号
20 SIGTSTP 终端停止信号
21 SIGTTIN 后台进程请求输入信号
22 SIGTTOU 后台进程请求输出信号
23 SIGURG 紧急数据信号
24 SIGXCPU 超出CPU时间限制信号
25 SIGXFSZ 超出文件大小限制信号
26 SIGVTALRM 虚拟定时器信号
27 SIGPROF 仿真计时器信号
28 SIGWINCH 窗口大小改变信号
29 SIGIO 异步I/O信号
30 SIGPWR 电源故障信号
31 SIGSYS 非法系统调用信号
32 SIGRTMIN 实时信号1
33-63 SIGRTMIN+1至SIGRTMAX 实时信号2至实时信号31
64 SIGRTMAX 实时信号32

这些信号中,前15个信号是POSIX标准信号,后49个信号是实时信号。实时信号的使用需要支持实时信号扩展的内核和glibc库。

除了SIGSTOP和SIGKILL两个信号外,进程能够忽略或捕获其他的全部信号。一个信号被捕获的意思是当一个信号到达时有相应的代码处理它。如果一个信号没有被这个进程所捕获,内核将采用默认行为处理。

捕获信号(用户程序)

在Linux用户程序中,可以使用signal函数或sigaction函数来捕获信号并执行相应的处理函数。以下是这两个函数的函数原型:

signal函数

#include <signal.h>
typedef void (*sighandler_t)(int);
sighandler_t signal(int signum, sighandler_t handler);

其中,signum参数指定要捕获的信号编号,handler参数是一个指向信号处理函数的指针,该函数接受一个整数参数,表示信号编号。signal函数将返回先前的信号处理函数。

sigaction函数

#include <signal.h>
typedef union sigval {
    int sival_int;
    void *sival_ptr;
} sigval_t;
struct sigevent {
    int sigev_notify;
    int sigev_signo;
    union sigval sigev_value;
    void (*sigev_notify_function)(union sigval);
    pthread_attr_t *sigev_notify_attributes;
};
struct sigaction {
    void (*sa_handler)(int);
    void (*sa_sigaction)(int, siginfo_t *, void *);
    sigset_t sa_mask;
    int sa_flags;
    void (*sa_restorer)(void);
};
int sigaction(int signum, const struct sigaction *act,
              struct sigaction *oldact);

其中,signum参数指定要捕获的信号编号,act参数是一个指向struct sigaction结构体的指针,该结构体包含了信号处理函数、信号屏蔽字和其他选项。oldact参数用于保存先前的信号处理函数。

在使用这两个函数时,需要注意信号处理函数的编写方式。信号处理函数必须是一个无返回值的函数,它接受一个整数参数,表示信号编号。在信号处理函数中,应该避免使用不可重入的函数和系统调用,因为它们可能会导致不可预测的行为。

signal函数实例

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

void handle_signal(int sig)
{
    printf("Received signal %d\n", sig);
}

int main()
{
    signal(SIGINT, handle_signal);
    while (1) {
        printf("Sleeping...\n");
        sleep(1);
    }
    return 0;
}

在这个例子中,我们使用signal函数捕获了SIGINT信号(即CTRL+C键中断信号),并指定了一个名为handle_signal的信号处理函数。在主函数中,我们使用一个无限循环来模拟程序的运行,每秒钟打印一次Sleeping…。当我们按下CTRL+C键时,handle_signal函数将被调用,输出Received signal 2。

sigaction函数实例

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

void handle_signal(int sig, siginfo_t *info, void *context)
{
    printf("Received signal %d from process %d\n", sig, info->si_pid);
}

int main()
{
    struct sigaction act;
    act.sa_sigaction = handle_signal;
    sigemptyset(&act.sa_mask);
    act.sa_flags = SA_SIGINFO;
    sigaction(SIGUSR1, &act, NULL);
    while (1) {
        printf("Sleeping...\n");
        sleep(1);
    }
    return 0;
}

在这个例子中,我们使用sigaction函数捕获了SIGUSR1信号(即用户自定义信号1),并指定了一个名为handle_signal的信号处理函数。与signal函数不同的是,这里我们使用了sigaction结构体来设置信号处理函数和其他选项。在handle_signal函数中,我们输出了信号编号和发送信号的进程ID。在主函数中,我们使用一个无限循环来模拟程序的运行,每秒钟打印一次Sleeping…。当我们向进程发送SIGUSR1信号时,handle_signal函数将被调用,输出Received signal 10 from process [PID]。

释放信号(驱动)

在设备驱动和应用程序的异步通知交互中,仅仅在应用程序端捕获信号是不够的,因为信号的源头在设备驱动端。因此,应该在合适的时机让设备驱动释放信号,在设备驱动程序中增加信号释放的相关代码。为了使设备支持异步通知机制,驱动程序中涉及3项工作。

  1. 支持F_SETOWN命令,能在这个控制命令处理中设置filp->f_owner为对应进程ID。不过此项工作已由内核完成,设备驱动无须处理。

  2. 支持F_SETFL命令的处理,每当FASYNC标志改变时,驱动程序中的fasync()函数将得以执行。因此,驱动中应该实现fasync()函数。

  3. 在设备资源可获得时,调用kill_fasync()函数激发相应的信号。

驱动中的上述3项工作和应用程序中的3项工作是一一对应的,图9.2所示为异步通知处理过程中用户空间和设备驱动的交互。

jiaohu

设备驱动中异步通知编程比较简单,主要用到一项数据结构和两个函数。数据结构是fasync_struct结构体,两个函数分别是:

1)处理FASYNC标志变更的函数。

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;
};

int fasync_helper(int, struct file *, int, struct fasync_struct **);

2)释放信号用的函数。

void kill_fasync(struct fasync_struct **, int, int);

和其他的设备驱动一样,将fasync_struct结构体指针放在设备结构体中仍然是最佳选择,代码清单9.3给出了支持异步通知的设备结构体模板。

/* 1: 添加变量 */
struct xxx_dev {
    struct cdev cdev;                    /* cdev结构体*/
    ...
    struct fasync_struct *async_queue;   /* 异步结构体指针 */
};

/* 2: 新建fasync驱动函数 */
static int xxx_fasync(int fd, struct file *filp, int mode)
{
    struct xxx_dev *dev = filp->private_data;
    return fasync_helper(fd, filp, mode, &dev->async_queue);
}

/* 3: 产生异步IO信号 */
static ssize_t xxx_write(struct file *filp, const char __user *buf, size_t count, loff_t *f_pos)
{
    struct xxx_dev *dev = filp->private_data;
    ...
    /* 产生异步读信号 */
    if (dev->async_queue)
         kill_fasync(&dev->async_queue, SIGIO, POLL_IN);
...
}

/* 4: 文件关闭时,删除文件的异步通知 */
static int xxx_release(struct inode *inode, struct file *filp)
{
    /* 将文件从异步通知列表中删除 */
    xxx_fasync(-1, filp, 0);
    ...
    return 0;
}