Linux中斷一網(wǎng)打盡(1) —— 中斷及其初始化

[toc]

Linux中斷一網(wǎng)打盡 —— 中斷及其初始化

前情提要

通過本文您可以了解到如下內(nèi)容:

  • Linux 中斷是什么,如何分類编矾,能干什么?
  • Linux 中斷在計(jì)算機(jī)啟動(dòng)各階段是如何初始化的?
中斷是什么

既然叫中斷, 那我們首先就會想到這個(gè)中斷是中斷誰宠漩?想一想計(jì)算機(jī)最核心的部分是什么?沒錯(cuò)懊直, CPU扒吁, 計(jì)算機(jī)上絕大部分的計(jì)算都在CPU中完成,因此這個(gè)中斷也就是中斷CPU當(dāng)前的運(yùn)行室囊,讓CPU轉(zhuǎn)而先處理這個(gè)引起中斷的事件雕崩,通常來說這個(gè)中斷的事件比較緊急,處理完畢后再繼續(xù)執(zhí)行之前被中斷的task融撞。比如盼铁,我們敲擊鍵盤,CPU就必須立即響應(yīng)這個(gè)操作尝偎,不然我們打字就全變成了慢動(dòng)作~饶火。說白了中斷其實(shí)就是一種主動(dòng)通知機(jī)制,如果中斷源不主動(dòng)通知致扯,那想知道其發(fā)生了什么事情肤寝,只能一次次地輪詢了,白白耗費(fèi)CPU抖僵。

中斷的分類

大的方向上一般分為兩大類:同步中斷和異步中斷鲤看,按Intel的說法,將異步中斷稱為中斷耍群,將同步中斷稱為異常刨摩。

異步中斷

主要是指由CPU以外的硬件產(chǎn)生的中斷寺晌,比如鼠標(biāo),鍵盤等澡刹。它的特點(diǎn)是相對CPU來說隨時(shí)隨機(jī)發(fā)生呻征,事先完全沒有預(yù)兆,不可預(yù)期的罢浇。異步中斷發(fā)生時(shí)陆赋,CPU基本上都正在執(zhí)行某條指令。

異步中斷可分為可屏蔽和不可屏蔽兩種嚷闭,字如其義不用多解釋攒岛。

同步中斷

主要是指由CPU在執(zhí)行命令過程中產(chǎn)生的異常,它一定是在CPU執(zhí)行完一條命令后才會發(fā)出胞锰,產(chǎn)生于CPU內(nèi)部灾锯。按其被CPU處理后返回位置的不同,我們將同步中斷分為故障(fault), 陷阱(trap)和終止(abort)三類顺饮。我們通過一個(gè)表格來作下對比區(qū)分:

中斷分類 特點(diǎn) 處理完畢后的返回位置 例子
故障(fault) 潛在可能恢復(fù)的錯(cuò)誤 重新執(zhí)行引起此故障的指令 缺頁中斷
陷阱(trap) 為了實(shí)現(xiàn)某種功能有意而為之發(fā)生的錯(cuò)誤 執(zhí)行引發(fā)當(dāng)前陷阱的指令的下一條指令 系統(tǒng)調(diào)用
終止(abort) 不可恢復(fù)的錯(cuò)誤 沒有返回,進(jìn)程將被終止

兩點(diǎn)說明:

  • 處理完畢后的返回位置:發(fā)生異常時(shí)凌那,CPU最終會進(jìn)入到相應(yīng)的異常處理程序中(簡單說就是CPU需要執(zhí)行一次跳轉(zhuǎn))在執(zhí)行具體操作前會設(shè)置好的異常處理完成后跳轉(zhuǎn)回的CS:IP, 即代碼段寄存器和程序指針寄存器,不同類型的異常其設(shè)置的CS:IP不同而已帽蝶;

  • 有些分類方法還會有一種叫可編程異常的,比如說把系統(tǒng)調(diào)用算作這一類励稳,也可以佃乘。但是如果按處理完畢后的返回位置來說系統(tǒng)調(diào)用是可以歸入陷阱這一類的。

硬件中斷的管理模型

我們都知道CPU上只有有限多的腳針驹尼,負(fù)責(zé)與外部通訊恕稠,比如有數(shù)據(jù)線,地址線等扶欣,也有中斷線鹅巍,但一般只有兩條NMI(不可屏蔽中斷線)和INTR(可屏蔽中斷線), 新的CPU有LINT0和LINT1腳針料祠。那您會問了骆捧,電腦上有那么多外設(shè),CPU就這兩根線髓绽,怎么接收這么多外設(shè)的中斷信號呢敛苇?確實(shí),因此CPU找了一個(gè)管理這些眾多中斷的代理人——中斷控制器。

就目前我們使用的SMP多核架構(gòu)里枫攀,我們經(jīng)常使用高級可編程中斷控制器APIC括饶, 老式的 8259A 可編程中斷控制器大家有興趣可自行搜索。

APIC分為兩部分来涨,IO APIC和Local APIC图焰,從名字上我們就可略知一二。

  • IO APIC: 用來連接各種外設(shè)的硬件控制器蹦掐,接收其發(fā)送的中斷請求信號技羔,然后將其傳送到Local APIC, 這個(gè)IO APIC一般會封裝在主板南板芯片上;

  • Local APIC: 基本上集成在了CPU里, 向CPU通知中斷發(fā)生卧抗。

  • 放張網(wǎng)上的圖:

    ioapic.jpg
中斷的初始化
Linux 啟動(dòng)流程

中斷的初始化是穿插在Linux本身啟動(dòng)和初始化過程中的藤滥,因此我們在這里簡要說一下Linux本身的初始化。

  • 64位Linux啟動(dòng)大的方向上需要經(jīng)過 實(shí)模式 -> 保護(hù)模式 -> 長模式 第三種模式的轉(zhuǎn)換;
  • 電源接通社裆,CPU啟動(dòng)并重置各寄存器后運(yùn)行于實(shí)模式下拙绊,CS:IP加載存儲于ROM中的一跳轉(zhuǎn)指令,跳轉(zhuǎn)到BIOS中泳秀;
  • BIOS啟動(dòng)标沪,硬件自測,讀取MRB;
  • BIOS運(yùn)行第一階段引導(dǎo)程序晶默,第一階段引導(dǎo)程序運(yùn)行第二階段引導(dǎo)程序,通常是 grub;
  • Grub開始引導(dǎo)內(nèi)核運(yùn)行;
  • 相關(guān)初始化后進(jìn)行保護(hù)模式航攒,再進(jìn)入長模式磺陡,內(nèi)核解壓縮;
  • 體系無關(guān)初始化部分;
  • 體系相關(guān)初始化部分;

總結(jié)了一張圖漠畜,僅供參考:

linux啟動(dòng)流程.png
中斷描述符表

外設(shè)千萬種,CPU統(tǒng)統(tǒng)不知道憔狞。所有的中斷到了CPU這里就只是一個(gè)中斷號,然后初始化階段設(shè)置好中斷號到中斷處理程序的對應(yīng)關(guān)系拍冠,CPU獲取到一個(gè)中斷號后簇抵,查到對應(yīng)的中斷處理程序調(diào)用就好了。

這兩者的對應(yīng)關(guān)系最后會抽象成了中斷向量表晃财, 現(xiàn)在叫 IDT中斷描述符表。

中斷的第一次初始化

實(shí)模式下的初始化

  • 上面那張Linux啟動(dòng)流程圖如果你仔細(xì)看的話會發(fā)現(xiàn)在BIOS程序加載運(yùn)行時(shí)断盛,在實(shí)模式下也有一個(gè)BIOS的中斷向量表,這個(gè)中斷向量表提供了一些類似于BIOS的系統(tǒng)調(diào)用一樣的方法钢猛。比如Linux在初始化時(shí)需要獲取物理內(nèi)存的詳情,就 是調(diào)用了BIOS的相應(yīng)中斷來獲取的厢洞。見下圖:
選區(qū)_035.png
中斷的第二次初始化
  • 在進(jìn)入到保護(hù)模式后躺翻,會全新初始化一個(gè)空的中斷描述符表 IDT, 供 kernel 使用;

  • Linux Kernel提供256個(gè)大小的中斷描述符表

    #define IDT_ENTRIES           256
    
    gate_desc idt_table[IDT_ENTRIES] __page_aligned_bss;
    
