首页 > 系统 > Linux > 正文

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

2022-07-09 12:57:31
字体:
来源:转载
供稿:网友
别人的经验,我们的阶梯!
 
大家好,我是道哥,今天我为大伙儿解说的技术知识点是:【中断程序如何发送信号给应用层】。
 
最近分享的几篇文章都比较基础,关于字符类设备的驱动程序,以及中断处理程序。
 
也许在现代的项目是用不到这样的技术,但是万丈高楼平地起。
 
只有明白了这些最基础的知识点之后,再去看那些进化出来的高级玩意,才会有一步一个脚印的获得感。
 
如果缺少了这些基础的环节,很多深层次的东西,学起来就有点空中楼阁的感觉。
 
就好比研究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 键,并且发送信号给应用程序了。

(编辑:错新网)

发表评论 共有条评论
用户名: 密码:
验证码: 匿名发表