中斷處理之RTC
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跑得相當好。
最後更新:2017-04-03 18:51:45