Linux中斷和中斷處理程序


背景

Linux中斷上半部,參見Linux中斷和中斷處理程序
Linux中斷下半部,參見Linux中斷下半部及推后執行的工作

這部分講Linux內核中斷和中斷處理程序。

[======]

中斷

硬件中斷 -- 異步中斷
中斷本質上是一種電信號,由硬件設備發出,用於通知處理器特定事件。
不同設備對應不同中斷,每個中斷通過唯一的數字標識,稱為中斷請求(IRQ)線。處理器內部對其編號,也稱為中斷號。

異常 -- 同步中斷
異常不同於中斷,產生時必須考慮與處理器時鍾同步。異常常稱為同步中斷,是處理器執行時由於編程失誤而導致的錯誤指令(如除0),或者執行期間出現特殊情況(如缺頁),必須靠內核來處理時,處理器就會產生一個異常。
而我們通常所說的是中斷,是指由硬件產生的異步中斷。

由於許多處理器體系結構處理異常與處理中斷的方式類似,因此內核對它們的處理也類似。

軟中斷
工作方式類似於異步中斷,區別是它是通過軟件引起的中斷,異步中斷是由硬件引起的。

[======]

中斷處理程序

響應一個特定中斷時,內核會執行一個函數,稱為中斷處理程序(interrupt handler)或中斷服務例程(interrupt service routine,ISR)。設備產生的每個中斷,都有一個中斷處理程序關聯。一個設備可能產生多種不同的中斷,那么該設備就可以對應多個中斷處理程序。相應地,設備驅動程序就需要准備多個這樣的函數。

Linux中斷處理程序特點
Linux中,中斷處理程序看起來像普通C函數,按特定類型聲明,以便內核能以標准的方式傳遞處理程序的信息。
中斷處理程序與其他內核函數的區別:中斷處理程序是被內核調用來響應中斷的,而它們運行於被稱為中斷上下文的特殊上下文中。

由於中斷隨時可能產生,因此中斷處理程序必須隨時、盡快執行,這樣才能盡快響應中斷,同時恢復中斷代碼的執行。

上下半部的對比
一般把中斷處理切分為2個部分或兩半:
1)上半部(top half)
中斷處理程序是上半部,接收到一個中斷,就立即開始執行,但只做嚴格時限的工作。如對接收中斷進行應答或復位硬件,這些工作都是在所有中斷被禁止的情況下完成的。
2)下半部(bottom half)
中斷處理程序中,能被允許稍后完成的工作會推遲到下半部。Linux提供下半部的各種機制。
本章考察上半部,下一章考察下半部。

[======]

注冊中斷處理程序

如果設備使用中斷(大部分設備這樣做),那么相應的驅動程序就需要注冊一個中斷處理程序。

驅動程序注冊並激活一個中斷處理程序:

/* request_irq: 分配一條給定的中斷線 */
int request_irq(unsigned int irq,  /* 要分配的中斷號 */
                irqreturn_t (*handler)(int, void *, struct pt_regs *), /* 指向實際中斷處理程序, 收到中斷時由內核調用 */
                unsigned long irqflags, /* 標志位 */
                const char* devname, /* 中斷相關的設備ASCII文本表示法 */
                void *dev_id); /* 用於共享中斷線 */

