LV10-03-IO模型-02-IO模型在驱动层的实现-03-信号驱动IO
本文主要是IO模型在驱动层的实现中的信号驱动IO的相关笔记,若笔记中有错误或者不合适的地方,欢迎批评指正😃。
点击查看使用工具及版本
Windows | windows11 |
Ubuntu | Ubuntu16.04的64位版本 |
VMware® Workstation 16 Pro | 16.2.3 build-19376536 |
SecureCRT | Version 8.7.2 (x64 build 2214) - 正式版-2020年5月14日 |
Linux开发板 | 华清远见 底板: FS4412_DEV_V5 核心板: FS4412 V2 |
u-boot | 2013.01 |
点击查看本文参考资料
参考方向 | 参考原文 |
--- | --- |
点击查看相关文件下载
文件 | 下载链接 |
--- | --- |
一、信号驱动
信号驱动是一种异步通知,异步通知的核心就是信号 ,所有的信号都定义在 linux 内核源码的这个文件中:
1 | arch/xtensa/include/uapi/asm/signal.h |
1.应用层
使用信号驱动的时候,我们需要在应用层完成三件事,一是信号注册,二是将设备的IO模式设置成信号驱动模式,三是完成信号处理函数。
1.1 signal()
在 linux 下可以使用 man signal 命令查看该函数的帮助手册,大概会有两种函数声明形式,但是其实是一样的,习惯上会使用形式一。
【说明】使用的命令为 man signal 或者 man 2 signal 查询到的系统调用的函数声明形式。
1 | /* 需包含的头文件 */ |
【说明】使用 man 3 signal 查询到的库函数中的函数声明形式。
1 | /* 需包含的头文件 */ |
点击查看两种声明的关系
我们先来看声明形式二:
1 | void (*signal(int sig, void (*func)(int)))(int); |
我们先拆分一下:
1 | /* 注意一下函数指针的形式:<数据类型> (*<函数指针名称>) (<参数说明列表>); */ |
- 由于 () 的存在, *func 是一个指针变量,后边的括号和 int 说明这个指针变量可以指向一个带有 int 形参的函数,前边的 void 说明这个函数指针指向的函数返回值为 void 类型,总的来说就是 void (*func)(int) 定义了一个函数指针变量 func ,它可以指向一个带有一个 int 参数的返回值为 void 类型的函数。可以指向的函数形式如下:
1 | void functionName(int arg); |
- 再往上一层看,这就到了 signal 了, * 的优先级是低于 () 的,所以 signal 先与后边的 (int sig, void (*func)(int)) 相结合,说说明 signal 是一个函数,并且带有两个参数,一个是 int 类型的变量,一个是 void (*func)(int) 类型的函数指针变量。
- 再看 signal 前边的 * 表示这是一个指针变量,也就是说这个 signal 函数的返回值是一个指针变量。
- 接着就是最后的 (int) ,这表示, signal 函数返回的指针变量可以指向一个带有 int 类型参数的函数。
- 最前边的 void 表示, signal 函数返回的指针变量可以指向一个带有 int 类型参数且没有返回值的函数。
总的来说,定义了一个指针函数 signal ,指针函数的返回值是一个函数指针,可以指向一个带有 int 类型参数且无返回值的函数;
而 signal 含有两个参数,一个是普通的整型变量,另一个是函数指针类型,可以指向带有一个 int 类型且无返回值的函数。
经过分析,会发现, signal 的返回值和 signal 函数第二个参数的类型是一样的,他们都是函数指针吗,都可以指向一个带有一个 int 类型参数的没有返回值的函数。
前边我们使用 typedef 简化过这样的定义的,所以这里,我们可以这样也做一个简化:
1 | typedef void (*pfunc)(int) |
这样,上边的函数就可以写为:
1 | pfunc signal(int sig, pfunc);/* 需要注意的是,声明只需要类型即可,不一定需要写出形参*/ |
这样是不是就跟前边形式一很像了呢?我们把 pfunc 换成 sighandler_t 其实就得到了形式一的声明,这完全是可以的,毕竟指针变量名只要符合标识规则即可
【函数说明】该函数可以修改进程对信号的处理方式,可将信号的处理方式设置为捕获信号、忽略信号以及系统默认操作。
【函数参数】
- signum : int 类型,此参数指定需要进行设置的信号,可使用信号名(宏)或信号的数字编号,不过一般建议使用信号名。
- handler : sighandler_t 类型,是一个函数指针,指向信号对应的信号处理函数,当进程接收到信号后会自动执行该处理函数;也可以指向几个预定义函数。
点击查看 handler 可指向的预定义函数
SIG_DFL | 表示设置为系统默认操作 |
SIG_IGN | 表示设置为忽视信号 |
【返回值】 sighandler_t 类型,是一个函数指针,成功情况下的返回值则是指向在此之前的信号处理函数;如果出错则返回 SIG_ERR ,并会设置 errno 。
【使用格式】一般情况下基本使用格式如下:
1 | /* 需要包含的头文件 */ |
【注意事项】信号处理函数声明一般如下:
1 | void handler(int sig); |
1.2 使用实例
1 | int fd = -1; /* 文件描述符 */ |
2.驱动层
2.1 相关结构体
2.1.1 fasync_struct
在驱动中实现信号驱动前,我们需要在驱动程序中定义一个 fasync_struct 结构体指针变量,该结构体定义在 linux 内核源码的这个文件中:
1 | include/linux/fs.h |
我们打开文件,该结构体定义如下:
1 | struct fasync_struct { |
2.2 相关函数
2.2.1 fasync_helper ()
我们使用以下命令查询一下函数所在头文件:
1 | grep fasync_helper -r -n ~/5linux/linux-3.14/include |
经过查找,我们可以得到如下信息:
1 | /* 需包含的头文件 */ |
【函数说明】该函数用于初始化或者释放定义的 fasync_struct 结构体指针 。
【函数参数】
- fd :int 类型,表示要操作的文件描述符。
- filp : struct file * 类型, 读写文件内容过程中用到的一些控制性数据组合而成的对象。
- on :int 类型,非0,表示初始化异步通知结构体指针,为0,表示释放异步通知结构体指针。
- fapp :struct fasync_struct ** 类型,要初始化的 fasync_struct 结构体指针变量。
【返回值】int 类型,成功返回非负数(>=0),失败返回一个负数
【使用格式】
1 | /* 需包含的头文件 */ |
【注意事项】 当应用程序通过下边的语句改变 fasync 标记的时候,驱动程序 file_operations 操作集中我们自己实现的 fasync 函数就会执行。
1 | fcntl(fd, F_SETFL, flags | FASYNC); |
2.2.2 kill_fasync ()
我们使用以下命令查询一下函数所在头文件:
1 | grep kill_fasync -r -n ~/5linux/linux-3.14/include |
经过查找,我们可以得到如下信息:
1 | /* 需包含的头文件 */ |
【函数说明】当设备可以访问的时候,驱动程序需要向应用程序发出信号,相当于产生“中断”。 kill_fasync() 函数负责发送指定的信号 。
【函数参数】
- fp :struct fasync_struct ** 类型,要操作的 fasync_struct 类型指针变量。
- sig : int 类型, 要发送的信号,一般是SIGIO。
- band :int 类型,可读时设置为 POLL_IN,可写时设置为 POLL_OUT。
【返回值】none
【使用格式】
1 | /* 需包含的头文件 */ |
【注意事项】 none
2.3 使用实例
2.3.1 初始化异步通知结构体
1 | /** |
2.3.2 释放异步通知结构体变量
1 | /** |
2.3.4 发送信号
1 | /** |
3. 使用步骤
3.1 应用层
- (1)注册信号处理函数
应用程序根据驱动程序所使用的信号来设置信号的处理函数,应用程序使用 signal 函数来设置信号的处理函数。
- (2)将本应用程序的进程号告诉给内核
1 | fcntl(fd, F_SETOWN, getpid()); |
- (3)开启异步通知
1 | flags = fcntl(fd, F_GETFL); /* 获取当前的进程状态 */ |
经过这一步,驱动程序中的 fasync 函数就会执行。
3.2 驱动层
- (1)定义struct fasync_struct类型结构体指针变量;
- (2)初始化struct fasync_struct结构体指针变量;
- (3)发送信号;
- (4)关闭设备时释放struct fasync_struct结构体指针变量。