【读书笔记】Linux内核设计与实现--中断和中断处理

众所周知,处理器的速度跟外围硬件设备的速度往往不在一个数量级上,因此,如果内核采取让处理器向硬件发出一个请求,然后专门等待回应的办法,显然差强人意。
既然硬件的响应这么慢,那么内核就应该在此期间处理其他事务,等待硬件真正完成了请求的操作之后,再回过头来对它进行处理。

Q:如何做?
A:轮询(polling)可能会是一种解决办法。不过这种方法很可能会让内核做不少无用功(轮询总会周期性的重复执行)。
因此,需要提供一种机制,让硬件在需要的时候再向内核发出信号(变内核主动为硬件主动)–中断机制。

1.中断

中断使得硬件得以发出通知给处理器,引起内核的关注。
中断本质上是一种特殊的电信号,由硬件设备发向处理器。
中断随时可以产生,因此中断处理程序也就随时可能执行。
不同的设备对于的中断不同,而每个中断都通过一个唯一的数字标志。
这些唯一的数字标志称为中断值(中断请求IRQ线)

异常(也称同步中断):
在操作系统中,讨论中断就不能不提及异常。
异常与中断不同,它在产生时必须考虑与处理器时钟同步。它们的工作方式类似,其差异只在于中断是由硬件而不是软件引起的。

ps:系统调用就是一种异常(系统调用处理程序异常),通过软中断实现系统调用。

2.中断处理程序

在响应一个特定中断的时候,内核会执行一个函数–中断处理程序(interrupt handler) 或 中断服务例程(interrupt service routine,ISR)。

一个设备的中断处理程序是它设备驱动程序的一部分–设备驱动程序是用于对设备进行管理的内核代码。

Q:中断处理程序和其他内核函数的区别?
A:中断处理程序就是普通的C函数,只不过必须按照特定的类型声明,以便内核能够以标准的方式传递处理程序的信息。其真正的区别在于,中断处理程序是被内核调用来响应中断的,而它们运行于称之为中断上下文(偶尔也称为原子上下文–上下文的执行代码不可阻塞)的特殊上下文中。

ps:中断处理程序通常不是和特定设备关联,而是和特定中断关联的,也就是说,如果一个设备可以产生多种不同的中断,那么该设备就可以对应多个中断处理程序。相应的,该设备的驱动程序也就需要准备多个这样的函数。

3.上半部与下半部的对比

又想中断处理程序运行得快,又想中断处理程序完成的工作量多,这两个目的显然有所抵触。鉴于两个目的之间存在此消彼长的矛盾关系,所以一般把中断处理切为两个部分或两半

中断处理程序是上半部(top half) – 接收到一个中断,它就立即开始执行,在所有中断被禁止的情况下完成(严格时限)。
能被允许稍后完成的工作会推迟到下半部(bottom half),然后在何时的时机,下半部会被开中断执行(Linux提供了实现下半部的各种机制)。

4.注册中断处理程序–request_irq

中断处理程序是管理硬件的驱动程序的组成部分。每一个设备都有相关的驱动程序,如果设备使用中断,那么相应的驱动程序就注册一个中断处理程序。

驱动程序可以通过request_irq()函数注册一个中断处理程序(被声明在<linux/interrupt.h>)中,并且激活给定的中断线,以处理中断:

/* request_irq:分配一条给定的中断线 */
int request_irq(unsigned int irq,irq_handler_t handler,unsigned long flags,const char *name,void *dev);

irq:要分配的中断号。(对某些设备该值通常预先确定的,如PC设备上的系统时钟或键盘,对于其他设备,通常可以通过探测获取,或者编程动态确定)
handler:是一个指针,指向处理这个中断的实际中断处理程序(回调)。只要操作系统一接收到中断,该函数就被调用。

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

flags:可以为0,也可以是一个或者多个标志的位掩码,定义在<linux/interrupt.h>中,较重要的有IRQF_DISABLED、IRQF_SAMPLE_RANDOM、IRQF_TIMER、IRQF_SHARED等。
name:是与中断相关的设备的ASCII文本表示。这些名字会被/proc/irq和/proc/interrupts文件使用。
dev:用于共享中断线。当一个中断处理程序需要释放时,dev将提供唯一的标志信息(cookie),以便从共享中断线的诸多中断处理程序中删除指定的那一个。如果没有这个参数,那么内核不可能知道在给定的中断线上到底要删除哪一个处理程序。如果无需共享中断线,那么将该参数赋值为空值(NULL)就可以了,但是,如果中断线是被共享的,那么就必须传递唯一的信息。另外,内核每次调用中断处理程序时,都会把这个指针传递给它。
ps:中断处理程序都是预先在内核进行注册的回调函数,而不同的函数位于不同的驱动程序中,所以在这些函数共享同一个中断线时,内核必须准确的为它们创造执行环境,此时就可有通过这个指针将有用的环境信息传递给它们。

request_irq()成功执行会返回0,如果返回非0值,就表示有错误发生,可根据错误码判断错误原因。

ps:

  1. request_irq()函数可能会睡眠,因此,不能在中断上下文或其他不允许阻塞的代码中调用该函数。
  2. 初始化硬件和注册中断处理程序的顺序必须正确,以防止中断处理程序在设备初始化完成之前就开始执行。

Q:request_irq()函数为何可能会睡眠?
A:在注册过程中,内核需要在/proc/irq文件中创建一个与中断对应的项。函数proc_mkdir()就是用来创建这个新的procfs项的。proc_mkdir()通过调用函数proc_create()对这个新的profs项进行设置,而proc_create()会调用函数kmalloc()来请求分配内存–函数kmalloc()是可以睡眠的。

Q:如何释放中断处理程序?
A:
卸载驱动程序时,需要注销相应的中断处理程序,并释放中断线。可调用如下方法实现:

void free_irq(unsigned int irq, void *dev);

ps:如果指定的中断线不是共享的,那么该函数删除处理程序的同时将禁用这条中断线。
如果中断线是共享的,则仅删除dev所对应的处理程序,则这条中断线本身只有在删除了最后一个处理程序才会被禁用。

因此,对于共享的中断线,唯一的dev是很重要的。

对于共享的中断线,需要一个唯一的信息来区分其上面的多个处理程序,并让free_irq()仅仅删除指定的处理程序。

不管在中断线共享不共享,如果dev非空,它都必须与需要删除的处理程序相匹配,必须从进程上下文中调用free_irq()。
中断注册方法表如下:

函数 描述
request_irq() 在给定的中断线上注册一给定的中断处理程序
free_irq() 如果在给定的中断线上没有中断处理程序,则注销相应的处理程序,并禁用其中断线

5.编写中断处理程序

中断处理程序的声明如下:

static irqreturn_t intr_handler(int irq, void *dev);

该函数的类型与request_irq()参数中的handler所要求的参数类型相匹配。
第一个参数irq就使这个处理程序要响应的中断的中断号。
ps:这个irq参数在没有dev这个参数的时候比较重要,在2.0以后的版本,irq这个参数没有太大的用处,一般打印日志信息时会打印irq号。
第二个参数dev是一个通用指针,与中断处理程序注册时传递给request_irq()的参数dev必须一致。 如果该值由唯一确定性(为了能支持共享),那么它就相当于一个cookie,可以用来区分共享同一中断处理程序的多个设备。

ps:对于每个设备而言,设备结构都是唯一的,可能在中断处理程序中用到。

返回值是一个特殊类型:irqreturn_t。
ps:中断处理程序通常会标记为static,它从来不会被别的文件中的代码直接调用。
中断处理程序可能返回两个特殊的值:IRQ_NONE和IRQ_HANDLED。

当中断处理程序检测到一个中断,但该中断对应的设备并不是在注册处理函数期间指定的产生源时返I回IRQ_NONE;
当中断处理程序被正确调用,且确实是它锁对应的设备产生了中断时,返回IRQ_HANDLED。

ps:
可以使用宏IRQ_RETVLA(val)返回返回值。
即,如果val未非0值返回IRQ_HANDLED;否则,返回IRQ_NONE。

重入和中断处理程序:
Linux中的中断处理程序是无须重入的。
当一个给定的中断处理程序正在执行时,相应的中断线在所有处理器上都会被屏蔽掉,以防止在同一中断上接收另一个新的中断。
通常情况下,所有其他的中断都是打开的,所以这些不同中断线上的其他中断都能被处理,当当前中断线总是被禁止的。

5.1 共享的中断处理程序

中断处理程序的共享与否在注册和运行方式上比较相似,但差异有如下几点,所有共享中断线的驱动程序也都必须满足以下要求:

  1. request_irq()的参数flags必须设置IRQF_SHARED标志
  2. 对于每个注册的中断处理程序来说,dev参数必须唯一。指向任意设备结构的指针就可以满足这一要求:通常会选择设备结构,因为它是唯一的,而且中断处理程序可能会用到。不能给共享的处理程序传递NULL值;
  3. 中断处理程序必须能够区分它的设备是否真的产生了中断,也就要求中断处理程序必须知道是与它对于的设备发出了中断,还是共享这条中断线的其他设备发出了这个中断,这既需要硬件的支持,也需要处理程序中有相关的逻辑处理。

指定IRQF_SHARED标志以调用request_irq()时,只有在以下两种情况才可能成功:中断线当前未被注册,或者在该线上的所有已注册处理程序都指定了IRQF_SHARED。

ps:
内核接收一个中断后,它将依次调用在该中断线上注册的每一个处理程序。因此,一个处理程序必须知道它是否应该未这个中断负责。如果与它相关的设备并没有产生中断,那么处理程序应该立即退出。这需要硬件设备提供状态寄存器(或类似机制),以便中断处理程序进行检测。

5.2 中断处理程序实例-rtc驱动程序

看完该实例–实例在rtc驱动代码的中断函数里,或者该书的7.5.2章节,有如下疑问:
共享中断线就不共享中断处理程序,共享中断处理程序就不共享中断线。 可以这样理解?
希望大佬评论区留言。

6.中断上下文

当执行一个中断处理程序时,内核处于中断上下文(interrupt context)中。
进程上下文是一种内核所处的操作模式,此时内核代表进程执行。在进程上下文中,可以通过current宏关联当前进程(java 的Context类)。
因为进程是以进程上下文的形式连接到上下文的形式连接到内核中的,因此,进程上下文可以睡眠,也可以调用调度程序(中断上下文不能睡眠,可参考这篇博文为什么中断不能休眠)。

因为中断处理程序打断了其他的代码(甚至可能是打断了在其他中断线上的另一中断处理程序),所以所有的中断处理程序必须尽可能的迅速、简介。尽量把工作从中断处理程序中分离出来,放在下半部来执行,因为下半部可以在更合适的时间运行。

在2.6以后的内核版本,内核栈和中断栈分别独立。
Q:何为中断栈?
A:为了应对栈大小的减少,中断处理程序拥有了自己的栈(以前共享的内核栈),每个处理器一个,大小为一页。这个栈就称为中断栈

7.中断处理机制的实现

中断处理系统在Linux中的实现非常依赖于体系结构,因为实现依赖于处理器、所使用的中断控制器的类型、体系结构的设计及机器本身。

下图展示了中断从硬件到内核的路由:
在这里插入图片描述
设备产生中断,通过总线把电信号发送给中断控制器。如果中断线是激活的(它们是允许被屏蔽的),那么中断控制器就会把中断发往处理器。
在大多数体系结构中,这个工作是通过电信号给处理器的特定管脚发送一个信号。除非在处理器上禁止该中断,否则,处理器就会立即停止它正在做的事,关闭中断系统,然后跳到内存中预定义的位置开始执行那里的代码。这个预定义的位置是由内核设置的,是中断处理程序的入口点。

Q:硬件中断始于硬件产生中断到中断线到处理器,那么处理器(内核)的中断以及处理是如何?
A:在内核中,中断的旅程开始于预定义入口点,类似于系统调用通过预定义的异常句柄进入内核。对于每条中断线,处理器都会跳到对应的一个唯一的位置。这样,内核就可以知道所接收中断的IRQ号了。
初始入口点只是在栈中保存这个号,并存放当前寄存器的值(这些值属于被中断的任务);然后,啮合开始调用函数do_IRQ()。
从这里开始,大部分中断处理代码是用C编写的–但他们依然与体系结构相关。
do_IRQ()声明如下:

unsigned int do_IRQ(struct pt_regs regs);

C的调用惯例是要把函数参数放在栈的顶部,因此,pt_regs结构包含原始寄存器的值,这些值是以前在汇编入口例程中保存在栈中的。中断的值也会得以保存,而do_IRQ()可以将它提取出来。

得到中断号后,do_IRQ()对所接收的中断进行应答,禁止这条线上的中断传递。然后,do_IRQ()需要确保在这条中断线上有一个有效的处理程序(中断处理程序),而且这个程序已经启动,但目前并没有执行。如果是这样,do_IRQ()就调用handle_IRQ_event()来运行为这条中断线所安装的中断处理程序。handle_IRQ_event()方法定义在文件kernel/irq/handler.c中。
从handle_IRQ_event()回到do_IRQ(),该函数做清理工作并返回到初始入口点,然后再从这个入口点跳到函数ret_from_intr()。
ret_from_intr()方法类似于初始入口代码,以汇编语言编写。该方法检查重新调度是否正在挂起(做一些从中断上下文出来后关于进程恢复或者调度的事情)

ps:handle_IRQ_event该方法的具体实现以及处理,可参考书中7.7章节。

8./proc/interrupts

procfs是一个虚拟文件系统,它只存在与内核内存,一般安装于/proc目录。
在procfs中读写文件都要调用内核函数,这些函数模拟从真是文件中读写。

