Linux中断和中断处理 “上部分”

中断

中断的背景

任何操作系统内核的核心任务,都包含有对连接到计算机上的硬件设备进行有效管理,如硬盘、蓝光碟机、键盘、鼠标、3D处理器,以及无线电等。而想要管理这些设备,首先要能和它们互通音信才行。众所周知,处理器的速度跟外围硬件设备的速度往往不在一个数量级上,因此,如果内核采取让处理器向硬件发出一个请求,然后专门等待回应的办法,显然差强人意。既然硬件的响应这么慢,那么内核就应该在此期间处理其他事务,等到硬件真正完成了请求的操作之后,再回过头来对它进行处理。

我们来提供一种机制,让硬件在需要的时候再向内核发出信号.

中断的内涵

从物理学的角度看,中断是一种电信号,由硬件设备生成,并直接送入中断控制器的输入引脚中--—中断控制器是个简单的电子芯片,其作用是将多路中断管线,采用复用技术只通过一个和处理器相连接的管线与处理器通信。当接收到一个中断后,中断控制器会给处理器发送一个电信号。处理器一经检测到此信号,便中断自己的当前工作转而处理中断。此后,处理器会通知操作系统已经产生中断,这样,操作系统就可以对这个中断进行适当地处理了。

不同的设备对应的中断不同,而每个中断都通过一个唯一的数字标志。因此,来自键盘的中断就有别于来自硬盘的中断,从而使得操作系统能够对中断进行区分,并知道哪个硬件设备产生了哪个中断。这样,操作系统才能给不同的中断提供对应的中断处理程序。

这些中断值通常被称为中断请求(IRQ)线。每个IRQ线都会被关联一个数值量─—---例如,在经典的PC机上,IRQ 0是时钟中断,而IRQ1是键盘中断。但并非所有的中断号都是这样严格定义的。例如,对于连接在PCI总线上的设备而言,中断是动态分配的。而且其他非PC的体系结构也具有动态分配可用中断的特性。重点在于特定的中断总是与特定的设备相关联,并且内核要知道这些信息。实际上,硬件发出中断是为了引起内核的关注:嗨,我有新的按键等待处理呢,读取并处理这些调皮鬼吧!

异常:同步异常(原因是处理器的时钟一致)

之间差异:其差异只在于中断是由硬件而不是软件引起的。

中断处理函数

在响应一个特定中断的时候,内核会执行一个函数,该函数叫做中断处理程序(interrupthandler)或中断服务例程( interrupt service routine,ISR)。产生中断的每个设备都有一个F相应的中断处理程序。例如,由一个函数专门处理来自系统时钟的中断,而另外一个函数专门处理由键盘产生的中断。一个设备的中断处理程序是它设备驱动程序(driver)的一部分--—设备驱动程序是用于对设备进行管理的内核代码。

