Linux中斷一網打盡(2) - IDT及中斷處理的實現

Linux中斷一網打盡(1) - IDT及中斷處理的實現

通過閱讀本文您可以了解到:

  • IDT是什么 ;
  • IDT如何被初始化;
  • 什么是門;
  • 傳統系統調用是如何實現的;
  • 硬件中斷的實現;
如何設置IDT
IDT 中斷描述符表定義

中斷描述符表簡單來說說是定義了發生中斷/異常時,CPU按這張表中定義的行為來處理對應的中斷/異常。

#define IDT_ENTRIES         256
gate_desc idt_table[IDT_ENTRIES] __page_aligned_bss;

從上面我們可以知道,其包含了256項,它是一個gate_desc的數據,其下標0-256就表示中斷向量,gate_desc我們在下面馬上介紹。

中斷描述符項定義
  • 當中斷發生,cpu獲取到中斷向量后,查找IDT中斷描述符表得到相應的中斷描述符,再根據中斷描述符記錄的信息來作權限判斷,運行級別轉換,最終調用相應的中斷處理程序;

  • 這里涉及到Linux kernel的分段式內存管理,我們這里不詳細展開,有興趣的同學可以自行學習。如下簡述之:

    1. 我們知道CPU只認識邏輯地址,邏輯地址經分段處理轉換成線性地址,線性地址經分頁處理最終轉換成物理地址,這樣就可以從內存中讀取了;

    2. 邏輯地址你可以簡單認為就是CPU執行代碼時從CS(代碼段寄存器) : IP (指令計數寄存器)中加載的代碼,實際上通過CS可以得到邏輯地址的基地址,再加上IP這個相對于基地址的偏移量,就得到真正的邏輯地址;

    3. CS寄存器16位,它不會包含真正的基地址,它一般被稱為段選擇子,包括一個index索引,指向GDTLDT的一項;一個指示位,指示index索引是屬于GDT還是LDT; 還有CPL, 表明當前代碼運行權限;

    4. GDT: 全局描述符表,每一項記錄著相應的段基址,段大小,段的訪問權限DPL等,到這里終于可以獲取到段基地址了,再加上之前IP寄存器里存放的偏移量,真正的邏輯地址就有了。

    5. 附上簡圖:


      idt2.jpg
  • 我們先看中斷描述符的定義:

    struct gate_struct {
      u16     offset_low;
      u16     segment;
      struct idt_bits bits;
      u16     offset_middle;
    #ifdef CONFIG_X86_64
      u32     offset_high;
      u32     reserved;
    #endif
    } __attribute__((packed));
    

    其中:

    1. offset_high,offset_middleoffset_low合起來就是中斷處理函數地址的偏移量;

    2. segment就是相應的段選擇子,根據它在GDT中查找可以最終獲取到段基地址;

    3. bits是該中斷描述符的一些屬性值:

      struct idt_bits {
         u16     ist : 3,
                 zero    : 5,
                 type    : 5,
                 dpl : 2,
                 p   : 1;
      } __attribute__((packed));
      

      ist表示此中斷處理函數是使用pre-cpu的中斷棧,還是使用IST的中斷棧;

      type表示所中斷是何種類型,目前有以下四種:

      enum {
         GATE_INTERRUPT = 0xE, //中斷門
         GATE_TRAP = 0xF, // 陷入門
         GATE_CALL = 0xC, // 調用門
         GATE_TASK = 0x5, // 任務門
      };
      

      的概念這里主要用作權限控制,我們從一個區域進到另一個區域需要通過一扇門,有門禁權限才可以通過,因此 dpl就是這個權限,實際中我們一般稱為RPL;

      我們后面會通過一個例子來講一下CPL,RPLDPL三者之間的關系。

IDT 中斷描述符表本身的存儲

IDT 中斷描述符表的物理地址存儲在IDTR寄存器中,這個寄存器存儲了IDT的基地址和長度。查詢時,從 IDTR 拿到 base address ,加上向量號 * IDT entry size,即可以定位到對應的表項(gate)。