參數說明:

  • irq 表示要分配的中斷號。對某些設備,該值通常預先固定的;對於大部分設備,該值可以探測,或者通過編程動態決定。

  • handler 一個函數指針,指向該中斷的實際中斷處理程序。OS收到中斷,該函數就會被調用。

  • irqflags 可以為0,也可以是下面一個或多個標志的位掩碼:
    1)SA_INTERRUPT 表明給定的中斷處理程序是一個快速中斷處理程序(fast interrupt handler),默認不帶該標志。過去,Linux將中斷處理程序分為快速和慢速兩種。哪些可以迅速執行但調用頻率可能會很高的中斷處理程序會貼上這樣的標簽。這樣做,通常需要修改中斷處理程序的行為,使得它們能盡可能快地執行。現在,加不加此標志的區別只剩一條:在本地處理器上,快速中斷處理程序在禁止所有中斷的情況下運行。好處是可以快速執行,不受其他中斷干擾。默認沒有該標志,除了正運行的中斷處理程序對應那條中斷線,其他所有中斷都是激活的。除了時鍾中斷,絕大多數中斷都不使用該標志。
    2)SA_SAMPLE_RANDOM 表明該設備產生的中斷對內核熵池(entropy pool)有貢獻。內核熵池負責提供從各種隨機事件導出真正的隨機數。如果指定該標志,那么來自該設備的中斷時間間隔就會作為熵填充到熵池。如果你的設備以預知的速率產生中斷(如系統定時器),或可能受到外部攻擊(如網卡)的影響,那么就不要設置該標志。
    3)SA_SHIRQ 表明可以在多個中斷處理程序之間共享中斷線。在同一個給定線上注冊的每個處理程序必須指定這個標志;否則,在每條線上只能有一個處理程序。

  • devname 與中斷相關的設備的ASCII文本表示法。例如,PC機上鍵盤中斷對應的這個值為“keyboard”。這些名字會被/proc/irq和/proc/interrupt文件使用,以便與用戶通信。

  • dev_id 用於共享中斷線。當一個中斷處理程序需要釋放時,dev_id將提供唯一的標志信息(cookie),以便從共享中斷線的諸多中斷處理程序中刪除指定的那一個。如果沒有該參數,那么內核不可能知道在給定的中斷線上到底要刪除哪個處理程序。如果無需共享中斷線,那么該參數置為NULL即可;如果需要共享,那么必須傳遞唯一的信息。除非設備又舊又破且位於ISA總線上,那么必須支持共享中斷。另外,內核每次調用中斷處理程序時,都會把這個指針傳遞給它(中斷處理程序都是預先在內核進行注冊的回調函數(callback function)),不同的函數位於不同的驅動程序中,因此這些函數共享同一個中斷線時,內核必須准確為它們創造執行環境,此時就可以通過該指針將有用的環境信息傳遞給它們了。實踐中,往往通過它傳遞驅動程序的設備結構:這個指針是唯一的,而且有可能在中斷處理程序內及設備模式中被用到。

返回值:
request_irq()執行成功時,返回0;出錯時,返回非0,指定的中斷處理程序不會被注冊。常見錯誤-EBUSY,表示給定的中斷線已經在使用(或當期用戶或者你沒有指定SA_SHIRQ)。

注意:
request_irq()可能會睡眠,因此不能在中斷上下文或其他不允許阻塞的代碼中調用。

request_irq()為什么會睡眠?
因為在注冊過程中,內核需要在/proc/irq文件創建一個與中斷對應的項。proc_mkdir()就是用來創建這個新的procfs項的,proc_mkdir()通過調用proc_create()對這個新的profs項進行設置,而proc_create()會調用kmalloc()來請求分配內存。kmalloc()是可以睡眠的。

通過request_irq()注冊中斷處理程序典型應用:

if (request_irq(irqn, my_interrupt, SA_SHIRQ, "my_device", dev)) { // not 0: error
    printk(KERN_ERR "mydevice:cannot register IRQ %d\n", irqn);
    return -EIO;
}

irqn 請求的中斷線,my_interrupt 中斷處理程序,"my_device" 設備名稱,通過dev_id傳遞dev結構體。返回非0,則表示注冊失敗;返回0,表示注冊成功,響應中斷時處理程序會被調用。

釋放中斷處理程序
卸載驅動程序,需要注銷相應的中斷處理程序,並釋放中斷線。調用:

void free_irq(unsigned int irq, void* dev_id);

如果指定的中斷線不是共享的,那么該函數將刪除處理程序同時禁用該中斷線;如果是共享的,則僅刪除dev_id對應的處理程序,而這條中斷線只有在刪除了最后一個處理程序時才會被禁用。這也是dev_id必須唯一的原因:共享的中斷線中,用來區分不同的處理程序。

必須從進程上下文中調用free_irq();

[======]

編寫中斷處理程序

一個典型的中斷處理程序聲明:

static irqreturn_t intr_handler(int irq, void *dev_id, struct pt_regs *regs);

其簽名必須與request_irq()中參數handler所要求函數類型一致。

