1 概念:
异步通知机制:一旦设备就绪,则主动通知应用程序,这样应用程序根本就不需要查询设备状态,是一种“信号驱动的异步I/O”。
信号是在软件层次上对中断机制的一种模拟,进程收到信号与处理器收到中断请求在原理上可以说是一样的。信号是异步的,一个进程不必通过任何操作来等待信号的到达,事实上,进程也不知道信号到底什么时候会到达。
2 异步通知机制无怪乎四个方面:谁发,怎么发,发什么,发给谁。满足这四个方面也就完成了信号的异步通知。
以按键产生信号为例:驱动程序处理按键操作,用户端相应对应按键值。
谁发:异步通知一般是由驱动软件或者是硬件检测到事件发生,将事件以信号的方式发送出去,所以可认为由驱动发送信号
怎么发:1.用fcnt的F_SETOWN命令来确定用户端作为按键信号处理的“属主(owner)”,属主进程号通过getpid()获取并保存到filp->f_owner中,并被按键驱动获取到,2.还要设置 FASYNC标志,通过fcnt的 F_SETFL设置。通过这两步,驱动端检测到按键信号后发送一个SIGIO信号,通过filp->f_owner存储到id号,主动发送到用户端,用户端去响应。
发什么:发送的是SIGIO信号。
发给谁:驱动发送信号出去是为了通知用户进行相应,所以发给用户端
3 具体过程
用户端:
3.1 执行怎么发两步设置,将用户端进程ID号告诉驱动。注册 对应驱动端的fasync_helper
fcntl(fd, F_SETOWN, getpid()); //设置用户进程为fd文件的拥有者,没有这一步,内核不会知道应该将信号发给哪个进程
oflags = fcntl(fd, F_GETFL); //获取设备文件的f_flags
fcntl(fd, F_SETFL, oflags | FASYNC); //为了启用异步通知机制,还需对设备设置FASYNC标志
3.2 客户端声明一个信号signal(SIGIO, &handler);,其中handle对应信号处理函数,保存接收到信号后的处理方式。可以默认打印输出。
驱动端:
3.3 在驱动端的file_operations中添加fasync
static const struct file_operations f_ops =
{
.owner = THIS_MODULE,
......
.fasync = my_fasync,
};
3.4 my_fasync中放入函数fasync_helper
//当一个用户端的FASYNC标志被修改时,调用驱动程序的fasync方法间接调用fasync_helper函数以便将当前进程加入到驱动程序的异步通知等待队列中。
static int my_fasync(int fd, struct file * file, int on)
{
err = fasync_helper(fd, file, on, &button); //根据传过来的ID
return 0;
}
//当设备可访问时,可使用kill_fasync函数发信号所有的相关进程。进程进而调用绑定的消息处理函数。
kill_fasync(&ctl->fasync, SIGIO, POLL_IN);
//分析fasync_helper的内部实现
int fasync_helper(int fd, struct file * filp, int on, struct fasync_struct **fapp)
{
struct fasync_struct *fa, **fp;
struct fasync_struct *new = NULL;
int result = 0;
if (on) {
new = kmem_cache_alloc(fasync_cache, GFP_KERNEL);
if (!new)
return -ENOMEM;
}
write_lock_irq(&fasync_lock);
//遍历整个异步通知队列,看是否存在对应的文件指针
for (fp = fapp; (fa = *fp) != NULL; fp = &fa->fa_next) {
if (fa->fa_file == filp) {//已存在
if(on) {
fa->fa_fd = fd;//文件描述符赋值 //注:不明白为什么这里只需要更新文件描述符,而不需要更新文件指针
kmem_cache_free(fasync_cache, new);//销毁刚创建的对象
} else {
*fp = fa->fa_next;
kmem_cache_free(fasync_cache, fa);//删除非目标对象 此用于应用程序屏蔽异步通知.
result = 1;
}
goto out;
}
}
//看到下面可以得知,所谓的把进程添加到异步通知队列中
//实则是将文件指针关联到异步结构体对象,然后将该对象挂载在异步通知队列中(等待队列也是这个原理)
//那么最后发送信号又是怎么知道是哪个进程的呢?我们看后面的kill_fasync函数。
if (on) {//不存在
new->magic = FASYNC_MAGIC;
new->fa_file = filp;//指定文件指针
new->fa_fd = fd;//指定文件描述符
new->fa_next = *fapp;//挂载在异步通知队列中
*fapp = new;//挂载
result = 1;
}
out:
write_unlock_irq(&fasync_lock);
return result;
}
//看看kill_fasync函数是怎么将信号通知指定进程的:
void __kill_fasync(struct fasync_struct *fa, int sig, int band)
{
while (fa) {
struct fown_struct * fown;
if (fa->magic != FASYNC_MAGIC) {
printk(KERN_ERR "kill_fasync: bad magic number in "
"fasync_struct!\n");
return;
}
fown = &fa->fa_file->f_owner;//这里便是回答上面的问题,如果知道是哪个进程的,通过异步对象的文件指针知道其属主进程
/* Don't send SIGURG to processes which have not set a queued signum: SIGURG has its own default signalling
mechanism. */
if (!(sig == SIGURG && fown->signum == 0))
send_sigio(fown, fa->fa_fd, band);//发送信号
fa = fa->fa_next;
}
}
void kill_fasync(struct fasync_struct **fp, int sig, int band)
{
/* First a quick test without locking: usually
* the list is empty.
*/
if (*fp) {
read_lock(&fasync_lock);
/* reread *fp after obtaining the lock */
__kill_fasync(*fp, sig, band);
read_unlock(&fasync_lock);
}
}
总结:用户端使用fcntl获取当前PID,并将PID和FANSYNC发送到驱动端(注册),驱动端收到后将其存入fasync_struct的结构链条中,按键触发后kill_fasync,通过其中的函数__kill_fasync遍历整个fasync_struct结构链条,找出对应PID,通过send_sigio将信号发送给用户端进程,用户端获取到主动上传的信号后跳转到与信号绑定的信号处理函数中,完成一次信号触发。
这里是上面64种信号的说明:
信号 | 起源 | 默认行为 | 含义 |
SIGHUP | POSIX | Term | 控制终端挂起 |
SIGINT | ANSI | Term | 键盘输入以终端进程(ctrl + C) |
SIGQUIT | POSIX | Core | 键盘输入使进程退出(Ctrl + \) |
SIGILL | ANSI | Core | 非法指令 |
SIGTRAP | POSIX | Core | 断点陷阱,用于调试 |
SIGABRT | ANSI | Core | 进程调用abort函数时生成该信号 |
SIGIOT | 4.2BSD | Core | 和SIGABRT相同 |
SIGBUS | 4.2BSD | Core | 总线错误,错误内存访问 |
SIGFPE | ANSI | Core | 浮点异常 |
SIGKILL | POSIX | Term | 终止一个进程。该信号不可被捕获或被忽略 |
SIGUSR1 | POSIX | Term | 用户自定义信号之一 |
SIGSEGV | ANSI | Core | 非法内存段使用 |
SIGUSR2 | POSIX | Term | 用户自定义信号二 |
SIGPIPE | POSIX | Term | 往读端关闭的管道或socket链接中写数据 |
SIGALRM | POSIX | Term | 由alarm或settimer设置的实时闹钟超时引起 |
SIGTERM | ANSI | Term | 终止进程。kill命令默认发生的信号就是SIGTERM |
SIGSTKFLT | Linux | Term | 早期的Linux使用该信号来报告数学协处理器栈错误 |
SIGCLD | System V | Ign | 和SIGCHLD相同 |
SIGCHLD | POSIX | Ign | 子进程状态发生变化(退出或暂停) |
SIGCONT | POSIX | Cont | 启动被暂停的进程(Ctrl+Q)。如果目标进程未处于暂停状态,则信号被忽略 |
SIGSTOP | POSIX | Stop | 暂停进程(Ctrl+S)。该信号不可被捕捉或被忽略 |
SIGTSTP | POSIX | Stop | 挂起进程(Ctrl+Z) |
SIGTTIN | POSIX | Stop | 后台进程试图从终端读取输入 |
SIGTTOU | POSIX | Stop | 后台进程试图往终端输出内容 |
SIGURG | 4.3 BSD | Ign | socket连接上接收到紧急数据 |
SIGXCPU | 4.2 BSD | Core | 进程的CPU使用时间超过其软限制 |
SIGXFSZ | 4.2 BSD | Core | 文件尺寸超过其软限制 |
SIGVTALRM | 4.2 BSD | Term | 与SIGALRM类似,不过它只统计本进程用户空间代码的运行时间 |
SIGPROF | 4.2 BSD | Term | 与SIGALRM 类似,它同时统计用户代码和内核的运行时间 |
SIGWINCH | 4.3 BSD | Ign | 终端窗口大小发生变化 |
SIGPOLL | System V | Term | 与SIGIO类似 |
SIGIO | 4.2 BSD | Term | IO就绪,比如socket上发生可读、可写事件。因为TCP服务器可触发SIGIO的条件很多,故而SIGIO无法在TCP服务器中用。SIGIO信号可用在UDP服务器中,但也很少见 |
SIGPWR | System V | Term | 对于UPS的系统,当电池电量过低时,SIGPWR信号被触发 |
SIGSYS | POSIX | Core | 非法系统调用 |
SIGUNUSED |
| Core | 保留,通常和SIGSYS效果相同 |