大山佬头像
关注

Linux GPIO 子系统中断嵌套处理详解:从 irq_chip 到 threaded irq 的优先级翻转案例分析

Linux GPIO 子系统中断嵌套处理详解:从 irq_chip 到 threaded irq 的优先级翻转案例分析

一、GPIO 中断在嵌入式 Linux 中的架构层次

嵌入式 Linux 的 GPIO 中断处理涉及四个层次:(1) GPIO Controller 硬件(如 GPIO 引脚电平变化触发)-> (2) irq_chip 驱动层(GPIO 控制器注册的中断控制器接口)-> (3) 通用中断子系统(kernel/irq/ 核心代码)-> (4) 设备驱动(request_threaded_irq 注册的 handler)。理解这四层的数据流对于排查优先级翻转、中断丢失等问题至关重要。

二、irq_chip 驱动实现与关键接口

GPIO 控制器的中断能力通过 struct irq_chip 向内核注册。以下以一款典型的 I2C GPIO 扩展芯片(如 PCA953x 系列)为例,展示 irq_chip 的核心实现:

/**
 * PCA953x GPIO 扩展器 irq_chip 实现
 * 该芯片通过 I2C 总线连接,提供 8/16 路 GPIO 及中断聚合
 */
#include <linux/gpio/driver.h>
#include <linux/interrupt.h>
#include <linux/i2c.h>
#include <linux/irq.h>
#include <linux/irqchip/chained_irq.h>

struct pca953x_chip {
    struct gpio_chip gpio;
    struct i2c_client *client;
    struct mutex lock;          /* I2C 寄存器访问互斥锁 */
    uint8_t irq_mask;          /* 本地中断屏蔽缓存 */
    uint8_t irq_stat;          /* 本地中断状态缓存 */
    int irq_parent;            /* 父中断号(SoC GPIO) */
};

/**
 * irq_bus_lock - I2C 寄存器访问加锁
 * 在中断流开始前获取 I2C 总线锁,防止读-改-写竞争
 */
static void pca953x_irq_bus_lock(struct irq_data *data) {
    struct gpio_chip *gc = irq_data_get_irq_chip_data(data);
    struct pca953x_chip *chip = gpiochip_get_data(gc);
    
    mutex_lock(&chip->lock);
}

/**
 * irq_bus_sync_unlock - 批量刷新寄存器并解锁
 * 在中断流结束后将缓存的屏蔽/触发状态写入硬件
 */
static void pca953x_irq_bus_sync_unlock(struct irq_data *data) {
    struct gpio_chip *gc = irq_data_get_irq_chip_data(data);
    struct pca953x_chip *chip = gpiochip_get_data(gc);
    int ret;
    
    /* 将本地缓存的irq_mask写入芯片寄存器 */
    ret = i2c_smbus_write_byte_data(chip->client, 
                                    0x02, /* 中断屏蔽寄存器地址 */
                                    chip->irq_mask);
    if (ret < 0) {
        dev_err(&chip->client->dev,
                "[错误] I2C 写入中断屏蔽寄存器失败: %d\n", ret);
    }
    
    mutex_unlock(&chip->lock);
}

/**
 * irq_mask - 屏蔽指定 GPIO 中断
 * 在本地缓存中标记,由 irq_bus_sync_unlock 统一写入硬件
 */
static void pca953x_irq_mask(struct irq_data *data) {
    struct gpio_chip *gc = irq_data_get_irq_chip_data(data);
    struct pca953x_chip *chip = gpiochip_get_data(gc);
    unsigned int offset = irqd_to_hwirq(data);
    
    if (offset >= chip->gpio.ngpio) {
        dev_err(&chip->client->dev,
                "[错误] GPIO偏移%u超出范围(最大%u)\n",
                offset, chip->gpio.ngpio - 1);
        return;
    }
    
    chip->irq_mask |= BIT(offset);  /* 对应位置1表示屏蔽 */
}

