当前位置: 首页 > news >正文

网站建设公司dz000专业制作网站建设

网站建设公司dz000,专业制作网站建设,备份wordpress到百度云,建立个人网页Linux中断是指在Linux操作系统中#xff0c;当硬件设备或软件触发某个事件时#xff0c;CPU会中断正在执行的任务#xff0c;并立即处理这个事件。它是实现实时响应和处理外部事件的重要机制#xff0c;Linux中断可以分为两种类型#xff1a;硬件中断和软件中断#xff0…Linux中断是指在Linux操作系统中当硬件设备或软件触发某个事件时CPU会中断正在执行的任务并立即处理这个事件。它是实现实时响应和处理外部事件的重要机制Linux中断可以分为两种类型硬件中断和软件中断也称为异常。硬件中断是由外部设备引发的如磁盘I/O完成、网络数据包到达等而软件中断是由CPU内部产生的如除零错误、页面故障等。 当一个中断被触发时CPU会根据预定义好的中断向量表找到相应的处理程序并跳转执行该程序。在Linux内核源码中各种硬件设备及其对应的中断处理程序都有相应的驱动程序进行管理通过对Linux中断源码分析可以深入了解内核是如何管理和响应不同类型的中断在调试和性能优化方面也具有重要作用。同时掌握 Linux 中断机制对于编写高性能、稳定的驱动程序以及理解操作系统内核运行原理非常有帮助。 一、Linux中断系统概述 在 Linux 系统的庞大体系中中断系统犹如其 “神经系统”占据着核心地位。它是硬件与软件之间进行高效交互的关键桥梁对系统的响应性以及并发处理能力起着决定性作用。当系统中的硬件设备需要及时处理某些事件时中断机制便会迅速发挥作用促使 CPU 暂停当前正在执行的任务转而优先处理这些紧急事务处理完成后再返回原任务继续执行。这种机制确保了系统能够快速响应外部事件极大地提升了系统的并发处理能力和整体性能。 1.1 中断基础概念 中断简单来说是指计算机在执行程序的过程中当遇到急需处理的事件时会暂停当前正在运行的程序转去执行相应的服务程序待处理完毕后再自动返回原程序继续执行这一过程就称为中断。从硬件层面来看中断是由硬件设备产生的电子信号这些信号通过中断请求线IRQ发送到中断控制器再由中断控制器将其传递给 CPU。例如当键盘有按键被按下时键盘控制器会立即产生一个中断信号通过特定的中断请求线发送给中断控制器进而通知 CPU 有按键事件需要处理。 从软件层面分析当中断信号被 CPU 接收后CPU 会暂停当前执行的指令保存当前程序的执行状态如寄存器的值、程序计数器的值等到堆栈中然后跳转到对应的中断处理程序ISRInterrupt Service Routine去执行相应的处理操作。中断处理程序完成任务后会从堆栈中恢复之前保存的程序执行状态使 CPU 继续执行原来被中断的程序。在整个系统运行过程中中断扮演着至关重要的角色它使得 CPU 能够及时响应各种外部设备的请求实现了硬件与软件之间的高效协同工作避免了 CPU 对外部设备状态的持续轮询大大提高了系统的效率和资源利用率。 1.2 Linux 中断的分类 在 Linux 系统中中断主要分为硬件中断和软件中断两大类它们各自具有独特的特点和应用场景。 硬件中断硬件中断是由外部硬件设备如键盘、鼠标、网卡、硬盘等触发的中断。其特点是异步性即可能在任何时间发生不受 CPU 控制。硬件中断具有较高的优先级一旦发生会立即打断 CPU 当前正在执行的任务使 CPU 迅速响应并处理相应的事件。例如当网卡接收到网络数据包时会立即产生一个硬件中断通知 CPU 有新的数据需要处理。硬件中断在系统中的应用场景非常广泛涵盖了各种外部设备与 CPU 之间的交互。在实时控制系统中传感器的数据采集设备通过硬件中断及时将采集到的数据传递给 CPU 进行处理确保系统能够对外部环境的变化做出快速响应。 软件中断软件中断则是由软件指令触发的中断它是一种在操作系统内部使用的中断机制用于实现系统调用、任务调度、定时器处理等功能。软件中断通常是由内核在特定情况下主动触发的其优先级相对较低。例如当用户程序需要调用系统函数如文件读写、进程创建等时会通过软中断指令如 x86 架构中的 int 0x80 或 syscall 指令将用户态切换到内核态内核根据调用号找到对应的系统调用处理函数进行处理处理完成后再返回用户态。软件中断在操作系统的任务调度方面发挥着重要作用。内核通过软件中断来实现进程的上下文切换当一个进程的时间片用完或者有更高优先级的进程需要运行时内核会触发软件中断暂停当前进程的执行保存其上下文信息然后调度其他进程运行。 1.3可编程中断控制器(PIC、APIC) 为了方便说明这里我们将PIC和APIC统称为中断控制器。中断控制器是作为中断(IRQ)和CPU核之间的一个桥梁而存在的每个CPU内部都有一个自己的中断控制器中断线并不是直接与CPU核相连而是与CPU内部或外部的中断控制器相连。而为什么叫做可编程中断控制器是因为其本身有一定的寄存器CPU可以通过操作设置中断控制器屏蔽某个中断引脚的信号实现硬件上的中断屏蔽。中断控制器也可以级联提供更多的中断线具体如下 如上图CPU的INTR与中断控制器的INT相连INTA与ACK相连当一个外部中断发生时(比如键盘中断IRQ1)中断控制器与CPU交互操作如下 IRQ1发生中断主中断控制器接收到中断信号检查中断屏蔽寄存器IRQ1是否被屏蔽如果屏蔽则忽略此中断信号。 将中断控制器中的中断请求寄存器对应的IRQ1位置位表示收到IRQ1中断。 中断控制器拉高INT引脚电平告知CPU有中断发生。 CPU每执行完一条指令时都会检查INTR引脚是否被拉高这里已被拉高。 CPU检查EFLAGS寄存器的中断运行标志位IF是否为1若为1表明允许中断通过INTA向中断控制器发出应答。 中断控制器接收到应答信号将IRQ1的中断向量号发到数据总线上此时CPU会通过数据总线读取IRQ1的中断向量号。 最后如果中断控制器需要EOI(End of Interrupt)信号CPU则会发送否则中断控制器自动将INT拉低并清除IRQ1对应的中断请求寄存器位。 在linux内核中用struct irq_chip结构体描述一个可编程中断控制器它的整个结构和调度器中的调度类类似里面定义了中断控制器的一些操作如下 struct irq_chip {/* 中断控制器的名字 */const char    *name;/* 控制器初始化函数 */unsigned int    (*irq_startup)(struct irq_data *data);/* 控制器关闭函数 */void        (*irq_shutdown)(struct irq_data *data);/* 使能irq操作通常是直接调用irq_unmask()通过data参数指明irq */void        (*irq_enable)(struct irq_data *data);/* 禁止irq操作通常是直接调用irq_mask严格意义上他俩其实代表不同的意义disable表示中断控制器根本就不响应该irq而mask时中断控制器可能响应该irq只是不通知CPU */void        (*irq_disable)(struct irq_data *data);/* 用于CPU对该irq的回应通常表示cpu希望要清除该irq的pending状态准备接受下一个irq请求 */void        (*irq_ack)(struct irq_data *data);/* 屏蔽irq操作通过data参数表明指定irq */void        (*irq_mask)(struct irq_data *data);/* 相当于irq_mask() irq_ack() */void        (*irq_mask_ack)(struct irq_data *data);/* 取消屏蔽指定irq操作 */void        (*irq_unmask)(struct irq_data *data);/* 某些中断控制器需要在cpu处理完该irq后发出eoi信号 */void        (*irq_eoi)(struct irq_data *data);/*  用于设置该irq和cpu之间的亲和力就是通知中断控制器该irq发生时那些cpu有权响应该irq */int        (*irq_set_affinity)(struct irq_data *data, const struct cpumask *dest, bool force);int        (*irq_retrigger)(struct irq_data *data);/* 设置irq的电气触发条件例如 IRQ_TYPE_LEVEL_HIGH(电平触发) 或 IRQ_TYPE_EDGE_RISING(边缘触发) */int        (*irq_set_type)(struct irq_data *data, unsigned int flow_type);/* 通知电源管理子系统该irq是否可以用作系统的唤醒源 */int        (*irq_set_wake)(struct irq_data *data, unsigned int on);void        (*irq_bus_lock)(struct irq_data *data);void        (*irq_bus_sync_unlock)(struct irq_data *data);void        (*irq_cpu_online)(struct irq_data *data);void        (*irq_cpu_offline)(struct irq_data *data);void        (*irq_suspend)(struct irq_data *data);void        (*irq_resume)(struct irq_data *data);void        (*irq_pm_shutdown)(struct irq_data *data);void        (*irq_calc_mask)(struct irq_data *data);void        (*irq_print_chip)(struct irq_data *data, struct seq_file *p);int        (*irq_request_resources)(struct irq_data *data);void        (*irq_release_resources)(struct irq_data *data);unsigned long    flags; }; 二、中断管理机制剖析 深入探究 Linux 中断管理机制如同探索一座精密而复杂的 “机械迷宫”其底层原理涵盖多个关键方面包括中断向量表与中断描述符的协同工作、中断请求队列的高效管理以及中断上下文与进程上下文的清晰界定。这些要素相互配合确保 Linux 系统在面对各种中断请求时能够有条不紊地协调系统资源实现高效稳定的运行。 2.1 中断向量表与中断描述符 中断向量表在 Linux 中断管理中扮演着 “索引目录” 的关键角色它是一个由中断向量组成的数组每个中断向量都对应着一个唯一的中断号这些中断号就像是一个个 “页码”指向特定的中断处理程序。在 x86 架构中中断向量表的大小通常为 256 个表项其中 0 - 31 号向量被系统保留用于处理 CPU 内部的异常和特殊中断如除零错误、页面错误等32 - 255 号向量则用于外部设备的中断。中断向量表的主要功能是为中断处理提供快速的索引当硬件设备产生中断信号时CPU 通过中断控制器获取中断号然后以这个中断号作为索引在中断向量表中迅速定位到对应的中断处理程序入口地址从而实现对中断的快速响应。 PU把中断向量表的向量类型分为三种类型任务门、中断门、陷阱门。CPU为了防止恶意程序访问中断限制了中断门的权限而在某些时候用户程序又必须使用中断所以Linux把中断描述符的中断向量类型改为了5种中断门系统门系统中断门陷阱门任务门。这个中断向量表的基地址保存在idtr寄存器中。 中断门用户程序不能访问的CPU中断门(权限字段为0)所有的中断处理程序都是这个被限定在内核态执行。会清除IF标志屏蔽可屏蔽中断。 系统门用户程序可以访问的CPU陷阱门(权限字段为3)。我们的系统调用就是通过向量128(0x80)系统门进入的。 系统中断门能够被用户进程访问的CPU陷阱门(权限字段为3)作为一个特别的异常处理所用。 陷阱门用户进程不能访问的CPU陷阱门(权限字段为0)大部分异常处理程序入口都为陷阱门。 任务门用户进程不能访问的CPU任务门(权限字段为0)Double fault异常处理程序入口。 当我们发生异常或中断时系统首先会判断权限字段(安全处理)权限通过则进入指定的处理函数而所有的中断门的中断处理函数都是同一个它首先是一段汇编代码汇编代码操作如下 执行SAVE_ALL宏保存中断向量号和寄存器上下文至当前运行进程的内核栈或者硬中断请求栈(当内核栈大小为8K时保存在内核栈若为4K则保存在硬中断请求栈)。 调用do_IRQ()函数。 跳转到ret_from_intr这是一段汇编代码主要用于判断是否需要进行调度。 中断描述符则是连接中断向量与中断处理程序的 “桥梁”它包含了丰富的信息用于详细描述中断的属性和处理方式。每个中断向量都有一个与之对应的中断描述符这些描述符通常存储在中断描述符表IDTInterrupt Descriptor Table中。中断描述符主要包含以下关键信息段选择子用于指定中断处理程序所在的代码段 偏移量指示中断处理程序在代码段中的具体位置访问权限和标志位这些信息用于控制中断的优先级、特权级以及中断处理过程中的一些特殊行为如中断是否可屏蔽、是否会导致任务切换等。通过这些信息中断描述符为 CPU 提供了准确的指令使其能够顺利地跳转到正确的中断处理程序并在处理中断时遵循相应的规则和权限。 在处理一个外部设备的中断时CPU 首先根据中断号在中断向量表中找到对应的中断描述符从中获取段选择子和偏移量然后通过这两个信息计算出中断处理程序的实际地址进而跳转到该地址执行中断处理程序完成对设备中断的响应和处理。 2.2 中断请求队列与处理流程 中断请求队列是Linux系统为了高效管理中断请求而采用的一种数据结构它类似于一个 “任务队列”用于存放多个中断请求。在实际的计算机系统中由于硬件资源的限制多个设备可能会共享同一条中断请求线IRQ这就导致多个设备的中断请求需要通过同一个中断向量来处理。为了区分这些不同设备的中断请求Linux 系统为每个中断向量设置了一个中断请求队列将共享同一中断向量的所有设备的中断请求组织成一个队列。 中断请求队列的数据结构主要由两部分组成irq_desc 结构体数组和 irqaction 结构体链表。irq_desc 结构体数组是中断请求队列的核心数组中的每个元素都对应着一个中断向量它包含了与该中断向量相关的各种信息如中断的状态是否被禁用、是否正在处理等、中断控制器的信息以及指向 irqaction 结构体链表的指针。irqaction 结构体链表则用于存储具体的中断处理信息每个链表节点代表一个设备的中断请求包含了设备的中断处理函数指针、中断标志位、设备名称以及设备 ID 等信息。通过这种数据结构Linux 系统能够清晰地管理和调度共享同一中断向量的多个设备的中断请求。 每个能够产生中断的设备或者模块都会在内核中注册一个中断服务例程(ISR)当产生中断时中断处理程序会被执行在中断处理程序中首先会保存中断向量号和上下文之后执行中断线对应的中断服务例程。对于CPU来说中断线是非常宝贵的资源而由于计算机的发展外部设备数量和种类越来越多导致了中断线资源不足的情况linux为了应对这种情况实现了两种中断线分配方式分别是共享中断线中断线动态分配。 共享中断线多个设备共用一条中断线当此条中断线发生中断时因为不可能预先知道哪个特定的设备产生了中断因此这条中断线上的每个中断服务例程都会被执行以验证是哪个设备产生的中断(一般的设备产生中断时会标记自己的状态寄存器中断服务例程通过检查每个设备的状态寄存器来查找产生中断的设备)。 中断线动态分配一条中断线在可能使用的时刻才与一个设备驱动程序关联起来这样一来即使几个硬件设备并不共享中断线同一个中断向量也可以由这几个设备在不同时刻运行。 共享中断线的分配方式是比较常见的一次典型的基于共享中断线的中断处理流程如下 由于中断处于中断上下文中所以在中断处理过程中会有以下几个特性 中断处理程序正在运行时CPU会通知中断控制器屏蔽产生此中断的中断线。此中断线发出的信号被暂时忽略当中断处理程序结束时恢复此中断线。 在中断服务例程的设计中原则上是立即处理紧急的操作将非紧急的操作延后处理(交给软中断进行处理)。 中断处理程序是运行在中断上下文但是其是代表进程运行的因此它所代表的进行必须处于TASK_RUNNING状态否则可能出现僵死情况因此在中断处理程序中不能执行任何阻塞过程 当硬件设备产生中断请求时Linux 系统的中断处理流程可以分为以下几个关键步骤 中断触发与识别硬件设备通过中断请求线向中断控制器发送中断信号中断控制器接收到信号后根据信号的来源和配置生成相应的中断号并将其发送给 CPU。 中断响应与上下文切换CPU 在接收到中断号后暂停当前正在执行的任务保存当前任务的上下文信息如寄存器的值、程序计数器的值等到堆栈中然后根据中断号在中断向量表中查找对应的中断处理程序入口地址开始进入中断处理程序执行。 中断请求队列处理中断处理程序首先根据中断号找到对应的 irq_desc 结构体从中获取 irqaction 结构体链表的指针然后遍历链表依次调用每个设备的中断处理函数。在调用中断处理函数之前中断处理程序会根据中断标志位进行一些必要的检查和准备工作如判断中断是否可屏蔽、是否需要保存和恢复中断现场等。 中断处理与返回设备的中断处理函数执行完成后返回一个状态值指示中断处理的结果。中断处理程序根据返回值进行相应的处理如如果所有设备的中断处理函数都返回成功则清除中断标志通知中断控制器中断处理已完成如果有设备的中断处理函数返回失败则可能需要进行一些错误处理如重新发送中断请求或通知用户程序。最后中断处理程序从堆栈中恢复之前保存的任务上下文信息将 CPU 的控制权交还给被中断的任务使其继续执行。 2.3 中断上下文与进程上下文 中断上下文和进程上下文是 Linux 系统中两个重要的概念它们分别代表了系统在不同运行状态下的环境和资源。中断上下文是指系统在处理中断时所处的环境当硬件设备产生中断请求时CPU 会暂停当前正在执行的任务进入中断处理程序执行此时系统就处于中断上下文。在中断上下文下系统主要关注的是如何快速响应和处理中断事件因此中断处理程序通常具有较高的优先级并且不能进行一些可能导致阻塞的操作如睡眠、访问文件系统等。 中断上下文的特点是短暂而高效它的存在是为了确保系统能够及时处理硬件设备的请求保证系统的实时性和稳定性。在处理网络数据包接收中断时中断处理程序需要迅速读取网络设备的缓冲区将数据包复制到系统内存中并通知相关的网络协议栈进行后续处理这个过程必须在短时间内完成以避免数据包丢失或网络延迟增加。 进程上下文则是指系统在执行进程时所处的环境每个进程都有自己独立的进程上下文包括进程的地址空间、寄存器的值、程序计数器的值以及堆栈等。在进程上下文下系统可以进行各种复杂的操作如文件读写、内存分配、进程调度等这些操作可以根据进程的需求进行灵活的调度和管理。进程上下文的切换相对较为复杂需要保存和恢复大量的上下文信息因此开销较大。当一个进程需要进行系统调用时它会通过软中断进入内核态此时系统会将进程上下文切换到内核态的进程上下文执行系统调用的处理函数处理完成后再将上下文切换回用户态的进程上下文。 在中断处理过程中中断上下文和进程上下文会相互切换这种切换对于系统的性能和稳定性有着重要的影响。当中断发生时系统会从当前的进程上下文切换到中断上下文执行中断处理程序中断处理完成后再切换回原来的进程上下文继续执行被中断的任务。 在这个过程中如果中断处理程序执行时间过长可能会导致其他进程的响应时间变长影响系统的整体性能如果中断处理程序在执行过程中进行了一些不恰当的操作如修改了进程上下文的关键数据可能会导致系统出现错误或崩溃。因此在设计和优化中断处理程序时需要充分考虑中断上下文和进程上下文的特点和限制合理安排中断处理的流程和操作以确保系统的高效稳定运行。 三、中断实现原理 3.1中断初始化 中断向量表中断描述符表中断描述符中断控制器描述符中断服务例程。可以说这几个结构组成了整个内核中断框架主体所以内核对整个中断的初始化工作大多集中在了这几个结构上。 在系统中当一个中断产生时首先CPU会从中断向量表中获取相应的中断向量并根据中断向量的权限位判断是否处于该权限之后跳转至中断处理函数在中断处理函数中会根据中断向量号获取中断描述符并通过中断描述符获取此中断对应的中断控制器描述符然后对中断控制器执行应答操作最后执行此中断描述符中的中断服务例程链表最后执行软中断。 而整个初始化的过程与中断处理过程相应首先先初始化中断向量表再初始化中断描述符表和中断描述符。中断控制器描述符是系统预定编写好的静态变量如i8259A中断控制器对应的变量就是i8259A_chip。这时一个中断已经初始化完毕之后驱动需要使用此中断时系统会将驱动中的中断处理加入到该中断的中断服务例程链表中。 如下图 ①初始化中断向量 虽然称之为中断向量表其实对于CPU来说只是一个起始地址此地址开始每向上8个字节为一个中断向量。我们的CPU上有一个idtr寄存器它专门用于保存中断向量表地址当产生一个中断时CPU会自动从idtr寄存器保存的中断向量表地址处获取相应的中断向量然后判断权限并跳转至中断处理函数。当计算机刚启动时首先会启动引导程序(BIOS)在BIOS中会把中断向量表存放在内存开始位置(0x00000000)。 BIOS会有自己的一些默认中断处理函数而当BIOS处理完后会将计算机控制器转交给linux而linux会在使用BIOS的中断向量表的同时重新设置新的中断向量表(新的地址保存在配置中的CONFIG_VECTORS_BASE)之后会完全使用新的中断向量表。 一般的我们也把中断向量表中的中断向量称为门描述符其大小为64位其主要保存了段选择符、权限位和中断处理程序入口地址。CPU主要将门分为三种任务门中断门陷阱门。虽然CPU把门描述符分为了三种但是linux为了处理更多种情况把门描述符分为了五种分别为中断门系统门系统中断门陷阱门任务门但其存储结构与CPU定义的门不变。结构如下 在一个门描述符中 P:代表的是段是否处于内存中因为linux从不把整个段交换的硬盘上所以P都被置为1。 DPL代表的是权限用于限制对这个段的存取当其为0时只有CPL0(内核态)才能够访问这个段当其为3时任何等级的CPL(用户态及内核态)都可以访问。 段选择符除了任务门设置为TSS段陷阱门和中断门都设置为__KERNER_CS(内核代码段)。 偏移量就是中断处理程序入口地址。 门描述符的初始化主要分为两部分我们知道中断向量表中保存的是中断和异常所以整个中断描述符的初始化需要分为中断初始化和异常初始化。而中断向量表的初始化情况是第一部分是经过一段汇编代码对整个中断向量表进行初始化第二部分是在系统进入start_kernel()函数后分别对异常和中断进行初始化。在linux中中断向量表用idt_table[NR_VECTORS]数组进行描述中断向量(门描述符)在系统中用struct desc_struct结构表示具体我们可以往下看。 第一部分 - 汇编代码(arch/x86/kernel/head_32.S) /**  setup_once**  The setup work we only want to run on the BSP.**  Warning: %esi is live across this function.*/ __INIT setup_once:movl $idt_table,%edi                           # idt_table就是中断向量表地址保存到edi中movl $early_idt_handlers,%eax                  # early_idt_handlers地址保存到eax中early_idt_handlers是二维数组每行9个字符movl $NUM_EXCEPTION_VECTORS,%ecx               # NUM_EXCEPTION_VECTORS地址保存到ecx中ecx用于循环NUM_EXCEPTION_VECTORS为32 1:movl %eax,(%edi)                               # 将eax的值保存到edi保存的地址中movl %eax,4(%edi)                              # 将eax的值保存到edi保存的地址4中/* interrupt gate, dpl0, present */movl $(0x8E000000 __KERNEL_CS),2(%edi)       # 将(0x8E000000 __KERNEL_CS)一共4个字节保存到edi保存的地址2的位置中addl $9,%eax                                   # eax 9指向early_idt_handlers数组下一列addl $8,%edi                                   # edi 8就是下一个门描述符地址loop 1b                                        # 根据ecx是否为0进行循环# 前32个中断向量初始化结果#  |63                         48|47                   32|31                  16|15                          0|#  |early_idt_handlers[i](高16位)|        0x8E00         |     __KERNEL_CS      |early_idt_handlers[i](低16位)|                           movl $256 - NUM_EXCEPTION_VECTORS,%ecx         # 256 - 32 保存到ecx进行新一轮的循环movl $ignore_int,%edx                          # ignore_int保存到edxmovl $(__KERNEL_CS 16),%eax                 # (__KERNEL_CS 16)保存到eaxmovw %dx,%ax       movw $0x8E00,%dx 2:movl %eax,(%edi)movl %edx,4(%edi)addl $8,%edi                                   # edi 8就是下一个门描述符地址loop 2b# 其他中断向量初始化结果#  |63                      48|47                   32|31                  16|15                       0|#  |    ignore_int(高16位)    |        0x8E00         |     __KERNEL_CS      |    ignore_int(低16位)    | 如果CPU是486之后会通过 lidt idt_descr 命令将中断向量表(idt_descr)地址放入idtr寄存器如果不是则暂时不会将idt_descr放入idtr寄存器(在trap_init()函数再执行这步操作)。idtr寄存器一共是48位低16位保存的是中断向量表长度高32位保存的是中断向量表基地址。我们可以看看idt_descr的形式如下 idt_descr:.word IDT_ENTRIES*8-1        # 这里放入的是表长度 256 * 8 - 1.long idt_table                # idt_table地址放在这idt_table定义在/arch/x86/kernel/trap.h中/* 我们再看看 idt_table 是怎么定义的idt_table代表的就是中断向量表 */ /* 代码地址arch/x86/kernel/Traps.c */ gate_desc idt_table[NR_VECTORS] __page_aligned_bss;/* 继续看看 gate_desc 用于描述一个中断向量 */ #ifdef CONFIG_X86_64 typedef struct gate_struct64 gate_desc; #else typedef struct desc_struct gate_desc; #endif/* 我们看看32位下的 struct desc_struct此结构就是一个中断向量(门描述符)  */ struct desc_struct {union {struct {unsigned int a;unsigned int b;};struct {u16 limit0;u16 base0;unsigned base1: 8, type: 4, s: 1, dpl: 2, p: 1;unsigned limit: 4, avl: 1, l: 1, d: 1, g: 1, base2: 8;};}; } __attribute__((packed)); 可以看出在汇编代码初始化部分所有的门描述符的DPL权限位都设置为0(用户态不可访问)段选择符设置为__KERNEL_CS内核代码段。而对于中断处理函数设置则不同前32个门描述符的中断处理函数为early_idt_handlers之后的门描述符的中断处理函数为ignore_int。而在linux中0~19的中断向量是用于异常和陷阱。20~31的中断向量是intel保留使用的。 ②初始化异常向量 异常向量作为在中断向量表中的前20个向量(0~19)在汇编代码中已经将其的处理函数设置为early_idt_handlers而进入start_kernel()函数后系统会在trap_init()函数中重新设置它们的处理函数由于异常和陷阱的特殊性它们并没有像中断这样复杂的数据结构单纯的每个异常和陷阱有它们自己的中断处理函数系统只是简单地把中断处理函数放入异常和陷阱的门描述符中。在了解trap_init()函数之前我们需要先了解如下几个函数 /* 设置一个中断门* n中断号 * addr:中断处理程序入口地址*/ #define set_intr_gate(n, addr)                        \do {                                \BUG_ON((unsigned)n 0xFF);                \_set_gate(n, GATE_INTERRUPT, (void *)addr, 0, 0,    \__KERNEL_CS);                    \_trace_set_gate(n, GATE_INTERRUPT, (void *)trace_##addr,\0, 0, __KERNEL_CS);            \} while (0)/* 设置一个系统中断门 */ static inline void set_system_intr_gate(unsigned int n, void *addr) {BUG_ON((unsigned)n 0xFF);_set_gate(n, GATE_INTERRUPT, addr, 0x3, 0, __KERNEL_CS); }/* 设置一个系统门 */ static inline void set_system_trap_gate(unsigned int n, void *addr) {BUG_ON((unsigned)n 0xFF);_set_gate(n, GATE_TRAP, addr, 0x3, 0, __KERNEL_CS); }/* 设置一个陷阱门 */ static inline void set_trap_gate(unsigned int n, void *addr) {BUG_ON((unsigned)n 0xFF);_set_gate(n, GATE_TRAP, addr, 0, 0, __KERNEL_CS); }/* 设置一个任务门 */ static inline void set_task_gate(unsigned int n, unsigned int gdt_entry) {BUG_ON((unsigned)n 0xFF);_set_gate(n, GATE_TASK, (void *)0, 0, 0, (gdt_entry3)); } 这几个函数用于设置不同门的API函数他们的参数n都为中断号而他们都会调用_set_gate()函数只是参数不同_set_gate()函数如下 /* 设置一个门描述符并写入中断向量表* gate: 中断号* type: 门类型* addr: 中断处理程序入口* dpl:  权限位* ist:  64位系统才使用* seg:  段选择符*/ static inline void _set_gate(int gate, unsigned type, void *addr,unsigned dpl, unsigned ist, unsigned seg) {gate_desc s;/* 生成一个门描述符 */pack_gate(s, type, (unsigned long)addr, dpl, ist, seg);/** does not need to be atomic because it is only done once at* setup time*//* 将新的门描述符写入中断向量表中的gate项使用memcpy进行写入 */write_idt_entry(idt_table, gate, s);/* 用于跟踪? 暂时还不清楚这个 trace_idt_table 的用途 */write_trace_idt_entry(gate, s); }了解了以上的设置门描述符的函数我们再看看trap_init()函数 void __init trap_init(void) {int i;/* 使用了EISA总线 */ #ifdef CONFIG_EISAvoid __iomem *p early_ioremap(0x0FFFD9, 4);if (readl(p) E (I8) (S16) (A24))EISA_bus 1;early_iounmap(p, 4); #endif/* Interrupts/Exceptions */ //enum { //    X86_TRAP_DE 0,    /*  0, 除0操作 Divide-by-zero */ //    X86_TRAP_DB,        /*  1, 调试使用 Debug */ //    X86_TRAP_NMI,        /*  2, 非屏蔽中断 Non-maskable Interrupt */ //    X86_TRAP_BP,        /*  3, 断点 Breakpoint */ //    X86_TRAP_OF,        /*  4, 溢出 Overflow */ //    X86_TRAP_BR,        /*  5, 越界异常 Bound Range Exceeded */ //    X86_TRAP_UD,        /*  6, 无效操作码 Invalid Opcode */ //    X86_TRAP_NM,        /*  7, 无效设备 Device Not Available */ //    X86_TRAP_DF,        /*  8, 双重故障 Double Fault */ //    X86_TRAP_OLD_MF,    /*  9, 协处理器段超限 Coprocessor Segment Overrun */ //    X86_TRAP_TS,        /* 10, 无效任务状态段(TSS) Invalid TSS */ //    X86_TRAP_NP,        /* 11, 段不存在 Segment Not Present */ //    X86_TRAP_SS,        /* 12, 栈段错误 Stack Segment Fault */ //    X86_TRAP_GP,        /* 13, 保护错误 General Protection Fault */ //    X86_TRAP_PF,        /* 14, 页错误 Page Fault */ //    X86_TRAP_SPURIOUS,    /* 15, 欺骗性中断 Spurious Interrupt */ //    X86_TRAP_MF,        /* 16, X87 浮点数异常 Floating-Point Exception */ //    X86_TRAP_AC,        /* 17, 对齐检查 Alignment Check */ //    X86_TRAP_MC,        /* 18, 设备检查 Machine Check */ //    X86_TRAP_XF,        /* 19, SIMD 浮点数异常 Floating-Point Exception */ //    X86_TRAP_IRET 32,    /* 32, 汇编指令异常 IRET Exception */ //};set_intr_gate(X86_TRAP_DE, divide_error);/* 在32位系统上其效果等同于 set_intr_gate  */set_intr_gate_ist(X86_TRAP_NMI, nmi, NMI_STACK);/* int4 can be called from all */set_system_intr_gate(X86_TRAP_OF, overflow);set_intr_gate(X86_TRAP_BR, bounds);set_intr_gate(X86_TRAP_UD, invalid_op);set_intr_gate(X86_TRAP_NM, device_not_available); #ifdef CONFIG_X86_32set_task_gate(X86_TRAP_DF, GDT_ENTRY_DOUBLEFAULT_TSS); #elseset_intr_gate_ist(X86_TRAP_DF, double_fault, DOUBLEFAULT_STACK); #endifset_intr_gate(X86_TRAP_OLD_MF, coprocessor_segment_overrun);set_intr_gate(X86_TRAP_TS, invalid_TSS);set_intr_gate(X86_TRAP_NP, segment_not_present);set_intr_gate(X86_TRAP_SS, stack_segment);set_intr_gate(X86_TRAP_GP, general_protection);set_intr_gate(X86_TRAP_SPURIOUS, spurious_interrupt_bug);set_intr_gate(X86_TRAP_MF, coprocessor_error);set_intr_gate(X86_TRAP_AC, alignment_check); #ifdef CONFIG_X86_MCEset_intr_gate_ist(X86_TRAP_MC, machine_check, MCE_STACK); #endifset_intr_gate(X86_TRAP_XF, simd_coprocessor_error);/* 将前32个中断号都设置为已使用状态 */for (i 0; i FIRST_EXTERNAL_VECTOR; i)set_bit(i, used_vectors);#ifdef CONFIG_IA32_EMULATION/* 设置0x80系统调用的系统中断门 */set_system_intr_gate(IA32_SYSCALL_VECTOR, ia32_syscall);set_bit(IA32_SYSCALL_VECTOR, used_vectors); #endif#ifdef CONFIG_X86_32/* 设置0x80系统调用的系统门 */set_system_trap_gate(SYSCALL_VECTOR, system_call);set_bit(SYSCALL_VECTOR, used_vectors); #endif/** Set the IDT descriptor to a fixed read-only location, so that the* sidt instruction will not leak the location of the kernel, and* to defend the IDT against arbitrary memory write vulnerabilities.* It will be reloaded in cpu_init() *//* 将中断向量表设置在一个固定的只读的位置,以便“sidt”指令不会泄漏内核的位置,和保护中断向量表可以处于任意内存写的漏洞。它将会在 cpu_init() 中被加载到idtr寄存器 */__set_fixmap(FIX_RO_IDT, __pa_symbol(idt_table), PAGE_KERNEL_RO);idt_descr.address fix_to_virt(FIX_RO_IDT);/* 执行CPU的初始化对于中断而言在 cpu_init() 中主要是将 idt_descr 放入idtr寄存器中 */cpu_init();/* x86_init是一个定义了很多x86体系上的初始化操作这里执行的另一个trap_init()函数为空函数什么都不做 */x86_init.irqs.trap_init();#ifdef CONFIG_X86_64/* 64位操作 *//* 将 idt_table 复制到 debug_idt_table 中 */memcpy(debug_idt_table, idt_table, IDT_ENTRIES * 16);set_nmi_gate(X86_TRAP_DB, debug);set_nmi_gate(X86_TRAP_BP, int3); #endif } 在代码中used_vectors变量是一个bitmap它用于记录中断向量表中哪些中断已经被系统注册和使用哪些未被注册使用。trap_init()已经完成了异常和陷阱的初始化。对于linux而言中断号0~19是专门用于陷阱和故障使用的以上代码也表明了这一点而20~31一般是intel用于保留的。而我们的外部IRQ线使用的中断为32~255(代码中32号中断被用作汇编指令异常中断)。 所以在trap_init()代码中专门对0~19号中断的门描述符进行了初始化最后将新的中断向量表起始地址放入idtr寄存器中。在trap_init()中我们看到每个异常和陷阱都有他们自己的处理函数不过它们的处理函数的处理方式都大同小异如下 #代码地址arch/x86/kernel/entry_32.S# 11号异常处理函数入口 ENTRY(segment_not_present)RING0_EC_FRAMEASM_CLACpushl_cfi $do_segment_not_presentjmp error_codeCFI_ENDPROC END(segment_not_present)# 12号异常处理函数入口 ENTRY(stack_segment)RING0_EC_FRAMEASM_CLACpushl_cfi $do_stack_segmentjmp error_codeCFI_ENDPROC END(stack_segment)# 17号异常处理函数入口 ENTRY(alignment_check)RING0_EC_FRAMEASM_CLACpushl_cfi $do_alignment_checkjmp error_codeCFI_ENDPROC END(alignment_check)# 0号异常处理函数入口 ENTRY(divide_error)RING0_INT_FRAMEASM_CLACpushl_cfi $0            # no error codepushl_cfi $do_divide_errorjmp error_codeCFI_ENDPROC END(divide_error) 在trap_init()函数中调用了cpu_init()函数在此函数中会将新的中断向量表地址放入idtr寄存器中而具体内核是如何实现的呢之前已经说明idtr寄存器的低16位保存的是中断向量表长度高32位保存的是中断向量表基地址相对于的内核定义了一个struct desc_ptr结构专门用于保存idtr寄存器内容其如下 /* 代码地址arch/x86/include/asm/Desc_defs.h */ struct desc_ptr {unsigned short size;unsigned long address; } __attribute__((packed)) ;/* 代码地址arch/x86/kernel/cpu/Common.c */ /* 专门用于保存需要写入idtr寄存器值的变量这里可以看出中断向量表长度为256 * 16 - 1地址为idt_table */ struct desc_ptr idt_descr { NR_VECTORS * 16 - 1, (unsigned long) idt_table }; 在cpu_init()中会调用load_current_idt()函数进行写入如下 static inline void load_current_idt(void) {if (is_debug_idt_enabled())/* 开启了中断调试用的是 debug_idt_descr 和 debug_idt_table */load_debug_idt();else if (is_trace_idt_enabled())/* 开启了中断跟踪用的是 trace_idt_descr 和 trace_idt_table */load_trace_idt();else/* 普通情况用的是 idt_descr 和 idt_table */load_idt((const struct desc_ptr *)idt_descr); }/* load_idt()的定义 */ #define load_idt(dtr)                native_load_idt(dtr)/* native_load_idt()的定义 */ static inline void native_load_idt(const struct desc_ptr *dtr) {asm volatile(lidt %0::m (*dtr)); } 到这异常和陷阱已经初始化完毕内核也已经开始使用新的中断向量表了BIOS的中断向量表就已经遗弃不再使用了。 ③初始化中断 内核是在异常和陷阱初始化完成的情况下才会进行中断的初始化中断的初始化也是处于start_kernel()函数中分为两个部分分别是early_irq_init()和init_IRQ()。early_irq_init()是第一步的初始化其工作主要是跟硬件无关的一些初始化比如一些变量的初始化分配必要的内存等。init_IRQ()是第二步其主要就是关于硬件部分的初始化了。首先我们先看看中断描述符表irq_desc[NR_IRQS] /* 中断描述符表 */ struct irq_desc irq_desc[NR_IRQS] __cacheline_aligned_in_smp {[0 ... NR_IRQS-1] {.handle_irq     handle_bad_irq,.depth         1,.lock         __RAW_SPIN_LOCK_UNLOCKED(irq_desc-lock),} }; 可以看到irq_desc数组有NR_IRQS个元素NR_IRQS并不是256-32实际上虽然中断向量表中一共有256项(前32项用作异常和intel保留)但并不是所有中断向量都会使用到所以中断描述符表也不一定是256-32项CPU可以使用多少个中断是由中断控制器(PIC、APIC)或者内核配置决定的我们看看NR_IRQS的定义 /* IOAPIC为外部中断控制器 */ #ifdef CONFIG_X86_IO_APIC #define CPU_VECTOR_LIMIT        (64 * NR_CPUS) #define NR_IRQS                    \(CPU_VECTOR_LIMIT IO_APIC_VECTOR_LIMIT ?    \(NR_VECTORS CPU_VECTOR_LIMIT)  :    \(NR_VECTORS IO_APIC_VECTOR_LIMIT)) #else /* !CONFIG_X86_IO_APIC: NR_IRQS_LEGACY 16 */ #define NR_IRQS            NR_IRQS_LEGACY #endif 这时我们可以先看看early_irq_init()函数 int __init early_irq_init(void) {int count, i, node first_online_node;struct irq_desc *desc;/* 初始化irq_default_affinity变量此变量用于设置中断默认的CPU亲和力 */init_irq_default_affinity();printk(KERN_INFO NR_IRQS:%d\n, NR_IRQS);/* 指向中断描述符表irq_desc */desc irq_desc;/* 获取中断描述符表长度 */count ARRAY_SIZE(irq_desc);for (i 0; i count; i) {/* 为kstat_irqs分配内存每个CPU有自己独有的kstat_irqs数据此数据用于统计 */desc[i].kstat_irqs alloc_percpu(unsigned int);/* 为 desc-irq_data.affinity 和 desc-pending_mask 分配内存 */alloc_masks(desc[i], GFP_KERNEL, node);/* 初始化中断描述符的锁 */raw_spin_lock_init(desc[i].lock);/* 设置中断描述符的锁所属的类此类用于防止死锁 */lockdep_set_class(desc[i].lock, irq_desc_lock_class);/* 一些变量的初始化 */desc_set_defaults(i, desc[i], node, NULL);}return arch_early_irq_init(); } 更多的初始化在desc_set_defaults()函数中 static void desc_set_defaults(unsigned int irq, struct irq_desc *desc, int node,struct module *owner) {int cpu;/* 中断号 */desc-irq_data.irq irq;/* 中断描述符的中断控制器芯片为 no_irq_chip  */desc-irq_data.chip no_irq_chip;/* 中断控制器的私有数据为空 */desc-irq_data.chip_data NULL;desc-irq_data.handler_data NULL;desc-irq_data.msi_desc NULL;/* 设置中断状态 desc-status_use_accessors 为初始化状态_IRQ_DEFAULT_INIT_FLAGS */irq_settings_clr_and_set(desc, ~0, _IRQ_DEFAULT_INIT_FLAGS);/* 中断默认被禁止设置 desc-irq_data-state_use_accessors IRQD_IRQ_DISABLED */irqd_set(desc-irq_data, IRQD_IRQ_DISABLED);/* 设置中断处理回调函数为 handle_bad_irqhandle_bad_irq作为默认的回调函数此函数中基本上不做什么处理就是在屏幕上打印此中断信息并且desc-kstat_irqs */desc-handle_irq handle_bad_irq;/* 嵌套深度为1表示被禁止1次 */desc-depth 1;/* 初始化此中断发送次数为0 */desc-irq_count 0;/* 无法处理的中断次数为0 */desc-irqs_unhandled 0;/* 在/proc/interrupts所显名字为空 */desc-name NULL;/* owner为空 */desc-owner owner;/* 初始化kstat_irqs中每个CPU项都为0 */for_each_possible_cpu(cpu)*per_cpu_ptr(desc-kstat_irqs, cpu) 0;/* SMP系统才使用的初始化设置* desc-irq_data.node first_online_node * desc-irq_data.affinity irq_default_affinity* 清除desc-pending_mask*/desc_smp_init(desc, node); } 整个early_irq_init()在这里就初始化完毕了相对来说比较简单可以说early_irq_init()只是初始化了中断描述符表中的所有元素。 在看init_IRQ()前需要看看legacy_pic这个变量它其实就是CPU内部的中断控制器i8259A定义了与i8259A相关的一些处理函数和中断数量如下 struct legacy_pic default_legacy_pic {.nr_legacy_irqs NR_IRQS_LEGACY,.chip   i8259A_chip,.mask mask_8259A_irq,.unmask unmask_8259A_irq,.mask_all mask_8259A,.restore_mask unmask_8259A,.init init_8259A,.irq_pending i8259A_irq_pending,.make_irq make_8259A_irq, };struct legacy_pic *legacy_pic default_legacy_pic; 在X86体系下CPU使用的内部中断控制器是i8259A内核就定义了这个变量进行使用在init_IRQ()中会将所有的中断描述符的中断控制器芯片指向i8259A具体我们先看看init_IRQ()代码 void __init init_IRQ(void) {int i;/** On cpu 0, Assign IRQ0_VECTOR..IRQ15_VECTORs to IRQ 0..15.* If these IRQs are handled by legacy interrupt-controllers like PIC,* then this configuration will likely be static after the boot. If* these IRQs are handled by more mordern controllers like IO-APIC,* then this vector space can be freed and re-used dynamically as the* irqs migrate etc.*//* nr_legacy_irqs() 返回 legacy_pic-nr_legacy_irqs为16* vector_irq是一个int型的数组长度为中断向量表长其保存的是中断向量对应的中断号(如果中断向量是异常则没有中断号)* i8259A中断控制器使用IRQ0~IRQ15这16个中断号这里将这16个中断号设置到CPU0的vector_irq数组的0x30~0x3f上。*/for (i 0; i nr_legacy_irqs(); i)per_cpu(vector_irq, 0)[IRQ0_VECTOR i] i;/* x86_init是一个结构体里面定义了一组X86体系下的初始化函数 */x86_init.irqs.intr_init(); } x86_init.irqs.intr_init()是一个函数指针其指向native_init_IRQ()我们可以直接看看native_init_IRQ() void __init native_init_IRQ(void) {int i;/* Execute any quirks before the call gates are initialised: *//* 这里又是执行x86_init结构中的初始化函数pre_vector_init()指向 init_ISA_irqs  */x86_init.irqs.pre_vector_init();/* 初始化中断向量表中的中断控制器中默认的一些中断门初始化 */apic_intr_init();/** Cover the whole vector space, no vector can escape* us. (some of these will be overridden and become* special SMP interrupts)*//* 第一个外部中断默认是32 */i FIRST_EXTERNAL_VECTOR;/* 在used_vectors变量中找出所有没有置位的中断向量我们知道在trap_init()中对所有异常和陷阱和系统调用中断都置位了used_vectors没有置位的都为中断* 这里就是对所有中断设置门描述符*/for_each_clear_bit_from(i, used_vectors, NR_VECTORS) {/* IA32_SYSCALL_VECTOR could be used in trap_init already. *//* interrupt[]数组保存的是外部中断的中断门信息* 这里将中断向量表中空闲的中断向量设置为中断门,interrupt是一个函数指针数组其将31~255数组元素指向interrupt[i]函数*/set_intr_gate(i, interrupt[i - FIRST_EXTERNAL_VECTOR]);}/* 如果外部中断控制器需要则安装一个中断处理例程irq2到中断IRQ2上 */if (!acpi_ioapic !of_ioapic nr_legacy_irqs())setup_irq(2, irq2);#ifdef CONFIG_X86_32/* 在x86_32模式下会为当前CPU分配一个中断使用的栈空间 */irq_ctx_init(smp_processor_id()); #endif } 在native_init_IRQ()中又使用了x86_init变量中的pre_vector_init函数指针其指向init_ISA_irqs()函数 void __init init_ISA_irqs(void) {/* CHIP默认是i8259A_chip */struct irq_chip *chip legacy_pic-chip;int i;#if defined(CONFIG_X86_64) || defined(CONFIG_X86_LOCAL_APIC)/* 使用了CPU本地中断控制器 *//* 开启virtual wire mode */init_bsp_APIC(); #endif/* 其实就是调用init_8259A()进行8259A硬件的初始化 */legacy_pic-init(0);for (i 0; i nr_legacy_irqs(); i)/* i为中断号chip是irq_chip结构最后是中断回调函数 * 设置了中断号i的中断描述符的irq_data.irq_chip i8259A_chip* 设置了中断回调函数为handle_level_irq*/irq_set_chip_and_handler(i, chip, handle_level_irq); } 在init_ISA_irqs()函数中最主要的就是将内核使用的外部中断的中断描述符的中断控制器设置为i8259A_chip中断回调函数为handle_level_irq。 回到native_init_IRQ()函数当执行完x86_init.irqs.pre_vector_init()之后会执行apic_initr_init()函数这个函数中会初始化一些中断控制器特定的中断函数(这些中断游离于之前描述的中断体系中它们没有自己的中断描述符中断向量中直接保存它们自己的中断处理函数类似于异常与陷阱的调用情况)具体我们看看 static void __init apic_intr_init(void) {smp_intr_init();#ifdef CONFIG_X86_THERMAL_VECTOR/* 中断号为: 0xfa处理函数为: thermal_interrupt */alloc_intr_gate(THERMAL_APIC_VECTOR, thermal_interrupt); #endif #ifdef CONFIG_X86_MCE_THRESHOLDalloc_intr_gate(THRESHOLD_APIC_VECTOR, threshold_interrupt); #endif#if defined(CONFIG_X86_64) || defined(CONFIG_X86_LOCAL_APIC)/* self generated IPI for local APIC timer */alloc_intr_gate(LOCAL_TIMER_VECTOR, apic_timer_interrupt);/* IPI for X86 platform specific use */alloc_intr_gate(X86_PLATFORM_IPI_VECTOR, x86_platform_ipi); #ifdef CONFIG_HAVE_KVM/* IPI for KVM to deliver posted interrupt */alloc_intr_gate(POSTED_INTR_VECTOR, kvm_posted_intr_ipi); #endif/* IPI vectors for APIC spurious and error interrupts */alloc_intr_gate(SPURIOUS_APIC_VECTOR, spurious_interrupt);alloc_intr_gate(ERROR_APIC_VECTOR, error_interrupt);/* IRQ work interrupts: */ # ifdef CONFIG_IRQ_WORKalloc_intr_gate(IRQ_WORK_VECTOR, irq_work_interrupt); # endif#endif } 在apic_intr_init()函数中使用了alloc_intr_gate()函数进行处理这个函数的处理也很简单置位该中断号所处used_vectors位置调用set_intr_gate()设置一个中断门描述符到这里整个中断及异常都已经初始化完成了。 在linux系统中中断一共有256个0~19主要用于异常与陷阱20~31是intel保留未使用。32~255作为外部中断进行使用。特别的0x80中断用于系统调用。 机器上电时BIOS会初始化一个中断向量表当交接给linux内核后内核会自己新建立一个中断向量表之后完全使用自己的中断向量表舍弃BIOS的中断向量表。 在x86上系统默认使用的中断控制器为i8259A。 中断描述符的初始化过程中内核会将中断描述符的默认中断控制器设置为i8259A中断处理回调函数为handle_level_irq()。 外部中断的门描述的中断处理函数都为interrupt[i]。 中断的初始化大体上分为两个部分第一个部分为汇编代码的中断向量表的初次初始化第二部分为C语言代码其又分为异常与陷阱的初始化和中断的初始化。如图 在汇编的中断向量表初始化过中其主要对整个中断向量表进行了初始化其主要工作是 所有的门描述符的权限位为0; 所有的门描述符的段选择符为__KERNEL_CS; 0~31的门描述符的中断处理程序为early_idt_handlers[i](0 i 31); 其他的门描述符的中断处理程序为ignore_int 而trap_init()所做的异常与陷阱初始化就是修改中断向量表的前19项(异常和中断)主要修改他们的中断处理函数入口和权限位特殊的如任务门还会设置它们的段选择符。在trap_init()中就已经把所有的异常和陷阱都初始化完成了并会把新的中断向量表地址放入idtr寄存器开始使用新的中断向量表。 在early_irq_init()中主要工作是初始化整个中断描述符表将数组中的每个中断描述符中的必要变量进行初始化最后在init_IRQ()中主要工作是初始化中断向量表中的所有中断门描述符对于一般的中断内核将它们的中断处理函数入口设置为interrupt[i]而一些特殊的中断会在apic_intr_init()中进行设置。之后init_IRQ()会初始化内部和外部的中断控制器最后将一般的中断使用的中断控制器设置为i8259A中断处理函数为handle_level_irq(电平触发)。 3.2中断运行流程 (2)禁止调度和抢占 首先我们需要了解当系统处于中断上下文时是禁止发生调度和抢占的。进程的thread_info中有个preempt_count成员变量其作为一个变量包含了3个计数器和一个标志位如下 位 描述 解释 0~7 抢占计数器 也可以说是锁占用数 8~15 软中断计数器 记录软中断被禁用次数0表示可以进行软中断。 16~27 硬中断计数器 表示中断处理嵌套次数irq_enter()增加它irq_exit()减少它。 28 PREEMPT_ACTIVE标志 表示正在进行内核抢占设置此标志也禁止了抢占。 当进入到中断时中断处理程序会调用irq_enter()函数禁止抢占和调度。当中断退出时会通过irq_exit()减少其硬件计数器。我们需要清楚的就是无论系统处于硬中断还是软中断调度和抢占都是被禁止的。 (2)中断产生 我们需要先明确一下中断控制器与CPU相连的三种线:INTR、数据线、INTA。 在硬件电路中中断的产生发生一般只有两种分别是:电平触发方式和边沿触发方式。当一个外部设备产生中断中断信号会沿着中断线到达中断控制器。中断控制器接收到该外部设备的中断信号后首先会检测自己的中断屏蔽寄存器是否屏蔽该中断。 如果没有则设置中断请求寄存器中中断向量号对应的位并将INTR拉高用于通知CPUCPU每当执行完一条指令时都会去检查INTR引脚是否有信号(这是CPU自动进行的)如果有信号CPU还会检查EFLAGS寄存器的IF标志位是否禁止了中断(IF 0)如果CPU未禁止中断CPU会自动通过INTA信号线应答中断控制器。CPU再次通过INTA信号线通知中断控制器此时中断控制器会把中断向量号送到数据线上CPU读取数据线获取中断向量号。到这里实际上中断向量号已经发送给CPU了如果中断控制器是AEIO模式则会自动清除中断向量号对应的中断请求寄存器的位如果是EIO模式则等待CPU发送的EIO信号后在清除中断向量号对应的中断请求寄存器的位。 用步骤描述就是 中断控制器收到中断信号 中断控制器检查中断屏蔽寄存器是否屏蔽该中断若屏蔽直接丢弃 中断控制器设置该中断所在的中断请求寄存器位 通过INTR通知CPU CPU收到INTR信号检查是否屏蔽中断若屏蔽直接无视 CPU通过INTA应答中断控制器 CPU再次通过INTA应答中断控制器中断控制器将中断向量号放入数据线 CPU读取数据线上的中断向量号 若中断控制器为EIO模式CPU发送EIO信号给中断控制器中断控制器清除中断向量号对应中断请求寄存器位 SMP系统 在SMP系统也就是多核情况下外部的中断控制器有可能会于多个CPU相连这时候当一个中断产生时中断控制器有两种方式将此中断送到CPU上分别是静态分发和动态分发。区别就是静态分发设置了指定中断送往指定的一个或多个CPU上。动态分发则是由中断控制器控制中断应该发往哪个CPU或CPU组。 CPU已经接收到了中断信号以及中断向量号。此时CPU会自动跳转到中断描述符表地址以中断向量号作为一个偏移量直接访问中断向量号对应的门描述符。在门描述符中有个特权级(DPL)系统会先检查这个位然后清除EFLAGS的IF标志位(这也说明了发发生中断时实际上CPU是禁止其他可屏蔽中断的)之后转到描述符中的中断处理程序中。在上一篇文章我们知道所有的中断门描述符的中断处理程序都被初始化成了interrupt[i]它是一段汇编代码。 (3)中断和异常发生时CPU自动完成的工作 我们先注意看一下中断描述符表里面的每个中断描述符都有一个段选择符和一个偏移量以及一个DPL(特权级)而偏移量其实就是中断处理程序的入口地址当中断或异常发生时 CPU首先会确定是中断或异常的向量号然后根据这个向量号作为偏移量通过读取idtr中保存的中断向量表(IDT)的基地址获取相应的门描述符。并从门描述符中拿出其中的段选择符 根据段选择符从GDT中获取这个段的段描述符(为什么只从GDT中获取因为初始化所有中段描述符时使用的段选择符几乎都是__USER_CS,__KERNEL_CS,TSS这几个段选择符对应的段描述符都保存在GDT中)。而这几个段描述符中的基地址都是0x00000000所以偏移量就是中断处理程序入口地址。 这时还没有进入到中断处理程序CPU会先使用CS寄存器的当前特权级(CPL)与中断向量描述符中对应的段描述符的DPL进行比较如果DPL的值 CPL的值则通过检查而DPL的值 CPL的值时会产生一个通用保护异常。这种情况发生的可能性很小因为在上一篇初始化的文章中也可以看出来中断初始化所用的段选择符都是__KERNEL_CS而异常的段选择符几乎也都是__KERNEL_CS只除了极特殊的几个除外。也就是大多数中断和异常的段选择符DPL都是0CPL无论是在内核态(CPL 0)或者是用户态(CPL 3)都可以执行这些中断和异常。这里的检查可以理解为检查是否需要切换段。 如果是用户程序的异常(非CPU内部产生的异常)这里还需要再进行多一步的检查(中断和CPU内部异常则不用进行这步检查) 我们回忆一下中断初始化的文章里面介绍了门描述符在门描述符中也有一个DPL位用户程序的异常还要对这个位进行检查当前特权级CPL的值 DPL的值时则通过检查否则不能通过检查而只有系统门和系统中断门的DPL是3其他的异常门的DPL都为0。这样做的好处是避免了用户程序访问陷阱门、中断门和任务门。 (这里可以理解为进入此门的权限, 所以只有系统门和系统中断门是程序能够主动进入的, 也就是我们做系统调用走的门) 如果以上检查都通过并且CS寄存器的特权级发生变化(用户态陷入内核)则CPU会访问此CPU的TSS段(通过tr寄存器)在TSS段中读取当前进程的SS和ESP到SS和ESP寄存器中这样就完成了用户态栈到内核态栈的切换。之后把之前的SS寄存器和ESP寄存器的值保存到当前内核栈中。 将eflags、CS、EIP寄存器的值压入内核栈中。这样就保存了返回后需要执行的上下文。 最后将刚才获取到的段选择符加载到CS寄存器(CS段切换)段描述符加载到CS对应的非编程寄存器中也就是CS寄存器保存的是__KERNEL_CSCS寄存器的非编程寄存器中保存的是对应的段描述符。而根据段描述符中的段基址段内偏移量(保存在门描述符中)则得到了处理程序入口地址实际上我们知道段基址是0x00000000段内偏移量实际上就是处理程序入口地址这个门描述符的段内偏移量会被放到IP寄存器中。 ①interrupt[i] interrupt[i]的每个元素都相同执行相同的汇编代码这段汇编代码实际上很简单它主要工作就是将中断向量号和被中断上下文(进程上下文或者中断上下文)保存到栈中最后调用do_IRQ函数。 # 代码地址:arch/x86/kernel/entry_32.S# 开始 1:    pushl_cfi $(~vector0x80)    /* Note: always in signed byte range */ # 先会执行这一句将中断向量号取反然后加上0x80压入栈中.if ((vector-FIRST_EXTERNAL_VECTOR)%7) 6jmp 2f                                                     # 数字定义的标号为临时标号可以任意重复定义例如2f代表正向第一次出现的标号2:3b代表反向第一次出现的标号3:.endif.previous                                             # .previous使汇编器返回到该自定义段之前的段进行汇编则回到上面的数据段.long 1b                                                 # 在数据段中执行标号1的操作.section .entry.text, ax                             # 回到代码段 vectorvector1 .endif .endr 2:    jmp common_interruptcommon_interrupt:ASM_CLACaddl $-0x80,(%esp)       # 此时栈顶是(~vector 0x80)这里再加上-0x80实际就是中断向量号取反用于区别系统调用系统调用是正数中断向量是负数SAVE_ALL                 # 保存现场将寄存器值压入栈中TRACE_IRQS_OFF           # 关闭中断跟踪movl %esp,%eax           # 将栈指针保存到eax寄存器供do_IRQ使用call do_IRQ              # 调用do_IRQjmp ret_from_intr        # 跳转到ret_from_intr进行中断返回的一些处理 ENDPROC(common_interrupt)CFI_ENDPROC 需要注意这里面有一个SAVE_ALL是用于保存用户态寄存器值的在上面的文章中有提到CPU会自动保存原来特权级的段和栈到内核栈中但是通用寄存器并不会保存所以这里会有个SAVE_ALL来保存通用寄存器的值。所以当SAVE_ALL执行完后所有的上下文都已经保存完毕最后结果如下: ②do_IRQ 这是中断处理的核心函数来到这里时系统已经做了两件事 系统屏蔽了所有可屏蔽中断(清除了CPU的IF标志位由CPU自动完成) 将中断向量号和所有寄存器值保存到内核栈中 在do_IRQ中首先会添加硬中断计数器此行为导致了中断期间禁止调度发送此后会根据中断向量号从vector_irq[]数组中获取对应的中断号并调用handle_irq()函数出来该中断号对应的中断出来例程。 __visible unsigned int __irq_entry do_IRQ(struct pt_regs *regs) {/* 将栈顶地址保存到全局变量__irq_regs中old_regs用于保存现在的__irq_regs值这一行代码很重要实现了嵌套中断情况下的现场保存与还原 */struct pt_regs *old_regs set_irq_regs(regs);/* 获取中断向量号因为中断向量号是以取反方式保存的这里再次取反 */unsigned vector ~regs-orig_ax;/* 中断向量号 */unsigned irq;/* 硬中断计数器增加硬中断计数器保存在preempt_count */irq_enter();/* 这里开始禁止调度因为preempt_count不为0 *//* 退出idle进程(如果当前进程是idle进程的情况下) */exit_idle();/* 根据中断向量号获取中断号 */irq __this_cpu_read(vector_irq[vector]);/* 主要函数是handle_irq进行中断服务例程的处理 */if (!handle_irq(irq, regs)) {/* EIO模式的应答 */ack_APIC_irq();/* 该中断号并没有发生过多次触发 */if (irq ! VECTOR_RETRIGGERED) {pr_emerg_ratelimited(%s: %d.%d No irq handler for vector (irq %d)\n,__func__, smp_processor_id(),vector, irq);} else {/* 将此中断向量号对应的vector_irq设置为未定义 */__this_cpu_write(vector_irq[vector], VECTOR_UNDEFINED);}}/* 硬中断计数器减少 */irq_exit();/* 这里开始允许调度 *//* 恢复原来的__irq_regs值 */set_irq_regs(old_regs);return 1; } do_IRQ()函数中最重要的就是handle_irq()处理了我们看看 bool handle_irq(unsigned irq, struct pt_regs *regs) {struct irq_desc *desc;int overflow;/* 检查栈是否溢出 */overflow check_stack_overflow();/* 获取中断描述符 */desc irq_to_desc(irq);/* 检查是否获取到中断描述符 */if (unlikely(!desc))return false;/* 检查使用的栈有两种情况如果进程的内核栈配置为8K则使用进程的内核栈如果为4K系统会专门为所有中断分配一个4K的栈专门用于硬中断处理栈一个4K专门用于软中断处理栈还有一个4K专门用于异常处理栈 */if (user_mode_vm(regs) || !execute_on_irq_stack(overflow, desc, irq)) {if (unlikely(overflow))print_stack_overflow();/* 执行handle_irq */desc-handle_irq(irq, desc);}return true; } 好的最后执行中断描述符中的handle_irq指针所指函数我们回忆一下在初始化阶段所有的中断描述符的handle_irq指针指向了handle_level_irq()函数文章开头我们也说过中断产生方式有两种一种电平触发、一种是边沿触发。handle_level_irq()函数就是用于处理电平触发的情况系统内建了一些handle_irq函数具体定义在include/linux/irq.h文件中我们罗列几种常用的 handle_simple_irq()  简单处理情况处理函数 handle_level_irq()    电平触发方式情况处理函数 handle_edge_irq() 边沿触发方式情况处理函数 handle_fasteoi_irq() 用于需要EOI回应的中断控制器 handle_percpu_irq() 此中断只需要单一CPU响应的处理函数 handle_nested_irq() 用于处理使用线程的嵌套中断 我们主要看看handle_level_irq()函数函数有兴趣的朋友也可以看看其他的因为触发方式不同通知中断控制器、CPU屏蔽、中断状态设置的时机都不同它们的代码都在kernel/irq/chip.c中。 /* 用于电平中断电平中断特点:* 只要设备的中断请求引脚中断线保持在预设的触发电平中断就会一直被请求所以为了避免同一中断被重复响应必须在处理中断前先把mask irq然后ack irq以便复位设备的中断请求引脚响应完成后再unmask irq*/ void handle_level_irq(unsigned int irq, struct irq_desc *desc) {raw_spin_lock(desc-lock);/* 通知中断控制器屏蔽该中断线并设置中断描述符屏蔽该中断 */mask_ack_irq(desc);/* 检查此irq是否处于运行状态也就是检查IRQD_IRQ_INPROGRESS标志和IRQD_WAKEUP_ARMED标志。大家可以看看还会检查poll */if (!irq_may_run(desc))goto out_unlock;desc-istate ~(IRQS_REPLAY | IRQS_WAITING);/* 增加此中断号所在proc中的中断次数 */kstat_incr_irqs_this_cpu(irq, desc);/** If its disabled or no action available* keep it masked and get out of here*//* 判断IRQ是否有中断服务例程(irqaction)和是否被系统禁用 */if (unlikely(!desc-action || irqd_irq_disabled(desc-irq_data))) {desc-istate | IRQS_PENDING;goto out_unlock;}/* 在里面执行中断服务例程 */handle_irq_event(desc);/* 通知中断控制器恢复此中断线 */cond_unmask_irq(desc);out_unlock:raw_spin_unlock(desc-lock); } 这个函数还是比较简单看handle_irq_event()函数 irqreturn_t handle_irq_event(struct irq_desc *desc) {struct irqaction *action desc-action;irqreturn_t ret;desc-istate ~IRQS_PENDING;/* 设置该中断处理正在执行设置此中断号的状态为IRQD_IRQ_INPROGRESS */irqd_set(desc-irq_data, IRQD_IRQ_INPROGRESS);raw_spin_unlock(desc-lock);/* 主要具体看 */ret handle_irq_event_percpu(desc, action);raw_spin_lock(desc-lock);/* 取消此中断号的IRQD_IRQ_INPROGRESS状态 */irqd_clear(desc-irq_data, IRQD_IRQ_INPROGRESS);return ret; } 再看handle_irq_event_percpu()函数 irqreturn_t handle_irq_event_percpu(struct irq_desc *desc, struct irqaction *action) {irqreturn_t retval IRQ_NONE;unsigned int flags 0, irq desc-irq_data.irq;/* desc中的action是一个链表每个节点包含一个处理函数这个循环是遍历一次action链表分别执行一次它们的处理函数 */do {irqreturn_t res;/* 用于中断跟踪 */trace_irq_handler_entry(irq, action);/* 执行处理在驱动中定义的中断处理最后就是被赋值到中断服务例程action的handler指针上这里就执行了驱动中定义的中断处理 */res action-handler(irq, action-dev_id);trace_irq_handler_exit(irq, action, res);if (WARN_ONCE(!irqs_disabled(),irq %u handler %pF enabled interrupts\n,irq, action-handler))local_irq_disable();/* 中断返回值处理 */switch (res) {/* 需要唤醒该中断处理例程的中断线程 */case IRQ_WAKE_THREAD:/** Catch drivers which return WAKE_THREAD but* did not set up a thread function*//* 该中断服务例程没有中断线程 */if (unlikely(!action-thread_fn)) {warn_no_thread(irq, action);break;}/* 唤醒线程 */__irq_wake_thread(desc, action);/* Fall through to add to randomness */case IRQ_HANDLED:flags | action-flags;break;default:break;}retval | res;/* 下一个中断服务例程 */action action-next;} while (action);add_interrupt_randomness(irq, flags);/* 中断调试会使用 */if (!noirqdebug)note_interrupt(irq, desc, retval);return retval; } 其实代码上很简单我们需要注意几个屏蔽中断的方式清除EFLAGS的IF标志、通知中断控制器屏蔽指定中断、设置中断描述符的状态为IRQD_IRQ_INPROGRESS。在上述代码中这三种状态都使用到了我们具体解释一下 清除EFLAGS的IF标志CPU禁止中断当CPU进入到中断处理时自动会清除EFLAGS的IF标志也就是进入中断处理时会自动禁止中断。在SMP系统中就是单个CPU禁止中断。 通知中断控制器屏蔽指定中断在中断控制器处就屏蔽中断这样该中断产生后并不会发到CPU上。在SMP系统中效果相当于所有CPU屏蔽了此中断。系统在执行此中断的中断处理函数才会要求中断控制器屏蔽该中断所以没必要在此中断的处理过程中中断控制器再发一次中断信号给CPU。 设置中断描述符的状态为IRQD_IRQ_INPROGRESS在SMP系统中同一个中断信号有可能发往多个CPU但是中断处理只应该处理一次所以设置状态为IRQD_IRQ_INPROGRESS其他CPU执行此中断时都会先检查此状态(可看handle_level_irq()函数)。 所以在SMP系统下对于handle_level_irq而言一次典型的情况是:中断控制器接收到中断信号发送给一个或多个CPU收到的CPU会自动禁止中断并执行中断处理函数在中断处理函数中CPU会通知中断控制器屏蔽该中断之后当执行中断服务例程时会设置该中断描述符的状态为IRQD_IRQ_INPROGRESS表明其他CPU如果执行该中断就直接退出因为本CPU已经在处理了。 四、中断的上半部与下半部处理 在 Linux 中断处理的复杂体系中中断的处理过程被巧妙地划分为上半部和下半部这种设计模式旨在实现中断处理的高效性与及时性充分满足系统对不同类型任务处理的需求。 4.1 上半部快速响应硬件 上半部在中断处理流程中扮演着至关重要的 “先锋官” 角色其主要任务是对硬件中断进行迅速响应执行那些对时间极为敏感、需要立即完成的关键操作。当硬件设备产生中断信号时上半部会立即被触发它会迅速读取硬件设备的状态信息以了解中断产生的具体原因和相关情况然后及时清除中断标志向硬件设备表明中断已经被响应避免中断信号的持续干扰。在处理网卡中断时上半部会迅速读取网卡的寄存器状态获取数据包的基本信息并清除网卡的中断标志确保网卡能够继续正常工作接收后续的数据包。 上半部在中断处理中具有关键作用它是系统与硬件设备之间的 “快速通道”能够确保硬件设备的请求得到及时处理维持系统的实时性和稳定性。由于硬件中断可能随时发生并且具有较高的优先级如果上半部不能快速执行就会导致其他中断无法及时响应从而使系统的响应速度大幅下降甚至可能影响整个系统的正常运行。因此上半部的代码通常需要经过精心优化以确保其能够在最短的时间内完成任务快速释放中断线让系统能够及时处理其他中断请求。上半部还需要与下半部密切配合将那些耗时较长、对时间敏感度较低的任务传递给下半部进行处理从而实现中断处理的高效分工。 4.2 下半部延迟处理任务 下半部作为中断处理的 “后续保障部队”主要负责处理那些在上半部无法完成的耗时任务。这些任务通常与硬件设备的基本操作关联不大但又需要对中断事件进行进一步的处理和分析。当下半部被触发时它会根据上半部传递过来的信息对中断事件进行深入处理如数据的进一步加工、存储、传输等。在处理磁盘中断时上半部可能只是简单地读取磁盘控制器的状态并清除中断标志而将数据从磁盘缓冲区读取到内存中的操作则会交给下半部来完成。 下半部在处理耗时任务时具有明显的优势它能够避免阻塞系统的运行。由于下半部的任务通常不需要立即完成因此可以在系统相对空闲的时候进行处理不会影响系统对其他紧急事件的响应能力。下半部在执行任务时可以打开中断允许其他中断的发生这使得系统在处理耗时任务的同时仍然能够及时响应其他硬件设备的请求提高了系统的并发处理能力。下半部与上半部之间存在着紧密的协作关系上半部负责快速响应硬件中断为下半部的处理提供必要的信息和准备工作下半部则根据上半部的结果对中断事件进行全面而细致的处理完成整个中断处理的任务。 4.3 软中断、Tasklet 和工作队列 在 Linux 系统中下半部的实现主要依赖于软中断、Tasklet 和工作队列这三种机制它们各自具有独特的特点和适用场景为 Linux 系统的中断处理提供了丰富的选择。 我们看到中断实际分为了两个部分俗称就是一部分是硬中断一部分是软中断。软中断是专门用于处理中断过程中费时费力的操作而为什么系统要分硬中断和软中断呢问得明白点就是为什么需要软中断。我们可以试着想想如果只有硬中断的情况下我们需要在中断处理过程中执行一些耗时的操作比如浮点数运算复杂算法的运算时其他的外部中断就不能得到及时的响应因为在硬中断过程中中断是关闭着的甚至一些很紧急的中断会得不到响应系统稳定性和及时性都受到很大影响。 所以linux为了解决上述这种情况将中断处理分为了两个部分硬中断和软中断。首先一个外部中断得到响应时会先关中断并进入到硬中断完成较为紧急的操作然后开中断并在软中断执行那些非紧急、可延时执行的操作在这种情况下紧急操作可以立即执行而其他的外部中断也可以获得一个较为快速的响应。这也是软中断存在的必要性。在软中断过程中是不可以被抢占也不能被阻塞的也不能在一个给定的CPU上交错执行。 (1)软中断详解 软中断是在中断框架中专门用于处理非紧急操作的在SMP系统中软中断可以并发地运行在多个CPU上但在一些路径在需要使用自旋锁进行保护。在系统中很多东西都分优先级软中断也不例外有些软中断要求更快速的响应运行在内核中软中断一共分为10个同时也代表着10种不同的优先级系统用一个枚举变量表示 enum {HI_SOFTIRQ0,                     /* 高优先级tasklet */                              /* 优先级最高 */TIMER_SOFTIRQ,                    /* 时钟相关的软中断 */NET_TX_SOFTIRQ,                   /* 将数据包传送到网卡 */NET_RX_SOFTIRQ,                   /* 从网卡接收数据包 */BLOCK_SOFTIRQ,                    /* 块设备的软中断 */BLOCK_IOPOLL_SOFTIRQ,             /* 支持IO轮询的块设备软中断 */TASKLET_SOFTIRQ,                  /* 常规tasklet */SCHED_SOFTIRQ,                    /* 调度程序软中断 */HRTIMER_SOFTIRQ,                  /* 高精度计时器软中断 */RCU_SOFTIRQ,                      /* RCU锁软中断该软中断总是最后一个软中断 */       /* 优先级最低 */NR_SOFTIRQS                       /* 软中断数为10 */ }; 注释中的tasklet我们之后会说明这里先无视它。每一个优先级的软中断都使用一个struct softirq_action结构来表示在这个结构中只有一个成员变量就是action函数指针因为不同的软中断它的处理方式可能不同从优先级表中就可以看出来有块设备的也有网卡处理的。系统将这10个软中断用softirq_vec[10]的数组进行保存。 /* 用于描述一个软中断 */ struct softirq_action {/* 此软中断的处理函数 */        void    (*action)(struct softirq_action *); };/* 10个软中断描述符都保存在此数组 */ static struct softirq_action softirq_vec[NR_SOFTIRQS] __cacheline_aligned_in_smp; 系统一般使用open_softirq()函数进行软中断描述符的初始化主要就是将action函数指针指向该软中断应该执行的函数。在start_kernel()进行系统初始化中就调用了softirq_init()函数对HI_SOFTIRQ和TASKLET_SOFTIRQ两个软中断进行了初始化 void __init softirq_init(void) {int cpu;for_each_possible_cpu(cpu) {per_cpu(tasklet_vec, cpu).tail per_cpu(tasklet_vec, cpu).head;per_cpu(tasklet_hi_vec, cpu).tail per_cpu(tasklet_hi_vec, cpu).head;}/* 开启常规tasklet */open_softirq(TASKLET_SOFTIRQ, tasklet_action);/* 开启高优先级tasklet */open_softirq(HI_SOFTIRQ, tasklet_hi_action); }/* 开启软中断 */ void open_softirq(int nr, void (*action)(struct softirq_action *)) {softirq_vec[nr].action action; }可以看到TASKLET_SOFTIRQ的action操作使用了tasklet_action()函数HI_SOFTIRQ的action操作使用了tasklet_hi_action()函数这两个函数我们需要结合tasklet进行说明。我们也可以看看其他的软中断使用了什么函数 open_softirq(TIMER_SOFTIRQ, run_timer_softirq); open_softirq(NET_TX_SOFTIRQ, net_tx_action); open_softirq(NET_RX_SOFTIRQ, net_rx_action); open_softirq(BLOCK_SOFTIRQ, blk_done_softirq); open_softirq(BLOCK_IOPOLL_SOFTIRQ, blk_iopoll_softirq); open_softirq(SCHED_SOFTIRQ, run_rebalance_domains); open_softirq(HRTIMER_SOFTIRQ, run_hrtimer_softirq); open_softirq(RCU_SOFTIRQ, rcu_process_callbacks); 实很明显可以看出除了TASKLET_SOFTIRQ和HI_SOFTIRQ其他的软中断更多地是用于特定的设备和环境对于我们普通的IO驱动和设备而已使用的软中断几乎都是TASKLET_SOFTIRQ和HI_SOFTIRQ而系统为了对这些不同IO设备进行统一的处理就在TASKLET_SOFTIRQ和HI_SOFTIRQ的action函数中使用到了tasklet。 对于每个CPU都有一个irq_cpustat_t的数据结构里面有一个__softirq_pending变量这个变量很重要用于表示该CPU的哪个软中断处于挂起状态在软中断处理时可以根据此值跳过不需要处理的软中断直接处理需要处理的软中断。内核使用local_softirq_pending()获取此CPU的__softirq_pending的值。 当使用open_softirq设置好某个软中断的action指针后该软中断就会开始可以使用了其实更明了地说从中断初始化完成开始即使所有的软中断都没有使用open_softirq()进行初始化软中断都已经开始使用了只是所有软中断的action都为空系统每次执行到软中断都没有软中断需要执行罢了。 在每个CPU上一次软中断处理的一个典型流程是 硬中断执行完毕开中断。 检查该CPU是否处于嵌套中断的情况如果处于嵌套中则不执行软中断也就是在最外层中断才执行软中断。 执行软中断设置一个软中断执行最多使用时间和循环次数(10次)。 进入循环获取CPU的__softirq_pending的副本。 执行此__softirq_pending副本中所有需要执行的软中断。 如果软中断执行完毕退出中断上下文。 如果还有软中断需要执行(在软中断期间又发发生了中断产生了新的软中断新的软中断记录在CPU的__softirq_pending上而我们的__softirq_pending只是个副本)。 检查此次软中断总共使用的时间和循环次数条件允许继续执行软中断循环次数减一并跳转到第4步。 我们具体看一下代码首先在irq_exit()中会检查是否需要进行软中断处理 void irq_exit(void) { #ifndef __ARCH_IRQ_EXIT_IRQS_DISABLEDlocal_irq_disable(); #elseWARN_ON_ONCE(!irqs_disabled()); #endifaccount_irq_exit_time(current);/* 减少preempt_count的硬中断计数器 */preempt_count_sub(HARDIRQ_OFFSET);/* in_interrupt()会检查preempt_count上的软中断计数器和硬中断计数器来判断是否处于中断嵌套中 *//* local_softirq_pending()则会检查该CPU的__softirq_pending变量是否有软中断挂起 */if (!in_interrupt() local_softirq_pending())invoke_softirq();tick_irq_exit();rcu_irq_exit();trace_hardirq_exit(); /* must be last! */ } 我们再进入到invoke_softirq(): static inline void invoke_softirq(void) {if (!force_irqthreads) { #ifdef CONFIG_HAVE_IRQ_EXIT_ON_IRQ_STACK/** We can safely execute softirq on the current stack if* it is the irq stack, because it should be near empty* at this stage.*//* 软中断处理函数 */__do_softirq(); #else/** Otherwise, irq_exit() is called on the task stack that can* be potentially deep already. So call softirq in its own stack* to prevent from any overrun.*/do_softirq_own_stack(); #endif} else {/* 如果强制使用软中断线程进行软中断处理会通知调度器唤醒软中断线程ksoftirqd */wakeup_softirqd();} } 重头戏就在__do_softirq()中我已经注释好了方便大家看 asmlinkage __visible void __do_softirq(void) {/* 为了防止软中断执行时间太长设置了一个软中断结束时间 */unsigned long end jiffies MAX_SOFTIRQ_TIME;/* 保存当前进程的标志 */unsigned long old_flags current-flags;/* 软中断循环执行次数: 10次 */int max_restart MAX_SOFTIRQ_RESTART;/* 软中断的action指针 */struct softirq_action *h;bool in_hardirq;__u32 pending;int softirq_bit;/** Mask out PF_MEMALLOC s current task context is borrowed for the* softirq. A softirq handled such as network RX might set PF_MEMALLOC* again if the socket is related to swap*/current-flags ~PF_MEMALLOC;/* 获取此CPU的__softirq_pengding变量值 */pending local_softirq_pending();/* 用于统计进程被软中断使用时间 */account_irq_enter_time(current);/* 增加preempt_count软中断计数器也表明禁止了调度 */__local_bh_disable_ip(_RET_IP_, SOFTIRQ_OFFSET);in_hardirq lockdep_softirq_start();/* 循环10次的入口每次循环都会把所有挂起需要执行的软中断执行一遍 */ restart:/* 该CPU的__softirq_pending清零当前的__softirq_pending保存在pending变量中 *//* 这样做就保证了新的软中断会在下次循环中执行 */set_softirq_pending(0);/* 开中断 */local_irq_enable();/* h指向软中断数组头 */h softirq_vec;/* 每次获取最高优先级的已挂起软中断 */while ((softirq_bit ffs(pending))) {unsigned int vec_nr;int prev_count;/* 获取此软中断描述符地址 */h softirq_bit - 1;/* 减去软中断描述符数组首地址获得软中断号 */vec_nr h - softirq_vec;/* 获取preempt_count的值 */prev_count preempt_count();/* 增加统计中该软中断发生次数 */kstat_incr_softirqs_this_cpu(vec_nr);trace_softirq_entry(vec_nr);/* 执行该软中断的action操作 */h-action(h);trace_softirq_exit(vec_nr);/* 之前保存的preempt_count并不等于当前的preempt_count的情况处理也是简单的把之前的复制到当前的preempt_count上这样做是防止最后软中断计数不为0导致系统不能够执行调度 */if (unlikely(prev_count ! preempt_count())) {pr_err(huh, entered softirq %u %s %p with preempt_count %08x, exited with %08x?\n,vec_nr, softirq_to_name[vec_nr], h-action,prev_count, preempt_count());preempt_count_set(prev_count);}/* h指向下一个软中断但下个软中断并不一定需要执行这里只是配合softirq_bit做到一个处理 */h;pending softirq_bit;}rcu_bh_qs();/* 关中断 */local_irq_disable();/* 循环结束后再次获取CPU的__softirq_pending变量为了检查是否还有软中断未执行 */pending local_softirq_pending();/* 还有软中断需要执行 */if (pending) {/* 在还有软中断需要执行的情况下如果时间片没有执行完并且循环次数也没到10次继续执行软中断 */if (time_before(jiffies, end) !need_resched() --max_restart)goto restart;/* 这里是有软中断挂起但是软中断时间和循环次数已经用完通知调度器唤醒软中断线程去执行挂起的软中断软中断线程是ksoftirqd这里只起到一个通知作用因为在中断上下文中是禁止调度的 */wakeup_softirqd();}lockdep_softirq_end(in_hardirq);/* 用于统计进程被软中断使用时间 */account_irq_exit_time(current);/* 减少preempt_count中的软中断计数器 */__local_bh_enable(SOFTIRQ_OFFSET);WARN_ON_ONCE(in_interrupt());/* 还原进程标志 */tsk_restore_flags(current, old_flags, PF_MEMALLOC); } 流程就和上面所说的一致如果还有不懂可以去内核代码目录/kernel/softirq.c查看源码。 (2)tasklet 软中断有多种部分种类有自己特殊的处理如从NET_TX_SOFTIRQ和NET_RT_SOFTIRQ、BLOCK_SOFTIRQ等而如HI_SOFTIRQ和TASKLET_SOFTIRQ则是专门使用tasklet。它是在I/O驱动程序中实现可延迟函数的首选方法如上一句所说它建立在HI_SOFTIRQ和TASKLET_SOFTIRQ这两种软中断之上多个tasklet可以与同一个软中断相关联系统会使用一个链表组织他们而每个tasklet执行自己的函数处理。而HI_SOFTIRQ和TASKLET_SOFTIRQ这两个软中断并没有什么区别他们只是优先级上的不同而已系统会先执行HI_SOFTIRQ的tasklet再执行TASKLET_SOFTIRQ的tasklet。同一个tasklet不能同时在几个CPU上执行一个tasklet在一个时间上只能在一个CPU的软中断链上不能同时在多个CPU的软中断链上并且当这个tasklet正在执行时其他CPU不能够执行这个tasklet。也就是说tasklet不必要编写成可重入的函数。 系统会为每个CPU维护两个链表用于保存HI_SOFTIRQ的tasklet和TASKLET_SOFTIRQ的tasklet这两个链表是tasklet_vec和tasklet_hi_vec它们都是双向链表如下 struct tasklet_head {struct tasklet_struct *head;struct tasklet_struct **tail; };static DEFINE_PER_CPU(struct tasklet_head, tasklet_vec); static DEFINE_PER_CPU(struct tasklet_head, tasklet_hi_vec); 在softirq_init()函数中会将每个CPU的tasklet_vec链表和tasklet_hi_vec链表进行初始化将他们的头尾相连实现为一个空链表。由于tasklet_vec和tasklet_hi_vec处理方式几乎一样只是软中断的优先级别不同我们只需要理解系统如何对tasklet_vec进行处理即可。需要注意的是tasklet_vec链表都是以顺序方式执行并不会出现后一个先执行再到前一个先执行(在软中断期间被中断的情况)。 介绍完tasklet_vec和tasklet_hi_vec链表我们来看看tasklettasklet简单来说就是一个处理函数的封装类似于硬中断中的irqaction结构。一般来说在一个驱动中如果需要使用tasklet进行软中断的处理只需要一个中断对应初始化一个tasklet它可以在每次中断产生时重复使用。系统使用tasklet_struct结构进行描述一个tasklet而且对于同一个tasklet_struct你可以选择放在tasklet_hi_vec链表或者tasklet_vec链表上。我们来看看 struct tasklet_struct {struct tasklet_struct *next;      /* 指向链表下一个tasklet */unsigned long state;              /* tasklet状态 */atomic_t count;                   /* 禁止计数器调用tasklet_disable()会增加此数tasklet_enable()减少此数 */void (*func)(unsigned long);      /* 处理函数 */unsigned long data;               /* 处理函数使用的数据 */ }; tasklet状态主要分为以下两种 TASKLET_STATE_SCHED:这种状态表示此tasklet处于某个tasklet链表之上(可能是tasklet_vec也可能是tasklet_hi_vec)。 TASKLET_STATE_RUN:表示此tasklet正在运行中。 这两个状态主要就是用于防止tasklet同时在几个CPU上运行和在同一个CPU上交错执行。 而func指针就是指向相应的处理函数。在编写驱动时我们可以使用tasklet_init()函数或者DECLARE_TASKLET宏进行一个task_struct结构的初始化之后可以使用tasklet_schedule()或者tasklet_hi_schedule()将其放到相应链表上等待CPU运行。我们使用一张图描述一下软中断和tasklet结合运行的情况 我们知道每个软中断都有自己的action函数在HI_SOFTIRQ和TASKLET_SOFTIRQ的action函数中就用到了它们对应的TASKLET_HI_VEC链表和TASKLET_VEC链表并依次顺序执行链表中的每个tasklet结点。 在SMP系统中我们会遇到一个问题两个CPU都需要执行同一个tasklet的情况虽然一个tasklet只能放在一个CPU的tasklet_vec链表或者tasklet_hi_vec链表上但是这种情况是有可能发生的我们设想一下中断在CPU1上得到了响应并且它的tasklet放到了CPU1的tasklet_vec上进行执行而当中断的tasklet上正在执行时此中断再次发生并在CPU2上进行了响应此时CPU2将此中断的tasklet放到CPU2的tasklet_vec上并执行到此中断的tasklet。 实际上为了处理这种情况在HI_SOFTIRQ和TASKLET_SOFTIRQ的action函数中会先将对应的tasklet链表取出来并把对应的tasklet链表的head和tail清空如果在执行过程中某个tasklet的state为TASKLET_STATE_RUN状态说明其他CPU正在处理这个tasklet这时候当前CPU则会把此tasklet加入到当前CPU已清空的tasklet链表的末尾然后设置__softirq_pending变量这样在下次循环软中断的过程中会再次检查这个tasklet。也就是如果其他CPU的这个tasklet一直不退出当前CPU就会不停的置位tasklet的pending然后不停地循环检查。 我们可以看看TASKLET_SOFTIRQ的action处理 static void tasklet_action(struct softirq_action *a) {struct tasklet_struct *list;local_irq_disable();/* 将tasklet链表从该CPU中拿出来 */list __this_cpu_read(tasklet_vec.head);/* 将该CPU的此软中断的tasklet链表清空 */__this_cpu_write(tasklet_vec.head, NULL);__this_cpu_write(tasklet_vec.tail, this_cpu_ptr(tasklet_vec.head));local_irq_enable();/* 链表已经处于list中并且该CPU的tasklet_vec链表为空 */while (list) {struct tasklet_struct *t list;list list-next;/* 检查并设置该tasklet为TASKLET_STATE_RUN状态 */if (tasklet_trylock(t)) {/* 检查是否被禁止 */if (!atomic_read(t-count)) {/* 清除其TASKLET_STATE_SCHED状态 */if (!test_and_clear_bit(TASKLET_STATE_SCHED,t-state))BUG();/* 执行该tasklet的func处理函数 */t-func(t-data);/* 清除该tasklet的TASKLET_STATE_RUN状态 */tasklet_unlock(t);continue;}tasklet_unlock(t);}/* 以下为tasklet为TASKLET_STATE_RUN状态下的处理 *//* 禁止中断 */local_irq_disable();/* 将此tasklet添加的该CPU的tasklet_vec链表尾部 */t-next NULL;*__this_cpu_read(tasklet_vec.tail) t;__this_cpu_write(tasklet_vec.tail, (t-next));/* 设置该CPU的此软中断处于挂起状态设置irq_cpustat_t的__sofirq_pending变量这样在软中断的下次执行中会再次执行此tasklet */__raise_softirq_irqoff(TASKLET_SOFTIRQ);/* 开启中断 */local_irq_enable();} } (3)软中断处理线程 当有过多软中断需要处理时为了保证进程能够得到一个满意的响应时间设计时给定软中断一个时间片和循环次数当时间片和循环次数到达但软中断又没有处理完时就会把剩下的软中断交给软中断处理线程进行处理这个线程是一个内核线程其作为一个普通进程优先级是120。其核心处理函数是run_ksoftirqd()其实此线程的处理也很简单就是调用了上面的__do_softirq()函数我们可以具体看看 /* 在smpboot_thread_fun的一个死循环中被调用 */ static void run_ksoftirqd(unsigned int cpu) {/* 禁止中断在__do_softirq()中会开启 */local_irq_disable();/* 检查该CPU的__softirq_pending是否有软中断被挂起 */if (local_softirq_pending()) {/** We can safely run softirq on inline stack, as we are not deep* in the task stack here.*//* 执行软中断 */__do_softirq();rcu_note_context_switch(cpu);/* 开中断 */local_irq_enable();/* 检查是否需要调度 */cond_resched();return;}/* 开中断 */local_irq_enable(); } 五、中断性能分析 5.1性能指标解读 在 Linux 系统中中断性能的评估涉及多个关键指标这些指标如同精密仪器上的刻度精准地反映着系统的运行状态和性能表现。 中断响应时间是其中最为关键的指标之一它是指从硬件设备发出中断请求的那一刻起到 CPU 开始执行对应的中断处理程序所经历的时间间隔。这一指标直接体现了系统对外部事件的即时反应能力在实时性要求极高的应用场景中如工业自动化控制系统传感器不断产生中断请求以汇报设备的运行状态此时中断响应时间的长短将直接影响系统对设备状态变化的捕捉和应对速度。若响应时间过长可能导致设备控制的延迟进而引发生产事故。 中断处理频率同样不容忽视它表示在单位时间内系统处理中断的次数。中断处理频率反映了系统对外部事件的处理效率和负载程度。在网络服务器中网卡会频繁产生中断以通知系统有新的数据包到达若中断处理频率过高可能意味着网络流量过大系统需要投入大量的资源来处理这些中断从而影响其他任务的执行效率。过高的中断处理频率还可能导致 CPU 频繁切换上下文增加系统的开销。 此外中断处理时间也是一个重要的考量因素它指的是中断处理程序从开始执行到执行完毕所花费的时间。中断处理时间的长短直接影响系统的整体性能和响应能力。如果中断处理时间过长会导致 CPU 长时间被中断处理程序占用无法及时处理其他任务从而使系统的响应速度变慢甚至可能造成系统的卡顿或死机。在处理磁盘 I/O 中断时如果中断处理程序需要花费大量时间来读写磁盘数据就会导致其他任务的执行被阻塞影响系统的整体运行效率。 5.2 性能分析工具与方法 在 Linux 系统中有许多实用的工具可用于中断性能分析这些工具就像一把把精准的 “手术刀”帮助系统管理员和开发者深入剖析系统中断性能找出潜在的问题和瓶颈。 top 命令是一款常用的系统性能分析工具它能够实时显示系统中各个进程的资源占用状况包括 CPU、内存等同时也提供了有关中断的重要信息。在 top 命令的输出结果中“% hi” 字段表示硬中断占用 CPU 的百分比“% si” 字段表示软中断占用 CPU 的百分比。通过观察这两个字段的值我们可以直观地了解到系统中硬中断和软中断对 CPU 资源的消耗情况。如果 “% hi” 值过高说明硬件中断频繁可能是某些硬件设备出现故障或配置不当需要进一步检查硬件连接和驱动程序如果 “% si” 值过高则可能表示系统中存在大量的软件中断如网络数据包的处理、内核定时器的管理等需要优化相关的软件代码或调整系统配置。 sarSystem Activity Reporter命令是另一个功能强大的系统性能分析工具它可以从多个方面对系统的活动进行详细报告其中就包括中断信息。使用 sar -I 命令可以查看系统的中断统计信息包括每个中断源的中断次数、中断频率等。通过分析这些数据我们可以深入了解系统中各个中断源的活动情况找出中断频繁的设备或驱动程序。sar -I SUM 命令可以统计系统总的中断次数和中断频率sar -I 1,2 命令可以分别查看中断源 1 和中断源 2 的中断统计信息。 除了 top 和 sar 命令外还有一些其他的工具和方法也可用于中断性能分析。/proc/interrupts 文件记录了系统中所有中断源的中断次数和中断处理程序的相关信息通过查看这个文件我们可以获取系统中断的详细状态。proc/softirqs 文件则提供了软中断的运行情况包括各种软中断的类型和计数。使用 perf 工具可以对系统进行性能剖析它能够跟踪中断处理程序的执行过程分析中断处理过程中的性能瓶颈从而为优化提供有力的依据。 六、中断性能优化策略 6.1 优化中断处理程序 编写高效的中断处理程序是提升中断性能的关键所在其要点涵盖多个重要方面。首要原则是尽量精简中断处理程序中的操作摒弃一切不必要的计算和数据处理任务。在处理网络中断时若仅需提取数据包的关键信息如源 IP 地址、目的 IP 地址等就应避免对整个数据包进行复杂的解析和处理可将这些操作推迟到中断处理程序之后的其他任务中执行。中断处理程序应尽可能缩短执行时间以减少对 CPU 资源的占用从而使系统能够及时响应其他中断请求。在处理硬件设备的中断时应迅速读取设备状态、完成必要的操作后立即返回避免在中断处理程序中执行耗时较长的 I/O 操作、复杂的算法计算等。 合理运用算法和数据结构同样至关重要。对于频繁访问的数据应采用高效的数据结构进行存储和管理以减少数据访问的时间开销。使用哈希表来存储设备状态信息能够快速定位和获取所需数据大大提高中断处理的效率。在算法选择上应优先选择时间复杂度较低的算法以确保在最短的时间内完成任务。在对数据进行排序时选择快速排序算法而非冒泡排序算法因为快速排序算法的平均时间复杂度为 O (n log n)而冒泡排序算法的时间复杂度为 O (n^2)在数据量较大时快速排序算法的效率优势将更加明显。 6.2 中断亲和性设置 中断亲和性是指将特定的中断固定分配给指定的 CPU 核心进行处理这一技术能够显著提升 CPU 缓存的利用率。在多核心 CPU 系统中每个核心都拥有独立的缓存当一个中断被分配到某个核心上处理时该核心的缓存中会存储与该中断相关的数据和指令。如果后续的中断也能被分配到同一个核心上处理就可以直接从缓存中读取数据避免了缓存未命中带来的性能损耗从而提高中断处理的速度和效率。在网络服务器中将网卡中断固定分配给特定的核心处理能够确保该核心的缓存中始终保存着与网络数据包处理相关的数据如网络协议栈的相关信息、数据包的缓存等当新的网络数据包到达时该核心能够迅速从缓存中获取所需信息快速处理中断提高网络数据的处理速度。 设置中断亲和性的方法在 Linux 系统中较为直观。可以通过修改 /proc/irq/[中断号]/smp_affinity 文件来实现该文件中的每一位对应一个 CPU 核心通过设置相应位的值为 1即可将中断绑定到对应的核心上。例如要将中断号为 8 的中断绑定到 CPU 核心 0 和核心 1 上可以执行命令 “echo 3 /proc/irq/8/smp_affinity”因为 3 的二进制表示为 0011对应 CPU 核心 0 和核心 1。也可以使用工具如 irqbalance 来自动管理中断亲和性irqbalance 能够根据系统的负载情况动态地调整中断的分配使中断均匀地分布在各个 CPU 核心上避免某个核心因中断负载过重而导致性能下降。 6.3 中断合并与节流 中断合并和节流是两种行之有效的优化技术它们能够有效降低中断频率提升系统性能。中断合并的原理是将多个连续的中断请求合并为一个中断进行处理。在网络设备中当有大量的网络数据包到达时如果每个数据包都产生一个中断会导致中断频率过高占用大量的 CPU 资源。通过中断合并技术可以将多个数据包的中断请求合并为一个当一定数量的数据包到达或者经过一定的时间间隔后才产生一个中断通知 CPU 进行处理。这样可以减少中断的次数降低 CPU 的中断处理开销提高系统的整体性能。 中断节流则是通过限制中断的产生频率来控制中断对系统资源的占用。它的实现方式是在一定的时间间隔内只允许产生一定数量的中断。在磁盘 I/O 操作中如果磁盘频繁地产生中断会影响系统的其他任务。通过设置中断节流规定每 10 毫秒只允许产生一次磁盘中断这样可以避免磁盘中断过于频繁使 CPU 能够更合理地分配资源同时处理其他任务。中断节流可以通过修改设备驱动程序或者使用系统提供的相关工具来实现具体的设置参数需要根据系统的实际情况和应用需求进行调整。 6.4 案例分析与实践 在某服务器应用场景中系统面临着高并发网络请求的挑战大量的网络中断使得 CPU 负载居高不下系统性能严重下降。通过深入分析发现网络中断处理程序存在效率低下的问题其中包含许多不必要的操作和复杂的算法导致中断处理时间过长。中断亲和性设置不合理网络中断在各个 CPU 核心上分布不均衡部分核心因中断负载过重而出现性能瓶颈。 针对这些问题采取了一系列优化措施。对网络中断处理程序进行了全面优化去除了不必要的操作简化了算法将一些复杂的数据处理任务推迟到中断处理之后的工作队列中执行大大缩短了中断处理时间。通过修改 /proc/irq/[中断号]/smp_affinity 文件将网络中断合理地分配到多个 CPU 核心上实现了中断亲和性的优化使各个核心的负载更加均衡。还启用了中断合并和节流机制减少了网络中断的频率降低了 CPU 的中断处理开销。 经过优化后系统性能得到了显著提升。CPU 负载从原来的经常超过 90% 降低到了 50% 左右网络请求的响应时间从平均几十毫秒缩短到了几毫秒系统的并发处理能力大幅提高能够稳定地应对高并发的网络请求满足了业务的发展需求。通过这个案例可以看出综合运用中断性能优化策略能够有效地解决系统中的中断性能问题提升系统的整体性能和稳定性。在实际的系统优化过程中需要根据具体的问题和需求有针对性地选择和实施优化措施不断调整和优化以达到最佳的性能效果。
http://www.zqtcl.cn/news/231192/

