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


    作 者:道哥,10+年嵌入式开发老兵,专注于:C/C++、嵌入式、Linux

    关注下方公众号,回复【书籍】,获取 Linux、嵌入式领域经典书籍;回复【PDF】,获取所有原创文章( PDF 格式)。

    目录

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

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

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

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

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

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

    就好比研究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. 注册中断号 1 的处理函数:myirq_handler();

    2. 创建设备节点 /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号中断的最右面。

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

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

    应用程序

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

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

    2. 注册信号 SIGUSR1 的处理函数;

    示例代码全貌

    #include <stdio.h>
    #include <stdlib.h>
    #include <unistd.h>
    #include <assert.h>
    #include <fcntl.h>
    #include <sys/ioctl.h>
    #include <signal.h>
    
    
    char *dev_name = "/dev/mydev";
    
    // 信号处理函数
    static void signal_handler(int signum, siginfo_t *info, void *context)
    {
    	// 打印接收到的信号值
        printf("signal_handler: signum = %d \n", signum);
        printf("signo = %d, code = %d, errno = %d \n",
    	         info->si_signo,
    	         info->si_code, 
    	         info->si_errno);
    }
    
    int main(int argc, char *argv[])
    {
    	int fd, count = 0;
    	int pid = getpid();
    
    	// 打开GPIO
    	if((fd = open(dev_name, O_RDWR | O_NDELAY)) < 0){
    		printf("open dev failed! \n");
    		return -1;
    	}
    
    	printf("open dev success! \n");
    	
    	// 注册信号处理函数
    	struct sigaction sa;
    	sigemptyset(&sa.sa_mask);
    	sa.sa_sigaction = &signal_handler;
    	sa.sa_flags = SA_SIGINFO;
    	
    	sigaction(SIGUSR1, &sa, NULL);
    
    	// set PID 
    	printf("call ioctl. pid = %d \n", pid);
    	ioctl(fd, 100, &pid);
    
    	// 死循环,等待接收信号
    	while (1)
    		sleep(1);
    
    	// 关闭设备
    	close(fd);
    }
    

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

    编译、测试

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

    $ 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 键,并且发送信号给应用程序了

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

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


    ------ End ------

    文中的测试代码和相关文档,已经放在网盘了。

    在公众号【IOT物联网小镇】后台回复关键字:1220,即可获取下载地址。

    谢谢!

    推荐阅读

    【1】《Linux 从头学》系列文章

    【2】C语言指针-从底层原理到花式技巧,用图文和代码帮你讲解透彻

    【3】原来gdb的底层调试原理这么简单

    【4】内联汇编很可怕吗?看完这篇文章,终结它!

    其他系列专辑:精选文章应用程序设计物联网C语言

    星标公众号,第一时间看文章!

  • 相关阅读:
    第一篇unity
    C#相关知识小结
    必须知道的八大种排序算法【java实现】
    JAVA八大排序算法
    二进制、八进制、十进制、十六进制之间的转换
    八大排序算法
    JSONArray数据转换成java List
    使用json-lib的JSONObject.toBean( )时碰到的日期属性转换的问题
    一探前端开发中的JS调试技巧
    SpringMVC注解说明
  • 原文地址:https://www.cnblogs.com/sewain/p/15785862.html
Copyright © 2020-2023  润新知