Linux内核驱动开发——中断与定时器

Linux 内核驱动中断架构

Linux 中断分为硬件中断 (外设产生,IRQ)和软中断 (内核软件触发),现代内核常用中断上下文 / 线程化中断分离:

硬件中断 → 中断顶半部(快速处理,禁止抢占)→ 底半部(耗时工作,可调度)

核心原则:顶半部快进快出,只做紧急硬件寄存器操作;耗时逻辑放到底半部 中断上下文特点:不能休眠、不能调用会阻塞的函数、不能访问用户空间、不能直接用 mutex。

tasklet

基于softirq软中断 实现的底半部,softirq 是内核最底层底半部,驱动一般不直接用 softirq,而是用封装好的 tasklet。 tasklet 内核保证:同一个 tasklet 不会在多个 CPU 上并发运行简化竞态处理。

核心特点

✅ 上下文:中断上下文 ❌ 不能休眠 ,不能调用 msleep()、mutex_lock()、wait_event、copy_to_user

✅ 可在 ISR(顶半部)中调用 tasklet_schedule() 触发调度

✅ 优先级高,执行时机:硬件中断返回后,马上执行

✅ 同一份 tasklet,即使多次 schedule,最多只执行一次

❗❗❗❗tasklet 里调用会休眠的函数 → Oops 内核崩溃。❗❗❗❗

demo示例

复制代码
DECLARE_TASKLET(mytasklet, mytasklet_func, 0);
void mytasklet_func(unsigned long data)
{
    //底半部代码,不能休眠
}
//在ISR中触发调度
tasklet_schedule(&mytasklet);

workqueue

内核进程上下文,内核提前创建一批 worker 内核线程,你把任务封装成 work,交给 worker 线程执行。

核心特点

✅ 上下文:进程上下文

✅ 允许休眠 ,可以用msleep、mutex、wait_event、copy_to_user

✅ 可以并发多个 CPU 上同时跑多个 work

✅ 执行时机:worker 线程被调度运行,延迟不确定,比 tasklet 慢

demo示例

复制代码
struct work_struct mywork;
void mywork_func(struct work_struct *work)
{
    //可休眠
}
//初始化
INIT_WORK(&mywork, mywork_func);
//ISR中提交工作
schedule_work(&mywork);

上下文 = 程序运行时的一套环境(寄存器、栈、内存映射、权限等)

  • 进程上下文 :属于某个用户进程 / 内核线程,是进程在内核态执行代码;可以休眠、可以被抢占。
  • 中断上下文 :硬件打断 CPU 正常执行,临时跑中断处理代码;不属于任何进程,不能休眠。

进程上下文(Process Context)

当应用程序触发系统调用(read/write/ioctl),进程从用户态陷入内核态,此时内核代码代表这个进程运行,这就是进程上下文。 也包括内核线程(workqueue 的 worker 线程),同样属于进程上下文。

中断上下文(Interrupt Context)

外设硬件触发中断 → CPU 停下当前正在跑的代码,切换到中断栈,执行中断处理函数(ISR、tasklet)。 中断不属于任何进程!没有对应的 PID。 哪怕中断打断的是 A 进程,中断代码本身不是 A 进程的代码,只是临时借用 CPU。

Linux 内核 阻塞 & 等待队列

Linux 字符设备驱动,实现阻塞读 / 写 ,用户态 read() 没有数据时进程休眠,有数据后唤醒。

核心原理

等待队列本质是进程链表,存放等待某个条件的进程。

  1. 进程调用wait_event*(),判断条件:条件成立直接返回;不成立则把当前进程加入等待队列,置为休眠状态,让出 CPU(阻塞)。
  2. 别的内核代码(中断 / 其他线程)满足条件后调用wake_up*(),遍历等待队列,唤醒休眠进程。
  3. 被唤醒后进程再次检查条件

初始化等待队列头两种方式

静态初始化(宏)

复制代码
DECLARE_WAIT_QUEUE_HEAD(wq);

动态初始化函数

