Linux驱动开发——中断编程之顶半部与底半部机制(1)

简介: Linux驱动开发——中断编程之顶半部与底半部机制(1)

文章目录

Linux中断编程之顶半部和底半部机制

概述

Linux内核对中断处理函数(isr_handler)的要求

Linux内核中断编程之顶半部特点

Linux中断编程之底半部特点

底半部实现机制之tasklet

案例实现(使用tasklet在按键后处理执行打印信息)

底半部实现机制之工作队列

案例实现(使用工作队列在按键后处理执行打印信息)

底半部实现机制之软中断

总结:


Linux中断编程之顶半部和底半部机制

概述

前面已经简单了解并实现了基于中断机制的任务处理在内核中,但是实际上Linux的中断机制远远不是只要这么简单的。

在Linux内核中,任务处理类型细分的话分为三类:进程、软中断、硬件中断。而其中软中断和硬件中断都是属于中断方式(打断当前任务获取CPU资源处理触发事件在还原当前执行任务)。

前面也曾提到过“优先级”一词,“优先级”是衡量一个任务获取CPU资源的一种能力评价,优先级越高那么获取CPU资源的能力越强,它投入运行的时间就能提前。Linux内核任务优先级划分:


  • 硬件中断的优先级高于软中断。
  • 软中断的优先级高于进程。
  • 进程之间也存在优先级,高优先级的进程和低优先级的进程。
  • 软中断同样具有优先级之分,但是硬件中断并没有优先级之分,先到先处理。
  • 当高优先级任务占用CPU资源的适合,其他进程会进入“休眠”,但是中断的世界里是没有休眠的,并且中断不参与进程的调度。中断的世界没有休眠,那么对中断的使用要求就很高了,越是高优先级的中断任务,那么对其处理时间就越要求短,如果高优先级任务长期占有CPU资源自然会导致延误很多其他中断或者进程的处理,所以就有了Linux中断编程中的顶半部和底半部机制了。

Linux内核对中断处理函数(isr_handler)的要求

注意:Linux内核要求中断处理函数执行的速度越快越好,更不能进行休眠操作,如果中断处理函数长时间占用CPU资源,而本身它的优先级最高,那么就会导致其他任务无法及时获取到CPU资源,也就无法及时运行处理,从而影响了并发和响应能力。

那么Linux内核中有些硬件的中断处理函数无法满足Linux内核的这个要求呢,比如在中断处理中需要等待外设的一些数据进行读取呢,那么对于这种不能满足Linux内核的要求的中断处理,势必会影响到Linux内核的并发和响应能力,对于这种情况,我们就需要使用Linux内核的顶半部和底半部机制来对中断处理任务进行优化了。


Linux内核中断编程之顶半部特点

顶半部就是做原先中断处理函数中比较紧急、耗时较短的内容,一旦硬件中断触发,CPU首先执行顶半部,执行时间非常快,执行完会立即释放CPU资源给其他任务使用,CPU在执行期间不允许发生CPU资源的切换,也就能踏踏实实的执行顶半部任务,顶半部本质就是中断处理函数,只是将原先中断处理中的最紧要、耗时最短的内容留下了而已。


Linux中断编程之底半部特点

底半部做原先中断处理函数中比较耗时且不紧急的内容,CPU会在“适当”的适合去执行底半部的任务(当CPU执行底半部的时候,底半部的优先级势必是当前最高的),但是CPU在执行期间如果再次遇到一个高优先级的任务到来,那么势必会抢走底半部的CPU资源,等到高优先级的任务执行完毕后再将CPU资源还给底半部,底半部继续进行处理,底半部本质就是延后执行的一种手段而已。

如果仅仅单纯将某个事件延后执行,可以利用底半部机制来实现,底半部实现方法有三种:


  • tasklet
  • 工作队列
  • 软中断

底半部实现机制之tasklet

tasklet特点: tasklet本身是基于软中断实现,优先级是高于进程而低于硬件中断,所以tasklet对应的延后处理函数不能进行休眠操作,tasklet对应的延后处理函数执行原先中断处理函数中毕竟耗时和不紧急的内容。


  • Linux内核描述tasklet的数据结构:
