Linux学习第23节,内核中的“中断”机制

前面几节,我们主要分析和讨论了 Linux 内核中常用的几种数据结构(链表、队列、映射、二叉树、红黑树)以及相关C语言代码实现,这是研究 Linux 内核其他内容的基础,本节将介绍一下内核中的“中断”机制。

中断的基本概念

现在来设想一个简单,但是又很常见的场景:当用户按下键盘时,Linux 内核是如何响应的呢?如果采取始终监测键盘的方法,效率就太低了,因为用户可能几分钟,甚至几小时都不会按下键盘。就算用户正在使用键盘写文章,手指的速度相对于 cpu 的速度也差了好几个数量级。

cpu 每秒运算次数以亿计,但是用户能一秒按键盘 100 次吗?

那么内核该如何响应键盘的输入,而又同时不会降低机器的整体性能呢?如果键盘在被敲击的时候,能够主动向 Linux 内核发送信号,就好办了。这样 Linux 内核就无需关心键盘,安心做自己的事就可以了,反正若是有人按下键盘,键盘会主动通知内核的。这其实就是中断机制

中断本质上是一种电信号,由硬件设备(如键盘鼠标)产生,并直接送到中断控制器的输入口,经复用处理后,中断控制器会向 cpu 发送一个电信号。cpu 接收到该信号后,会中断自己当前的工作,并通知 Linux 内核,这时内核就能做出适当的处理了(例如处理键盘输入的字符)。

现代计算机一般都较为复杂,硬件外设也非常多,不仅仅有键盘,还有声卡,显卡,硬盘等。这么多硬件设备都产生中断的话,Linux 内核怎么知道某个中断是谁产生的呢?所以,内核为每个中断都设定了一个唯一的数字标志(中断号)。这么一来,有中断产生时,只需对比中断号就能知道是哪个硬件设备产生的了。

这里再提一下“异常”的概念。中断是由硬件主动产生的,它才不会管 cpu,随时可能发生。而异常则是由 cpu 主动产生的,一般是遇到无法处理,必须求助内核的情况时才会产生,例如某个数被 0 除。

Linux 内核中的中断处理程序

上面我们提到,Linux 内核为了分辨中断是哪个硬件设备产生的,为每个中断都定义了独一无二的中断号,其实,内核还为每个中断都定义了中断处理程序。中断处理程序运行在中断上下文中,而不是进程上下文中,这就要求中断处理程序应尽可能简洁,以便能快速完成工作,尽快恢复中段代码的执行。

但有时需求是矛盾的,即:想中断处理程序运行的快,又想中断处理程序干得活多。例如网络设备的中断处理程序,它需要对硬件做出应答,把网络数据从硬件设备拷贝到内存,还要把数据送往合适的协议栈分析,这样的工作量显然不小,但是又要求系统快速做出响应的同时,不影响其他设备。

针对这种情况,Linux 内核将中断处理程序分为两部分(上半部,下半部),上半部只做必须快速完成的工作,例如对硬件设备做出应答,下半部则负责完成余下的较为耗时的工作。上半部的工作一般都是“现在立刻马上做”,而下半部的工作则可以暂缓到 cpu 没那么忙的时候再做。

现在知道了 Linux 内核关于中断的总体设计了,来看看具体的 C语言代码是怎样实现的吧。

注册中断处理程序

每一个中断都对应一个中断处理程序,那么这个“对应”关系 Linux 内核是如何实现的呢?上面我们提到,每个中断都有独一无二的中断号,所以将中断处理程序与中断号对应起来可以了。因此,Linux 内核定义了 request_irq() 函数用于注册中断处理程序,它的C语言代码如下,请看:

 536 int request_irq(unsigned int irq, irq_handler_t handler,
    537         unsigned long irqflags, const char *devname, void *dev_id)
-   538 {
|   539     struct irqaction *action;
|   540     int retval;    
     ...
||  586         local_irq_save(flags);
||  587         handler(irq, dev_id);
||  588         local_irq_restore(flags);
||  589     }
|   590 #endif
|   591 
|   592     retval = setup_irq(irq, action);
|   593     if (retval)
|   594         kfree(action);
|   595 
|   596     return retval;
|   597 }


其中 irq 是分配给中断的中断号,这个值有的设备是预先设定好的,有些则可以通过C语言编程动态获取。handler 就是指向中断处理函数的指针了,它的原型如下:

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

irqflags 是标志位,常用的主要有以下几个:

  • IRQF_DISABLED,使用该标志位注册的中断处理程序运行时,会禁止其他中断。
  • IRQF_SAMPLE_RANDOM,Linux 内核维护一个熵池,用于实现随机功能,该标志位表明中断会对熵池有贡献。
  • IRQF_SHARED,每个中断线都可以有不止一个中断处理程序,此标志位则决定是否使用多个中断程序复用一条中断线。