又想中断处理程序运行得快,又想中断处理程序完成的工作量多,这两个目的显然有所抵触。鉴于两个目的之间存在此消彼长的矛盾关系,所以我们一般把中断处理切为两个部分或两半。中断处理程序是上半部( top half〉——接收到一个中断,它就立即开始执行,但只做有严格时限的工作,例如对接收的中断进行应答或复位硬件,这些工作都是在所有中断被禁止的情况下完成的。能够被允许稍后完成的工作会推迟到下半部(bottom half)去。此后,在合适的时机,下半部会被开中断执行。Linux 提供了实现下半部的各种机制。

应用

让我们考察一下上半部和下半部分割的例子,还是以我们的老朋友——网卡作为实例。当网卡接收来自网络的数据包时,需要通知内核数据包到了。网卡需要立即完成这件事,从而优化网络的吞吐量和传输周期,以避免超时。因此,网卡立即发出中断:嗨,内核,我这里有最新数据包了。内核通过执行网卡已注册的中断处理程序来做出应答。

中断开始执行,通知硬件,拷贝最新的网络数据包到内存,然后读取网卡更多的数据包。这些都是重要、紧迫而又与硬件相关的工作。内核通常需要快速的拷贝网络数据包到系统内存,因为网卡上接收网络数据包的缓存大小固定,而且相比系统内存也要小得多。所以上述拷贝动作一旦被延迟,必然造成缓存溢出-—-进入的网络包占满了网卡的缓存,后续的入包只能被丢弃。当网络数据包被拷贝到系统内存后,中断的任务算是完成了,这时它将控制权交还给系统被中断前原先运行的程序。处理和操作数据包的其他工作在随后的下半部中进行。

注册中断程序

request_irq(unsigned int irq, irq_handler_t handler, unsigned long flags,
            const char *name, void *dev)
{
        return request_threaded_irq(irq, handler, NULL, flags, name, dev);
}

第一个参数irq表示要分配的中断号。对某些设备,如传统PC设备上的系统时钟或键盘,这个值通常是预先确定的。而对于大多数其他设备来说,这个值要么是可以通过探测获取,要么可以通过编程动态确定。

第二个参数handler是一个指针,指向处理这个中断的实际中断处理程序。只要操作系统一接收到中断,该函数就被调用。第二个参数handler是一个指针,指向处理这个中断的实际中断处理程序。只要操作系统一接收到中断,该函数就被调用。

第三个参数flags可以为0,也可能是下列一个或多个标志的位掩码。

flgs
IRQF_DISABLED该标志被设置后,意味着内核在处理中断处理程序本身期间,要禁止所有的其他中断。如果不设置,中断处理程序可以与除本身外的其他任何中断同时运行。多数中断处理程序是不会去设置该位的,因为禁止所有中断是一种野蛮行为。这种用法留给希望快速执行的轻量级中断。这一标志是SA_INTERRUPT标志的当前表现形式,在过去的中断中用以区分“快速”和“慢速”中断。
IRQF_SAMPLE_RANDOM·IRQF_SAMPLE_RANDOM—此标志表明这个设备产生的中断对内核嫡池(entropy pool)有贡献。内核嫡池负责提供从各种随机事件导出的真正的随机数。如果指定了该标志,那么来自该设备的中断间隔时间就会作为墙填充到嫡池。如果你的设备以预知的速率产生中断(如系统定时器),或者可能受外部攻击者(如联网设备)的影响,那么就不要设置这个标志。相反,有其他很多硬件产生中断的速率是不可预知的,所以都能成为一种较好的嫡源。
IRQF_TIMER定时器中断
IRQF_SHARED·IRQF_SHARED——此标志表明可以在多个中断处理程序之间共享中断线。在同一个给员线上注册的每个处理程序必须指定这个标志﹔否则,在每条线上只能有一个处理程序。

第四个参数name是与中断相关的设备的ASCII文本表示。例如,PC机上键盘中断对应的这个值为“keyboard"。

第五个参数dev用于共享中断线。当一个中断处理程序需要释放时(稍后讨论),dev将提供唯一的标志信息(cookie),以便从共享中断线的诸多中断处理程序中删除指定的那一个。如果没有这个参数,那么内核不可能知道在给定的中断线上到底要删除哪一个处理程序。如果无须共享中断线,那么将该参数赋为空值(NULL)就可以了,但是,如果中断线是被共享的,那么就必须传递唯一的信息(除非设备又旧又破且位于ISA总线上,那么就必须支持共享中断)。另外,内核每次调用中断处理程序时,都会把这个指针传递给它°。实践中往往会通过它传递驱动程序的设备结构﹔这个指针是唯一的,而且有可能在中断处理程序内被用到。

注意,request_irq)函数可能会睡眠,因此,不能在中断上下文或其他不允许阻塞的代码中调用该函数。天真地在睡眠不安全的上下文中调用request_irq()函数,是一种常见错误

释放中断程序

void free_irq(unsigned int irq, void*dev)

如果指定的中断线不是共享的,那么,该函数删除处理程序的同时将禁用这条中断线。如果中断线是共享的,则仅删除dev所对应的处理程序,而这条中断线本身只有在删除了最后一个处理程序时才会被禁用。由此可以看出为什么唯一的dev如此重要。对于共享的中断线,需要一个唯一的信息来区分其上面的多个处理程序,并让 free_irq)仅仅删除指定的处理程序。不管在哪种情况下(共享或不共享),如果dev非空,它都必须与需要删除的处理程序相匹配。必须从进程上下文中调用free irq

编写中断程序

static irqreturn intr_handle(int irq,void *dev)

