Linux驱动技术(六) _内核中断
在硬件上,中断源可以通过中断控制器向CPU提交中断,进而引发中断处理程序的执行,不过这种硬件中断体系每一种CPU都不一样,而Linux作为操作系统,需要同时支持这些中断体系,如此一来,Linux中就提出了软中断的概念,也有人叫内核中断,其本质就是使用统一的方式对不同硬件中断体系中的中断号进行再映射,在操作系统中操作的中断号都是这些映射过的软中断号。就是下图中的irq_num
Linux内核中定义了名为irq_desc的中断例程描述符表来描述中断服务例程,每一个irq_desc对象数组中的下标就是软中断号。其中的struct irqaction对象描述了这个中断服务例程的中断的具体信息。
//include/linux/irqdesc.h
40 struct irq_desc {
41 struct irq_data irq_data;
42 unsigned int __percpu *kstat_irqs;
43 irq_flow_handler_t handle_irq;
44 #ifdef CONFIG_IRQ_PREFLOW_FASTEOI
45 irq_preflow_handler_t preflow_handler;
46 #endif
47 struct irqaction *action; /* IRQ action list */
48 unsigned int status_use_accessors;
49 unsigned int core_internal_state__do_not_mess_with_it;
50 unsigned int depth; /* nested irq disables */
51 unsigned int wake_depth; /* nested wake enables */
52 unsigned int irq_count; /* For detecting broken IRQs */
53 unsigned long last_unhandled; /* Aging timer for unhandled count */
54 unsigned int irqs_unhandled;
55 raw_spinlock_t lock;
56 struct cpumask *percpu_enabled;
57 #ifdef CONFIG_SMP
58 const struct cpumask *affinity_hint;
59 struct irq_affinity_notify *affinity_notify;
60 #ifdef CONFIG_GENERIC_PENDING_IRQ
61 cpumask_var_t pending_mask;
62 #endif
63 #endif
64 unsigned long threads_oneshot;
65 atomic_t threads_active;
66 wait_queue_head_t wait_for_threads;
67 #ifdef CONFIG_PROC_FS
68 struct proc_dir_entry *dir;
69 #endif
70 int parent_irq;
71 struct module *owner;
72 const char *name;
73 } ____cacheline_internodealigned_in_smp;
74
76 extern struct irq_desc irq_desc[NR_IRQS]; //NR_IRQS表示中断源的数目
//linux/interrupt.h
104 //一个irq action的描述结构
105 struct irqaction {
106 irq_handler_t handler;
107 void *dev_id;
108 void __percpu *percpu_dev_id;
109 struct irqaction *next;
110 irq_handler_t thread_fn;
111 struct task_struct *thread;
112 unsigned int irq;
113 unsigned int flags;
114 unsigned long thread_flags;
115 unsigned long thread_mask;
116 const char *name;
117 struct proc_dir_entry *dir;
118 } ____cacheline_internodealigned_in_smp;
struct irqaction
--105-->handler: 中断处理函数
--106-->name: 设备名
--107-->dev_id: 设备识别id
--109-->next: 指向下一个irqaction的指针
--110-->irq: 硬件中断号!!!
--113-->flags:IRQF_DISABLED .etc
--110-->thread_fn: 线程中断的中断处理函数
--111-->thread: 线程中断的线程指针
--114-->thread_flags: 与@thread关联的flags
--115-->thread_mask: 追踪@thread activity的位掩码
--116-->dir: 指向proc/irq/NN/name的入口指针
raw_local_irq_save(x) //禁止所有中断
raw_local_irq_enable //取消禁止中断
写中断处理程序
1. 编写中断处理函数
下面这个就是中断处理程序的原型,中断发生后,内核会将软中断号和注册时的data作为参数传入。中断处理程序ISR是在中断发生时被调用的用来处理中断的函数,不是进程上下文,在中断期间运行,不能执行可能休眠的操作,不能同用户空间交换数据,不能调用schedule()函数放弃调度
实现中断处理有一个原则:尽可能快的处理并返回,冗长的计算处理工作应该交给tasklet或任务队列在安全的时间内进行。此外,硬件系统中常使用共享中断,即多个设备共享一根线。即该(软硬)中断号可以被多个设备共享,这在实际的硬件连接中经常见到,可以节约很多资源,但是如此一来,就给软件的编写的提出了要求,内核给出的方案是一旦接收到来自一条中断线的中断,它就会循环执行所有注册到该中断线(->硬中断号->软中断号)的handler,这样,每一个handler就有义务判断到底是不是自己负责的设备来的中断,handler原型的dev_id也正是为了这个目的而存在,我们可以将该handler负责的设备的中断状态寄存器的地址作为dev_id和handler一起注册,当内核遍历执行所有的handler的时候,会将每一个中断的dev_id作为参数依次传入每一个handler,在每一个handler内部首次通过读取这个寄存器来快速判断是否是自己负责的设备发出的。这也就是共享中断必须设置dev_id参数的原因之一。至此,就可以实现多个设备对这一中断线的"shared"。注意,对于这个"shared",并不是在时间上允许多个中断同时发生,而是一种空间上的、中断号上的"shared",此外,这个"shared"和三星芯片中常见的Shared Peripheral Interrupts(SPI)不是一回事,后者表示这个中断可以被GIC router到任意一个CPU中。
88 typedef irqreturn_t (*irq_handler_t)(int, void *);
irqreturn_t xxx_interrupt(int irq, void *dev_id)
{
...
int status = read_irq_status(); /* 读取相应的寄存器获取中断源 */
if(!is_myirq(dev_id,status)){ /* 判断是否是本设备中断 */
return IRQ_NONE;
}
/* 中断处理程序 */
return IRQ_HANDLED
}
2. 注册中断处理函数
下面这个就是注册中断的API,flags取值在"interrupt.h"有定义,常用的有IRQF_DISABLED和IRQF_SHARED,前者表示中断程序调用时屏蔽所有中断,"所有"指的是屏蔽所有中断线的中断,本中断线的中断本来就是屏蔽的,内核默认不允许中断嵌套。IRQF_SHARED表示多个设备共享中断,即该中断线上连接了多个相同或不同的设备。
除了中断类型,flags还需要"位或"上触发方式,eg:IRQF_DISABLED|IRQF_TRIGGER_RISING
/**
* @flags:中断标志位。
* @dev_id用于共享中断,用来标识是哪个设备触发了中断,通常传入相应设备的中断状态寄存器的地址
* @name 是中断名称,可以在/proc/interrupt中看到
*/
int requst_irq(unsigned int irq,irq_handler_t handler, unsigned long flags, const char *name,void * dev_id);
3. 释放中断处理函数
中断号也是一种系统资源,使用完毕后要释放,注意,free_irq的第二个参数应当与request_irq()中最后一个参数相同,这样才能将这个handler从这个中断线中的handler链表中删除。
/**
* free_irq - 释放irq
*/
void free_irq(unsigned int irqno,void * dev_id);
底半部
中断不属于进程上下文,所以不能被内核调度,如果进入了中断处理函数,就只能将其执行完毕,不能被打断,这样带来的一个问题是如果在中断处理函数中执行耗时操作,就会极大的影响系统性能,为了解决这个问题,Linux内核中提出了中断顶半部和`底半部(bottom half)的概念,对于耗时的中断处理程序,将重要的、必要的操作放在顶半部执行,这部分和传统的中断概念一样,一旦开始就必须执行完毕;将中断处理程序中耗时的,不那么紧要的操作放在底半部,防止整个中断处理程序过多的占用系统资源。
Linux内核提供的3种中断底半部机制:工作队列,tasklet,软中断。其中软中断机制是内核底层的机制,包括定时器,异步通知等很多机制都是基于软中断,开发者不应该直接操作,所以这里仅讨论前两种
Tasklet
每个tasklet都和一个函数相关联,当tasklet被运行时,该函数就被调用,并且tasklet可以调度自己。
//定义一个处理函数
void my_tasklet_fcn(unsigned long){}
//定义一个tasklet结构my_tasklet,并与处理函数相关联,
DECLARE_TASKLET(my_tasklet,my_tasklet_fcn,data);
//调度tasklet
tasklet_schedule(&my_tasklet);
工作队列
工作队列和tasklet类型,tasklet多运行于中断上下文,而工作队列多运行与进程上下文
此外,tasklet中不能睡眠,而工作队列处理函数允许睡眠(正是由于它被当作内核线程被调度)
//定义一个工作队列
struct work_queue my_wq;
//定义一个处理函数
void my_wq_fcn(unsigned long){}
//初始化工作队列并将其与处理函数绑定
INIT_WORK(&my_wq,my_wq_fcn);
//调度工作队列执行
schedule_work(&my_wq);
static irqreturn_t handler_t(int irq, void *dev)
{
//top half
schedule_work(&ws);
return IRQ_HANDLED;
}
void work_func(struct work_struct *work)
{
//bottom half
ssleep(3);
}
static int __init demo_init(void)
{
...
INIT_WORK(&ws, work_func);
request_irq(irq, handler_t, IRQF_TRIGGER_RISING, "demo", NULL);
...
}
其他API
除了上述API,内核还提供了其他的中断操作API,在内核代码中被广泛使用。
raw_local_irq_save(x) //禁止所有中断
raw_local_irq_enable //取消禁止中断
//下面三个函数用于可编程中断控制器,对所有CPU都有效
//屏蔽一个中断
void disable_irq(int irq); //立即返回
void disable_irq_nosync(); //等待目前中断处理返程
//使能一个中断
void enable_irq()