request_irq() 函数的 devname 参数则是用于描述中断名字的,dev_id 在使用了 IRQF_SHARED 标志位时非常有用,它是区分共享一个中断线的中断处理程序们的重要参数,这一点从 request_irq() 函数的C语言源代码可以看出:

|   554     if ((irqflags & IRQF_SHARED) && !dev_id)
|   555         return -EINVAL;
|   556     if (irq >= NR_IRQS)
|   557         return -EINVAL;
|   558     if (irq_desc[irq].status & IRQ_NOREQUEST)
|   559         return -EINVAL;
|   560     if (!handler)
|   561         return -EINVAL;

如果没有使用 IRQF_SHARED 位,dev_id 参数可以赋值为 NULL。

request_irq() 函数的其他部分就比较简单了,它的主要工作就是保存传递给它的参数,请看如下C语言代码:

|   563     action = kmalloc(sizeof(struct irqaction), GFP_ATOMIC);
|   564     if (!action)
|   565         return -ENOMEM;
|   566 
|   567     action->handler = handler;
|   568     action->flags = irqflags;
|   569     cpus_clear(action->mask);
|   570     action->name = devname;
|   571     action->next = NULL;
|   572     action->dev_id = dev_id;


最后再将 action 传递给 setup_irq() 函数,我们继续跟踪,分析它的C语言源代码,容易看出它的功能也是非常简单的:

这一步清楚的说明了 IRQF_SAMPLE_RANDOM 的作用。 setup_irq() 函数的其他主要工作就是检查标志位,设置标志位,如果一切符合要求,就把中断处理函数挂到 irq 队列的最后:

最后,Linux 内核会将相关信息注册,请看如下C语言代码:

new->irq = irq;
register_irq_proc(irq);
new->dir = NULL;
register_handler_proc(irq, new);

这两个函数执行后,我们可以在 Linux 系统中的 /proc 目录下看到

/proc/irq/1234...
/proc/irq/1234.../handler/...

这里面就记载着中断的相关信息。现在知道了 Linux 内核是如何注册中断处理程序的了,再来看看内核是如何执行中断处理程序的。

Linux 内核是如何执行中断处理程序的

内核中大多中断处理程序都是在 do_IRQ() 中完成执行的,它的 C语言源代码如下:

     69 unsigned int do_IRQ(struct pt_regs *regs)
-    70 {   
|    71     struct pt_regs *old_regs;
|    72     /* high bit used in ret_from_ code */
|    73     int irq = ~regs->orig_ax;
|    74     struct irq_desc *desc = irq_desc + irq;
...
|   143     irq_exit();
|   144     set_irq_regs(old_regs);
|   145     return 1;
|   146 }


能够看出,do_IRQ() 函数是从全局变量 irq_desc 获取中断信息的,irq_desc 的内容则来自 request_irq() 函数。这就非常清楚了,Linux 内核通过 request_irq() 函数注册中断处理程序,并将相关信息保存在 irq_desc,do_IRQ() 函数则负责从 irq_desc 中获取中断处理程序,在合适的时候执行。

从上图中的C语言代码可以看出,do_IRQ() 函数首先把当前的寄存器信息保存下来,然后检查栈空间是否够用。然后确保进入了中断栈,否则就手动新建一个栈帧供中断处理程序使用。Linux 内核执行中断处理程序是通过在注册中断处理程序时传递给 irq_desc 结构体成员 handle_irq 的函数指针实现的。这一过程可以从下图C语言代码看出。

在 do_IRQ() 函数的最后,内核恢复了之前保存的寄存器信息,以便其他程序恢复运行。

Linux 内核是如何释放中断处理程序的

既然有注册,就应该有释放。当 Linux 卸载驱动程序时,需要注销相应的中断处理程序,并释放中断线,这一过程由 free_irq() 函数完成,它的C语言代码原型如下:

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

free_irq() 函数会根据 dev_id 遍历整个 irq_desc,查找到要释放的中断处理程序后,就将其释放,并且设置 IRQ_DISABLED 标志位。

不过对于以 IRQF_SHARED 标志位注册的中断处理程序,还要特殊处理一下:

if (action->flags & IRQF_SHARED) {
     local_irq_save(flags);
     action->handler(irq, dev_id);
    local_irq_restore(flags);
}

至此,Linux 内核是如何注册中断处理程序,又是如何执行如何释放的,相信大家已经比较清楚了。

阅读更多:   Linux笔记
添加新评论

icon_redface.gificon_idea.gificon_cool.gif2016kuk.gificon_mrgreen.gif2016shuai.gif2016tp.gif2016db.gif2016ch.gificon_razz.gif2016zj.gificon_sad.gificon_cry.gif2016zhh.gificon_question.gif2016jk.gif2016bs.gificon_lol.gif2016qiao.gificon_surprised.gif2016fendou.gif2016ll.gif