struct tasklet_struct {
  void (*func)(unsigned long data);
  unsigned long data;
  ...
};


成员说明:


  • func:tasklet的延后处理函数,做原先中断处理函数中不紧急或者耗时较长的内容,由于基于软中断实现,所以里面不能进行休眠操作,形参data是保存传递进来的参数。
  • data:给tasklet延后处理函数传递的参数。
  • 利用tasklet实现底半部延后执行的编程步骤:


  1. 定义初始化一个tasklet对象
int g_data = 0x55;
DECLARE_TASKLET(btn_tasklet, //对象名
        btn_tasklet_func,//tasklet的延后处理函数
        (unsigned long)&g_data);//给延后处理函数传递的参数
//结果:
btn_tasklet.func = btn_tasklet_func;
btn_tasklet.data = (unsigned long)&g_data;

  1. 编写tasklet延后处理函数(例如上面的btn_tasklet_func)
//data = (unsigned long)&g_data;
void btn_tasklet_func(unsigned long data)
{
  //做原先中断处理函数中耗时不紧急的内容
}

  1. 在顶半部中断处理函数中向内核“登记”(而不是调用),一旦登记完成,内核会在“适当”的时候去调用tasklet的延后处理函数,登记的函数为:
tasklet_schedule(&btn_tasklet);

案例实现(使用tasklet在按键后处理执行打印信息)

  • btn_drv.c
#include <linux/init.h>
#include <linux/module.h>
#include <linux/irq.h>
#include <linux/interrupt.h>
#include <linux/gpio.h>
#include <mach/platform.h>
#include <linux/input.h> //标准按键值:KEY_*
//声明描述按键信息相关的数据结构
struct btn_resource {
    int gpio; //按键对应的GPIO编号
    char *name; //按键名称
    int code; //按键值
};
//定义初始化按键硬件信息对象
static struct btn_resource btn_info[] = {
    {
        .gpio = PAD_GPIO_A+28,
        .name = "KEY_UP",
        .code = KEY_UP
    },
    {
        .gpio = PAD_GPIO_B+9,
        .name = "KEY_DOWN",
        .code = KEY_DOWN
    },
};
//记录按键的硬件信息
static struct btn_resource *pdata;
//tasklet延后处理函数
//适当的时候被内核调用
//基于软中断实现
//不能进行休眠操作
//data = (unsigned long)&g_data;
static void btn_tasket_function(unsigned long data)
{
    int state;
    //1.获取按键对应GPIO的电平状态,获取按键的操作状态
    state = gpio_get_value(pdata->gpio);
    //2.打印按键信息
    printk("底半部:%s:按键值[%d]按键状态[%s]\n",
            __func__, pdata->code, state?"松开":"按下");
}
static int g_data = 0x5555;
//定义初始化一个tasklet对象
static DECLARE_TASKLET(btn_tasklet, //定义对象
                    btn_tasket_function,//指定延后处理函数
                    (unsigned long)&g_data);//传参