第二个参数dev是一个通用指针,它与在中断处理程序注册时传递给request_irq(的参数dev必须一致。如果该值有唯一确定性(这样做是为了能支持共享),那么它就相当于一个cookie,可以用来区分共享同一中断处理程序的多个设备。另外dev也可能指向中断处理程序使用的一个数据结构。因为对每个设备而言,设备结构都是唯一的,而且可能在中断处理程序中也用得到,因此,它也通常被看做dev。

中断处理程序的返回值是一个特殊类型:irqreturn_t。中断处理程序可能返回两个特殊的值:IRQ_NONE和 IRQ_HANDLED。当中断处理程序检测到一个中断,但该中断对应的设备并不是在注册处理函数期间指定的产生源时,返回IRQ_NONE;当中断处理程序被正确调用,且确实是它所对应的设备产生了中断时,返回IRQ_HANDLED。另外,也可以使用宏IRQ_RETVAL(val)。如果val为非0值,那么该宏返回IRQ_HANDLED;否则,返回IRQ_NONE。利用这些特殊的值,内核可以知道设备发出的是否是一种虚假的(未请求)中断。如果给定中断线上所有中断处理程序返回的都是IRQ_NONE,那么,内核就可以检测到出了问题。注意,irqreturn_t这个返回类型实际上就是一个int型。之所以使用这些特殊值是为了与早期的内核保持兼容——2.6版之前的内核并不支持这种特性,中断处理程序只需返回void就行了。如果要在2.4或更早的内核上使用这样的驱动程序,只需简单地将typedef irqreturn_t改为void,屏蔽掉此特性,并给no-ops定义不同的返回值,其他用不着做什么大的修改。中断处理程序通常会标记为static,因为它从来不会被别的文件中的代码直接调用。

重入和中断处理程序

Linux中的中断处理程序是无须重入的。当一个给定的中断处理程序正在执行时,相应的中断线在所有处理器上都会被屏蔽掉,以防止在同一中断线上接收另一个新的中断。通常情况下,所有其他的中断都是打开的,所以这些不同中断线上的其他中断都能被处理,但当前中断线总是被禁止的。由此可以看出,同一个中断处理程序绝对不会被同时调用以处理嵌套的中断。这极大地简化了中断处理程序的编写。

中断程序实例

让我们考察一个实际的中断处理程序,它来自real-time clock (RTC)驱动程序,可以在drivers/charlrtc.c中找到。很多机器(包括PC)都可以找到RTC。它是一个从系统定时器中独立出来的设备,用于设置系统时钟,提供报警器( alarm)或周期性的定时器。对大多数体系结构而言,系统时钟的设置,通常只需要向某个特定的寄存器或IO地址写入想要的时间就可以了。然而报警器或周期性定时器通常就得靠中断来实现。这种中断与生活中的闹铃差不多,中断发出时,报警器或定时器就会启动。

RTC驱动程序装载时,rtc_init()函数会被调用,对这个驱动程序进行初始化。它的职责之一就是注册中断处理程序:

if(request_irq(rtc_irq,rtc_interrupt,IRQF_SHARED,"rtc",(void*)&rtc_port))
{
  printf(KERN_ERR,"rtc:cannot register IRQ %d\n",rtc_irq);
return -EIO
}

从中我们看到,中断号由rtc_irq指定。这个变量用于为给定体系结构指定RTC中断。例如,在PC上,RTC位于IRQ8。第二个参数是我们的中断处理程序rtc_interrupt—---它将与其他中断处理程序共享中断线,因为它设置了IRQF_SHARED标志。由第四个参数我们看出,驱动程序的名称为“rtc”。因为这个设备允许共享中断线,所以它给dev型参传递了一个面向每个设备的实参值。

static iqreturn_t rtc_interrupt(int irq,void *dev)
{
    /*可以是报警器中断、更新完成的中断或周期性中断我们
    把状态保存在rtc_irq_data的低字节中,
    而把从最后一次读取之后所接收的中断号保存在其余字节中*/
    spin_lock(&rtc_lock);

    rtc_irq_data+=0x1000;
    rtc_irq_data&=~0xff;
    rtc_irq_data |=(CMOs_READ(RTC_INTR_FL.AGS)& 0xFO ) ;
   
    if (rtc_status & RTC_TIMER_ON)
    mod_timer(&rtc_irq_timer,jiffies + H2/rtc_freq + 2*HZ/100);
    spin_unlock (&rtc_lock) ;
    /*现在执行其余的操作*/
    spin_lock (&rtc_task_lock) ;
    if (rtc_callback)
    spin_unlock ( &rtc_task_lock);
    rtc_callback->func(rtc_callback->private_data);
    kill_fasync ( &rtc_async_queue.sIGIo.POLL_IN) ;
    return IRQ_HANDLED;
    

    spin_unlock ( &rtc_task_lock);





}

只要计算机一接收到RTC中断,就会调用这个函数。首先要注意的是使用了自旋锁—---第一次调用是为了保证rtc_irq_data不被SMP机器上的其他处理器同时访问,第二次调用避免rtc_callback出现相同的情况。

rtc_irq_data变量是无符号长整数,存放有关RTC的信息,每次中断时都会更新以反映中断的状态。

中断上下文

当执行一个中断处理程序时,内核处于中断上下文( interrput context)中。让我们先回忆一下进程上下文。进程上下文是一种内核所处的操作模式,此时内核代表进程执行——-例如,执行系统调用或运行内核线程。在进程上下文中,可以通过current宏关联当前进程。此外,因为进程是以进程上下文的形式连接到内核中的,因此,进程上下文可以睡眠,也可以调用调度程序。中断上下文具有较为严格的时间限制,因为它打断了其他代码。中断上下文中的代码应当迅速、简洁,尽量不要使用循环去处理繁重的工作。有-点非常重要,请永远牢记:中断处理程序打断了其他的代码(甚至可能是打断了在其他中断线上的另一中断处理程序)。正是因为这种异步执行的特性,所以所有的中断处理程序必须尽可能的迅速、简洁。尽量把工作从中断处理程序中分离出来,放在下半部来执行,因为下半部可以在更合适的时间运行。

总结

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

在内核中,中断的旅程开始于预定义入口点,这类似于系统调用通过预定义的异常句柄进入内核。对于每条中断线,处理器都会跳到对应的一个唯一的位置。这样,内核就可知道所接收中断的IRQ号了。初始入口点只是在栈中保存这个号,并存放当前寄存器的值(这些值属于被中断的任务);然后,内核调用函数do_IRQ(。从这里开始,大多数中断处理代码是用C编写的——但它们依然与体系结构相关。

ret_from_intr()例程类似于初始入口代码,以汇编语言编写。这个例程检查重新调度是否正在挂起如果重新调度正在挂起,而且内核正在返回用户空间(也就是说,中断了用户进程),那么,schedule()被调用。如果内核正在返回内核空间(也就是说,中断了内核本身),只有在preempt_count为0时,schedule()才会被调用,否则,抢占内核便是不安全的。在schedule()返回之后,或者如果没有挂起的工作,那么,原来的寄存器被恢复,内核恢复到曾经中断的点。

中断控制

禁止和激活中断

local_irq_disable()
local_irq_enable()

如果在调用local_irq_discable()例程之前已经禁止了中断,那么该例程往往会带来潜在的危险﹔同样相应的local_irq_enable()例程也存在潜在危险,因为它将无条件地激活中断,尽管这些中断可能在开始时就是关闭的。所以我们需要一种机制把中断恢复到以前的状态而不是简单地禁止或激活。内核普遍关心这点是因为,内核中一个给定的代码路径既可以在中断激活的情况下达到,也可以在中断禁止的情况下达到,这取决于具体的调用链。例如,想象一下前面的代码片段是一个大函数的组成部分。这个函数被另外两个函数调用:其中一个函数禁止中断,而另一个函数不禁止中断。因为随着内核的不断增长,要想知道到达这个函数的所有代码路径将变得越来越困难,因此,在禁止中断之前保存中断系统的状态会更加安全一些。相反,在准备激活中断时,只需把中断恢复到它们原来的状态。

local_irq_save(flags)//禁止中断

local_irq_restore(flags)

禁止指定中断线

extern void disable_irq_nosync(unsigned int irq);

extern void disable_irq(unsigned int irq);

extern void enable_irq(unsigned int irq);

extern void synchronize(unsigned int irq);

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

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

所有这三个函数可以从中断或进程上下文中调用,而且不会睡眠。但如果从中断上下文中调用,就要特别小心!例如,当你正在处理一条中断线时,并不想激活它(回想当某个处理程序的中断线正在被处理时,它被屏蔽掉)。

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值