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
2
3
4
5
6
7
8
9
10
11
12
13
14
root@alpha-imx6ull:# kill -l
1) SIGHUP 2) SIGINT 3) SIGQUIT 4) SIGILL 5) SIGTRAP
6) SIGABRT 7) SIGBUS 8) SIGFPE 9) SIGKILL 10) SIGUSR1
11) SIGSEGV 12) SIGUSR2 13) SIGPIPE 14) SIGALRM 15) SIGTERM
16) SIGSTKFLT 17) SIGCHLD 18) SIGCONT 19) SIGSTOP 20) SIGTSTP
21) SIGTTIN 22) SIGTTOU 23) SIGURG 24) SIGXCPU 25) SIGXFSZ
26) SIGVTALRM 27) SIGPROF 28) SIGWINCH 29) SIGIO 30) SIGPWR
31) SIGSYS 34) SIGRTMIN 35) SIGRTMIN+1 36) SIGRTMIN+2 37) SIGRTMIN+3
38) SIGRTMIN+4 39) SIGRTMIN+5 40) SIGRTMIN+6 41) SIGRTMIN+7 42) SIGRTMIN+8
43) SIGRTMIN+9 44) SIGRTMIN+10 45) SIGRTMIN+11 46) SIGRTMIN+12 47) SIGRTMIN+13
48) SIGRTMIN+14 49) SIGRTMIN+15 50) SIGRTMAX-14 51) SIGRTMAX-13 52) SIGRTMAX-12
53) SIGRTMAX-11 54) SIGRTMAX-10 55) SIGRTMAX-9 56) SIGRTMAX-8 57) SIGRTMAX-7
58) SIGRTMAX-6 59) SIGRTMAX-5 60) SIGRTMAX-4 61) SIGRTMAX-3 62) SIGRTMAX-2
63) SIGRTMAX-1 64) SIGRTMAX

这些信号的含义在应用编程的时候都了解过了,这里就不在多说,他们在内核中是有宏定义的,有多个头文件中都有的样子,但是值都是一样的,例如 signal.h - include/uapi/asm-generic/signal.h

点击查看详情
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
#define SIGHUP    1  /* 终端挂起或控制进程终止 */
#define SIGINT 2 /* 终端中断(Ctrl+C 组合键) */
#define SIGQUIT 3 /* 终端退出(Ctrl+\组合键) */
#define SIGILL 4 /* 非法指令 */
#define SIGTRAP 5 /* debug 使用,有断点指令产生 */
#define SIGABRT 6 /* 由 abort(3)发出的退出指令 */
#define SIGIOT 6 /* IOT 指令 */
#define SIGBUS 7 /* 总线错误 */
#define SIGFPE 8 /* 浮点运算错误 */
#define SIGKILL 9 /* 杀死、终止进程 */
#define SIGUSR1 10 /* 用户自定义信号 1 */
#define SIGSEGV 11 /* 段违例(无效的内存段) */
#define SIGUSR2 12 /* 用户自定义信号 2 */
#define SIGPIPE 13 /* 向非读管道写入数据 */
#define SIGALRM 14 /* 闹钟 */
#define SIGTERM 15 /* 软件终止 */
#define SIGSTKFLT 16 /* 栈异常 */
#define SIGCHLD 17 /* 子进程结束 */
#define SIGCONT 18 /* 进程继续 */
#define SIGSTOP 19 /* 停止进程的执行,只是暂停 */
#define SIGTSTP 20 /* 停止进程的运行(Ctrl+Z 组合键) */
#define SIGTTIN 21 /* 后台进程需要从终端读取数据 */
#define SIGTTOU 22 /* 后台进程需要向终端写数据 */
#define SIGURG 23 /* 有"紧急"数据 */
#define SIGXCPU 24 /* 超过 CPU 资源限制 */
#define SIGXFSZ 25 /* 文件大小超额 */
#define SIGVTALRM 26 /* 虚拟时钟信号 */
#define SIGPROF 27 /* 时钟信号描述 */
#define SIGWINCH 28 /* 窗口大小改变 */
#define SIGIO 29 /* 可以进行输入/输出操作 */
#define SIGPOLL SIGIO
/* #define SIGLOS 29 */
#define SIGPWR 30 /* 断点重启 */
#define SIGSYS 31 /* 非法的系统调用 */
#define SIGUNUSED 31 /* 未使用信号 */

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
2
3
4
5
6
7
8
NAME
fcntl - manipulate file descriptor

