目录

1. 中断概述

1.1 什么是中断

1.2 为什么引入中断

1.3 中断的分类

1.4 CPU什么时候响应中断

2. 中断控制器简介

2.1 中断的C/S模型结构

2.2 作为中介的中断控制器

2.3 高级可编程中断控制器(APIC)

2.4 机制与策略分离的中断机制

3. Linux内核中断子系统框架

4. 中断向量与中断描述符表

4.1 中断向量

4.1.1 中断向量的概念

4.1.2 中断向量的分配

4.2 中断描述符表(IDT)

4.2.1 实模式中断向量表

4.2.2 保护模式中断描述符表

4.2.3 中断描述符表的定义与加载

4.2.4 门描述符类型

4.3 中断描述符表的初始化

4.3.1 中断描述符表的3次初始化

4.3.2 通用门设置函数

4.3.3 初始化陷阱门和系统门

4.3.4 初始化中断门

5. 中断处理过程

5.1 相关汇编命令

5.1.1 调用过程指令CALL

5.1.2 过程返回指令RET

5.1.3 调用中断过程的指令INT

5.1.4 中断返回指令IRET

5.2 中断和异常的硬件处理

5.2.1 确定中断向量

5.2.2 读取中断描述符

5.2.3 读取段描述符

5.2.4 中断有效性检查

5.2.5 检查特权级变化并压栈

5.2.6 跳转到中断处理程序

5.3 中断处理程序与中断服务例程(ISR)

5.3.1 中断处理程序

5.3.2 中断服务例程(ISR)

5.4 中断描述符

5.4.1 Linux 2.6.11版本(ulk版本)

5.4.2 Linux 2.6.35版本(目前常用接口版本)

5.5 中断服务例程的注册和注销

5.5.1 中断线共享数据结构irqaction

5.5.2 注册中断服务例程

5.5.3 注销中断服务例程

5.6 中断处理流程

5.6.1 asm_do_IRQ函数

5.6.2 generic_handle_irq函数

5.6.3 handle_level_irq函数

5.6.4 handle_IRQ_event函数

5.7 中断返回

5.7.1 中断 / 异常 / 系统调用返回概述

5.7.2 ret_from_intr流程简介

5.7.3 中断与系统调度

5.7.4 中断与信号处理

6. 中断底半部处理机制

6.1 为什么引入中断底半部机制

6.2 软中断(softirq)机制

6.2.1 适用场景

6.2.2 softirq_action数据结构

6.2.3 注册软中断

6.2.4 触发软中断

6.2.5 执行软中断

6.3 小任务(tasklet)机制

6.3.1 小任务机制概述

6.3.2 小任务数据结构

6.3.3 定义小任务

6.3.4 调度小任务

6.3.5 执行小任务

6.3.6 杀死小任务

6.4 工作队列(workqueue)机制

6.4.1 工作队列机制概述

6.4.2 工作队列数据结构

6.4.3 工作数据结构

6.4.3 创建工作队列

6.4.4 创建工作

6.4.5 调度工作

6.4.6 销毁工作队列

6.5 中断下半部小结

6.5.1 中断底半部使用原则

6.5.2 中断底半部选择标准

7. 时钟中断机制

7.1 时钟中断是操作系统的脉搏

7.2 基本时钟硬件

7.3 基本时钟运行机制

7.4 Linux系统时间

7.4.1 tick

7.4.2 jiffies

7.4.3 xtime

7.4 Linux时钟框架简介

7.5 定时器及应用

7.5.1 定时器数据结构

7.5.2 创建定时器

7.5.3 定时器操作

7.5.4 执行定时器

7.5.5 timer_pending函数使用实例


1. 中断概述

1.1 什么是中断

① 中断是CPU对系统发生的某个事件做出的一种反应

当中断发生时,CPU暂停正在执行的程序,保留现场后自动转去执行相应事件的处理程序,处理完成后返回到断点继续执行被打断的程序

② 中断是操作系统的脉搏,是并发处理的基础

1.2 为什么引入中断

① 引入中断的最初目的是为了支持CPU和设备之间的并行操作

CPU启动设备进行输入输出后,设备便可以独立工作了,此时CPU可以去处理其他事务。当设备完成输入输出后,通过向CPU发出中断,报告此次输入输出的结果,让CPU决定后续的处理,从而避免了CPU对设备的轮询

此时的中断被称为外部中断

② 随着计算机体系结构的改进,出现了内部中断(或称作异常),内部中断出现的目的如下,

a. 处理计算机运行时出现的某些随机事件

b. 为了编程方便

1.3 中断的分类

在80x86体系结构中,中断分类如下,

① 由于异常本质上是CPU发出的中断信号,所以其特点是不使用中断控制器,也不能被屏蔽

② 所有IO设备产生的中断请求(IRQ)均引起可屏蔽中断

③ 计算机内部硬件出现故障时(如硬件故障)产生不可屏蔽中断

说明1:INTR和NMI有不同的外部管脚

说明2:中断屏蔽的2个层次

① CPU层面关中断,即清除EFLAGS的中断标志位(IF)

② 中断控制器层面关中断,即设置中断控制器中的屏蔽寄存器

1.4 CPU什么时候响应中断

CPU收到中断信号之后,并不立即响应,而是在执行每条指令周期的最后一个时钟周期进行中断检测。一旦检测到中断信号有效并且中断允许标志位为1时,CPU才在当前指令执行完以后转入中断响应流程

所以外部中断不需要重试中断发生前的指令

2. 中断控制器简介

2.1 中断的C/S模型结构

① 系统中有n个CPU可用来接收中断事件并进行处理

② 系统中有若干个形成树状结构中断控制器,这些控制器汇集系统中所有外设的中断请求,并将中断事件分发给某一个CPU进行处理

③ 外设发出请求,该请求并不是马上传给CPU,而是被中断控制器收集,中断控制器相当于中介,在外设和CPU之间架起了桥梁,当CPU接收到请求之后才给予应答

2.2 作为中介的中断控制器

① 外设通过外部中断线发出中断请求,进入中断控制器

② 中断控制器通过CPU的INTR引脚向CPU发出中断请求

③ CPU通过中断应答引脚INTA应答请求

说明1:并不是每个设备都可以向中断线发送中断信号,只有对某一条确定的中断线拥有了控制权,才可以向这条中断线发送信号

说明2:由于计算机外设越来越多,原有的中断线已经不够使用,所以才有了共享中断的模式

2.3 高级可编程中断控制器(APIC)

目前x86使用APIC作为中断控制器

① 每个x86的CPU核有一个本地APIC,这些本地APIC通过中断控制器通信总线(Interrupt Controller Communication Bus)连接到IO APIC上

在更新的x86架构中,直接系统系统总线连接IO APIC和本地APIC,而不是使用中断控制器通信总线

② IO APIC收集各个外设的中断,并翻译成总线上的信息,传递给某个CPU上的本地APIC

③ Local APIC用来响应本地中断和发送到本CPU的中断,也用来向其他CPU发送IPI中断

说明1:IO APIC中断处理流程简介

① 连接到IO APIC引脚的设备触发中断

② 检测Delivery Status是否为0(IDLE),不是0则等待,否则将其置为1(sent pending)

③ 将中断发送至对应的CPU

④ CPU响应中断

⑤ 对于level触发的中断,设置IRR为1,并等待CPU触发EOI(End Of Interrupt),再将IRR复位为0

⑥ 将Delivery status置为0,到此时,方可继续响应这条中断线上的中断

说明2:Local APIC中断处理流程简介

① 判断接收到的中断目的地是否为本CPU,如果不是则丢弃

② 如果是非Fixed类型的中断(e.g. SMI / NMI)则直接传递给CPU

③ 对于Fixed类型的中断,经过IRR和ISR传递给CPU

④ CPU触发EOI,Local APIC处理此EOI并将之传给IO APIC,完成此次中断处理

说明3:中断状态标识

IRR:Interrupt Request Register

ISR:In-Service Register

