Linux内核中断系统处理机制

一、中断概述

中断是指在CPU正常运行期间,由于内外部事件或由程序预先安排的事件引起的CPU暂时停止正在运行的程序,转而为该内部或外部事件或预先安排的事件服务的程序中去,服务完毕后再返回去继续运行被暂时中断的程序。

1.1中断类型

同步中断由CPU本身产生,又称为内部中断。这里同步是指中断请求信号与代码指令之间的同步执行,在一条指令执行完毕后,CPU才能进行中断,不能在执行期间。所以也称为异常(exception)。

异步中断是由外部硬件设备产生,又称为外部中断,与同步中断相反,异步中断可在任何时间产生,包括指令执行期间,所以也被称为中断(interrupt)。

异常又可分为可屏蔽中断(Maskable interrupt)和非屏蔽中断(Nomaskable interrupt)。而中断可分为故障(fault)、陷阱(trap)、终止(abort)三类。

从广义上讲,中断又可分为四类:中断故障陷阱终止。这些类别之间的异同点请参考表 1。

有些参考资料中按照中断来源进行分类,如下图所示(个人建议不采用这种方式):

图1-1

1.2区分中断号与中断向

I/O设备把中断信号发送给中断控制器(8259A)时与之相关联的是一个中断号,当中断控制器把中断信号发送给CPU时与之关联的是一个中断向量。换个角度分析就是中断号是从中断控制器层面划分,中断向量是从CPU层面划分,所以中断号与中断向量之间存在一对一映射关系。在Intel X86中最大支持256种中断,从0到255开始编号,这个8位的编号就是中断向量。其中将0到31保留用于异常处理和不可屏蔽中断。

二、中断数据处理结构

Linux内核中处理中断主要有三个数据结构,irq_desc,irq_chip和irqaction。

在\include\linux\ irq.h中定义了

1)irq_desc用于描述IRQ线的属性与状态,被称为中断描述符。

/** * struct irq_desc - interrupt descriptor * @irq:           interrupt number for this descriptor * @timer_rand_state:    pointer to timer rand state struct * @kstat_irqs:        irq stats per cpu * @irq_2_iommu:  iommu with this irq * @handle_irq:             highlevel irq-events handler [if NULL, __do_IRQ()] * @chip:         low level interrupt hardware access * @msi_desc:        MSI descriptor * @handler_data:  per-IRQ data for the irq_chip methods * @chip_data:        platform-specific per-chip private data for the chip *                    methods, to allow shared chip implementations * @action:             the irq action chain * @status:             status information * @depth:             disable-depth, for nested irq_disable() calls * @wake_depth:           enable depth, for multiple set_irq_wake() callers * @irq_count:        stats field to detect stalled irqs * @last_unhandled:      aging timer for unhandled count * @irqs_unhandled:      stats field for spurious unhandled interrupts * @lock:         locking for SMP * @affinity:            IRQ affinity on SMP * @node:       node index useful for balancing * @pending_mask: pending rebalanced interrupts * @threads_active: number of irqaction threads currently running * @wait_for_threads:    wait queue for sync_irq to wait for threaded handlers * @dir:           /proc/irq/ procfs entry * @name:             flow handler name for /proc/interrupts output */ struct irq_desc{   unsigned int          irq;   struct timer_rand_state *timer_rand_state;   unsigned int            *kstat_irqs; #ifdef CONFIG_INTR_REMAP   struct irq_2_iommu      *irq_2_iommu; #endif   irq_flow_handler_t handle_irq;   struct irq_chip              *chip;   struct msi_desc            *msi_desc;   void               *handler_data;   void               *chip_data;   struct irqaction      *action;   /* IRQ action list */   unsigned int          status;            /* IRQ status */    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;   spinlock_t             lock; #ifdef CONFIG_SMP   cpumask_var_t             affinity;   unsigned int          node; #ifdef CONFIG_GENERIC_PENDING_IRQ   cpumask_var_t             pending_mask; #endif #endif   atomic_t         threads_active;   wait_queue_head_t       wait_for_threads; #ifdef CONFIG_PROC_FS   struct proc_dir_entry    *dir; #endif   const char             *name; }

2)irq_chip用于描述不同类型的中断控制器。