參數說明:

  • irq 該處理程序要響應的中斷的中斷線號。目前,沒有太大用處,可能打印LOG時會用到。內核2.0之前,因為沒有dev_id參數,必須通過irq才能區分使用相同中斷程序、相同的中斷處理程序的多個設備。如,具有多個相同類型硬盤驅動控制器的計算機。
  • dev_id 與中斷處理程序注冊時傳遞給request_irq()的參數dev_id必須一致。如果該值具有唯一確定性(建議采用這樣的值,以便支持共享),那么它就相當於一個cookie,可以用來區分共享同一個中斷處理程序的多個設備。另外,dev_id也可能指向一個中斷處理程序使用的一個數據結構,對每個設備而言,設備結構都是唯一的。
  • regs 指向結構的指針,包含處理中斷前處理器的寄存器和狀態。除了調試,其他時候很少使用。

返回值:
返回值是一個特殊類型:irqerturn_t。實際是int類型,為了與早期內核兼容,因為2.6以前實際是void類型。
可能返回2個特殊值:IRQ_NONE和IRQ_HANDLED。當中斷處理程序檢測到一個中斷,但該中斷並非注冊處理函數時指定的產生源時,返回IRQ_NONE;當中斷處理程序被正確調用,而且確實是它所對應的設備產生的中斷時,返回IRQ_HANDLED。
返回值也可以用宏IRQ_RETVAL(x):x為非0,宏返回IRQ_HANDLED;x為0,返回IRQ_NONE。

定義說明:
中斷處理程序通常標記為static,因為從來不會在其他文件被直接調用。

可重入性
Linux中中斷處理程序無需重入。當一個給定的中斷處理程序正在執行時,相應的中斷線在所有處理器上都會被屏蔽,以防在同一個中斷線上接收另一個新的中斷。也就是說,同一個中斷線,同一個中斷處理程序,在執行完畢之前不可能同時在2個處理器上執行,加上中斷處理程序不能休眠,因此無需考慮可重入性。
注意:
1)如果中斷處理程序可以被中斷(不是同一個中斷線的中斷),稱為嵌套中斷,或中斷嵌套。
2)舊版本Linux允許中斷嵌套,但2010以后提交的版本中,已經禁止了中斷嵌套。參考Linux的中斷可以嵌套嗎? | 知乎

共享的中斷處理程序

共享的和非共享的處理程序,注冊和運行方式相似,差異主要有三點:
1)request_irq()的參數flgs必須設置SA_SHIRQ標志。
2)對每個注冊的中斷處理程序來說,dev_id參數必須唯一。指向任一設備結構的指針就可以滿足這一要求;通常會選設備結構,因為它是唯一的,而且中斷處理性可能會用到。不能給共享的處理程序傳遞NULL值。
3)中斷處理程序必須能區分其設備是否真正產生中斷。既需要硬件支持,也需要處理程序中有相關的處理邏輯。

所有共享中斷線的驅動程序,都必須滿足以上要求。否則,中斷線無法共享。

中斷處理程序實例

一個來自RTC驅動程序的中斷處理程序,可在drivers/char/rtc.c中找到。RTC用於設置系統時鍾,提供alarm或周期性定時器。

RTC驅動程序裝載時,rtc_init()會被調用,對驅動程序進行初始化。
1)注冊中斷處理程序:

/* 對RTC_IRQ 注冊rtc_interrupt */
if (request_irq(RTC_IRQ, rtc_interrupt, SA_INTERRUPT, "rtc", NULL)) {
    printk(KERN_ERR "rtc:cannot register IRQ %d\n", RTC_IRQ);
    return -EIO;
}

SA_INTERRUPT指明是fast IRQ,會禁用其他中斷,該中斷線也不允許共享,因此處理程序不會用到什么特殊值,傳入dev_id
可以是NULL。

2)定義處理程序本身

/*
 * 很小的中斷處理程序, 運行時設置了SA_INTERRUPT, 但可能與set_rtc_mmss() 調用產生沖突(rtc中斷和時鍾中斷可以輕易在兩個不同的CPU上同時運行). 因此, 我們需要rtc_lock自旋鎖串行訪問該芯片. 自旋鎖的實現在時鍾代碼中, 與體系結構相關.
 * 關於set_rtc_mmss(), 參見 ./arch/XXXX/kernel/time.c
 *
 */