/**
 * irq_ack - 确认中断(清除硬件中断状态)
 * 必须在中断处理的最开始调用, 防止中断重入
 */
static void pca953x_irq_ack(struct irq_data *data) {
    struct gpio_chip *gc = irq_data_get_irq_chip_data(data);
    struct pca953x_chip *chip = gpiochip_get_data(gc);
    unsigned int offset = irqd_to_hwirq(data);
    
    if (offset >= chip->gpio.ngpio) {
        return;
    }
    
    /* 清除对应位的中断状态 */
    chip->irq_stat &= ~BIT(offset);
}

static struct irq_chip pca953x_irq_chip = {
    .name            = "pca953x",
    .irq_mask        = pca953x_irq_mask,
    .irq_unmask      = pca953x_irq_unmask,
    .irq_ack         = pca953x_irq_ack,
    .irq_bus_lock    = pca953x_irq_bus_lock,
    .irq_bus_sync_unlock = pca953x_irq_bus_sync_unlock,
};

三、threaded irq 与优先级翻转案例分析

request_threaded_irq 是嵌入式 Linux 中广泛使用的中断注册机制。它将中断处理分为两部分:(1) top-half(硬中断上下文),执行时间敏感的操作(ack、屏蔽、唤醒线程);(2) threaded handler(内核线程上下文),执行耗时操作(I2C 读取、数据处理)。

优先级翻转案例

在某安防设备的 Linux BSP 中,出现以下配置:

  • GPIO 中断 A(运动检测,request_threaded_irq 注册,top-half 耗时 ~5μs,threaded handler 耗时 ~2ms)优先级为 IRQF_ONESHOT
  • GPIO 中断 B(按键检测,request_irq 注册,仅 top-half,耗时 ~10μs)
  • 运动检测线程优先级:SCHED_FIFO priority 50
  • 按键中断优先级:硬件 IRQ 优先级相同(同为 GPIO 控制器中断线)

问题现场:按键中断触发时,若恰好运动检测的 threaded handler 持有 chip->lock 互斥锁,按键中断的 top-half 在 irq_bus_lock 中阻塞。虽然硬件中断优先级相同,但 threaded handler 优先级 50 > 按键线程优先级(默认 120),导致按键中断处理被推迟 2ms+。

/**
 * 错误的驱动实现 - 存在优先级翻转隐患
 * threaded handler 持有 I2C 锁时间过长,阻塞其他中断的 bus_lock
 */
static irqreturn_t motion_threaded_handler(int irq, void *dev_id) {
    struct pca953x_chip *chip = (struct pca953x_chip *)dev_id;
    
    mutex_lock(&chip->lock);  /* 持有锁期间执行I2C操作 */
    
    /* I2C 读取传感器数据 - 可能耗时 1-2ms */
    uint8_t buf[64];
    int ret = i2c_master_recv(chip->client, buf, sizeof(buf));
    if (ret < 0) {
        dev_err(&chip->client->dev,
                "[错误] 运动传感器I2C读取失败: %d\n", ret);
        mutex_unlock(&chip->lock);
        return IRQ_HANDLED;
    }
    
    /* 数据处理 - 额外耗时 */
    process_motion_data(buf, ret);
    
    mutex_unlock(&chip->lock);  /* 2ms后才释放 */
    return IRQ_HANDLED;
}

解决策略

/**
 * 修复后的实现 - 使用分离锁避免优先级翻转
 * bus_lock 仅用于寄存器访问同步, 数据锁独立管理
 */
static DEFINE_MUTEX(data_lock);    /* 传感器数据保护锁 */
static DEFINE_MUTEX(bus_lock);     /* I2C 总线访问锁 */

