閱讀1016 返回首頁    go 阿裏雲 go 技術社區[雲棲]


中斷處理之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

  上一篇:go 數碼管移動
  下一篇:go hdu 1075 What Are You Talking About 字典樹