Linux驱动-key、中断

最近一直在学Linux字符设备驱动,从最基础的轮询读取,一步步到中断、底半部机制,再到input子系统和内核定时器。

一、IO读取方式:轮询与阻塞IO

用户态读取设备,大体分两种思路:非阻塞轮询 、阻塞IO 。

轮询就是在循环里不停调用read(),CPU反复查询设备状态,CPU占用高;阻塞IO是驱动里让进程休眠,等事件到来之后再唤醒进程,空闲时CPU可以调度别的任务,效率更高。

要在驱动实现阻塞等待,核心依靠等待队列,4个常用接口:

  1. wait_event(wq, condition):不可中断等待。条件不满足就一直阻塞,收到信号也不会唤醒。
  2. wait_event_interruptible(wq, condition):可中断等待。事件发生或者收到用户态信号,都会唤醒进程,返回非0值代表被信号打断。
  3. wait_event_timeout(wq, condition, timeout):带超时的不可中断等待,超时自动唤醒。
  4. wait_event_interruptible_timeout(wq, condition, timeout):带超时的可中断等待。

唤醒对应的接口:

wake_up() 配合wait_event;wake_up_interruptible() 配合wait_event_interruptible。

注意:等待队列的条件变量,是共享资源,多并发场景下要考虑加锁保护。

二、中断基础:设备树获取中断号,注册中断

Linux 中断处理分为顶半部与底半部:顶半部在硬中断上下文快速完成硬件相关紧急操作,底半部延后处理耗时任务,以此缩短关中断时间,提升系统实时性。

Linux驱动中使用外部中断,标准流程:

  1. 设备树 里配置中断信息:interrupt-parent 指定中断控制器,interrupts 写中断号和触发方式。
  2. 在probe函数,用of_irq_get()从设备树节点拿到中断号。旧方式也可以用platform_get_resource获取。
  3. 使用request_irq / devm_request_irq注册中断处理函数(顶半部)。devm版本的好处是资源会自动释放,不容易内存泄漏。
  4. 中断处理函数(顶半部)执行要快,不能做耗时操作,不能休眠。

中断相关接口:

request_irq:注册中断处理函数

free_irq:释放中断号

disable_irq:关中断

enable_irq:开中断

中断资源保护:自旋锁

中断上下文里不能用互斥锁(会休眠),中断里面加锁只能选自旋锁spinlock。

spin_lock / spin_unlock:基础自旋锁。单核下只是关闭抢占;多核下循环忙等直到拿到锁。

spin_lock_irqsave / spin_unlock_irqrestore:上锁同时关闭本地中断,防止中断抢占临界区。

关键点:

  1. 单核CPU:spinlock只关闭高优先级抢占;
  2. 多核CPU:多个CPU之间会循环争抢锁;
  3. 自旋锁持有期间代码不能休眠,一旦休眠直接死锁。

三、中断底半部:tasklet 和 workqueue

中断顶半部要求执行极快,硬件信号应答必须在这里完成;耗时操作要放到底半部。两种常用实现:

  1. tasklet :属于中断上下文。依然运行在中断环境,不允许休眠,调度后延迟执行。适合短任务。
  2. workqueue(工作队列) :属于进程上下文。允许msleep、阻塞,适合有延时、耗时的任务。

按键驱动Demo:阻塞IO + 等待队列 + workqueue底半部

这个驱动使用platform框架+misc杂项设备,按下按键触发中断,中断里唤醒等待队列,用户态read阻塞等待按键按下,同时使用工作队列做底半部任务。

复制代码
#include <linux/init.h>
#include <linux/module.h>
#include <linux/cdev.h>
#include <linux/fs.h>
#include <linux/platform_device.h>
#include <linux/miscdevice.h>
#include <linux/of_gpio.h>
#include <linux/gpio.h>
#include <linux/uaccess.h>
#include <linux/wait.h>
#include <linux/sched.h>
#include <linux/interrupt.h>
#include <linux/irqreturn.h>
#include <linux/of_irq.h>
#include <linux/workqueue.h>

static int keygpiono = 0;
static wait_queue_head_t wq;
static int condition = 0;
static int irqno = 0;
static struct work_struct workqueue;

static void key_workqueue_func(struct work_struct *work)
{
    msleep(10);
    pr_info("key pressed\n");
    return;
}

static void key_func(unsigned long data)
{
    msleep(10);
    pr_info("key pressed\n");
    return;
}
static DECLARE_TASKLET(mytasklet, key_func, 0);

static irqreturn_t key_handler(int irqno, void *pdev)
{
    condition = 1;
    wake_up_interruptible(&wq);
    schedule_work(&workqueue);
    //tasklet_schedule(&mytasklet);
    return IRQ_HANDLED;
}

