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
2
3
4
5
/* 需包含的头文件 */
#include <signal.h>
/* 函数声明 */
typedef void (*sighandler_t)(int);
sighandler_t signal(int signum, sighandler_t handler);

【说明】使用 man 3 signal 查询到的库函数中的函数声明形式。

1
2
3
4
/* 需包含的头文件 */
#include <signal.h>
/* 函数声明 */
void (*signal(int sig, void (*func)(int)))(int);
点击查看两种声明的关系

我们先来看声明形式二:

1
void (*signal(int sig, void (*func)(int)))(int);

我们先拆分一下:

1
2
3
4
5
6
7
/* 注意一下函数指针的形式:<数据类型> (*<函数指针名称>) (<参数说明列表>);  */
void ( ) (int)
|
*signal(int sig, )
|
void (*func)(int)

  • 由于 () 的存在, *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
2
3
4
5
6
7
8
9
10
11
12
/* 需要包含的头文件 */
#include <signal.h>

/* 至少应该有的语句 */
typedef void (*sighandler_t)(int);/* 不需要使用sighandler_t定义变量时,好像不写也可以 */
void sigintHandle(int sig);

/* 需要函数返回值 */
sighandler_t oldact;
oldact = signal(SIGINT, sigintHandle);
/* 或者不需要函数返回值 */
signal(SIGINT, sigintHandle);

【注意事项】信号处理函数声明一般如下:

1
void handler(int sig);

1.2 使用实例

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
36
37
38
39
40
int fd = -1; /* 文件描述符 */
/** 信号处理函数 */
void sigio_handler(int signo)
{
// 读写数据
}

int main(int argc, char *argv[])
{
int flag = 0;
/* 1.判断参数个数是否合法 */
if (argc < 2)
{
printf("\n%s /dev/dev_name\n\n", argv[0]);
return -1;
}
/* 2.打开字符设备 */
if ((fd = open(argv[1], O_RDWR)) < 0) /* 默认就是阻塞的 */
{
printf("open %s failed\n", argv[1]);
return -1;
}
/* 3.注册信号处理函数 */
signal(SIGIO, sigio_handler);
/* 4.开启异步通知相关操作 */
fcntl(fd, F_SETOWN, getpid()); /* 将本应用程序的进程号告诉给内核 */
flag = fcntl(fd, F_GETFL); /* 获取当前的进程状态 */
flag |= FASYNC;
fcntl(fd, F_SETFL, flag); /* 开启当前进程异步通知功能 */
/* 5.循环等待 */
while (1)
{
sleep(1);
}
/* 6.关闭字符设备 */
close(fd);
fd = -1;

return 0;
}

2.驱动层

2.1 相关结构体

2.1.1 fasync_struct

在驱动中实现信号驱动前,我们需要在驱动程序中定义一个 fasync_struct 结构体指针变量,该结构体定义在 linux 内核源码的这个文件中:

1
include/linux/fs.h

我们打开文件,该结构体定义如下:

1
2
3
4
5
6
7
8
struct fasync_struct {
spinlock_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;
};

2.2 相关函数

2.2.1 fasync_helper ()

我们使用以下命令查询一下函数所在头文件:

1
grep fasync_helper -r -n ~/5linux/linux-3.14/include

经过查找,我们可以得到如下信息:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
/* 需包含的头文件 */
#include <linux/fs.h>

/* 函数声明 */
extern int fasync_helper(int, struct file *, int, struct fasync_struct **);
/* 函数定义,定义在 fs/fcntl.c 文件 */
/*
* fasync_helper() is used by almost all character device drivers
* to set up the fasync queue, and for regular files by the file
* lease code. It returns negative on error, 0 if it did no changes
* and positive if it added/deleted the entry.
*/
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);
}

【函数说明】该函数用于初始化或者释放定义的 fasync_struct 结构体指针 。

【函数参数】

  • fd :int 类型,表示要操作的文件描述符。
  • filp : struct file * 类型, 读写文件内容过程中用到的一些控制性数据组合而成的对象。
  • on :int 类型,非0,表示初始化异步通知结构体指针,为0,表示释放异步通知结构体指针。
  • fapp :struct fasync_struct ** 类型,要初始化的 fasync_struct 结构体指针变量。

【返回值】int 类型,成功返回非负数(>=0),失败返回一个负数

【使用格式】

1
2
3
4
5
6
7
8
9
10
11
12
13
/* 需包含的头文件 */
#include <linux/fs.h>

/* 至少要有的语句 */
struct fasync_struct *pasync_obj;
// 释放pasync_obj
fasync_helper(-1, pfile, 0, &pasync_obj); // 释放pasync_obj
// 初始化pasync_obj
static int mydev_fasync(int fd, struct file *pfile, int mode)
{
struct _my_dev *p_my_dev = (struct _my_dev *)(pfile->private_data); /* 通过私有数据方式获取 g_my_dev设备属性 */
return (fasync_helper(fd, pfile, mode, &p_my_dev->pasync_obj));
}

【注意事项】 当应用程序通过下边的语句改变 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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
/* 需包含的头文件 */
#include <linux/fs.h>