//顶半部:中断处理函数
//一旦中断到来,先执行顶半部
//不允许CPU资源切换
static irqreturn_t button_isr(int irq, void *dev)
{
    //1.获取对应的当前触发的硬件中断的硬件信息
    pdata = (struct btn_resource *)dev;
    //2.向内核登记底半部tasklet延后处理函数,一旦登记
    //完成,内核将来在适当的时候调用延后处理函数
    tasklet_schedule(&btn_tasklet);
    //3.验证顶半部的执行先于延后处理函数
    printk("顶半部:%s\n", __func__);
    return IRQ_HANDLED; //执行成功
}
static int btn_init(void)
{
    int i;
    //1.申请GPIO资源
    //配置为输入功能由内核已经帮你实现
    //2.申请中断资源,注册中断处理函数
    //注意:多个按键共享一个中断处理函数
    for (i = 0; i < ARRAY_SIZE(btn_info); i++) {
        //由GPIO编号求中断号
        int irq = gpio_to_irq(btn_info[i].gpio);
        gpio_request(btn_info[i].gpio, 
                        btn_info[i].name);
        request_irq(irq,  //中断号
                button_isr,//注册的中断处理函数
            IRQF_TRIGGER_FALLING|IRQF_TRIGGER_RISING,
                btn_info[i].name,//中断名称
                &btn_info[i] //将每个按键对应的硬件信息传                                递给中断处理函数 
                );
    }
    return 0;
}
static void btn_exit(void)
{
    int i;
    //1.释放中断资源删除中断处理函数
    //2.释放GPIO资源
    for(i = 0; i < ARRAY_SIZE(btn_info); i++) {\
        int irq = gpio_to_irq(btn_info[i].gpio);
        gpio_free(btn_info[i].gpio);
        free_irq(irq, &btn_info[i]);
    }
}
module_init(btn_init);
module_exit(btn_exit);
MODULE_LICENSE("GPL");


  • Makefile
obj-m += btn_drv.o
all:
  make -C /opt/kernel SUBDIRS=$(PWD) modules
clean:
  make -C /opt/kernel SUBDIRS=$(PWD) clean


  • 执行实现:

2019122922464946.png

底半部实现机制之工作队列

工作队列特点: 工作队列诞生的本质就是解决tasklet的延后处理函数不能进行休眠操作的问题,因为在某些场合需要在延后的处理流程中进行休眠操作,对于这样的情况必须使用工作队列,所以工作队列是基于进程实现的,所以自然工作队列的延后处理函数是可以进行休眠操作,但是它的优先级低于软中断,低于硬件中断,工作队列延后处理函数中依旧还是去处理中断处理遗留下来的耗时较长或者不紧急的内容。


  • Linux内核描述工作队列的数据结构:
struct work_struct {
  work_func_t func;
  ...
};


成员说明:


func:工作队列的延后处理函数,里面可以进行休眠操作。
配套函数:
//给工作队列添加一个延后处理函数
INIT_WORK(&工作队列对象, 工作队列的延后处理函数);
//向内核登记工作队列的延后处理函数,一旦登记完成
//将来内核在适当的时候调用其延后处理函数
schedule_work(&工作队列对象);


利用工作队列实现延后执行的编程步骤:


  1. 定义初始化工作队列对象。
struct work_struct btn_work; //定义对象
INIT_WORK(&btn_work, btn_work_function);

  1. 编写工作队列延后处理,此函数可以进行休眠操作。
//work指针就是指向自己定义初始化的对象
//work=&btn_work
void btn_work_function(struct work_struct *work)
{
  //不紧急耗时较长的内容
}

  1. 向内核登记工作队列的延后处理函数,一旦登记完成内核在适当的时候调用延后处理函数。
schedule_work(&btn_work);

案例实现(使用工作队列在按键后处理执行打印信息)

  • btn_drv.c
