10-中断与时钟 10.1 中断与定时器中断是设备主动通知 CPU 的机制定时器是内核在未来某时刻执行某段代码的机制。二者共同点回调在原子上下文软中断上下文执行不能睡眠。7.2.5 下中断子系统的层次关键概念概念说明IRQ numberLinux 逻辑中断号由 irqchip 的 IRQ domain 映射得到hwirq硬件中断号GIC/APIC 的编号irq_desc每个 Linux IRQ 的描述符include/linux/irqdesc.hirqaction每个注册的 handlerkernel/irq/manage.c顶半部top half硬中断上下文极短、不能睡眠底半部bottom half延后执行的重活见 10.3.310.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允许多个设备共享该 IRQIRQF_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.5threaded 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()的 flagsinclude/linux/workqueue.hflag含义WQ_UNBOUND不绑定 CPU适合可能长时间运行的 workWQ_MEM_RECLAIM内存回收路径可用设备驱动几乎总要加WQ_HIGHPRI高优先级WQ_CPU_INTENSIVECPU 密集型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。中断上下文的资源限制必须遵守操作硬中断软中断/taskletthreaded IRQ/workqueuekmalloc(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_SIMm CONFIG_KEYBOARD_GPIOm CONFIG_CONFIGFS_FSy CONFIG_IRQ_SIMyguest 中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/gpiogetlibgpiod操作虚拟线配合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_workstruct 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_listdelayed_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_*()精确高分辨率延时hrtimerschedule_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_ONESHOT10.8 本章实验清单实验目的关键观察gpio-sim gpio-keys无硬件模拟按键中断/proc/interrupts计数增长自定义 GPIO 中断驱动理解gpiod_to_irq() threaded IRQdmesg打印按键次数second 秒字符设备理解 timer 等待队列每秒一个递增数字delayed_work 版本对比 timer 与 workqueuework 中可睡眠hrtimer 高频定时理解高分辨率定时器/proc/timer_list可见共享中断观察理解IRQF_SHARED/proc/interrupts同一 IRQ 多 handlerusleep_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-simgpio-keys完整复现原书的按键中断实验无需任何真实硬件。