分享

Linux内核中断系统处理机制

 新用户8389DdzY 2021-10-31
日期内核版本架构作者内容
2018-8-23Linux-2.6.32

X86

BystanderLinux内核中断
  • 一、中断概述

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

1.1中断类型

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

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

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

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

表 1:中断类别及其行为

类别

原因

异步/同步

返回行为

中断

来自I/O设备的信号

异步

总是返回到下一条指令

陷阱

有意的异常

同步

总是返回到下一条指令

故障

潜在可恢复的错误

同步

返回到当前指令

终止

不可恢复的错误

同步

不会返回

 

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

                                                                     图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线的属性与状态,被称为中断描述符。

  1. /**
  2. * struct irq_desc - interrupt descriptor
  3. * @irq: interrupt number for this descriptor
  4. * @timer_rand_state: pointer to timer rand state struct
  5. * @kstat_irqs: irq stats per cpu
  6. * @irq_2_iommu: iommu with this irq
  7. * @handle_irq: highlevel irq-events handler [if NULL, __do_IRQ()]
  8. * @chip: low level interrupt hardware access
  9. * @msi_desc: MSI descriptor
  10. * @handler_data: per-IRQ data for the irq_chip methods
  11. * @chip_data: platform-specific per-chip private data for the chip
  12. * methods, to allow shared chip implementations
  13. * @action: the irq action chain
  14. * @status: status information
  15. * @depth: disable-depth, for nested irq_disable() calls
  16. * @wake_depth: enable depth, for multiple set_irq_wake() callers
  17. * @irq_count: stats field to detect stalled irqs
  18. * @last_unhandled: aging timer for unhandled count
  19. * @irqs_unhandled: stats field for spurious unhandled interrupts
  20. * @lock: locking for SMP
  21. * @affinity: IRQ affinity on SMP
  22. * @node: node index useful for balancing
  23. * @pending_mask: pending rebalanced interrupts
  24. * @threads_active: number of irqaction threads currently running
  25. * @wait_for_threads: wait queue for sync_irq to wait for threaded handlers
  26. * @dir: /proc/irq/ procfs entry
  27. * @name: flow handler name for /proc/interrupts output
  28. */
  29. struct irq_desc{
  30. unsigned int irq;
  31. struct timer_rand_state *timer_rand_state;
  32. unsigned int *kstat_irqs;
  33. #ifdef CONFIG_INTR_REMAP
  34. struct irq_2_iommu *irq_2_iommu;
  35. #endif
  36. irq_flow_handler_t handle_irq;
  37. struct irq_chip *chip;
  38. struct msi_desc *msi_desc;
  39. void *handler_data;
  40. void *chip_data;
  41. struct irqaction *action; /* IRQ action list */
  42. unsigned int status; /* IRQ status */
  43. unsigned int depth; /* nested irq disables */
  44. unsigned int wake_depth; /* nested wake enables */
  45. unsigned int irq_count; /* For detecting broken IRQs */
  46. unsigned long last_unhandled; /* Aging timer for unhandled count */
  47. unsigned int irqs_unhandled;
  48. spinlock_t lock;
  49. #ifdef CONFIG_SMP
  50. cpumask_var_t affinity;
  51. unsigned int node;
  52. #ifdef CONFIG_GENERIC_PENDING_IRQ
  53. cpumask_var_t pending_mask;
  54. #endif
  55. #endif
  56. atomic_t threads_active;
  57. wait_queue_head_t wait_for_threads;
  58. #ifdef CONFIG_PROC_FS
  59. struct proc_dir_entry *dir;
  60. #endif
  61. const char *name;
  62. }

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

  1. /**
  2. * struct irq_chip - hardware interrupt chip descriptor
  3. *
  4. * @name: name for /proc/interrupts
  5. * @startup: start up the interrupt (defaults to ->enable if NULL)
  6. * @shutdown: shut down the interrupt (defaults to ->disable if NULL)
  7. * @enable: enable the interrupt (defaults to chip->unmask if NULL)
  8. * @disable: disable the interrupt (defaults to chip->mask if NULL)
  9. * @ack: start of a new interrupt
  10. * @mask: mask an interrupt source
  11. * @mask_ack: ack and mask an interrupt source
  12. * @unmask: unmask an interrupt source
  13. * @eoi: end of interrupt - chip level
  14. * @end: end of interrupt - flow level
  15. * @set_affinity: set the CPU affinity on SMP machines
  16. * @retrigger: resend an IRQ to the CPU
  17. * @set_type: set the flow type (IRQ_TYPE_LEVEL/etc.) of an IRQ
  18. * @set_wake: enable/disable power-management wake-on of an IRQ
  19. *
  20. * @bus_lock: function to lock access to slow bus (i2c) chips
  21. * @bus_sync_unlock: function to sync and unlock slow bus (i2c) chips
  22. *
  23. * @release: release function solely used by UML
  24. * @typename: obsoleted by name, kept as migration helper
  25. */
  26. struct irq_chip {
  27. const char *name;
  28. unsigned int (*startup)(unsigned int irq);
  29. void (*shutdown)(unsigned int irq);
  30. void (*enable)(unsigned int irq);
  31. void (*disable)(unsigned int irq);
  32. void (*ack)(unsigned int irq);
  33. void (*mask)(unsigned int irq);
  34. void (*mask_ack)(unsigned int irq);
  35. void (*unmask)(unsigned int irq);
  36. void (*eoi)(unsigned int irq);
  37. void (*end)(unsigned int irq);
  38. int (*set_affinity)(unsigned int irq,
  39. const struct cpumask *dest);
  40. int (*retrigger)(unsigned int irq);
  41. int (*set_type)(unsigned int irq, unsigned int flow_type);
  42. int (*set_wake)(unsigned int irq, unsigned int on);
  43. void (*bus_lock)(unsigned int irq);
  44. void (*bus_sync_unlock)(unsigned int irq);
  45. /* Currently used only by UML, might disappear one day.*/
  46. #ifdef CONFIG_IRQ_RELEASE_METHOD
  47. void (*release)(unsigned int irq, void *dev_id);
  48. #endif
  49. /*
  50. * For compatibility, ->typename is copied into ->name.
  51. * Will disappear.
  52. */
  53. const char *typename;
  54. }

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

  1. /**
  2. * struct irqaction - per interrupt action descriptor
  3. * @handler: interrupt handler function
  4. * @flags: flags (see IRQF_* above)
  5. * @name: name of the device
  6. * @dev_id: cookie to identify the device
  7. * @next: pointer to the next irqaction for shared interrupts
  8. * @irq: interrupt number
  9. * @dir: pointer to the proc/irq/NN/name entry
  10. * @thread_fn: interupt handler function for threaded interrupts
  11. * @thread: thread pointer for threaded interrupts
  12. * @thread_flags: flags related to @thread
  13. */
  14. struct irqaction {
  15. irq_handler_t handler;
  16. unsigned long flags;
  17. const char *name;
  18. void *dev_id;
  19. struct irqaction *next;
  20. int irq;
  21. struct proc_dir_entry *dir;
  22. irq_handler_t thread_fn;
  23. struct task_struct *thread;
  24. unsigned long thread_flags;
  25. };
  • 三、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中找出门描述符,并执行相关中断程序。

                                                                                      图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同时操作),因此也需要使用自旋锁来保护其数据结构。

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

  1. enum
  2. {
  3. HI_SOFTIRQ=0,
  4. TIMER_SOFTIRQ,
  5. NET_TX_SOFTIRQ,
  6. NET_RX_SOFTIRQ,
  7. BLOCK_SOFTIRQ,
  8. BLOCK_IOPOLL_SOFTIRQ,
  9. TASKLET_SOFTIRQ,
  10. SCHED_SOFTIRQ,
  11. HRTIMER_SOFTIRQ,
  12. RCU_SOFTIRQ, /* Preferable RCU should always be the last softirq */
  13. NR_SOFTIRQS
  14. };

 

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,中断向量号用十六进制表示:

  1. SPURIOUS_APIC_VECTOR0xff
  2. ERROR_APIC_VECTOR0xfe
  3. RESCHEDULE_VECTOR0xfd
  4. CALL_FUNCTION_VECTOR0xfc
  5. CALL_FUNCTION_SINGLE_VECTOR0xfb
  6. THERMAL_APIC_VECTOR0xfa
  7. THRESHOLD_APIC_VECTOR0xf9
  8. REBOOT_VECTOR0xf8
  9. INVALIDATE_TLB_VECTOR_END0xf7
  10. INVALIDATE_TLB_VECTOR_START0xf0

 

4.2中断亲和力

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

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

    本站是提供个人知识管理的网络存储空间,所有内容均由用户发布,不代表本站观点。请注意甄别内容中的联系方式、诱导购买等信息,谨防诈骗。如发现有害或侵权内容,请点击一键举报。
    转藏 分享 献花(0

    0条评论

    发表

    请遵守用户 评论公约

    类似文章 更多