SYNOPSIS
#include <unistd.h>
#include <fcntl.h>

int fcntl(int fd, int cmd, ... /* arg */ );

该函数用于对一个打开的文件描述符执行一系列控制操作。

函数参数:

  • 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
2
3
4
5
6
7
8
struct fasync_struct {
rwlock_t fa_lock;
int magic;
int fa_fd;
struct fasync_struct *fa_next; /* singly linked list */
struct file *fa_file;
struct rcu_head fa_rcu;
};

一般将 struct fasync_struct 结构体指针变量定义到设备结构体中 ,例如:

1
2
3
4
5
6
7
8
9
10
11
typedef struct __CHAR_DEVICE
{
char dev_name[32]; // 设备名称, /dev/dev-name
dev_t dev_num; // 定义dev_t类型(32位大小)的变量dev_num,用来存放设备号
struct cdev s_cdev; // 定义cdev结构体类型的变量scdev
struct class *class; // 定于struct class *类型结构体变量 class,表示要创建的类
struct device *device; // 设备
char buf[BUFSIZE]; // 设置数据存储数组mem
struct mutex mutex_lock; // 互斥锁
struct fasync_struct *async_queue; /* 异步相关结构体 */
} _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
2
3
4
5
6
7
8
9
int fasync_helper(int fd, struct file * filp, int on, struct fasync_struct **fapp)
{
if (!on)
return fasync_remove_entry(filp, fapp);
return fasync_add_entry(fd, filp, fapp);
}

EXPORT_SYMBOL(fasync_helper);

fasync_helper() 函数的前三个参数就是 fasync 函数的那三个参数,第四个参数就是要初始化的 struct fasync_struct 结构体指针变量。当应用程序通过“fcntl(fd, F_SETFL, flags | FASYNC)”改变fasync 标记的时候,驱动程序 file_operations 操作集中的 file_operations.fasync 函数就会执行。

2.2.2 参考示例

驱动程序中的 fasync 函数参考示例如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
struct xxx_dev
{
//......
struct fasync_struct *async_queue; /* 异步相关结构体 */
};

static int xxx_fasync(int fd, struct file *pFile, int on)
{
struct xxx_dev *pdev = (xxx_dev)pFile->private_data;

if (fasync_helper(fd, pFile, on, &pdev->async_queue) < 0)
return -EIO;
return 0;
}

static struct file_operations xxx_ops = {
//......
.fasync = xxx_fasync,
//......
};

在关闭驱动文件的时候需要在 file_operations 操作集中的 file_operations.release 函数中释放 struct fasync_structstruct fasync_struct 的释放函数同样为 fasync_helper()file_operations.release 函数参数参考实例如下:

1
2
3
4
5
6
7
8
9
static int xxx_release(struct inode *pInode, struct file *pFilp)
{
return xxx_fasync(-1, pFilp, 0); /* 删除异步通知 */
}

static struct file_operations xxx_ops = {
//......
.release = xxx_release,
};

2.3 kill_fasync()

当设备可以访问的时候,驱动程序需要向应用程序发出信号,此时我们在应用程序注册的 SIGIO 信号处理函数就会被执行。 kill_fasync() 函数负责发送指定的信号:

1
2
3
4
5
6
7
8
9
10
11
12
void kill_fasync(struct fasync_struct **fp, int sig, int band)
{
/* First a quick test without locking: usually
* the list is empty.
*/
if (*fp) {
rcu_read_lock();
kill_fasync_rcu(rcu_dereference(*fp), sig, band);
rcu_read_unlock();
}
}
EXPORT_SYMBOL(kill_fasync);

函数参数:

  • 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
2
flags = fcntl(fd, F_GETFL);         /* 获取当前的进程状态 */
fcntl(fd, F_SETFL, flags | FASYNC); /* 开启当前进程异步通知功能 */

重点就是通过 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
2
3
./app_demo.out /dev/sdevchr 1 0 &
./app_demo.out /dev/sdevchr 2 0 abcdefg
./app_demo.out /dev/sdevchr 2 0 sumu
image-20250124235506119

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

参考资料:

【Linux】一篇文章彻底搞定信号!_sigrtmin-CSDN博客

Unix/Linux编程:fcntl函数总结-CSDN博客