《Linux 设备驱动开发详解:基于最新的 Linux 4.0 内核》
第 10 章 Linux 设备驱动中的中断与时钟
参考:宋宝华 著,机械工业出版社,2015年版
10.1 中断与定时器
10.1.1 中断的概念
**中断(Interrupt)**是处理器响应硬件事件的机制。当外设需要 CPU 处理时,向 CPU 发送中断信号,CPU 暂停当前任务,跳转到中断处理程序(ISR,Interrupt Service Routine)执行,完成后恢复原来的任务。
中断处理流程:
CPU 正常执行代码
↓ 硬件产生中断信号(如串口收到数据)
CPU 完成当前指令
↓
保存当前 CPU 状态(寄存器、PC 等)到栈
↓
跳转到中断向量表,找到对应的中断处理函数
↓
执行中断处理函数(ISR)
↓
恢复 CPU 状态
↓
继续执行被中断的代码
10.1.2 中断的分类
Linux 中断分类:
按来源分:
硬件中断(Hardware Interrupt)
├── 外部中断(External Interrupt):来自外设(GPIO、UART、USB 等)
└── 内部中断(Internal Interrupt):来自 CPU 内部(定时器、DMA 等)
软件中断(Software Interrupt)
├── 系统调用(syscall / SWI):用户空间进入内核的入口
└── 软中断(softirq):内核内部的软件中断机制
按是否可屏蔽分:
可屏蔽中断(Maskable Interrupt,IRQ):可以通过 CPSR 的 I 位禁止
不可屏蔽中断(Non-Maskable Interrupt,NMI):不能被屏蔽,用于严重错误
按触发方式分:
电平触发(Level Triggered):高/低电平期间持续触发
边沿触发(Edge Triggered):上升沿/下降沿触发一次
10.1.3 中断号(IRQ Number)
Linux 使用虚拟中断号(IRQ Number)来标识中断,与硬件中断号通过中断控制器(如 GIC)映射:
bash
# 查看系统中断信息
cat /proc/interrupts
# CPU0 CPU1
# 0: 12345 0 GIC 27 arch_timer
# 16: 0 0 GIC 16 IPI: Rescheduling interrupts
# 17: 0 0 GIC 17 IPI: Function call interrupts
# 32: 1234 0 GIC 32 uart-pl011
# 33: 0 0 GIC 33 mmc0
# ...
# 列:中断号 | CPU0计数 | CPU1计数 | 中断控制器 | 硬件中断号 | 设备名称
# 查看中断控制器信息
cat /proc/irq/32/chip_name
# GIC
# 查看中断亲和性(哪个 CPU 处理)
cat /proc/irq/32/smp_affinity
# 3 ← 二进制 11,表示 CPU0 和 CPU1 都可以处理
10.1.4 定时器的概念
**定时器(Timer)**是一种特殊的中断源,按照设定的时间间隔周期性地产生中断,用于:
-
系统时钟(jiffies 计数)
-
进程调度(时间片轮转)
-
驱动中的超时检测
-
周期性任务执行
Linux 定时器体系:
硬件定时器(Hardware Timer)
↓ 产生时钟中断(HZ 次/秒)
时钟中断处理函数(timer_interrupt)
↓
更新 jiffies(系统时钟节拍计数)
↓
检查并触发到期的软件定时器
↓
进程调度(时间片检查)HZ:每秒时钟中断次数(通常 100、250 或 1000)
jiffies:系统启动以来的时钟节拍总数
10.2 Linux 中断编程
10.2.1 申请中断
c
#include <linux/interrupt.h>
/*
* request_irq:申请中断
*
* irq:中断号(虚拟 IRQ 号)
* handler:中断处理函数
* flags:中断标志
* name:中断名称(显示在 /proc/interrupts 中)
* dev:传递给中断处理函数的参数(通常为设备结构体指针)
*
* 返回:0(成功),负值(失败)
*/
int request_irq(unsigned int irq,
irq_handler_t handler,
unsigned long flags,
const char *name,
void *dev);
/* 常用中断标志(flags)*/
IRQF_SHARED /* 共享中断:多个设备共用同一中断号 */
IRQF_TRIGGER_RISING /* 上升沿触发 */
IRQF_TRIGGER_FALLING /* 下降沿触发 */
IRQF_TRIGGER_HIGH /* 高电平触发 */
IRQF_TRIGGER_LOW /* 低电平触发 */
IRQF_ONESHOT /* 中断处理完成前禁止该中断(用于线程化中断)*/
IRQF_NO_THREAD /* 禁止中断线程化 */
/*
* free_irq:释放中断
* dev 必须与 request_irq 时传入的 dev 相同
*/
void free_irq(unsigned int irq, void *dev);
10.2.2 中断处理函数
c
/*
* 中断处理函数的标准形式
*
* irq:中断号
* dev_id:request_irq 时传入的 dev 参数
*
* 返回值:
* IRQ_HANDLED:中断已被本处理函数处理
* IRQ_NONE:中断不是本设备产生的(用于共享中断)
* IRQ_WAKE_THREAD:唤醒中断线程(用于线程化中断)
*/
static irqreturn_t my_irq_handler(int irq, void *dev_id)
{
struct my_dev *dev = dev_id;
/*
* 中断上下文的限制:
* ✗ 不能睡眠(不能调用 msleep、wait_event 等)
* ✗ 不能调用可能睡眠的函数(kmalloc(GFP_KERNEL)、mutex_lock 等)
* ✗ 不能访问用户空间内存(copy_to/from_user)
* ✓ 可以使用自旋锁(spin_lock)
* ✓ 可以使用 kmalloc(GFP_ATOMIC)
* ✓ 可以调度 tasklet 或工作队列处理耗时操作
*/
/* 1. 读取中断状态寄存器,确认是本设备的中断 */
u32 status = readl(dev->base + INT_STATUS_REG);
if (!(status & MY_INT_FLAG))
return IRQ_NONE; /* 不是本设备的中断(共享中断场景)*/
/* 2. 清除中断标志(防止重复触发)*/
writel(MY_INT_FLAG, dev->base + INT_STATUS_REG);
/* 3. 快速处理(中断上下文,必须尽快完成)*/
dev->irq_count++;
dev->last_data = readl(dev->base + DATA_REG);
/* 4. 耗时操作交给底半部处理 */
tasklet_schedule(&dev->tasklet); /* 调度 tasklet */
/* 或 */
schedule_work(&dev->work); /* 调度工作队列 */
return IRQ_HANDLED;
}
10.2.3 完整的中断驱动示例
c
/*
* irq_driver.c ------ 完整的中断驱动示例
* 以 GPIO 按键中断为例
*/
#include <linux/module.h>
#include <linux/interrupt.h>
#include <linux/gpio.h>
#include <linux/platform_device.h>
#include <linux/of_gpio.h>
#include <linux/of_irq.h>
struct button_dev {
int gpio; /* GPIO 编号 */
int irq; /* 中断号 */
int press_count; /* 按键次数 */
spinlock_t lock; /* 保护 press_count */
};
/* 中断处理函数 */
static irqreturn_t button_irq_handler(int irq, void *dev_id)
{
struct button_dev *dev = dev_id;
unsigned long flags;
int gpio_val;
/* 读取 GPIO 电平(判断是按下还是释放)*/
gpio_val = gpio_get_value(dev->gpio);
spin_lock_irqsave(&dev->lock, flags);
if (gpio_val == 0) {
/* 低电平:按键按下 */
dev->press_count++;
pr_info("button: 按键按下,总次数 = %d\n", dev->press_count);
} else {
/* 高电平:按键释放 */
pr_info("button: 按键释放\n");
}
spin_unlock_irqrestore(&dev->lock, flags);
return IRQ_HANDLED;
}
static int button_probe(struct platform_device *pdev)
{
struct button_dev *dev;
int ret;
/* 分配设备结构体 */
dev = devm_kzalloc(&pdev->dev, sizeof(*dev), GFP_KERNEL);
if (!dev)
return -ENOMEM;
spin_lock_init(&dev->lock);
/* 从设备树获取 GPIO */
dev->gpio = of_get_named_gpio(pdev->dev.of_node, "button-gpio", 0);
if (!gpio_is_valid(dev->gpio)) {
dev_err(&pdev->dev, "无效的 GPIO\n");
return -EINVAL;
}
/* 申请 GPIO */
ret = devm_gpio_request_one(&pdev->dev, dev->gpio,
GPIOF_IN, "button");
if (ret) {
dev_err(&pdev->dev, "申请 GPIO 失败\n");
return ret;
}
/* 获取中断号(从 GPIO 转换)*/
dev->irq = gpio_to_irq(dev->gpio);
if (dev->irq < 0) {
dev_err(&pdev->dev, "获取中断号失败\n");
return dev->irq;
}
/* 申请中断(双边沿触发,检测按下和释放)*/
ret = devm_request_irq(&pdev->dev, dev->irq,
button_irq_handler,
IRQF_TRIGGER_RISING | IRQF_TRIGGER_FALLING,
"button_irq", dev);
if (ret) {
dev_err(&pdev->dev, "申请中断失败\n");
return ret;
}
platform_set_drvdata(pdev, dev);
dev_info(&pdev->dev, "按键驱动加载,GPIO=%d, IRQ=%d\n",
dev->gpio, dev->irq);
return 0;
}
static int button_remove(struct platform_device *pdev)
{
/* devm_ 申请的资源自动释放 */
dev_info(&pdev->dev, "按键驱动卸载\n");
return 0;
}
static const struct of_device_id button_of_match[] = {
{ .compatible = "myvendor,gpio-button" },
{}
};
MODULE_DEVICE_TABLE(of, button_of_match);
static struct platform_driver button_driver = {
.probe = button_probe,
.remove = button_remove,
.driver = {
.name = "gpio-button",
.of_match_table = button_of_match,
},
};
module_platform_driver(button_driver);
MODULE_LICENSE("GPL v2");
10.2.4 中断的使能与禁止
c
/* ── 禁止/使能特定中断 ──────────────────────────────────── */
disable_irq(irq); /* 禁止指定中断(等待当前处理完成)*/
disable_irq_nosync(irq); /* 禁止指定中断(不等待,立即返回)*/
enable_irq(irq); /* 使能指定中断 */
/* ── 禁止/使能本地 CPU 所有中断 ─────────────────────────── */
local_irq_disable(); /* 禁止本地 CPU 中断 */
local_irq_enable(); /* 使能本地 CPU 中断 */
unsigned long flags;
local_irq_save(flags); /* 保存中断状态并禁止 */
local_irq_restore(flags); /* 恢复中断状态 */
/* ── 判断当前是否在中断上下文 ───────────────────────────── */
in_interrupt() /* 在中断上下文(硬中断或软中断)中返回非0 */
in_irq() /* 在硬中断上下文中返回非0 */
in_softirq() /* 在软中断上下文中返回非0 */
in_atomic() /* 在原子上下文(不能睡眠)中返回非0 */
10.2.5 共享中断
多个设备共用同一中断号时,需要使用共享中断:
c
/* 共享中断的申请 */
request_irq(irq, handler_a, IRQF_SHARED, "device_a", dev_a);
request_irq(irq, handler_b, IRQF_SHARED, "device_b", dev_b);
/*
* 共享中断处理函数必须:
* 1. 检查是否是本设备产生的中断
* 2. 如果不是,返回 IRQ_NONE
* 3. 如果是,处理后返回 IRQ_HANDLED
*/
static irqreturn_t handler_a(int irq, void *dev_id)
{
struct device_a *dev = dev_id;
/* 检查是否是设备A的中断 */
if (!(readl(dev->base + STATUS) & DEV_A_INT_FLAG))
return IRQ_NONE; /* 不是设备A的中断,让其他处理函数处理 */
/* 处理设备A的中断 */
/* ... */
return IRQ_HANDLED;
}
10.3 中断底半部机制
10.3.1 顶半部与底半部的概念
中断处理分为顶半部(Top Half)和底半部(Bottom Half):
顶半部(Top Half):
- 在中断上下文中执行
- 必须尽快完成(通常 < 几十微秒)
- 只做最紧急的工作:读取状态、清除中断标志、保存数据
- 不能睡眠
底半部(Bottom Half):
- 在进程上下文或软中断上下文中执行
- 可以做耗时操作
- 可以睡眠(工作队列)
- 处理顶半部推迟的工作
为什么需要底半部?
中断处理时,CPU 的中断被禁止(或同级中断被禁止)
如果中断处理函数执行时间过长,会导致:
- 其他中断无法及时响应
- 系统实时性下降
- 用户体验变差
解决方案:顶半部快速处理,底半部延迟处理
10.3.2 tasklet
tasklet 是最简单的底半部机制,运行在软中断上下文中,不能睡眠:
c
#include <linux/interrupt.h>
/* ── 定义 tasklet ──────────────────────────────────────── */
/* 方法一:静态定义 */
static void my_tasklet_func(unsigned long data);
DECLARE_TASKLET(my_tasklet, my_tasklet_func, 0);
/* 或禁用状态的 tasklet */
DECLARE_TASKLET_DISABLED(my_tasklet, my_tasklet_func, 0);
/* 方法二:动态初始化 */
struct tasklet_struct my_tasklet;
tasklet_init(&my_tasklet, my_tasklet_func, (unsigned long)dev);
/* ── tasklet 处理函数 ──────────────────────────────────── */
static void my_tasklet_func(unsigned long data)
{
struct my_dev *dev = (struct my_dev *)data;
/*
* tasklet 运行在软中断上下文:
* ✗ 不能睡眠
* ✓ 可以使用自旋锁
* ✓ 可以访问设备寄存器
*/
pr_info("tasklet 执行,处理中断数据\n");
process_irq_data(dev);
}
/* ── 在中断处理函数中调度 tasklet ──────────────────────── */
static irqreturn_t my_irq_handler(int irq, void *dev_id)
{
struct my_dev *dev = dev_id;
/* 顶半部:快速处理 */
dev->irq_data = readl(dev->base + DATA_REG);
writel(INT_CLEAR, dev->base + INT_STATUS_REG);
/* 调度 tasklet(底半部)*/
tasklet_schedule(&my_tasklet);
return IRQ_HANDLED;
}
/* ── tasklet 控制 ──────────────────────────────────────── */
tasklet_schedule(&my_tasklet); /* 调度 tasklet(如果未调度)*/
tasklet_hi_schedule(&my_tasklet); /* 高优先级调度 */
tasklet_disable(&my_tasklet); /* 禁用 tasklet */
tasklet_enable(&my_tasklet); /* 使能 tasklet */
tasklet_kill(&my_tasklet); /* 等待 tasklet 完成并禁用 */
tasklet 的完整使用案例:
c
struct uart_dev {
void __iomem *base;
int irq;
struct tasklet_struct rx_tasklet;
unsigned char rx_buf[256];
int rx_len;
spinlock_t lock;
};
/* tasklet 处理函数:处理接收到的数据 */
static void uart_rx_tasklet(unsigned long data)
{
struct uart_dev *dev = (struct uart_dev *)data;
unsigned long flags;
unsigned char buf[256];
int len;
spin_lock_irqsave(&dev->lock, flags);
memcpy(buf, dev->rx_buf, dev->rx_len);
len = dev->rx_len;
dev->rx_len = 0;
spin_unlock_irqrestore(&dev->lock, flags);
/* 处理接收到的数据(可以做较复杂的操作)*/
pr_info("uart: 处理 %d 字节数据\n", len);
/* parse_protocol(buf, len); */
}
/* 中断处理函数(顶半部)*/
static irqreturn_t uart_irq_handler(int irq, void *dev_id)
{
struct uart_dev *dev = dev_id;
unsigned long flags;
u32 status = readl(dev->base + UART_STATUS);
if (!(status & UART_RX_READY))
return IRQ_NONE;
spin_lock_irqsave(&dev->lock, flags);
/* 快速读取 FIFO 中的数据 */
while (readl(dev->base + UART_STATUS) & UART_RX_READY) {
if (dev->rx_len < sizeof(dev->rx_buf))
dev->rx_buf[dev->rx_len++] = readb(dev->base + UART_DATA);
}
spin_unlock_irqrestore(&dev->lock, flags);
/* 调度 tasklet 处理数据 */
tasklet_schedule(&dev->rx_tasklet);
return IRQ_HANDLED;
}
static int uart_probe(struct platform_device *pdev)
{
struct uart_dev *dev;
/* ... */
/* 初始化 tasklet */
tasklet_init(&dev->rx_tasklet, uart_rx_tasklet, (unsigned long)dev);
/* 申请中断 */
request_irq(dev->irq, uart_irq_handler, 0, "uart", dev);
return 0;
}
static int uart_remove(struct platform_device *pdev)
{
struct uart_dev *dev = platform_get_drvdata(pdev);
/* 必须先 kill tasklet,再释放中断 */
tasklet_kill(&dev->rx_tasklet);
free_irq(dev->irq, dev);
return 0;
}
10.3.3 工作队列
工作队列(Work Queue)是最灵活的底半部机制,运行在进程上下文 中,可以睡眠:
c
#include <linux/workqueue.h>
/* ── 使用内核默认工作队列 ──────────────────────────────── */
/* 定义工作项 */
struct work_struct my_work;
/* 工作处理函数 */
static void my_work_func(struct work_struct *work)
{
struct my_dev *dev = container_of(work, struct my_dev, work);
/*
* 工作队列运行在进程上下文:
* ✓ 可以睡眠(msleep、wait_event 等)
* ✓ 可以调用 kmalloc(GFP_KERNEL)
* ✓ 可以调用 mutex_lock
* ✓ 可以访问用户空间(copy_to/from_user)
*/
pr_info("工作队列执行,处理中断数据\n");
/* 可以做耗时操作 */
msleep(10);
process_complex_data(dev);
}
/* 初始化工作项 */
INIT_WORK(&my_work, my_work_func);
/* 在中断处理函数中调度工作 */
static irqreturn_t my_irq_handler(int irq, void *dev_id)
{
struct my_dev *dev = dev_id;
/* 顶半部处理 */
dev->irq_data = readl(dev->base + DATA_REG);
/* 调度工作队列(底半部)*/
schedule_work(&dev->work);
return IRQ_HANDLED;
}
/* ── 延迟工作(delayed_work)──────────────────────────── */
struct delayed_work my_delayed_work;
static void my_delayed_work_func(struct work_struct *work)
{
struct delayed_work *dwork = to_delayed_work(work);
struct my_dev *dev = container_of(dwork, struct my_dev, delayed_work);
pr_info("延迟工作执行\n");
}
INIT_DELAYED_WORK(&my_delayed_work, my_delayed_work_func);
/* 500ms 后执行 */
schedule_delayed_work(&my_delayed_work, msecs_to_jiffies(500));
/* 取消延迟工作 */
cancel_delayed_work_sync(&my_delayed_work);
/* ── 创建专用工作队列 ──────────────────────────────────── */
struct workqueue_struct *my_wq;
/* 创建单线程工作队列 */
my_wq = create_singlethread_workqueue("my_driver_wq");
/* 创建多线程工作队列(每个 CPU 一个线程)*/
my_wq = create_workqueue("my_driver_wq");
/* 向专用工作队列提交工作 */
queue_work(my_wq, &my_work);
queue_delayed_work(my_wq, &my_delayed_work, msecs_to_jiffies(100));
/* 等待工作队列中所有工作完成 */
flush_workqueue(my_wq);
/* 销毁工作队列 */
destroy_workqueue(my_wq);
工作队列的完整使用案例:
c
struct sensor_dev {
void __iomem *base;
int irq;
struct work_struct data_work;
struct workqueue_struct *wq;
int raw_data;
struct mutex mutex;
};
/* 工作处理函数:复杂的数据处理 */
static void sensor_data_work(struct work_struct *work)
{
struct sensor_dev *dev = container_of(work, struct sensor_dev, data_work);
int processed_data;
/* 可以睡眠的复杂处理 */
mutex_lock(&dev->mutex);
/* 模拟耗时的数据处理(如 I2C 读取、数据转换)*/
msleep(5);
processed_data = dev->raw_data * 100 / 4096; /* ADC 转换 */
pr_info("sensor: 原始数据 %d → 处理后 %d\n",
dev->raw_data, processed_data);
mutex_unlock(&dev->mutex);
}
/* 中断处理函数(顶半部)*/
static irqreturn_t sensor_irq_handler(int irq, void *dev_id)
{
struct sensor_dev *dev = dev_id;
/* 快速读取原始数据 */
dev->raw_data = readl(dev->base + ADC_DATA_REG);
/* 清除中断 */
writel(ADC_INT_CLEAR, dev->base + ADC_INT_REG);
/* 调度工作队列处理数据 */
queue_work(dev->wq, &dev->data_work);
return IRQ_HANDLED;
}
static int sensor_probe(struct platform_device *pdev)
{
struct sensor_dev *dev;
dev = devm_kzalloc(&pdev->dev, sizeof(*dev), GFP_KERNEL);
if (!dev) return -ENOMEM;
mutex_init(&dev->mutex);
/* 创建专用工作队列 */
dev->wq = create_singlethread_workqueue("sensor_wq");
if (!dev->wq) return -ENOMEM;
/* 初始化工作项 */
INIT_WORK(&dev->data_work, sensor_data_work);
/* 申请中断 */
devm_request_irq(&pdev->dev, dev->irq, sensor_irq_handler,
0, "sensor", dev);
platform_set_drvdata(pdev, dev);
return 0;
}
static int sensor_remove(struct platform_device *pdev)
{
struct sensor_dev *dev = platform_get_drvdata(pdev);
/* 等待所有工作完成并销毁工作队列 */
flush_workqueue(dev->wq);
destroy_workqueue(dev->wq);
return 0;
}
10.3.4 软中断
**软中断(softirq)**是 Linux 内核内部使用的底半部机制,优先级高于工作队列,不能睡眠:
c
/* Linux 内核预定义的软中断类型(优先级从高到低)*/
enum {
HI_SOFTIRQ = 0, /* 高优先级 tasklet */
TIMER_SOFTIRQ, /* 定时器 */
NET_TX_SOFTIRQ, /* 网络发送 */
NET_RX_SOFTIRQ, /* 网络接收 */
BLOCK_SOFTIRQ, /* 块设备 */
BLOCK_IOPOLL_SOFTIRQ,/* 块设备 I/O 轮询 */
TASKLET_SOFTIRQ, /* 普通 tasklet */
SCHED_SOFTIRQ, /* 调度器 */
HRTIMER_SOFTIRQ, /* 高精度定时器 */
RCU_SOFTIRQ, /* RCU */
NR_SOFTIRQS
};
/*
* 软中断通常由内核子系统使用,驱动开发者一般不直接使用软中断
* 驱动应该使用 tasklet 或工作队列
*
* 软中断的特点:
* - 可以在多个 CPU 上同时运行(同一软中断的不同实例)
* - 不能睡眠
* - 优先级高于 tasklet
* - 驱动开发者通常不需要直接使用
*/
/* 触发软中断 */
raise_softirq(NET_RX_SOFTIRQ);
/* 软中断处理函数注册(内核初始化时)*/
open_softirq(MY_SOFTIRQ, my_softirq_handler);
10.3.5 threaded_irq(中断线程化)
threaded_irq 是 Linux 2.6.30 引入的机制,将中断处理函数在内核线程中执行:
c
/*
* request_threaded_irq:申请线程化中断
*
* irq:中断号
* handler:顶半部处理函数(在中断上下文中执行,可为 NULL)
* thread_fn:线程处理函数(在内核线程中执行,可以睡眠)
* flags:中断标志
* name:中断名称
* dev:设备指针
*/
int request_threaded_irq(unsigned int irq,
irq_handler_t handler,
irq_handler_t thread_fn,
unsigned long flags,
const char *name,
void *dev);
/* 使用示例 */
/* 顶半部:在中断上下文中执行,快速判断是否需要线程处理 */
static irqreturn_t my_hard_irq(int irq, void *dev_id)
{
struct my_dev *dev = dev_id;
/* 检查是否是本设备的中断 */
if (!(readl(dev->base + STATUS) & MY_INT))
return IRQ_NONE;
/* 清除中断 */
writel(MY_INT, dev->base + STATUS);
/* 返回 IRQ_WAKE_THREAD:唤醒线程处理函数 */
return IRQ_WAKE_THREAD;
}
/* 线程处理函数:在内核线程中执行,可以睡眠 */
static irqreturn_t my_thread_irq(int irq, void *dev_id)
{
struct my_dev *dev = dev_id;
/*
* 在内核线程中执行:
* ✓ 可以睡眠
* ✓ 可以调用 mutex_lock
* ✓ 可以调用 kmalloc(GFP_KERNEL)
* ✓ 可以进行 I2C/SPI 通信
*/
pr_info("线程化中断处理\n");
/* 通过 I2C 读取传感器数据(会睡眠)*/
i2c_smbus_read_byte_data(dev->i2c_client, REG_DATA);
return IRQ_HANDLED;
}
/* 申请线程化中断 */
ret = request_threaded_irq(irq,
my_hard_irq, /* 顶半部 */
my_thread_irq, /* 线程处理函数 */
IRQF_ONESHOT, /* 线程处理完成前禁止该中断 */
"my_device",
dev);
/*
* 简化版:顶半部为 NULL,直接使用线程处理
* 内核会使用默认的顶半部(直接唤醒线程)
*/
ret = request_threaded_irq(irq,
NULL, /* 使用默认顶半部 */
my_thread_irq,
IRQF_ONESHOT | IRQF_TRIGGER_FALLING,
"my_device",
dev);
10.3.6 底半部机制对比
底半部机制对比:
机制 运行上下文 可睡眠 适用场景
─────────────────────────────────────────────────────────────
tasklet 软中断上下文 否 简单、快速的延迟处理
工作队列 进程上下文 是 耗时操作、需要睡眠的处理
软中断 软中断上下文 否 内核子系统(驱动不直接使用)
threaded_irq 内核线程 是 需要睡眠的中断处理(推荐)
选择建议:
需要睡眠 → 工作队列 或 threaded_irq
不需要睡眠,简单处理 → tasklet
高性能网络/存储 → 软中断(内核子系统)
新驱动开发 → 优先考虑 threaded_irq(更简洁)
10.4 实时性与中断线程化
10.4.1 实时性的概念
**实时性(Real-Time)**是指系统对外部事件的响应时间是可预测和有界的:
实时性分类:
硬实时(Hard Real-Time):
响应时间有严格上限,超时即为失败
例:汽车安全气囊(必须在 10ms 内响应)
需要:RTOS 或 PREEMPT_RT 补丁
软实时(Soft Real-Time):
响应时间有期望上限,偶尔超时可接受
例:音视频播放(偶尔卡顿可接受)
需要:低延迟内核配置
Linux 标准内核的实时性问题:
- 中断处理期间禁止抢占,导致延迟不可预测
- 自旋锁持有期间不可抢占
- 大量代码在不可抢占的上下文中执行
10.4.2 PREEMPT_RT 补丁与中断线程化
Linux 的 PREEMPT_RT 补丁通过将所有中断处理线程化来提高实时性:
PREEMPT_RT 的核心改变:
1. 中断线程化
所有硬件中断处理函数在内核线程中执行
中断线程可以被更高优先级的实时任务抢占
2. 自旋锁替换为互斥锁
大多数 spinlock 替换为可睡眠的 rt_mutex
支持优先级继承,防止优先级反转
3. 完全可抢占内核
内核代码几乎所有地方都可以被抢占
中断线程化的效果:
标准内核:中断延迟 100μs~10ms(不可预测)
PREEMPT_RT:中断延迟 < 100μs(可预测)
在驱动中支持中断线程化:
c
/*
* 使用 request_threaded_irq 的驱动天然支持中断线程化
* 在 PREEMPT_RT 内核上,线程化中断可以被实时任务抢占
*/
/* 设置中断线程的实时优先级 */
struct irq_desc *desc = irq_to_desc(irq);
if (desc && desc->action && desc->action->thread) {
struct sched_param param = { .sched_priority = 50 };
sched_setscheduler(desc->action->thread,
SCHED_FIFO, ¶m);
}
10.5 Linux 时钟编程
10.5.1 jiffies 与 HZ
c
#include <linux/jiffies.h>
/*
* HZ:每秒的时钟节拍数
* 通常为 100(嵌入式)、250 或 1000(桌面/服务器)
*/
printk("HZ = %d\n", HZ);
/*
* jiffies:系统启动以来的时钟节拍总数(unsigned long)
* 在 32 位系统上,约 49.7 天溢出(HZ=1000 时)
* 在 64 位系统上,几乎不会溢出
*/
unsigned long now = jiffies;
/* jiffies 与时间单位的转换 */
unsigned long ms_to_jiffies(unsigned int ms);
unsigned long usecs_to_jiffies(unsigned int us);
unsigned int jiffies_to_msecs(unsigned long j);
unsigned int jiffies_to_usecs(unsigned long j);
/* 时间比较(处理溢出)*/
time_after(a, b) /* a > b(考虑溢出)*/
time_before(a, b) /* a < b(考虑溢出)*/
time_after_eq(a, b) /* a >= b */
time_before_eq(a, b) /* a <= b */
/* 使用示例 */
unsigned long timeout = jiffies + msecs_to_jiffies(500); /* 500ms 后超时 */
while (!condition) {
if (time_after(jiffies, timeout)) {
pr_err("操作超时\n");
return -ETIMEDOUT;
}
msleep(10);
}
10.5.2 内核定时器(kernel timer)
内核定时器用于在指定时间后执行回调函数:
c
#include <linux/timer.h>
/* ── 定义和初始化定时器 ────────────────────────────────── */
struct timer_list my_timer;
/* Linux 4.15+ 新接口(推荐)*/
timer_setup(&my_timer, my_timer_callback, 0);
/* 旧接口(Linux 4.15 之前)*/
init_timer(&my_timer);
my_timer.function = my_timer_callback;
my_timer.data = (unsigned long)dev;
/* ── 定时器回调函数 ────────────────────────────────────── */
/* 新接口(Linux 4.15+)*/
static void my_timer_callback(struct timer_list *t)
{
struct my_dev *dev = from_timer(dev, t, timer);
pr_info("定时器触发\n");
/* 定时器是一次性的,如需周期性触发,需要重新设置 */
mod_timer(&dev->timer, jiffies + msecs_to_jiffies(1000));
}
/* 旧接口 */
static void my_timer_callback_old(unsigned long data)
{
struct my_dev *dev = (struct my_dev *)data;
pr_info("定时器触发(旧接口)\n");
}
/* ── 定时器操作 ────────────────────────────────────────── */
/* 设置超时时间并启动定时器 */
mod_timer(&my_timer, jiffies + msecs_to_jiffies(1000)); /* 1秒后触发 */
/* 添加定时器(首次启动)*/
add_timer(&my_timer);
/* 删除定时器(不等待回调完成)*/
del_timer(&my_timer);
/* 删除定时器(等待回调完成,安全)*/
del_timer_sync(&my_timer);
/* 检查定时器是否处于激活状态 */
int active = timer_pending(&my_timer);
定时器的完整使用案例(LED 闪烁):
c
struct led_dev {
int gpio;
int state;
struct timer_list blink_timer;
unsigned int interval_ms; /* 闪烁间隔(毫秒)*/
};
/* 定时器回调:翻转 LED 状态 */
static void led_blink_callback(struct timer_list *t)
{
struct led_dev *dev = from_timer(dev, t, blink_timer);
dev->state = !dev->state;
gpio_set_value(dev->gpio, dev->state);
/* 重新设置定时器,实现周期性闪烁 */
mod_timer(&dev->blink_timer,
jiffies + msecs_to_jiffies(dev->interval_ms));
}
static int led_probe(struct platform_device *pdev)
{
struct led_dev *dev;
dev = devm_kzalloc(&pdev->dev, sizeof(*dev), GFP_KERNEL);
if (!dev) return -ENOMEM;
dev->gpio = of_get_named_gpio(pdev->dev.of_node, "led-gpio", 0);
dev->interval_ms = 500; /* 500ms 闪烁间隔 */
dev->state = 0;
devm_gpio_request_one(&pdev->dev, dev->gpio, GPIOF_OUT_INIT_LOW, "led");
/* 初始化定时器 */
timer_setup(&dev->blink_timer, led_blink_callback, 0);
/* 启动定时器 */
mod_timer(&dev->blink_timer,
jiffies + msecs_to_jiffies(dev->interval_ms));
platform_set_drvdata(pdev, dev);
dev_info(&pdev->dev, "LED 闪烁驱动加载,间隔 %dms\n", dev->interval_ms);
return 0;
}
static int led_remove(struct platform_device *pdev)
{
struct led_dev *dev = platform_get_drvdata(pdev);
/* 停止定时器(必须在释放 GPIO 之前)*/
del_timer_sync(&dev->blink_timer);
gpio_set_value(dev->gpio, 0); /* 熄灭 LED */
dev_info(&pdev->dev, "LED 闪烁驱动卸载\n");
return 0;
}
10.5.3 高精度定时器(hrtimer)
内核定时器的精度受 HZ 限制(最小 1/HZ 秒),高精度定时器(hrtimer)可以达到纳秒级精度:
c
#include <linux/hrtimer.h>
#include <linux/ktime.h>
struct hrtimer my_hrtimer;
/* 高精度定时器回调函数 */
static enum hrtimer_restart my_hrtimer_callback(struct hrtimer *timer)
{
struct my_dev *dev = container_of(timer, struct my_dev, hrtimer);
pr_info("高精度定时器触发\n");
/* 返回 HRTIMER_RESTART:重新启动定时器(周期性)*/
/* 返回 HRTIMER_NORESTART:不重启(一次性)*/
/* 周期性:前进 1ms */
hrtimer_forward_now(timer, ms_to_ktime(1));
return HRTIMER_RESTART;
}
/* 初始化高精度定时器 */
hrtimer_init(&my_hrtimer, CLOCK_MONOTONIC, HRTIMER_MODE_REL);
my_hrtimer.function = my_hrtimer_callback;
/* 启动定时器(1ms 后触发)*/
hrtimer_start(&my_hrtimer, ms_to_ktime(1), HRTIMER_MODE_REL);
/* 取消定时器 */
hrtimer_cancel(&my_hrtimer);
/* ktime 操作 */
ktime_t now = ktime_get();
ktime_t kt = ktime_set(1, 500000000); /* 1秒500毫秒 */
ktime_t kt2 = ms_to_ktime(100); /* 100毫秒 */
ktime_t kt3 = ns_to_ktime(1000); /* 1000纳秒 */
s64 ns = ktime_to_ns(kt); /* 转换为纳秒 */
10.6 内核延时
10.6.1 忙等待延时
忙等待延时会占用 CPU,适用于极短时间的延时(通常 < 10μs):
c
#include <linux/delay.h>
/*
* ndelay(ns):纳秒级忙等待
* 适用于 < 1μs 的延时
* 精度:依赖于 CPU 频率校准
*/
ndelay(100); /* 等待约 100 纳秒 */
/*
* udelay(us):微秒级忙等待
* 适用于 1μs ~ 1000μs 的延时
* 可以在中断上下文中使用
* 注意:us 不能太大(通常 < 1000),否则会触发内核警告
*/
udelay(10); /* 等待约 10 微秒 */
/*
* mdelay(ms):毫秒级忙等待(不推荐)
* 实现为循环调用 udelay
* 会长时间占用 CPU,影响系统响应
* 除非在中断上下文中必须使用,否则应使用 msleep
*/
mdelay(5); /* 等待约 5 毫秒(不推荐!)*/
10.6.2 睡眠延时
睡眠延时会让出 CPU,适用于较长时间的延时,只能在进程上下文中使用:
c
#include <linux/delay.h>
/*
* msleep(ms):毫秒级睡眠
* 进程进入睡眠,让出 CPU
* 不可被信号中断
* 适用于 > 20ms 的延时
*/
msleep(100); /* 睡眠约 100 毫秒 */
/*
* msleep_interruptible(ms):可被信号中断的毫秒睡眠
* 返回:0(正常唤醒),剩余毫秒数(被信号中断)
*/
unsigned long remaining = msleep_interruptible(1000);
if (remaining) {
pr_info("睡眠被信号中断,剩余 %lu ms\n", remaining);
}
/*
* ssleep(s):秒级睡眠
*/
ssleep(2); /* 睡眠 2 秒 */
/*
* usleep_range(min_us, max_us):微秒范围睡眠(推荐)
* 比 udelay 更节能(允许 CPU 进入低功耗状态)
* 适用于 10μs ~ 20ms 的延时
* min_us:最短睡眠时间
* max_us:最长睡眠时间(给内核一定的调度余地)
*/
usleep_range(900, 1100); /* 睡眠 900μs ~ 1100μs */
usleep_range(10000, 11000);/* 睡眠 10ms ~ 11ms */
10.6.3 延时函数选择原则
延时函数选择指南:
延时时间 推荐函数 说明
─────────────────────────────────────────────────────────────
< 1μs ndelay() 纳秒级忙等待
1μs ~ 10μs udelay() 微秒级忙等待
10μs ~ 20ms usleep_range() 微秒范围睡眠(节能)
> 20ms msleep() 毫秒级睡眠
特殊场景:
中断上下文 只能用 ndelay/udelay/mdelay(忙等待)
需要可中断 msleep_interruptible()
精确延时 hrtimer(高精度定时器)
注意事项:
1. 忙等待期间 CPU 无法做其他事,影响系统性能
2. 睡眠延时不能在中断上下文中使用
3. usleep_range 的范围越大,内核调度越灵活,系统性能越好
4. 不要在持有自旋锁时使用睡眠延时
10.6.4 延时的综合案例
c
/*
* 综合案例:SPI Flash 写操作等待
* SPI Flash 写操作需要等待内部写周期完成(最长 5ms)
*/
static int spi_flash_wait_ready(struct spi_device *spi)
{
u8 status;
unsigned long timeout = jiffies + msecs_to_jiffies(100); /* 100ms 超时 */
do {
/* 读取状态寄存器 */
spi_read_status(spi, &status);
if (!(status & FLASH_STATUS_WIP))
return 0; /* 写操作完成 */
/* 短暂睡眠,避免频繁轮询 */
usleep_range(500, 1000); /* 等待 500μs ~ 1ms */
} while (time_before(jiffies, timeout));
pr_err("SPI Flash 写操作超时\n");
return -ETIMEDOUT;
}
/*
* 综合案例:I2C 设备复位
* 复位后需要等待设备就绪
*/
static int i2c_device_reset(struct i2c_client *client)
{
/* 拉低复位引脚 */
gpio_set_value(reset_gpio, 0);
udelay(10); /* 保持复位 10μs(忙等待,时间短)*/
/* 释放复位引脚 */
gpio_set_value(reset_gpio, 1);
msleep(50); /* 等待设备启动 50ms(睡眠,时间长)*/
/* 验证设备是否就绪 */
return i2c_smbus_read_byte(client) >= 0 ? 0 : -ENODEV;
}
本章小结
| 章节 | 核心知识点 | 关键 API |
|---|---|---|
| 10.1 中断与定时器 | 中断的概念与分类;中断号;定时器体系;jiffies/HZ | /proc/interrupts、jiffies、HZ |
| 10.2 Linux中断编程 | request_irq 参数详解;中断处理函数规范;共享中断;中断使能/禁止 |
request_irq()、free_irq()、IRQ_HANDLED、IRQ_NONE |
| 10.3.1 tasklet | 软中断上下文;不能睡眠;DECLARE_TASKLET;tasklet_schedule;完整UART案例 |
tasklet_init()、tasklet_schedule()、tasklet_kill() |
| 10.3.2 工作队列 | 进程上下文;可以睡眠;INIT_WORK;延迟工作;专用工作队列;完整传感器案例 |
INIT_WORK()、schedule_work()、create_singlethread_workqueue() |
| 10.3.3 软中断 | 内核子系统使用;多CPU并发;驱动不直接使用 | raise_softirq()、open_softirq() |
| 10.3.4 threaded_irq | 中断线程化;可以睡眠;IRQ_WAKE_THREAD;IRQF_ONESHOT |
request_threaded_irq()、IRQ_WAKE_THREAD |
| 10.4 实时性 | 硬/软实时;PREEMPT_RT补丁;中断线程化提高实时性 | IRQF_ONESHOT、实时调度 |
| 10.5 时钟编程 | jiffies时间比较;内核定时器(timer_setup/mod_timer);高精度定时器(hrtimer);LED闪烁案例 |
timer_setup()、mod_timer()、del_timer_sync()、hrtimer_start() |
| 10.6 内核延时 | 忙等待(ndelay/udelay);睡眠延时(msleep/usleep_range);选择原则;SPI Flash等待案例 | udelay()、msleep()、usleep_range() |
中断底半部选择决策树
需要在中断后执行延迟处理?
↓
需要睡眠?
是 → 工作队列(schedule_work)或 threaded_irq
否 → 继续判断
↓
需要高性能(网络/存储)?
是 → 软中断(内核子系统)
否 → tasklet(简单场景)
新驱动开发建议:
优先使用 threaded_irq(最简洁,可睡眠)
其次使用工作队列(灵活,可睡眠)
最后考虑 tasklet(不能睡眠,但开销小)
参考文献:宋宝华《Linux设备驱动开发详解:基于最新的Linux 4.0内核》,机械工业出版社,2015年