中斷的第三次初始化
  • 在進(jìn)入到長模式后,在x86_64_start_kernel先初始化前32個(gè)異常類型的中斷(即上面定義的 idt_table 的前32項(xiàng))踊淳;

    void __init idt_setup_early_handler(void)
    {
      int i;
    
      for (i = 0; i < NUM_EXCEPTION_VECTORS; i++)
          set_intr_gate(i, early_idt_handler_array[i]);
    
      load_idt(&idt_descr);
    }
    

    其中 early_idt_handler_array這個(gè)數(shù)組放置了32個(gè)異常類型的中斷處理程序陕靠,我們先看一下它的定義:

    const char early_idt_handler_array[32][9];
    

    二維數(shù)組,每一個(gè)early_idt_handler_array[i]有9個(gè)字節(jié)垄开。

    這個(gè) early_idt_handler_array的初始化很有意思,它用AT&T的匯編代碼完成溉躲,在文件arch/x86/kernel/head_64.S中:

    ENTRY(early_idt_handler_array)
      i = 0
      .rept NUM_EXCEPTION_VECTORS
      .if ((EXCEPTION_ERRCODE_MASK >> i) & 1) == 0
          UNWIND_HINT_IRET_REGS
          pushq $0    # Dummy error code, to make stack frame uniform
      .else
          UNWIND_HINT_IRET_REGS offset=8
      .endif
      pushq $i        # 72(%rsp) Vector number
      jmp early_idt_handler_common
      UNWIND_HINT_IRET_REGS
      i = i + 1
      .fill early_idt_handler_array + i*EARLY_IDT_HANDLER_SIZE - ., 1, 0xcc
      .endr
      UNWIND_HINT_IRET_REGS offset=16
    END(early_idt_handler_array)
    
    

    這段匯編循環(huán)遍歷32次來初始化每一個(gè)early_idt_handler_array[i], 也就是填充它的9個(gè)字節(jié):其中2個(gè)字節(jié)是壓棧錯(cuò)誤碼指令益兄,2個(gè)字節(jié)是壓棧向量號指令,余下的5個(gè)字節(jié)是函數(shù)跳轉(zhuǎn)指令(jmp early_idt_handler_common)疑枯。由此我們可以看出蛔六,這前32個(gè)異常類型的中斷處理函數(shù)最終都會調(diào)用到early_idt_handler_common, 這個(gè)函數(shù)這里就不貼它的代碼了,我們說下它的大致流程:

    a. 先將各寄存器的值壓棧保存国章;
    b. 如果是 缺頁異常,就調(diào)用 `early_make_patable`; 
    c. 如果是 其他異常氓拼,就調(diào)用 `early_fixup_exception`; 
    
  • 體系結(jié)構(gòu)相關(guān)的中斷初始化

    這也是一次部分初始化,它發(fā)生在 start_kernelsetup_arch中坏匪,即發(fā)生在 Linux 啟動(dòng)流程中的體系結(jié)構(gòu)初始化部分撬统。這部分實(shí)際上是更新上面已初始化的32個(gè)異常類中的X86_TRAP_DB(1號, 用于debug)和X86_TRAP_BP(3號, 用于debug時(shí)的斷點(diǎn));

    static const __initconst struct idt_data early_idts[] = {
      INTG(X86_TRAP_DB,       debug),
      SYSG(X86_TRAP_BP,       int3),
    };
    
    void __init idt_setup_early_traps(void)
    {
      idt_setup_from_table(idt_table, early_idts, ARRAY_SIZE(early_idts),
                   true);
      load_idt(&idt_descr);
    }
    
    

    debugint3這兩個(gè)匯編實(shí)現(xiàn)的中斷處理程序這里我們就不詳述了。

  • 更新 X86_TRAP_PF 缺頁異常的中斷處理程序

    void __init idt_setup_early_pf(void)
    {
      idt_setup_from_table(idt_table, early_pf_idts,
                   ARRAY_SIZE(early_pf_idts), true);
    }
    
    static const __initconst struct idt_data early_pf_idts[] = {
      INTG(X86_TRAP_PF,       page_fault),
    };
    
  • trap_init中調(diào)用 idt_setup_traps更新部分異常的中斷處理程序:

    void __init idt_setup_traps(void)
    {
      idt_setup_from_table(idt_table, def_idts, ARRAY_SIZE(def_idts), true);
    }
    
    static const __initconst struct idt_data def_idts[] = {
      INTG(X86_TRAP_DE,       divide_error),
      INTG(X86_TRAP_NMI,      nmi),
      INTG(X86_TRAP_BR,       bounds),
      INTG(X86_TRAP_UD,       invalid_op),
      INTG(X86_TRAP_NM,       device_not_available),
      INTG(X86_TRAP_OLD_MF,       coprocessor_segment_overrun),
      INTG(X86_TRAP_TS,       invalid_TSS),
      INTG(X86_TRAP_NP,       segment_not_present),
      INTG(X86_TRAP_SS,       stack_segment),
      INTG(X86_TRAP_GP,       general_protection),
      INTG(X86_TRAP_SPURIOUS,     spurious_interrupt_bug),
      INTG(X86_TRAP_MF,       coprocessor_error),
      INTG(X86_TRAP_AC,       alignment_check),
      INTG(X86_TRAP_XF,       simd_coprocessor_error),
    
    #ifdef CONFIG_X86_32
      TSKG(X86_TRAP_DF,       GDT_ENTRY_DOUBLEFAULT_TSS),
    #else
      INTG(X86_TRAP_DF,       double_fault),
    #endif
      INTG(X86_TRAP_DB,       debug),
    
    #ifdef CONFIG_X86_MCE
      INTG(X86_TRAP_MC,       &machine_check),
    #endif
    
      SYSG(X86_TRAP_OF,       overflow),
    #if defined(CONFIG_IA32_EMULATION)
      SYSG(IA32_SYSCALL_VECTOR,   entry_INT80_compat),
    #elif defined(CONFIG_X86_32)
      SYSG(IA32_SYSCALL_VECTOR,   entry_INT80_32),
    #endif
    };
    
  • trap_init中調(diào)用 idt_setup_ist_traps更新部分異常的中斷處理程序,

    看到這里您可能問凭迹,上面不是調(diào)用了idt_setup_traps嗅绸,怎么這時(shí)又調(diào)用idt_setup_ist_traps? 這兩者有什么區(qū)別撕彤?說起來話有點(diǎn)長,我們盡量從流程上給大家講清楚羹铅,但不深入到具體的細(xì)節(jié)。

    1. 想說明這個(gè)問題麻蹋,我們先來講下棧這個(gè)東西:

      a. 首先每個(gè)進(jìn)程都有自己的用戶態(tài)棧焊切,對應(yīng)進(jìn)程虛擬地址空間內(nèi)的stack部分,用于進(jìn)程在用戶態(tài)變量申請蛛蒙,函數(shù)調(diào)用等操作渤愁;

      b. 除了用戶態(tài)棧,每個(gè)進(jìn)程在創(chuàng)建時(shí)(內(nèi)核對應(yīng)創(chuàng)建 task_struct結(jié)構(gòu))同時(shí)會創(chuàng)建對應(yīng)的內(nèi)核棧诺苹,這里進(jìn)程由用戶態(tài)進(jìn)入到內(nèi)核態(tài)執(zhí)行函數(shù)時(shí),相應(yīng)的所用的棧也會切換到內(nèi)核棧收奔;

      c. 如果內(nèi)核進(jìn)入到中斷處理程序,早期的kernel針對中斷處理程序的執(zhí)行會使用當(dāng)前中斷task的內(nèi)核棧坪哄,這里有存在一定的問題,存在棧溢出的風(fēng)險(xiǎn)模暗。舉個(gè)例子念祭,如果在中斷處理程序里又發(fā)生了異常中斷,此時(shí)會觸發(fā)double fault粱坤,但其在處理過程中依然要使用當(dāng)前task的內(nèi)核棧,并且當(dāng)前task內(nèi)核棧已滿枚驻,double fault無法被正確處理。為了解決這樣的內(nèi)部测秸,linux kernel引出了獨(dú)立的內(nèi)核棧灾常,針對SMP系統(tǒng),它還是pre-cpu的钞瀑。我們來看一下其初始化:

      void irq_ctx_init(int cpu)
      {
         union irq_ctx *irqctx;
      
         if (hardirq_ctx[cpu])
             return;
      
          // 硬中斷獨(dú)立棧
         irqctx = (union irq_ctx *)&hardirq_stack[cpu * THREAD_SIZE];
         irqctx->tinfo.task      = NULL;
         irqctx->tinfo.cpu       = cpu;
         irqctx->tinfo.preempt_count = HARDIRQ_OFFSET;
         irqctx->tinfo.addr_limit    = MAKE_MM_SEG(0);
      
         hardirq_ctx[cpu] = irqctx;
      
          //軟中斷獨(dú)立棧
         irqctx = (union irq_ctx *)&softirq_stack[cpu * THREAD_SIZE];
         irqctx->tinfo.task      = NULL;
         irqctx->tinfo.cpu       = cpu;
         irqctx->tinfo.preempt_count = 0;
         irqctx->tinfo.addr_limit    = MAKE_MM_SEG(0);
      
         softirq_ctx[cpu] = irqctx;
      
         printk("CPU %u irqstacks, hard=%p soft=%p\n",
             cpu, hardirq_ctx[cpu], softirq_ctx[cpu]);
      }
      

      可以看到還特別貼心地為softirq也開辟了單獨(dú)的棧。

  1. 在x86_64位系統(tǒng)中缠俺,還引入了一種新的棧配置:IST(Interrupt Stack Table)贷岸。目前Linux kernel中每個(gè)cpu最多支持7個(gè)IST,可以通過tss.ist[]來訪問偿警。

  2. 現(xiàn)在我們再來看idt_setup_ist_traps,其實(shí)就是重新初始化一個(gè)異常處理盒使,讓這些異常處理使用IST作為中斷棧七嫌。

    void __init idt_setup_ist_traps(void)
    {
     idt_setup_from_table(idt_table, ist_idts, ARRAY_SIZE(ist_idts), true);
    }
    
    static const __initconst struct idt_data ist_idts[] = {
     ISTG(X86_TRAP_DB,   debug,      IST_INDEX_DB),
     ISTG(X86_TRAP_NMI,  nmi,        IST_INDEX_NMI),
     ISTG(X86_TRAP_DF,   double_fault,   IST_INDEX_DF),
    #ifdef CONFIG_X86_MCE
     ISTG(X86_TRAP_MC,   &machine_check, IST_INDEX_MCE),
    #endif
    };
    
    #define ISTG(_vector, _addr, _ist)           \
     G(_vector, _addr, _ist + 1, GATE_INTERRUPT, DPL0, __KERNEL_CS)
    

    其中 IST_INDEX_DB IST_INDEX_NMI IST_INDEX_DF IST_INDEX_MCE就是要使用的ist[]的索引。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末骂维,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子航闺,更是在濱河造成了極大的恐慌猴誊,老刑警劉巖,帶你破解...
    沈念sama閱讀 218,755評論 6 507
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件懈叹,死亡現(xiàn)場離奇詭異,居然都是意外死亡胧洒,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,305評論 3 395
  • 文/潘曉璐 我一進(jìn)店門卫漫,熙熙樓的掌柜王于貴愁眉苦臉地迎上來肾砂,“玉大人,你說我怎么就攤上這事镐确。” “怎么了源葫?”我有些...
    開封第一講書人閱讀 165,138評論 0 355
  • 文/不壞的土叔 我叫張陵,是天一觀的道長嚷狞。 經(jīng)常有香客問我储矩,道長褂乍,這世上最難降的妖魔是什么持隧? 我笑而不...
    開封第一講書人閱讀 58,791評論 1 295
  • 正文 為了忘掉前任屡拨,我火速辦了婚禮,結(jié)果婚禮上呀狼,老公的妹妹穿的比我還像新娘。我一直安慰自己绝编,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,794評論 6 392
  • 文/花漫 我一把揭開白布十饥。 她就那樣靜靜地躺著逗堵,像睡著了一般。 火紅的嫁衣襯著肌膚如雪眷昆。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,631評論 1 305
  • 那天作媚,我揣著相機(jī)與錄音帅刊,去河邊找鬼。 笑死弟灼,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的田绑。 我是一名探鬼主播抡爹,決...
    沈念sama閱讀 40,362評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼欧穴!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起涮帘,我...
    開封第一講書人閱讀 39,264評論 0 276
  • 序言:老撾萬榮一對情侶失蹤调缨,失蹤者是張志新(化名)和其女友劉穎疮鲫,沒想到半個(gè)月后弦叶,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,724評論 1 315
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡燕侠,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,900評論 3 336
  • 正文 我和宋清朗相戀三年贬循,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片杖虾。...
    茶點(diǎn)故事閱讀 40,040評論 1 350
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡媒熊,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出嚷往,到底是詐尸還是另有隱情,我是刑警寧澤皮仁,帶...
    沈念sama閱讀 35,742評論 5 346
  • 正文 年R本政府宣布菲宴,位于F島的核電站,受9級特大地震影響喝峦,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜粟耻,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,364評論 3 330
  • 文/蒙蒙 一眉踱、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧谈喳,春花似錦、人聲如沸叁执。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,944評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽霹俺。三九已至泌神,卻和暖如春电爹,著一層夾襖步出監(jiān)牢的瞬間哀卫,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 33,060評論 1 270
  • 我被黑心中介騙來泰國打工此改, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留侄柔,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 48,247評論 3 371
  • 正文 我出身青樓移剪,卻偏偏與公主長得像薪者,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個(gè)殘疾皇子言津,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,979評論 2 355