复制代码
wait_queue_head_t wq;
init_waitqueue_head(&wq);

等待休眠 API(wait_event 系列)

复制代码
// 1. 不可中断休眠:不能被信号打断,慎用!
wait_event(wq, condition);

// 2. 可中断休眠:收到信号会返回非0,驱动最常用
wait_event_interruptible(wq, condition);
// 返回值:0=正常唤醒;负数(-ERESTARTSYS)=被信号打断

// 3. 带超时的可中断阻塞,超时自动唤醒
wait_event_interruptible_timeout(wq, condition, timeout);

按键中断处理------tasklet下半部线程实现demo

  1. 定义 tasklet 结构体

  2. 编写 tasklet 处理函数(下半部函数)

  3. 中断上半部(中断服务函数 ISR)

    //保存中断号
    static int irq_key_num;
    //等待队列唤醒标志位
    static int condition;
    //定义并初始化等待队列的头
    DECLARE_WAIT_QUEUE_HEAD(wq);

    //定义底板部的结构体
    static struct tasklet_struct tsk;

    //底半部处理函数
    void tasklet_func(unsigned long data)
    {
    //等待条件置1
    condition = 1;
    //唤醒阻塞在wq等待队列的进程
    wake_up_interruptible(&wq);
    //打印传入参数
    printk("tasklet data = %ld\n",data);

    }

    /*中断上半部服务函数
    *irq_num:中断号
    *dev:中断传参
    *

    • */
      static irqreturn_t key_irq_handler(int irq_num, void *dev)
      {
      if(100 != *(int *)dev)
      return IRQ_NONE;

      //调度tasklet触发底半部,快速退出中断上下文
      tasklet_schedule(&tsk);
      printk("irq_num = %d dev = %d\n",irq_num,*(int *)dev);

      return IRQ_HANDLED;
      }

  4. 修改驱动层提供给用户层的函数接口

    static ssize_t read(struct file * file, char __user * buf, size_t size, loff_t * offset)
    {
    printk("key read start......\n");
    //重置唤醒标志位,进入阻塞
    condition = 0;
    //阻塞等待
    wait_event_interruptible(wq,condition);
    printk("key read end........\n");

    复制代码
     return 0;

    }

  5. 修改驱匹配处理函数,加入对tasklet的初始化

    static int probe(struct platform_device * pdev)
    {
    int ret = 0;
    /查找设备节点/
    struct device_node * dev_node = of_find_node_by_path("/test_irqkey");
    if(NULL == dev_node)
    {
    printk("of_find_node_by_path not found /test_irqkey\n");
    return -1;
    }

    复制代码
     irq_key_num = irq_of_parse_and_map(dev_node,0);
     printk("irq_key_num = %d\n",irq_key_num);
    
     ret = request_irq(irq_key_num,key_irq_handler,IRQF_TRIGGER_FALLING,"key_irq",&arg);
     
     if(ret < 0)
     {
     	goto err_request_irq;
     }
    
     /*注册设备*/	
     ret = misc_register(&misc);
     if(ret < 0)
     	goto err_register;
     
     tasklet_init(&tsk,tasklet_func,1000);
    
     printk("key_init ********************\n");
     return ret;	

    err_register:
    misc_deregister(&misc);
    err_request_irq:
    disable_irq(irq_key_num);
    free_irq(irq_key_num,&arg);
    printk("key_init faikey********************\n");
    return ret;

    }

  6. 注销tasklet资源,添加在rmmove回调处理函数中

    static int my_remove(struct platform_device *pdev)
    {
    // 第一步:kill tasklet,放在最前面!
    tasklet_kill(&tsk);

    复制代码
     // 第二步:注销杂项设备
     misc_deregister(&misc);
    
     // 第三步:释放中断
     free_irq(irq_key_num, &arg);
    
     return 0;

    }

按键中断处理------workqueue下半部线程实现demo

定义工作结构体 + 工作处理函数

