实例二——从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资源;只要做好设定,以一定频率进行查询就可以了。
#include
#include
#include
#include
#include
#include
#include
#include
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中断世界。
更详细的内容和其它一些注意事项请参考内核源代码包中Documentations/rtc.txt
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 (int , void *dev_id,
struct *)
{
/*
* 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.
*/
(&);
+= 0x100;
&= ~0xff;
|= (() & 0xF0);
if ( & )
(&,
+ /
q
感受RTC——
那么PowerOff(关机)算不算中断呢?如果从字面上讲,肯定符合汉语对中断的定义,但是从信号格式、处理方法等方面来看,就很难符合我们的理解了。Intel怎么说的呢?该中断没有采用通用的中断处理机制。那么到底是不时中断呢?我也说不上来:(
之所以这里使用汇编而不是C来实现这些函数,是因为C编译器会在函数的实现中推入额外的栈信息。而CPU在中断来临时保存和恢复现场都按照严格的格式进行,一个字节的变化都不能有。