10.1 中断与定时器
中断是设备主动通知 CPU 的机制;定时器是内核在未来某时刻执行某段代码的机制。二者共同点:回调在原子上下文(软中断上下文)执行,不能睡眠。
7.2.5 下中断子系统的层次:
关键概念:
| 概念 | 说明 |
|---|---|
| IRQ number | Linux 逻辑中断号,由 irqchip 的 IRQ domain 映射得到 |
| hwirq | 硬件中断号(GIC/APIC 的编号) |
| irq_desc | 每个 Linux IRQ 的描述符(include/linux/irqdesc.h) |
| irqaction | 每个注册的 handler(kernel/irq/manage.c) |
| 顶半部(top half) | 硬中断上下文,极短、不能睡眠 |
| 底半部(bottom half) | 延后执行的重活,见 10.3.3 |
10.2 Linux 中断处理程序架构
7.2.5 的处理流程(kernel/irq/handle.c):
- 硬件中断发生,CPU 进入
arch_irq_enter()。 generic_handle_domain_irq()/handle_irq_event()找到irq_desc。- 依次调用该 IRQ 上注册的所有
irqaction->handler(顶半部)。 - 如果 handler 返回
IRQ_WAKE_THREAD且注册了 threaded handler,唤醒对应的irq/%d-%s内核线程执行thread_fn。 - 若注册了
IRQF_ONESHOT,线程执行完毕后才重新使能该中断。 - 触发软中断(
raise_softirq())处理tasklet/网络收包等。 arch_irq_exit()。
为什么必须拆成两半:
- 硬中断期间该 IRQ 线被屏蔽(非
IRQF_PERCPU),处理越久,设备越可能丢中断。 - 硬中断上下文不能睡眠、不能获取 mutex、不能调用可能睡眠的分配。
- 因此硬中断只做"取走硬件数据、确认中断、唤醒后续处理"。
10.3 Linux 中断编程
10.3.1 申请和释放中断
/* include/linux/interrupt.h */ typedef irqreturn_t (*irq_handler_t)(int irq, void *dev_id); /* 最常用:devm 版本,卸载时自动释放 */ int devm_request_irq(struct device *dev, unsigned int irq, irq_handler_t handler, unsigned long irqflags, const char *devname, void *dev_id); /* 带线程化的 devm 版本 */ int devm_request_threaded_irq(struct device *dev, unsigned int irq, irq_handler_t handler, irq_handler_t thread_fn, unsigned long irqflags, const char *devname, void *dev_id); /* 非 devm 版本(需要显式 free_irq) */ int request_irq(unsigned int irq, irq_handler_t handler, unsigned long flags, const char *name, void *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); void free_irq(unsigned int irq, void *dev_id); /* 探测式获取(不确定是否有 IRQ 时) */ int request_any_context_irq(unsigned int irq, irq_handler_t handler, unsigned long flags, const char *name, void *dev_id);返回值:
IRQ_HANDLED /* 是本设备的中断,已处理 */ IRQ_NONE /* 不是本设备的中断(共享中断必须正确返回) */ IRQ_WAKE_THREAD /* 请内核唤醒 thread_fn */irqflags取值表(7.2.5):
| 标志 | 含义 |
|---|---|
IRQF_SHARED | 允许多个设备共享该 IRQ |
IRQF_TRIGGER_RISING/FALLING/HIGH/LOW | 触发方式 |
IRQF_ONESHOT | 线程处理完前不复位中断(threaded IRQ 常用) |
IRQF_NO_THREAD | 禁止线程化(RT 内核中用于不可线程化的中断) |
IRQF_PERCPU | 每 CPU 中断 |
IRQF_NO_AUTOEN | 注册时不自动使能(需显式enable_irq()) |
IRQF_NO_SUSPEND | 挂起时不屏蔽 |
IRQF_COND_ONESHOT | 共享中断时同意ONESHOT语义 |
IRQF_PROBE_SHARED | 探测共享中断 |
典型实现:
static irqreturn_t my_hardirq(int irq, void *dev_id) { struct my_dev *dev = dev_id; if (!my_is_my_irq(dev)) return IRQ_NONE; /* 共享中断必须判断 */ /* 只做最小工作:读状态、清中断、唤醒线程 */ return IRQ_WAKE_THREAD; } static irqreturn_t my_threadfn(int irq, void *dev_id) { struct my_dev *dev = dev_id; /* 可以睡眠、可以用 mutex、可以 copy_to_user */ process(dev); return IRQ_HANDLED; } static int my_probe(struct platform_device *pdev) { struct my_dev *dev = ...; int irq = platform_get_irq(pdev, 0); if (irq < 0) return irq; return devm_request_threaded_irq(&pdev->dev, irq, my_hardirq, my_threadfn, IRQF_TRIGGER_FALLING | IRQF_ONESHOT, dev_name(&pdev->dev), dev); }7.2.5 的关键变化:
IRQF_DISABLED已被删除(4.0 时代已废弃)。所有 handler 现在默认在关中断状态下运行。IRQF_ONESHOT与IRQF_COND_ONESHOT对线程化中断很重要。- 7.2.5 推荐优先用
devm_request_irq()/devm_request_threaded_irq(),卸载时自动释放。 request_threaded_irq()支持"handler 为 NULL 时使用默认主处理函数"的用法(返回IRQ_WAKE_THREAD)。- 7.2.5 中
free_irq()会等待正在运行的 handler 返回;devm版本在devres回滚时自动调用。 - 中断处理函数的
dev_id在共享中断中必须唯一且非 NULL。
10.3.2 使能和屏蔽中断
disable_irq(irq); /* 屏蔽并等待正在执行的 handler 结束 */ disable_irq_nosync(irq); /* 屏蔽但不等待(可在中断上下文调用) */ enable_irq(irq); /* 使能,必须与 disable 配对 */7.2.5 新增:scope-based 守卫
#include <linux/interrupt.h> scoped_guard(disable_irq, irq) { /* 离开作用域自动 enable_irq(irq) */ }include/linux/interrupt.h中有DEFINE_LOCK_GUARD_1(disable_irq, int, disable_irq(*_T->lock), enable_irq(*_T->lock))。
重要规则:
disable_irq()不可在中断上下文调用(会死锁,因为它要等待 handler 返回);中断上下文用disable_irq_nosync()。enable_irq()与disable_irq()必须严格配对,计数不平衡会触发Unbalanced enable for IRQ警告。- 不要在 handler 里
disable_irq()自己,用IRQF_ONESHOT或返回IRQ_WAKE_THREAD让内核管理。
10.3.3 底半部机制
7.2.5 可用的底半部机制:
| 机制 | 上下文 | 可睡眠 | 并发性 | 状态 |
|---|---|---|---|---|
| threaded IRQ | 内核线程 | 是 | 每个 IRQ 一个线程 | 首选 |
| workqueue | 内核线程 | 是 | 可并行 | 首选 |
| tasklet | 软中断 | 否 | 同一 tasklet 不并发 | 仅维护遗留代码 |
| softirq | 软中断 | 否 | 同类型可多核并发 | 子系统专用 |
| timer | 软中断 | 否 | 每 CPU | 见 10.5 |
threaded IRQ(推荐)
见 10.3.1 的devm_request_threaded_irq()。优点:可在thread_fn中睡眠、获取 mutex、调用copy_to_user()。
workqueue
/* include/linux/workqueue.h */ struct work_struct work; INIT_WORK(&work, my_work_fn); schedule_work(&work); /* 提交到系统工作队列 */ queue_work(my_wq, &work); /* 提交到自定义队列 */ struct delayed_work dwork; INIT_DELAYED_WORK(&dwork, my_delayed_fn); schedule_delayed_work(&dwork, HZ); /* 1 秒后执行 */ queue_delayed_work(my_wq, &dwork, msecs_to_jiffies(100)); mod_delayed_work(my_wq, &dwork, HZ); /* 修改已排队的延时 */ /* 自定义工作队列 */ struct workqueue_struct *wq; wq = alloc_workqueue("mywq", WQ_UNBOUND | WQ_MEM_RECLAIM, 0); /* 7.2.5 资源管理版本 */ wq = devm_alloc_workqueue(dev, "mywq", WQ_MEM_RECLAIM, 0); /* 取消 */ cancel_work_sync(&work); cancel_delayed_work_sync(&dwork); flush_workqueue(wq); destroy_workqueue(wq);alloc_workqueue()的 flags(include/linux/workqueue.h):
| flag | 含义 |
|---|---|
WQ_UNBOUND | 不绑定 CPU,适合可能长时间运行的 work |
WQ_MEM_RECLAIM | 内存回收路径可用(设备驱动几乎总要加) |
WQ_HIGHPRI | 高优先级 |
WQ_CPU_INTENSIVE | CPU 密集型 |
WQ_FREEZABLE | 可冻结(系统挂起时暂停) |
WQ_POWER_EFFICIENT | 省电模式 |
WQ_SYSFS | 在 sysfs 中暴露 |
max_active | 最大并发 work 数,0 表示默认 |
7.2.5 变化:
alloc_workqueue()现在展开为alloc_hooks(alloc_workqueue_noprof(...)),带内存分配 profiling 支持。- 新增
devm_alloc_workqueue(),设备卸载时自动destroy_workqueue()。 WQ_POWER_EFFICIENT成为省电默认(CONFIG_WQ_POWER_EFFICIENT_DEFAULT)。
tasklet(遗留)
struct tasklet_struct t; tasklet_setup(&t, my_tasklet_fn); /* 7.2.5 推荐,回调带 tasklet 参数 */ /* 或 */ tasklet_init(&t, my_tasklet_fn_old, data); tasklet_schedule(&t); tasklet_disable(&t); tasklet_enable(&t); tasklet_kill(&t); /* 模块卸载前必须调用 */7.2.5 建议:tasklet 只用于维护遗留代码。原因:
- 软中断上下文不能睡眠;
- 与现代
PREEMPT_RT的模型冲突(RT 上 tasklet 被转成线程); - 并发模型难以推理。
新代码一律用 threaded IRQ 或 workqueue。
中断上下文的资源限制(必须遵守)
| 操作 | 硬中断 | 软中断/tasklet | threaded IRQ/workqueue |
|---|---|---|---|
kmalloc(GFP_KERNEL) | 否 | 否 | 是 |
kmalloc(GFP_ATOMIC) | 是 | 是 | 是 |
mutex_lock() | 否 | 否 | 是 |
spin_lock() | 是 | 是 | 是 |
copy_to_user() | 否 | 否 | 是 |
msleep() | 否 | 否 | 是 |
printk() | 是 | 是 | 是 |
synchronize_rcu() | 否 | 否 | 是 |
10.3.4 实例:GPIO 按键的中断
QEMU 下没有真实 GPIO 按键,但有两条替代路径:
- gpio-sim + gpio-keys:用 configfs 创建虚拟 GPIO 线,用
gpio-keys驱动绑定。 - 自定义 platform 驱动 + gpio-sim:自己写驱动,从 gpio-sim 获取 GPIO 与 IRQ。
路径一:gpio-keys(推荐,无需写代码)
内核配置:
CONFIG_GPIO_SIM=m CONFIG_KEYBOARD_GPIO=m CONFIG_CONFIGFS_FS=y CONFIG_IRQ_SIM=yguest 中:
mount -t configfs none /sys/kernel/config mkdir /sys/kernel/config/gpio-sim/button mkdir /sys/kernel/config/gpio-sim/button/bank0 echo 1 > /sys/kernel/config/gpio-sim/button/bank0/num_lines echo "button" > /sys/kernel/config/gpio-sim/button/bank0/line0/name echo 1 > /sys/kernel/config/gpio-sim/button/bank0/line0/consumer echo 1 > /sys/kernel/config/gpio-sim/button/live # 激活 ls /sys/bus/gpio/devices/ gpiodetect # libgpiod 工具 gpioinfo # 查看线状态用gpioset/gpioget(libgpiod)操作虚拟线,配合gpio-keys即可在evtest/input-events中看到按键事件。
路径二:写一个带中断的 GPIO 驱动
#include <linux/module.h> #include <linux/platform_device.h> #include <linux/gpio/consumer.h> #include <linux/interrupt.h> #include <linux/of.h> struct btn_dev { struct gpio_desc *gpio; int irq; struct work_struct work; atomic_t count; }; static irqreturn_t btn_irq(int irq, void *data) { struct btn_dev *dev = data; atomic_inc(&dev->count); schedule_work(&dev->work); /* 延后处理 */ return IRQ_HANDLED; } static void btn_work(struct work_struct *work) { struct btn_dev *dev = container_of(work, struct btn_dev, work); pr_info("button pressed %d times\n", atomic_read(&dev->count)); } static int btn_probe(struct platform_device *pdev) { struct btn_dev *dev; int ret; dev = devm_kzalloc(&pdev->dev, sizeof(*dev), GFP_KERNEL); if (!dev) return -ENOMEM; dev->gpio = devm_gpiod_get(&pdev->dev, "button", GPIOD_IN); if (IS_ERR(dev->gpio)) return PTR_ERR(dev->gpio); dev->irq = gpiod_to_irq(dev->gpio); if (dev->irq < 0) return dev->irq; INIT_WORK(&dev->work, btn_work); atomic_set(&dev->count, 0); ret = devm_request_threaded_irq(&pdev->dev, dev->irq, NULL, btn_irq, IRQF_TRIGGER_FALLING | IRQF_ONESHOT, "btn", dev); if (ret) return ret; platform_set_drvdata(pdev, dev); return 0; } static void btn_remove(struct platform_device *pdev) { struct btn_dev *dev = platform_get_drvdata(pdev); cancel_work_sync(&dev->work); } static const struct of_device_id btn_of_match[] = { { .compatible = "qemu,sim-button" }, { } }; MODULE_DEVICE_TABLE(of, btn_of_match); static struct platform_driver btn_driver = { .probe = btn_probe, .remove = btn_remove, .driver = { .name = "sim-button", .of_match_table = btn_of_match, }, }; module_platform_driver(btn_driver); MODULE_LICENSE("GPL");QEMU 中测试:通过 gpio-sim 触发虚拟线变化,观察dmesg输出。
10.4 中断共享
多个设备共用一个 IRQ 线时需要:
- 注册时加
IRQF_SHARED。 - 每个 handler 必须判断中断是否来自自己,不是则返回
IRQ_NONE。 dev_id必须唯一且非 NULL(用于free_irq定位)。- 所有共享者都注册了
IRQF_SHARED才能共享。
static irqreturn_t shared_handler(int irq, void *dev_id) { struct my_dev *dev = dev_id; u32 status = readl(dev->base + IRQ_STATUS); if (!(status & MY_IRQ_BIT)) return IRQ_NONE; /* 不是我的中断 */ writel(status & MY_IRQ_BIT, dev->base + IRQ_CLEAR); return IRQ_HANDLED; }为什么 QEMU 中值得练习:virtio、qemu-xhci等设备都会共享 PCI 中断(INTx),可以观察/proc/interrupts中同一 IRQ 上的多个 handler 名称。
10.5 内核定时器
10.5.1 内核定时器编程
/* include/linux/timer.h */ struct timer_list my_timer; /* 7.2.5 唯一推荐方式 */ timer_setup(&my_timer, my_timer_cb, 0); /* 栈上定时器 */ timer_setup_on_stack(&my_timer, my_timer_cb, 0); destroy_timer_on_stack(&my_timer); /* 回调原型(7.2.5 起) */ static void my_timer_cb(struct timer_list *t) { struct my_dev *dev = from_timer(dev, t, timer); ... } /* 启动/修改 */ mod_timer(&my_timer, jiffies + HZ); /* 1 秒后 */ mod_timer(&my_timer, jiffies + msecs_to_jiffies(500)); add_timer(&my_timer); /* 等价于 mod_timer(t, t->expires) */ del_timer(&my_timer); del_timer_sync(&my_timer); /* 等待回调结束,模块卸载必须用 */ /* 关闭定时器(7.2.5 新增,替代 del_timer_sync 的更安全版本) */ timer_shutdown_sync(&my_timer); timer_shutdown(&my_timer);7.2.5 的重大变化:
init_timer()已被彻底删除,只能用timer_setup()。这是 4.0 代码移植时的第一大坑。- 回调原型从
void (*)(unsigned long)变成void (*)(struct timer_list *),用from_timer()反查所属结构体。 del_timer_sync()在模块卸载路径应优先替换为timer_shutdown_sync(),因为后者保证回调不会再次自我重启,避免卸载后回调重新激活定时器导致的 UAF。- 定时器回调在软中断上下文运行,不能睡眠。
- 自重启定时器:在回调里再次
mod_timer(),或用timer_reduce()。
jiffies 与时间换算:
#include <linux/jiffies.h> #include <linux/ktime.h> HZ /* 每秒 tick 数(CONFIG_HZ,常见 250/1000) */ jiffies /* 全局 tick 计数 */ msecs_to_jiffies(ms) usecs_to_jiffies(us) jiffies_to_msecs(j) time_after(a, b) / time_before(a, b) / time_after_eq(a, b) /* 必须用这些比较 */ ktime_get() / ktime_get_ns() / ktime_get_boottime() ktime_set(secs, nsecs)注意:jiffies比较必须用time_after()/time_before(),直接用>在回绕时会出错。
10.5.2 内核中延迟的工作 delayed_work
struct delayed_work dwork; INIT_DELAYED_WORK(&dwork, my_delayed_fn); schedule_delayed_work(&dwork, HZ); /* 延时 1 秒执行一次 */ schedule_delayed_work_on(cpu, &dwork, HZ); mod_delayed_work(system_wq, &dwork, HZ); cancel_delayed_work(&dwork); /* 不等待 */ cancel_delayed_work_sync(&dwork); /* 等待,卸载必须用 */定时器 vs delayed_work:
| 维度 | timer_list | delayed_work |
|---|---|---|
| 上下文 | 软中断 | 内核线程 |
| 能否睡眠 | 否 | 是 |
| 适用 | 极短、高频、无阻塞 | 需要睡眠或耗时的延迟任务 |
| 7.2.5 推荐 | 硬件相关的精确短延时 | 绝大多数延时任务 |
10.5.3 实例:秒字符设备
原书的"second"设备:写入后 1 秒才可读,或在 1 秒后产生事件。7.2.5 版本:
#include <linux/module.h> #include <linux/fs.h> #include <linux/cdev.h> #include <linux/device.h> #include <linux/timer.h> #include <linux/wait.h> #include <linux/jiffies.h> #define SECOND_MAJOR 0 #define SECOND_MAGIC 's' struct second_dev { struct cdev cdev; struct device *device; struct timer_list timer; struct wait_queue_head wq; atomic_t counter; /* 秒计数 */ atomic_t has_data; /* 是否有新数据 */ }; static struct class *second_class; static struct second_dev *second_devp; static void second_timer_fn(struct timer_list *t) { struct second_dev *dev = from_timer(dev, t, timer); atomic_inc(&dev->counter); atomic_set(&dev->has_data, 1); wake_up_interruptible(&dev->wq); /* 唤醒阻塞读者 */ mod_timer(&dev->timer, jiffies + HZ); /* 重新定时 */ } static ssize_t second_read(struct file *filp, char __user *buf, size_t count, loff_t *ppos) { struct second_dev *dev = filp->private_data; int counter; char kbuf[32]; int len; if (!atomic_read(&dev->has_data)) { if (filp->f_flags & O_NONBLOCK) return -EAGAIN; if (wait_event_interruptible(dev->wq, atomic_read(&dev->has_data))) return -ERESTARTSYS; } atomic_set(&dev->has_data, 0); counter = atomic_read(&dev->counter); len = scnprintf(kbuf, sizeof(kbuf), "%d\n", counter); if (copy_to_user(buf, kbuf, len)) return -EFAULT; return len; } static int second_open(struct inode *inode, struct file *filp) { struct second_dev *dev = container_of(inode->i_cdev, struct second_dev, cdev); filp->private_data = dev; /* 每次打开重新定时,避免长期空转 */ mod_timer(&dev->timer, jiffies + HZ); return 0; } static int second_release(struct inode *inode, struct file *filp) { return 0; } static const struct file_operations second_fops = { .owner = THIS_MODULE, .open = second_open, .read = second_read, .release = second_release, }; static int __init second_init(void) { dev_t devno; int ret; ret = alloc_chrdev_region(&devno, 0, 1, "second"); if (ret) return ret; second_devp = kzalloc(sizeof(*second_devp), GFP_KERNEL); if (!second_devp) { ret = -ENOMEM; goto err_region; } init_waitqueue_head(&second_devp->wq); atomic_set(&second_devp->counter, 0); atomic_set(&second_devp->has_data, 0); timer_setup(&second_devp->timer, second_timer_fn, 0); cdev_init(&second_devp->cdev, &second_fops); second_devp->cdev.owner = THIS_MODULE; ret = cdev_add(&second_devp->cdev, devno, 1); if (ret) goto err_free; second_class = class_create("second"); if (IS_ERR(second_class)) { ret = PTR_ERR(second_class); goto err_cdev; } second_devp->device = device_create(second_class, NULL, devno, NULL, "second"); if (IS_ERR(second_devp->device)) { ret = PTR_ERR(second_devp->device); goto err_class; } mod_timer(&second_devp->timer, jiffies + HZ); pr_info("second: loaded\n"); return 0; err_class: class_destroy(second_class); err_cdev: cdev_del(&second_devp->cdev); err_free: kfree(second_devp); err_region: unregister_chrdev_region(devno, 1); return ret; } static void __exit second_exit(void) { dev_t devno = MKDEV(MAJOR(second_devp->cdev.dev), 0); timer_shutdown_sync(&second_devp->timer); /* 7.2.5:阻止回调重启定时器 */ device_destroy(second_class, devno); class_destroy(second_class); cdev_del(&second_devp->cdev); kfree(second_devp); unregister_chrdev_region(devno, 1); pr_info("second: unloaded\n"); } module_init(second_init); module_exit(second_exit); MODULE_LICENSE("GPL");验证:
insmod second.ko cat /dev/second # 立即返回,每秒打印一个递增计数 timeout 5 cat /dev/second dmesg | tail10.6 内核延时
10.6.1 短延迟(忙等,不睡眠)
/* include/linux/delay.h */ ndelay(ns); /* 纳秒,实际按 us 粒度向上取整 */ udelay(us); /* 微秒,通常 < 1000us */ mdelay(ms); /* 毫秒,内部循环 udelay */规则:
udelay()依赖__udelay()的忙等循环,不能在长延时中使用(会占用 CPU)。mdelay()大于MAX_UDELAY_MS时会拆成多次udelay(1000)。- 忙等期间不释放 CPU,只能用于寄存器复位等待等极短场景。
10.6.2 长延迟(可睡眠)
msleep(ms); /* 不可中断睡眠 */ msleep_interruptible(ms); /* 可中断,返回剩余 ms */ ssleep(sec); /* 秒 */ usleep_range(min_us, max_us); /* 推荐:给调度器选择空间,更省电 */ usleep_range_idle(min, max); /* 7.2.5 新增,IDLE 状态,不计入负载 */7.2.5 推荐:需要几十微秒以上延时且可以睡眠时,用usleep_range()而非udelay(),因为它让高分辨率定时器有合并窗口,显著降低功耗。
10.6.3 睡着延迟(等待事件)
真正的"睡着延迟"应使用等待队列:
/* 等待某个条件,最多等待 100ms */ ret = wait_event_interruptible_timeout(wq, condition, msecs_to_jiffies(100)); /* 精确睡眠 */ ktime_t timeout = ktime_set(0, 100 * NSEC_PER_MSEC); set_current_state(TASK_UNINTERRUPTIBLE); schedule_hrtimeout(&timeout, HRTIMER_MODE_REL);| 需求 | 推荐 API |
|---|---|
| < 10us 且不可睡眠 | ndelay()/udelay() |
| 10us ~ 20ms,可睡眠 | usleep_range() |
| > 20ms,可睡眠 | msleep()/ssleep() |
| 等待事件 | wait_event_*() |
| 精确高分辨率延时 | hrtimer+schedule_hrtimeout() |
10.6.4 hrtimer(高分辨率定时器)
/* include/linux/hrtimer.h */ struct hrtimer hr; /* 7.2.5 新式初始化 */ hrtimer_setup(&hr, my_hr_cb, CLOCK_MONOTONIC, HRTIMER_MODE_REL); static enum hrtimer_restart my_hr_cb(struct hrtimer *t) { struct my_dev *dev = from_timer(dev, t, hr); ... hrtimer_forward_now(t, ms_to_ktime(10)); /* 每 10ms 一次 */ return HRTIMER_RESTART; /* 或 HRTIMER_NORESTART */ } hrtimer_start(&hr, ms_to_ktime(100), HRTIMER_MODE_REL); hrtimer_cancel(&hr);7.2.5 变化:
- 新增
hrtimer_setup()/hrtimer_setup_on_stack()/hrtimer_setup_sleeper_on_stack(),替代hrtimer_init()(hrtimer_init()仍存在但已标注为旧接口)。 hrtimer回调在硬中断上下文(HRTIMER_MODE_HARD)或软中断上下文执行,不能睡眠。hrtimer的精度取决于CONFIG_HIGH_RES_TIMERS与硬件时钟源。
QEMU 中的定时器实验观察点:
cat /proc/timer_list # 需要 CONFIG_TIMER_STATS? 实际为 /proc/timer_list cat /proc/interrupts | grep -i timer cat /sys/devices/system/clocksource/clocksource0/current_clocksource10.7 常见错误
| 错误 | 现象 | 修正 |
|---|---|---|
用init_timer() | 编译失败(7.2.5 已删除) | 改用timer_setup() |
定时器回调原型仍是void (*)(unsigned long) | 编译错误/类型不匹配 | 改成void (*)(struct timer_list *)+from_timer() |
卸载时只用del_timer() | UAF:回调可能还在跑 | 用del_timer_sync()或timer_shutdown_sync() |
| 在定时器回调中睡眠 | BUG: scheduling while atomic | 改用 delayed_work |
在硬中断中disable_irq() | 死锁 | 用disable_irq_nosync() |
enable_irq()与disable_irq()不配对 | Unbalanced enable警告 | 严格计数配对 |
| 共享中断不判断来源 | 别的设备中断被误处理 | 返回IRQ_NONE |
忘记tasklet_kill() | 卸载后软中断执行已释放内存 | 卸载前tasklet_kill() |
用>比较 jiffies | 回绕后判断错误 | 用time_after()/time_before() |
在硬中断里kmalloc(GFP_KERNEL) | 睡眠警告 | 用GFP_ATOMIC或延后到线程 |
IRQF_ONESHOT缺失导致中断风暴 | 中断线反复触发 | 加IRQF_ONESHOT |
10.8 本章实验清单
| 实验 | 目的 | 关键观察 |
|---|---|---|
| gpio-sim + gpio-keys | 无硬件模拟按键中断 | /proc/interrupts计数增长 |
| 自定义 GPIO 中断驱动 | 理解gpiod_to_irq()+ threaded IRQ | dmesg打印按键次数 |
| second 秒字符设备 | 理解 timer + 等待队列 | 每秒一个递增数字 |
| delayed_work 版本 | 对比 timer 与 workqueue | work 中可睡眠 |
| hrtimer 高频定时 | 理解高分辨率定时器 | /proc/timer_list可见 |
| 共享中断观察 | 理解IRQF_SHARED | /proc/interrupts同一 IRQ 多 handler |
usleep_range功耗对比 | 理解长延时选择 | 与udelay的 CPU 占用差异 |
10.9 总结
- 中断必须分两半:硬中断只做最少工作,重活放 threaded IRQ 或 workqueue。
- 7.2.5 下
init_timer()已删除,必须用timer_setup();回调签名已改变。 - 卸载定时器必须用
timer_shutdown_sync()(或del_timer_sync()),防止回调重启定时器。 - tasklet 只用于遗留代码;新代码用 threaded IRQ / workqueue。
- 短延时用
udelay,长延时用usleep_range/msleep,等待事件用wait_event_*。 - QEMU 中可用
gpio-sim+gpio-keys完整复现原书的按键中断实验,无需任何真实硬件。