阻塞与非阻塞访问、 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;
}
标签:异步,struct,int,通知,fasync,信号,进程 From: https://www.cnblogs.com/zhiminyu/p/17614796.html