Linux驱动实践:中断处理函数如何【发送信号】给应用层?

系统 Linux
如果缺少了这些基础的环节,很多深层次的东西,学起来就有点空中楼阁的感觉。就好比研究Linux内核,如果一上来就从Linux 4.x/5.x内核版本开始研究,可以看到很多“历史遗留”代码。

[[441156]]

别人的经验,我们的阶梯!

大家好,我是道哥,今天我为大伙儿解说的技术知识点是:【中断程序如何发送信号给应用层】。

最近分享的几篇文章都比较基础,关于字符类设备的驱动程序,以及中断处理程序。

也许在现代的项目是用不到这样的技术,但是万丈高楼平地起。

只有明白了这些最基础的知识点之后,再去看那些进化出来的高级玩意,才会有一步一个脚印的获得感。

如果缺少了这些基础的环节,很多深层次的东西,学起来就有点空中楼阁的感觉。

就好比研究Linux内核,如果一上来就从Linux 4.x/5.x内核版本开始研究,可以看到很多“历史遗留”代码。

这些代码就见证着Linux一步一步的发展历史,甚至有些人还会专门去研究 Linux 0.11 版本的内核源码,因为很多基本思想都是一样的。

今天这篇文章,主要还是以代码实例为主,把之前的两个知识点结合起来:

在中断处理函数中,发送信号给应用层,以此来通知应用层处理响应的中断业务。

驱动程序

示例代码全貌

所有的操作都是在 ~/tmp/linux-4.15/drivers 目录下完成的。

首先创建驱动模块目录:

  1. $ cd ~/tmp/linux-4.15/drivers 
  2. $ mkdir my_driver_interrupt_signal 
  3. $ touch my_driver_interrupt_signal.c 

