/************************************************************************************
*本文为个人学习记录,如有错误,欢迎指正。
* https://blog.csdn.net/fridayll/article/details/51854126
* https://www.cnblogs.com/edver/p/7260696.html
* https://www.linuxidc.com/Linux/2017-08/146264.htm
* https://www.cnblogs.com/amanlikethis/p/6941666.html?utm_source=itdadao&utm_medium=referral
* https://www.cnblogs.com/chen-farsight/p/6155503.html
************************************************************************************/
1. 中断简介
中断是指在CPU正常运行期间,由于内外部事件或由程序预先安排的事件引起的CPU暂时停止正在运行的程序,转而为该内部或外部事件或预先安排的事件服务的程序中去,服务完毕后再返回去继续运行被暂时中断的程序。Linux中通常分为外部中断(又叫硬件中断)和内部中断(又叫异常)。
2. Linux内核中断机制的初始化
2.1 相关数据结构
linux内核将所有的中断统一编号,使用一个irq_desc[NR_IRQS]的结构体数组来描述这些中断:每个数组项对应着一个中断源,记录了中断的入口处理函数(不是用户注册的处理函数)、中断标记,并提供了中断的底层硬件访问函数(中断清除、屏蔽、使能)。另外,通过这个结构体数组项成员action,能够找到用户注册的中断处理函数。
(1)irq_desc[NR_IRQS]数组
struct irq_desc irq_desc[NR_IRQS] __cacheline_aligned_in_smp = { [0 ... NR_IRQS-1] = { .status = IRQ_DISABLED, .chip = &no_irq_chip, .handle_irq = handle_bad_irq, .depth = 1, .lock = __RAW_SPIN_LOCK_UNLOCKED(irq_desc->lock), } };
(2)struct irq_desc
Linux内核中使用struct irq_desc来描述一个中断源。Linux内核中将CPU的所有中断进行编号,具体编号定义在/kernel/arch/arm/mach-s5pv210/include/mach/irqs.h。
struct irq_desc { unsigned int irq; /*中断号*/ irq_flow_handler_t handle_irq; /* 当前中断的处理函数入口 */ struct irq_chip *chip; /* 低层的硬件访问 */ struct msi_desc *msi_desc; void *handler_data; void *chip_data; struct irqaction *action; /* 用户提供的中断处理函数链表 */ unsigned int status; /* IRQ状态 */ unsigned int depth; /* nested irq disables */ unsigned int wake_depth; /* nested wake enables */ unsigned int irq_count; /* For detecting broken IRQs */ unsigned int irqs_unhandled; spinlock_t lock; const char *name; /* 中断名称 */ } ____cacheline_internodealigned_in_smp;
(3)struct irq_chip
struct irq_chip 是中断控制器描述符, CPU所对应的一个具体的中断控制器。
struct irq_chip { const char *name; unsigned int (*startup)(unsigned int irq); //启动中断 void (*shutdown)(unsigned int irq); //关闭中断 void (*enable)(unsigned int irq); //使能中断 void (*disable)(unsigned int irq); //禁止中断 void (*ack)(unsigned int irq); //中断应答函数,就是清除中断标识函数 void (*mask)(unsigned int irq); //中断屏蔽函数 void (*mask_ack)(unsigned int irq); //屏蔽中断应答函数,一般用于电平触发方式,需要先屏蔽再应答 void (*unmask)(unsigned int irq); //开启中断 void (*eoi)(unsigned int irq); void (*end)(unsigned int irq); int (*set_affinity)(unsigned int irq, const struct cpumask *dest); int (*retrigger)(unsigned int irq); int (*set_type)(unsigned int irq, unsigned int flow_type);//设置中断类型,其中包括设置GPIO口为中断输入 int (*set_wake)(unsigned int irq, unsigned int on); void (*bus_lock)(unsigned int irq); //上锁函数 void (*bus_sync_unlock)(unsigned int irq); //解锁 /* Currently used only by UML, might disappear one day.*/ #ifdef CONFIG_IRQ_RELEASE_METHOD void (*release)(unsigned int irq, void *dev_id); #endif /* * For compatibility, ->typename is copied into ->name. * Will disappear. */ const char *typename; };
(4)struct irqaction
struct irqaction是中断服务程序描述符,该IRQ对应的一系列中断服务程序。
struct irqaction { irq_handler_t handler; //用户注册的中断处理函数 unsigned long flags; //中断标识 const char *name; //用户注册的中断名字,cat/proc/interrupts时可以看到 void *dev_id; //可以是用户传递的参数或者用来区分共享中断 struct irqaction *next; //irqaction结构链,一个共享中断可以有多个中断处理函数 int irq; //中断号 struct proc_dir_entry *dir; irq_handler_t thread_fn; struct task_struct *thread; unsigned long thread_flags; };
2.2 中断的初始化
Linux内核在初始化阶段调用 init_IRQ()函数用来初始化中断体系结构,即初始化irq_desc[NR_IRQS]数组。
//所在文件:/kernel/arch/arm/kernel/irq.c void __init init_IRQ(void) { int irq; for (irq = 0; irq < NR_IRQS; irq++) irq_desc[irq].status |= IRQ_NOREQUEST | IRQ_NOPROBE; init_arch_irq(); }
s5pv210_init_irq()函数在arch/arm/mach-s5pv210/cpu.c中定义,它为所有的中断设置了芯片相关的数据结构irq_desc[irq].chip,设置了处理函数入口irq_desc[irq].handle_irq。
void __init s5pv210_init_irq(void) { u32 vic[4]; /* S5PV210 supports 4 VIC */ /* All the VICs are fully populated. */ vic[0] = ~0; vic[1] = ~0; vic[2] = ~0; vic[3] = ~0; s5p_init_irq(vic, ARRAY_SIZE(vic)); }
2.3 中断的处理流程
中断的处理流程如下:
1) 发生中断后,CPU执行异常向量vector_irq的代码;
2)在vector_irq里面,最终会调用中断处理C程序总入口函数asm_do_IRQ();
3)asm_do_IRQ()根据中断号调用irq_des[NR_IRQS]数组中的对应数组项中的handle_irq();
4)handle_irq()会使用chip的成员函数来设置硬件,例如清除中断,禁止中断,重新开启中断等;
5)handle_irq逐个调用用户在action链表中注册的处理函数。
可见,中断体系结构的初始化,就是构造irq_desc[NR_IRQS]这个数据结构;用户注册中断就是构造action链表;用户卸载中断就是从action链表中去除对应的项。
3. Linux内核中断处理程序架构
3.1 申请和释放中断
(1)申请IRQ
/* 参数: ** irq:要申请的硬件中断号
** ** handler:中断处理函数(顶半部)
** ** irqflags:触发方式及工作方式 ** 触发方式:IRQF_TRIGGER_RISING 上升沿触发 ** IRQF_TRIGGER_FALLING 下降沿触发 ** IRQF_TRIGGER_HIGH 高电平触发 ** IRQF_TRIGGER_LOW 低电平触发
** ** 工作方式:默认是快速中断(一个设备占用,且中断例程回调过程中会屏蔽中断) ** IRQF_SHARED:共享中断
** ** dev_id:在共享中断时会用到(中断注销与中断注册的此参数应保持一致)
** ** 返回值:成功返回 - 0;失败返回 - 负值(绝对值为错误码) */ int request_irq(unsigned int irq, irq_handler_t handler, unsigned long irqflags, const char *devname, void *dev_id)
(2)释放IRQ
/* 参数: ** irq:要注销的硬件中断号 ** dev_id:在共享中断时会用到(中断注销与中断注册的此参数应保持一致) */ void free_irq(unsigned int irq, void *dev_id);
3.2 Linux中断处理中的顶半部和底半部机制
(1)顶半部和底半部机制
由于中断服务程序的执行并不存在于进程上下文,因此,要求中断服务程序的时间尽可能的短。 为了在中断执行事件尽可能短和中断处理需完成大量耗时工作之间找到一个平衡点,Linux将中断处理分为两个部分:顶半部(top half)和底半部(bottom half)。
顶半部完成尽可能少的比较紧急的功能,它往往只是简单地读取寄存器中的中断状态并清除中断标志后进行“登记中断”的工作。“登记”意味着将底半部的处理程序挂载到该设备的底半部指向队列中去。底半部作为工作重心,完成中断事件的绝大多数任务。
(2)顶半部和底半部划分原则
1) 如果一个任务对时间非常敏感,将其放在顶半部中执行;
2) 如果一个任务和硬件有关,将其放在顶半部中执行;
3) 如果一个任务要保证不被其他中断打断,将其放在顶半部中执行;
4) 如果中断要处理的工作本身很少,所有的工作可在顶半部全部完成;
5) 其他所有任务,考虑放置在底半部执行。
(3)举例分析
当网卡接受到数据包时,通知内核,触发中断,所谓的顶半部就是,及时读取数据包到内存,防止因为延迟导致丢失,这是很急迫的工作。读到内存后,对这些数据的处理不再紧迫,此时内核可以去执行中断前运行的程序,而对网络数据包的处理则交给底半部处理。
3.3 底半部处理策略1:tasklet(小任务)
引入tasklet,最主要的是考虑支持SMP,提高SMP多个cpu的利用率;不同的tasklet可以在不同的cpu上运行。但是tasklet属于中断上下文,因此不能被阻塞,不能睡眠,不可被打断。
tasklet使用模版:
/* 定义tasklet和底半部处理函数并关联 */ void xxx_tasklet(unsigned long data); //定义一个名为 my_tasklet 的 struct tasklet 并将其与 my_tasklet_func 绑定,data为传入 my_tasklet_func的参数 DECLARE_TASKLET(xxx_tasklet, xxx_tasklet_func, data); /* 中断处理底半部 */ void xxx_tasklet_func() { /* 中断处理具体操作 */ } /* 中断处理顶半部 */ irqreturn xxx_interrupt(int irq, void *dev_id) { //do something task_schedule(&xxx_tasklet);//调用tasklet_schedule()函数使系统在适当的时候调度运行底半部 //do something return IRQ_HANDLED; } /* 设备驱动模块 init */ int __init xxx_init(void) { ... /* 申请设备中断 */ result = request_irq(xxx_irq, xxx_interrupt, IRQF_DISABLED, "xxx", NULL); ... return 0; } /* 设备驱动模块exit */ void __exit xxx_exit(void) { ... /* 释放中断 */ free_irq(xxx_irq, NULL); } module_init(xxx_init); module_exit(xxx_exit);
3.4 底半部处理策略2:workqueue(工作队列)
workqueue的突出特点是下半部会交给worker thead,因此下半部处于进程上下文,可以被重新调度,可以阻塞,也可以睡眠。
workqueue使用模板:
/* 定义工作队列和底半部函数 */ struct work_struct xxx_wq; void xxx_do_work(unsigned long); /* 中断处理底半部 */ void xxx_work(unsigned long) { /* do something */ } /* 中断处理顶半部 */ irqreturn_t xxx_interrupt(int irq, void *dev_id) { ... schedule_work(&xxx_wq); ... return IRQ_HANDLED; } /* 设备驱动模块 init */ int __init xxx_init(void) { ... /* 申请设备中断 */ result = request_irq(xxx_irq, xxx_interrupt, IRQF_DISABLED, "xxx", NULL); /* 初始化工作队列 */ INIT_WORK(&xxx_wq, xxx_do_work);//关联工作队列和底半部函数 ... return 0; } /* 设备驱动模块exit */ void __exit xxx_exit(void) { ... /* 释放中断 */ free_irq(xxx_irq, NULL); } module_init(xxx_init); module_exit(xxx_exit);