Linux 设备驱动中的异步通知与异步 I/O(1)
成于坚持,败于止步
异步通知的概念与作用
阻塞与非阻塞访问、poll()函数提供了较好的解决设备访问的机制,但是如果有了异步通知整套机制就更加完整了。
异步通知的意思是:一旦设备就绪,则主动通知应用程序,这样应用程序根本就不需要查询设备状态,这一点非常类似于硬件上“中断”的概念,比较准确的称谓是“信号驱动的异步 I/O”。信号是在软件层次上对中断机制的一种模拟,在原理上,一个进程收到一个信号与处理器收到一个中断请求可以说是一样的。信号是异步的,一个进程不必通过任何操作来等待信号的到达,事实上,进程也不知道信号到底什么时候到达。
阻塞 I/O 意味着一直等待设备可访问后再访问,非阻塞 I/O 中使用 poll()意味着查询设备是否可访问,而异步通知则意味着设备通知自身可访问,实现了异步 I/O。由此可见,这几种方式 I/O 可以互为补充。
图 9.1 呈现了阻塞 I/O,结合 poll()的非阻塞 I/O 及异步通知在时间先后顺序上的不同。
阻塞、非阻塞 I/O、异步通知本身没有优劣,应该根据不同的应用场景合理选择。
Linux 异步通知编程
Linux 信号
使用信号进行进程间通信(IPC)是 UNIX 系统中的一种传统机制,Linux 系统也支持这种机制。在 Linux 系统中,异步通知使用信号来实现,Linux 系统中可用的信号及其定义如下所示。
信号 值 含义除了 SIGSTOP 和 SIGKILL 两个信号外,进程能够忽略或捕获其他的全部信号。一个信号被捕获的意思是当一个信号到达时有相应的代码处理它。如果一个信号没有被这个进程所捕获,内核将采用默认行为处理。
SIGHUP 1 挂起
SIGINT 2 终端中断
SIGQUIT 3 终端退出
SIGILL 4 无效命令
SIGTRAP 5 跟踪陷阱
SIGIOT 6 IOT 陷阱
SIGBUS 7 BUS 错误
SIGFPE 8 浮点异常
SIGKILL 9 强行终止(不能被捕获或忽略)
SIGUSR1 10 用户定义的信号 1
SIGSEGV 11 无效的内存段处理
SIGUSR2 12 用户定义的信号 2
SIGPIPE 13 半关闭管道发生写操作
SIGALRM 14 计时器到期
SIGTERM 15 终止
SIGSTKFLT 16 堆栈错误
SIGCHLD 17 子进程已经停止或退出
SIGCONT 18 如果停止了,继续执行
SIGSTOP 19 停止执行(不能被捕获或忽略)
SIGTSTP 20 终端停止信号
SIGTTIN 21 后台进程需要从终端读取输入
SIGTTOU 22 后台进程需要向从终端写出
SIGURG 23 紧急的套接字事件
SIGXCPU 24 超额使用 CPU 分配的时间
SIGXFSZ 25 文件尺寸超额
SIGVTALRM 26 虚拟时钟信号
SIGPROF 27 时钟信号描述
SIGWINCH 28 窗口尺寸变化
SIGIO 29 I/O
SIGPWR 30 断电重启
信号的接收
在用户程序中,为了捕获信号,可以使用 signal()函数来设置对应信号的处理函数,如下所示:
void (*signal(int signum, void (*handler))(int))(int);
该函数原型较难理解,它可以分解如下:
typedef void (*sighandler_t)(int);
sighandler_t signal(int signum, sighandler_t handler);
第一个参数指定信号的值,第二个参数指定针对前面信号值的处理函数,若为SIG_IGN,表示忽略该信号;若为 SIG_DFL,表示采用系统默认方式处理信号;若为用户自定义的函数,则信号被捕获到后,该函数将被执行。 如果 signal()调用成功,它返回最后一次为信号 signum 绑定的处理函数 handler值,失败则返回 SIG_ERR。
在进程执行时,按下[Ctrl+c]组合键将向其发出 SIGINT 信号,kill 正在运行的进程将向其发出 SIGTERM 信号,代码清单 的进程捕获这两个信号并输出信号值。
1 void sigterm_handler(int signo) 2 {
3 printf("Have caught sig N.O. %d\n", signo);
4 exit(0);
5 }
6
7 int main(void)
8 {
9 signal(SIGINT, sigterm_handler);
10 signal(SIGTERM, sigterm_handler);
11 while(1);
12
13 return 0;
14 }
除了 signal()函数外,sigaction()函数可用于改变进程接收到特定信号后的行为,它的原型如下:
int sigaction(int signum,const struct sigaction *act,struct sigaction *oldact);
该函数的第一个参数为信号的值,可以为除 SIGKILL 及 SIGSTOP 外的任何一个特定有效的信号。第二个参数是指向结构体 sigaction 的一个实例的指针,在结构体sigaction 的实例中,指定了对特定信号的处理函数,若为空,则进程会以默认方式对
信号处理。第三个参数 oldact 指向的对象用来保存原来对相应信号的处理函数,可指定 oldact 为 NULL。如果把第二、第三个参数都设为 NULL,那么该函数可用于检查信号的有效性。
先来看一个使用信号实现异步通知的例子,它通过 signal(SIGIO, input_handler)对标准输入文件描述符 STDIN_FILENO 启动信号机制。用户输入后,应用程序将接收到 SIGIO 信号,其处理函数 input_handler()将被调用,如代码清单所示。
1 #include <sys/types.h>
2 #include <sys/stat.h>
3 #include <stdio.h>
4 #include <fcntl.h>
5 #include <signal.h>
6 #include <unistd.h>
7 #define MAX_LEN 100
8 void input_handler(int num)
9 {
10 char data[MAX_LEN];
11 int len;
12
13 //读取并输出 STDIN_FILENO 上的输入
14 len = read(STDIN_FILENO, &data, MAX_LEN);
15 data[len] = 0;
16 printf("input available:%s\n", data);
17 }
18
19 main()
20 {
21 int oflags;
22
23 //启动信号驱动机制
24 signal(SIGIO, input_handler);
25 fcntl(STDIN_FILENO, F_SETOWN, getpid());
26 oflags = fcntl(STDIN_FILENO, F_GETFL);
27 fcntl(STDIN_FILENO, F_SETFL, oflags | FASYNC);
28
29 //最后进入一个死循环,仅为保持进程不终止,如果程序中没有这个死循环
30 //会立即执行完毕
31 while (1);
32 }
上述代码 24 行为 SIGIO 信号安装 input_handler()作为处理函数,第 25 行设置本进程为 STDIN_FILENO 文件的拥有者(owner),没有这一步内核不会知道应该将信号发给哪个进程。而为了启用异步通知机制,还需对设备设置 FASYNC 标志,第 26~27 行代码实现此目的。整个程序的执行效果如下:
[root@localhost driver_study]# ./signal_test
I am Chinese.
input available: I am Chinese.
I love Linux driver.
input available: I love Linux driver.
从中可以看出,当用户输入一串字符后,标准输入设备释放 SIGIO 信号,这个信号“中断”驱使对应的应用程序中的 input_handler()得以执行,将用户输入显示出来。
由此可见,为了在用户空间中能处理一个设备释放的信号,它必须完成以下 3 项工作。
通过 F_SETOWN IO 控制命令设置设备文件的拥有者为本进程,这样从设备驱动发出的信号才能被本进程接收到。
通过 F_SETFL IO 控制命令设置设备文件支持 FASYNC,即异步通知模式。
通过 signal()函数连接信号和信号处理函数。
信号的释放
在设备驱动和应用程序的异步通知交互中,仅仅在应用程序端捕获信号是不够的,因为信号没有的源头在设备驱动端。因此,应该在合适的时机让设备驱动释放信号,在设备驱动程序中增加信号释放的相关代码。
为了使设备支持异步通知机制,驱动程序中涉及以下 3 项工作。
支持 F_SETOWN 命令,能在这个控制命令处理中设置 filp->f_owner 为对应进程 ID。不过此项工作已由内核完成,设备驱动无须处理。
支持F_SETFL命令的处理,每当FASYNC标志改变时,驱动程序中的fasync()函数将得以执行。因此,驱动中应该实现 fasync()函数。
在设备资源可获得时,调用 kill_fasync()函数激发相应的信号。
驱动中的上述 3 项工作和应用程序中的 3 项工作是一一对应的,图 9.2 所示为异步通知处理过程中用户空间和设备驱动的交互。
设备驱动中异步通知编程比较简单,主要用到一项数据结构和两个函数。数据结构是 fasync_struct 结构体,两个函数分别如下。
处理 FASYNC 标志变更的函数。
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_struct 结构体指针放在设备结构体中仍然是最佳选择,代码清单给出了支持异步通知的设备结构体模板。
1 struct xxx_dev在设备驱动的 fasync() 函数中,只需要简单地将该函数的 3 个参数以及fasync_struct 结构体指针的指针作为第 4 个参数传入 fasync_helper()函数即可。代码清单给出了支持异步通知的设备驱动程序 fasync()函数的模板。
2 {
3 struct cdev cdev; /*cdev 结构体*/
4 ...
5 struct fasync_struct *async_queue; /* 异步结构体指针 */
6 };
1 static int xxx_fasync(int fd, struct file *filp, int mode) 2 { 3 struct xxx_dev *dev = filp->private_data; 4 return fasync_helper(fd, filp, mode, &dev->async_queue); 5 }在设备资源可以获得时,应该调用 kill_fasync()释放 SIGIO 信号,可读时第 3 个参数设置为 POLL_IN,可写时第 3 个参数设置为 POLL_OUT。下面代码为释放信号的范例。
1 static ssize_t xxx_write(struct file *filp, const char _ _user *buf, size_t count, 2 loff_t *f_pos) 3 { 4 struct xxx_dev *dev = filp->private_data; 5 ... 6 /* 产生异步读信号 */ 7 if (dev->async_queue) 8 kill_fasync(&dev->async_queue, SIGIO, POLL_IN); 9 ... 10 }最后,在文件关闭时,即在设备驱动的 release()函数中,应调用设备驱动的 fasync()函数将文件从异步通知的列表中删除。下面代码清单给出了支持异步通知的设备驱动release()函数的模板。
1 static int xxx_release(struct inode *inode, struct file *filp) 2 { 3 struct xxx_dev *dev = filp->private_data; 4 /* 将文件从异步通知列表中删除 */ 5 xxx_fasync(-1, filp, 0); 6 ... 7 return 0; 8 }
就到这里了,O(∩_∩)O~
我的专栏地址:http://blog.csdn.net/column/details/linux-driver-note.html
待续。。。。