idt1.jpg
設置IDT
  • 設置中斷門類型的IDT描述符

    static void set_intr_gate(unsigned int n, const void *addr)
    {
      struct idt_data data;
    
      BUG_ON(n > 0xFF);
    
      memset(&data, 0, sizeof(data));
      data.vector = n; // 中斷向量
      data.addr   = addr; // 中斷處理函數的地址
      data.segment    = __KERNEL_CS; // 段選擇子
      data.bits.type  = GATE_INTERRUPT; // 類型
      data.bits.p = 1;
    
      idt_setup_from_table(idt_table, &data, 1, false);
    }
    

    上面的函數主要是填充好idt_data,然后調用idt_setup_from_table;

  • idt_setup_from_table:

    static void
    idt_setup_from_table(gate_desc *idt, const struct idt_data *t, int size, bool sys)
    {
      gate_desc desc;
    
      for (; size > 0; t++, size--) {
          idt_init_desc(&desc, t);
          write_idt_entry(idt, t->vector, &desc);
          if (sys)
              set_bit(t->vector, system_vectors);
      }
    }
    

    首先使用 idt_data結構來填充中斷描述符變量idt_init_desc, 然后將這個中斷描述符變量copy進idt_table

    看,就是這么簡單~~~

  • gate_desc的多種初始化方法

    因為gate_desc是通過ida_dat填充的,所以這里關鍵是idt_data的初始化,我們詳細看一下:

    /* Interrupt gate 
    中斷門,DPL = 0
    只能從內核調用
    */
    #define INTG(_vector, _addr)              \
      G(_vector, _addr, DEFAULT_STACK, GATE_INTERRUPT, DPL0, __KERNEL_CS)
    
    /* System interrupt gate
    系統中斷門,DPL = 3
    可以從用戶態調用,比如系統調用
    */
    #define SYSG(_vector, _addr)              \
      G(_vector, _addr, DEFAULT_STACK, GATE_INTERRUPT, DPL3, __KERNEL_CS)
    
    /*
     * Interrupt gate with interrupt stack. The _ist index is the index in
     * the tss.ist[] array, but for the descriptor it needs to start at 1.
     中斷門, DPL = 0
     只能從內核態調用,使用TSS.IST[]作為中斷棧 
     */
    #define ISTG(_vector, _addr, _ist)            \
      G(_vector, _addr, _ist + 1, GATE_INTERRUPT, DPL0, __KERNEL_CS)
    
    /* Task gate
    任務門, DPL = 0
    只能作內核態調用 
    */
    #define TSKG(_vector, _gdt)               \
      G(_vector, NULL, DEFAULT_STACK, GATE_TASK, DPL0, _gdt << 3)
    
    

    我們再來看下G這個宏的實現:

    #define G(_vector, _addr, _ist, _type, _dpl, _segment)    \
      {                       \
          .vector     = _vector,      \
          .bits.ist   = _ist,         \
          .bits.type  = _type,        \
          .bits.dpl   = _dpl,         \
          .bits.p     = 1,            \
          .addr       = _addr,        \
          .segment    = _segment,     \
      }
    

    實際上就是填充idt_data的各個字段。

傳統系統調用的實現

這里所說的傳統系統調用主要指舊的32位系統使用 int 0x80軟件中斷來進入內核態,實現的系統調用。因為這種傳統系統調用方式需要進入內核后作權限驗證,還要切換內核棧后作大量壓棧方式,調用結束后清理棧作恢復,兩個字太慢,后來CPU從硬件上支持快速系統調用sysenter/sysexit, 再后來又發展到syscall/sysret, 這兩種都不需要通過中斷方式進入內核態,而是直接轉換到內核態,速度快了很多。

傳統系統調用相關 IDT 的設置
  • Linux系統啟動過程中內核壓解后最終都調用到start_kernel, 在這里會調用trap_init, 然后又會調用idt_setup_traps:

    void __init idt_setup_traps(void)
    {
      idt_setup_from_table(idt_table, def_idts, ARRAY_SIZE(def_idts), true);
    }
    

    我們來看這里的def_idts的定義:

    static const __initconst struct idt_data def_idts[] = {
      ....
    #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
    };
    

? 上面的SYSG(IA32_SYSCALL_VECTOR, entry_INT80_32)就是設置系統調用的異常中斷處理程序,其中 #define IA32_SYSCALL_VECTOR 0x80

再看一下SYSG的定義:

#define SYSG(_vector, _addr)                \
    G(_vector, _addr, DEFAULT_STACK, GATE_INTERRUPT, DPL3, __KERNEL_CS)

它初始化一個中斷門,權限是DPL3, 因此從用戶態是允許發起系統調用的。

  • 我們調用系統調用,不大可能自已手寫匯編代碼,都是通過glibc來調用,基本流程是保存參數到寄存器,然后保存系統調用向量號到eax寄存器,然后調用int 0x80進入內核態,切換到內核棧,將用戶態時的ss/sp/eflags/cs/ip/error code依次壓入內核棧。

  • entry_INT80_32系統調用對應的中斷處理程序

    ENTRY(entry_INT80_32)
      ASM_CLAC
      pushl   %eax            /* pt_regs->orig_ax */
    
      SAVE_ALL pt_regs_ax=$-ENOSYS switch_stacks=1    /* save rest */
    
      TRACE_IRQS_OFF
    
      movl    %esp, %eax
      call    do_int80_syscall_32
    .Lsyscall_32_done:
    ...
    .Lirq_return:
    
      INTERRUPT_RETURN
    
    ...
    ENDPROC(entry_INT80_32)
    

    我們略去了中間的一些細節部分,可以看到首先將中斷向量號壓棧,再保存所有當前的寄存器值到pt_regs, 保存當前棧指針到%eax寄存器,最后再調用 do_int80_syscall_32, 這個函數中就會執行具體的中斷處理,然后INTERRUPT_RETURN恢復棧,作好返回用戶態的準備。

  • do_int80_syscall_32調用 do_syscall_32_irqs_on,我們看一下其實現:

static __always_inline void do_syscall_32_irqs_on(struct pt_regs *regs)
  {
    struct thread_info *ti = current_thread_info();
    unsigned int nr = (unsigned int)regs->orig_ax;
  
  #ifdef CONFIG_IA32_EMULATION
    ti->status |= TS_COMPAT;
  #endif
  
    if (READ_ONCE(ti->flags) & _TIF_WORK_SYSCALL_ENTRY) {
        nr = syscall_trace_enter(regs);
    }
  
    if (likely(nr < IA32_NR_syscalls)) {
        nr = array_index_nospec(nr, IA32_NR_syscalls);
  #ifdef CONFIG_IA32_EMULATION
        regs->ax = ia32_sys_call_table[nr](regs);
  #else
        regs->ax = ia32_sys_call_table[nr](
            (unsigned int)regs->bx, (unsigned int)regs->cx,
            (unsigned int)regs->dx, (unsigned int)regs->si,
            (unsigned int)regs->di, (unsigned int)regs->bp);
  #endif /* CONFIG_IA32_EMULATION */
    }
  
    syscall_return_slowpath(regs);
  }

通過中斷向量號nria32_sys_call_table中斷向量表中索引到具體的中斷處理函數然后調用之,其結果最終合存入%eax寄存器。

一圖以蔽之
idt3.jpg
硬件中斷的實現
硬件中斷的IDT初始化和調用流程

這里我們不講解具體的代碼細節,只關注流程 。

硬件中斷相關IDT的初始化也是在Linux啟動時完成,在start_kernel中通過調用init_IRQ完成,我們來看一下:

void __init init_IRQ(void)
{
    int i;
    for (i = 0; i < nr_legacy_irqs(); i++)
        per_cpu(vector_irq, 0)[ISA_IRQ_VECTOR(i)] = irq_to_desc(i);

    BUG_ON(irq_init_percpu_irqstack(smp_processor_id()));

    x86_init.irqs.intr_init(); // 即調用  native_init_IRQ
}

void __init native_init_IRQ(void)
{
    /* Execute any quirks before the call gates are initialised: */
    x86_init.irqs.pre_vector_init();

    idt_setup_apic_and_irq_gates();
    lapic_assign_system_vectors();

    if (!acpi_ioapic && !of_ioapic && nr_legacy_irqs())
        setup_irq(2, &irq2);
}