static irqreturn_t motion_threaded_handler_fixed(int irq, void *dev_id) {
    struct pca953x_chip *chip = (struct pca953x_chip *)dev_id;
    uint8_t buf[64];
    int ret;
    
    /* 第一步: 短暂持有bus_lock做I2C读取 */
    mutex_lock(&bus_lock);
    ret = i2c_master_recv(chip->client, buf, sizeof(buf));
    mutex_unlock(&bus_lock);  /* 立即释放, 不阻塞其他中断 */
    
    if (ret < 0) {
        dev_err(&chip->client->dev,
                "[错误] 运动传感器I2C读取失败: %d\n", ret);
        return IRQ_HANDLED;
    }
    
    /* 第二步: 在独立锁保护下处理数据 */
    mutex_lock(&data_lock);
    process_motion_data(buf, ret);
    mutex_unlock(&data_lock);
    
    return IRQ_HANDLED;
}

四、嵌套中断处理与中断风暴防护

GPIO 控制器在电平触发模式下容易出现中断风暴:外设拉低电平 -> 内核处理中断 -> top-half 中 ack + mask -> threaded handler 处理 -> unmask -> 电平仍为低 -> 立即再次触发中断。若不加以控制,可能导致 CPU 100% 时间消耗在中断处理上。

防护机制实现

/**
 * 中断风暴检测与自动降级
 * 利用内核 irq_desc 中的 irq_count 计数器
 * 当指定时间窗口内中断次数超过阈值时自动切换为轮询模式
 */
#include <linux/irqdesc.h>
#include <linux/kthread.h>

#define STORM_THRESHOLD    1000   /* 1秒内中断次数阈值 */
#define STORM_WINDOW_MS    1000   /* 检测窗口1000ms */

static int gpio_storm_detect(struct irq_desc *desc) {
    static unsigned long last_check_jiffies = 0;
    static unsigned int last_irq_count = 0;
    
    /* 每1000ms检测一次 */
    if (time_before(jiffies, last_check_jiffies + msecs_to_jiffies(STORM_WINDOW_MS))) {
        return 0;  /* 未到检测周期 */
    }
    
    unsigned int current_count = desc->irq_count;
    unsigned int delta = current_count - last_irq_count;
    
    last_irq_count = current_count;
    last_check_jiffies = jiffies;
    
    if (delta > STORM_THRESHOLD) {
        pr_warn("[警告] GPIO中断%d疑似风暴: %u次/%ums\n",
                desc->irq_data.irq, delta, STORM_WINDOW_MS);
        return 1;  /* 检测到风暴 */
    }
    
    return 0;
}

/**
 * 中断风暴时的降级处理
 * 禁用中断 -> 切换为50ms定时轮询 -> 风暴结束后恢复中断模式
 */
static irqreturn_t gpio_storm_handler(int irq, void *dev_id) {
    struct irq_desc *desc = irq_to_desc(irq);
    
    if (gpio_storm_detect(desc)) {
        /* 进入风暴模式: 屏蔽中断,启用轮询 */
        disable_irq_nosync(irq);
        schedule_delayed_work(&gpio_poll_work, 
                              msecs_to_jiffies(50));
        return IRQ_HANDLED;
    }
    
    /* 正常中断处理 */
    return IRQ_WAKE_THREAD;
}

五、总结

Linux GPIO 中断子系统的深度设计关键点:(1) irq_chipbus_lock/bus_sync_unlock 机制为慢速总线(I2C/SPI)上的 GPIO 扩展器提供了批量寄存器操作能力,但需注意锁持有时间;(2) threaded irq 将耗时操作从硬中断上下文剥离,但线程优先级不当会导致实质上的中断延迟——按键案例中延迟达到 2ms+;(3) 中断风暴防护必须在驱动层实现,内核提供 irq_count 计数器作为检测基础。实践中"分离锁 + 中断次数监控 + 自动降级为轮询"的三层防护体系,可以在保证响应性的同时避免中断风暴导致系统瘫痪。

转载自 CSDN-专业IT技术社区

原文链接:https://blog.csdn.net/qq_42431428/article/details/163219285

文章来源crawl

评论

赞0

评论列表

微信小程序
QQ小程序

关于作者

点赞数:0
关注数:0
粉丝:0
文章:0
关注标签:0
加入于:--