LV06-07-IO模型-04-信号驱动IO
来详细了解一下信号驱动IO?若笔记中有错误或者不合适的地方,欢迎批评指正😃。
点击查看使用工具及版本
PC端开发环境 | Windows | Windows11 |
Ubuntu | Ubuntu20.04.2的64位版本 | |
VMware® Workstation 17 Pro | 17.6.0 build-24238078 | |
终端软件 | MobaXterm(Professional Edition v23.0 Build 5042 (license)) | |
Win32DiskImager | Win32DiskImager v1.0 | |
Linux开发板环境 | Linux开发板 | 正点原子 i.MX6ULL Linux 阿尔法开发板 |
uboot | NXP官方提供的uboot,使用的uboot版本为U-Boot 2019.04 | |
linux内核 | linux-4.19.71(NXP官方提供) |
点击查看本文参考资料
分类 | 网址 | 说明 |
官方网站 | https://www.arm.com/ | ARM官方网站,在这里我们可以找到Cotex-Mx以及ARMVx的一些文档 |
https://www.nxp.com.cn/ | NXP官方网站 | |
https://www.nxpic.org.cn/ | NXP 官方社区 | |
https://u-boot.readthedocs.io/en/latest/ | u-boot官网 | |
https://www.kernel.org/ | linux内核官网 |
点击查看相关文件下载
分类 | 网址 | 说明 |
NXP | https://github.com/nxp-imx | NXP imx开发资源GitHub组织,里边会有u-boot和linux内核的仓库 |
nxp-imx/linux-imx/releases/tag/v4.19.71 | NXP linux内核仓库tags中的v4.19.71 | |
nxp-imx/uboot-imx/releases/tag/rel_imx_4.19.35_1.1.0 | NXP u-boot仓库tags中的rel_imx_4.19.35_1.1.0 | |
I.MX6ULL | i.MX 6ULL Applications Processors for Industrial Products | I.MX6ULL 芯片手册(datasheet,可以在线查看) |
i.MX 6ULL Applications ProcessorReference Manual | I.MX6ULL 参考手册(下载后才能查看,需要登录NXP官网) | |
Source Code | https://elixir.bootlin.com/linux/latest/source | linux kernel源码 |
kernel/git/stable/linux.git - Linux kernel stable tree | linux kernel源码(官网,tag 4.19.71) | |
https://elixir.bootlin.com/u-boot/latest/source | uboot源码 |
一、信号
1. 什么是信号?
我们首先来回顾一下“中断”,中断是处理器提供的一种异步机制,我们配置好中断以后就可以让处理器去处理其他的事情了,当中断发生以后会触发我们事先设置好的中断服务函数,在中断服务函数中做具体的处理。比如我们在裸机里学习的 GPIO 按键中断,我们通过按键去开关蜂鸣器,采用中断以后处理器就不需要时刻的去查看按键有没有被按下,因为按键按下以后会自动触发中断。
同样的, Linux 应用程序可以通过阻塞或者非阻塞这两种方式来访问驱动设备,通过阻塞方式访问的话应用程序会处于休眠态,等待驱动设备可以使用,非阻塞方式的话会通过 poll 函数来不断的轮询,查看驱动设备文件是否可以使用。这两种方式都需要应用程序主动的去查询设备的使用情况,如果能提供一种类似中断的机制,当驱动程序可以访问的时候主动告诉应用程序那就最好了。
“信号”为此应运而生,信号类似于我们硬件上使用的“中断”,只不过信号是软件层次上的。算是在软件层次上对中断的一种模拟,驱动可以通过主动向应用程序发送信号的方式来报告自己可以访问了,应用程序获取到信号以后就可以从驱动设备中读取或者写入数据了。整个过程就相当于应用程序收到了驱动发送过来了的一个中断,然后应用程序去响应这个中断,在整个处理过程中应用程序并没有去查询驱动设备是否可以访问,一切都是由驱动设备自己告诉给应用程序的。
信号其实就是一个软件中断。
2. 有哪些信号?
我们可以使用以下命令查看linux下支持的信号:
1 | root@alpha-imx6ull:# kill -l |
这些信号的含义在应用编程的时候都了解过了,这里就不在多说,他们在内核中是有宏定义的,有多个头文件中都有的样子,但是值都是一样的,例如 signal.h - include/uapi/asm-generic/signal.h
点击查看详情
1 |
|
3. 如何使用信号?
信号一般需要在应用程序中使用,如果要在应用程序中使用信号,那么就必须设置信号所使用的信号处理函数 。可以看这篇笔记《LV05-05-进程通信-03-信号 | 苏木》
3.1 signal()
signal 函数原型如下所示:
1 | sighandler_t signal(int signum, sighandler_t handler) |
该函数用于注册一个信号的处理函数。
参数说明:
signum:要设置处理函数的信号。
handler: 信号的处理函数。
返回值: 设置成功的话返回信号的前一个处理函数,设置失败的话返回 SIG_ERR。
3.2 信号处理函数
信号处理函数原型如下所示 :
1 | typedef void (*sighandler_t)(int) |
3.3 fcntl()
可以使用man fcntl命令在linux中查看函数的帮助手册:
1 | NAME |
该函数用于对一个打开的文件描述符执行一系列控制操作。
函数参数:
- fd:被操作的文件描述符
- cmd:操作文件描述符的命令, cmd 参数决定了要如何操作文件描述符 fd
- …:根据 cmd 的参数来决定是不是需要使用第三个参数
返回值:失败返回-1,并设置errno。
操作文件描述符的命令如下表
命令名 | 描述 |
---|---|
F_DUPFD | 复制文件描述符 |
F_GETFD | 获取文件描述符标志 |
F_SETFD | 设置文件描述符标志 |
F_GETFL | 获取文件状态标志 |
F_SETFL | 设置文件状态标志 |
F_GETLK | 获取文件锁 |
F_SETLK | 设置文件锁 |
F_SETLKW | 类似 F_SETLK, 但等待返回 |
F_GETOWN | 获取当前接收 SIGIO 和 SIGURG 信号的进程 ID 和进程组 ID |
F_SETOWN | 设置当前接收 SIGIO 和 SIGURG 信号的进程 ID 和进程组 ID |
二、信号驱动IO
1. 什么是信号驱动IO
信号驱动 IO 不需要应用程序查询设备的状态, 一旦设备准备就绪, 会触发 SIGIO 信号, 进而调用注册的处理函数。
仍旧以钓鱼为例。 小马同学喜欢吃新鲜的鱼, 但是不想自己钓, 所以他请了一个助手来帮他钓鱼, 他自己去忙其他的事情(进程不阻塞, 立即返回) 。 如果有鱼上钩助手会帮忙钓上来(将数据拷贝到指定的缓冲区) , 并立即通知小马同学回来把鱼取走(处理数据) 。
2. 相关数据结构与API
2.1 struct fasync_struct
struct fasync_struct 结构体定义如下:
1 | struct fasync_struct { |
一般将 struct fasync_struct 结构体指针变量定义到设备结构体中 ,例如:
1 | typedef struct __CHAR_DEVICE |
2.2 fasync 函数
2.2.1 函数说明
如果要使用异步通知,需要在设备驱动中实现 file_operations 操作集中的 file_operations.fasync 函数
1 | int (*fasync) (int, struct file *, int); |
file_operations.fasync 函数里面一般通过调用 fasync_helper() 函数来初始化前面定义的 struct fasync_struct 结构体指针, fasync_helper() 函数原型如下:
1 | int fasync_helper(int fd, struct file * filp, int on, struct fasync_struct **fapp) |
fasync_helper() 函数的前三个参数就是 fasync 函数的那三个参数,第四个参数就是要初始化的 struct fasync_struct 结构体指针变量。当应用程序通过“fcntl(fd, F_SETFL, flags | FASYNC)”改变fasync 标记的时候,驱动程序 file_operations 操作集中的 file_operations.fasync 函数就会执行。
2.2.2 参考示例
驱动程序中的 fasync 函数参考示例如下:
1 | struct xxx_dev |
在关闭驱动文件的时候需要在 file_operations 操作集中的 file_operations.release 函数中释放 struct fasync_struct, struct fasync_struct 的释放函数同样为 fasync_helper() , file_operations.release 函数参数参考实例如下:
1 | static int xxx_release(struct inode *pInode, struct file *pFilp) |
2.3 kill_fasync()
当设备可以访问的时候,驱动程序需要向应用程序发出信号,此时我们在应用程序注册的 SIGIO 信号处理函数就会被执行。 kill_fasync() 函数负责发送指定的信号:
1 | void kill_fasync(struct fasync_struct **fp, int sig, int band) |
函数参数:
- fp:要操作的 struct fasync_struct
- sig:发送的信号
- band: 可读的时候设置成 POLLIN , 可写的时候设置成 POLLOUT
返回值: 无。
3. 应用程序的处理
如果要实现信号驱动 IO, 需要应用程序和驱动程序配合, 应用程序使用信号驱动 IO 的步骤 如下:
(1)注册信号处理函数 应用程序使用 signal 函数来注册 SIGIO 信号的信号处理函数。
(2)将本应用程序的进程号告诉给内核 。
这一步需要使用fcntl()函数将本应用程序的进程号告诉给内核:
1 | fcntl(fd, F_SETOWN, getpid()) |
- (3)开启信号驱动 IO 通常使用 fcntl 函数的 F_SETFL 命令打开 FASYNC 标志。
1 | flags = fcntl(fd, F_GETFL); /* 获取当前的进程状态 */ |
重点就是通过 fcntl 函数设置进程状态为 FASYNC,经过这一步,驱动程序中的 fasync 函数就会执行。
4. 驱动程序实现 fasync
(1)应用程序开启信号驱动 IO 时, 会触发驱动中的 fasync 函数。 所以首先在 file_operations 结构体中实现 file_operations.fasync 函数 ,函数原型如下:
1 | int (*fasync) (int, struct file *, int); |
(2)在驱动中的 fasync 函数调用 fasync_helper() 函数来操作 struct fasync_struct 结构体 。fasync_helper() 函数原型如下:
1 | int fasync_helper(int fd, struct file * filp, int on, struct fasync_struct **fapp); |
(3)当设备准备好的时候, 驱动程序需要调用 kill_fasync() 函数通知应用程序, 此时应用程序的 SIGIO 信号处理函数就会被执行。 kill_fasync() 负责发送指定的信号, 函数原型如下 :
1 | void kill_fasync(struct fasync_struct **fp, int sig, int band); |
三、信号驱动IO demo
1. demo源码
08_IO_model/05_io_signal · 苏木/imx6ull-driver-demo - 码云 - 开源中国
2. 开发板验证
我们执行以下命令:来验证效果:
1 | ./app_demo.out /dev/sdevchr 1 0 & |

可以看到,当没有写入数据的时候,会打印等待信号,当我们执行了写入命令后,SIGIO信号对应的处理函数执行,从缓冲区读出写入的数据。
参考资料: