Linux内核中断系统处理机制-详细分析

日期 内核版本 架构 作者 内容
2018-8-23 Linux-2.6.32

X86

Bystander Linux内核中断
  • 一、中断概述

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

1.1中断类型

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

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

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

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

表 1:中断类别及其行为

类别

原因

异步/同步

返回行为

中断

来自I/O设备的信号

异步

总是返回到下一条指令

陷阱

有意的异常

同步

总是返回到下一条指令

故障

潜在可恢复的错误

同步

返回到当前指令

终止

不可恢复的错误

同步

不会返回

 

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

Linux内核中断系统处理机制-详细分析_第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)初始化

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

  1. 第一个过程在内核引导过程。由两个步骤组成,首先给分配IDT分配2KB空间(256中断向量,每个向量由8bit组成)并初始化;然后把IDT起始地址存储到IDTR寄存器中。
  2. 第二个过程内核在初始化自身的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中找出门描述符,并执行相关中断程序。

Linux内核中断系统处理机制-详细分析_第2张图片

                                                                                      图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和工作队列实现。

Linux内核中断系统处理机制-详细分析_第3张图片

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

3.4.1顶/底半部划分原则:

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

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

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

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

 

3.4.2底半部实现机制

Linux内核中断系统处理机制-详细分析_第4张图片

                                                                                            图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_VECTOR		0xff
ERROR_APIC_VECTOR		0xfe
RESCHEDULE_VECTOR		0xfd
CALL_FUNCTION_VECTOR		0xfc
CALL_FUNCTION_SINGLE_VECTOR	0xfb
THERMAL_APIC_VECTOR		0xfa
THRESHOLD_APIC_VECTOR		0xf9
REBOOT_VECTOR			0xf8
INVALIDATE_TLB_VECTOR_END	0xf7
INVALIDATE_TLB_VECTOR_START	0xf0

 

4.2中断亲和力

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

以上便是本人对Linux中断的理解,若有纰漏欢迎指正!

你可能感兴趣的:(linux内核)