复制代码
   // 定义等待队列头,用于实现阻塞等待与唤醒机制
   static wait_queue_head_t wq;

   // 定义工作队列项结构体,用于workqueue中断下半部
   static struct work_struct work;

   /**
    * @brief workqueue底半部处理函数,内核worker线程回调执行
    * @param work 工作项结构体指针,workqueue框架自动传入
    */
   static void work_func(struct work_struct *work)
   {
       ssleep(1);                // 睡眠1秒,work运行在进程上下文,允许睡眠(tasklet不可以)
       // 将条件标志置1,作为等待队列的唤醒条件
       condition = 1;
       wake_up_interruptible(&wq); // 唤醒等待队列wq上,可被信号中断的阻塞进程
       // 打印日志,标记下半部函数执行
       printk("work func.....\n");
   }

另外还需要修改提供给用户态调用的接口函数,进入阻塞等待

复制代码
   static ssize_t read(struct file * file, char __user * buf, size_t size, loff_t * offset)
   {
   	printk("key read start......\n");
   	//重置唤醒标志位,进入阻塞
   	condition = 0;
   	//阻塞等待
   	wait_event_interruptible(wq,condition);
   	printk("key read end........\n");

   	return 0;
   }

中断上半部(ISR) 硬件中断触发,进入中断服务函数

复制代码
   /*中断上半部服务函数
    *irq_num:中断号
    *dev:中断传参,request_irq注册时传入的设备参数
    *
    * */
   static irqreturn_t key_irq_handler(int irq_num, void *dev)
   {
   	// 判断传入参数的值是否等于100,不匹配说明不是本中断的处理函数,直接返回IRQ_NONE
   	if(100 != *(int *)dev)
   		return IRQ_NONE;
   	
   	// 调度工作项,提交work到内核工作队列,触发中断下半部work_func执行
   	schedule_work(&work);	
   	printk("irq_num = %d dev = %d\n",irq_num,*(int *)dev);

   	return IRQ_HANDLED; // 中断正常处理完毕
   }

修改驱动初始化函数,加入队列初始化部分,work结构体初始化部分

复制代码
   /**
    * @brief platform驱动匹配成功后执行的探测函数
    * @param pdev platform设备指针
    * @return 成功返回0;失败返回负的错误码
    */
   static int probe(struct platform_device * pdev)
   {
   	int ret = 0;
   	/*查找设备节点:根据设备树节点路径找到/test_irqkey节点*/
   	struct device_node * dev_node = of_find_node_by_path("/test_irqkey");
   	if(NULL == dev_node)
   	{
   		printk("of_find_node_by_path not found  /test_irqkey\n");
   		return -1;	
   	}
   	
   	// 从设备树节点解析并映射得到内核可用的中断号
   	irq_key_num = irq_of_parse_and_map(dev_node,0);
   	printk("irq_key_num = %d\n",irq_key_num);

   	// 申请中断:下降沿触发,绑定中断处理函数key_irq_handler,传参&arg
   	ret = request_irq(irq_key_num,key_irq_handler,IRQF_TRIGGER_FALLING,"key_irq",&arg);
   	
   	if(ret < 0)
   	{
   		goto err_request_irq;
   	}

   	/*注册杂项设备,生成/dev下设备文件,关联fops,供用户态访问*/	
   	ret = misc_register(&misc);
   	if(ret < 0)
   		goto err_register;

   	init_waitqueue_head(&wq);  //初始化等待队列头,用于read阻塞唤醒

   	INIT_WORK(&work,work_func); //初始化work工作项,绑定下半部处理函数work_func

   	printk("key_init ********************\n");
   	return ret;	//探测成功返回0


   //错误处理分支:misc_register注册失败
   err_register:
   	misc_deregister(&misc);   //注销杂项设备
   //错误处理分支:request_irq申请中断失败
   err_request_irq:
   	disable_irq(irq_key_num); //关闭中断
   	free_irq(irq_key_num,&arg); //释放中断资源
   	printk("key_init faikey********************\n");
   	return ret;
   }

Linux 内核定时器基础概念

1. 作用

内核定时器是内核用来在未来某个指定时间点执行一段回调函数的机制,属于软件定时器,不是硬件定时器。

