彻底弄懂Linux驱动中的异步通知的概念和作用

您所在的位置:网站首页 异步通信 彻底弄懂Linux驱动中的异步通知的概念和作用

彻底弄懂Linux驱动中的异步通知的概念和作用

2023-11-23 08:49| 来源: 网络整理| 查看: 265

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

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

【文章福利】小编推荐自己的Linux内核源码交流群:【869634926】整理了一些个人觉得比较好的学习书籍、视频资料共享在群文件里面,有需要的可以自行添加哦!!!前50名可进群领取,并额外赠送一份价值600的内核资料包(含视频教程、电子书、实战项目及代码)!点击下方链接即可免费领取内核相关学习资料哦

学习直通车:Linux内核源码/内存调优/文件系统/进程管理/设备驱动/网络协议栈

学习目标

1、掌握异步通知的概念和作用

2、掌握Linux异步通知的编程方法

开发工具

硬件工具1)exynos4412 开发板3)PC 机4)串口软件工具1)虚拟机 Vmware2)Ubuntu12.04.23)超级终端(串口助手)

01 异步通知的概念和作用

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

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

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

02 Linux的信号

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

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

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

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

SIGABRTSIGFPE

信号值含义SIGHUP1本信号在用户终端连接(正常或非正常)结束时发出, 通常是在终端的控制进程结束时, 通知同一session内的各个作业, 这时它们与控制终端不再关联。登录Linux时,系统会分配给登录用户一个终端(Session)。在这个终端运行的所有程序,包括前台进程组和后台进程组,一般都属于这个Session。当用户退出Linux登录时,前台进程组和后台有对终端输出的进程将会收到SIGHUP信号。这个信号的默认操作为终止进程,因此前台进程组和后台有终端输出的进程就会中止。不过可以捕获这个信号,比如wget能捕获SIGHUP信号,并忽略它,这样就算退出了Linux登录,wget也能继续下载。此外,对于与终端脱离关系的守护进程,这个信号用于通知它重新读取配置文件。SIGINT2程序终止(interrupt)信号, 在用户键入INTR字符(通常是Ctrl-C)时发出,用于通知前台进程组终止进程。SIGQUIT3和SIGINT类似, 但由QUIT字符(通常是Ctrl-/)来控制. 进程在因收到SIGQUIT退出时会产生core文件, 在这个意义上类似于一个程序错误信号。SIGILL4执行了非法指令. 通常是因为可执行文件本身出现错误, 或者试图执行数据段. 堆栈溢出时也有可能产生这个信号。SIGTRAP5由断点指令或其它trap指令产生. 由debugger使用。6调用abort函数生成的信号。SIGBUS7非法地址, 包括内存地址对齐(alignment)出错。比如访问一个四个字长的整数, 但其地址不是4的倍数。它与SIGSEGV的区别在于后者是由于对合法存储地址的非法访问触发的(如访问不属于自己存储空间或只读存储空间)。SIGFPE8在发生致命的算术运算错误时发出. 不仅包括浮点运算错误, 还包括溢出及除数为0等其它所有的算术的错误SIGKILL9用来立即结束程序的运行. 本信号不能被阻塞、处理和忽略。如果管理员发现某个进程终止不了,可尝试发送这个信号。SIGUSR110留给用户使用SIGSEGV11试图访问未分配给自己的内存, 或试图往没有写权限的内存地址写数据.SIGUSR212留给用户使用SIGPIPE13管道破裂。这个信号通常在进程间通信产生,比如采用FIFO(管道)通信的两个进程,读管道没打开或者意外终止就往管道写,写进程会收到SIGPIPE信号。此外用Socket通信的两个进程,写进程在写Socket的时候,读进程已经终止。SIGALRM14时钟定时信号, 计算的是实际的时间或时钟时间. alarm函数使用该信号.SIGTERM15程序结束(terminate)信号, 与SIGKILL不同的是该信号可以被阻塞和处理。通常用来要求程序自己正常退出,shell命令kill缺省产生这个信号。如果进程终止不了,我们才会尝试SIGKILL。SIGSTKFLT16linux专用,数学协处理器的栈异常SIGCHLD17子进程结束时, 父进程会收到这个信号。如果父进程没有处理这个信号,也没有等待(wait)子进程,子进程虽然终止,但是还会在内核进程表中占有表项,这时的子进程称为僵尸进程。这种情况我们应该避免(父进程或者忽略SIGCHILD信号,或者捕捉它,或者wait它派生的子进程,或者父进程先终止,这时子进程的终止自动由init进程来接管)。SIGCONT18让一个停止(stopped)的进程继续执行. 本信号不能被阻塞. 可以用一个handler来让程序在由stopped状态变为继续执行时完成特定的工作. 例如, 重新显示提示符SIGSTOP19停止(stopped)进程的执行. 注意它和terminate以及interrupt的区别:该进程还未结束, 只是暂停执行. 本信号不能被阻塞, 处理或忽略.SIGTSTP20停止进程的运行, 但该信号可以被处理和忽略. 用户键入SUSP字符时(通常是Ctrl-Z)发出这个信号SIGTTIN21当后台作业要从用户终端读数据时, 该作业中的所有进程会收到SIGTTIN信号. 缺省时这些进程会停止执行.SIGTTOU22类似于SIGTTIN, 但在写终端(或修改终端模式)时收到.SIGURG23有"紧急"数据或out-of-band数据到达socket时产生.SIGXCPU24超过CPU时间资源限制. 这个限制可以由getrlimit/setrlimit来读取/改变。SIGXFSZ25当进程企图扩大文件以至于超过文件大小资源限制。SIGVTALRM26虚拟时钟信号. 类似于SIGALRM, 但是计算的是该进程占用的CPU时间.SIGPROF27类似于SIGALRM/SIGVTALRM, 但包括该进程用的CPU时间以及系统调用的时间.SIGWINCH28窗口大小改变时发出.SIGIO29文件描述符准备就绪, 可以开始进行输入/输出操作.SIGPWR30Power failureSIGSYS31非法的系统调用。

在以上列出的信号中,程序不可捕获、阻塞或忽略的信号有: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在进程挂起时是继续,否则是忽略,不能被阻塞。

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

03 信号的捕获

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 #include #include #include #include #include #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()函数连接信号和信号处理函数。

04 信号的释放

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

为了使设备支持异步通知机制, 驱动程序中涉及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; } 文献参考:https://mp.weixin.qq.com/s/7znj6jyWYMo1hk3RRb-cfw(版权归原作者所有,侵删)

猜你喜欢

最新干货!使用eBPF LSM热修复Linux内核漏洞

深度剖析Linux内核通用链表与内存池的使用

盘点那些Linux内核调试手段——内核打印

Linux 环境下网络分析和抓包是怎么操作的?

浅谈ARM64Linux内核页表的块映射

解决Linux内核调测两大难题:内存被改与内存泄露



【本文地址】


今日新闻


推荐新闻


CopyRight 2018-2019 办公设备维修网 版权所有 豫ICP备15022753号-3