#include <linux/init.h>
#include <linux/module.h>
#include <linux/irq.h>
#include <linux/interrupt.h>
#include <linux/gpio.h>
#include <mach/platform.h>
#include <linux/input.h> //标准按键值:KEY_*
//声明描述按键信息相关的数据结构
struct btn_resource {
    int gpio; //按键对应的GPIO编号
    char *name; //按键名称
    int code; //按键值
};
//定义初始化按键硬件信息对象
static struct btn_resource btn_info[] = {
    {
        .gpio = PAD_GPIO_A+28,
        .name = "KEY_UP",
        .code = KEY_UP
    },
    {
        .gpio = PAD_GPIO_B+9,
        .name = "KEY_DOWN",
        .code = KEY_DOWN
    },
};
//记录按键的硬件信息
static struct btn_resource *pdata;
//工作队列延后处理函数
//适当的时候被内核调用
//基于进程实现
//能进行休眠操作
//work=&btn_work
static void btn_work_function(struct work_struct *work)
{
    int state;
    //1.获取按键对应GPIO的电平状态,获取按键的操作状态
    state = gpio_get_value(pdata->gpio);
    //2.打印按键信息
    printk("底半部:%s:按键值[%d]按键状态[%s]\n",
            __func__, pdata->code, state?"松开":"按下");
}
//定义一个工作队列对象
static struct work_struct btn_work; 
//顶半部:中断处理函数
//一旦中断到来,先执行顶半部
//不允许CPU资源切换
static irqreturn_t button_isr(int irq, void *dev)
{
    //1.获取对应的当前触发的硬件中断的硬件信息
    pdata = (struct btn_resource *)dev;
    //2.向内核登记底半部延后处理函数,一旦登记
    //完成,内核将来在适当的时候调用延后处理函数
    schedule_work(&btn_work);
    //3.验证顶半部的执行先于延后处理函数
    printk("顶半部:%s\n", __func__);
    return IRQ_HANDLED; //执行成功
}
static int btn_init(void)
{
    int i;
    //1.申请GPIO资源
    //配置为输入功能由内核已经帮你实现
    //2.申请中断资源,注册中断处理函数
    //注意:多个按键共享一个中断处理函数
    for (i = 0; i < ARRAY_SIZE(btn_info); i++) {
        //由GPIO编号求中断号
        int irq = gpio_to_irq(btn_info[i].gpio);
        gpio_request(btn_info[i].gpio, 
                        btn_info[i].name);
        request_irq(irq,  //中断号
                button_isr,//注册的中断处理函数
            IRQF_TRIGGER_FALLING|IRQF_TRIGGER_RISING,
                btn_info[i].name,//中断名称
                &btn_info[i] //将每个按键对应的硬件信息传                                递给中断处理函数 
                );
    }
    //2.初始化工作队列指定延后处理函数
    INIT_WORK(&btn_work, btn_work_function);
    return 0;
}
static void btn_exit(void)
{
    int i;
    //1.释放中断资源删除中断处理函数
    //2.释放GPIO资源
    for(i = 0; i < ARRAY_SIZE(btn_info); i++) {\
        int irq = gpio_to_irq(btn_info[i].gpio);
        gpio_free(btn_info[i].gpio);
        free_irq(irq, &btn_info[i]);
    }
}
module_init(btn_init);
module_exit(btn_exit);
MODULE_LICENSE("GPL");


  • Makefile
obj-m += btn_drv.o
all:
  make -C /opt/kernel SUBDIRS=$(PWD) modules
clean:
  make -C /opt/kernel SUBDIRS=$(PWD) clean


  • 执行实现

20191229225245934.png底半部实现机制之软中断

软中断特点: tasklet本身是基于软中断,软中断的延后处理函数同样不能进行休眠操作,软中断的延后处理函数可以同时运行在多个CPU核上,而tasklet的延后处理函数同一时刻只能运行在一个CPU核上,所以软中断的延后处理函数在设计的时候务必考虑可重入性,软中断编程实现上不能以insmod/rmmod形式动态的安装和写在,必须和uImage编译在一起,这样软件设计相对比较繁琐,所以就有了tasklet的产生。tasklet本质就是解决软中断设计的繁琐问题(软中断的替代)。


  • 基于以上特点,所以这里就暂时不详细描述软中断的底半部实现了。

总结:

  • 如果延后执行的内容中,没有休眠操作,用tasklet或者工作队列都可以
  • 如果延后执行的内容中,有休眠操作,只能用工作队列
  • 如果延后执行的内容中,没有休眠操作,并且考虑效率问题,使用tasklet