static irqreturn_t rtc_interrupt(int irq, void* dev_id, struct pt_regs* regs) {
    /*
     * 可以是alarm中斷, 更新完成的中斷或周期性中斷. 我們把狀態保存在rtc_irt_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);

    /* 執行其余操作 */
    spin_lock(&rtc_task_lock);
    {
        if (rtc_callback)
            rtc_callback->func(rtc_callback->private_data); /* 調用預先設置好的回調函數. RTC允許預先注冊一個回調函數 */
    }
    spin_unlock(&rtc_task_lock);
    wake_up_interruptible(&rtc_wait); /* 喚醒一個等待隊列上的可中斷任務 */

    kill_fasync(&rtc_async_queue, SIGIO, POLL_IN); /* 向用戶發送可讀信號 */
    return IRQ_HANDLED; /* 該中斷處理程序被正確調用 */
}

[======]

中斷上下文

當執行一個中斷處理程序(中斷上半部)或中斷下半部時,內核處於中斷上下文(interrupt context)中。
進程上下文是一種內核所處的模式,此時內核代表進程執行 -- 例如,執行系統調用或運行內核線程。進程上下文中,可通過current宏關聯當前進程。此外,因為進程是以進程上下文的形式連接到內核中,因此,在進程上下文可以睡眠,也可以調用調度程序。

中斷上下文的特性:
1)不能睡眠
中斷上下文和進程沒有什么關系,跟current宏也不相干(盡管它會指向被中斷的進程)。因為沒有進程的背景,所以中斷上下文不可以睡眠,否則怎么再對它重新調度(處理器調度的是進程)?
2)嚴格時間限制,盡可能迅速、簡短
中斷上下文具有嚴格的時間限制,因為它打斷了其他代碼(進程和另一個中斷處理程序)。中斷上下文的代碼應當迅速簡潔,盡量不用循環處理繁重工作。
3)盡量把繁重工作放在下半部執行
因為中斷處理程序要求迅速、簡短,因此可以盡量把工作從中斷處理性中分離出來,放到下半部執行。
4)中斷處理程序棧有限,謹慎使用。
中斷處理程序共享所中斷進程的內核棧,大小是兩頁,i.e. 32bit體系結構上是8KB,64bit體系結構上是16KB。

[======]

中斷處理機制的實現

中斷處理系統的實現依賴於體系結構。
下圖展示了從硬件到內核的路由的一個示例:

中斷的入口點是:硬件產生了中斷,處理器開始跳轉到對應的一個唯一位置,在棧中保存這個號,並存放當前寄存器的值(被中斷任務),開始處理中斷。
中斷產生到內核處理中斷的步驟:
1)硬件產生中斷。
2)處理器跳轉到中斷向量表對應的中斷處理程序,棧中保存中斷號及打斷任務保存線程。
3)內核調用do_IRQ()。之后,大多數中斷處理代碼用C編寫。
do_IRQ() 聲明:

unsigned int do_IRQ(struct pt_regs regs);

regs 保存原始寄存器的值,也會保存中斷的值。do_IRQ()提取中斷號,對應x86代碼:

int irq = regs.orig_eax & 0xFF;

計算出中斷號后,do_IRQ()對所接收的中斷進行應答,禁止這條線上的中斷傳遞。這些操作通過調用mask_and_ack_8259A()來完成。

4)處理中斷
如果中斷線上已安裝中斷處理程序,就調用handle_IRQ_event()處理;如果沒有,就直接調用ret_from_intr(),返回內核運行中斷的代碼。
x86上,handle_IRQ_event() :

asmlinkage int handle_IRQ_event(unsigned int irq, struct pt_regs* regs, struct irqaction* action)
{
    int status = 1;
    int retval = 0;
    
    if (!(action->flags & SA_INTERRUPT)) /* 注冊的中斷處理函數沒設置標志, 或者注冊中斷處理程序時設置fast IRQ, 禁用了中斷 */
        local_irq_enable(); /* 打開被關閉的中斷 */
    /* 遍歷action鏈表, 通過已注冊的handler處理中斷 */
    do {
        status |= action->flags;
        retval |= action->handler(irq, action->dev_id, regs);
        action = action->next;
    } while (action);

    if (status & SA_SAMPLE_RANDOM) /* 注冊時指定SA_SAMPLE_RANDOM, 表明中斷對內核熵池有貢獻 */
        add_interrupt_randomness(irq); /* 用中斷時間間隔時間為隨機數產生器產生熵, 加入內核熵池 */
    local_irq_disable(); /* 禁止本地中斷 */
    return retval;
}

5)調用ret_from_intr()

類似於初始入口代碼,用匯編編寫。用於檢查重新調度釋放正在掛起(意味着設置了need_resched)。如果重新調度正在掛起,並且內核正在返回用戶空間(i.e. 中斷了用戶進程),那么schedule()被調用。如果內核正在返回內核空間(i.e. 中斷了內核本身),只有preempt_count(搶占計數器)為0時,schedule()才會被調用(否則,搶占內核便是不安全的)。schedule() 返回后,或者如果沒有掛起的工作,那么原來的寄存器被恢復,內核恢復到曾經中斷的點。

x86 上,初始匯編例程位於arch/i386/kernel/entry.S,C方法位於arch/i386/kernel/irq.c 。
其他體系結構與此類似。

/proc/interrupts 查看中斷統計信息
procfs 是一個虛擬文件系統,只存在於內核內存,一般安裝於/proc目錄下。在procfs中讀寫文件都要調用內核函數,這些函數模擬從真實文件中讀或寫。/proc/interrupts 文件存放系統中與中斷相關的統計信息。

$ cat /proc/interrupts
            CPU0       CPU1       CPU2       CPU3       
   0:         32          0          0          0   IO-APIC    2-edge      timer
   1:         46          0         74          0   IO-APIC    1-edge      i8042
   8:          1          0          0          0   IO-APIC    8-edge      rtc0
   9:          0          0          0          0   IO-APIC    9-fasteoi   acpi
  12:       1287          0        279          0   IO-APIC   12-edge      i8042
  14:          0          0          0          0   IO-APIC   14-edge      ata_piix
  15:          0          0          0          0   IO-APIC   15-edge      ata_piix
  16:        906          0        842          0   IO-APIC   16-fasteoi   vmwgfx, snd_ens1371
  17:       7556          0       3617          0   IO-APIC   17-fasteoi   ehci_hcd:usb3, ioc0
  18:         38          0          0          0   IO-APIC   18-fasteoi   uhci_hcd:usb4
  19:        112          0         21        103   IO-APIC   19-fasteoi   eth0
  24:          0          0          0          0   PCI-MSI 344064-edge      PCIe PME, pciehp
  25:          0          0          0          0   PCI-MSI 346112-edge      PCIe PME, pciehp
  26:          0          0          0          0   PCI-MSI 348160-edge      PCIe PME, pciehp
  27:          0          0          0          0   PCI-MSI 350208-edge      PCIe PME, pciehp
  28:          0          0          0          0   PCI-MSI 352256-edge      PCIe PME, pciehp
  29:          0          0          0          0   PCI-MSI 354304-edge      PCIe PME, pciehp
  30:          0          0          0          0   PCI-MSI 356352-edge      PCIe PME, pciehp
  31:          0          0          0          0   PCI-MSI 358400-edge      PCIe PME, pciehp
  32:          0          0          0          0   PCI-MSI 360448-edge      PCIe PME, pciehp
  33:          0          0          0          0   PCI-MSI 362496-edge      PCIe PME, pciehp
  34:          0          0          0          0   PCI-MSI 364544-edge      PCIe PME, pciehp
  35:          0          0          0          0   PCI-MSI 366592-edge      PCIe PME, pciehp
  36:          0          0          0          0   PCI-MSI 368640-edge      PCIe PME, pciehp
  37:          0          0          0          0   PCI-MSI 370688-edge      PCIe PME, pciehp
  38:          0          0          0          0   PCI-MSI 372736-edge      PCIe PME, pciehp
  39:          0          0          0          0   PCI-MSI 374784-edge      PCIe PME, pciehp
  40:          0          0          0          0   PCI-MSI 376832-edge      PCIe PME, pciehp
  41:          0          0          0          0   PCI-MSI 378880-edge      PCIe PME, pciehp
  42:          0          0          0          0   PCI-MSI 380928-edge      PCIe PME, pciehp
  43:          0          0          0          0   PCI-MSI 382976-edge      PCIe PME, pciehp
  44:          0          0          0          0   PCI-MSI 385024-edge      PCIe PME, pciehp
  45:          0          0          0          0   PCI-MSI 387072-edge      PCIe PME, pciehp
  46:          0          0          0          0   PCI-MSI 389120-edge      PCIe PME, pciehp
  47:          0          0          0          0   PCI-MSI 391168-edge      PCIe PME, pciehp
  48:          0          0          0          0   PCI-MSI 393216-edge      PCIe PME, pciehp
  49:          0          0          0          0   PCI-MSI 395264-edge      PCIe PME, pciehp
  50:          0          0          0          0   PCI-MSI 397312-edge      PCIe PME, pciehp
  51:          0          0          0          0   PCI-MSI 399360-edge      PCIe PME, pciehp
  52:          0          0          0          0   PCI-MSI 401408-edge      PCIe PME, pciehp
  53:          0          0          0          0   PCI-MSI 403456-edge      PCIe PME, pciehp
  54:          0          0          0          0   PCI-MSI 405504-edge      PCIe PME, pciehp
  55:          0          0          0          0   PCI-MSI 407552-edge      PCIe PME, pciehp
  56:         97          0          0          0   PCI-MSI 1572864-edge      xhci_hcd
  57:          0          0          0          0   PCI-MSI 1572865-edge      xhci_hcd
  58:          0          0          0          0   PCI-MSI 1572866-edge      xhci_hcd
  59:          0          0          0          0   PCI-MSI 1572867-edge      xhci_hcd
  60:          0          0          0          0   PCI-MSI 1572868-edge      xhci_hcd
  61:         84          0         56          0   PCI-MSI 1130496-edge      0000:02:05.0
  62:          0          0          0          0   PCI-MSI 129024-edge      vmw_vmci
  63:          0          0          0          0   PCI-MSI 129025-edge      vmw_vmci