/proc/interrupts文件存放的是系统中与中断相关的统计信息。下图是多处理器(SMP)上输出信息:
在这里插入图片描述
第一列是中断线(中断号)
第二列是CPU0(处理器1)一个接收中断数目的计数器。
倒数第二列是处理这个中断的中断控制器。
最后一列是与这个中断相关的设备名字,这个名字是通过参数devname提供给函数request_irq()的。
如果中断是共享的,则这条中断线上注册的所有设备都会列出来。

ps:
procfs代码位于fs/proc中。提供/proc/interrupts的函数是与体系结构相关的,叫做show_interrupts()。

9.中断控制

Linux内核提供了一组接口用于操作机器上的中断状态。这些接口提供了能够禁止当前处理器的中断系统,或屏蔽掉整个机器的一条中断线的能力,这些例程都是与体系结构相关的,可以在<asm/system.h>和<asm/irq.h>中找到。

一般来说,控制中断系统的原因归根结底是需要提供同步。通过禁止中断,可以确保某个中断处理程序不会抢占当前的代码。此外,禁止中断还以禁止内核抢占。

ps:
禁止中断提供保护机制,防止来自其他中断处理程序的并发访问,而没有防止来自其他处理器的并发访问。因此需要获取某种锁,锁提供保护机制,保护多处理器的并发。

9.1 禁止和激活中断

用于禁止当前处理器上的本地中断,随后又激活的语句为:

local_irq_disable();
local_irq_enable();

这两个函数通常以单个汇编指令来实现(依赖体系结构)。

ps:
在禁止中断之前保存中断系统的状态会更加安全。相反,在准备激活中断时,只需把中断恢复到它们原来的状态。
eg:

unsigned long flags;

local irq_save(flags);	/* 禁止中断 */
/* ...... */
local_irq_restore(flags);	/* 中断被恢复到它们原来的状态 */

local_irq_save()和local_irq_restore()的调用必须在用一个函数中进行。
why?
A:这些方法至少部分要以宏的形式实现,因此表面上flags参数(这些参数必须定义为unsigned long类型)是以值传递的。该参数包含具体体系结构的数据,也就是包含中断系统的状态。至少有一种体系结构把栈信息与值相结合(SPARC),因此flags不能传递给另一个函数(特别是它必须驻留在同一栈帧中)

9.2 禁止指定中断线

Linux提供了四个接口用来屏蔽掉一条中断线

void disable_irq(unsigned int irq);
void disable_irq_nosync(unsigned int irq);
void enable_irq(unsigned int irq);
void synchronize_irq(unsigned int irq);

前两个函数禁止中断控制器上指定的中断线,即禁止给定中断向系统中所有处理器的传递。
函数只有在当前正在执行的所有处理程序完成后,disable_irq()才能返回。因此,调用者不仅要确保不在指定线上传递新的中断,同时还要确保所有已经开始执行的处理程序已全部退出。
函数disable_irq_nosync()不会等待当前中断处理程序执行完毕。

函数synchronize_riq()等待一个特定的中断处理程序的退出。如果该处理程序正在执行,那么该函数必须退出后才能返回。

ps:
1.上述函数可以嵌套调用;
2.disable_irq和disable_irq_nosync的调用必须和enable成对配套使用才能保证真正的激活中端线。
3.禁止多个中断处理程序共享的中断线是不合适的。禁止中断线就禁止了这条线上的所有设备的中断传递。

9.3 中断系统的状态

Q:如何了解中断系统的状态,或者是否正处于中断上下文的执行状态中?
A:宏irqs_disable()定义在<asm/system.h>中,如果本地处理器上中断系统被禁止,则返回非0,否则返回0。
在<linux/hardirq.h>中定义的两个宏提供一个用来检查内核的当前上下文的接口,如下:

in_interrupt()
in_irq()

in_interrupt宏:如果内核处于任何类型的中断处理中,返回0。 说明内核此刻正在执行中断处理程序,或者正在执行下半部处理程序。
in_irq宏只有在内核确实在正在执行中断处理程序时才返回非0。

中断控制方法列表如下:

函数 说明
local_irq_disable() 禁止本地中断传递
local_irq_enable() 激活本地中断传递
local_irq_save() 保存本地中断传递的当前状态,然后禁止本地中断传递
locar_irq_restore() 恢复本地中断传递到给定的状态
disable_irq() 禁止给定中断线,并确保该函数返回之前在该中断线上没有处理程序在运行
disable_irq_nosync() 禁止给定中断线
enable_irq() 激活给定中断线
irqs_disabled() 如果本地中断传递被禁止,则返回非0;否则返回0
in_interrupt() 如果在中断上下文中,则返回0;如果在进程上下文中,则返回0
in_irq() 如果当前正在执行中断处理程序,则返回非0,否则返回0
发布了91 篇原创文章 · 获赞 17 · 访问量 5万+

猜你喜欢

转载自blog.csdn.net/qq_23327993/article/details/105434469
今日推荐