/* 函数声明 */
extern void kill_fasync(struct fasync_struct **, int, int);
/* 函数定义,定义在 fs/fcntl.c 文件 */
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();
}
}

【函数说明】当设备可以访问的时候,驱动程序需要向应用程序发出信号,相当于产生“中断”。 kill_fasync() 函数负责发送指定的信号 。

【函数参数】

  • fp :struct fasync_struct ** 类型,要操作的 fasync_struct 类型指针变量。
  • sig : int 类型, 要发送的信号,一般是SIGIO。
  • band :int 类型,可读时设置为 POLL_IN,可写时设置为 POLL_OUT。

【返回值】none

【使用格式】

1
2
3
4
5
6
/* 需包含的头文件 */
#include <linux/fs.h>

/* 至少要有的语句 */
struct fasync_struct *pasync_obj;
kill_fasync(&pasync_obj, SIGIO, POLL_IN);

【注意事项】 none

2.3 使用实例

2.3.1 初始化异步通知结构体

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
/**
* @Function : mydev_fasync
* @brief : 应用调用fcntl设置FASYNC时调用该函数产生异步通知结构对象,并将其地址设置到设备结构成员中
* @param fd :int 类型,文件描述符。
* @param pfile : struct file * 类型,指向open产生的struct file类型的对象,表示本次ioctl对应的那次open。
* (读写文件内容过程中用到的一些控制性数据组合而成的对象)
* @param mode :int 类型,。
* @return : 异步通知结构对象
* @Description : 函数名初始化给struct file_operations的成员.fasync
* 结构体指针变量。当应用程序通过 “ fcntl(fd, F_SETFL, flags | FASYNC)” 改变
* fasync标记的时候,驱动程序 file_operations操作集中的 fasync函数就会执行。
*/
static int mydev_fasync(int fd, struct file *pfile, int mode)
{
struct _my_dev *p_my_dev = (struct _my_dev *)(pfile->private_data); /* 通过私有数据方式获取 g_my_dev设备属性 */
/* 创建异步通知结构对象 */
return (fasync_helper(fd, pfile, mode, &p_my_dev->pasync_obj));
}

2.3.2 释放异步通知结构体变量

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
/**
* @Function : mydev_close
* @brief : 关闭设备
* @param pnode : struct inode * 类型,内核中记录文件元信息的结构体
* @param pfile : struct file * 类型,读写文件内容过程中用到的一些控制性数据组合而成的对象
* @return : 0,操作完成
* @Description : 函数名初始化给 struct file_operations 的成员 .close
*/
int mydev_close(struct inode *pnode, struct file *pfile)
{
struct _my_dev *p_my_dev = (struct _my_dev *)(pfile->private_data); /* 通过私有数据方式获取 g_my_dev设备属性 */
/* 释放异步通知结构对象 */
if (p_my_dev->pasync_obj != NULL)
fasync_helper(-1, pfile, 0, &p_my_dev->pasync_obj);
printk("mydev_close is called!\n");
return 0;
}

2.3.4 发送信号

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
/**
* @Function : mydev_write
* @brief : 写设备
* @param pfile : struct file * 类型,指向open产生的struct file类型的对象,表示本次write对应的那次open。
* (读写文件内容过程中用到的一些控制性数据组合而成的对象)
* @param pbuf :__user * 类型,指向用户空间一块内存,用来保存被写的数据。
* @param count :size_t 类型,用户期望写入的字节数。
* @param ppos :loff_t * 类型,对于需要位置指示器控制的设备操作有用,用来指示写入的起始位置,写完后也需要变更位置指示器的指示位置。
* @return : 成功返回本次成功写入的字节数,失败返回-1
* @Description : 函数名初始化给 struct file_operations 的成员 .write
*/
ssize_t mydev_write(struct file *pfile, const char __user *pbuf, size_t count, loff_t *ppos)
{
// 其他操作... ...
/* 写函数中有数据可读时向应用层发信号 */
/** kill_fasync:发信号
* struct fasync_struct ** 指向保存异步通知结构地址的指针
* int 信号 SIGIO/SIGKILL/SIGCHLD/SIGCONT/SIGSTOP
* int 读写信息POLLIN、POLLOUT
*/
if (p_my_dev->pasync_obj != NULL)
kill_fasync(&p_my_dev->pasync_obj, SIGIO, POLL_IN);
return size;
}

3. 使用步骤

3.1 应用层

  • (1)注册信号处理函数

应用程序根据驱动程序所使用的信号来设置信号的处理函数,应用程序使用 signal 函数来设置信号的处理函数。

  • (2)将本应用程序的进程号告诉给内核
1
fcntl(fd, F_SETOWN, getpid());
  • (3)开启异步通知
1
2
flags = fcntl(fd, F_GETFL); /* 获取当前的进程状态 */
fcntl(fd, F_SETFL, flags | FASYNC); /* 开启当前进程异步通知功能 */

经过这一步,驱动程序中的 fasync 函数就会执行。

3.2 驱动层

  • (1)定义struct fasync_struct类型结构体指针变量;
  • (2)初始化struct fasync_struct结构体指针变量;
  • (3)发送信号;
  • (4)关闭设备时释放struct fasync_struct结构体指针变量。