static ssize_t key_read(struct file *fp, char __user *puser, size_t n, loff_t *off)
{
    unsigned long nret = 0;
    int value = 0;
    condition = 0;
    wait_event_interruptible(wq, condition);
    value = gpio_get_value(keygpiono);
    nret = copy_to_user(puser, &value, sizeof(value));
    if (nret) {
        pr_info("copy_to_user failed\n");
        return -1;
    }
    return sizeof(value);
}

static struct file_operations fops = {
    .owner = THIS_MODULE,
    .read = key_read,
};

static struct miscdevice key_misc = {
    .minor = MISC_DYNAMIC_MINOR,
    .name = "key_misc",
    .fops = &fops,
};

static int key_probe(struct platform_device *pdevice)
{
    int ret = 0;
    ret = misc_register(&key_misc);
    if (ret) {
        pr_info("misc_register failed\n");
        return -1;
    }
    INIT_WORK(&workqueue, key_workqueue_func);
    init_waitqueue_head(&wq);
    keygpiono = of_get_named_gpio(pdevice->dev.of_node, "gpio-key", 0);
    if (keygpiono < 0) {
        pr_info("of_get_named_gpio failed\n");
        return -1;
    }
    ret = devm_gpio_request(&pdevice->dev, keygpiono, "key_platform_drv");
    if (ret) {
        pr_info("devm_gpio_request failed\n");
        return -1;
    }
    irqno = of_irq_get(pdevice->dev.of_node, 0);
    if (irqno < 0) {
        pr_info("of_irq_get failed\n");
        return -1;
    }
    ret = devm_request_irq(&pdevice->dev, irqno, key_handler, IRQF_TRIGGER_FALLING, "key_platform_drv", NULL);
    if (ret) {
        pr_info("request_irq failed\n");
        return -1;
    }
    gpio_direction_input(keygpiono);
    pr_info("key_probe success\n");
    return 0;
}

static int key_remove(struct platform_device *pdevice)
{
    misc_deregister(&key_misc);
    pr_info("key_remove success\n");
    return 0;
}

static struct platform_device_id key_id_tables[] = {
    { .name = "putekey"},
    {},
};
static struct of_device_id  key_of_match_table[] = {
    {.compatible = "pute,putekey"},
    {},
};
struct platform_driver key_drv = {
    .probe = key_probe,
    .remove = key_remove,
    .driver = {
        .name = "putekey",
        .owner = THIS_MODULE,
        .of_match_table = key_of_match_table,
    },
    .id_table = key_id_tables,
};
module_platform_driver(key_drv);
MODULE_LICENSE("GPL");

这个代码里msleep(10)放在workqueue没问题,如果放到tasklet或者中断顶半部就会直接报错,因为中断上下文不能休眠。

四、Input子系统:标准输入设备驱动

上面是自己写misc设备上报按键值。Linux内核提供了input子系统,专门统一管理按键、触摸屏、鼠标这类输入设备。

使用input子系统的开发流程:

  1. input_allocate_device():分配input设备结构体
  2. input_set_capability():告诉内核这个设备具备什么事件能力(EV_KEY按键事件)
  3. input_register_device():注册input设备
  4. 中断触发后,调用input_event()上报事件,input_sync()同步事件,通知应用层。

应用层可以通过/dev/input/eventX读取按键事件,不用自己写字符设备read逻辑。

Input子系统按键驱动代码

复制代码
#include <linux/init.h>
#include <linux/module.h>
#include <linux/cdev.h>
#include <linux/fs.h>
#include <linux/platform_device.h>
#include <linux/miscdevice.h>
#include <linux/of_gpio.h>
#include <linux/gpio.h>
#include <linux/uaccess.h>
#include <linux/wait.h>
#include <linux/sched.h>
#include <linux/interrupt.h>
#include <linux/irqreturn.h>
#include <linux/of_irq.h>
#include <linux/workqueue.h>
#include <linux/input.h>

static int irqno = 0;
struct input_dev *pkey_dev = NULL;

static void mytasklet_func(unsigned long data)
{
    input_event(pkey_dev, EV_KEY, KEY_0, 1);
    input_sync(pkey_dev);
    input_event(pkey_dev, EV_KEY, KEY_0, 0);
    input_sync(pkey_dev);
    return;
}
static DECLARE_TASKLET(mytasklet, mytasklet_func, 0);

static irqreturn_t key_isr(int irqno, void *pdev)
{
    tasklet_schedule(&mytasklet);
    return IRQ_HANDLED;
}

