linux kernel 中断、异常和系统调用

在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内核中一般是一条跳转指令).

异常向量表.png

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和调用号执行相应的系统调用函数.

你可能感兴趣的:(linux kernel 中断、异常和系统调用)