相关文章
|
3月前
|
缓存 Linux 开发者
Linux内核中的并发控制机制
本文深入探讨了Linux操作系统中用于管理多线程和进程的并发控制的关键技术,包括原子操作、锁机制、自旋锁、互斥量以及信号量。通过详细分析这些技术的原理和应用,旨在为读者提供一个关于如何有效利用Linux内核提供的并发控制工具以优化系统性能和稳定性的综合视角。
|
6天前
|
Linux
Linux编程: 在业务线程中注册和处理Linux信号
本文详细介绍了如何在Linux中通过在业务线程中注册和处理信号。我们讨论了信号的基本概念,并通过完整的代码示例展示了在业务线程中注册和处理信号的方法。通过正确地使用信号处理机制,可以提高程序的健壮性和响应能力。希望本文能帮助您更好地理解和应用Linux信号处理,提高开发效率和代码质量。
38 17
|
15天前
|
Linux
Linux编程: 在业务线程中注册和处理Linux信号
通过本文,您可以了解如何在业务线程中注册和处理Linux信号。正确处理信号可以提高程序的健壮性和稳定性。希望这些内容能帮助您更好地理解和应用Linux信号处理机制。
50 26
|
2月前
|
存储 编译器 Linux
动态链接的魔法:Linux下动态链接库机制探讨
本文将深入探讨Linux系统中的动态链接库机制,这其中包括但不限于全局符号介入、延迟绑定以及地址无关代码等内容。
835 25
|
2月前
|
存储 监控 Linux
嵌入式Linux系统编程 — 5.3 times、clock函数获取进程时间
在嵌入式Linux系统编程中,`times`和 `clock`函数是获取进程时间的两个重要工具。`times`函数提供了更详细的进程和子进程时间信息,而 `clock`函数则提供了更简单的处理器时间获取方法。根据具体需求选择合适的函数,可以更有效地进行性能分析和资源管理。通过本文的介绍,希望能帮助您更好地理解和使用这两个函数,提高嵌入式系统编程的效率和效果。
121 13
|
2月前
|
监控 算法 Linux
Linux内核锁机制深度剖析与实践优化####
本文作为一篇技术性文章,深入探讨了Linux操作系统内核中锁机制的工作原理、类型及其在并发控制中的应用,旨在为开发者提供关于如何有效利用这些工具来提升系统性能和稳定性的见解。不同于常规摘要的概述性质,本文将直接通过具体案例分析,展示在不同场景下选择合适的锁策略对于解决竞争条件、死锁问题的重要性,以及如何根据实际需求调整锁的粒度以达到最佳效果,为读者呈现一份实用性强的实践指南。 ####
|
2月前
|
消息中间件 安全 Linux
深入探索Linux操作系统的内核机制
本文旨在为读者提供一个关于Linux操作系统内核机制的全面解析。通过探讨Linux内核的设计哲学、核心组件、以及其如何高效地管理硬件资源和系统操作,本文揭示了Linux之所以成为众多开发者和组织首选操作系统的原因。不同于常规摘要,此处我们不涉及具体代码或技术细节,而是从宏观的角度审视Linux内核的架构和功能,为对Linux感兴趣的读者提供一个高层次的理解框架。
|
3月前
|
算法 Linux 开发者
Linux内核中的锁机制:保障并发控制的艺术####
本文深入探讨了Linux操作系统内核中实现的多种锁机制,包括自旋锁、互斥锁、读写锁等,旨在揭示这些同步原语如何高效地解决资源竞争问题,保证系统的稳定性和性能。通过分析不同锁机制的工作原理及应用场景,本文为开发者提供了在高并发环境下进行有效并发控制的实用指南。 ####
|
3月前
|
缓存 Linux 开发者
Linux内核中的并发控制机制:深入理解与应用####
【10月更文挑战第21天】 本文旨在为读者提供一个全面的指南,探讨Linux操作系统中用于实现多线程和进程间同步的关键技术——并发控制机制。通过剖析互斥锁、自旋锁、读写锁等核心概念及其在实际场景中的应用,本文将帮助开发者更好地理解和运用这些工具来构建高效且稳定的应用程序。 ####
62 5
|
3月前
|
Linux 数据库
Linux内核中的锁机制:保障并发操作的数据一致性####
【10月更文挑战第29天】 在多线程编程中,确保数据一致性和防止竞争条件是至关重要的。本文将深入探讨Linux操作系统中实现的几种关键锁机制,包括自旋锁、互斥锁和读写锁等。通过分析这些锁的设计原理和使用场景,帮助读者理解如何在实际应用中选择合适的锁机制以优化系统性能和稳定性。 ####
82 6