NMI:          0          0          0          0   Non-maskable interrupts
LOC:       4802       4029       6007       6308   Local timer interrupts
SPU:          0          0          0          0   Spurious interrupts
PMI:          0          0          0          0   Performance monitoring interrupts
IWI:          0          0          0          0   IRQ work interrupts
RTR:          0          0          0          0   APIC ICR read retries
RES:      14669      15576      16058      16864   Rescheduling interrupts
CAL:       2240       1257       1400       1781   Function call interrupts
TLB:        508        587        449        589   TLB shootdowns
TRM:          0          0          0          0   Thermal event interrupts
THR:          0          0          0          0   Threshold APIC interrupts
DFR:          0          0          0          0   Deferred Error APIC interrupts
MCE:          0          0          0          0   Machine check exceptions
MCP:          1          1          1          1   Machine check polls
HYP:          0          0          0          0   Hypervisor callback interrupts
ERR:          0
MIS:          0
PIN:          0          0          0          0   Posted-interrupt notification event
PIW:          0          0          0          0   Posted-interrupt wakeup event
  • 第1列是中斷線,這里中斷號為01,89,...,24~63。沒有顯示安裝處理程序的中斷線。
  • 第25列是一個接收中斷數目的計數器。系統中的每個處理器都有這樣的列,表明有4個這樣的處理器(CPU0CPU3)。可以看到,4個處理器的時鍾中斷(Local timer interrupts)總共已經接收21146次中斷,如果HZ為1000,代表機器已經啟動21秒。
  • IO-APIC-level或IO-APIC-edge列,作為自己的中斷控制器。
  • 最后一列是與該中斷相關的設備名字,該名字通過參數devname提供給函數request_irq()(中斷注冊函數)的。如果中斷線是共享的,這條中斷線上注冊的所有設備都會列出來。

[======]

中斷控制

Linux內核提供一組接口用於操作機器上的中斷狀態:能用於禁止當前處理器的中斷系統,屏蔽整個機器的一條中斷線。例程與體系結構相關,位於<asm/system.h>,<asm/irq.h>。

為什么會要控制中斷系統?
因為需要提供同步,通過禁止中斷,可以確保某個中斷處理程序不會搶占當前的代碼。禁止中斷還可以禁止內核搶占。不過,不論禁止中斷 or 禁止內核搶占,都沒有提供任何保護機制來防止來自其他處理器的並發訪問。

禁止內核搶占:preempt_disable、preempt_enable等;

禁止和激活中斷

用於禁止當前處理器上的本地中斷,隨后由激活:

local_irq_disable();
/* 禁止中斷 */
local_irq_enable();

上面的例程存在問題,因為其實現往往是簡單禁止、允許中斷(x86下,local_irq_disable <=> cli指令,local_irq_enable <=> sti指令),如果在這之前已經禁止中斷,調用該例程后,將無條件恢復中斷。解決辦法:只是恢復到禁止中斷前的狀態。