文件内容如下:

  1. #include <linux/module.h> 
  2. #include <linux/kernel.h> 
  3. #include <linux/ctype.h> 
  4. #include <linux/device.h> 
  5. #include <linux/cdev.h> 
  6.  
  7. #include <asm/siginfo.h> 
  8. #include <linux/pid.h> 
  9. #include <linux/uaccess.h> 
  10. #include <linux/sched/signal.h> 
  11. #include <linux/pid_namespace.h> 
  12. #include <linux/interrupt.h> 
  13.  
  14. // 中断号 
  15. #define IRQ_NUM         1 
  16.  
  17. // 定义驱动程序的 ID,在中断处理函数中用来判断是否需要处理     
  18. #define IRQ_DRIVER_ID   1234 
  19.  
  20. // 设备名称 
  21. #define MYDEV_NAME      "mydev" 
  22.  
  23. // 驱动程序数据结构 
  24. struct myirq 
  25.     int devid; 
  26. }; 
  27.   
  28. struct myirq mydev  ={ IRQ_DRIVER_ID }; 
  29.  
  30. #define KBD_DATA_REG        0x60   
  31. #define KBD_STATUS_REG      0x64 
  32. #define KBD_SCANCODE_MASK   0x7f 
  33. #define KBD_STATUS_MASK     0x80 
  34.  
  35. // 设备类 
  36. static struct class *my_class; 
  37.  
  38. // 用来保存设备 
  39. struct cdev my_cdev; 
  40.  
  41. // 用来保存设备号 
  42. int mydev_major = 0; 
  43. int mydev_minor = 0; 
  44.  
  45. // 用来保存向谁发送信号,应用程序通过 ioctl 把自己的进程 ID 设置进来。 
  46. static int g_pid = 0; 
  47.  
  48. // 用来发送信号给应用程序 
  49. static void send_signal(int sig_no) 
  50.     int ret; 
  51.     struct siginfo info; 
  52.     struct task_struct *my_task = NULL
  53.     if (0 == g_pid) 
  54.     { 
  55.         // 说明应用程序没有设置自己的 PID 
  56.         printk("pid[%d] is not valid! \n", g_pid); 
  57.         return
  58.     } 
  59.  
  60.     printk("send signal %d to pid %d \n", sig_no, g_pid); 
  61.  
  62.     // 构造信号结构体 
  63.     memset(&info, 0, sizeof(struct siginfo)); 
  64.     info.si_signo = sig_no; 
  65.     info.si_errno = 100; 
  66.     info.si_code = 200; 
  67.  
  68.     // 获取自己的任务信息,使用的是 RCU 锁 
  69.     rcu_read_lock(); 
  70.     my_task = pid_task(find_vpid(g_pid), PIDTYPE_PID); 
  71.     rcu_read_unlock(); 
  72.  
  73.     if (my_task == NULL
  74.     { 
  75.         printk("get pid_task failed! \n"); 
  76.         return
  77.     } 
  78.  
  79.     // 发送信号 
  80.     ret = send_sig_info(sig_no, &info, my_task); 
  81.     if (ret < 0)  
  82.     { 
  83.            printk("send signal failed! \n"); 
  84.     } 
  85.  
  86. //中断处理函数 
  87. static irqreturn_t myirq_handler(int irq, void * dev) 
  88.     struct myirq mydev; 
  89.     unsigned char key_code; 
  90.     mydev = *(struct myirq*)dev;     
  91.      
  92.     // 检查设备 id,只有当相等的时候才需要处理 
  93.     if (IRQ_DRIVER_ID == mydev.devid) 
  94.     { 
  95.         // 读取键盘扫描码 
  96.         key_code = inb(KBD_DATA_REG); 
  97.      
  98.         if (key_code == 0x01) 
  99.         { 
  100.             printk("EXC key is pressed! \n"); 
  101.             send_signal(SIGUSR1); 
  102.         } 
  103.     }    
  104.  
  105.     return IRQ_HANDLED; 
  106.  
  107. // 驱动模块初始化函数 
  108. static void myirq_init(void) 
  109.     printk("myirq_init is called. \n"); 
  110.  
  111.     // 注册中断处理函数 
  112.     if(request_irq(IRQ_NUM, myirq_handler, IRQF_SHARED, MYDEV_NAME, &mydev)!=0) 
  113.     { 
  114.         printk("register irq[%d] handler failed. \n", IRQ_NUM); 
  115.         return -1; 
  116.     } 
  117.  
  118.     printk("register irq[%d] handler success. \n", IRQ_NUM); 
  119.  
  120. // 当应用程序打开设备的时候被调用 
  121. static int mydev_open(struct inode *inode, struct file *file) 
  122.      
  123.     printk("mydev_open is called. \n"); 
  124.     return 0;    
  125.  
  126. static long mydev_ioctl(struct file* file, unsigned int cmd, unsigned long arg) 
  127.     void __user *pArg; 
  128.     printk("mydev_ioctl is called. cmd = %d \n", cmd); 
  129.     if (100 == cmd) 
  130.     { 
  131.         // 说明应用程序设置进程的 PID  
  132.         pArg = (void *)arg; 
  133.         if (!access_ok(VERIFY_READ, pArg, sizeof(int))) 
  134.         { 
  135.             printk("access failed! \n"); 
  136.             return -EACCES; 
  137.         } 
  138.  
  139.         // 把用户空间的数据复制到内核空间 
  140.         if (copy_from_user(&g_pid, pArg, sizeof(int))) 
  141.         { 
  142.             printk("copy_from_user failed! \n"); 
  143.             return -EFAULT; 
  144.         } 
  145.     } 
  146.  
  147.     return 0; 
  148.  
  149. static const struct file_operations mydev_ops={ 
  150.     .owner = THIS_MODULE, 
  151.     .open  = mydev_open, 
  152.     .unlocked_ioctl = mydev_ioctl 
  153. }; 
  154.  
  155. static int __init mydev_driver_init(void) 
  156.     int devno; 
  157.     dev_t num_dev; 
  158.  
  159.     printk("mydev_driver_init is called. \n"); 
  160.  
  161.     // 注册中断处理函数 
  162.     if(request_irq(IRQ_NUM, myirq_handler, IRQF_SHARED, MYDEV_NAME, &mydev)!=0) 
  163.     { 
  164.         printk("register irq[%d] handler failed. \n", IRQ_NUM); 
  165.         return -1; 
  166.     } 
  167.  
  168.     // 动态申请设备号(严谨点的话,应该检查函数返回值) 
  169.     alloc_chrdev_region(&num_dev, mydev_minor, 1, MYDEV_NAME); 
  170.  
  171.     // 获取主设备号 
  172.     mydev_major = MAJOR(num_dev); 
  173.     printk("mydev_major = %d. \n", mydev_major); 
  174.  
  175.     // 创建设备类 
  176.     my_class = class_create(THIS_MODULE, MYDEV_NAME); 
  177.  
  178.     // 创建设备节点 
  179.     devno = MKDEV(mydev_major, mydev_minor); 
  180.      
  181.     // 初始化cdev结构 
  182.     cdev_init(&my_cdev, &mydev_ops); 
  183.  
  184.     // 注册字符设备 
  185.     cdev_add(&my_cdev, devno, 1); 
  186.  
  187.     // 创建设备节点 
  188.     device_create(my_class, NULL, devno, NULL, MYDEV_NAME); 
  189.  
  190.     return 0; 
  191.  
  192. static void __exit mydev_driver_exit(void) 
  193. {    
  194.     printk("mydev_driver_exit is called. \n"); 
  195.  
  196.     // 删除设备节点 
  197.     cdev_del(&my_cdev); 
  198.     device_destroy(my_class, MKDEV(mydev_major, mydev_minor)); 
  199.  
  200.     // 释放设备类 
  201.     class_destroy(my_class); 
  202.  
  203.     // 注销设备号 
  204.     unregister_chrdev_region(MKDEV(mydev_major, mydev_minor), 1); 
  205.  
  206.     // 注销中断处理函数 
  207.     free_irq(IRQ_NUM, &mydev); 
  208.  
  209. MODULE_LICENSE("GPL"); 
  210. module_init(mydev_driver_init); 
  211. module_exit(mydev_driver_exit); 

以上代码主要做了两件事情:

  • 注册中断号 1 的处理函数:myirq_handler();
  • 创建设备节点 /dev/mydev;

这里的中断号1,是键盘中断。

因为它是共享的中断,因此当键盘被按下的时候,操作系统就会依次调用所有的中断处理函数,当然就包括我们的驱动程序所注册的这个函数。

中断处理部分相关的几处关键代码如下:

  1. //中断处理函数 
  2. static irqreturn_t myirq_handler(int irq, void * dev) 
  3.     ... 
  4.  
  5. // 驱动模块初始化函数 
  6. static void myirq_init(void) 
  7.     ... 
  8.     request_irq(IRQ_NUM, myirq_handler, IRQF_SHARED, MYDEV_NAME, &mydev); 
  9.     ... 

在中断处理函数中,目标是发送信号 SIGUSR1 到应用层,因此驱动程序需要知道应用程序的进程号(PID)。

根据之前的文章Linux驱动实践:驱动程序如何发送【信号】给应用程序?,应用程序必须主动把自己的 PID 告诉驱动模块才可以。这可以通过 write 或者ioctl函数来实现,

驱动程序用来接收 PID 的相关代码是:

  1. static long mydev_ioctl(struct file* file, unsigned int cmd, unsigned long arg) 
  2.     ... 
  3.     if (100 == cmd) 
  4.     { 
  5.         pArg = (void *)arg; 
  6.         ... 
  7.         copy_from_user(&g_pid, pArg, sizeof(int)); 
  8.     } 

知道了应用程序的 PID,驱动程序就可以在中断发生的时候(按下键盘ESC键),发送信号出去了:

  1. static void send_signal(int sig_no) 
  2.     struct siginfo info; 
  3.     ... 
  4.     send_sig_info(...); 
  5.  
  6. static irqreturn_t myirq_handler(int irq, void * dev) 
  7.     ... 
  8.     send_signal(SIGUSR1); 

Makefile 文件

  1. ifneq ($(KERNELRELEASE),) 
  2.     obj-m := my_driver_interrupt_signal.o 
  3. else 
  4.     KERNELDIR ?= /lib/modules/$(shell uname -r)/build 
  5.     PWD := $(shell pwd) 
  6. default
  7.     $(MAKE) -C $(KERNELDIR) M=$(PWD) modules 
  8. clean: 
  9.     rm -rf *.o *.ko *.mod.* modules.* Module.*  
  10.     $(MAKE) -C $(KERNEL_PATH) M=$(PWD) clean 
  11. endif 

编译、测试

首先查看一下加载驱动模块之前,1号中断的所有驱动程序:

再看一下设备号:

  1. $ cat /proc/devices 

因为驱动注册在创建设备节点的时候,是动态请求系统分配的。

根据之前的几篇文章可以知道,系统一般会分配244这个主设备号给我们,此刻还不存在这个设备号。

编译、加载驱动模块:

  1. $ make 
  2. $ sudo insmod my_driver_interrupt_signal.ko 

首先看一下 dmesg 的输出信息:

然后看一下中断驱动程序:

可以看到我们的驱动程序( mydev )已经登记在1号中断的最右面。

最后看一下设备节点情况:

驱动模块已经准备妥当,下面就是应用程序了。

应用程序

应用程序的主要功能就是两部分:

通过 ioctl 函数把自己的 PID 告诉驱动程序;

注册信号 SIGUSR1 的处理函数;

示例代码全貌

  1. #include <stdio.h> 
  2. #include <stdlib.h> 
  3. #include <unistd.h> 
  4. #include <assert.h> 
  5. #include <fcntl.h> 
  6. #include <sys/ioctl.h> 
  7. #include <signal.h> 
  8.  
  9.  
  10. char *dev_name = "/dev/mydev"
  11.  
  12. // 信号处理函数 
  13. static void signal_handler(int signum, siginfo_t *info, void *context) 
  14.     // 打印接收到的信号值 
  15.     printf("signal_handler: signum = %d \n", signum); 
  16.     printf("signo = %d, code = %d, errno = %d \n"
  17.              info->si_signo, 
  18.              info->si_code,  
  19.              info->si_errno); 
  20.  
  21. int main(int argc, char *argv[]) 
  22.     int fd, count = 0; 
  23.     int pid = getpid(); 
  24.  
  25.     // 打开GPIO 
  26.     if((fd = open(dev_name, O_RDWR | O_NDELAY)) < 0){ 
  27.         printf("open dev failed! \n"); 
  28.         return -1; 
  29.     } 
  30.  
  31.     printf("open dev success! \n"); 
  32.      
  33.     // 注册信号处理函数 
  34.     struct sigaction sa; 
  35.     sigemptyset(&sa.sa_mask); 
  36.     sa.sa_sigaction = &signal_handler; 
  37.     sa.sa_flags = SA_SIGINFO; 
  38.      
  39.     sigaction(SIGUSR1, &sa, NULL); 
  40.  
  41.     // set PID  
  42.     printf("call ioctl. pid = %d \n", pid); 
  43.     ioctl(fd, 100, &pid); 
  44.  
  45.     // 死循环,等待接收信号 
  46.     while (1) 
  47.         sleep(1); 
  48.  
  49.     // 关闭设备 
  50.     close(fd); 

在应用程序的最后,是一个 while(1) 死循环。因为只有在按下键盘上的ESC按键时,驱动程序才会发送信号上来,因此应用程序需要一直存活着。

编译、测试

新开一个中断窗口,编译、执行应用程序:

  1. $ gcc my_interrupt_singal.c -o my_interrupt_singal 
  2. $ sudo ./my_interrupt_singal 
  3. open dev success!  
  4. call ioctl. pid = 12907 
  5.  
  6. // 这里进入 while 循环 

由于应用程序调用了 open 和 ioctl 这两个函数,因此,驱动程序中两个对应的函数就会被执行。

这可以通过 dmesg 命令的输出信息看出来:

这个时候,按下键盘上的 ESC 键,此时驱动程序中打印如下信息:

说明:驱动程序捕获到了键盘上的 ESC 键,并且发送信号给应用程序了。

在执行应用程序的终端窗口中,可以看到如下输出信息:

 说明:应用程序接收到了驱动程序发来的信号!

 

责任编辑:武晓燕 来源: IOT物联网小镇
相关推荐

2021-12-06 07:47:36

Linux 驱动程序Linux 系统

2017-01-16 15:05:17

Linux信号机制分析

2017-01-16 14:48:42

Linux信号机制分析

2020-08-17 08:18:51

Java

2011-09-09 16:19:40

Android Web

2016-11-29 15:22:47

协议应用层安全层

2021-12-27 07:55:59

Linux 中断处理Linux 系统

2020-12-29 09:11:33

LinuxLinux内核

2010-06-13 17:51:16

SET应用层协议

2024-01-08 09:08:53

2010-06-25 15:22:16

2011-11-21 09:55:31

2010-06-13 17:46:47

2012-01-13 10:13:57

软件定义网络SDNOpenFlow

2010-06-21 17:58:06

2014-12-15 11:23:00

Docker Comp分布式应用容器应用

2010-06-09 10:25:18

SET应用层协议

2014-06-27 10:04:55

网络协议ipv4IP

2017-05-11 09:10:31

CAN-bus应用层协议

2010-06-09 10:28:20

点赞
收藏

51CTO技术栈公众号