linux下中断子系统

时间:2022-09-29 04:18:03

在驱动程序初始化时,若使用到中断,通常调用函数reqeust_irq() 建立该驱动程序对应的irqaction结构体,并登记到irq_desc[irq_num]->action链表中去。

当发生中断后,首先获取触发中断的HW interrupt ID,然后通过irq domain翻译成IRQ number,(1.找到root interrupt controller对应的irq_domian;2.根据HW寄存器信息和irq_domain获取HW interrupt ID;3.调用irq_find_mapping找到对应的irq number)就可以获取对应的中断描述符。调用中断描述符中饿irq_events_handler来进行中断处理就OK了。

在device tree初始化的时候,形成了系统内所有的device node的树状结构(当然其中包括所有和中断外设的node)

在machine driver初始化的时候会调用of_irq_init函数,在该函数中会扫描所有interrupt controller的节点,并调用合适的interrupt controller driver进行初始化。


中断控制器是连接外设中断系统和CPU系统的桥梁。

驱动程序在请求中断服务时,会使用IRQ编号注册该中断,中断发生时,cpu通常会从中断控制器中获得相关信息,然后计算出相应的IRQ编号,然后把该IRQ编号传递给驱动程序。

对底层的封装主要包括:

实现不同i体系结构中断入口,通常用汇编实现

中断控制器进行封装和实现

系统启动阶段,中断子系统完成了必要的初始化操作,为驱动程序申请中断服务做好了准备。


实际上软中断更多的是在中断的退出阶段执行(irq_exit),以便达到更快的相应,加入守护进程机制,只是担心一旦有大量的软中断等待执行,会使得内核过长的停留在中断上下文中。


  几个比较关键的数据结构

struct irqaction //

 struct irqaction {
irq_handler_t handler;
unsigned long flags;
void *dev_id;
struct irqaction *next;
int irq;
irq_handler_t thread_fn;
struct task_struct *thread;
unsigned long thread_flags;
unsigned long thread_mask;
const char *name;
struct proc_dir_entry *dir;
} ____cacheline_internodealigned_in_smp;

struct irq_desc //interrupt desciptor

  struct irq_desc {
struct irq_data irq_data;
struct timer_rand_state *timer_rand_state;
unsigned int __percpu *kstat_irqs;
irq_flow_handler_t handle_irq;
#ifdef CONFIG_IRQ_PREFLOW_FASTEOI
irq_preflow_handler_t preflow_handler;
#endif
struct irqaction *action; /* IRQ action list */
unsigned int status_use_accessors;
unsigned int core_internal_state__do_not_mess_with_it;
unsigned int depth; /* nested irq disables */
unsigned int wake_depth; /* nested wake enables */
unsigned int irq_count; /* For detecting broken IRQs */
unsigned long last_unhandled; /* Aging timer for unhandled count */
unsigned int irqs_unhandled;
raw_spinlock_t lock;
#ifdef CONFIG_SMP
const struct cpumask *affinity_hint;
struct irq_affinity_notify *affinity_notify;
#ifdef CONFIG_GENERIC_PENDING_IRQ
cpumask_var_t pending_mask;
#endif
#endif
unsigned long threads_oneshot;
atomic_t threads_active;
wait_queue_head_t wait_for_threads;
#ifdef CONFIG_PROC_FS
struct proc_dir_entry *dir;
#endif
const char *name;
} ____cacheline_internodealigned_in_smp;

struct irq_chip //hardware interrupt chip descriptor中断控制器

 struct irq_chip {
const char *name;
unsigned int (*irq_startup)(struct irq_data *data);
void (*irq_shutdown)(struct irq_data *data);
void (*irq_enable)(struct irq_data *data);
void (*irq_disable)(struct irq_data *data);

void (*irq_ack)(struct irq_data *data);
void (*irq_mask)(struct irq_data *data);
void (*irq_mask_ack)(struct irq_data *data);
void (*irq_unmask)(struct irq_data *data);
void (*irq_eoi)(struct irq_data *data);

int (*irq_set_affinity)(struct irq_data *data, const struct cpumask *dest, bool force);
int (*irq_retrigger)(struct irq_data *data);
int (*irq_set_type)(struct irq_data *data, unsigned int flow_type);
int (*irq_set_wake)(struct irq_data *data, unsigned int on);

void (*irq_bus_lock)(struct irq_data *data);
void (*irq_bus_sync_unlock)(struct irq_data *data);

void (*irq_cpu_online)(struct irq_data *data);
void (*irq_cpu_offline)(struct irq_data *data);

void (*irq_suspend)(struct irq_data *data);
void (*irq_resume)(struct irq_data *data);
void (*irq_pm_shutdown)(struct irq_data *data);

void (*irq_print_chip)(struct irq_data *data, struct seq_file *p);

unsigned long flags;

/* Currently used only by UML, might disappear one day.*/
#ifdef CONFIG_IRQ_RELEASE_METHOD
void (*release)(unsigned int irq, void *dev_id);
#endif
};
  request_irq().

  request_thread_irq().//分配中断资源,使能中断线和中断处理函数。

irq_to_desc();//将中断号变成中断描述符指针,主要是通过1.radix_tree_lookup(&irq_desc_tree, irq)获得 2.irq_desc[irq]获得。两者的区别在是否配置CONFIG_SPARSE_IRQ

之后进行一系列的参数赋值。

最终调用_setup_irq()去注册中断处理函数。

对不同的irq flags做对应的处理

register_irq_proc();

register_handler_proc();


在init/main.c中的strat_kernel函数中有early_init_irq()和init_IRQ(),softirq_init().

init_IRQ().是cpu厂商自己编写的,不同的cpu代码不同。

先将每个中断向量号设置为noprobe。(irq_set_noprobe)

再调用硬件相关代码,mach_init_IRQ(),该函数值之前setup_arch()中的config_BSP()已经被注册回调函数,

  void __init config_BSP(void)
{
printk("C-SKY Silan IVS2 Board\n");

mach_time_init = xxx_timer_init;
mach_hwclk = xxx_hwclk;
mach_init_IRQ = xxx_init_IRQ;
mach_get_auto_irqno = xxx_get_auto_irqno;
mach_reset = xxx_machine_restart;
#ifdef CONFIG_ARCH_USES_GETTIMEOFFSET
mach_tick = xxx_tick;
mach_gettimeoffset = xxx_timer_offset;
#endif
#ifdef CONFIG_CPU_USE_FIQ
mach_init_FIQ = xxx_init_FIQ;
#endif
prom_meminit();
}

最后调用在arch/xxx_cpu/xxx_sys/irq.c下的xxx_init_IRQ()

  irq_set_chip_and_handler(i, xxx_irq_chip, handle_level_irq).

参数中有一个函数指针handle_level_irq表示是电平中断流控.

 void    
irq_set_chip_and_handler_name(unsigned int irq, struct irq_chip *chip,
irq_flow_handler_t handle, const char *name)
{
irq_set_chip(irq, chip);
__irq_set_handler(irq, handle, 0, name);
}

最终调用两个函数

irq_set_chip().将中断控制器irq_chip赋给irq_desc->irq_data.chip;

__irq_set_handler().将中断触发方式赋给irq_desc->handle_irq.

软中断更多的是在中断的退出阶段执行,以便达到更快的响应,加入守护进程机制,只是担心一旦有大量的软中断等待执行,会使得内核过长地留在中断上下文中。

softirq_init().中关键的是两个函数

open_softirq(TASKLET_SOFTIRQ, tasklet_action);

open_softirq(HI_SOFTIRQ, tasklet_hi_action);

想要说明一点,tasklet是建立在软中断上的一种延迟执行机制,它的实现基于TASKLET_SOFTIRQ和HI_SOFTIRQ这两种软中断类型。

 中断向量号在arcj/xxx_cpu/xxx_sys/include/mach/xxx_irq.h中


中断通常被定义为一个事件,该事件改变处理器执行的指令顺序。

异常是由程序的错误产生的,或者由内核必须处理的异常条件产生的。

中断处理程序闭一个进程要“轻”(中断的上下文很少,建立或终止中断处理需要的时间很少)。

中断处理程序必须编写成使相应的内核控制路径能以嵌套的方式进行。

在临界区,中断必须禁止。

异常分为:处理器探测异常(1.故障2.陷阱-主要用途是为了调试程序3.异常中止)和编程异常(通常叫做软中断,用于执行系统调用和给调试程序通报一个特定的事件)。

非屏蔽中断的向量和异常的向量是固定的,但非可屏蔽中断的向量可以通过对中断控制器的编程来改变。

所有的IRQ线都与一个名为可编程中断控制器的硬件电路的输入引脚相连。

禁止的中断是丢失不了的,他们一旦被激活,PIC又把他们发送给CPU。

内核必须为每种异常提供一个专门的异常处理程序,对于某些异常,CPU控制器单元在开始执行异常处理程序前会产生一个硬件出错码,并且压入内核态堆栈。

中断描述符表(IDT)是一个系统表,与每一个中断或异常向量相联系,每一个向量在表中有着项相应的中断或异常处理程序的入口地址。内核在允许中断发生前,必须适当的初始化IDT。

每个中断或异常都会引起一个内核控制路径或者说代表当前进程在内核态执行单独的指令序列。

允许内核控制路径嵌套执行必须付出代价,那就是中断处理程序必须永不阻塞,也就是中断处理程序运行期间不能发生进程切换。

假定内核没有bug,那么大多数异常就只在CPU处于用户态时发生。

但缺页异常发生在内核态,当处理这样一个异常时,内核可以挂起当前进程,并用另一个进程代替它。直到请求的页可以使用为止。

尽管处理中断的内核控制路径代表当前进程运行,但由I/O设备产生的中断并不引起当前进程的专有数据结构。

中断处理程序从不执行可以导致缺页(意味着进程切换)的操作。

CPU产生的大部分异常都由linux解释为出错条件,当其中一个异常发生时,内核就向引起异常的进程发送一个信号向它通知一个反常条件。

执行异常处理函数的C函数名总是由do_前缀和处理程序名组成。其中的大部分函数把硬件出错和异常向量保存在当前进程的描述符中。

出现在内核态的任何其他异常都是由于内核的bug引起的。

三种主要的中断类型:I/O中断;时钟中断;处理器间中断。

中断处理程序是代表进程执行的,它所代表的进程必须总处于TASK_RUNNING状态。否则可能出现系统僵死情形。

不管引起中断的电路种类如何,所有的I/O中断处理程序执行四个相同的基本操作:

1.在内核态堆栈中保存IRQ的值和寄存器的内容

2.为正在给IRQ线服务的PIC发送一个应答,这将允许PIC进一步发出中断

3.执行共享这个IRQ的所有设备的中断服务例程(ISR)。

4跳到中断返回的地址后终止

linux使用向量128实现系统调用。

内核必须在启用中断前发现IRQ号与I/O设备之间的对应。

每个中断向量都有它自己的irq_desc描述符。

如果一个中断内核没有处理,那么这个中断就是意外中断。内核把中断和意外中断的总次数分别放在irq_desc描述符的irq_count和irqs_unhandled字段中,当第100000次中断产生时,如果意外中断的次数超过99900,内核才禁止这条IRQ线。

在系统初始化期间,init_IRQ()函数把每个IRQ主描述符的status描述符设置成IRQD_IRQ_DISABLED.

多个设备能共享一个单独的IRQ。因此内核要维护多个irqaction描述符,其中的每个描述符设计一个特定的硬件设备和一个特定的中断。

irq_stat数组包含NR_CPUS个元素,系统中的每个CPU对应一个元素。每个元素的类型为irq_cpustat_t,该类型包含几个计数器和内核记录CPU正在做什么的标志。

所有CPU服务于I/O中断的执行时间片几乎相同。

当硬件设备产生了一个中断信号时,多APIC系统选择其中一个CPU,并把该信号传递给相应的本地APIC,本地APIC又依次中断它的CPU,这个事件不通报给其他的所有CPU。

系统管理员可以通过向文件/proc/irq/n/smp_affinity中写入新的CPU位图掩码也可以改变指定中断IRQ的亲和力。

每个进程的thread_info描述符与thread_union结构中的内核栈紧邻。而根据内核编译时的选项不同,thread_union结构可能占一个页框或两个页框。如果thread_union结构的大小为8KB,那么当前进程的内核栈被用于所有类型内核控制路径。如果thread_union结构的大小为4KB,内核就使用三种类型的栈:

1.异常栈 对系统中的每个进程,内核使用不同的异常栈

2.硬中断请求栈 系统中的每个CPU都有一个硬中断请求栈,而且每个栈占用一个单独的页框

3. 软中断请求 系统中的每个CPU都有一个软中断请求栈,而且每个栈占用一个单独的页框

寄存器的保存和恢复必须使用汇编语言代码,保存寄存器是中断处理程序做的第一件事情。

在激活一个准备利用IRQ线的设备之前,其相应的驱动程序调用request_irq().这个函数建立一个新的irqaction描述符并用参数值初始化,然后调用set_irq()函数把这个描述符插入到合适的IRQ链表。

当设备操作结束时,驱动程序调用free_irq()函数从IRQ链表中删除这个描述符并释放相应的内存区。

可延迟中断是在开中断的情况下执行。

两种非紧迫,可中断的内核函数:1 可延迟函数(软中断和tasklet)2 工作队列来执行函数

软中断和tasklet有密切的关系,tasklet是在软中断之上实现。

软中断的分配是静态的(即在编译时定义),而tasklet的分配和初始化可以在运行时运行(安装一个内核模块)。

软中断可以并发地运行在多个CPU上,因此,软中断是可重入函数而且必须明确地使用自旋锁保护其数据结构。

相同类型的tasklet总是串行地执行,换句话说不能在两个CPU上同时运行相同类型的tasklet,但不同类型的tasklet可以运行在几个CPU上并发执行。

tasklet的串行化使tasklet函数不必是可重入的。

由给定CPU激活的一个可延迟函数必须在同一个CPU上执行。

一个软中断的下标决定了它的优先级,低下标意味着高优先级。

表示软中断的主要数据结构是softirq_vec数组,该数组包含类型为softirq_action.

32位的preempt_count字段用来跟踪内核抢占和内核控制路径的嵌套,该字段存放在每个进程描述符的thread_info字段中。

实现软中断的最后一个关键的数据结构是每个CPU都有的32位掩码(描述挂起的软中断),它存放在irq_cpustat_t数据结构的__softirq_pending字段中。

open_softirq()函数处理软中断的初始化。

raise_softirq()函数用来激活软中断,它接受软中断下标nr为参数。

如果在这样一个检查点(local_softirq_pending()不为0)检测到挂起的软中断,内核就调用do_softirq()来处理他们。

__do_softirq()函数读取本地cpu的软中断掩码并执行与每个设置为相关的延迟函数。

为了保证可延迟函数的低延迟性,__do_softirq()一直运行到执行完所有挂起的软中断。

每个ksoftirqd/n内核线程都运行run_ksoftirqd()函数。

软中断函数可以重新激活自己,实际上,网络软中断和tasklet软中断都可以这么做。

tasklet是I/O驱动程序中实现可延迟函数的首选方法。tasklet建立在两个叫做HI_SOFTIRQ和TASKLET_SOFTIRQ的软中断之上。几个tasklet可以与同一个软中断相关联,每个tasklet执行自己的函数。

tasklet和高优先级的tasklet分别存放在tasklet_vec和tasklet_hi_vec数组中。

tasklet描述符是一个tasklet_struct类型的数据结构。

首先应该分配一个新的tasklet_struct数据结构,并调用tasklet_init()初始化它,该函数接受的参数为tasklet描述符的地址,tasklet函数的地址和它的可选整型参数。

除非tasklet函数重新激活自己,否则tasklet的每次激活至多激活tasklet函数的一次执行。

工作队列允许内核函数(非常像可延迟函数)被激活,而且稍后由一种工作者线程(worker thread)的特殊内核线程执行。

可延迟函数运行在中断上下文中,而工作队列的函数运行在进程上下文中。可执行函数的唯一方式是在进程上下文中运行。

在中断上下文中不能发生进程切换。可延迟函数和工作队列都不能访问进程的用户态地址空间。

create_workqueue()函数接受一个字符串作为参数,返回新创建工作队列qorkqueue_struct描述符的地址。该函数还创建n个工作者线程(n是当前cpu的个数)。

每个工作者线程在worker_thread()函数内部不断地执行循环操作,因而线程在绝大多数时间里处于睡眠状态并等待某些工作被插入队列。

由于工作队列可以阻塞,因此,可以让工作者线程睡眠,甚至可以让他迁移到另一个CPU上恢复执行。

在绝大多数情况下,为了运行一个函数而创建整个工作者线程开销太大了。因此,内核引入了叫做events的预定义工作队列,所有的内核开发者都可以随意使用它。预定义工作队列只是一个包括不同内核层函数和I/O驱动程序的标准工作队列。

一些记录挂起进程切换的请求,挂起信号和单步执行的标志被存放在thread_info的flag字段中,这个字段也存放着其他与从中断和异常返回无关的标志。