简述
异步通知的意思是:一旦设备就绪,则主动通知应用程序,这样应用程序根本就不需要查询设备状态,这一点非常类似于硬件上“中断”的概念,比较准确的称谓是“信号驱动的异步I/O”。信号是在软件层次上对中断机制的一种模拟,在原理上,一个进程收到一个信号与处理器收到一个中断请求可以说是一样的。信号是异步的,一个进程不必通过任何操作来等待信号的到达,事实上,进程也不知道信号到底什么时候到达。
下图呈现了阻塞I/O,结合轮询的非阻塞I/O及基于SIGIO的异步通知在时间先后顺序上的不同。
信号
信号(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项工作。
-
支持
F_SETOWN命令
,能在这个控制命令处理中设置filp->f_owner
为对应进程ID。不过此项工作已由内核完成,设备驱动无须处理。 -
支持
F_SETFL
命令的处理,每当FASYNC
标志改变时,驱动程序中的fasync()
函数将得以执行。因此,驱动中应该实现fasync()
函数。 -
在设备资源可获得时,调用
kill_fasync()
函数激发相应的信号。
驱动中的上述3项工作和应用程序中的3项工作是一一对应的,图9.2所示为异步通知处理过程中用户空间和设备驱动的交互。
设备驱动中异步通知编程比较简单,主要用到一项数据结构和两个函数。数据结构是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;
}