一、Linux内核中断系统简介
1、网络参考资料: ① 【原创】Linux中断子系统(一)-中断控制器及驱动分析 - LoyenWang - 博客园 2、Linux内核中断介绍: linux内核提供了完善的中断框架;我们只需要申请中断,然后注册中断处理函数即可,使用非常方便,不需要一系列复杂的寄存器配置。linux内核中断属于对GIC中断管理器硬件中断的逻辑扩展与映射;linux将硬件中断ID通过线性映射、树状映射和直接映射等方式转换为逻辑中断ID。 |
/
二、Linux内核提供的API函数
1、中断处理函数: linux申请使用中断时,需要指定目标中断处理函数。处理函数的基本结构如下:irqreturn_t (*irq_handler_t)(int,void*)。 参数解释: ① irqreturn_t原型:#include <linux/irqreturn.h> enum irqreturn { IRQ_NONE = (0 << 0), IRQ_HANDLED = (1 << 0), IRQ_WAKE_THREAD = (1 << 1), }; typedef enum irqreturn irqreturn_t; 作为函数的返回值,一般中断服务函数的返回值使用形式如下: return IRQ_RETVAL(IRQ_HANDLED) ② int 作为传入参数,一般是触发中断函数的具体中断号 ③ void* 作为传入参数,一般传输设备数据结构对象地址,用于支持面向对象编程 /// 2、request_irq函数: 函数原型:#include <linux/irq.h> int __must_check request_irq(unsigned int irq,irq_handler_t handler,unsigned long flags,const char *name,void *dev); //向Linux内核请求使用中断 参数解释: ① unsigned int irq:要申请使用的中断号(逻辑ID) ② irq_handler_t handler:中断处理函数的接口指针,当中断事件发生时,函数将被执行 ③ unsigned long flags:中断标志,就是配置中断触发条件(在linux/interrupt.h有定义);常用的标志如下: ④ const char *name:中断名字,设置以后可以在/proc/interrupts 文件中看到对应的中断名字 ⑤ void *dev:如果将 flags 设置为 IRQF_SHARED 的话,dev 用来区分不同的中断,一般情况下将dev 设置为设备结构体,dev 会传递给中断处理函数 irq_handler_t 的第二个参数。 返回值:0 中断申请成功,其他负值 中断申请失败,如果返回-EBUSY 的话表示中断已经被申请了。 /// 3、free_irq函数: 函数原型:#include <linux/irq.h> void free_irq(unsigned int irq,void *dev_id); //释放对应的中断 参数解释: ① unsigned int irq:要释放关闭的中断ID号 ② void *dev_id:如果中断设置为共享(IRQF_SHARED)的话,此参数用来区分具体的中断。共享中断只有在释放最后中断处理函数的时候才会被禁止掉。 /// 4、中断的使能和禁止函数: #include <linux/irq.h> #include <linux/irqflags.h> ① void enable_irq(unsigned int irq); //使能对应ID的中断 ② void disable_irq(unsigned int irq); //禁止对应ID的中断,会阻塞等待中断函数执行完成 注意事项:disable_irq会阻塞等待所有正在执行的中断服务函数执行完毕才会退出! ③ void disable_irq_nosync(unsigned int irq); //非阻塞禁止中断,即无论是否有中断服务函数正在执行,都直接禁止并返回 ④ #define local_irq_enable() //使能当前CPU的中断管理系统,但不恢复之前的系统中断状态 ⑤ #define local_irq_disable() //关闭当前CPU的中断管理系统,不响应所有中断,但也没有保存历史中断状态 ⑥ #define local_irq_save(unsigned long flags) //保存当前CPU中断状态到flags中,并关闭中断管理器 ⑦ #define local_irq_restore(unsigned long flags) //从flags中恢复当前CPU的中断状态,并使能中断管理器 /// 5、获取逻辑中断号: #include <linux/of_irq.h> #include <linux/gpio.h> ① unsigned int irq_of_parse_and_map(struct device_node *dev,int index); //从设备树目标节点上获取中断号,其实是读取转换interrupts的属性信息 ② int gpio_to_irq(unsigned gpio); //获取指定gpio口的逻辑中断ID号 /// |
/
三、Linux内核中断的上半部与下半部
1、基本的介绍: 上半部:上半部就是中断处理函数,那些处理过程很快,不会占用很长时间的处理就可以放在上半部分完成。 下半部:如果处理过程比较耗时,那么就将这些比较耗时的代码提取出来,交给下半部分去执行,这样中断处理函数就会快进快出。 Linux内核将中断执行任务分为上半部和下半部的主要目的就是实现中断处理函数的快进快出,那些对时间敏感、执行速度快的操作可以放到中断处理函数中,也就是上半部;剩下的所有工作都可以放到下半部去执行。关于中断下半部分的实现机制,linux内核提供了多种方式。 / 2、软中断实现下半部: Linux内核使用结构体softirq_action表示软中断,softirq_action结构体定义在文件include/linux/interrupt.h中,用于指定软中断执行的函数;linux内核默认提供了10个软中断,在include/linux/interrupt.h中有一个NR_SOFTIRQS的枚举量,枚举了这10种不同的软件中断;在kernel/softirq.c里实现了这10个软中断执行函数,是一个结构体数组struct softirq_action softirq_vec[NR_SOFTIRQS],这个数据是全局变量,也就是说SOC中的所有CPU执行软中断的函数都是同一个,但每个CPU都有自己的触发和控制机制。并且只执行自己所触发的软件中断。软中断在使用前,需要事先跟随内核进行静态编译注册!! 结构体softirq_action的原型: #include <linux/interrupt.h> struct softirq_action { void (*action)(struct softirq_action *); //软中断执行函数入口 }; 软件中断函数接口数组:<kernel/softirq.c> static struct softirq_action softirq_vec[NR_SOFTIRQS]; //这是全局数组,所有CPU共享 枚举变量 NR_SOFTIRQS: #include <linux/interrupt.h> enum { HI_SOFTIRQ=0, TIMER_SOFTIRQ, NET_TX_SOFTIRQ, NET_RX_SOFTIRQ, BLOCK_SOFTIRQ, BLOCK_IOPOLL_SOFTIRQ, TASKLET_SOFTIRQ, SCHED_SOFTIRQ, HRTIMER_SOFTIRQ, RCU_SOFTIRQ, /* Preferable RCU should always be the last softirq */
NR_SOFTIRQS }; 要使用软中断,就需要事先在内核静态编译注册;注册函数为open_softirq:<kernel/softirq.c> void open_softirq(int nr,void (*action)(struct softirq_action *)); //就是往全局数组softirq_vec的指定nr元素挂载中断执行函数 注册好后,当需要调用执行时,使用函数raise_softirq:<kernel/softirq.c> void raise_softirq(unsigned int nr); //保存系统中断状态后,关闭系统中断;执行对应软件中断后,再恢复系统中断 软中断必须在编译的时候静态注册!Linux 内核使用 softirq_init 函数初始化软中断:<kernel/softirq.c> void __init softirq_init(void) { int cpu; for_each_possible_cpu(cpu) { per_cpu(tasklet_vec, cpu).tail = &per_cpu(tasklet_vec, cpu).head; per_cpu(tasklet_hi_vec, cpu).tail = &per_cpu(tasklet_hi_vec, cpu).head; } /* linux内核默认注册启用了tasklet和hi软中断 */ open_softirq(TASKLET_SOFTIRQ, tasklet_action); open_softirq(HI_SOFTIRQ, tasklet_hi_action); } / 3、tasklet实现下半部:(上面那种方法实际上是将tasklet的进一步封装运用!!) tasklet是利用软中断实现的另外一种下半部机制,可以将tasklet结构体作为链表循环调度。Linux内核使用结构体tasklet_struct来描述一个tasklet: #include <linux/interrupt.h> struct tasklet_struct { struct tasklet_struct *next; //支持结构体链表,但如果调用函数tasklet_schedule,每次执行一次,都会被设为NULL unsigned long state; //被执行时,会被置1,否则为0 atomic_t count; //计数器,可以记录对tasklet的引用次数 void (*func)(unsigned long); //tasklet具体执行的函数 unsigned long data; //传入执行函数的参数 }; tasklet_struct初始化函数tasklet_init:<kernel/softirq.c> void tasklet_init(struct tasklet_struct *t,void (*func)(unsigned long),unsigned long data) { t->next = NULL; t->state = 0; atomic_set(&t->count, 0); t->func = func; t->data = data; } 也可以使用宏定义来DECLARE_TASKLET来初始化: #include <linux/interrupt.h> #define DECLARE_TASKLET(name,func,data) \ struct tasklet_struct name = { NULL, 0, ATOMIC_INIT(0), func, data } 调用函数tasklet_schedule就可以指定的tasklet在合适的时间被系统自动调用执行: #include <linux/interrupt.h> void tasklet_schedule(struct tasklet_struct *t); / 4、工作队列实现下半部: 工作队列是另外一种下半部执行方式。工作队列在进程的上下文执行,将要推后的工作交给一个内核线程去执行;因此,工作队列允许睡眠或重新调度。如果下半部程序需要睡眠,那么可以使用工作队列来实现。 在Linux内核中,每个CPU都有一个对应的工作线程,每个线程管理一个工作队列,每个队列上有若干个工作需要处理。作为驱动开发者,我们只需要创建工作节点并初始化加入队列当中即可,linux内核会轮询执行队列中所有的工作;每个工作被执行一次后将被标记为已经执行完成。 Linux内核使用work_struct结构体表示一个工作,结构体原型如下: #include <linux/workqueue.h> struct work_struct { atomic_long_t data; //用于标志工作是否被执行的数据?? struct list_head entry; work_func_t func; //工作处理函数接口 #ifdef CONFIG_LOCKDEP struct lockdep_map lockdep_map; #endif }; Linux内核使用结构体workqueue_struct来表示一个工作队列,结构体原型如下:<kernel/workqueue.c> struct workqueue_struct{ struct list_head pwqs; /* WR: all pwqs of this wq */ struct list_head list; /* PR: list of all workqueues */ struct mutex mutex; /* protects this wq */ int work_color; /* WQ: current work color */ int flush_color; /* WQ: current flush color */ atomic_t nr_pwqs_to_flush; /* flush in progress */ struct wq_flusher *first_flusher; /* WQ: first flusher */ struct list_head flusher_queue; /* WQ: flush waiters */ struct list_head flusher_overflow; /* WQ: flush overflow list */ struct list_head maydays; /* MD: pwqs requesting rescue */ struct worker *rescuer; /* I: rescue worker */ int nr_drainers; /* WQ: drain in progress */ int saved_max_active; /* WQ: saved pwq max_active */ struct workqueue_attrs *unbound_attrs; /* WQ: only for unbound wqs */ struct pool_workqueue *dfl_pwq; /* WQ: only for unbound wqs */ #ifdef CONFIG_SYSFS struct wq_device *wq_dev; /* I: for sysfs interface */ #endif #ifdef CONFIG_LOCKDEP struct lockdep_map lockdep_map; #endif char name[WQ_NAME_LEN]; /* I: workqueue name */ /* * Destruction of workqueue_struct is sched-RCU protected to allow * walking the workqueues list without grabbing wq_pool_mutex. * This is used to dump all workqueues from sysrq. */ struct rcu_head rcu; /* hot fields used during command issue, aligned to cacheline */ unsigned int flags ____cacheline_aligned; /* WQ: WQ_* flags */ struct pool_workqueue __percpu *cpu_pwqs; /* I: per-cpu pwqs */ struct pool_workqueue __rcu *numa_pwq_tbl[]; /* FR: unbound pwqs indexe d by node */ }; Linux内核使用worker结构体来表示工作者线程,结构体原型如下:<kernel/workqueue_internal.h> struct worker{ /* on idle list while idle, on busy hash table while busy */ union { struct list_head entry; /* L: while idle */ struct hlist_node hentry; /* L: while busy */ }; struct work_struct *current_work; /* L: work being processed */ work_func_t current_func; /* L: current_work's fn */ struct pool_workqueue *current_pwq; /* L: current_work's pwq */ bool desc_valid; /* ->desc is valid */ struct list_head scheduled; /* L: scheduled works */ /* 64 bytes boundary on 64bit, 32 on 32bit */ struct task_struct *task; /* I: worker task */ struct worker_pool *pool; /* I: the associated pool */ /* L: for rescuers */ struct list_head node; /* A: anchored at pool->workers */ /* A: runs through worker->node */ unsigned long last_active; /* L: last active timestamp */ unsigned int flags; /* X: flags */ int id; /* I: worker id */ /* * Opaque string set with work_set_desc(). Printed out with task * dump for debugging - WARN, BUG, panic or sysrq. */ char desc[WORKER_DESC_LEN]; /* used only by rescuers to point to the target workqueue */ struct workqueue_struct *rescue_wq; /* I: the workqueue to rescue */ }; 在实际的驱动开发中,我们只需要定义工作(work_struct)并调用函数加入队列即可。以下是Linux内核提供的接口原型: ① #define INIT_WORK(_work,_func) //初始化一个工作,指定运行的程序接口<linux/workqueue.h> 参数解释: _work:目标work_struct结构体对象地址; _func:目标工作的具体执行函数指针; 返回值:无 ② #define DECLARE_WORK(n,f) //初始化一个工作对象并返回 参数解释: n:目标work_struct结构体对象地址; f:目标工作的具体执行函数指针; 返回值:初始化后的工作结构体对象 ③ bool schedule_work(struct work_struct *work); //将工作对象加入工作队列中,等待自动被执行 参数解释: *work:需要被执行的工作对象 返回值:0=加入成功并准备好 1=工作对象忙碌或已经在队列中了; / |
/
四、Linux内核中断与设备树
新出的Linux通常都会使用到设备树,Linux内核通过读取设备树中的中断属性信息来配置中断。对于中断控制器而言,设备树绑定信息可以参考文档Documentation/devicetree/bindings/arm/gic.txt。以下以正点原子NXP开发板提供的内核源码为例,具体解释设备树中的内容: / </arch/arm/boot/dts/imx6ull.dtsi> 截图内容解读: ① compatible:compatible 属性值为“arm,cortex-a7-gic”在 Linux 内核源码中搜索“arm,cortex-a7-gic”即可找到 GIC 中断控制器驱动文件; ② #interrupt-cells:对于中断管理器描述而言,表示此中断控制器下设备的cells大小;对于中断设备而言,会使用interrupts属性描述中断信息,而#interrupt-cells则表示属性中断信息分为几个单元(通常是3);第一个=中断类型,0表示SPI,1表示PPI;第二个=硬件中断号;第三个=中断触发的条件。 ③ interrupt-controller:该属性信息为空,表示当前节点为中断控制器或中断控制设备节点 / </arch/arm/boot/dts/imx6ull.dtsi> 截图内容解释:这个是gpio5作为中断控制设备的描述节点 ① interrupts:用于描述中断设备的中断信息,对于gpio5来说共有两个,其中断类型都是SPI,触发条件都IRQ_TYPE_LEVEL_HIGH,不同之处在于中断源不同,具体定义需要查看芯片手册。 ② #interrupt-cells:这里用于表示使用此中断设备的具体外设中,interrupts属性值的cells。 </arch/arm/boot/dts/imx6ull-alientek-emmc.dts> 截图内容解释: ① interrupt-parent:指定外设使用的中断管理设备,这里使用了gpio5; ② interrupts:指定具体使用那个中断接口并设置中断触发条件,这里使用gpio5_IO00,8(bit3=1)表示低电平触发; |