Linux异步通知

简介

1、Linux 应用程序可以通过阻塞或者非阻塞这两种方式来访问驱动设备

  • 通过阻塞方式访问的话应用程序会处于休眠态,等待驱动设备可以使用
  • 非阻塞方式的话会通过 poll 函数来不断的轮询,查看驱动设备文件是否可以使用。

2、上述方式都需要应用程序主动的去查询设备的使用情况,因此诞生了信号(类似硬件使用的中断),当驱动程序可以访问的时候主动就会告诉应用程序。

  • 信号是软件层次上的中断,算软件层次上的对中断的一种模拟,驱动可以通过主动向应用程序发送信号的方式来报告自己可以访问了,应用程序获取到信号以后就可以从驱动设备中读取或者写入数据了。整个过程就相当于应用程序收到了驱动发送过来了的一个中断,然后应用程序去响应这个中断,在整个处理过程中应用程序并没有去查询驱动设备是否可以访问,一切都是由驱动设备自己告诉给应用程序的。

3、阻塞、非阻塞、异步通知,这三种是针对不同的场合提出来的不同的解决方法,没有优劣之分,在实际的工作和学习中,根据实际需求合理选择处理方式。

信号

异步通知的核心就是信号,在 arch/xtensa/include/uapi/asm/signal.h 文件中定义了 Linux 所支持的所有信号,这些信号如下所示:

34 #define SIGHUP 1 /* 终端挂起或控制进程终止 */
35 #define SIGINT 2 /* 终端中断(Ctrl+C 组合键) */
36 #define SIGQUIT 3 /* 终端退出(Ctrl+\组合键) */
37 #define SIGILL 4 /* 非法指令 */
38 #define SIGTRAP 5 /* debug 使用,有断点指令产生 */
39 #define SIGABRT 6 /* 由 abort(3)发出的退出指令 */
40 #define SIGIOT 6 /* IOT 指令 */
41 #define SIGBUS 7 /* 总线错误 */
42 #define SIGFPE 8 /* 浮点运算错误 */
43 #define SIGKILL 9 /* 杀死、终止进程 */
44 #define SIGUSR1 10 /* 用户自定义信号 1 */
45 #define SIGSEGV 11 /* 段违例(无效的内存段) */
46 #define SIGUSR2 12 /* 用户自定义信号 2 */
47 #define SIGPIPE 13 /* 向非读管道写入数据 */
48 #define SIGALRM 14 /* 闹钟 */
49 #define SIGTERM 15 /* 软件终止 */
50 #define SIGSTKFLT 16 /* 栈异常 */
51 #define SIGCHLD 17 /* 子进程结束 */
52 #define SIGCONT 18 /* 进程继续 */
53 #define SIGSTOP 19 /* 停止进程的执行,只是暂停 */
54 #define SIGTSTP 20 /* 停止进程的运行(Ctrl+Z 组合键) */
55 #define SIGTTIN 21 /* 后台进程需要从终端读取数据 */
56 #define SIGTTOU 22 /* 后台进程需要向终端写数据 */
57 #define SIGURG 23 /* 有"紧急"数据 */
58 #define SIGXCPU 24 /* 超过 CPU 资源限制 */
59 #define SIGXFSZ 25 /* 文件大小超额 */
60 #define SIGVTALRM 26 /* 虚拟时钟信号 */
61 #define SIGPROF 27 /* 时钟信号描述 */
62 #define SIGWINCH 28 /* 窗口大小改变 */
63 #define SIGIO 29 /* 可以进行输入/输出操作 */
64 #define SIGPOLL SIGIO 
65 /* #define SIGLOS 29 */
66 #define SIGPWR 30 /* 断点重启 */
67 #define SIGSYS 31 /* 非法的系统调用 */
68 #define SIGUNUSED 31 /* 未使用信号 */
  • 在上述这些信号中,只有 SIGKILL(9)和 SIGSTOP(19)这两个信号不能被忽略,其他的信号都可以忽略。
  • 这些信号就相当于中断号,不同的中断号代表了不同的中断,不同的中断所做的处理不同,因此,驱动程序可以通过向应用程序发送不同的信号来实现不同的功能。

应用中的API

signal 指定信号的处理函数

在应用程序中使用 signal 函数来设置指定信号的处理函数

sighandler_t signal(int signum, sighandler_t handler)
  • signum:要设置处理函数的信号。
  • handler:信号的处理函数。
  • 返回值:设置成功的话返回信号的前一个处理函数,设置失败的话返回 SIG_ERR。

信号处理函数

信号处理函数原型如下所示:

typedef void (*sighandler_t)(int)

使用

前面使用“kill -9 PID”杀死指定进程的方法就是向指定的进程(PID)发送SIGKILL 这个信号。当按下键盘上的 CTRL+C 组合键以后会向当前正在占用终端的应用程序发出 SIGINT 信号,SIGINT 信号默认的动作是关闭当前应用程序。这里我们修改一下 SIGINT 信号的默认处理函数,当按下 CTRL+C 组合键以后先在终端上打印出“SIGINT signal!”这行字符串,然后再关闭当前应用程序。

1 #include "stdlib.h"
2 #include "stdio.h"
3 #include "signal.h"
4 
5 void sigint_handler(int num) 
6 { 
7 		printf("\r\nSIGINT signal!\r\n");
8 		exit(0);
9 }
10
11 int main(void)
12 {
13		signal(SIGINT, sigint_handler);
14 		while(1);
15 		return 0;
16 }

编译

gcc signaltest.c -o signaltest

当按下 CTRL+C 组合键以后 sigint_handler 这个 SIGINT 信号处理函数执行了,并且输出了“SIGINT signal!”这行字符串。

