0%

中断学习

写在前面

在ENOS系统移植的过程中需要调试CPU和交换芯片的中断,这里记录一下对中断的学习!

中断简介

Linux内核需要对连接到计算机上的所有硬件设备进行管理,他们之间需要互相通信,一般有两种方案可以实现。

1
2
1. 轮询(polling)内核定期对设备的状态进行查询,然后做出相应的处理
2. 中断(interrupt)让硬件在需要的时候向内核发出信号(变内核主动为硬件主动)

轮询是周期性的重复执行,大量耗用CPU的时间,效率比较低,对于实时性比较高的操作,肯定是不适用的。

从物理学的角度看,中断是一种电信号,由硬件设备产生,并直接送入中断控制器(如8259A)的输入引脚上,然后再由中断控制器向CPU发送相应的信号。处理器检测到该信号,便中断当前正在处理的工作,转而去处理中断。对于软件开发人员,一般需要用到的就是中断号和中断处理函数。

提一下,PCIE可以通过MSI(message signaled interrupts)方式实现中断:

CPu里面有一段特殊的寄存器空间,往这个寄存器里面写数据,就会触发CPU中断。pci设备经过配置以后,一旦需要上报中断就会往cpu这种寄存器里面写一个值,触发cpu中断。

中断的处理流程:

  1. 保存现场
  2. 执行中断
  3. 恢复被中断进程的现场,继续执行

中断分类

中断可分为同步(synchronous)中断和异步(asynchronous)中断:

1
2
1. 同步中断是当指令执行时由CPU控制单元产生,之所以称为同步,是因为只有在一条指令执行完毕后CPU才会发出中断,而不是在代码指令执行期间,比如系统调用
2. 异步中断是指由其他硬件设备依照CPU时钟信号随机产生,即意味着中断能够在指令之间产生,例如键盘中断

同步中断称为异常(exception),异常可分为故障(fault)、陷阱(trap)、终止(abort)三类。
异步中断被称为中断(interrupt),中断可分为可屏蔽中断(Maskable interrupt,外部设备产生的)和非屏蔽中断(Nomaskable interrupt,计算机内部硬件产生的)
异常是CPU发出的中断信号,与中断控制器无关,不能被屏蔽。

广义上讲中断可分为四类:中断、故障、陷阱、终止。它们之间的异同点参照下表。

类别 原因 异步/同步 返回行为
中断 来自I/O设备的信号 异步 总是返回到下一条指令
陷阱 有意的异常 同步 总是返回到下一条指令
故障 潜在可恢复的错误 同步 返回到当前指令
终止 不可恢复的错误 同步 不会返回

中断控制器

常见的中断控制器有两种,两片8259A外部芯片’级联’和多级I/O APIC系统,见下图:

至于硬件实现细节这里不做过多描述。辨别一个系统是否正在使用I/O APIC,可以使用如下命令查看:

可以看到第6列上显示的是IO-APIC,如果上面显示的是XY-APIC,说明系统正在使用8259A芯片。
对上面文件的输出,解释如下:

  1. 第一列表示IRQ中断号
  2. 第二、三、四、五列表示相应的CPu核心被中断的次数
  3. 第六列表示使用控制器
  4. 第七列表示硬件中断号和中断触发方式(电平或边沿)
  5. 第八列表示中断名称
  6. 有一些IRQ号会表示为NMI,LOC之类的,这是系统保留的,用户无法访问和配置

此外,/proc/interrupts文件中列出的是当前系统使用的中断情况,如果某个中断处理没有安装(包括安装后卸载的),是不会显示的。但是/proc/stat会记录机器从启动开始各个中断序号发生中断的次数。

中断向量

x86中支持256种中断,将这些中断源按照0到255的顺序对没中中断进行编号,这个标号叫做中断向量,通常用8位无符号整数来存储这个向量。中断号与中断向量一一映射。
中断号和中断向量概念不同。当I/O设备把中断信号发送个中断控制器时,与之关联的是一个中断号;而当中断控制器将该中断信号传递给CPU时,与之关联的是一个中断向量。中断号是以中断控制器的角度而言的;中断向量则是以CPU的角度而言的。
通常,Intel将编号为0~31的向量分配给异常和非屏蔽中断。

中断服务例程

在响应一个具体的中断时,内核会执行一个函数,这个函数被称为中断服务例程(interrupt service routine, ISR)。每一个设备的驱动程序中都会定义相关的中断服务例程。

现今的中断处理流程都会分为两部分:上半部分(top half)和下半部分(bottom half),原因如下:

  1. 中断可以随时打断CPU对其它程序的执行,如果被打断的代码对系统很重要,那么此时中断处理程序的执行时间应该越短越好
  2. 中断处理程序在执行时,会屏蔽同条中断线上的中断请求;如果设置了IRQF_DISABLE,那么该中断服务程序执行时是会屏蔽其他所有其它的中断请求。那么此时应该让中断处理程序执行的越快越好。

这样划分是有一定原因的,因为我们必须有一个快速、异步而且简单的处理程序专门来负责对硬件的中断请求作出快速响应,与此同时也要完成那些对时间要求很严格的操作。而那些对时间要求相对宽松,其它的剩余工作则会在稍后的任意时间执行,也就是所谓的下半部分执行。

上半部分只能通过中断处理程序实现,下半部分可以通过多种机制来完成:小任务(tasklet),工作队列,软中断,不管是哪种机制,他们均为下半部分提供了一种执行机制,比上半部分灵活多了,至于何时执行,则由内核负责。

第一个中断测试程序

了解了下中断的基本概念,下面就写一个小demo来实际测试一下吧。代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
#include <linux/init.h>                 
#include <linux/kernel.h>
#include <linux/module.h>
#include <linux/moduleparam.h>
#include <linux/interrupt.h>
#include <linux/stat.h>
#include <linux/slab.h>

static int irq = 1; //保存中断号irq
static char *devname = NULL; //保存中断名称*devname

//利用宏module_param来接受参数
module_param(irq, int, 00644); //S_IRUGO=00644
module_param(devname, charp, 00644);

//定义一个结构体,在request_irq函数中的void *dev_id经常设置为结构体或NULL
struct dev_info{
int irq_id;
char *dev_name;
};

struct dev_info *mydev_info = NULL;

//声明中断处理函数(上半部分)
static irqreturn_t myirq_handler(int irq, void *dev);

static int __init myirq_init(void)
{
printk("zhw test:Module is working ...\n");
//分配struct dev_info结构体内存
mydev_info = kmalloc(sizeof(struct dev_info), GFP_KERNEL);
if(!mydev_info)
{
printk("kmalloc failed!\n");
return -1;
}
memset(mydev_info, 0, sizeof(struct dev_info));
mydev_info->irq_id = irq;
//分配结构体struct dev_info->char *dev_name内存
mydev_info->dev_name = kmalloc(10, GFP_KERNEL);
if(!mydev_info->dev_name)
{
printk("kmalloc 1 failed!\n");
return -1;
}
mydev_info->dev_name = devname;

if(request_irq(irq, &myirq_handler, IRQF_SHARED, devname, mydev_info))
{
printk("%s request IRQ:%d failed\n", devname, irq);
return -1;
}
printk("%s request IRQ:%d success..\n", devname ,irq);
return 0;
}

static void __exit myirq_exit(void)
{
printk("unloading my module ..\n");
free_irq(irq, mydev_info);
printk("freeing IRQ %d\n", irq);
}

static irqreturn_t myirq_handler(int irq, void *dev)
{
struct dev_info mydev;
static int count = 1;
mydev = *(struct dev_info *)dev;

printk("key:%d\n", count);
printk("devname:%s. devid:%d\n is working..\n", mydev.dev_name, mydev.irq_id);
printk("ISR is leaving\n");
count++;
return IRQ_HANDLED;
}

module_init(myirq_init);
module_exit(myirq_exit);

MODULE_LICENSE("GPL");

因为中断程序一般包含在某个设备的驱动程序中,所以这个程序本质就是一个内核模块。这里面主要就是驱动的初始化,退出,以及中断服务例程(ISR)。这里共享键盘的中断号,x86下键盘的中断号是1.
Makefile如下:

1
2
3
4
5
6
7
8
9
obj-m:=first_interrupt.o                                                                                                                                                                                          
KDIR:=/lib/modules/$(shell uname -r)/build
PWD:=$(shell pwd)

default:
$(MAKE) -C $(KDIR) M=$(PWD) modules

clean:
rm -rf .*.cmd *.o *.mod.c *.ko .tmp_versions

使用方法:

  1. cat /proc/interrupts查看中断号,注意如果是使用ssh或telent到linux上的是不会响应键盘中断的,需要使用虚拟机来实验
  2. 加载驱动sudo insmod ./first_interrupt.ko irq=1 devname=zhwirq
  3. 查看驱动lsmod | grep first,查看中断cat /proc/interrupts | grep zhw
  4. dmesg查看内核日志文件,dmesg | tail -20
  5. 卸载驱动sudo rmmod first_interrupt

加载驱动后,先进行驱动初始化,之后每当有键盘中断触发后,都会进入ISR,卸载驱动后不会再触发。

参考资料:
Linux下的中断(interrupt) 简介
中断入门
PCI&PCIE MSI中断
第一个中断驱动程序
如何编译内核ko