TMR:Trigger Mode Register

2.4 机制与策略分离的中断机制

① 尽管中断和CPU密切相关,但是CPU的设计独立于中断控制器的设计

② 尽管中断是操作系统非常重要的组成部分,但是操作系统的设计者只负责提供接口,通过该接口可以调用针对具体设备的中断服务程序

③ 中断和对中断的处理被解除了耦合,无论是要注册新的中断还是改变现有的中断服务程序,CPU架构和操作系统都无需修改,这其中的功臣就是中断控制器

说明:所以中断控制器的引入有2个功能,

① 在硬件层面,实现对CPU唯一中断引脚的复用(外设中断很多,但INTR引脚只有一个)

② 在操作系统层面,实现了中断处理机制与策略的分离(通过下个小节,可以更好地理解)

3. Linux内核中断子系统框架

中断子系统由4个部分组成,

① 硬件无关代码

这部分称作Linux内核通用中断处理模块,这部分抽象了不同CPU体系结构 & 不同中断控制器中断处理的相同内容

各个外设的驱动代码也希望能用统一的接口实现中断管理

② CPU体系结构相关的中断处理

和系统使用的具体的CPU体系结构相关

③ 中断控制器驱动代码

和系统使用的中断控制器相关

④ 普通外设驱动

使用Linux内核通用中断处理模块的API来实现自己的驱动逻辑

4. 中断向量与中断描述符表

4.1 中断向量

4.1.1 中断向量的概念

Intel x86体系结构支持256种向量中断,每个中断源都被分配了一个8位无符号整数作为类型码,也就是中断向量

4.1.2 中断向量的分配

① 不可屏蔽中断和异常的向量是固定的

② 可屏蔽中断的向量可以通过中断控制器编程来改变

说明:外设可屏蔽中断的中断向量

IRQ线是从0开始顺序编号的,所以IRQn的缺省中断向量是n+32,如前所述IRQ和中断向量之间的映射可以通过中断控制器端口来修改

使用cat /proc/interrupts可以查看当前系统中各种外设的IRQ,可见此处的编号是IRQ编号,而不是中断向量号

要注意区别IRQ序号和中断向量号,IRQ序号在中断控制器端标识,中断向量号在CPU端标识

下面简单说明一下cat /proc/interrupts显示的各列的含义,

第1列:IRQ序号

CPU0 & CPU1:每个CPU核上发生该中断的次数

第4列:中断控制器名称(在Linux 2.6.11版本中为struct hw_interrupt_type结构的typename字段;在后续版本中为struct irq_chip的name字段)

第5列:设备名称(即调用request_irq函数时的devname参数)

4.2 中断描述符表(IDT)

4.2.1 实模式中断向量表

① 在实模式中,CPU把内存中从0开始的1KB作为中断向量表使用

② 每个表项占4B(段地址2B + 偏移量2B),这样就构成了相应中断处理程序的入口地址

③ 每个表项4B,共256个中断向量,所以4B * 256 = 1KB

4.2.2 保护模式中断描述符表

在保护模式下,段寄存器用于存储段选择符,段内偏移也增加到32位,所以原先4B的表项不足以描述。在保护模式下,每个中断描述符占8B,如下图所示,

说明1:在保护模式下,中断向量表改称作中断描述符表IDT(Interrupt Descriptor Table),其中的每个表项叫做一个门描述符(gate descriptor)

说明2:中断向量就是中断在中断向量表或中断描述符表中的索引

说明3:在保护模式下,中断描述符表在内存中的位置不再局限于从0地址开始,而是可以存储在内存的任何位置。CPU中增设了一个中断描述符表寄存器IDTR,用来存放中断描述符表在内存中的起始地址

IDTR是一个48位的寄存器,其中低16位保存中断描述符表的大小,高32位保存中断描述符表的基地址

4.2.3 中断描述符表的定义与加载

加载中断描述符表的指令为LIDT,指令格式为,

LIDT 48位的伪描述符

在Linux的80386架构中,中断描述符表被组织为struct Xgt_desc_struct结构,可见低16位为表的大小,高32位为表的地址,且二者之间通过__attribute__((packed))标识为没有间隙(从定义可见,80386中的gdt也是采用相同的数据结构表示)

在head.S中,中断描述符表定义如下,

其中实际存放中断描述符表的idt_table定义在traps.c文件中

最终中断描述符表的加载在cpu_init函数中以内嵌汇编的形式实现

4.2.4 门描述符类型

"门"的含义是当中断发生时必须先通过这些门,然后才能进入相应的处理程序,主要门描述符类型如下,

4.2.4.1 中断门(Interrupt Gate)

① 类型码为110

② 中断门的请求特权级(DPL)为0,因此用户态的进程不能访问

③ 所有中断处理程序都由中断门激活,并全部限制在内核态

4.2.4.2 陷阱门(Trap Gate)

① 类型码为111,DPL为0

② 与中断门唯一的区别是控制权通过陷阱门进入处理程序时,维持中断标志位(IF)不变,也就是不会再次关中断

4.2.4.3 系统门(System Gate)

① Linux内核特别设置的,用来让用户态进程访问的陷阱门,因此DPL为3

② 系统调用就是通过系统门进入内核的

也就是说,系统门是DPL为3的陷阱门

4.3 中断描述符表的初始化

4.3.1 中断描述符表的3次初始化

在80386启动过程中,在不同的阶段会对中断描述符表进行不同的初始化,

4.3.1.1 实模式初始化

当计算机运行在实模式时,中断描述符表被初始化,并由BIOS使用,此时的中断向量表还是从内存0地址处开始的

4.3.1.2 预初始化

在head.S的汇编语言初始化阶段,会调用setup_idt函数初始化中断描述符表(此时使用的IDT已经是内核数据段的idt_table数组),在填充表项时,使用了一个空的中断处理程序,因为现在处于初始化阶段,还没有任何中断处理程序

在构造中断描述符表项时使用的中断处理函数为ignore_int,该函数实现如下,

可见该中断处理函数只是打印相关信息,并不做实际处理

4.3.1.3 最终初始化

内核在启动分页功能后对IDT进行第二次初始化,使用实际的陷阱和中断处理程序替换之前空的处理程序。一旦这个过程完成,对每个异常,IDT都有一个专门的陷阱门或系统门;而对每个外部中断,IDT都包含专门的中断门

4.3.2 通用门设置函数

门设置函数用于构造门描述符并填充到IDT的指定位置,由_set_gate宏完成

参数

含义

gate_addr

门在IDT中的地址

type

门类型

dpl

门特权级

addr

中断 / 异常处理函数地址,也就是门描述符中的偏移量

seg

门描述符中的段选择符

4.3.3 初始化陷阱门和系统门

4.3.3.1 初始化函数

如上文所述,系统门和陷阱门的类型相同,只是DPL不同。系统门用于实现系统调用,允许用户态进程访问,所以权限为3

4.3.3.2 初始化流程

陷阱门和系统门的初始化由trap_init函数完成

注意此处2个门的设置,

① 中断向量为14的缺页异常,此处实际初始化的是一个中断门,也就是说调用page_fault时会关中断

② 中断向量为0x80(SYSCALL_VECTOR)的系统门,该系统门用于实现系统调用

4.3.4 初始化中断门

4.3.4.1 初始化函数

4.3.4.2 初始化流程

中断门的初始化由init_IRQ函数完成(arch/i386/kernel/i8259.c)

说明1:函数调用关系

start_kernel
--> trap_init
--> init_IRQ

可见在内核初始化的过程中进行了中断 / 异常的设置

说明2:pre_intr_init_hook函数的作用

在80386实现中,该函数初始化了中断描述符,绑定了中断控制器handler

说明3:中断门初始化范围

init_IRQ初始化了中断向量从0x20(即32)开始到255的IDT表项,同时跳过用于系统调用的0x80中断向量,使用interrupt数组进行设置,如果后续有中断门要修改中断处理函数,可以在intr_init_hook函数中进行

根据注释,如果使能了smp,某些中断门需要被替换,我们看下intr_init_hook函数最终调用的smp_intr_init函数

其中初始化了IPI相关的中断门,有没有看到我们之前提到的和invalid TLB相关的核间中断(不得不说,Linux内核各个子模块之间相互关联交织,真的非常复杂)

说明4:interrupt数组的构成

interrupt数组在entry.S文件中定义,数组中的每个元素指向一个中断处理程序的指针

可见每个中断处理程序是把中断号减256的结果保存在栈中,这是进入中断处理程序后第一个压入栈的值,是一个负数,正数留给系统调用使用

对于每个中断处理程序,唯一不同的就是压入栈中的这个数值,之后所有中断处理程序都跳转到一段相同的代码common_interrupt

说明5:common_interrupt函数

common_interrupt函数首先调用SAVE_ALL保存所有寄存器到栈中,然后调用do_IRQ函数,由于之前将esp赋值给eax,所以调用do_IRQ函数的参数就是指向所有保存有寄存器的栈顶指针

在中断处理完成后,调用ret_from_intr实现中断的返回

5. 中断处理过程

5.1 相关汇编命令

5.1.1 调用过程指令CALL

指令格式:CALL 过程名

功能:在取出CALL指令之后以及执行CALL指令之前,指令指针寄存器EIP指向紧接着CALL指令的下一条指令,CALL指令先将EIP值压入栈中,再将控制转移到过程名标识的地址处

5.1.2 过程返回指令RET

指令格式:RET

功能:与CALL指令配对使用,用于过程返回。当遇到RET指令时,栈内信息可使控制权直接回到CALL指令的下一条指令

5.1.3 调用中断过程的指令INT

指令格式:INT 中断向量

功能:EFLAGS、CS和EIP寄存器的值被压入栈内,控制权被转移到由中断向量指定的中断处理程序

说明:用户态进程是可以通过INT指令触发各种中断的,如果触发的是INT 0x80,则是进入系统调用流程

如果用户态进程错误触发了其他中断,则依靠中断门的DPL,可阻止非法访问

5.1.4 中断返回指令IRET

指令格式:IRET

功能:在中断处理程序结束处调用,将EIP、CS和EFLAGS寄存器内容从栈中弹出,并将控制权返回到发生中断的地方

5.2 中断和异常的硬件处理

当CPU检测到中断发生后,将进行如下操作,

5.2.1 确定中断向量

CPU首先从中断控制器的一个端口取得中断向量i(在0 ~ 255之间)

5.2.2 读取中断描述符

通过IDTR寄存器找到IDT表,读取IDT表的第i项(即第i个中断描述符)

5.2.3 读取段描述符

从GDTR寄存器找到GDT表,结合中断描述符中的段选择符,在GDT表中获得中断处理程序对应的段描述符,这个段描述符指定了中断处理程序所在段的基地址(在Linux中其实就是0地址)

5.2.4 中断有效性检查

中断有效性检查分两步进行,

① 段级检查

如果CPL小于段描述符中的DPL,则产生General Protection异常,因为中断处理程序的特权级不能低于引起中断的程序的特权级

② 门级检查

对于编程异常(即用INT指令触发的异常),进行门级检查,如果CPL大于中断描述符的DPL,则产生General Protection异常,这可以避免应用程序访问特殊的陷阱门或中断门

对于外部IO产生的中断或CPU内部产生的异常,不进行门级检查

5.2.5 检查特权级变化并压栈

当中断发生在用户态(特权级为3),而中断处理程序运行在内核态(特权级为0),特权级发生了变化,所以会引起栈的切换,即从用户态栈切换到内核态栈

当中断发生在内核态,则不会切换特权级与栈

可见当从用户态栈切换到内核态栈时,先将用户态栈的值压入中断处理程序的内核态栈中,之后将EFLAGS、CS和EIP入栈,用于后续的中断返回。如果异常产生了一个硬错误码,也一并压栈

上述栈的切换与压栈均由CPU进行

说明:中断处理程序在内核态使用哪个栈 ?

默认情况下,中断处理程序没有自己的栈,是共享所中断进程的内核栈,也就是current标识的进程的内核栈,由于内核栈的大小是2页,所以在栈中获取空间必须非常节省

5.2.6 跳转到中断处理程序

此时CPU已经从段描述符中获取了中断处理程序的段基址,从中断描述符中可获取中断处理程序的偏移量,二者相加就得到了中断处理程序的入口地址,便可跳转执行

5.3 中断处理程序与中断服务例程(ISR)

5.3.1 中断处理程序

共享同一条中断线的所有中断请求,有一个总的中断处理程序

5.3.2 中断服务例程(ISR)

每个中断请求都有自己单独的中断服务例程

5.4 中断描述符

在内核中,对于每个IRQ都会用struct irq_desc来描述,该结构被称为中断描述符。中断描述符中记录了IRQ的中断控制器、中断服务例程、IRQ自身的属性和资源等信息

由于内核不同版本改动很大,下面分版本说明

5.4.1 Linux 2.6.11版本(ulk版本)

5.4.1.1 中断描述符数据结构

此处重点说明2个成员,

① handler

描述中断控制器,用于实现对中断的硬件层面的(low-level)处理

② action

用于实现中断线的共享,也就是request_irq函数注册的中断服务例程(ISR)

5.4.1.2 中断描述符组织

在kernel/irq/handle.c中定义了irq_desc数组,保存了所有IRQ的中断描述符信息,是Linux内核中维护IRQ资源的管理单元

在初始化阶段,内核提供了一个硬件无关的中断控制器模板no_irq_type,其中所提供的hook点均为空函数

说明:中断描述符组织关系

5.4.1.3 中断描述符设置

对中断描述符的设置主要就是设置中断控制器相关的handler字段和中断处理逻辑相关的action字段,此处主要说明中断控制器相关的设置

需要注意的是,中断描述符的设置是体系结构和硬件相关的操作,在80386 + APIC中,中断控制器的绑定由ioapic_register_intr函数实现

说明1:结合代码分析,此处设置的中断门在init_IRQ函数中会被覆盖

说明2:函数调用流程

start_kernel
--> smp_prepare_boot_cpu--> smp_boot_cpus--> smpboot_setup_io_apic--> setup_IO_APIC--> setup_IO_APIC_irqs--> ioapic_register_intr

5.4.2 Linux 2.6.35版本(目前常用接口版本)

5.4.2.1 中断描述符数据结构

① chip

原先与中断控制器硬件相关的handler成员被抽象为chip成员

② handle_irq

high-level的中断处理函数,如果为空,则调用__do_IRQ函数

③ action

仍然为中断处理例程列表

5.4.2.2 中断描述符组织

irq_desc数组仍然定义在handle.c文件中,同时初始化时绑定了硬件无关的中断控制器

说明:中断描述符组织关系

下图其实是更新的内核版本,chip成员被存储在irq_data成员中

5.4.2.3 中断描述符设置

在S5PV210 + VIC中,中断控制器的绑定由vic_set_irq_sources函数实现

说明:函数调用关系

s5pv210_init_irq
--> s5p_init_irq--> vic_init--> vic_set_irq_sources // 会绑定irq_chip结构体

5.5 中断服务例程的注册和注销

说明:此处以Linux 2.6.35版本为例,该版本的接口为目前常用的接口

5.5.1 中断线共享数据结构irqaction

共享中断线的每个设备都会生成一个irqaction结构

成员

含义

handler

指向一个具体设备的中断服务例程(ISR)

flags

描述中断线与设备之间关系的一组标志

name

设备名称,出现在/proc/interrupts等文件中

dev_id

用于标识共享中断线的不同设备,是handler和request_irq的参数

next

指向irqaction链表的下一个节点

thread_fn & thread

用于支持中断线程化处理

说明1:handler的返回值

handler的类型如下,

typedef irqreturn_t (*irq_handler_t)(int, void *);

其中返回值类型irqreturn_t如下,在中断处理例程中必须正确返回不同的标识,否则会影响中断处理流程(详见后文分析)

IRQ_NONE:中断不是本设备产生的

IRQ_HANDLED:中断是本设备产生,并且做了正确的处理

IRQ_WAKE_THREAD:唤醒中断线程化的中断处理线程

说明2:标志位flags

下面介绍常用的几个标志位,

标志

含义

IRQF_DISABLED

运行irqaction中的handler时关闭中断,目前是一个空操作并打算移除。这里的关中断是CPU级的关中断,即屏蔽所有可屏蔽中断,在80386 + 2.6.11版本内核中有作用

IRQF_SAMPLE_RANDOM

可以把这个设备看作是随机时间发生源,因此内核可以用他做随机数发生器

IRQF_SHARED

允许其他设备共享这条中断线

IRQF_ONESHOT

中断顶半部执行完成后不使能中断,直到中断线程运行完成后才使能

5.5.2 注册中断服务例程

在Linux 2.6.35中,最终是调用request_threaded_irq函数实现中断服务例程的注册,其中handler为传统的中断顶半部,thread_fn为中断线程化处理函数

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)
{struct irqaction *action;struct irq_desc *desc;int retval;// 注册为共享的中断,dev_id不能为空,必须传递唯一标识if ((irqflags & IRQF_SHARED) && !dev_id)return -EINVAL;// 根据中断号找到中断描述符// irq就是中断在irq_desc数组中的下标desc = irq_to_desc(irq);if (!desc)return -EINVAL;// 检查该中断是否可申请if (desc->status & IRQ_NOREQUEST)return -EINVAL;// 如果handler为NULL,说明希望进行中断线程化处理// 此时如果thread_fn也为NULL,则为错误// 对于中断线程化处理,内核提供默认的中断顶半部irq_default_primary_handlerif (!handler) {if (!thread_fn)return -EINVAL;handler = irq_default_primary_handler;}// 分配irqaction结构并赋值action = kzalloc(sizeof(struct irqaction), GFP_KERNEL);if (!action)return -ENOMEM;action->handler = handler;action->thread_fn = thread_fn;action->flags = irqflags;action->name = devname;action->dev_id = dev_id;// 注册irqactionchip_bus_lock(irq, desc);retval = __setup_irq(irq, desc, action);chip_bus_sync_unlock(irq, desc);if (retval)kfree(action);return retval;
}

说明:irq_default_primary_handler函数

该函数仅仅返回IRQ_WAKE_THREAD,标识要唤醒中断线程化处理的线程,如果用户编程时同时提供handler和thread_fn,则用户自定义的handler也必须返回IRQ_WAKE_THREAD才能唤醒中断处理线程,详见下文分析

说明2:如果在注册中断时没有指定IRQF_NOAUTOEN标志,则注册中断时会使能中断

5.5.3 注销中断服务例程

下面分析下__free_irq函数的主体部分,

static struct irqaction *__free_irq(unsigned int irq, void *dev_id)
{struct irq_desc *desc = irq_to_desc(irq);struct irqaction *action, **action_ptr;unsigned long flags;WARN(in_interrupt(), "Trying to free IRQ %d from IRQ context!\n", irq);if (!desc)return NULL;raw_spin_lock_irqsave(&desc->lock, flags);// 根据dev_id找到共享中断中正确的irqaction结构action_ptr = &desc->action;for (;;) {action = *action_ptr;if (!action) {WARN(1, "Trying to free already-free IRQ %d\n", irq);raw_spin_unlock_irqrestore(&desc->lock, flags);return NULL;}if (action->dev_id == dev_id)break;action_ptr = &action->next;}// 从desc的action列表中删除要释放的irqaction*action_ptr = action->next;// 如果删除了action列表中的最后一个成员,则关闭中断if (!desc->action) {desc->status |= IRQ_DISABLED;if (desc->chip->shutdown)desc->chip->shutdown(irq);elsedesc->chip->disable(irq);}raw_spin_unlock_irqrestore(&desc->lock, flags);unregister_handler_proc(irq, action);/* Make sure it's not being used on another CPU: */synchronize_irq(irq);// 如果使用中断线程化处理,则注销该线程// kthread_stop为同步注销,会等待要注销的内核线程结束才返回if (action->thread) {if (!test_bit(IRQTF_DIED, &action->thread_flags))kthread_stop(action->thread);put_task_struct(action->thread);}// 返回irqaction结构指针供释放return action;
}

5.6 中断处理流程

说明:以S5PV210(ARMv7) + Linux 2.6.35版本为例,这样描述会与之前的内容有些脱节,但是对我个人比较有用,我们可以把注意力集中在中断处理中体系结构无关的部分

5.6.1 asm_do_IRQ函数

file:arch/arm/kernel/irq.c

该函数被汇编代码调用,是中断处理的第1个C函数,该函数的核心是调用generic_handle_irq函数

5.6.2 generic_handle_irq函数

file:include/linux/irq.h

这步的调用主要是为了过渡到中断控制器的handle_irq函数

5.6.3 handle_level_irq函数

file:kernel/irq/chip.c

说明1:在关中断的情况下运行中断顶半部,是关什么中断 ?

这个问题其实不是那么好回答的,这与体系结构和中断控制器相关。在S5PV210 + Linux 2.6.35中,handle_level_irq先调用mask_ack_irq屏蔽了指定的中断,也就是正在处理的中断,然后在函数最后调用unmask_irq使能该中断,所以在handle_level_irq函数调用中断顶半部的过程中,当前处理的中断是一定被关闭的

但是我们知道,ARMv7架构中,进入IRQ模式时硬件会自动将CPSR的I位置为1,也就是在CPU级关中断

这其实就引出了IRQF_DISABLED标志逐渐废止的原因。在ARM架构中,本来就会在CPU级关中断,所以该标志没有效果。在X86结构中,也不再提倡关闭所有中断,这种行为比较野蛮(《Linux内核设计与实现》)

说明2:该函数调用的handle_IRQ_event函数将实际调用中断处理例程,在此前后会通过IRQ_INPROGRESS标志位标识中断顶半部是否处理结束

说明3:中断线程化支持简介

如果注册中断时指定了IRQF_ONESHOT标志,此处不会开中断,也就是说在中断顶半部结束时仍处于关中断的状态,这个操作就涉及内核对中断线程化的支持

如果采用中断线程化,在__setup_irq函数中会创建一个内核线程

irq_thread函数的主体就是进入睡眠,然后等待被唤醒,唤醒后运行thread_fn,下面是被唤醒后的核心操作

action->thread_fn(action->irq, action->dev_id);
if (oneshot)irq_finalize_oneshot(action->irq, desc);

此处是想说明一下oneshot的含义,如果申请中断时指定了IRQF_ONESHOT标志,在中断处理线程运行完成后就会进入irq_finalize_oneshot函数

如前所述,由于指定了oneshot标志,此时仍处于关中断的状态,所以irq_finalize_oneshot函数中将使能中断

在irq_finalize_oneshot函数中还轮询了IRQ_INPROGRESS标志,根据注释,这是为了处理一种极为罕见的情况,就是中断顶半部在一个CPU上还没有运行完,但是被唤醒并且被调度到其他CPU上运行的中断处理线程已经运行完毕

5.6.4 handle_IRQ_event函数

handle_IRQ_event函数的主体为下面的循环

// 遍历所有irqaction结构
do {trace_irq_handler_entry(irq, action);// 运行中断顶半部ret = action->handler(irq, action->dev_id);trace_irq_handler_exit(irq, action, ret);switch (ret) {case IRQ_WAKE_THREAD: // 顶半部返回IRQ_WAKE_THREAD,唤醒中断处理线程ret = IRQ_HANDLED;if (unlikely(!action->thread_fn)) {warn_no_thread(irq, action);break;}if (likely(!test_bit(IRQTF_DIED,&action->thread_flags))) {set_bit(IRQTF_RUNTHREAD, &action->thread_flags);wake_up_process(action->thread); // 唤醒中断处理线程}/* Fall through to add to randomness */case IRQ_HANDLED:status |= action->flags;break;default: // 对应返回IRQ_NONEbreak;}retval |= ret;action = action->next;
} while (action);return retval;

说明1:遍历所有irqaction

对于共享中断线的所有设备,他们注册的中断服务例程都会被调用到,由这些ISR自己通过读取设备的寄存器判断是否是自身触发的中断

说明2:从代码中可见,如果使用中断线程化处理但不指定IRQF_ONESHOT标志,如果中断处理线程结束前再次触发中断,此次的唤醒是没有实际意义的

但是并不是说这种情况就是错误的,这需要程序作者明确中断触发次数与中断处理线程被唤醒的次数是否需要一一对应

补充:X86架构中断处理流程图

可见不同体系结构在体系无关部分的处理是类似的

5.7 中断返回

5.7.1 中断 / 异常 / 系统调用返回概述

中断返回和异常返回的流程基本一致,差别在于异常返回时需要关中断,因为异常使用陷阱门实现,而进入陷阱门时不会自动关中断

5.7.2 ret_from_intr流程简介

① 判断进入中断前是用户空间还是内核空间

② 如果进入中断前是内核空间,则直接调用RESTORE_ALL

③ 如果进入中断前是用户空间,则可能需要进行一次调度;如果不调度,则可能有信号需要处理;最终还是调用RESOTRE_ALL

④ 最终调用iret指令,将控制权交给CPU从中断返回

5.7.3 中断与系统调度

① 中断 / 异常(包括系统调用)返回时,是进行调度的重要时机

② 时钟中断返回时是调度一来的最主要时机点,时钟中断处理函数不会直接进行调度,而是根据调度算法决定是否需要调度,以及调度的下一个任务。如果需要调度,则设置调度标志NEED_RESCHED

③ 调度的实际执行是在中断返回的时候检查NEED_RESCHED标记,如果设置则进行调度

5.7.4 中断与信号处理

① 信号处理是在当前进程从内核态返回用户态时进行的,在发生中断、异常(包括系统调用)或fork时,都有可能从内核态返回用户态,因此都是处理信号的时机

② 只有当前进程的信号才能在此时得到处理,其他非正在运行的进程的信号无法处理

6. 中断底半部处理机制

6.1 为什么引入中断底半部机制

① 中断服务例程(ISR)都是在中断请求关闭的条件下执行的,以避免嵌套而使得中断控制复杂化。但是中断是一个随机事件,会随时到来,如果关中断的事件太长,CPU就不能及时响应其他的中断请求,从而造成中断的丢失

② 引入中断底半部机制的目标就是尽可能快地处理完中断请求,把更多的处理向后推迟

③ Linux内核将中断处理分为不可中断的顶半部(top half)和可中断的底半部(bottom half),所有底半部机制的核心特征就是可以响应中断,差别只是在于是在中断上下文还是在进程上下文中运行

④ 中断顶半部必须在一个新的中断产生之前结束

6.2 软中断(softirq)机制

6.2.1 适用场景

① 软中断用在对底半部执行时间要求比较紧急的场合

② 在中断上下文中运行

6.2.2 softirq_action数据结构

action为软中断要执行的函数,调用该函数时,传递的参数就是对应的softirq_action结构指针

data成员全程无参与,在2.6.35及之后的版本中,该成员被删除

说明1:在sofirq.c中定义了softirq_vec数组,用于管理系统中所有softirq_action结构

说明2:软中断类型

softirq_vec数组可容纳32个成员,每个成员对应一个软中断类型,而实际使用的软中断类型由如下的枚举类型标识

该枚举类型随着内核版本变迁逐渐增加,其中TASKLET_SOFTIRQ就是用于支持小任务机制的软中断

6.2.3 注册软中断

注册软中断由open_softirq函数实现

通过该函数很容易找到不同类型软中断的注册点,

软中断类型

注册点

软中断action

HI_SOFIRQ

start_kernel

--> softirq_init(softirq.c)

tasklet_hi_action

TIMER_SOFTIRQ

start_kernel

--> init_timers(timer.c)

run_timer_softirq

NET_TX_SOFTIRQ

net_dev_init(dev.c)

为subsys_initcall

net_tx_action

NET_RX_SOFTIRQ

net_dev_init(dev.c)

为subsys_initcall

net_rx_action

TASKLET_SOFTIRQ

start_kernel

--> softirq_init(softirq.c)

tasklet_action

说明1:普通的内核定时器就是基于TIMER_SOFTIRQ软中断实现的

说明2:软中断类型的枚举值也标识了软中断的优先级,详见后文分析

6.2.4 触发软中断

触发软中断由raise_sofirq_irqoff函数实现,调用该函数时必须关中断

该函数主要完成2个任务,

① 标识指定类型的软中断发生

#define __raise_softirq_irqoff(nr) do { local_softirq_pending() \
|= 1UL << (nr); } while (0)#define local_softirq_pending() \__IRQ_STAT(smp_processor_id(), __softirq_pending)#define __IRQ_STAT(cpu, member) (irq_stat[cpu].member)

所谓标识,就是将CPU中断状态标志中与指定软中断对应的位置为1

② 唤醒softirqd内核线程,该内核线程可运行软中断

不是说软中断是在中断上下文运行的吗,怎么会出现一个处理软中断的内核线程 ? 后文将予以说明

6.2.5 执行软中断

软中断在irq_exit函数中被执行,当中断顶半部处理完成后,该函数被调用

说明1:local_sofirq_pending函数就是查看raise_sofirq_irqoff函数设置的标志位

#define local_softirq_pending() \__IRQ_STAT(smp_processor_id(), __softirq_pending)

说明2:说好的在开中断的情况下运行中断底半部呢,哪儿开中断了呢 ?

invoke_softirq函数会调用do_softirq函数,并最终调用__do_softirq函数,在该函数中打开了中断并实际处理软中断

说明3:为什么需要ksoftirqd内核线程

这里有一段处理值得注意,在处理完当前的soft_irq pending后,内核会再次检查是否有新的soft_irq pending,这个过程会重复max_restart次,如果之后还有soft_irq pending,则会唤醒ksoftirqd内核线程来处理软中断

这里主要是处理在软中断函数中又触发了软中断的情况,如果没有该内核线程,此处就无法处理所有的软中断

说明4:根据遍历soft_irq pending的顺序,就很容易理解软中断类型是和软中断优先级对应的

6.3 小任务(tasklet)机制

6.3.1 小任务机制概述

① 小任务机制是基于软中断实现的

② 小任务机制是IO驱动程序中实现可延迟函数的首选方法

③ 所谓小任务就是执行一些迷你任务

6.3.2 小任务数据结构

成员

含义

next

指向链表中下一个结构

state

小任务状态

TASKLET_STATE_SCHED:表示小任务已被调度,正准备投入运行

TASKLET_STATE_RUN:表示小任务正在运行,该状态只有在多处理器上才使用,单处理器系统是清楚小任务是否在运行中的

count

小任务引用计数,如果不为0,则小任务被禁止,不允许执行;只有当他为0时,小任务才激活

func & data

底半部中要推迟执行的函数,data为其参数

说明:小任务链表

每个CPU上均定义了小任务链表,用于组织小任务结构

6.3.3 定义小任务

6.3.3.1 静态创建小任务

说明:二者的差别在于加DISABLED后缀的宏创建的是禁止状态的小任务,即使调度了该小任务,也不会被执行,需要调用enable_tasklet将其使能

相应地,使用tasklet_disable函数可以禁止小任务,该函数会等待正在运行的小任务执行完成

6.3.3.2 动态创建小任务

动态创建就是自己定义各组件,然后调用tasklet_init将各组件绑定。从代码可见,tasklet_init创建的是激活状态的小任务

6.3.4 调度小任务

说明1:如果小任务已经处于SCHED状态,则不予调度

说明2:__tasklet_schedule函数实现

__tasklet_schedule函数将小任务插入表头,然后触发TASKLET类型的软中断。同时需要注意,此处是插入当前CPU的tasklet_vec链表

这里我们使用了"链表"一词,而非队列,因为这不是一个先进先出的结构,新的小任务是加入链表表头的,而任务从表头加入又和中断底半部是开中断有关,即在遍历运行小任务时,可能有新的小任务被加进来,因此只能加在链表头部

6.3.5 执行小任务

调度小任务之后,只是触发了软中断,实际的小任务执行是在上文介绍的tasklet_action函数中

说明1:小任务如何支持SMP

要理解tasklet_action函数,就需要理解小任务机制是如何支持SMP的,下面予以说明,

SMP下小任务2种状态的含义

TASKLET_STATE_SCHED:小任务已被调度,即已经被加入某个CPU的小任务链表

TASKLET_STATE_RUN:小任务正在运行

在SMP下就存在这种情况,一个小任务在某个CPU上运行时,又被加入另一个CPU的小任务链表,而另一个CPU上也要运行tasklet_action

这就可以理解在tasklet_action中,如果tasklet_trylock失败为何要将该小任务重新加入当前CPU小任务链表的表头,因为这个小任务正在别的CPU上运行,在本CPU上的运行就暂时延后

由于将小任务又加回了小任务链表,所以最后再触发一次TASKLET软中断,以便该小任务可以被执行

这也就出现了上文提到的,在软中断中又触发软中断的情况,所以引入了处理软中断的内核线程,内核的设计真的是环环相扣

所以在SMP中,小任务的状态变迁如下,

① 调度小任务时设置TASKLET_STATE_SCHED标志

② 运行小任务时设置TASKLET_STATE_RUN标志并清除TASKLET_STATE_SCHED标志

③ 小任务运行完成,清除TASKLET_STATE_RUN标志

说明2:操作每CPU上的小任务链表时需要关中断,因为中断顶半部中会调用tasklet_schedule将小任务加入队列,这其实是一个中断顶半部和中断底半部之间的互斥

而一旦关闭了中断,本CPU上的调度也就停止了,所以是实现互斥代价最大但最彻底的手段

6.3.6 杀死小任务

tasklet_kill

说明1:tasklet_kill可能导致睡眠,所以不能在中断上下文中调用

说明2:函数流程分析

① 对于SCHED标志未被设置的小任务,tasklet_kill会设置SCHED标志,然后等待RUN标志被清除,最后清除SCHED标志

对于从未被调度过的小任务,这么做也是安全的,所以一般用在初始化阶段的错误处理中

② 对于SCHED标志被设置的小任务,tasklet_kill会先等待其SCHED标志被清空,也就是小任务被运行了,然后等待其运行结束

说明3:yield函数

在tasklet_kill函数中,调用yield函数暂时放弃CPU。由于调用sys_sched_yield之前将task设为RUNIING状态,所以不会被移出就绪队列,所以可以被再次调度到

6.4 工作队列(workqueue)机制

6.4.1 工作队列机制概述

① 基于软中断的中断底半部机制在中断上下文中运行,所以不能挂起。而且由于是串行执行(__do_softirq函数),因此只要有一个软中断处理时间较长,就会导致其他中断响应的延迟

② 工作队列将一组内核线程作为中断守护线程使用,将中断底半部交由该内核线程运行,所以可以使用所有可以在线程中使用的方法

③ 可以将多个中断放在一个内核线程中,也可以为每个中断分配一个线程,内核默认会启动一个工作者线程keventd_wq

6.4.2 工作队列数据结构

成员

含义

cpu_wq

每CPU工作队列数组,如果创建多线程工作队列,内核根据当前系统CPU的个数,在每个内核上创建一个工作队列

name

工作者线程名称

list

组织工作队列链表,只有多线程工作队列才链入全局链表,单线程工作队列该字段为空

说明:包含实际内容的每CPU工作队列数据结构

成员

含义

lock

保护worklist的自旋锁

worklist

工作队列,组织work_struct结构

more_work

工作者线程实现睡眠的等待队列头

wq

所属的workqueue结构

thread

工作者线程

6.4.3 工作数据结构

成员

含义

pending

工作是否在等待处理

entry

维护工作链表,链入cpu_workqueue_struct结构的worklist

func & data

工作要执行的函数和参数

wq_data

内部使用

timer

延迟调度的工作所用的内核定时器

说明:工作队列 & 每CPU工作队列 & 工作数据结构关系

如果创建单线程工作队列,则只有红框部分内容

6.4.3 创建工作队列

创建单线程和多线程工作队列都是通过__create_workqueue函数实现,只是传递的参数不同

下面就分析下__create_workqueue函数

说明1:统一的工作者线程函数

无论是创建单线程还是多线程工作队列,最终的工作者线程函数都是worker_thread,下面分析下worker_thread的主体部分

工作者线程的主要工作就是等待被唤醒,然后调用run_workqueue函数执行每CPU工作队列上的所有工作

说明2:内核默认创建的event工作队列

先说明一下函数调用关系,

init // 就是init进程的入口函数
--> do_basic_setup--> init_workqueues

可见每个CPU都会有一个events工作者线程

说明3:run_workqueu函数分析

此处需要注意,run_workqueue函数每完成一个work都会在work_done等待队列上唤醒一次,下文会见到与之对应的操作

6.4.4 创建工作

6.4.4.1 静态创建

6.4.4.2 动态创建

动态创建就是定义后再设置

6.4.5 调度工作

6.4.5.1 __queue_work函数实现

__queue_work函数是将工作加入每CPU工作队列的核心函数,在完成工作的入队后就唤醒工作者线程

6.4.5.2 queue_work函数实现

说明1:未执行完的work不会被重复入队

work_struct结构的pending字段初始值为0,加入工作队列时将其置为1,run_workqueue函数在处理完该工作后该工作出队,并将pending字段置为0

所以如果queue_work时返回0,说明该work仍在工作队列中,尚未执行

因此,如果在中断顶半部调用queue_work,如果中断到来时上一次的工作尚未处理完,此次的queue_work是失败的。这再次提醒我们注意中断处理时间和中断的间隔,一定要区分哪些任务需要中断底半部处理,哪些任务需要驱动程序处理

说明2:对于单线程工作队列,无论在哪个CPU上调度工作,都是加入CPU0的工作队列

说明3:怎么理解queue_work函数的注释

多CPU系统中也可以建立单线程工作队列,此时无论在哪个CPU提交工作,都是由CPU0处理

说明4:多线程工作队列的并发影响

如果中断被不同的CPU响应,并且在中断顶半部中调用queue_work,会不会导致并发问题 ?

如果只有一个work_struct是不会的,因为调度任务成功的前提是之前提交的任务已经完成。但是如果有一组work_struct可供调度,则会被提交到不同CPU的每CPU工作队列,此时系统无法保证这些工作的执行顺序,这点在驱动程序设计中需要加以考虑

说明5:多线程工作队列适用场景探讨

根据上面的分析,多线程工作队列适用于有多个work_struct可供调度,且对工作的执行顺序没有要求的情况

6.4.5.3 schedule_work函数实现

schedule_work只是将工作加入系统默认的event工作队列

6.4.5.4 延迟工作实现

使用queue_delayed_work & schedule_delayed_work可以创建推迟对工作的调度,内部的实现原理就是利用内核定时器,当定时器到期时才将工作加入工作队列

6.4.6 销毁工作队列

使用destroy_workqueue函数,可以注销一个工作队列,其中flush_workqueu函数会等待当前工作队列上的工作都执行完成

分析flush_cpu_workqueue函数可知,该同步通过cpu_workqueue_struct结构中的work_done等待队列实现

说明1:cwq->thread == current分支的含义

如果是在工作者线程调用destroy_workqueue函数,则无需同步,直接调用run_workqueue函数执行完当前所有work即可

说明2:如果是非工作者线程调用destroy_workqueue,则是通过work_done等待队列实现同步。只要工作队列上还有工作,调用线程就会进入睡眠

结合上文分析,在run_workqueue函数中,每完成一个work都会在work_down等待队列上唤醒一次,所以此处的等待操作是合理的

说明3:flush_cpu_workqueue函数的锁操作

此处在调用schedule之前释放了互斥锁,当再次被调度运行时立即上锁,进而实现对cpu_workqueue_struct的保护

6.5 中断下半部小结

6.5.1 中断底半部使用原则

① 软中断最好不用,他甚至不算是一种真正的中断处理机制,只是小任务机制的实现基础

② 工作队列也要少用,如果不是必须要用到进程上下文才能使用的机制,就不要使用工作队列

③ 中断处理过程只是对中断进行简单的处理,即使是中断底半部,也只是做必须在中断中要做的事情(e.g. 保存数据,读取寄存器确定硬件状态),其他都应交给驱动程序完成

6.5.2 中断底半部选择标准

如果推后执行的任务需要睡眠,就选择工作队列;如果推后执行的任务不需要睡眠,就选择小任务

7. 时钟中断机制

7.1 时钟中断是操作系统的脉搏

整个操作系统的活动都受到时钟中断的激励,系统利用时钟中断维持系统时间、促使进程的切换,以保证所有进程共享CPU;利用时钟中断进行记账、监督系统工作以及确定未来的调度优先级等工作

7.2 基本时钟硬件

① Linux的OS时钟的物理产生原因是可编程定时/计数器产生的输出脉冲,这个脉冲通过中断控制器,就可以定期触发一个中断请求信号,这就是时钟中断

② 时钟中断的周期,也就是脉冲信号的周期,称作tick

③ 从本质上说,时钟中断只是一个周期性的信号,完全是硬件行为,该信号触发CPU去执行一个中断服务程序

7.3 基本时钟运行机制

7.4 Linux系统时间

7.4.1 tick

① 节拍率(HZ)是时钟中断的频率

② 目前的Linux已经采用tickless时间系统,与传统的实现方式差别很大

7.4.2 jiffies

① jiffies用来记录自系统启动以来产生的总节拍数

② jiffies是内核中的一个全局变量

③ jiffies总是unsigned long类型

7.4.3 xtime

① xtime和RTC时间一样,是日常使用的墙上时间

② 实际时间存放在xtime中,系统启动时内核通过读取RTC来初始化实际时间

③ xtime类型为struct timespec

说明:操作系统的时间基准由设计者决定,Linux的时间基准为1970年1月1日凌晨0点

7.4 Linux时钟框架简介

目前的Linux时钟框架相当复杂,此处给出框架结构图

① 时钟事件设备(clock_event_device)用于实现普通定时器和高精度定时器,同时也用于产生节拍tick事件,供给进程调度子系统使用

② 从软件架构上看,时钟事件设备被分为两层,与硬件相关的被放在machine层,与硬件无关的通用代码被集中到了通用事件框架层。这符合内核对软件的设计需求,平台的开发者只需实现平台相关的接口即可,无需关注复杂的时间框架

③ tick_device是基于时钟时间设备的进一步封装,用于代替原有的时钟滴答中断,给内核提供节拍事件,以完成进程的调度和进程信息统计,负载平衡和事件更新等操作

在新版内核代码的NO_TICK模式下,仍然有节拍率(tick rate)的概念。节拍率通过静态预处理定义,仍为HZ,内核会根据HZ值设置时钟事件,启动tick中断。HZ表示1秒产生多少个时钟硬件中断,tick就表示连续两个中断的间隔时间

我们查看一下当前使用的Ubuntu系统中的HZ值,

可见HZ值为250,即一个tick为4ms

说明1:软件框架的更新往往是为了适配硬件的更新,下面简要介绍一下与时钟有关的硬件

① 实时时钟RTC(Real Time Clock):用于长时间存放系统时间的设备,即使关机后也可依靠主板CMOS电池继续保持系统的计时

② 可编程间隔器PIT(Programmable Interval Timer):该设备可以周期性地发送一个时间中断信号。在Linux系统中,该中断时间间隔由HZ标识,这个时间间隔也被称为一个tick

③ 时间戳计数器TSC(Time Stamp Clock):CPU附带一个64位的时间戳寄存器,当时钟信号到来时该寄存器值自动加1

④ 高精度计时器(HPET):这是一种由Intel开发的新型定时芯片,该设备有一组计时器,每个计时器对应有自己的时钟信号,时钟信号到来时自动加1

⑤ CPU本地定时器:在处理器的本地APIC提供的一个定时设备,可以单次或周期性地产生中断信号

⑥ 高精度定时器(hrtimer):提供纳秒级的定时精度,以满足对精确时间有迫切需求的应用程序或内核驱动,如多媒体应用、音频设备的驱动程序等

说明2:高精度定时器简介

① 高精度定时器以红黑树方式组织,树的最左边的节点就是最快到期的定时器

② 每个CPU有一个hrtimer_cpu_base结构,这个结构管理着3种不同的时间基准系统的hrtimer,分别是实时时间、启动时间和单调时间

每种基准系统通过一个字段,指向他们各自的红黑树

7.5 定时器及应用

7.5.1 定时器数据结构

成员

含义

entry

用于组织定时器链表

expire

以tick为单位的定时器到期绝对时间

lock

保护定时器的自旋锁

function & data

定时器到期要执行的函数和参数

base

定时器所在的定时器向量

说明:上述数据结构基于ulk对应的2.6.11版本

7.5.2 创建定时器

定义定时器变量后,使用init_timer进行初始化

之后就可以填充结构中需要的值,示例如下,

struct timer_list my_timer;
init_timer(&my_timer);
my_timer.expires = jiffies + delay; // 定时器到期的绝对时间
my_timer.function = my_function;
my_timer.data = 0;

7.5.3 定时器操作

操作

函数

激活定时器

void add_timer(struct timer_list *timer);

停止定时器

int del_timer(struct timer_list *timer);

修改定时器时间

int mod_timer(struct timer_list *timer, unsigned long expires);

判断定时器是否已被激活

static inline int timer_pending(const struct timer_list *timer);

说明1:内核可以保证不会在定时时间到期前运行定时器处理函数,但是有可能延误定时器的执行。因为是在软中断中执行,不具备硬实时能力

说明2:del_timer可以作用于被激活或未激活的定时器,只是返回值不同,如果定时器未被激活,该函数返回0;否则返回1

不需要为已经到期的定时器调用该函数,因为他们会被自动从定时器链表中删除

说明3:add_timer操作是对mod_timer操作的封装

mod_timer函数会判断修改前后的定时器绝对到期时间是否相同,只有不相同时,才会更新并加入合适的定时器向量,下图为__mod_timer函数中的相关逻辑

注意:后续的timer_pending函数就是依据timer_list的base成员是否被设置,来判断定时器是否已处于pending状态

7.5.4 执行定时器

内核定时器作为软中断运行,在时钟中断处理函数中,会调用run_local_timers函数,该函数会触发定时器软中断

而定时器软中断对应的处理函数为run_timer_softirq

对具体细节的分析,可参考《深入理解Linux内核》chapter 6:定时测量章节的学习笔记

说明1:内核动态定时器管理方式简介

内核动态定时器的动态,是指内核的定时器队列是可以动态变化的,其关键就在于定时器向量的概念

所谓定时器向量就是指一条定时器队列,队列中的每一个元素都是一个timer_list结构,而队列中所有的定时器都会在同一个时刻到期,也就是说队列中每一个timer_list结构都具有相同的expires值,不同的定时器队列根据其expires值不同再连接成一个双向循环的队列