unsigned long flags;

local_irq_save(flags); /* 禁止中斷 */
/* ... */
local_irq_restore(flags); /* 中斷恢復到原來的狀態 */

由於flags包含具體體系結構的數據,即中斷系統的狀態,因此,flags不能傳遞給另一個函數,必須駐留在同一棧幀中。基於此,local_irq_save和restore的調用必須在同一個函數中。

前面這些禁止和激活中斷的函數,都可以在中斷上下文和進程上下文中調用。

不再使用全局cli()

x86下,cli() 能禁止系統中所有處理器上的中斷。如果另一個處理器調用了該方法,那么它就不得不等待,直到中斷重新被激活才能繼續執行
。cli()對應激活函數是sti()。
取消全局cli() 優點:
1)強制驅動程序編寫者實現真正的加鎖。特定目的的細粒度鎖 比全局鎖要快很多,也完全吻合cli()使用初衷。
2)是的很多代碼更具流線型,避免代碼的成簇布局。得到的中斷系統更簡單、易理解。

禁止指定中斷線

local_irq_disable/local_irq_save 是禁止整個處理器上所有中斷。某些情況下,只需要禁止某條特定中斷線就足夠了。這就是所謂屏蔽條(masking out)一條中斷線。Linux提供4個接口:

void disable_irq(unsigned int irq); 
void disable_irq_nosync(unsigned int irq); 
void enable_irq(unsigned int irq);
void synchronize_irq(unsigned int irq);

disable_irq()和disable_irq_nosync()禁止指定中斷線向所有處理器傳遞。
只有當前正在執行的所有處理程序完成后, disable_irq()才能返回。因此,調用者不僅要確保不在指定線上傳遞新的中斷,同時還要確保所有已經開始執行的處理程序已全部退出。
disable_irq_nosync()不會等待當前中斷處理程序執行完畢。

synchronize_irq() 等待一個特定的中斷處理程序的退出。如果該處理程序正在執行,那么函數必須退出后才能返回。

使用注意事項:
1)禁止和使能指定中斷線次數需配對
上面3個函數調用可以嵌套,但在一條指定的中斷線上,對disable_irq()和disable_irq_nosync()的每次調用,都需要相應地調用一次enable_irq()。只有在對enable_irq()完成最后一次調用后,才真正重新激活了中斷線。

2)不要在中斷上下文激活當前中斷線
當正在處理一條中斷線時,本意並不想激活它,因此使能指定中斷線時要特別小心。當某個處理程序的中斷線正在處理時,默認會被屏蔽掉。

3)禁止多個中斷處理程序共享的中斷線是不合適的
禁止某個共享中斷線,也就禁止了這條線上所有設備的中斷傳遞。因此,新設備的驅動程序應該傾向於不使用這些接口(如PCI設備必須支持中斷線共享)。

中斷系統的狀態

如何了解中斷系統的狀態,如中斷當前是禁止的還是激活的?或者當前是否正處於中斷上下文的執行狀態中?
本小節提供這方面回答。

<asm/system.h>中
irqs_disable() 如果本地處理器上的中斷系統被禁止,則返回非0;否則,返回0。
<asm/hardirq.h>中
in_interrupt() 如果內核處於中斷上下文,則返回非0;如果在進程上下文,返回0。
in_irq() 如果當前正在執行中斷處理程序,則返回非0;否則,返回0.

所有中斷控制方法列表:

[======]

別打斷我,馬上結束

本章考察了硬件異步中斷,中斷用來打斷操作系統。
1)大多數硬件都通過中斷與操作系統通信,對給定硬件的驅動程序注冊中斷處理程序(request_irq),可以響應並處理來自相關硬件的中斷。中斷過程所做工作主要包括:應答、重新設置硬件,從設備拷貝數據到內存,或者從內存拷貝數據到設備,處理硬件請求,發送新硬件請求。
2)內核提供接口包括:注冊、釋放中斷處理程序,禁止中斷,屏蔽指定中斷線,檢查中斷系統的狀態。
3)對中斷處理程序本身的要求:簡短、迅速。將中斷工作分為兩半,上半部是中斷處理程序;下半部執行推后的工作。

[======]


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM