中断引入与简述
1 CPU模式
ARM CPU模式存在两类7种:
1)usr:用户
2)特权模式:
1> sys:系统
2> 异常模式
- a. und:未定义模式
- b. svc:管理
- c. abt:终止(①指令预期终止;②数据访问终止)
- d. irq:中断模式
- e. fiq:快中断模式
模式解读(以后再详细补充,这里简单说明):
除用户模式外,其余6种为特权模式,这些特权模式可直接修改CPSR进入其他模式,而usr模式是无法通过CPSR进入其他模式的,app一般运行usr模式,这导致app是无法直接访问硬件的,若需要访问硬件,则必须切换模式。对于CPU而言,复位之后,处于SVC模式,复位后添加切换至usr模式,设置相应栈后,即跳转执行。usr模式进入特权模式,通常以以下三种方式:
- 中断
- und
- swi软中断
例如:定义swi 0x123会触发swi异常,进入异常列表的地址执行定义好的函数,在Bootloader中的Start.S文件中会定义异常向量列表,如下所示,这里的swi异常就会去0x8地址执行,其余异常基本一致
.globl _start
_start:
b reset // 0地址
ldr pc, _undefined_instruction // 4
ldr pc, _software_interrupt // 8
ldr pc, _prefetch_abort // c
ldr pc, _data_abort // 16
ldr pc, _not_used // 20
ldr pc, _irq // 24 发生中断时,CPU跳到这个地址执行该指令
从这里也可以看到模式切换就是在需要跳转的位置定义CPU能够识别的异常指令即可。
异常与中断重点:保护现场与恢复现场
刚刚说的CPSR寄存器,对于没有接触过底层编程的可能不太熟悉(可以读一下《ARM体系结构与编程》中对ARM处理器37个寄存器的解读)
CPSR: 当前程序状态寄存器
SPSR: 保存的程序状态寄存器
要弄明白这两个寄存器作用,必须对CPU处理异常的过程有一个了解,中断异常的处理过程有三步:
a. 保护现场(冻结当前现场,将状态存储到某些寄存器)
b. 处理异常(跳转到异常点执行)
c. 恢复现场
具体而言就是(以irq为例):
进入异常
a. 取下一条指令地址保存到LR寄存器(PC+4或PC+8,涉及ARM流水线知识)
sub lr, lr, #4
b. 将CPSR保存到SPSR
stmdb sp!, {r0-r12, lr}
// stmdb压栈操作
sp -= 4,sp = lr
sp -= 4,sp = r12
...
sp -= 4, sp = r0
c. 修改CPSR模式进入异常模式(M4~M0)
d. 跳到异常向量表执行
退出异常
a. PC = LR - offset
b. 将保存的SPSR恢复给CPSR
ldmia sp!, {r0-r12, pc}
c. 清中断
这些状态都是保存到栈上,对于进程、线程和函数跳转在栈上的处理过程基本都一致。
注:本文不过多涉及硬件方面知识,可以参考附件中的链接
2 中断子系统
对于中断而言,在Linux系统中牵扯的知识点较多,例如:中断不可嵌套性、同步机制、工作队列、中断上下文、中断上下部、共享中断、中断线程化等等。
2.1 中断相关结构体
2.1.1 irq_desc
Linux将中断相关处理,以结构体数组来存储,即irq_desc,在使用中断时,在irq_desc数组中找到空闲项填入,这一项下标就是virq number,同时以当前域的irq_domain.linear_revmap来保存hwirq与virq对应关系。中断发生时,通过异常向量表跳到指定地址,读取中断控制器来获取hwirq,反推virq,在以virq作为下标找到irq_desc结构体,调用中断处理函数handle_irq,再遍历action链表确定共享中断中具体中断,从而调用action.handler,并使用chip操作清中断。
对于外部中断而言,子中断控制器可以读取特定寄存器来确定为何种中断,在外部中断多的情况下,再通过遍历action链表,显然复杂度上升,是不可取的。在存在外部中断情况下,驱动可以提供中断分发函数,将irq_desc.handle_irq指向它,在中断分发函数来确定外部中断号。
struct irq_desc {
struct irq_common_data irq_common_data;
struct irq_data irq_data;
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; /* action链表,用于中断处理函数 */
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 tot_count;
unsigned int irq_count; /* For detecting broken IRQs */
unsigned long last_unhandled; /* Aging timer for unhandled count */
unsigned int irqs_unhandled;
atomic_t threads_handled;
#ifdef CONFIG_SPRD_IRQS_MONITOR
unsigned long tot_times; /* total ns in per sec */
#endif
int threads_handled_last;
raw_spinlock_t lock;
struct cpumask *percpu_enabled;
const struct cpumask *percpu_affinity;
#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_PM_SLEEP
unsigned int nr_actions;
unsigned int no_suspend_depth;
unsigned int cond_suspend_depth;
unsigned int force_resume_depth;
#endif
#ifdef CONFIG_PROC_FS
struct proc_dir_entry *dir;
#endif
#ifdef CONFIG_GENERIC_IRQ_DEBUGFS
struct dentry *debugfs_file;
#endif
#ifdef CONFIG_SPARSE_IRQ
struct rcu_head rcu;
struct kobject kobj;
#endif
struct mutex request_mutex;
int parent_irq;
struct module *owner;
const char *name;
} ____cacheline_internodealigned_in_smp;
2.1.2 irqaction
一个中断可以包含多个处理函数,结合硬件来讲,就是复用,在一个Pin上接有多个外设,也就是说当前中断源可以有很多种,即共享中断。irqaction是一个链表结构,专用来存储这些中断信息,可以用来存储共享中断。
struct irqaction {
irq_handler_t handler; //等于用户注册的中断处理函数,中断发生时就会运行这个中断处理函数
void *dev_id; //设备id
void __percpu *percpu_dev_id;
struct irqaction *next;
irq_handler_t thread_fn;
struct task_struct *thread;
struct irqaction *secondary;
unsigned int irq; //中断号
unsigned int flags; //中断标志,注册时设置,比如上升沿中断,下降沿中断等
unsigned long thread_flags;
unsigned long thread_mask; //中断掩码
const char *name; //中断名称,产生中断的硬件的名字
struct proc_dir_entry *dir; //指向IRQn相关的/proc/irq/
} ____cacheline_internodealigned_in_smp;
2.1.3 irq_data
可以看到irq_data中存在一个irq和一个hwirq,这两个概念如下:
irq:虚拟中断号;CPU为每一个外设中断的编号,是一个虚拟的中断ID,和硬件无关
hwirq:硬件中断号;中断控制器用于标识外设中断的ID,对于多中断控制器级联情况下,hwirq不能再作为唯一标识外设中断的id,hwirq在不同中断控制器上是可以重复编码的。
拓展:另还有软件中断号,只不过利用了硬件中断的概念,实现宏观上的异步执行。在Linux中断分为中断上半部与中断下半部,上半部通常处理一些紧急且不耗时间的任务(不可嵌套),下半部用以处理耗时任务(实现可以软中断、tasklet、工作队列形式实现),可线程化为内核线程。下本部利用workqueue存储,与应用线程一起调度。CPU多核环境下,衍生出request_threaded_irq来将每一个中断都线程化,便于多核处理高效性。
Linux中断子系统会将hwirq映射为irq,利用irq_domain
struct irq_data {
u32 mask;
unsigned int irq;
unsigned long hwirq;
struct irq_common_data *common;
struct irq_chip *chip; // 硬件操作
struct irq_domain *domain;
#ifdef CONFIG_IRQ_DOMAIN_HIERARCHY
struct irq_data *parent_data;
#endif
void *chip_data;
};
2.1.4 irq_chip
irq_chip为硬件相关操作函数。
struct irq_chip {
struct device *parent_device;
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_calc_mask)(struct irq_data *data);
void (*irq_print_chip)(struct irq_data *data, struct seq_file *p);
int (*irq_request_resources)(struct irq_data *data);
void (*irq_release_resources)(struct irq_data *data); //释放中断服务函数
void (*irq_compose_msi_msg)(struct irq_data *data, struct msi_msg *msg);
void (*irq_write_msi_msg)(struct irq_data *data, struct msi_msg *msg);
int (*irq_get_irqchip_state)(struct irq_data *data, enum irqchip_irq_state which, bool *state);
int (*irq_set_irqchip_state)(struct irq_data *data, enum irqchip_irq_state which, bool state);
int (*irq_set_vcpu_affinity)(struct irq_data *data, void *vcpu_info);
void (*ipi_send_single)(struct irq_data *data, unsigned int cpu);
void (*ipi_send_mask)(struct irq_data *data, const struct cpumask *dest);
unsigned long flags;
};
2.1.5 irq_domain
irq_domain主要用于将hwirq映射到irq,驱动中request_irq(irq, handler),此处的irq是虚拟中断号。
irq_domain域的引入主要是考虑中断控制器与中断越来越多背景下,传统为每一个中断来写一个宏在大大增加工作量的同时也使得确保硬件中断对应中断号不可重复性的任务变得复杂。irq_domain域的引入使得hwirq与irq不再绑定,irq可以是随意的一个irq_desc中的空闲项。同时hwirq在不同中断控制器上是会重复编码的,以irq_domain来限定当前hwirq属于哪一个Interrupt controller。
Q:硬件 -> GPIO中断 -> GIC中断 -> CPU情景下,CPU如何处理处理中断?
A:CPU处理过程与硬件响应是相反的
Q:设备树中指定的通常是hwirq,发生硬件中断时,如何反算irq?
A:与irq_domain相关,在特定域中转换,不同域转换方法不同
struct irq_domain {
struct list_head link;
const char *name;
const struct irq_domain_ops *ops; //操作函数
void *host_data;
unsigned int flags;
unsigned int mapcount;
/* Optional data */
struct fwnode_handle *fwnode;
enum irq_domain_bus_token bus_token;
struct irq_domain_chip_generic *gc;
#ifdef CONFIG_IRQ_DOMAIN_HIERARCHY
struct irq_domain *parent;
#endif
#ifdef CONFIG_GENERIC_IRQ_DEBUGFS
struct dentry *debugfs_file;
#endif
/* reverse map data. The linear map gets appended to the irq_domain */
irq_hw_number_t hwirq_max;
unsigned int revmap_direct_max_irq;
unsigned int revmap_size;
struct radix_tree_root revmap_tree;
unsigned int linear_revmap[];
};
struct irq_domain_ops {
int (*match)(struct irq_domain *d, struct device_node *node,
enum irq_domain_bus_token bus_token);
int (*select)(struct irq_domain *d, struct irq_fwspec *fwspec,
enum irq_domain_bus_token bus_token);
int (*map)(struct irq_domain *d, unsigned int virq, irq_hw_number_t hw); //把hwirq转换为irq
void (*unmap)(struct irq_domain *d, unsigned int virq);
int (*xlate)(struct irq_domain *d, struct device_node *node,
const u32 *intspec, unsigned int intsize,
unsigned long *out_hwirq, unsigned int *out_type); // 解析设备树的中断属性,提取出hwirq、type等信息
#ifdef CONFIG_IRQ_DOMAIN_HIERARCHY
/* extended V2 interfaces to support hierarchy irq_domains */
int (*alloc)(struct irq_domain *d, unsigned int virq,
unsigned int nr_irqs, void *arg);
void (*free)(struct irq_domain *d, unsigned int virq,
unsigned int nr_irqs);
void (*activate)(struct irq_domain *d, struct irq_data *irq_data);
void (*deactivate)(struct irq_domain *d, struct irq_data *irq_data);
int (*translate)(struct irq_domain *d, struct irq_fwspec *fwspec,
unsigned long *out_hwirq, unsigned int *out_type);
#endif
};
上述结构体层级大致为以下结构:
在设备树中指明中断号后,内核需先解析设备树才能将中断号与虚拟中断号关联起来,从而形成
2.2 中断上下文、中断上下部
当前及以下章节为中断相关知识点拓展,只作简要说明。
处理器运行状态通常有三种:①内核态,运行于进程上下文,内核代表进程运行于内核空间;②内核态,运行于中断上下文,内核代表硬件运行于内核空间;③用户态,运行于用户空间。这里的进程上下文与中断上下文是很重要的概念。
(1)进程上下文:用户态通过系统调用或异常进入内核态,用户进程传递给内核的这些参数以及内核要保存的那一整套的变量和寄存器值和当时的环境等
(2)中断上下文:硬件信号触发进入中断模式,硬件传递过来的相关参数和内核需要保存的一些其他环境等
其实进程上下文与中断上下文就是保存状态切换后的现场而言,不做过多说明,主要说一下中断上下部。
在前面也略微提了一下中断上下部概念,主要就是由于中断不可嵌套性导致在关中断状态下,系统如果处理不完当前中断,会导致可能出现假死状态,从而引入中断上下部来平衡调度中断事务的处理机制。
中断上下部是多对一的,上半部处理完成,恢复下半部调度,下半部并不只有当前中断上半部所对应的下半部,下半部是一个workqueue,包含了所有中断的下半部,上半部结束会恢复下半部正常调度。
2.3 工作队列与线程化中断
中断下半部会以内核线程worker来操作,与用户线程一起来调度。创建内核线程,其中保存了workqueue,用以存放中断下半部处理函数。中断上下文总是可以抢占进程上下文,系统必须在处理完所以挂起中断与软中断后才能继续执行正常任务。,因此实时性得不到保证。将中断线程化(时钟中断不可线程化),可以任意指定优先级进行执行,便于调度与多核分发处理。
static inline int __must_check
request_irq(unsigned int irq, irq_handler_t handler, unsigned long flags,
const char *name, void *dev)
参数1:注册中断服务函数的中断号
参数2:中断服务函数,上半部处理并触发下半部操作
参数3:中断标志位
参数4:请求中断的设备名称
参数5:传入中断处理程序的参数,共享中断必须给出该参数
int request_threaded_irq(unsigned int irq, irq_handler_t handler,
irq_handler_t thread_fn, unsigned long irqflags,
const char *devname, void *dev_id)
参数1:注册中断服务函数的中断号
参数2:中断服务函数,上半部处理,可直接为NULL,完全交由线程处理
参数3:中断线程化
参数4:中断标志位
参数5:请求中断的设备名称
参数6:传入中断处理程序的参数,共享中断必须给出该参数,共享中断下,IRQ number对应若干个irqaction,需指定具体ID的irqaction
目前的内核中,request_irq还是调用的request_threaded_irq,只不过对于参数thread_fn直接传入的NULL,默认不将中断线程化而已。
static inline int __must_check
request_irq(unsigned int irq, irq_handler_t handler, unsigned long flags,
const char *name, void *dev)
{
return request_threaded_irq(irq, handler, NULL, flags, name, dev);
}
int request_threaded_irq(unsigned int irq, irq_handler_t handler,
irq_handler_t thread_fn, unsigned long irqflags,
const char *devname, void *dev_id)
{
struct irqaction *action;
struct irq_desc *desc;
int retval;
if (irq == IRQ_NOTCONNECTED)
return -ENOTCONN;
/*
* Sanity-check: shared interrupts must pass in a real dev-ID,
* otherwise we'll have trouble later trying to figure out
* which interrupt is which (messes up the interrupt freeing
* logic etc).
*
* Also IRQF_COND_SUSPEND only makes sense for shared interrupts and
* it cannot be set along with IRQF_NO_SUSPEND.
*/
if (((irqflags & IRQF_SHARED) && !dev_id) ||
(!(irqflags & IRQF_SHARED) && (irqflags & IRQF_COND_SUSPEND)) ||
((irqflags & IRQF_NO_SUSPEND) && (irqflags & IRQF_COND_SUSPEND)))
return -EINVAL;
desc = irq_to_desc(irq);
if (!desc)
return -EINVAL;
if (!irq_settings_can_request(desc) ||
WARN_ON(irq_settings_is_per_cpu_devid(desc)))
return -EINVAL;
if (!handler) {
if (!thread_fn)
return -EINVAL;
handler = irq_default_primary_handler;
}
action = kzalloc(sizeof(struct irqaction), GFP_KERNEL);
if (!action)
return -ENOMEM;
action->handler = handler;
action->thread_fn = thread_fn;
action->flags = irqflags;
action->name = devname;
action->dev_id = dev_id;
retval = irq_chip_pm_get(&desc->irq_data);
if (retval < 0) {
kfree(action);
return retval;
}
retval = __setup_irq(irq, desc, action);
if (retval) {
irq_chip_pm_put(&desc->irq_data);
kfree(action->secondary);
kfree(action);
}
#ifdef CONFIG_DEBUG_SHIRQ_FIXME
if (!retval && (irqflags & IRQF_SHARED)) {
/*
* It's a shared IRQ -- the driver ought to be prepared for it
* to happen immediately, so let's make sure....
* We disable the irq to make sure that a 'real' IRQ doesn't
* run in parallel with our fake.
*/
unsigned long flags;
disable_irq(irq);
local_irq_save(flags);
handler(irq, dev_id);
local_irq_restore(flags);
enable_irq(irq);
}
#endif
return retval;
}
EXPORT_SYMBOL(request_threaded_irq);
2.4 中断处理流程
中断子系统知识点是相当多的,以前学习的内核也比较老,针对性的深入也不多,能力有限,只能做一个简单梳理,可直接参考附件中链接。
结合前面的分析,ARM处理异常时会跳转到异常向量表去执行,异常向量表可位于0x0或0xffff0000地址,设置到哪个地址由SCTLR寄存器控制,启用MMU的ARM Linux使用0xffff0000地址,这是由于Linux中0地址开始的3G空间为用户空间(32位操作系统)。
在内核周工也保存了一个异常向量:
.L__vectors_start: // arch/arm/kernel/entry-armv.S
W(b) vector_rst
W(b) vector_und
W(ldr) pc, .L__vectors_start + 0x1000
W(b) vector_pabt
W(b) vector_dabt
W(b) vector_addrexcptn
W(b) vector_irq
W(b) vector_fiq
这里的vector_irq直接找是找不到的,之前说过,对于每一种异常而言,处理流程基本类似,保存线程、执行、恢复线程,内核为了去除冗余,对这些向量表的代码用了一种取巧的方式,将相同部分提取出来,形成以下部分:
.macro vector_stub, name, mode, correction=0
.align 5
vector_\name:
.if \correction
sub lr, lr, #\correction
.endif
@
@ Save r0, lr_<exception> (parent PC) and spsr_<exception>
@ (parent CPSR)
@
stmia sp, {r0, lr} @ save r0, lr
mrs lr, spsr
str lr, [sp, #8] @ save spsr
@
@ Prepare for SVC32 mode. IRQs remain disabled.
@
mrs r0, cpsr
eor r0, r0, #(\mode ^ SVC_MODE | PSR_ISETSTATE)
msr spsr_cxsf, r0
@
@ the branch table must immediately follow this code
@
and lr, lr, #0x0f
THUMB( adr r0, 1f )
THUMB( ldr lr, [r0, lr, lsl #2] )
mov r0, sp
ARM( ldr lr, [pc, lr, lsl #2] )
movs pc, lr @ branch to handler in SVC mode
ENDPROC(vector_\name)
macro指令就是把所需要重复出现的程序块定义成宏指令,这里就是宏定义为vector_srub,对vector_irq就是vector_stub irq, mode, correction
vector_stub irq, IRQ_MODE, 4
.long __irq_usr @ 0 (USR_26 / USR_32)
.long __irq_invalid @ 1 (FIQ_26 / FIQ_32)
.long __irq_invalid @ 2 (IRQ_26 / IRQ_32)
.long __irq_svc @ 3 (SVC_26 / SVC_32)
.long __irq_invalid @ 4
.long __irq_invalid @ 5
.long __irq_invalid @ 6
.long __irq_invalid @ 7
.long __irq_invalid @ 8
.long __irq_invalid @ 9
.long __irq_invalid @ a
.long __irq_invalid @ b
.long __irq_invalid @ c
.long __irq_invalid @ d
.long __irq_invalid @ e
.long __irq_invalid @ f
__irq_usr:
usr_entry
kuser_cmpxchg_check
irq_handler
get_thread_info tsk
mov why, #0
b ret_to_user_from_irq
UNWIND(.fnend )
ENDPROC(__irq_usr)
__irq_svc:
svc_entry
irq_handler\
.macro irq_handler
#ifdef CONFIG_MULTI_IRQ_HANDLER
ldr r1, =handle_arch_irq
mov r0, sp
badr lr, 9997f
ldr pc, [r1]
#else
arch_irq_handler_default
#endif
最终跳到C函数handle_arch_irq去执行
void __init set_handle_irq(void (*handle_irq)(struct pt_regs *))
{
if (handle_arch_irq)
return;
handle_arch_irq = handle_irq;
}
以drivers/irqchip/irq-s3c24xx.c为例分析过程:
s3c_init_intc_of
set_handle_irq(s3c24xx_handle_irq);
s3c24xx_handle_irq
s3c24xx_handle_intc
irq_domain_get_of_node(intc->domain)
handle_domain_irq(intc->domain, intc_offset + offset, regs);
__handle_domain_irq(domain, hwirq, true, regs)
irq_find_mapping(domain, hwirq)
/
if (hwirq < domain->revmap_direct_max_irq) {
data = irq_domain_get_irq_data(domain, hwirq);
if (data && data->hwirq == hwirq)
return hwirq;
}
/* Check if the hwirq is in the linear revmap. */
if (hwirq < domain->revmap_size)
return domain->linear_revmap[hwirq];
/
generic_handle_irq(irq)
struct irq_desc *desc = irq_to_desc(irq);
generic_handle_irq_desc
desc->handle_irq(desc)
总结起来就是:
① 读取中断控制器,拿到hworq
② 根据hwirq,在irq_domain中拿到virq
③ 调用irq_desc[virq].handle_irq
附录
(1)ATPCS:
① 子程序通过R0~R3来传递参数;
② 子程序使用R4~R11来保存局部变量;
③ 寄存器R12用作scratch寄存器,记为ip(发现Linux内核中的汇编直接使用ip这个符号);
④ R13为SP
⑤ R14为LR
⑥ R15为PC
(2)参考
内核中断系统中的设备树
Linux kernel的中断子系统之(一):综述
Linux kernel的中断子系统之(二):IRQ Domain介绍
Linux kernel中断子系统之(五):驱动申请中断API
Linux kernel的中断子系统之(六):ARM中断处理过程