Linux 驱动框架---驱动中的中断
在mcu开发中中断就是用来处理执行过程中发生的一些需要处理事件(事件有外部的和内部的),停止当前正在处理的事情转而去执行中断服务,以完成必要的事件的处理。在Linux中中断一样是如此使用,但是基于常见的中断控制器的特性比如不支持中断嵌套,不支持中断嵌套的CPU在处理一个中断时是无法响应其他中断的,所以就会导致整个系统的实时性就比较差,所以在Linux的思路就是尽量减短中断上下文执行的指令数量,把一些必须在中断上下文执行的代码放在中断上下文中执行,而一些儿可以适当推迟的处理延迟处理。这就是Linux的中断处理程序的机制,将中断处理分为上半段(顶半部)和下半段(低半部)。和硬件有关的实时性要求比较高的的处理过程在顶半部执行处理,而其余的放到底半部处理。低半部的实现机制由有许多种如软中断,tasklet、work queue等。这里可以参考我的另一篇博客。
Linux中断编程
这里不考虑Linux中断子系统的初始化等相关的内容,只了解具体的驱动编程需要的基本内容,驱动中使用中断需要申请并指定处理接口,完成指定的处理接口函数最重要的是实现低半部机制,而好消息是现在Linux内核已经将中断在大多数情况下线程化了,所以低半部很少使用但不是不使用。
申请中断
int request_irq(int irq,irq_handler handler,unsigned long flags,const char * name,void *dev);
其中irq为中断号,这里是Linux中断子系统的中断号;
handler 为中断处理接口函数;
flags为中断的标志常有如下;
//指定中断触发类型:边沿和高低电平 #define IRQF_TRIGGER_NONE 0x00000000 #define IRQF_TRIGGER_RISING 0x00000001 #define IRQF_TRIGGER_FALLING 0x00000002 #define IRQF_TRIGGER_HIGH 0x00000004 //高可用有效。新增加的标志 #define IRQF_TRIGGER_LOW 0x00000008 #define IRQF_TRIGGER_MASK(IRQF_TRIGGER_HIGH | IRQF_TRIGGER_LOW | \ IRQF_TRIGGER_RISING | IRQF_TRIGGER_FALLING) #define IRQF_TRIGGER_PROBE 0x00000010 /* *这些标志仅由内核用作irq处理例程的一部分。 *首先考虑在共享中断中注册 *性能原因) */ #define IRQF_DISABLED 0x00000020 // IRQF_DISABLED-调用动作处理程序时保持禁用irqs #define IRQF_SAMPLE_RANDOM 0x00000040 // IRQF_SAMPLE_RANDOM-irq用于提供随机生成器 #define IRQF_SHARED 0x00000080 // IRQF_SHARED-允许在多个设备之间共享irq #define IRQF_PROBE_SHARED 0x00000100 // IRQF_PROBE_SHARED-由呼叫者在期望发生共享不匹配时设置 #define IRQF_TIMER 0x00000200 // IRQF_TIMER-将该中断标记为定时器中断的标志 #define IRQF_PERCPU 0x00000400 // IRQF_PERCPU-中断是每个CPU #define IRQF_NOBALANCING 0x00000800 // IRQF_NOBALANCING-将该中断从irq平衡中排除的标志 #define IRQF_IRQPOLL 0x00001000 // IRQF_IRQPOLL-中断用于轮询(仅中断
name 为创建文件系统/proc/irq 下的文件接口需要。dev 在共享中断时必须传入参数,因为是区别处理不同的设备中断的区分标志。这个函数执行成功返回0,其余则是失败。这种中断的申请后使用完成是需要释放的。释放的接口就是free_irq():
void free_irq(unsigned int irq,void * dev_id);
irq 为Linux中断子系统的中断号。
dev_id 在共享中断时常为设备描述。
除此之外还有一个自动释放中断的接口就是linux内核常见的devm_xxx接口,第一个参数就是关联的device。
int devm_request_irq(struct device* dev,unsigned int irq ,irq_hander_t handler,unsigned long irqflags,const char* name ,void * dev_id)
使能和屏蔽中断
有时候需要临时关闭一个中断,某一个CPU上的中断,甚至整个系统的中断Linux提供的API如下:
//使能或关闭某一个中断 void disable_irq(unsigned int irq); void disable_irq_nosync(unsigned int irq); void enable_irq(unsigned int irq);
其中disable_irq_nosync 接口区别于disable_irq的是前者直接返回,而后者会等待当前的中断处理完成,所以也就意味着disable_irq接口会导致休眠阻塞所以不能在中断上下文中调用。除此之外内核还提供了关闭和使能指定CPU上的中断的接口,这些接口的实现依赖于具体的硬件:
//关闭调用接口的当前CPU上的所有中断 local_irq_disable() //使能调用接口的当前CPU上的所有中断 local_irq_enable()
如果需要关闭前保存中断控制寄存器的状态并在恢复中断时恢复寄存器的配置则可以使用如下的接口:
//关闭并保存寄存器状态 local_irq_save(flags) //开启并恢复寄存器状态 loacl_irq_restore(flags)
中断底半部
为了改善Linux内核的实时性内核将中断相应分出了顶半部和底半部,低半部的实现机制主要有tasklet,工作队列,软中断和线程化。线程化是现在新内核默认的处理方式,在中断申请注册的过程中如果没有强制不能线程化或没有给两个中断服务接口(一个非线程化一个线程化)内核会默认将注册的非线程化的接口进行线程化具体参考 Linux内核实现透视---硬中断。其次是软中断这是一个依赖内核线程和内核数据结构的子系统,软件接口主要是完成内核软中断数据结构的组织,进而使软中断内核线程可以维护处理需要处理调用的接口函数。tasklet是一种依赖软中断实现的低半部机制,他的实现是在软中断中划分了一部分线程专门用于处理tasklet数据结构中待处理的接口(通过API接口加入的)所以他和软中断的特性一样:会并发的执行在不同的CPU上,且执行在软中断上下文具体参考Linux内核实现透视---软中断&Tasklet。最后是工作队列这是Linux内核为中断低半部设计的另一机制,他的特点是对资源的消耗更加的少并且work只会线性的串行执行而不会并发具体参考Linux内核实现透视---工作队列。接下来简单的学习一下内核驱动开发常用的底半部机制的API接口其中软中断很少直接使用:
tasklet
使用比较简单,基本就是定义好处理函数后定义tasklet并初始化将处理函数绑定到定义的tasklet上后就需要通过接口进行调度,之后就等待内核调度软中断处理线程来执行了。
//数据类型 struct tasklet_struct { struct tasklet_struct *next; unsigned long state; atomic_t count; void (*func)(unsigned long); unsigned long data; }; //tasklet 处理接口的类型 void tasklet_func(unsigned long); //定义并且初始化 DECLARE_TASKLET(name,function,data) //调度 实际上将tasklet 对象加入内核数据链表由内核决定执行时机 tasklet_schedule(struct tasklet_struct* )
工作队列
工作队列的使用分两种情况一种是使用内核定义工作队列,还有一种是自己定义工作队列,两种使用方式要根据使用的具体情况决定使用哪一种。常用的是直接使用系统内定义的默认共享工作队列,因为通常情况下这些内建的工作队列和处理线程已经足够可以及时处理这些需求,除非是有特别多的工作会加入到工作队列时,其他一般不需要自己定义工作队列。下面就是使用默认工作队列的常用API。
//定义工作 struct work_struct xxx_work; //定义服务接口函数 void xxx_func(struct work_struct* work); //初始化工作队列 其实就是绑定处理函数接口 INIT_WORK(struct work_queue* wq,work_func); //将工作加入到工作队列等待调度 schedule_work(struct work_struct * work); //确保没有工作队列入口在系统中任何地方运行,会等待所有在处理的处理完成 void flush_scheduled_work(void); //延时一定时间后再调度执行工作 int schedule_delayed_work(struct delayed_struct *work, unsigned long delay); //取消加入工作队列的工作 int cancel_delayed_work(struct delayed_struct *work);
细心点会发现取消的接口只放了延时工作队列的,因为非延时的工作队列通常在执行了调度请求后很快就会执行了,此时取消常常是用处不大的。当然如果发起调度和取消调度都在中断上下文肯定就是可以的,具体的原因如果理解不了可以看前面的Linux内核实现透视---工作队列这一篇。最后再来看一下自定义工作队列的使用方法因为使用内核提供的共享列队,列队是保持顺序执行的,做完一个工作才做下一个,如果一个工作内有耗时大的处理如阻塞等待信号或锁,那么后面的工作都不会执行。如果确保工作队列及时执行,那么可以创建自己的工作者线程,所有工作可以加入自己创建的工作列队,列队中的工作运行在创建的工作者线程中。创建工作列队使用3个宏 成功后返回workqueue_struct *指针,并创建了工作者线程。三个宏主要区别在后面两个参数singlethread和freezeable,singlethread为0时会为每个cpu上创建一个工作者线程,为1时只在当前运行的cpu上创建一个工作者线程。freezeable会影响内核线程结构体thread_info的PF_NOFREEZE标记见代码片段;如果设置了PF_NOFREEZE这个flag,那么系统挂起时候这个进程不会被挂起。
if (!cwq->freezeable) current->flags |= PF_NOFREEZE; set_user_nice(current, -5);
使用接口
//多处理器时会为每个cpu创建一个工作者线程 #define create_workqueue(name) __create_workqueue((name), 0, 0) //只创建一个工作者线程,系统挂起时线程也挂起 #define create_freezeable_workqueue(name) __create_workqueue((name), 1, 1) //只创建一个工作者线程,系统挂起是线程线程不挂起 #define create_singlethread_workqueue(name) __create_workqueue((name), 1, 0) //以上三个宏调用__create_workqueue函数 extern struct workqueue_struct *__create_workqueue(const char *name,int singlethread, int freezeable); //释放创建的工作列队资源 void destroy_workqueue(struct workqueue_struct *wq) //延时调用指定工作列队的工作 queue_delayed_work(struct workqueue_struct *wq,struct delay_struct *work, unsigned long delay) //取消指定工作列队的延时工作 cancel_delayed_work(struct delay_struct *work) //将工作加入指定工作列队进行调度 queue_work(struct workqueue_struct *wq, struct work_struct *work) //等待列队中的任务全部执行完毕。 void flush_workqueue(struct workqueue_struct *wq);
Linux内核的中断开发使用到这里基本常用的接口函数都覆盖到了,具体的实现机制单独有时间的时候在深入学习研究。到时候放到内核实现透视章节记录。
参考:
宋宝华:Linux设备驱动开发详解
BLOG:https://blog.csdn.net/fontlose/article/details/8286445