static int key_probe(struct platform_device *pdevice)
{
    int ret = 0;
    pkey_dev = input_allocate_device();
    if (NULL == pkey_dev) {
        pr_info("input_allocate_device failed\n");
        return -1;
    }
    input_set_capability(pkey_dev, EV_KEY, KEY_0);
    ret = input_register_device(pkey_dev);
    if (ret) {
        pr_info("input_register_device failed\n");
        return -1;
    }
    irqno = of_irq_get(pdevice->dev.of_node, 0);
    if (irqno < 0) {
        pr_info("of_irq_get failed\n");
        return -1;
    }
    ret = devm_request_irq(&pkey_dev->dev, irqno, key_isr, IRQF_TRIGGER_FALLING, "key_platform_input_drv", NULL);
    if (ret) {
        pr_info("devm_request_irq failed\n");
        return -1;
    }
    return 0;
}

static int key_remove(struct platform_device *pdevice)
{
    input_unregister_device(pkey_dev);
    input_free_device(pkey_dev);
    return 0;
}

static struct platform_device_id key_id_tables[] = {
    { .name = "putekey"},
    {},
};
static struct of_device_id  key_of_match_table[] = {
    {.compatible = "pute,putekey"},
    {},
};
struct platform_driver key_drv = {
    .probe = key_probe,
    .remove = key_remove,
    .driver = {
        .name = "putekey",
        .owner = THIS_MODULE,
        .of_match_table = key_of_match_table,
    },
    .id_table = key_id_tables,
};
module_platform_driver(key_drv);
MODULE_LICENSE("GPL");

五、内核定时器

内核定时器依靠系统节拍jiffies,HZ代表每秒节拍数,HZ=100就是1秒100次节拍。

定时器属于一次性触发,到点后执行回调函数。回调运行在中断上下文,不能休眠。

开发步骤:

  1. 定义timer结构体,设置超时回调函数
  2. init_timer初始化
  3. add_timer添加定时器
  4. mod_timer修改定时超时时间
  5. del_timer删除定时器

内核定时器示例代码

复制代码
#include <linux/fs.h>
#include <linux/module.h>
#include <linux/cdev.h>
#include <linux/init.h>
#include <linux/timer.h>

static void timeout(unsigned long data)
{
    pr_info("timeout ok!\n");
    return;
}
static DEFINE_TIMER(mtimer, timeout, 0, 0);

static int __init kernel_timer_init(void)
{
    init_timer(&mtimer);
    add_timer(&mtimer);
    mod_timer(&mtimer, jiffies + msecs_to_jiffies(100));
    pr_info("add timer success\n");
    return 0;
}
static void __exit kernel_timer_exit(void)
{
    del_timer(&mtimer);
    pr_info("del timer success\n");
    return;
}
module_init(kernel_timer_init);
module_exit(kernel_timer_exit);
MODULE_LICENSE("GPL");

六、延时函数

  • udelay():忙等待延时,CPU空转,延时精度高,适合微秒级,不能休眠。
  • msleep():休眠延时,让出CPU调度,精度一般,进程上下文可用,中断上下文不能调用。
  • msleep_interruptible():可被信号打断的休眠延时。

这段时间把中断上下半部、等待队列、input子系统、内核定时器串联起来,可以明显感受到Linux驱动的设计思想:顶半部快处理,底半部做耗时任务;阻塞IO减少CPU占用;子系统封装通用设备逻辑 。

之前写的misc按键驱动是自己从零实现字符设备,而input子系统是内核提供好的框架,直接上报标准事件,应用层不用适配特殊设备文件,这也是内核子系统的优势。

相关推荐
励志师code1 小时前
数据搬运与存储对接实战:rclone / juicesync 迁移 + 目录对账 + 容器多源挂载
运维·云计算
小龙报1 小时前
【零到全栈】从扩展名到终端实操,打通 Linux 基础
linux·ubuntu·云原生·职场和发展·创业创新·学习方法·程序员创富
星恒随风1 小时前
Linux进程间通信(一):从匿名管道、命名管道到进程池,深入理解Pipe、FIFO与文件描述符
linux·运维·笔记·学习·状态模式
wuminyu1 小时前
ZGC染色指针在虚拟线程stackChunkOop上的并发屏障全景分析
java·linux·c语言·jvm·c++
刘胡子大叔2 小时前
服务器不能编译怎么办
运维·服务器
硅基手札2 小时前
【I2C 技术系列 11】I2C Linux 驱动框架
linux·驱动开发·stm32·单片机·嵌入式硬件·mcu·计算机外设
imDwAaY2 小时前
程序里的地址是真实内存地址吗?从虚拟内存到地址转换
java·linux·服务器
INGNIGHT2 小时前
486. 预测赢家(博弈dp/区间dp)
linux·c++
每日品牌解读2 小时前
企业经由 API 调用大模型,推荐哪些拥有数据隐私保护能力的平台?
运维·网络