特点:单次触发,定时到期执行一次,执行完自动失效;如果要周期性定时,需要在回调内部重新启动定时器。

2. 核心基础:节拍 jiffies & HZ

  1. HZ :系统节拍率,编译内核时配置。代表每一秒产生多少次系统时钟中断(tick)
    • HZ=100 → 1tick = 10ms
    • HZ=250 → 1tick = 4ms
    • HZ=1000 →1tick =1ms
  2. jiffies :内核全局unsigned long变量。每来一次 tick 中断,jiffies+1 ,用来记录系统运行的节拍总数。
    • 定时器的到期时间expires,就是jiffies 的绝对目标值
    • 工具函数:msecs_to_jiffies(ms)、usecs_to_jiffies(us),把毫秒 / 微秒转换成节拍数
    • 写法示例:jiffies + msecs_to_jiffies(2000) → 当前节拍 + 2000ms 后的节拍,作为到期时间

3. timer_list(低分辨率定时器)

内核传统定时器,基于 jiffies 实现,精度受 HZ 限制,毫秒级。

  • 结构体struct timer_list:描述一个定时器实例
    • expires:到期的绝对 jiffies 值
    • function:到期执行的回调函数
    • flags:标志位

相关函数接口:

函数 作用 特点
timer_setup 初始化定时器、绑定回调 只做初始化,不启动
add_timer 启动定时器 需要预先设置 expires
mod_timer 更新超时时间 激活 / 未激活都能用,推荐首选
del_timer 删除定时器 不等待回调执行完成
del_timer_sync 同步删除 等待回调结束,exit 函数首选
timer_pending 查询状态 判断定时器是否挂在时间轮

代码实现demo

复制代码
static struct timer_list timer;

// 回调函数
static void timer_handler(unsigned long data)
{
	printk("timer_handler  data = %ld\n", data);
	// 重新设置到期时间,实现周期10ms
	mod_timer(&timer, jiffies + msecs_to_jiffies(10));
}

static int __init timer_init(void)
{
	// 1. 设置到期绝对jiffies时间:当前jiffies +10ms
	timer.expires = jiffies + msecs_to_jiffies(10);
	// 2. 指定回调函数
	timer.function = timer_handler;
	// 3. 传给回调的参数data
	timer.data = 100;
	// 4. 初始化timer_list结构体(链表等成员)
	init_timer(&timer);
	// 5. 添加定时器到内核时间轮,开始计时
	add_timer(&timer);
	printk("timer_init  ...\n");
	return 0;
}

static void __exit timer_exit(void)
{
	del_timer(&timer);
	printk("timer_exit  ...\n");
}

module_init(timer_init);
module_exit(timer_exit);
MODULE_LICENSE("GPL");
相关推荐
2301_808414382 小时前
Linux中动静态库的理解
linux·运维·服务器
分布式存储与RustFS3 小时前
模型 checkpoint 放对象存储:分片上传、断点续传与残留清理
运维·云原生·开源·对象存储·分布式存储·s3·性能基准
M78佐菲3 小时前
ARM学习笔记(11)
linux·arm开发·笔记·嵌入式硬件·学习
M78佐菲3 小时前
ARM学习笔记(10)
linux·arm开发·笔记·嵌入式硬件·学习
半仙白桑3 小时前
内核篇第十五讲:system-V共享内存
linux·共享内存·shmget·shmat·shmdt·shmctl
一条破秋裤4 小时前
Linux 线程创建:pthread_create 与基本回收
java·linux·运维
s6516654965 小时前
IDT和GDT是什么
linux
@嵌入式扫地僧5 小时前
ECC 编程智能体嵌入式适配实战:ESP32/RK3588 驱动开发提效 60%+ 指南
驱动开发·嵌入式开发·代码优化·ai辅助开发·编程智能体
Android系统攻城狮6 小时前
Linux Gstreamer深度解析之gst_audio_decoder_set_estimate_rate调用流程与实战(四十五)
linux·gstreamer音视频·音视频进阶