1. 程式人生 > >Linux核心入門——使用者態向核心態切換

Linux核心入門——使用者態向核心態切換

除了使用者資料段、使用者程式碼段、核心資料段、核心程式碼段這4個段以外,Linux還使用了其它幾個專門的段,下面我們專門來探討,如圖:在單處理器系統中只有一個GDT,而在多處理器系統中每個CPU對應一個GDT。所有的GDT都存放在cpu_gdt_table 陣列中,而所有GDT(當初始化gdtr 暫存器時使用)的地址和它們的大小存放在cpu_gdt_descr陣列中,這些符號都在檔案arch/i386/kernel/head.S中被定義。

我們再把這個知識擴充套件一下,80x86體系的286以後出現了一個新段,叫做任務狀態段(TSS),主要用來儲存處理器中各個暫存器的內容。Linux為每個處理器都有一個相應的TSS相關的資料結構,每個TSS相應的線性地址空間都是核心資料段相應線性地址空間的一個小子集。所有的任務狀態段都順序地存放在init_tss陣列中;值得特別說明的是,第n個CPU的TSS描述符的Base欄位指向init_tss陣列的第n個元素。G(粒度)標誌被清0,而Limit欄位置為0xeb,因為TSS段是236位元組長。Type欄位置為9或11(可用的32位TSS),且DPL置為0,因為不允許使用者態下的程序訪問TSS段。

好了,回到剛才的問題,我們談到了,使用者程序要想訪問核心提供的資料結構和函式時,須進行切換,即由使用者態轉向核心態,那麼核心棧的地址從何而來?

於是乎,當程序由使用者態進入核心態時,必發生中斷,因為核心態的CPL優先順序高,所以要進行棧的切換。那麼就會讀tr暫存器以訪問該程序(現在還是使用者態)的TSS段。隨後用TSS中核心態堆疊段ss0和棧指標esp0裝載SS和esp暫存器,這樣就實現了使用者棧到核心棧的切換了

。同時,核心用一組mov指令儲存所有暫存器到核心態堆疊上,這也包括使用者態中ss和esp這對暫存器的內容。

中斷或異常處理結束時,CPU控制單元執行iret命令,重新讀取棧中的暫存器內容來更新各個CPU暫存器,以重新開始執行使用者態程序,此時將會根據棧中內容恢復應用態程序的狀態。
這裡還要強調一下,TSS只有一個(如果是多CPU架構,則每個CPU一個),其ss和esp儲存在TSS結構中,不允許使用者態程序訪問,Linux描述TSS的格式的資料結構是tss_struct:

struct tss_struct {
    unsigned short    back_link,__blh;
   unsigned long    esp0;
    unsigned short    ss0,__ss0h;

    unsigned long    esp1;
    unsigned short    ss1,__ss1h;    /* ss1 is used to cache MSR_IA32_SYSENTER_CS */
    unsigned long    esp2;
    unsigned short    ss2,__ss2h;
    unsigned long    __cr3;
    unsigned long    eip;
    unsigned long    eflags;
    unsigned long    eax,ecx,edx,ebx;
    unsigned long    esp;

    unsigned long    ebp;
    unsigned long    esi;
    unsigned long    edi;
    unsigned short    es, __esh;
    unsigned short    cs, __csh;
    unsigned short    ss, __ssh;
    unsigned short    ds, __dsh;
    unsigned short    fs, __fsh;
    unsigned short    gs, __gsh;
    unsigned short    ldt, __ldth;
    unsigned short    trace, io_bitmap_base;
    /*
     * The extra 1 is there because the CPU will access an
     * additional byte beyond the end of the IO permission
      * bitmap. The extra byte must be all 1 bits, and must
     * be within the limit.
     */

    unsigned long    io_bitmap[IO_BITMAP_LONGS + 1];
    /*
     * Cache the current maximum and the last task that used the bitmap:
     */

    unsigned long io_bitmap_max;
    struct thread_struct *io_bitmap_owner;
    /*
     * pads the TSS to be cacheline-aligned (size is 0x100)
     */

    unsigned long __cacheline_filler[35];
    /*
     * .. and then another 0x100 bytes for emergency kernel stack
     */

    unsigned long stack[64];
} __attribute__((packed));


這就是TSS段的全部內容,不多。每次切換時,核心都更新TSS的某些欄位以便想要的CPU控制單元可以安全地檢索到它需要的資訊,這也是Linux安全性的體現之一。所以,TSS只是反映了CPU上當前程序的特性級別,沒有必要執行的程序保留TSS。

linux2.4之前的核心有程序最大數的限制,受限制的原因是,每一個程序都有自已的TSS和LDT,而TSS(任務描述符)和LDT(私有描述符)必須放在GDT中,GDT最大隻能存放8192個描述符,除掉系統用的12描述符之外,最大程序數=(8192-12)/2, 總共4090個程序。

從Linux2.4以後,全部程序使用同一個TSS,準確的說是,每個CPU一個TSS,在同一個CPU上的程序使用同一個TSS。TSS的定義在asm-i386/processer.h中,定義如下:

extern struct tss_struct init_tss[NR_CPUS];

在start_kernel()->trap_init()->cpu_init()初始化並載入TSS:

void __init cpu_init (void)
{
  int nr = smp_processor_id();    //獲取當前cpu

  struct tss_struct * t = &init_tss[nr];     //當前cpu使用的tss

  t->esp0 = current->thread.esp0;            //把TSS中esp0更新為當前程序的esp0
  set_tss_desc(nr,t);
  gdt_table[__TSS(nr)].b &= 0xfffffdff;
  load_TR(nr);                               //載入TSS
  load_LDT(&init_mm.context);                //載入LDT
}


我們知道,任務切換(硬切換)需要用到TSS來儲存全部暫存器(2.4以前使用jmp來實現切換),中斷髮生時也需要從TSS中讀取ring0的esp0,那麼,程序使用相同的TSS,任務切換怎麼辦?

其實2.4以後不再使用硬切換,而是使用軟切換,暫存器不再儲存在TSS中了,而是儲存在task->thread中,只用TSS的esp0和IO許可點陣圖,所以,在程序切換過程中,只需要更新TSS中的esp0、io_bitmap,程式碼在sched.c中:

schedule()->switch_to()->__switch_to(),

void fastcall __switch_to(struct task_struct *prev_p, struct task_struct *next_p)
{
  struct thread_struct *prev = &prev_p->thread,
  *next = &next_p->thread;
  struct tss_struct *tss = init_tss + smp_processor_id(); //當前cpu的TSS

 /*
   * Reload esp0, LDT and the page table pointer:
   */
   ttss->esp0 = next->esp0; //用下一個程序的esp0更新tss->esp0


  //拷貝下一個程序的io_bitmap到tss->io_bitmap
  if (prev->ioperm || next->ioperm) {
  if (next->ioperm) {
   /*
    * 4 cachelines copy ... not good, but not that
    * bad either. Anyone got something better?
    * This only affects processes which use ioperm().
    * [Putting the TSSs into 4k-tlb mapped regions
    * and playing VM tricks to switch the IO bitmap
    * is not really acceptable.]
    */

   memcpy(tss->io_bitmap, next->io_bitmap,
     IO_BITMAP_BYTES);
   tss->bitmap = IO_BITMAP_OFFSET;
  } else
   /*
    * a bitmap offset pointing outside of the TSS limit
    * causes a nicely controllable SIGSEGV if a process
    * tries to use a port IO instruction. The first
    * sys_ioperm() call sets up the bitmap properly.
    */

   tss->bitmap = INVALID_IO_BITMAP_OFFSET;
  }
}