前面几节,我们主要分析和讨论了 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 内核是如何注册中断处理程序,又是如何执行如何释放的,相信大家已经比较清楚了。