/** * struct irq_chip - hardware interrupt chip descriptor * * @name:             name for /proc/interrupts * @startup:           start up the interrupt (defaults to ->enable if NULL) * @shutdown:              shut down the interrupt (defaults to ->disable if NULL) * @enable:            enable the interrupt (defaults to chip->unmask if NULL) * @disable:           disable the interrupt (defaults to chip->mask if NULL) * @ack:          start of a new interrupt * @mask:              mask an interrupt source * @mask_ack:       ack and mask an interrupt source * @unmask:          unmask an interrupt source * @eoi:          end of interrupt - chip level * @end:         end of interrupt - flow level * @set_affinity:      set the CPU affinity on SMP machines * @retrigger:         resend an IRQ to the CPU * @set_type:          set the flow type (IRQ_TYPE_LEVEL/etc.) of an IRQ * @set_wake:        enable/disable power-management wake-on of an IRQ * * @bus_lock:         function to lock access to slow bus (i2c) chips * @bus_sync_unlock:    function to sync and unlock slow bus (i2c) chips * * @release:           release function solely used by UML * @typename:              obsoleted by name, kept as migration helper */ 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);   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; }

在\include\linux\ interrupt.h中定义了 irqaction用来描述特定设备所产生的中断描述符。

/** * struct irqaction - per interrupt action descriptor * @handler:   interrupt handler function * @flags: flags (see IRQF_* above) * @name:      name of the device * @dev_id:     cookie to identify the device * @next:  pointer to the next irqaction for shared interrupts * @irq:    interrupt number * @dir:    pointer to the proc/irq/NN/name entry * @thread_fn: interupt handler function for threaded interrupts * @thread:     thread pointer for threaded interrupts * @thread_flags:    flags related to @thread */ struct irqaction {   irq_handler_t handler;   unsigned long flags;   const char *name;   void *dev_id;   struct irqaction *next;   int irq;   struct proc_dir_entry *dir;   irq_handler_t thread_fn;   struct task_struct *thread;   unsigned long thread_flags; };

三、Linux中断机制

Linux中断机制由三部分组成:

1、中断子系统初始化:内核自身初始化过程中对中断处理机制初始化,例如中断的数据结构以及中断请求等。

2、中断或异常处理:中断整体处理过程。

3、中断API:为设备驱动提供API,例如注册,释放和激活等。

3.1中断子系统初始化

3.1.1中断描述符表(IDT)初始化

中断描述符表初始化需要经过两个过程:

第一个过程在内核引导过程。由两个步骤组成,首先给分配IDT分配2KB空间(256中断向量,每个向量由8bit组成)并初始化;然后把IDT起始地址存储到IDTR寄存器中。

第二个过程内核在初始化自身的start_kernal函数中使用trap_init初始化系统保留中断向量,使用init_IRQ完成其余中断向量初始化。

3.1.2中断请求队列初始化

init_IRQ调用pre_intr_init_hook,进而最终调用init_ISA_irqs初始化中断控制器以及每个IRQ线的中断请求队列。

3.2中断或异常处理

中断处理过程:设备产生中断,并通过中断线将中断信号送往中断控制器,如果中断没有被屏蔽则会到达CPU的INTR引脚,CPU立即停止当前工作,根据获得中断向量号从IDT中找出门描述符,并执行相关中断程序。

异常处理过程:异常是由CPU内部发生所以不会通过中断控制器,CPU直接根据中断向量号从IDT中找出门描述符,并执行相关中断程序。

图3-1

中断控制器处理主要有5个步骤:1.中断请求 2.中断相应 3.优先级比较 4.提交中断向量 5.中断结束。这里不再赘述5个步骤的具体流程。

