Linux驱动践行 中断处理函数如何 发送信号 给应用层?
发布时间:2021-12-24 00:47:23 所属栏目:Linux 来源:互联网
导读:别人的经验,我们的阶梯! 大家好,我是道哥,今天我为大伙儿解说的技术知识点是:【中断程序如何发送信号给应用层】。 最近分享的几篇文章都比较基础,关于字符类设备的驱动程序,以及中断处理程序。 也许在现代的项目是用不到这样的技术,但是万丈高楼平地起
别人的经验,我们的阶梯! 大家好,我是道哥,今天我为大伙儿解说的技术知识点是:【中断程序如何发送信号给应用层】。 最近分享的几篇文章都比较基础,关于字符类设备的驱动程序,以及中断处理程序。 也许在现代的项目是用不到这样的技术,但是万丈高楼平地起。 只有明白了这些最基础的知识点之后,再去看那些进化出来的高级玩意,才会有一步一个脚印的获得感。 如果缺少了这些基础的环节,很多深层次的东西,学起来就有点空中楼阁的感觉。 就好比研究Linux内核,如果一上来就从Linux 4.x/5.x内核版本开始研究,可以看到很多“历史遗留”代码。 这些代码就见证着Linux一步一步的发展历史,甚至有些人还会专门去研究 Linux 0.11 版本的内核源码,因为很多基本思想都是一样的。 今天这篇文章,主要还是以代码实例为主,把之前的两个知识点结合起来: 在中断处理函数中,发送信号给应用层,以此来通知应用层处理响应的中断业务。 驱动程序 示例代码全貌 所有的操作都是在 ~/tmp/linux-4.15/drivers 目录下完成的。 首先创建驱动模块目录: $ cd ~/tmp/linux-4.15/drivers $ mkdir my_driver_interrupt_signal $ touch my_driver_interrupt_signal.c 文件内容如下: #include <linux/module.h> #include <linux/kernel.h> #include <linux/ctype.h> #include <linux/device.h> #include <linux/cdev.h> #include <asm/siginfo.h> #include <linux/pid.h> #include <linux/uaccess.h> #include <linux/sched/signal.h> #include <linux/pid_namespace.h> #include <linux/interrupt.h> // 中断号 #define IRQ_NUM 1 // 定义驱动程序的 ID,在中断处理函数中用来判断是否需要处理 #define IRQ_DRIVER_ID 1234 // 设备名称 #define MYDEV_NAME "mydev" // 驱动程序数据结构 struct myirq { int devid; }; struct myirq mydev ={ IRQ_DRIVER_ID }; #define KBD_DATA_REG 0x60 #define KBD_STATUS_REG 0x64 #define KBD_SCANCODE_MASK 0x7f #define KBD_STATUS_MASK 0x80 // 设备类 static struct class *my_class; // 用来保存设备 struct cdev my_cdev; // 用来保存设备号 int mydev_major = 0; int mydev_minor = 0; // 用来保存向谁发送信号,应用程序通过 ioctl 把自己的进程 ID 设置进来。 static int g_pid = 0; // 用来发送信号给应用程序 static void send_signal(int sig_no) { int ret; struct siginfo info; struct task_struct *my_task = NULL; if (0 == g_pid) { // 说明应用程序没有设置自己的 PID printk("pid[%d] is not valid! n", g_pid); return; } printk("send signal %d to pid %d n", sig_no, g_pid); // 构造信号结构体 memset(&info, 0, sizeof(struct siginfo)); info.si_signo = sig_no; info.si_errno = 100; info.si_code = 200; // 获取自己的任务信息,使用的是 RCU 锁 rcu_read_lock(); my_task = pid_task(find_vpid(g_pid), PIDTYPE_PID); rcu_read_unlock(); if (my_task == NULL) { printk("get pid_task failed! n"); return; } // 发送信号 ret = send_sig_info(sig_no, &info, my_task); if (ret < 0) { printk("send signal failed! n"); } } //中断处理函数 static irqreturn_t myirq_handler(int irq, void * dev) { struct myirq mydev; unsigned char key_code; mydev = *(struct myirq*)dev; // 检查设备 id,只有当相等的时候才需要处理 if (IRQ_DRIVER_ID == mydev.devid) { // 读取键盘扫描码 key_code = inb(KBD_DATA_REG); if (key_code == 0x01) { printk("EXC key is pressed! n"); send_signal(SIGUSR1); } } return IRQ_HANDLED; } // 驱动模块初始化函数 static void myirq_init(void) { printk("myirq_init is called. n"); // 注册中断处理函数 if(request_irq(IRQ_NUM, myirq_handler, IRQF_SHARED, MYDEV_NAME, &mydev)!=0) { printk("register irq[%d] handler failed. n", IRQ_NUM); return -1; } printk("register irq[%d] handler success. n", IRQ_NUM); } // 当应用程序打开设备的时候被调用 static int mydev_open(struct inode *inode, struct file *file) { printk("mydev_open is called. n"); return 0; } static long mydev_ioctl(struct file* file, unsigned int cmd, unsigned long arg) { void __user *pArg; printk("mydev_ioctl is called. cmd = %d n", cmd); if (100 == cmd) { // 说明应用程序设置进程的 PID pArg = (void *)arg; if (!access_ok(VERIFY_READ, pArg, sizeof(int))) { printk("access failed! n"); return -EACCES; } // 把用户空间的数据复制到内核空间 if (copy_from_user(&g_pid, pArg, sizeof(int))) { printk("copy_from_user failed! n"); return -EFAULT; } } return 0; } static const struct file_operations mydev_ops={ .owner = THIS_MODULE, .open = mydev_open, .unlocked_ioctl = mydev_ioctl }; static int __init mydev_driver_init(void) { int devno; dev_t num_dev; printk("mydev_driver_init is called. n"); // 注册中断处理函数 if(request_irq(IRQ_NUM, myirq_handler, IRQF_SHARED, MYDEV_NAME, &mydev)!=0) { printk("register irq[%d] handler failed. n", IRQ_NUM); return -1; } // 动态申请设备号(严谨点的话,应该检查函数返回值) alloc_chrdev_region(&num_dev, mydev_minor, 1, MYDEV_NAME); // 获取主设备号 mydev_major = MAJOR(num_dev); printk("mydev_major = %d. n", mydev_major); // 创建设备类 my_class = class_create(THIS_MODULE, MYDEV_NAME); // 创建设备节点 devno = MKDEV(mydev_major, mydev_minor); // 初始化cdev结构 cdev_init(&my_cdev, &mydev_ops); // 注册字符设备 cdev_add(&my_cdev, devno, 1); // 创建设备节点 device_create(my_class, NULL, devno, NULL, MYDEV_NAME); return 0; } static void __exit mydev_driver_exit(void) { printk("mydev_driver_exit is called. n"); // 删除设备节点 cdev_del(&my_cdev); device_destroy(my_class, MKDEV(mydev_major, mydev_minor)); // 释放设备类 class_destroy(my_class); // 注销设备号 unregister_chrdev_region(MKDEV(mydev_major, mydev_minor), 1); // 注销中断处理函数 free_irq(IRQ_NUM, &mydev); } MODULE_LICENSE("GPL"); module_init(mydev_driver_init); module_exit(mydev_driver_exit); 以上代码主要做了两件事情: 注册中断号 1 的处理函数:myirq_handler(); 创建设备节点 /dev/mydev; 这里的中断号1,是键盘中断。 因为它是共享的中断,因此当键盘被按下的时候,操作系统就会依次调用所有的中断处理函数,当然就包括我们的驱动程序所注册的这个函数。 中断处理部分相关的几处关键代码如下: //中断处理函数 static irqreturn_t myirq_handler(int irq, void * dev) { ... } // 驱动模块初始化函数 static void myirq_init(void) { ... request_irq(IRQ_NUM, myirq_handler, IRQF_SHARED, MYDEV_NAME, &mydev); ... } 在中断处理函数中,目标是发送信号 SIGUSR1 到应用层,因此驱动程序需要知道应用程序的进程号(PID)。 根据之前的文章Linux驱动实践:驱动程序如何发送【信号】给应用程序?,应用程序必须主动把自己的 PID 告诉驱动模块才可以。这可以通过 write 或者ioctl函数来实现, 驱动程序用来接收 PID 的相关代码是: static long mydev_ioctl(struct file* file, unsigned int cmd, unsigned long arg) { ... if (100 == cmd) { pArg = (void *)arg; ... copy_from_user(&g_pid, pArg, sizeof(int)); } } 知道了应用程序的 PID,驱动程序就可以在中断发生的时候(按下键盘ESC键),发送信号出去了: static void send_signal(int sig_no) { struct siginfo info; ... send_sig_info(...); } static irqreturn_t myirq_handler(int irq, void * dev) { ... send_signal(SIGUSR1); } Makefile 文件 ifneq ($(KERNELRELEASE),) obj-m := my_driver_interrupt_signal.o else KERNELDIR ?= /lib/modules/$(shell uname -r)/build PWD := $(shell pwd) default: $(MAKE) -C $(KERNELDIR) M=$(PWD) modules clean: rm -rf *.o *.ko *.mod.* modules.* Module.* $(MAKE) -C $(KERNEL_PATH) M=$(PWD) clean endif 编译、测试 首先查看一下加载驱动模块之前,1号中断的所有驱动程序: 再看一下设备号: $ cat /proc/devices 因为驱动注册在创建设备节点的时候,是动态请求系统分配的。 根据之前的几篇文章可以知道,系统一般会分配244这个主设备号给我们,此刻还不存在这个设备号。 编译、加载驱动模块: $ make $ sudo insmod my_driver_interrupt_signal.ko 首先看一下 dmesg 的输出信息: 然后看一下中断驱动程序: 可以看到我们的驱动程序( mydev )已经登记在1号中断的最右面。 最后看一下设备节点情况: 驱动模块已经准备妥当,下面就是应用程序了。 应用程序 应用程序的主要功能就是两部分: 通过 ioctl 函数把自己的 PID 告诉驱动程序; 注册信号 SIGUSR1 的处理函数; 示例代码全貌 #include <stdio.h> #include <stdlib.h> #include <unistd.h> #include <assert.h> #include <fcntl.h> #include <sys/ioctl.h> #include <signal.h> 编译、测试 新开一个中断窗口,编译、执行应用程序: $ gcc my_interrupt_singal.c -o my_interrupt_singal $ sudo ./my_interrupt_singal open dev success! call ioctl. pid = 12907 // 这里进入 while 循环 由于应用程序调用了 open 和 ioctl 这两个函数,因此,驱动程序中两个对应的函数就会被执行。 这可以通过 dmesg 命令的输出信息看出来: 这个时候,按下键盘上的 ESC 键,此时驱动程序中打印如下信息: 说明:驱动程序捕获到了键盘上的 ESC 键,并且发送信号给应用程序了。 (编辑:晋中站长网) 【声明】本站内容均来自网络,其相关言论仅代表作者个人观点,不代表本站立场。若无意侵犯到您的权利,请及时与联系站长删除相关内容! |
站长推荐
热点阅读