定时器expires值与jiffies变量的差值决定了一个定时器在多长时间后到期,在32位系统中,这个时间差值的最大值应该为2^32,如果是基于定时器向量的定义,那么内核将要至少维护2^32个time_list结构类型的指针,这显然是不现实的。另一方面,从内核本身来看,他所关心的定时器应当是那些当前已经到期或者即将要到期的定时器,所以对于即将到期的定时器,Linux会严格按照定时器向量的基本定义来组织他们,将最近到期的定时器按照各自不同的expires值组织成256个定时器向量

对于其他的定时器,由于他们离到期还有一段时间,因此内核并不关心他们,而是将他们组织在一个松散的队列中,即各定时器的expires值可以互不相同的一个定时器队列。这种定时器组织形式的基本思想就来源于时间轮(Timing-Wheel)算法

另外,由于对定时器到期时间的检查总是由可延迟函数进行,而可延迟函数被激活后很长时间才能被执行,因此内核不能保证定时器函数正好在定时器到期时被执行,只能保证他们在适当的时间执行,或者说他们会在定时器到期后延迟几百毫秒再被执行。因此,内核定时器精度不高,对于必须严格遵守定时时间的实时应用来说,应当选用hrtimer高精度定时器

说明2:__run_timers核心逻辑

可见在执行定时器回调函数时,一定已经解除pending状态,timer_list的base成员已经被置为NULL

7.5.5 timer_pending函数使用实例

① timer_pending的判断标准

说明1:2.6.11版本通过定时向量字段判断定时器是否处于pending状态,之所以强调版本,是因为后续版本有变化

在2.6.35版本中,改为使用entry字段来判断

说明2:timer_pending函数并没有进行互斥处理,因此调用者需要确保序列化调用

② 在按键去抖动操作中使用动态定时器

在按键中断的顶半部ISR中,根据定时器是否处于pending状态分别调用add_timer与mod_timer

irqreturn_t key_isr(int this_irq, void *data)
{if (timer_pending(&timer)) {// timer已经处于pending状态,更改到期时间mod_timer(&timer, jiffies + delay);} else {// timer尚未处于pending状态,启动定时器timer.expires = jiffies + delay;add_timer(&timer);}
}

需要如此处理的原因在于一旦调用add_timer或mod_timer,定时器便开始计时,因此不能在驱动的初始化过程中启动定时器

③ 讨论:timer_pending的互斥问题

如上文所述,timer_pending函数在判断timer->base成员时,并没有进行互斥处理。那么我们就先来分析下,哪些场景会使用这个字段

a. timer_pending函数

该函数会读取该字段

b. del_timer函数

在删除timer时,也会将timer->base字段置为NULL

该操作会对timer所述的定时器向量上锁,但是不会对timer本身上锁

c. __mod_timer(add_timer & mod_timer函数会调用)

可见在__mod_timer函数中,会对定时器 & 定时器向量均上锁

d. __run_timers(执行到期的动态定时器)

该函数也只会对timer所述的定时器向量上锁

从上述分析可见,对timer->base字段进行完全正确的互斥是很难的

Linux操作系统原理与应用05:中断和异常相关推荐

  1. Linux 操作系统原理 — 内存 — 页式管理、段式管理与段页式管理

    目录 文章目录 目录 前文列表 页式管理 快表 多级页表 基于页表的虚实地址转换原理 应用 TLB 快表提升虚实地址转换速度 页式虚拟存储器工作的全过程 缺页中断 为什么 Linux 默认页大小是 4 ...

  2. Linux 操作系统原理 — 内存 — 内存分配算法

    目录 文章目录 目录 前文列表 内存碎片 伙伴(Buddy)分配算法 Slab 算法 虚拟内存的分配 内核态内存分配 vmalloc 函数 kmalloc 用户态内存分配 malloc 申请内存 用户 ...

  3. Linux 操作系统原理 — 内存 — 基于局部性原理实现的内/外存交换技术

    目录 文章目录 目录 前文列表 基于局部性原理实现的内-外存交换技术 局部性原理 Swap 交换分区 前文列表 <Linux 操作系统原理 - 内存 - 物理存储器与虚拟存储器> < ...

  4. Linux 操作系统原理 — 内存 — 基于 MMU 硬件单元的虚/实地址映射技术

    目录 文章目录 目录 前文列表 物理地址与虚拟地址 内存空间的组织方式 虚拟地址空间的编址 内核态地址空间 用户态地址空间 内-外存空间的交换与虚拟存储空间之间的映射关系 缺页异常 前文列表 < ...

  5. Linux 操作系统原理 — 系统结构

    目录 文章目录 目录 Linux 系统架构 Linux 内核 内存管理 进程管理 文件系统 设备驱动程序 网络接口 Shell Linux 系统架构 Linux 系统一般有 4 个主要部分:内核.Sh ...

  6. linux的原理和运用,Linux操作系统原理与应用_内存寻址

    原标题:Linux操作系统原理与应用_内存寻址 第五讲今天上线啦. 在本次课程中,陈老师详细的讲解了有关于内存寻址的演变的相关知识. 第一部分中,介绍了关于内存寻址的相关背景知识.内存寻址-操作系统设 ...

  7. linux操作系统原理_Linux内核分析-操作系统是如何工作的(二)

    linux操作系统的主要构架如图1所示,我们知道,操作系统是通过管理CPU进程.存储器.文件系统.设备驱动.以及网络接口等相关部分来工作的,我们这里主要是通过分析关于CPU的操作即进程的管理执行来分析 ...

  8. Linux操作系统原理与应用03:进程

    目录 1. 进程简介 1.1 程序和进程 1.2 进程的定义 1.2.1 正文段 1.2.2 用户数据段 1.2.3 系统数据段 1.3 进程的层次结构 1.3.1 进程的亲缘关系 1.3.2 进程树 ...

  9. Linux操作系统原理与应用01:概述

    目录 1. Linux内核的技术特点 1.1 单内核结构 1.1.1 单内核特性 1.1.2 微内核特性 1.2 抢占式内核 1.2.1 非抢占式内核特性 1.2.2 抢占式内核特性 1.3 支持动态 ...

最新文章

  1. gin自定义HTTP配置
  2. mfc程序转化为qt_智慧虎超:小程序如何为珠宝行业助力?低频商品的高频转化你懂吗...
  3. boost::boost::stoer_wagner_min_cut用法的测试程序
  4. poj-1659-Frogs Neighborhood-(图论-是否可图)
  5. 特斯拉入驻天猫卖车了 将连做8天直播
  6. CCNP实验---EIGRP自动汇总
  7. 软件架构发展的几个阶段
  8. 几种不同格式的json解析
  9. 云分众享,阿里云盘资源搜索工具
  10. 文科生学大数据分析吃力吗
  11. jquery-重要的方法和注意事项
  12. python——计时器,走马灯
  13. 电视root工具_TapTap | 无需Root,成功移植 IOS14,拿下!!!
  14. Systemd基础篇:4:对服务启动出现的问题进行debug的方法
  15. 计算机网络技术计什么意思,计算机网络技术和计机应用技术.doc
  16. 有眼界才有境界,有实力才有魅力,有思路才有出路,有作为才有地位。
  17. 【Java】页面静态化
  18. win10 charles 抓 IOS https
  19. 连接网络怎么连接无线网络连接服务器,连接无线网络能玩局域网游戏吗怎么设置...
  20. 一个人颓废的九大根源

热门文章

  1. CTF中遇到不知道文件类型_遇到孩子厌学不知道怎么沟通?做好这些策略,孩子肯定爱学...
  2. MySQL懒查询_mysql 联查的基本命令
  3. java pfx提取私钥加签,详解pfx证书提取公私钥的方法
  4. Spring Boot接口返回的字段名和实体类中定义的字段名不一致
  5. 全国专业技术人员计算机应用能力考试题,2017年全国专业技术人员计算机应用能力考试题库...
  6. C语言极坐标转直角坐标,C语言实现直角坐标转换为极坐标的方法
  7. MySQL访问权限管理
  8. linux ssh端口是否打开,如何查看linux中的ssh端口开启状态
  9. 根据线程名获取线程及停止线程
  10. Kotlin入门(24)如何自定义视图