CPU处理流程主要有6个步骤:1.确定中断或异常的中断向量 2.通过IDTR寄存器找到IDT 3.特权检查 4.特权级发生变化,进行堆栈切换 5.如果是异常将异常代码压入堆栈,如果是中断则关闭可屏蔽中断 6.进入中断或异常服务程序执行。这里不再赘述6个步骤的具体流程。

3.3中断API

内核提供的API主要用于驱动的开发。

注册IRQ:

int request_irq(unsigned int irq, irq_handler_t handler, unsigned long flags, const char *name, void *dev);

释放IRQ:

void free_irq(unsigned int, void *);

注:IRQ线资源非常宝贵,我们在使用时必须先注册,不使用时必须释放IRQ资源。

激活当前CPU中断:

local_irq_enable();

禁止当前CPU中断:

local_irq_disable();

激活指定中断线:

void enable_irq(unsigned int irq);

禁止指定中断线:

void disable_irq(unsigned int irq);

禁止指定中断线:

void disable_irq_nosync(unsigned int irq);

注:此函数调用irq_chip中disable禁止指定中断线,所以不会保证中断线上执行的中断服务程序已经退出。

3.4中断机制划分

由于中断会打断内核中进程的正常调度运行,所以要求中断服务程序尽可能的短小精悍;但是在实际系统中,当中断到来时,要完成工作往往进行大量的耗时处理。因此期望让中断处理程序运行得快,并想让它完成的工作量多,这两个目标相互制约,诞生——顶/底半部机制。

中断处理程序是顶半部——接受中断,它就立即开始执行,但只有做严格时限的工作。能够被允许稍后完成的工作会推迟到底半部去,此后,在合适的时机,底半部会被开终端执行。顶半部简单快速,执行时禁止一些或者全部中断。

底半部稍后执行,而且执行期间可以响应所有的中断。这种设计可以使系统处于中断屏蔽状态的时间尽可能的短,以此来提高系统的响应能力。顶半部只有中断处理程序机制,而底半部的实现有软中断,tasklet和工作队列实现。

图3-2 注:登记中断,将底半部处理程序挂到该设备的低半部执行队列中。

3.4.1顶/底半部划分原则:

 1) 如果一个任务对时间非常敏感,将其放在顶半部中执行;

 2) 如果一个任务和硬件有关,将其放在顶半部中执行;

 3) 如果一个任务要保证不被其他中断打断,将其放在顶半部中执行;

 4) 其他所有任务,考虑放置在底半部执行。

3.4.2底半部实现机制

图3-3

软中断:

软中断作为下半部机制的代表,是随着SMP(share memory processor)的出现应运而生的,它也是tasklet实现的基础(tasklet实际上只是在软中断的基础上添加了一定的机制)。软中断一般是“可延迟函数”的总称,有时候也包括了tasklet(请读者在遇到的时候根据上下文推断是否包含tasklet)。它的出现就是因为要满足上面所提出的上半部和下半部的区别,使得对时间不敏感的任务延后执行,软中断执行中断处理程序留给它去完成的剩余任务,而且可以在多个CPU上并行执行,使得总的系统效率可以更高。它的特性包括:

a)产生后并不是马上可以执行,必须要等待内核的调度才能执行。软中断不能被自己打断,只能被硬件中断打断(上半部)。

b)可以并发运行在多个CPU上(即使同一类型的也可以)。所以软中断必须设计为可重入的函数(允许多个CPU同时操作),因此也需要使用自旋锁来保护其数据结构。

内核中定义了几种软中断的用途:

enum {   HI_SOFTIRQ=0,    TIMER_SOFTIRQ,    NET_TX_SOFTIRQ,    NET_RX_SOFTIRQ,    BLOCK_SOFTIRQ,    BLOCK_IOPOLL_SOFTIRQ,    TASKLET_SOFTIRQ,    SCHED_SOFTIRQ,    HRTIMER_SOFTIRQ,    RCU_SOFTIRQ,  /* Preferable RCU should always be the last softirq */    NR_SOFTIRQS };

Tasklet:

tasklet是通过软中断实现的,所以它本身也是软中断。

软中断用轮询的方式处理。假如正好是最后一种中断,则必须循环完所有的中断类型,才能最终执行对应的处理函数。显然当年开发人员为了保证轮询的效率,于是限制中断个数为32个。

为了提高中断处理数量,顺道改进处理效率,于是产生了tasklet机制。

Tasklet采用无差别的队列机制,有中断时才执行,免去了循环查表之苦。Tasklet作为一种新机制,显然可以承担更多的优点。正好这时候SMP越来越火了,因此又在tasklet中加入了SMP机制,保证同种中断只能在一个cpu上执行。在软中断时代,显然没有这种考虑。因此同一种软中断可以在两个cpu上同时执行,很可能造成冲突。

总结下tasklet的优点:

(1)无类型数量限制;

(2)效率高,无需循环查表;

(3)支持SMP机制;

它的特性如下:

1)一种特定类型的tasklet只能运行在一个CPU上,不能并行,只能串行执行。

2)多个不同类型的tasklet可以并行在多个CPU上。

3)软中断是静态分配的,在内核编译好之后,就不能改变。但tasklet就灵活许多,可以在运行时改变(比如添加模块时)。

工作队列:

上面我们介绍的可延迟函数运行在中断上下文中,于是导致了一些问题,说明它们不可挂起,也就是说软中断不能睡眠、不能阻塞,原因是由于中断上下文出于内核态,没有进程切换,所以如果软中断一旦睡眠或者阻塞,将无法退出这种状态,导致内核会整个僵死。因此,可阻塞函数不能用软中断来实现。但是它们往往又具有可延迟的特性。而且由于是串行执行,因此只要有一个处理时间较长,则会导致其他中断响应的延迟。为了完成这些不可能完成的任务,于是出现了工作队列,它能够在不同的进程间切换,以完成不同的工作。

工作队列能运行在进程上下文,它将工作给一个内核线程,作为中断守护线程来使用。多个中断可以放在一个线程中,也可以每个中断分配一个线程。我们用结构体workqueue_struct表示工作者线程,工作者线程是用内核线程实现的。而工作者线程是如何执行被推后的工作——有这样一个链表,它由结构体work_struct组成,而这个work_struct则描述了一个工作,一旦这个工作被执行完,相应的work_struct对象就从链表上移去,当链表上不再有对象时,工作者线程就会继续休眠。因为工作队列是线程,所以我们可以使用所有可以在线程中使用的方法。

如何选择下半部机制:

  1. 软中断和tasklet运行在中断上下文,工作队列运行在进程上下文。如果需要休眠则选择工作队列,否则选择tasklet;如果对性能要求较高则选择软中断。
  2. 从易用性考虑,首选工作队列,然后tasklet,最后是软中断,因为软中断需要静态创建。
  3. 从代码安全考虑,如果对底半部代码保护不够安全,则选择tasklet,因为相对软中断,tasklet对锁要求低,上面也简述它们工作方式以及运用场景。

四、多处理器系统中断相关概念

4.1处理器间中断

在多处理器系统中,操作系统需要在多个处理器中协调操作,所以需要处理器中断(Inter-Processor-Interrupt,IPI)实现,IPI是一种特殊硬件中断,由CPU送出,其他CPU接收,处理CPU之间通信和同步操作。以下是x86中SMP定义的IPI,中断向量号用十六进制表示:

SPURIOUS_APIC_VECTOR0xffERROR_APIC_VECTOR0xfeRESCHEDULE_VECTOR0xfdCALL_FUNCTION_VECTOR0xfcCALL_FUNCTION_SINGLE_VECTOR0xfbTHERMAL_APIC_VECTOR0xfaTHRESHOLD_APIC_VECTOR0xf9REBOOT_VECTOR0xf8INVALIDATE_TLB_VECTOR_END0xf7INVALIDATE_TLB_VECTOR_START0xf0

4.2中断亲和力

将一个或多个中断服务程序绑定到特定的CPU上处理,这就是中断亲和力(SMP IRQ affinity)。我们可以使用中断亲和力来均衡各个CPU的负载,提高系统处理能力。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

简说Linux内核

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值