相关文章:

  • 天津市建设局网站口碑营销相关案例
  • 怎么有自己的网站厂字形网页布局网站
  • 广州市财贸建设开发监理网站工程建设企业等采用
  • 网站建设规模设想自己建立网站教程
  • 兰溪建设局网站门户网站建设招标
  • 用wp做网站备案怎么查自己的邮箱号
  • 苏州企业网站建设公司价格数字媒体应用 网站开发
  • 西宁做网站seo四川省的住房和城乡建设厅网站首页
  • 响应式网站 有哪些弊端可以发广告的网站
  • wordpress 漫画站wordpress加目录
  • 天津商城网站制作深圳品牌网站设计公司
  • 初学网站开发上海市普陀区建设规划局网站
  • 网站开发完成后如何发布做网站用vs还是dw
  • 怎么看网站是否备案可信赖的菏泽网站建设
  • 做网站的优点系统软件开发服务
  • 深圳品牌营销网站建设尚品中国网站
  • 新建网站怎么做关键词南阳手机网站制作
  • 宁波网站建设应届生公司网站备案需要每年做吗
  • 汽车设计网站论坛网站 备案
  • 网站源码带手机版展示型网站首页设计解析
  • 备案的网站名称能重复备案吗为什么打开Wordpress很慢
  • vps网站建设个人网站二级域名做淘宝客
  • 用cms织梦做网站图文教程wordpress分类文章排序
  • 台州网站策划云南招聘网
  • 网站如何设定关键词wordpress 文章关联
  • 京津冀网站建设公司建设监理工程师网站
  • 网站建设的500字小结那些网站做网批
  • 怎么做视频网站首页网站建设公司创业计划书
  • 网加思维做网站推广项目营销推广策划
  • 郫县专业的网站建设免费自己创建个人网站