中断絮说(三)-从外而内,Linux对中断的支持
在Linux中,中断处理程序看起来就是普普通通的C函数。只不过这些函数必须按照特定的类型声明,以便内核能够以标准的方式传递处理程序的信息,在其他方面,它们与一般的函数看起来别无二致。中断处理程序与其它内核函数的真正区别在于,中断处理程序是被内核调用来响应中断的,而它们运行于我们称之为中断上下文的特殊上下文中。关于中断上下文,我们将在后面讨论。
中断可能随时发生,因此中断处理程序也就随时可能执行。所以必须保证中断处理程序能够快速执行,这样才能保证尽可能快地恢复被中断代码的执行。因此,尽管对硬件而言,迅速对其中断进行服务非常重要。但对系统的其它部分而言,让中断处理程序在尽可能短的时间内完成执行也同样重要。
即使最精简版的中断服务程序,它也要与硬件进行交互,告诉该设备中断已被接收。但通常我们不能像这样给中断服务程序随意减负,相反,我们要靠它完成大量的其它工作。作为一个例子,我们可以考虑一下网络设备的中断处理程序面临的挑战。该处理程序除了要对硬件应答,还要把来自硬件的网络数据包拷贝到内存,对其进行处理后再交给合适的协议栈或应用程序。显而易见,这种运动量不会太小。
现在我们来分析一下Linux操作系统为了支持中断机制,具体都需要做些什么工作。
首先,操作系统必须保证新的中断能够被支持。计算机系统硬件留给外设的是一个统一的中断信号接口。它固化了中断信号的接入和传递方法,拿PC机来说,中断机制是靠两块8259和CPU协作实现的。外设要做的只是把中断信号发送到8259的某个特定引脚上,这样8259就会为此中断分配一个标识——也就是通常所说的中断向量,通过中断向量,CPU就能够在以中断向量为索引的表——中断向量表——里找到中断服务程序,由它决定具体如何处理中断。(具体细节还请查阅参考资料1,对于为何采用这种机制,该资料有精彩描述)这是硬件规定的机制,软件只能无条件服从。
因此,操作系统对新中断的支持,说简单点,就是维护中断向量表。新的外围设备加入系统,首先得明确自己的中断向量号是多少,还得提供自身中断的服务程序,然后利用Linux的内核调用界面,把〈中断向量号、中断服务程序〉这对信息填写到中断向量表中去。这样CPU在接收到中断信号时就会自动调用中断服务程序了。这种注册操作一般是由设备驱动程序完成的。
其次,操作系统必须提供给程序员简单可靠的编程界面来支持中断。中断的基本流程前面已经讲了,它会打断当前正在进行的工作去执行中断服务程序,然后再回到先前的任务继续执行。这中间有大量需要解决问题:如何保护现场、嵌套中断如何处理等等,操作系统要一一化解。程序员,即使是驱动程序的开发人员,在写中断服务程序的时候也很少需要对被打断的进程心存怜悯。(当然,出于提高系统效率的考虑,编写驱动程序要比编写用户级程序多一些条条框框,谁让我们顶着系统程序员的光环呢?)
操作系统为我们屏蔽了这些与中断相关硬件机制打交道的细节,提供了一套精简的接口,让我们用极为简单的方式实现对实际中断的支持,Linux是怎么完美的做到这一点的呢?
CPU对中断处理的流程:
我们首先必须了解CPU在接收到中断信号时会做什么。没办法,操作系统必须了解硬件的机制,不配合硬件就寸步难行。现在我们假定内核已被初始化,CPU在保护模式下运行。
CPU执行完一条指令后,下一条指令的逻辑地址存放在cs和eip这对寄存器中。在执行新指令前,控制单元会检查在执行前一条指令的过程中是否有中断或异常发生。如果有,控制单元就会抛下指令,进入下面的流程:
1.确定与中断或异常关联的向量i (0~255)。
2.籍由idtr寄存器从IDT表中读取第i项(在下面的描述中,我们假定该IDT表项中包含的是一个中断门或一个陷阱门)。
3.从gdtr寄存器获得GDT的基地址,并在GDT表中查找,以读取IDT表项中的选择符所标识的段描述符。这个描述符指定中断或异常处理程序所在段的基地址。
4.确信中断是由授权的(中断)发生源发出的。首先将当前特权级CPL(存放在cs寄存器的低两位)与段描述符(即DPL,存放在GDT中)的描述符特权级比较,如果CPL小于DPL,就产生一个“通用保护”异常,因为中断处理程序的特权不能低于引起中断的程序的特权。对于编程异常,则做进一步的安全检查:比较CPL与处于IDT中的门描述符的DPL,如果DPL小于CPL,就产生一个“通用保护”异常。这最后一个检查可以避免用户应用程序访问特殊的陷阱门或中断门。
5.检查是否发生了特权级的变化,也就是说, CPL是否不同于所选择的段描述符的DPL。如果是,控制单元必须开始使用与新的特权级相关的栈。通过执行以下步骤来做到这点:
a.读tr寄存器,以访问运行进程的TSS段。
b.用与新特权级相关的栈段和栈指针的正确值装载ss和esp寄存器。这些值可以在TSS中找到(参见第三章的“任务状态段”一节)。
c.在新的栈中保存ss和esp以前的值,这些值定义了与旧特权级相关的栈的逻辑地址。
6.如果故障已发生,用引起异常的指令地址装载cs和eip寄存器,从而使得这条指令能再次被执行。
7.在栈中保存eflag、cs及eip的内容。
8.如果异常产生了一个硬错误码,则将它保存在栈中。
9.装载cs和eip寄存器,其值分别是IDT表中第i项门描述符的段选择符和偏移量域。这些值给出了中断或者异常处理程序的第一条指令的逻辑地址。
控制单元所执行的最后一步就是跳转到中断或者异常处理程序。换句话说,处理完中断信号后,控制单元所执行的指令就是被选中的处理程序的第一条指令。
中断或异常被处理完后,相应的处理程序必须产生一条iret指令,把控制权转交给被中断的进程,这将迫使控制单元:
1.用保存在栈中的值装载cs、eip、或eflag寄存器。如果一个硬错误码曾被压入栈中,并且在eip内容的上面,那么,执行iret指令前必须先弹出这个硬错误码。
2.检查处理程序的CPL是否等于cs中最低两位的值(这意味着被中断的进程与处理程序运行在同一特权级)。如果是,iret终止执行;否则,转入下一步。
3. 从栈中装载ss和esp寄存器,因此,返回到与旧特权级相关的栈。
4. 检查ds、es、fs及gs段寄存器的内容,如果其中一个寄存器包含的选择符是一个段描述符,并且其DPL值小于CPL,那么,清相应的段寄存器。控制单元这么做是为了禁止用户态的程序(CPL=3)利用内核以前所用的段寄存器(DPL=0)。如果不清这些寄存器,怀有恶意的用户程序就可能利用它们来访问内核地址空间。
再次,操作系统必须保证中断信息能够高效可靠的传递
中断絮说(四)-从RTC设备学习中断
从RTC设备学习中断
系统实时钟
每台PC机都有一个实时钟(Real Time Clock)设备。在你关闭计算机电源的时候,由它维持系统的日期和时间信息。
此外,它还可以用来产生周期信号,频率变化范围从2Hz到8192Hz——当然,频率必须是2的倍数。这样该设备就能被当作一个定时器使用,比如我们把频率设定为4Hz,那么设备启动后,系统实时钟每秒就会向CPU发送4次定时信号——通过8号中断提交给系统(标准PC机的IRQ 8是如此设定的)。由于系统实时钟是可编程控制的,你也可以把它设成一个警报器,在某个特定的时刻拉响警报——向系统发送IRQ 8中断信号。由此看来,IRQ 8与生活中的闹铃差不多:中断信号代表着报警器或定时器的发作。
在Linux操作系统的实现里,上述中断信号可以通过/dev/rtc(主设备号10,从设备号135,只读字符设备)设备获得。对该设备执行读(read)操作,会得到unsigned long型的返回值,最低的一个字节表明中断的类型(更新完毕update-done,定时到达alarm-rang,周期信号periodic);其余字节包含上次读操作以来中断到来的次数。如果系统支持/proc文件系统,/proc/driver/rtc中也能反映相同的状态信息。
该设备只能由每个进程独占,也就是说,在一个进程打开(open)设备后,在它没有释放前,不允许其它进程再打开它。这样,用户的程序就可以通过对/dev/rtc执行read()或select()系统调用来监控这个中断——用户进程会被阻塞,直到系统接收到下一个中断信号。对于一些高速数据采集程序来说,这个功能非常有用,程序无需死守着反复查询,耗尽所有的CPU资源;只要做好设定,以一定频率进行查询就可以了。更详细的内容和其它一些注意事项请参考内核源代码包中Documentations/rtc.txt
#include <stdio.h>
#include <linux/rtc.h>
#include <sys/ioctl.h>
#include <sys/time.h>
#include <sys/types.h>
#include <fcntl.h>
#include <unistd.h>
#include <errno.h>
int main(void)
{
int i, fd, retval, irqcount = 0;
unsigned long tmp, data;
struct rtc_time rtc_tm;
// 打开RTC设备
fd = open ("/dev/rtc", O_RDONLY);
if (fd == -1) {
perror("/dev/rtc");
exit(errno);
}
fprintf(stderr, "\n\t\t\tEnjoy TV while boiling water.\n\n");
// 首先是一个报警器的例子,设定10分钟后"响铃"
// 获取RTC中保存的当前日期时间信息
/* Read the RTC time/date */
retval = ioctl(fd, RTC_RD_TIME, &rtc_tm);
if (retval == -1) {
perror("ioctl");
exit(errno);
}
fprintf(stderr, "\n\nCurrent RTC date/time is %d-%d-%d,%02d:
%02d:%02d.\n",
rtc_tm.tm_mday, rtc_tm.tm_mon + 1, rtc_tm.tm_year + 1900,
rtc_tm.tm_hour, rtc_tm.tm_min, rtc_tm.tm_sec);
// 设定时间的时候要避免溢出
rtc_tm.tm_min += 10;
if (rtc_tm.tm_sec >= 60) {
rtc_tm.tm_sec %= 60;
rtc_tm.tm_min++;
}
if (rtc_tm.tm_min == 60) {
rtc_tm.tm_min = 0;
rtc_tm.tm_hour++;
}
if (rtc_tm.tm_hour == 24)
rtc_tm.tm_hour = 0;
// 实际的设定工作
retval = ioctl(fd, RTC_ALM_SET, &rtc_tm);
if (retval == -1) {
perror("ioctl");
exit(errno);
}
// 检查一下,看看是否设定成功
/* Read the current alarm settings */
retval = ioctl(fd, RTC_ALM_READ, &rtc_tm);
if (retval == -1) {
perror("ioctl");
exit(errno);
}
fprintf(stderr, "Alarm time now set to %02d:%02d:%02d.\n",
rtc_tm.tm_hour, rtc_tm.tm_min, rtc_tm.tm_sec);
// 光设定还不成,还要启用alarm类型的中断才行
/* Enable alarm interrupts */
retval = ioctl(fd, RTC_AIE_ON, 0);
if (retval == -1) {
perror("ioctl");
exit(errno);
}
// 现在程序可以耐心的休眠了,10分钟后中断到来的时候它就会被唤醒
/* This blocks until the alarm ring causes an interrupt */
retval = read(fd, &data, sizeof(unsigned long));
if (retval == -1) {
perror("read");
exit(errno);
}
irqcount++;
fprintf(stderr, " okay. Alarm rang.\n");
}
这个例子稍微显得有点复杂,用到了open、ioctl、read等诸多系统调用,初看起来让人眼花缭乱。其实如果简化一下的话,过程还是“烧开水”:设定定时器、等待定时器超时、执行相应的操作(“关煤气灶”)。
读者可能不理解的是:这个例子完全没有表现出中断带来的好处啊,在等待10分钟的超时过程中,程序依然什么都不能做,只能休眠啊?
读者需要注意自己的视角,我们所说的中断能够提升并发处理能力,提升的是CPU的并发处理能力。在这里,上面的程序可以被看作是烧开水,在烧开水前,闹铃已经被上好,10分钟后CPU会被中断(闹铃声)惊动,过来执行后续的关煤气工作。也就是说,CPU才是这里唯一具有处理能力的主体,我们在程序中主动利用中断机制来节省CPU的耗费,提高CPU的并发处理能力。这有什么好处呢?试想如果我们还需要CPU烤面包,CPU就有能力完成相应的工作,其它的工作也一样。这其实是在多任务操作系统环境下程序生存的道德基础——“我为人人,人人为我”。
好了,这段程序其实是我们进入Linux中断机制的引子,现在我们就进入Linux中断世界。
RTC中断服务程序
RTC中断服务程序包含在内核源代码树根目录下的driver/char/rtc.c文件中,该文件正是RTC设备的驱动程序——我们曾经提到过,中断服务程序一般由设备驱动程序提供,实现设备中断特有的操作。
SagaLinux中注册中断的步骤在Linux中同样不能少,实际上,两者的原理区别不大,只是Linux由于要解决大量的实际问题(比如SMP的支持、中断的共享等)而采用了更复杂的实现方法。
RTC驱动程序装载时,rtc_init()函数会被调用,对这个驱动程序进行初始化。该函数的一个重要职责就是注册中断处理程序:
if (request_irq(RTC_IRQ,rtc_interrupt,SA_INTERRUPT,”rtc”,NULL)){
printk(KERN_ERR “rtc:cannot register IRQ %d\n”,rtc_irq);
return –EIO;
}
这个request_irq函数显然要比SagaLinux中同名函数复杂很多,光看看参数的个数就知道了。不过头两个参数两者却没有区别,依稀可以推断出:它们的主要功能都是完成中断号与中断服务程序的绑定。
关于Linux提供给系统程序员的、与中断相关的函数,很多书籍都给出了详细描述,如“Linux Kernel Development”。我这里就不做重复劳动了,现在集中注意力在中断服务程序本身上。
static irqreturn_t rtc_interrupt(int irq, void *dev_id,
struct pt_regs *regs)
{
/*
* Can be an alarm interrupt, update complete interrupt,
* or a periodic interrupt. We store the status in the
* low byte and the number of interrupts received since
* the last read in the remainder of rtc_irq_data.
*/
spin_lock (&rtc_lock);
rtc_irq_data += 0x100;
rtc_irq_data &= ~0xff;
rtc_irq_data |= (CMOS_READ(RTC_INTR_FLAGS) & 0xF0);
if (rtc_status & RTC_TIMER_ON)
mod_timer(&rtc_irq_timer,
jiffies + HZ/rtc_freq
+ 2*HZ/100);
spin_unlock (&rtc_lock);
/* Now do the rest of the actions */
spin_lock(&rtc_task_lock);
if (rtc_callback)
rtc_callback->func(rtc_callback->private_data);
spin_unlock(&rtc_task_lock);
wake_up_interruptible(&rtc_wait);
kill_fasync (&rtc_async_queue, SIGIO, POLL_IN);
return IRQ_HANDLED;
}
这里先提醒读者注意一个细节:中断服务程序是static类型的,也就是说,该函数是本地函数,只能在rtc.c文件中调用。这怎么可能呢?根据我们从SagaLinux中得出的经验,中断到来的时候,操作系统的中断核心代码一定会调用此函数的,否则该函数还有什么意义?实际上,request_irq函数会把指向该函数的指针注册到相应的查找表格中(还记得SagaLinux中的irq_handler[]吗?)。static只能保证rtc.c文件以外的代码不能通过函数名字显式地调用函数,而对于指针,它就无法画地为牢了。
程序用到了spin_lock函数,它是Linux提供的自旋锁相关函数,关于自旋锁的详细情况,我们会在以后的文章中详细介绍。你先记住,自旋锁是用来防止SMP结构中的其他CPU并发访问数据的,在这里被保护的数据就是rtc_irq_data。rtc_irq_data存放有关RTC的信息,每次中断时都会更新以反映中断的状态。
接下来,如果设置了RTC周期性定时器,就要通过函数mod_timer()对其更新。定时器是Linux操作系统中非常重要的概念,我们会在以后的文章中详加解释。
代码的最后一部分要通过设置自旋锁进行保护,它会执行一个可能被预先设置好的回调函数。RTC驱动程序允许注册一个回调函数,并在每个RTC中断到来时执行。
wake_up_interruptible是个非常重要的调用,在它执行后,系统会唤醒睡眠的进程,它们等待的RTC中断到来了。这部分内容涉及等待队列,我们也会在以后的文章中详加解释。
最简单的改动
我们来更进一步感受中断,非常简单,我们要在RTC的中断服务程序中加入一条printk语句,打印什么呢?“I’m coming, interrupt!”。
下面,我们把它加进去:
… …
spin_unlock(&rtc_task_lock);
printk(“I’m coming , interrupt!\n”);
wake_up_interruptible(&rtc_wait);
… …
没错,就先做这些,请你找到代码树的drivers\char\rtc.c文件,在其中irqreturn_t rtc_interrupt函数中加入这条printk语句。然后重新编译内核模块(当然,你要在配置内核编译选项时包含RTC,并且以模块形式)现在,当我们插入编译好的rtc.o模块,执行前面实时钟部分介绍的用户空间程序,你就会看到屏幕上打印的“I’m coming , interrupt!”信息了。
这是一次实实在在的中断服务过程,如果我们通过ioctl改变RTC设备的运行方式,设置周期性到来的中断的话,假设我们将频率定位8HZ,你就会发现屏幕上每秒打印8次该信息。
动手修改RTC实际上是对中断理解最直观的一种办法,我建议你不但注意中断服务程序,还可以看一下RTC驱动中ioctl的实现,这样你会更加了解外部设备和驱动程序、中断服务程序之间实际的互动情况。
不仅如此,通过修改RTC驱动程序,我完成了不少稀奇古怪的工作,比如说,在高速数据采集过程中,我就是利用高频率的RTC中断检查高速AD采样板硬件缓冲区使用情况,配合DMA共同完成数据采集工作的。当然,在有非常严格时限要求的情况下,这样不一定适用。但是,在两块12位20兆采样率的AD卡交替工作,对每秒1KHz的雷达视频数据连续采样的情况下,我的RTC跑得相当好。
当然,这可能不是一种美观和标准的做法,但是,我只是一名程序员而不是艺术家,只是了解了这么一点点中断知识,我就完成了工作,我想或许您也希望从系统底层的秘密中获得收益吧,让我们在以后的文章中再见。