驱动中的信号处理

fasync_struct 结构体

fasync_struct 定义在驱动程序中,也就是在我们一直用的结构体中也就是设备结构体中定义一个这个结构体,结构体中的内容如下:

struct fasync_struct {
 spinlock_t fa_lock;
 int magic;
 int fa_fd;
 struct fasync_struct *fa_next;
 struct file *fa_file;
 struct rcu_head fa_rcu;
};
/* imx6uirq设备结构体 */
struct imx6uirq_dev{
	dev_t devid;			/* 设备号 	 */	
	struct cdev cdev;		/* cdev 	*/                 
	struct class *class;	/* 类 		*/
	struct device *device;	/* 设备 	 */
	int major;				/* 主设备号	  */
	int minor;				/* 次设备号   */
	struct device_node	*nd; /* 设备节点 */	
	atomic_t keyvalue;		/* 有效的按键键值 */
	atomic_t releasekey;	/* 标记是否完成一次完成的按键,包括按下和释放 */
	struct timer_list timer;/* 定义一个定时器*/
	struct irq_keydesc irqkeydesc[KEY_NUM];	/* 按键init述数组 */
	unsigned char curkeynum;				/* 当前init按键号 */
	wait_queue_head_t r_wait;				/* 读等待队列头 */
	struct fasync_struct *async_queue;		/* 异步相关结构体 */
};

fasync 函数

如果要使用异步通知,需要在设备驱动中实现 file_operations 操作集中的 fasync 函数

int (*fasync) (int fd, struct file *filp, int on)

fasync 函数里面一般通过调用 fasync_helper 函数来初始化前面定义的 fasync_struct 结构体指针,fasync_helper 函数原型如下:

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

参考示例:

1 struct xxx_dev { 
2 ......
3 struct fasync_struct *async_queue; /* 异步相关结构体 */
4 };
5
6 static int xxx_fasync(int fd, struct file *filp, int on) 
7 { 
8 struct xxx_dev *dev = (xxx_dev)filp->private_data; 
9 
10 if (fasync_helper(fd, filp, on, &dev->async_queue) < 0)
11 return -EIO;
12 return 0;
13 }
14
15 static struct file_operations xxx_ops = {
16 ......
17 .fasync = xxx_fasync,
18 ......
19 };

关闭驱动文件

在关闭驱动文件的时候需要在 file_operations 操作集中的 release 函数中释放 fasync_struct,fasync_struct 的释放函数同样为 fasync_helper

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

第 3 行通过调用示例代码 53.1.2.3 中的 xxx_fasync 函数来完成 fasync_struct 的释放工作,但是,其最终还是通过 fasync_helper 函数完成释放工作。

kill_fasync _ 向应用程序发送信号

当设备可以访问的时候,驱动程序需要向应用程序发出信号,相当于产生“中断”。kill_fasync函数负责发送指定的信号,kill_fasync 函数原型如下所示:

void kill_fasync(struct fasync_struct **fp, int sig, int band)
  • fp:要操作的 fasync_struct。
  • sig:要发送的信号。
  • band:可读时设置为 POLL_IN,可写时设置为 POLL_OUT。
  • 返回值:无。

应用程序对异步通知的处理

应用程序对异步通知的处理包括以下三步:
1、注册信号处理函数

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

2、将本应用程序的进程号告诉给内核

使用 fcntl(fd, F_SETOWN, getpid())
将本应用程序的进程号告诉给内核。

3、开启异步通知

flags = fcntl(fd, F_GETFL); /* 获取当前的进程状态 */
fcntl(fd, F_SETFL, flags | FASYNC); /* 开启当前进程异步通知功能 */

重点就是通过 fcntl 函数设置进程状态为 FASYNC,经过这一步,驱动程序中的 fasync 函数就会执行。

代码

实现功能

当按键按下以后驱动程序向应用程序发送 SIGIO 信号,应用程序获取到 SIGIO 信号以后读取并且打印出按键值。

头文件

ctrl+shift+p
Linux异步通知_第1张图片

{
    "configurations": [
        {
            "name": "Linux",
            "includePath": [
                "${workspaceFolder}/**",
                "/home/ni/linux/IMX6ULL/uboot/modi_nxp/2modi_old_linux kernel/include",
                "/home/ni/linux/IMX6ULL/uboot/modi_nxp/2modi_old_linux kernel/arch/arm/include",
                "/home/ni/linux/IMX6ULL/uboot/modi_nxp/2modi_old_linux kernel/arch/arm/include/generated"
            ],
            "defines": [],
            "compilerPath": "/usr/bin/clang",
            "cStandard": "c11",
            "cppStandard": "c++17",
            "intelliSenseMode": "clang-x64"
        }
    ],
    "version": 4
}

makefile

KERNELDIR := /home/ni/linux/IMX6ULL/uboot/modi_nxp/2modi_old_linux_kernel
CURRENT_PATH := $(shell pwd)

#obj-m 表示将 led.c 这个文件编译为 led.ko 模块。
obj-m := asyncnoti.o

build: kernel_modules

#后面的 modules 表示编译模块,
#-C 表示将当前的工作目录切换到指定目录中,也就是 KERNERLDIR 目录。
# M 表示模块源码目录,“make modules”命令中加入 M=dir 以后程序会自动到指定的 dir 目录中读取模块的源码并将其编译为.ko 文件。
kernel_modules:
	$(MAKE) -C $(KERNELDIR) M=$(CURRENT_PATH) modules

clean:
	$(MAKE) -C $(KERNELDIR) M=$(CURRENT_PATH) clean


#make编译

make

你可能感兴趣的:(linux,linux,驱动开发,网络)