在ARM64和MIPS这些精简指令集计算机体系结构中,中断、系统调用和其他打断程序正常执行流的事件统称为异常,这是广义的异常.狭义的异常专制执行指令时触发的异常.
1 ARM64异常处理
1.1 异常分类
在ARM64体系结构中,异常分为同步异常和异步异常.
同步异常是试图执行指令时生成的异常,或是作为指令的执行结果生成的异常.同步异常包括如下:
(1)系统调用,异常级别0使用svc指令陷入异常级别1,异常级别1使用hvc指令陷入异常级别2,异常级别2使用smc指令陷入异常级别3.
(2)数据中止,即访问数据时的页错误异常,虚拟地址没有映射到物理地址,或者没有写权限.
(3)指令中止,即取指令时的页错误异常,虚拟地址没有映射到物理地址,或者没有执行权限.
(4)栈指针或者指令地址没有对齐.
(5)没有定义的指令.
(6)调试异常.
异步异常不是由正在执行的指令生成的,和正在执行的指令没有关联.异步异常包括如下:
(1)中断(IRQ),即普通优先级的中断.
(2)快速中断(FIQ),即高优先级的中断.
(3)系统错误(SError),是由硬件错误触发的异常,例如最常见的是吧脏数据从cache line写回内存时触发异步的数据中止错误.
1.2 异常向量表
当异常发生的时候,处理器需要执行异常的处理程序.存储异常处理程序的内存位置称为异常向量,通常把所有异常向量存放在一张表中,称为异常向量表.对于ARM64处理器的异常级别1,2和3,每个异常都有自己的异常向量表,异常向量表的起始虚拟地址存放在寄存器VBAR_ELn(向量基准地址寄存器,Vector Based Address Register)中.
每个异常向量表有16项,分为4组,每组4项,每项的长度是128字节.(要执行的指令,可以存放32条指令,Linux内核中一般是一条跳转指令).
ARM64架构内核定义的异常级别1的异常向量表如下:
ENTRY(vectors)
ventry el1_sync_invalid // Synchronous EL1t
ventry el1_irq_invalid // IRQ EL1t
ventry el1_fiq_invalid // FIQ EL1t
ventry el1_error_invalid // Error EL1t
ventry el1_sync // Synchronous EL1h
ventry el1_irq // IRQ EL1h
ventry el1_fiq_invalid // FIQ EL1h
ventry el1_error_invalid // Error EL1h
ventry el0_sync // Synchronous 64-bit EL0
ventry el0_irq // IRQ 64-bit EL0
ventry el0_fiq_invalid // FIQ 64-bit EL0
ventry el0_error_invalid // Error 64-bit EL0
#ifdef CONFIG_COMPAT
ventry el0_sync_compat // Synchronous 32-bit EL0
ventry el0_irq_compat // IRQ 32-bit EL0
ventry el0_fiq_invalid_compat // FIQ 32-bit EL0
ventry el0_error_invalid_compat // Error 32-bit EL0
#else
ventry el0_sync_invalid // Synchronous 32-bit EL0
ventry el0_irq_invalid // IRQ 32-bit EL0
ventry el0_fiq_invalid // FIQ 32-bit EL0
ventry el0_error_invalid // Error 32-bit EL0
#endif
END(vectors)
ventry是一个宏,参数是跳转标号,即异常处理程序的标号,宏的定义如下:
.macro ventry label
.align 7
b \label
.endm
展开后,即每个异常向量只有一条指令,就是跳转到对应的处理程序.
从异常级别1的异常向量表可以看出如下内容:
(1)有些异常向量的跳转标号带有"invalid",说明内核不支持这些异常,例如内核不支持ARM64处理器的快速中断.
(2)对于内核模式生成的异常,Linux内核选择使用异常级别1的栈指针寄存器.
(3)对于内核模式生成的同步异常,入口是el1_sync.
(4)对于处理器处在内核模式,中断的入口是el1_irq.
(5)对于64位应用程序在用户模式下生成的同步异常,入口是el0_sync.
(6)如果处理器正在用户模式下执行64位应用程序,中断的入口是el0_irq.
(7)对于32位应用程序在用户模式下生成的同步异常,入口是el0_sync_compat.
(8)如果处理器正在用户模式下执行32位应用程序,中断的入口是el0_irq_compat.
1.3 异常处理
当处理器取出异常处理的时候,自动执行下面的操作.
(1)把当前的处理器状态(PSTATE)保存到寄存器SPSR_EL1(保存程序状态寄存器)中.
(2)把返回地址保存在寄存器ELR_EL1(异常链接寄存器)中.
如果是系统调用那么返回地址就是系统调用指令后面的指令.
如果是除系统调用外的同步异常,那么返回地址是生成异常的指令,需要重新执行.
如果是异步异常,那么返回地址是没有执行的第一条指令.
(3)把处理器状态的DAIF这4个异常掩码位都设置为1,禁止这4种异常,D是调试掩码位,A是系统错误掩码位,I是中断掩码位,F是快速中断掩码位.
(4)如果是同步异常,把错误地址保存在寄存器FAR_EL1(错误地址寄存器)中.例如在访问数据时生成的页错误异常,错误地址就是数据的虚拟地址;取指令时生成的页错误异常,错误地址就是指令的虚拟地址.
(5)如果是同步异常或系统错误异常,把生成异常的原因保存在寄存器ESR_EL1(异常症状寄存器)中.
(6)如果处理器处于用户模式,那么把异常级别提升到1.
(7)根据向量基准地址寄存器VBAR_EL1,异常类型和生成异常的异常级别计算出异常向量的虚拟地址,执行异常向量.
当异常处理程序执行完的时候,调用kernel_exit返回.kernel_exit是一个宏,参数el是返回的异常级别,0表示返回异常级别0,1表示返回异常级别1.主要代码如下:
.macro kernel_exit, el
.if \el != 0
/* Restore the task's original addr_limit. */
ldr x20, [sp, #S_ORIG_ADDR_LIMIT]
str x20, [tsk, #TI_ADDR_LIMIT]
.endif
ldp x21, x22, [sp, #S_PC] // load ELR, SPSR
.if \el == 0
ct_user_enter
ldr x23, [sp, #S_SP] // load return stack pointer
msr sp_el0, x23
#ifdef CONFIG_ARM64_ERRATUM_845719
alternative_if_not ARM64_WORKAROUND_845719
nop
nop
#ifdef CONFIG_PID_IN_CONTEXTIDR
nop
#endif
alternative_else
tbz x22, #4, 1f
#ifdef CONFIG_PID_IN_CONTEXTIDR
mrs x29, contextidr_el1
msr contextidr_el1, x29
#else
msr contextidr_el1, xzr
#endif
1:
alternative_endif
#endif
.endif
msr elr_el1, x21 // set up the return data
msr spsr_el1, x22
ldp x0, x1, [sp, #16 * 0]
ldp x2, x3, [sp, #16 * 1]
ldp x4, x5, [sp, #16 * 2]
ldp x6, x7, [sp, #16 * 3]
ldp x8, x9, [sp, #16 * 4]
ldp x10, x11, [sp, #16 * 5]
ldp x12, x13, [sp, #16 * 6]
ldp x14, x15, [sp, #16 * 7]
ldp x16, x17, [sp, #16 * 8]
ldp x18, x19, [sp, #16 * 9]
ldp x20, x21, [sp, #16 * 10]
ldp x22, x23, [sp, #16 * 11]
ldp x24, x25, [sp, #16 * 12]
ldp x26, x27, [sp, #16 * 13]
ldp x28, x29, [sp, #16 * 14]
ldr lr, [sp, #S_LR]
add sp, sp, #S_FRAME_SIZE // restore sp
eret // return to kernel
.endm
首先使用保存在内存栈里面的寄存器恢复通用寄存器,然后执行指令eret返回,继续执行被打断的程序.执行指令eret的时候,处理器自动使用寄存器SPSR_EL1保存的值恢复处理器状态,使用寄存器ELR_EL1保存的返回地址恢复程序计数器(PC).
2 中断
中断是外围设备通知处理器的一种机制,典型的例子是:网卡从网络收到报文,把报文放到接收环,然后发送中断请求通知处理器,接着处理器响应中断请求,执行中断处理程序,从网卡的接收环取走报文;网卡驱动程序发送报文的时候,把报文放到网卡的发送环,当网卡从发送环取出报文发送的时候,发送中断请求通知处理器发送完成.
2.1 中断控制器
外围设备不是把中断请求直接发给处理器,而是发给中断控制器,由中断控制器转发给处理器.ARM公司提供了一种标准的中断控制器,称为通用中断控制器(GIC).目前GIC架构规范有4个版本:v1~v4.GIC v2最多支持8个处理器,GIC v3最多支持128个处理器,GIC v3和GIC v4只支持ARM64处理器.
从软件的角度看,GIC v2控制器有两个主要的功能块.
(1)分发器:系统中所有的中断源连接到分发器,分发器的寄存器用来控制单个中断的属性:优先级,状态,安全,转发信息(可以被转发到哪些处理器)和使能状态.分发器决定哪个中断应该通过处理器的接口发送到哪个处理器.
(2)处理器接口(CPU interface):处理器通过处理器接口接收中断.处理器接口提供的寄存器用来屏蔽和识别中断,控制中断的状态.每个处理器有一个单独的处理器接口.
软件通过中断号识别中断,每个中断号唯一对应一个中断源.
中断有以下4种类型:
(1)软件生成的中断(SGI):中断号1~15,通常用来实现处理器间中断(IPI).这种中断是由软件写分发器的软件生成中断寄存器(GICD_SGIR)生成的.
(2)私有外设中断(PPI):中断号16~31.处理器私有的中断源,不同的处理器的相同中断源没有关系,比如每个处理器的定时器.
(3)共享外设中断(SPI):中断号32~1020.这种中断可以被中断控制器转发到多个处理器.
(4)局部特定外设中断(LPI):基于消息的中断.
中断有以下4种状态:
(1)Inactive:中断源没有发送中断.
(2)Pending:中断源已经发送中断,等待处理器处理.
(3)Active:处理器已经确认中断,正在处理.
(4)Active and pending:处理器正在处理中断,相同的中断源又发送了一个中断.
中断的状态转换过程如下:
(1)Inactive->Pending:外设发送了中断.
(2)Pending->Active:处理器确认了中断.
(3)Active->Inactive:处理器处理完中断.
处理器可以通过中断控制器的寄存器访问中断控制器.中断控制器的寄存器和物理内存使用统一的物理地址空间,把寄存器的物理地址映射到内核的虚拟地址空间,可以像访问内存一样访问寄存器.所有处理器可以访问公共的分发器,但是每个处理器使用相同的地址只能访问自己私有的处理器接口.
外设把中断发送给分发器,如果中断的状态是Inactive,那么切换到Pending;如果中断的状态已经是Active那么切换到Active and pending.
分发器取出优先级最高的状态为pending的中断,转发到目标处理器的处理器接口,然后处理器接口把中断发送到处理器.
处理器取出中断,执行中断处理器程序,中断处理程序读取处理器接口重的中断确认寄存器,得到中断号,读取操作导致分发器里面的中断状态切换到Active.中断处理程序根据中断号可以知道中断由哪个设备发出,从而调用该设备的处理程序.
中断处理程序执行的时候,把中断号写到处理器接口的中断结束寄存器中,指示中断处理完成,分发器里面的中断状态从Active切换到Inactive,或者从Active and pending切换到Pending.
不同种类的中断控制器的访问方法存在差异,内核定义了中断控制器描述符irq_chip,每种中断控制器自定义各种操作函数,GIV v2控制器的描述符如下:
static struct irq_chip gic_eoimode1_chip = {
.name = "GICv2",
.irq_mask = gic_eoimode1_mask_irq,
.irq_unmask = gic_unmask_irq,
.irq_eoi = gic_eoimode1_eoi_irq,
.irq_set_type = gic_set_type,
#ifdef CONFIG_SMP
.irq_set_affinity = gic_set_affinity,
#endif
.irq_get_irqchip_state = gic_irq_get_irqchip_state,
.irq_set_irqchip_state = gic_irq_set_irqchip_state,
.irq_set_vcpu_affinity = gic_irq_set_vcpu_affinity,
.flags = IRQCHIP_SET_TYPE_MASKED |
IRQCHIP_SKIP_SET_WAKE |
IRQCHIP_MASK_ON_SUSPEND,
};
2.2 中断域
一个大型系统可能有多个中断控制器,这些控制器可以级联,一个中断控制器作为中断源连接到另一个中断控制器,但只有一个中断控制器作为根控制器直接连接到处理器.为了把每个中断控制器本地的硬件中断号映射到全局唯一的Linux中断号(虚拟中断号),内核定义了中断域irq_domain,每个中断控制器有自己的中断域.
2.2.1 创建中断域
中断控制器的驱动程序使用分配函数irq_domain_add_*()创建和注册中断域.不同的映射方式提供不同的分配函数.
线性映射
static inline struct irq_domain *irq_domain_add_linear(struct device_node *of_node,
unsigned int size,
const struct irq_domain_ops *ops,
void *host_data)
{
return __irq_domain_add(of_node_to_fwnode(of_node), size, size, 0, ops, host_data);
}
树映射:如果硬件中断号可能非常大,那么树映射是好的选择.
static inline struct irq_domain *irq_domain_add_tree(struct device_node *of_node,
const struct irq_domain_ops *ops,
void *host_data)
{
return __irq_domain_add(of_node_to_fwnode(of_node), 0, ~0, 0, ops, host_data);
}
不映射:有些中断控制很强,硬件中断号是可以配置的.直接把Linux中断号写到硬件,硬件中断号就是Linux中断号,不需要映射.
static inline struct irq_domain *irq_domain_add_nomap(struct device_node *of_node,
unsigned int max_irq,
const struct irq_domain_ops *ops,
void *host_data)
{
return __irq_domain_add(of_node_to_fwnode(of_node), 0, max_irq, max_irq, ops, host_data);
}
分配函数把主要工作委托给函数__irq_domain_add,函数__irq_domain_add的执行过程是:分配一个irq_domain结构体,初始化成员,然后把中断域添加到全局链表irq_domain_list中.
2.2.2 创建映射
创建中断域以后,需要向中断域添加硬件中断号到Linux中断号的映射,内核提供了函数irq_create_mapping:
输入参数是中断域和硬件中断号,返回Linux中断号.该函数首先分配Linux中断号,然后把硬件中断号到Linux中断号的映射添加到中断域.
unsigned int irq_create_mapping(struct irq_domain *domain,
irq_hw_number_t hwirq)
{
struct device_node *of_node;
int virq;
if (domain == NULL)
domain = irq_default_domain;
if (domain == NULL) {
WARN(1, "%s(, %lx) called with NULL domain\n", __func__, hwirq);
return 0;
}
of_node = irq_domain_get_of_node(domain);
virq = irq_find_mapping(domain, hwirq);
if (virq) {
pr_debug("-> existing mapping on virq %d\n", virq);
return virq;
}
virq = irq_domain_alloc_descs(-1, 1, hwirq, of_node_to_nid(of_node));
if (virq <= 0) {
pr_debug("-> virq allocation failed\n");
return 0;
}
if (irq_domain_associate(domain, virq, hwirq)) {
irq_free_desc(virq);
return 0;
}
return virq;
}
EXPORT_SYMBOL_GPL(irq_create_mapping);
2.2.3 查找映射
中断处理程序需要根据硬件中断号查找Linux中断号,内核提供了函数irq_find_mapping:
输入参数是中断域和硬件中断号,返回Linux中断号.
unsigned int irq_find_mapping(struct irq_domain *domain,
irq_hw_number_t hwirq)
{
struct irq_data *data;
if (domain == NULL)
domain = irq_default_domain;
if (domain == NULL)
return 0;
if (hwirq < domain->revmap_direct_max_irq) {
data = irq_domain_get_irq_data(domain, hwirq);
if (data && data->hwirq == hwirq)
return hwirq;
}
if (hwirq < domain->revmap_size)
return domain->linear_revmap[hwirq];
rcu_read_lock();
data = radix_tree_lookup(&domain->revmap_tree, hwirq);
rcu_read_unlock();
return data ? data->irq : 0;
}
EXPORT_SYMBOL_GPL(irq_find_mapping);
2.3 中断控制器驱动初始化
ARM64架构使用扁平设备树描述板卡的硬件信息.DTS设备树源文件,DTB设备树二进制文件.设备启动时,引导程序把设备树二进制文件从存储设备读到内存中,引导内核的时候把设备树二进制文件的起始地址传给内核,内核解析设备树二进制文件,得到硬件信息.
在内核初始化的时候,匹配设备树文件中的中断控制器的属性"compatible"和内核的中断控制器匹配表,找到合适的中断控制器驱动程序,执行驱动成勋的初始化函数.函数irqchip_init把主要工作委托给函数of_irq_init,传入中断控制器匹配表的起始地址__irqchip_of_table.
2.4 Linux中断处理
对于中断控制器的每个中断源,内核分配一个Linux中断号和一个中断描述符,在中断描述符中有两个层次的中断处理函数.
(1)第一层处理函数是中断描述符的成员handle_irq;
(2)第二层处理函数是设备驱动程序注册的处理函数.中断描述符有一个中断处理链表,每个中断处理描述符保存设备驱动程序注册的处理函数.因为多个设备可以共享同一个硬件中断号,所以中断处理链表可能挂载多个中断处理描述符.
把硬件中断号映射到Linux中断号的时候,根据硬件中断的类型设置中断描述符的成员handle_irq,以GIC v2控制器为例:
static int gic_irq_domain_map(struct irq_domain *d, unsigned int irq,
irq_hw_number_t hw)
{
struct irq_chip *chip = &gic_chip;
if (static_key_true(&supports_deactivate)) {
if (d->host_data == (void *)&gic_data[0])
chip = &gic_eoimode1_chip;
}
if (hw < 32) {
irq_set_percpu_devid(irq);
irq_domain_set_info(d, irq, hw, chip, d->host_data,
handle_percpu_devid_irq, NULL, NULL);
irq_set_status_flags(irq, IRQ_NOAUTOEN);
} else {
irq_domain_set_info(d, irq, hw, chip, d->host_data,
handle_fasteoi_irq, NULL, NULL);
irq_set_probe(irq);
}
return 0;
}
(1)如果硬件中断号小于32,说明是软件生成的中断或者私有外设中断,那么把中断描述符的成员handle_irq设置为函数handle_percpu_devid_irq.
(2)如果硬件中断号大于或等于32,说明是共享外设中断,那么把中断描述符的成员handle_irq设置为handle_fasteoi_irq.
设备驱动程序可以使用函数request_irq注册中断处理函数.
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);
}
(1)参数irq是Linux中断号.
(2)参数handler是处理函数.
(3)参数flags是标志位.
(4)参数name是设备名称.
(5)参数dev是传给处理函数的参数.
假设处理器在用户模式下执行64位应用程序,中断控制器是GIC v2控制器,Linux中断处理流程如下:
(1)读取处理器接口的中断确认寄存器得到中断号,分发器里面的中断状态切换到Active.
(2)如果硬件中断号大于15且小于1020,即中断是由外围设备发送的,处理器如下:
a. 把中断号写到处理器接口的中断结束寄存器中,指示中断处理完成,分发器里面的中断状态从Active或者Active and pending切换到pending.
b. 调用函数irq_enter,进入中断上下文.
c. 调用函数irq_find_mapping,根据硬件中断号查找Linux中断号.
d. 调用中断描述符的成员handle_irq.
e. 调用函数irq_exit,退出中断上下文.
(3)如果硬件中断号小于16,即中断是由软件生成的,处理如下:
a. 把中断号写到处理器接口的中断结束寄存器中,指示中断处理完整.
b. 调用函数handle_IPI进行处理.
2.5 中断线程化
中断线程化就是使用内核线程处理中断,目的就是减少系统关中断的时间,增强系统的实时性.内核提供的函数request_threaded_irq用来注册线程化的中断.
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);
参数thread_fn是线程处理函数.
少数中断不能线程化,典型的例子是时钟中断,时钟中断是调度器的脉搏,内核依靠周期性的时钟中断夺回处理器的控制权.对于不能线程化的中断,注册处理函数的时候必须设置标志IRQF_NO_THREAD.如果开启了强制中断线程化的配置宏CONFIG_IRQ_FORCED_THREAD,那么除了标志IRQF_NO_THREAD以外的所有中断线程化.
每个中断处理描述符对应一个内核线程,成员thread指向内核线程的进程描述符,成员thread_fn指向线程处理函数.
struct irqaction {
irq_handler_t handler;
void *dev_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;
} ____cacheline_internodealigned_in_smp;
中断处理线程是优先级为50,调度策略是SCHED_FIFO的实时内核线程.
在中断处理程序中,如果是共享外设中断,中断描述符的成员handle_irq是函数handle_fasteoi_irq,handle_fasteoi_irq调用函数handle_irq_event,执行设备驱动程序注册的处理函数.函数handle_irq_event把主要工作委托给函数handle_irq_event_percpu.该函数遍历中断描述符的中断处理链表,执行每个中断处理描述符的处理函数.如果处理函数返回IRQ_WAKE_THREAD,说明是线程化的中断,那么唤醒中断处理线程.
irqreturn_t handle_irq_event_percpu(struct irq_desc *desc)
{
irqreturn_t retval = IRQ_NONE;
unsigned int flags = 0, irq = desc->irq_data.irq;
struct irqaction *action = desc->action;
while (action) {
irqreturn_t res;
trace_irq_handler_entry(irq, action);
res = action->handler(irq, action->dev_id);
trace_irq_handler_exit(irq, action, res);
switch (res) {
case IRQ_WAKE_THREAD:
if (unlikely(!action->thread_fn)) {
warn_no_thread(irq, action);
break;
}
__irq_wake_thread(desc, action);
case IRQ_HANDLED:
flags |= action->flags;
break;
default:
break;
}
retval |= res;
action = action->next;
}
add_interrupt_randomness(irq, flags);
if (!noirqdebug)
note_interrupt(desc, retval);
return retval;
}
中断处理线程的处理函数是irq_thread,调用函数irq_thread_fn,然后函数irq_thread_fn调用注册的线程处理函数.
2.6 禁止/开启中断
软件可以禁止中断,使处理器不响应所有中断请求,但是不可屏蔽中断(NMI)是个例外.
禁止中断的接口如下:
(1)local_irq_disable().
(2)local_irq_save(flags):首先把中断状态保存到参数flags中,然后禁止中断.
这两个接口只能禁止本处理器的中断,不能禁止其他处理器的中断.禁止中断以后,处理器不会响应中断请求.
开启中断的接口如下:
(1)local_irq_enable().
(2)local_irq_restore(flags):恢复本处理器的中断状态.
local_irq_disable和local_irq_enable不能嵌套使用,local_irq_save可以嵌套使用.
软件可以禁止某个外围设备的中断,中断控制器不会把该设备发送的中断转发给处理器.
禁止单个中断的函数是:
void disable_irq(unsigned int irq)
{
if (!__disable_irq_nosync(irq))
synchronize_irq(irq);
}
EXPORT_SYMBOL(disable_irq);
irq是Linux中断号.
开启单个中断的函数是:
void enable_irq(unsigned int irq)
{
unsigned long flags;
struct irq_desc *desc = irq_get_desc_buslock(irq, &flags, IRQ_GET_DESC_CHECK_GLOBAL);
if (!desc)
return;
if (WARN(!desc->irq_data.chip,
KERN_ERR "enable_irq before setup/request_irq: irq %u\n", irq))
goto out;
__enable_irq(desc);
out:
irq_put_desc_busunlock(desc, flags);
}
EXPORT_SYMBOL(enable_irq);
最终是通过设置分发器的寄存器实现.
中断亲和性:管理员可以设置中断亲和性,允许中断控制器把某个中断转发给哪些处理器,可通过proc文件系统实现进行配置.
内核提供了设置中断亲和性的函数:
static inline int
irq_set_affinity(unsigned int irq, const struct cpumask *cpumask)
{
return __irq_set_affinity(irq, cpumask, false);
}
2.7 处理器间中断
处理器间中断(IPI)是一种特殊的中断,在多处理器系统中,一个处理器可以向其他处理器发送中断,要求目标处理器执行某件事情.
(1)在所有其他处理器上执行一个函数
#define smp_call_function(func, info, wait) \
(up_smp_call_function(func, info))
参数func是要执行的函数,目标处理器在中断处理程序中执行该函数;参数info是传给函数func的参数;参数wait表示是否需要等待目标处理器执行完函数.
(2)在指定的处理器上执行一个函数
int smp_call_function_single(int cpu, smp_call_func_t func, void *info,
int wait)
(3)要求指定的处理器重新调用进程
void smp_send_reschedule(int cpu)
对于ARM64架构的GIC控制器,把处理器间中断称为软件生成的中断,可以写分发器的寄存器GICD_SGIR以生成处理器间中断.
前面提到的函数handle_IPI负责处理处理器间中断,参数ipinr是硬件中断号.
目前支持7种处理器间中断.
(1)IPI_RESCHEDULE:硬件中断号是0,重新调用进程,函数smp_send_reschedule生成的中断.
(2)IPI_CALL_FUNC:硬件中断号是1,执行函数.
(3)IPI_CPU_STOP:硬件中断号是2,使处理器停止.
(4)IPI_CPU_CRASH_STOP:硬件中断号是3,使处理器停止.
(5)IPI_TIMER:硬件中断号是4,广播的时钟事件.
(6)IPI_IRQ_WORK:硬件中断号是5,在硬中断上下文执行回调函数,函数irq_work_queue生成的中断.
(7)IPI_WAKEUP:硬件中断号是6,唤醒处理器.
2.8 中断下半部
为了避免处理复杂的中断嵌套,中断处理程序在关中断的情况下执行的.但关闭中断的时间太长,可能会导致中断请求丢失.最激进的解决方式是中断线程化.常用的解决方法是:把中断处理程序分为两部分,上半部在关中断的情况下执行,只做对时间非常敏感,与硬件相关或者不能被其他中断打断的工作;下半部(bottom half, bh):在开启中断的情况下执行,可以被其他中断打断.
上半部称为硬中断,下半部有3种:软中断(softir),小任务(tasklet)和工作队列(workqueue).3种下半部的区别如下:
(1)软中断和小任务不允许睡眠;工作队列是使用内核线程实现的,处理函数可以睡眠.
(2)软中断的种类是编译时静态定义的,在运行时不能添加或删除;小任务可以在运行时添加或删除.
(3)同一种软中断的处理函数可以在多个处理器上同时执行,处理函数必须是可以重入的,需要使用锁保护临界区;一个小任务同一时刻只能在一个处理器上执行,不要求处理函数是可以重入的.
2.8.1 软中断
软中断(softirq)是中断处理程序在开启中断的情况下执行的部分,可以被硬中断抢占.内核定义了一张软中断向量表,每种软中断有一个唯一的编号,对应一个softirq_action实例,softirq_action实例的成员action是处理函数.
static struct softirq_action softirq_vec[NR_SOFTIRQS] __cacheline_aligned_in_smp;
struct softirq_action
{
void (*action)(struct softirq_action *);
};
目前内核定义了10种软中断,各种软中断的编号如下:
enum
{
HI_SOFTIRQ=0,//高优先级的小任务
TIMER_SOFTIRQ,//定时器软中断
NET_TX_SOFTIRQ,//网络栈发送报文的软中断
NET_RX_SOFTIRQ,//网络栈接受报文的软中断
BLOCK_SOFTIRQ,//块设备软中断
BLOCK_IOPOLL_SOFTIRQ,//支持IO轮询的块设备软中断.
TASKLET_SOFTIRQ,//低优先级的小任务
SCHED_SOFTIRQ,//调度软中断
HRTIMER_SOFTIRQ, /* Unused, but kept as tools rely on the
numbering. Sigh! */
RCU_SOFTIRQ, /* Preferable RCU should always be the last softirq */
NR_SOFTIRQS
};
通过open_softirq注册软中断的处理函数,通过raise_softirq用来触发软中断;在已经禁止中断的情况下可以调用函数raise_softirq_irqoff来触发软中断.
执行软中断:
(1)在中断处理程序的后半部分执行软中断,对执行时间有限制:不能吵过2毫秒,并且最多执行10次.
(2)每个处理器有一个软中断线程,调度策略是SCHED_NORMAL,优先级是120.
(3)开启软中断的函数local_bh_enable.
如果开启了强制中断线程化,那么所有软中断由软中断线程执行.
在中断处理程序的后半部分,调用函数irq_exit以退出中断上下文,处理软中断.
2.8.2 抢占计数器
每个进程的thread_info结构体有一个抢占计数器:preempt_count,它用来表示当前进程能不能被抢占.
抢占:指当前进程在内核模式下运行的时候可以被其他进程抢占,如果优先级更高的进程处于就绪状态,强行剥夺当前进程的处理器使用权.
如果抢占计数器为0,表示可以抢占;如果不为0,表示不能抢占.内核按照各种场景对抢占计数器的位进行了划分.其中0 ~ 7位抢占计数,8 ~ 15位是软中断计数,16~19位是硬中断计数,第20位是不可屏蔽中断计数.
各种场景分别利用各自的位禁止或开启抢占.
(1)普通场景(PREEMPT_MASK):preempt_disable和preempt_enable.
(2)软中断场景(SOFTIRQ_MASK):local_bh_disable和local_bh_enable.
(3)硬中断场景(HARDIRQ_MASK):__irq_enter和__irq_exit.
(4)不可屏蔽中断场景(NMI_MASK)nmi_enter和nmi_exit.
反过来,也可以通过抢占计数器的值判断当前处在什么场景.
2.8.3 tasklet
tasklet是基于软中断实现的.根据优先级可以分为两种:低优先级tasklet(软中断TASKLET_SOFTIRQ)和高优先级tasklet(软中断HI_SOFTIRQ).
2.8.4 工作队列
工作队列(work queue)是使用内核线程异步执行函数的通用机制.工作队列是中断处理程序的一种下半部机制,中断处理程序可以把耗时比较长并且可能睡眠的函数交给工作队列.
同时,内核的很多模块需要异步执行函数,这些模块可以创建一个内核线程来异步执行函数.但是每个模块都能创建自己的内核线程,会造成内核线程的数量过多,内存消耗较大,影响系统性能.所以,最好的方法是提供一种通用机制,让这些模块把需要异步执行的函数交给工作队列执行,共享内核线程,节省资源.
编程接口
内核使用工作项保存需要异步执行的函数,工作项的数据类型是work_struct,需要异步执行的函数的原型如下所示:
typedef void (*work_func)(struct work_struct *work);
有一类工作项称为延迟工作项,数据结构是delayed_work.把延迟工作项添加到工作队列中的时候,延迟一段时间才会真正加入到工作队列中.延迟工作项是工作项和定时器的结合,可以避免使用者自己创建定时器.
我们可以使用内核定义的工作队列,也可以自己创建专用的工作队列.内核定义了以下工作队列:
extern struct workqueue_struct *system_wq;
extern struct workqueue_struct *system_highpri_wq;
extern struct workqueue_struct *system_long_wq;
extern struct workqueue_struct *system_unbound_wq;
extern struct workqueue_struct *system_freezable_wq;
extern struct workqueue_struct *system_power_efficient_wq;
extern struct workqueue_struct *system_freezable_power_efficient_wq;
定义工作项
定义一个静态的工作项,参数n是变量名称,参数f是工作项的处理函数.
DECLEAR_WORK(n,f)
定义一个静态的延迟工作项,参数n是变量名称,参数f是工作项的处理函数.
DECLEAR_DELAYED_WORK(n,f)
在运行时动态初始化工作项,方法如下:
INIT_WORK(_work,_func):初始化一个工作项,参数_work是工作项的地址,参数_func是需要异步执行的函数.
INIT_DELAYED_WORK(_work,_func):初始化一个延时工作项,参数_work是延迟工作项的地址,参数_func是需要异步执行的函数.
在全局工作队列中添加一个工作项.
static inline bool schedule_work(struct work_struct *work);
在全局工作队列中添加一个延迟工作项.
static inline bool schedule_delayed_work_on(int cpu, struct delayed_work *dwork,
unsigned long delay);
冲刷全局队列,确保全局工作队列的所有工作项执行完.
static inline void flush_scheduled_work(void);
分配工作队列
#define alloc_workqueue(fmt, flags, max_active, args...) \
__alloc_workqueue_key((fmt), (flags), (max_active), \
NULL, NULL, ##args)
在指定的工作队列中添加一个工作项
bool queue_work_on(int cpu, struct workqueue_struct *wq,
struct work_struct *work);
在指定的工作队列中添加一个延迟工作项
static inline bool queue_delayed_work(struct workqueue_struct *wq,
struct delayed_work *dwork,
unsigned long delay);
冲刷工作队列,确保工作队列中的所有项执行完.
void flush_workqueue(struct workqueue_struct *wq);
技术原理
work:工作,也称为工作项.
work_queue:工作队列,就是工作的集合,work_queue和work是一对多的关系.
worker:工人,一个工人对应一个内核线程,我们把工人对应的内核线程称为工人线程.
worker_pool:工人池,就是工人的集合,工人池和工作是一对多的关系.
pool_workqueue:中介,负责建立工作队列和工人池之间的关系.工作队列和pool_workqueue是一对多的关系,pool_workqueue是工人池是一对一的关系
工作队列分两种:
(1)绑定处理器的工作队列:默认创建绑定处理器的工作队列,每个工人线程绑定到一个处理器.
工作队列在每个处理器上有一个pool_workqueue实例,一个pool_workqueue实例对应一个工人池,一个工人池有一条工人链表,每个工人对应一个内核线程.向工作队列中添加工作项的时候,选择当前处理器的pool_workqueue实例,工人池和工人线程.
(2)不绑定处理器的工作队列:创建工作队列的时候需要指定标志位WQ_UNBOUND,工人线程不绑定到某个处理器,可以在处理器之间迁移.
工作队列在每个内存节点上有一个pool_workqueue实例,一个pool_workqueue实例对应一个工人池,一个工人池有一条工人链表,每个工人对应一个内核线程.向工作队列中添加工作项的时候,选择当前处理器所属的内存节点的pool_workqueue实例,工人池和工人线程.
3 系统调用
系统调用是内核给用户程序提供的编程接口.用户程序调用系统调用,通常使用glibc库针对单个系统调用封装的函数.如果glibc库没有对针对某个系统调用封装函数,用户程序可以使用通用的封装函数syscall.
ARM64处理器提供的系统调用指令svc,调用约定如下:
(1)64位应用程序使用寄存器x8传递系统调用号,32位应用程序使用寄存器x7传递系统调用号.
(2)使用寄存器x0~x6最多可以传递7个参数.
(3)当系统调用执行完的时候,使用寄存器x0存放返回值.
3.1 定义系统调用
Linux内核使用宏SYSCALL_DEFINE定义系统调用.
#define SYSCALL_DEFINE0(sname) \
SYSCALL_METADATA(_##sname, 0); \
asmlinkage long sys_##sname(void)
#define SYSCALL_DEFINE1(name, ...) SYSCALL_DEFINEx(1, _##name, __VA_ARGS__)
#define SYSCALL_DEFINE2(name, ...) SYSCALL_DEFINEx(2, _##name, __VA_ARGS__)
#define SYSCALL_DEFINE3(name, ...) SYSCALL_DEFINEx(3, _##name, __VA_ARGS__)
#define SYSCALL_DEFINE4(name, ...) SYSCALL_DEFINEx(4, _##name, __VA_ARGS__)
#define SYSCALL_DEFINE5(name, ...) SYSCALL_DEFINEx(5, _##name, __VA_ARGS__)
#define SYSCALL_DEFINE6(name, ...) SYSCALL_DEFINEx(6, _##name, __VA_ARGS__)
SYSCALL_DEFINE后面的数字表示系统调用的参数个数,SYSCALL_DEFINE0表示系统调用没有参数,SYSCALL_DEFINE6表示系统调用有6个参数.如果参数超过6个,使用宏SYSCALL_DEFINEx.
系统调用的函数以sys_开头.
需要在系统调用表中保存系统调用号和处理函数的映射关系,ARM64架构定义的系统调用表sys_call_table如下:
void *sys_call_table[__NR_syscalls] = {
[0 ... __NR_syscalls-1] = sys_ni_syscall,
#include
};
#define __NR_restart_syscall 0
__SYSCALL(__NR_restart_syscall, sys_restart_syscall)
#define __NR_exit 1
__SYSCALL(__NR_exit, sys_exit)
#define __NR_fork 2
__SYSCALL(__NR_fork, sys_fork)
#define __NR_read 3
__SYSCALL(__NR_read, sys_read)
#define __NR_write 4
__SYSCALL(__NR_write, sys_write)
#define __NR_open 5
__SYSCALL(__NR_open, compat_sys_open)
#define __NR_close 6
__SYSCALL(__NR_close, sys_close)
/* 7 was sys_waitpid */
__SYSCALL(7, sys_ni_syscall)
#define __NR_creat 8
__SYSCALL(__NR_creat, sys_creat)
#define __NR_link 9
__SYSCALL(__NR_link, sys_link)
#define __NR_unlink 10
__SYSCALL(__NR_unlink, sys_unlink)
#define __NR_execve 11
__SYSCALL(__NR_execve, compat_sys_execve)
#define __NR_chdir 12
__SYSCALL(__NR_chdir, sys_chdir)
...
3.2 执行系统调用
ARM64处理器把系统调用划分到同步异常,在异常级别1的异常向量表中,系统调用的入口有两个:
(1)如果是64位应用程序执行系统调用指令svc,系统调用入口是el0_sync.
(2)如果是32位应用程序执行系统调用指令svc,系统调用入口是el0_sync_compat.
以el0_sync为例,读取异常寄存器esr_el1,解析异常症状寄存器的异常类型字段,如果是系统调用,跳转到el0_svc.
el0_svc负责执行系统调用.el0_svc根据sys_call_table和调用号执行相应的系统调用函数.