文章目錄
轉載地址(本文也是轉載):http://blog.chinaunix.net/uid-15443744-id-2772596.html
每台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 <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中斷世界。
更詳細的內容和其它一些注意事項請參考核心原始碼包中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
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_fre
q
+ 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——最簡單的改動
我們來更進一步感受中斷,非常簡單,我們要在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跑得相當好。
當然,這可能不是一種美觀和標準的做法,但是,我只是一名程式員而不是藝術家,只是瞭解了這麼一點點中斷知識,我就完成了工作,我想或許您也希望從系統底層的秘密中獲得收益吧,讓我們在以後的文章中再見。
[1]那麼PowerOff(關機)算不算中斷呢?如果從字面上講,肯定符合漢語對中斷的定義,但是從訊號格式、處理方法等方面來看,就很難符合我們的理解了。Intel怎麼說的呢?該中斷沒有採用通用的中斷處理機制。那麼到底是不時中斷呢?我也說不上來:(
[2]之所以這裡使用彙編而不是C來實現這些函數,是因為C編譯器會在函數的實現中推入額外的棧資訊。而CPU在中斷來臨時儲存和恢複現場都按照嚴格的格式進行,一個位元組的變化都不能有。