重點在于idt_setup_apic_and_irq_gates:

 */
void __init idt_setup_apic_and_irq_gates(void)
{
    int i = FIRST_EXTERNAL_VECTOR;
    void *entry;

    idt_setup_from_table(idt_table, apic_idts, ARRAY_SIZE(apic_idts), true);

    for_each_clear_bit_from(i, system_vectors, FIRST_SYSTEM_VECTOR) {
        entry = irq_entries_start + 8 * (i - FIRST_EXTERNAL_VECTOR);
        set_intr_gate(i, entry);
    }
}

其中的set_intr_gate用來初始化硬件相關的調用門,其對應的中斷門處理函數在irq_entries_start中定義,它位于arch/x86/entry/entry_64.S中:

    .align 8
ENTRY(irq_entries_start)
    vector=FIRST_EXTERNAL_VECTOR
    .rept (FIRST_SYSTEM_VECTOR - FIRST_EXTERNAL_VECTOR)
    UNWIND_HINT_IRET_REGS
    pushq   $(~vector+0x80)         /* Note: always in signed byte range */
    jmp common_interrupt
    .align  8
    vector=vector+1
    .endr
END(irq_entries_start)

這段匯編實現對不大熟悉匯編的同學可能看起來有點暈,其實很簡單它相當于填充一個中斷處理函數的數組,填充多少次呢? (FIRST_SYSTEM_VECTOR - FIRST_EXTERNAL_VECTOR)這就是次數,數組的每一項都是一個函數:

    UNWIND_HINT_IRET_REGS
    pushq   $(~vector+0x80)         /* Note: always in signed byte range */
    jmp common_interrupt

即先將中斷號壓棧,然后跳轉到common_interrupt執行,可以看到這個common_interrupt是硬件中斷的通用處理函數,它里面最主要的就是調用do_IRQ:

__visible unsigned int __irq_entry do_IRQ(struct pt_regs *regs)
{
    struct pt_regs *old_regs = set_irq_regs(regs);
    struct irq_desc * desc;
    /* high bit used in ret_from_ code  */
    unsigned vector = ~regs->orig_ax;

    entering_irq();

    /* entering_irq() tells RCU that we're not quiescent.  Check it. */
    RCU_LOCKDEP_WARN(!rcu_is_watching(), "IRQ failed to wake up RCU");

    desc = __this_cpu_read(vector_irq[vector]);
    if (likely(!IS_ERR_OR_NULL(desc))) {
        if (IS_ENABLED(CONFIG_X86_32))
            handle_irq(desc, regs);
        else
            generic_handle_irq_desc(desc);
    } else {
        ack_APIC_irq();

        if (desc == VECTOR_UNUSED) {
            pr_emerg_ratelimited("%s: %d.%d No irq handler for vector\n",
                         __func__, smp_processor_id(),
                         vector);
        } else {
            __this_cpu_write(vector_irq[vector], VECTOR_UNUSED);
        }
    }

    exiting_irq();

    set_irq_regs(old_regs);
    return 1;
}

首先根據中斷向量號獲取到對應的中斷描述符irq_desc, 然后調用generic_handle_irq來處理:

static inline void generic_handle_irq_desc(struct irq_desc *desc)
{
    desc->handle_irq(desc);
}

這里最終會調用到中斷描述符的handle_irq,因此另一個重點就是這個中斷描述符的設置了,它可以單開一篇文章來講,我們暫不詳述了。

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 230,825評論 6 546
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 99,814評論 3 429
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事?!?“怎么了?”我有些...
    開封第一講書人閱讀 178,980評論 0 384
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 64,064評論 1 319
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 72,779評論 6 414
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 56,109評論 1 330
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 44,099評論 3 450
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 43,287評論 0 291
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 49,799評論 1 338
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 41,515評論 3 361
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,750評論 1 375
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 39,221評論 5 365
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,933評論 3 351
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 35,327評論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 36,667評論 1 296
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 52,492評論 3 400
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 48,703評論 2 380

推薦閱讀更多精彩內容