异步通知

阻塞与非阻塞访问、 poll()函数提供了较好的解决设备访问的机制, 但是如果有了异步通知, 整套机制则更加完整了。

在设备驱动中使用异步通知可以使得在进行对设备的访问时, 由驱动主动通知应用程序进行访问。 这样, 使用非阻塞I/O的应用程序无须轮询设备是否可访问, 而阻塞访问也可以被类似“中断”的异步通知所取代。
除了异步通知以外, 应用还可以在发起I/O请求后, 立即返回。 之后, 再查询I/O完成情况, 或者I/O完成后被调回。 这个过程叫作异步I/O。

异步通知的意思是: 一旦设备就绪, 则主动通知应用程序, 这样应用程序根本就不需要查询设备状态, 这一点非常类似于硬件上“中断”的概念, 比较准确的称谓是“信号驱动的异步I/O”。 信号是在软件层次上对中断机制的一种模拟, 在原理上, 一个进程收到一个信号与处理器收到一个中断请求可以说是一样的。 信号是异步的, 一个进程不必通过任何操作来等待信号的到达, 事实上, 进程也不知道信号到底什么时候到达。阻塞I/O意味着一直等待设备可访问后再访问, 非阻塞I/O中使用poll()意味着查询设备是否可访问, 而异步通知则意味着设备通知用户自身可访问, 之后用户再进行I/O处理,即:由驱动发起,主动通知应用程序。 由此可见, 这几种I/O方式可以相互补充。

 

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

 

 

 

注:阻塞、 非阻塞I/O、 异步通知本身没有优劣, 应该根据不同的应用场景合理选择

使用信号进行进程间通信(IPC) 是UNIX中的一种传统机制, Linux也支持这种机制。 在Linux中, 异步通知使用信号来实现。

在linux终端中输入“kill -l ”命令即可查看所有支持的信号,如下所示:

编号为1 ~ 31的信号为传统UNIX支持的信号,是不可靠信号(非实时的),编号为32 ~ 64的信号是后来扩充的,称做可靠信号(实时信号)。不可靠信号和可靠信号的区别在于前者不支持排队,可能会造成信号丢失,而后者不会。

 

传统UNIX支持的信号及其定义如下表所示:

SIGABRTSIGFPE

信号 含义
SIGHUP 1

本信号在用户终端连接(正常或非正常)结束时发出, 通常是在终端的控制进程结束时, 通知同一session内的各个作业, 这时它们与控制终端不再关联。

登录Linux时,系统会分配给登录用户一个终端(Session)。在这个终端运行的所有程序,包括前台进程组和后台进程组,一般都属于这个Session。当用户退出Linux登录时,前台进程组和后台有对终端输出的进程将会收到SIGHUP信号。这个信号的默认操作为终止进程,因此前台进程组和后台有终端输出的进程就会中止。不过可以捕获这个信号,比如wget能捕获SIGHUP信号,并忽略它,这样就算退出了Linux登录,wget也能继续下载。

此外,对于与终端脱离关系的守护进程,这个信号用于通知它重新读取配置文件。

SIGINT 2 程序终止(interrupt)信号, 在用户键入INTR字符(通常是Ctrl-C)时发出,用于通知前台进程组终止进程。
SIGQUIT 3 和SIGINT类似, 但由QUIT字符(通常是Ctrl-/)来控制. 进程在因收到SIGQUIT退出时会产生core文件, 在这个意义上类似于一个程序错误信号。
SIGILL 4 执行了非法指令. 通常是因为可执行文件本身出现错误, 或者试图执行数据段. 堆栈溢出时也有可能产生这个信号。
SIGTRAP 5 由断点指令或其它trap指令产生. 由debugger使用。
6 调用abort函数生成的信号。  
SIGBUS 7 非法地址, 包括内存地址对齐(alignment)出错。比如访问一个四个字长的整数, 但其地址不是4的倍数。它与SIGSEGV的区别在于后者是由于对合法存储地址的非法访问触发的(如访问不属于自己存储空间或只读存储空间)。
SIGFPE 8 在发生致命的算术运算错误时发出. 不仅包括浮点运算错误, 还包括溢出及除数为0等其它所有的算术的错误
SIGKILL 9 用来立即结束程序的运行. 本信号不能被阻塞、处理和忽略。如果管理员发现某个进程终止不了,可尝试发送这个信号。
SIGUSR1 10 留给用户使用
SIGSEGV 11 试图访问未分配给自己的内存, 或试图往没有写权限的内存地址写数据.
SIGUSR2 12 留给用户使用
SIGPIPE 13 管道破裂。这个信号通常在进程间通信产生,比如采用FIFO(管道)通信的两个进程,读管道没打开或者意外终止就往管道写,写进程会收到SIGPIPE信号。此外用Socket通信的两个进程,写进程在写Socket的时候,读进程已经终止。
SIGALRM 14 时钟定时信号, 计算的是实际的时间或时钟时间. alarm函数使用该信号.
SIGTERM 15 程序结束(terminate)信号, 与SIGKILL不同的是该信号可以被阻塞和处理。通常用来要求程序自己正常退出,shell命令kill缺省产生这个信号。如果进程终止不了,我们才会尝试SIGKILL。
 SIGSTKFLT 16 linux专用,数学协处理器的栈异常
SIGCHLD 17 子进程结束时, 父进程会收到这个信号。

如果父进程没有处理这个信号,也没有等待(wait)子进程,子进程虽然终止,但是还会在内核进程表中占有表项,这时的子进程称为僵尸进程。这种情况我们应该避免(父进程或者忽略SIGCHILD信号,或者捕捉它,或者wait它派生的子进程,或者父进程先终止,这时子进程的终止自动由init进程来接管)。

 SIGCONT 18 让一个停止(stopped)的进程继续执行. 本信号不能被阻塞. 可以用一个handler来让程序在由stopped状态变为继续执行时完成特定的工作. 例如, 重新显示提示符
SIGSTOP 19 停止(stopped)进程的执行. 注意它和terminate以及interrupt的区别:该进程还未结束, 只是暂停执行. 本信号不能被阻塞, 处理或忽略.
SIGTSTP 20 停止进程的运行, 但该信号可以被处理和忽略. 用户键入SUSP字符时(通常是Ctrl-Z)发出这个信号
SIGTTIN 21 当后台作业要从用户终端读数据时, 该作业中的所有进程会收到SIGTTIN信号. 缺省时这些进程会停止执行.
SIGTTOU 22 类似于SIGTTIN, 但在写终端(或修改终端模式)时收到.
SIGURG 23 有"紧急"数据或out-of-band数据到达socket时产生.
SIGXCPU 24 超过CPU时间资源限制. 这个限制可以由getrlimit/setrlimit来读取/改变。
SIGXFSZ 25 当进程企图扩大文件以至于超过文件大小资源限制。
SIGVTALRM 26 虚拟时钟信号. 类似于SIGALRM, 但是计算的是该进程占用的CPU时间.
SIGPROF 27 类似于SIGALRM/SIGVTALRM, 但包括该进程用的CPU时间以及系统调用的时间.
SIGWINCH 28 窗口大小改变时发出.
SIGIO 29 文件描述符准备就绪, 可以开始进行输入/输出操作.
SIGPWR 30 Power failure
SIGSYS 31 非法的系统调用。

在以上列出的信号中,程序不可捕获、阻塞或忽略的信号有:SIGKILL,SIGSTOP
不能恢复至默认动作的信号有:SIGILL,SIGTRAP
默认会导致进程流产的信号有:SIGABRT,SIGBUS,SIGFPE,SIGILL,SIGIOT,SIGQUIT,

SIGSEGV,SIGTRAP,SIGXCPU,SIGXFSZ
默认会导致进程退出的信号有:SIGALRM,SIGHUP,SIGINT,SIGKILL,SIGPIPE,SIGPOLL,

SIGPROF,SIGSYS,SIGTERM,SIGUSR1,SIGUSR2,

SIGVTALRM
默认会导致进程停止的信号有:SIGSTOP,SIGTSTP,SIGTTIN,SIGTTOU
默认进程忽略的信号有:SIGCHLD,SIGPWR,SIGURG,SIGWINCH

此外,SIGIO在SVR4是退出,在4.3BSD中是忽略;SIGCONT在进程挂起时是继续,否则是忽略,不能被阻塞。 

一个信号被捕获的意思是当一个信号到达时有相应的代码处理它。 如果一个信号没有被这个进程所捕获, 内核将采用默认行为处理。

信号的捕获

1、函数介绍

在用户程序中, 为了捕获信号, 可以使用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信号, 以下代码的进程可捕获这两个信号并输出信号值:

void sigterm_handler(int signo)
{
    printf("Have caught sig N.O. %d\n", signo);
    exit(0);
}
int main(void)
{
    signal(SIGINT, sigterm_handler);
    signal(SIGTERM, sigterm_handler);
    while(1);
    return 0;

}

除了signal()函数外, sigaction()函数可用于改变进程接收到特定信号后的行为, 它的原型为:
int sigaction(int signum,const struct sigaction *act,struct sigaction *oldact));
第一个参数为信号的值, 可以是除SIGKILL及SIGSTOP外的任何一个特定有效的信号。 

第二个参数是指向结构体sigaction的一个实例的指针, 在结构体sigaction的实例中, 指定了对特定信号的处理函数, 若为空, 则进程会以缺省方式对信号处理; 

第三个参数oldact指向的对象用来保存原来对相应信号的处理函数, 可指定oldact为NULL。 如果把第二、 第三个参数都设为NULL, 那么该函数可用于检查信号的有效性。

2、例子说明

下面是一个使用信号实现异步通知的例子, 它通过signal(SIGIO, input_handler) 对标准输入文件描述符STDIN_FILENO启动信号机制。 用户输入后, 应用程序将接收到SIGIO信号, 其处理函数input_handler()将被调用, 如代码所示:


#include <sys/types.h>
#include <sys/stat.h>
#include <stdio.h>
#include <fcntl.h>
#include <signal.h>
#include <unistd.h>
#define MAX_LEN 100


void input_handler(int num)
{
    char data[MAX_LEN];
    int len;

    /* 读取并输出STDIN_FILENO上的输入 */
    len = read(STDIN_FILENO, &data, MAX_LEN);
    data[len] = 0;
    printf("input available:%s\n", data);
}

void main()
{
    int oflags;

    /* 启动信号驱动机制 */

    /*为SIGIO信号安装input_handler()作为处理函数*/
    signal(SIGIO, input_handler);

    /*设置本进程为STDIN_FILENO文件的拥有者,

     没有这一步,内核不会知道应该将信号发给哪个进程*/

    fcntl(STDIN_FILENO, F_SETOWN, getpid());

    /*而为了启用异步通知机制, 还需对设备设置FASYNC标志,     下面两行行代码可实现此目的。 */
    oflags = fcntl(STDIN_FILENO, F_GETFL);
    fcntl(STDIN_FILENO, F_SETFL, oflags | FASYNC);

    /* 最后进入一个死循环, 仅为保持进程不终止, 如果程序中
    没有这个死循会立即执行完毕 */
    while (1);
}

整个程序的执行效果如下图:

 


从中可以看出, 当用户输入一串字符后(如:hello world和hello

linux), 标准输入设备释放SIGIO信号, 这个信号“中断”与驱使对应的应用程序中的input_handler()得以执行, 并将用户输入显示出来。

由此可见, 为了能在用户空间中处理一个设备释放的信号, 它必须完成3项工作。
1) 通过F_SETOWN IO控制命令设置设备文件的拥有者为本进程, 这样从设备驱动发出的信号才能被本进程接收到。
2) 通过F_SETFL IO控制命令设置设备文件以支持FASYNC, 即异步通知模式。
3) 通过signal()函数连接信号和信号处理函数。

 

信号的释放

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


为了使设备支持异步通知机制, 驱动程序中涉及3项工作。
1) 支持F_SETOWN命令, 能在这个控制命令处理中设置filp->f_owner为对应进程ID。 不过此项工作已由内核完成, 设备驱动无须处理。
2) 支持F_SETFL命令的处理, 每当FASYNC标志改变时, 驱动程序中的fasync()函数将得以执行。因此, 驱动中应该实现fasync()函数。
3) 在设备资源可获得时, 调用kill_fasync()函数激发相应的信号。
驱动中的上述3项工作和应用程序中的3项工作是一一对应的, 如下图所示为异步通知处理过程中用户空间和设备驱动的交互:

 

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

数据结构是fasync_struct结构体, 两个函数分别是:
1) 处理FASYNC标志变更的函数。
int fasync_helper(int fd, struct file *filp, int mode, struct fasync_struct **fa);
2) 释放信号用的函数。
void kill_fasync(struct fasync_struct **fa, int sig, int band);

和其他的设备驱动一样, 将fasync_struct结构体指针放在设备结构体中仍然是最佳选择, 如下代码清单给出了支持异步通知的设备结构体模板:
struct xxx_dev {
    struct cdev cdev; /* cdev结构体*/
    ...
    struct fasync_struct *async_queue; /* 异步结构体指针 */
};
在设备驱动的fasync()函数中, 只需要简单地将该函数的3个参数以及fasync_struct结构体指针的指针作为第4个参数传入fasync_helper()函数即可。

下面的代码清单给出了支持异步通知的设备驱动程序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);
}
在设备资源可以获得时,应该调用kill_fasync()释放SIGIO信号。 在可读时, 第3个参数设置为POLL_IN, 在可写时, 第3个参数设置为POLL_OUT。

下面的代码清单给出了释放信号的范例:
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);
    ...
}
最后, 在文件关闭时, 即在设备驱动的release()函数中,应调用设备驱动的fasync()函数将文件从异步通知的列表中删除。

下面的代码给出了支持异步通知的设备驱动release()函数的模板。
static int xxx_release(struct inode *inode, struct file *filp)
{
    /* 将文件从异步通知列表中删除 */
    xxx_fasync(-1, filp, 0);
    ...
    return 0;
}

 

posted @ 2023-08-08 16:48  burlingame  阅读(55)  评论(0编辑  收藏  举报