1. 程式人生 > >2018-2019-1 20189221 《Linux核心原理與分析》第九周作業

2018-2019-1 20189221 《Linux核心原理與分析》第九周作業

2018-2019-1 20189221 《Linux核心原理與分析》第九周作業

實驗八 理理解程序排程時機跟蹤分析程序排程與程序切換的過程

程序排程

進度排程時機:

1、中斷處理過程(包括時鐘中斷、I/O中斷、系統呼叫和異常)中,直接呼叫schedule(),或者返回使用者態時根據need_resched標記呼叫schedule();

2、核心執行緒可以直接呼叫schedule()進行程序切換,也可以在中斷處理過程中進行排程,也就是說核心執行緒作為一類的特殊的程序可以主動排程,也可以被動排程;

3、使用者態程序無法實現主動排程,僅能通過陷入核心態後的某個時機點進行排程,即在中斷處理過程中進行排程。

程序切換

為了控制程序的執行,核心必須有能力掛起正在CPU上執行的程序,並恢復以前掛起的某個程序的執行。這種行為被稱為程序切換(process switch)、任務切換(task switch)或上下文切換(context switch)。

掛起正在CPU上執行的程序,與中斷時儲存現場是不同的,中斷前後是在同一個程序上下文中,只是由使用者態轉向核心態執行。

程序上下文

包含了程序執行需要的所有資訊,包括:

1、使用者地址空間:包括程式程式碼,資料,使用者堆疊等

2、控制資訊:程序描述符,核心堆疊等

3、硬體上下文(與中斷儲存硬體上下文的方法不同)

最一般的情況

正在執行的使用者態程序X切換到執行使用者態程序Y的過程

  • 正在執行的使用者態程序X
  • 發生中斷——save cs:eip/esp/eflags(current) to kernel stack,then load cs:eip(entry of a specific ISR) and ss:esp(point to kernel stack).SAVE_ALL //儲存現場
  • 中斷處理過程中或中斷返回前呼叫了schedule(),其中的switch_to做了關鍵的程序上下文切換
  • 標號1之後開始執行使用者態程序Y(這裡Y曾經通過以上步驟被切換出去過因此可以從標號1繼續執行)
  • restore_all //恢復現場
  • iret - pop cs:eip/ss:esp/eflags from kernel stack
  • 繼續執行使用者態程序Y

幾種特殊情況

  • 通過中斷處理過程中的排程時機,使用者態程序與核心執行緒之間互相切換和核心執行緒之間互相切換,與最一般的情況非常類似,只是核心執行緒執行過程中發生中斷沒有程序使用者態和核心態的轉換;
  • 核心執行緒主動呼叫schedule(),只有程序上下文的切換,沒有發生中斷上下文的切換,與最一般的情況略簡略;
  • 建立子程序的系統呼叫在子程序中的執行起點及返回使用者態,如fork;
  • 載入一個新的可執行程式後返回到使用者態的情況,如execve;

schedule()函式

schedule()函式程式碼:

static void __sched __schedule(void)
{
  struct task_struct *prev, *next;
  unsigned long *switch_count;
  struct rq *rq;
  int cpu;

need_resched:
  preempt_disable();
  cpu = smp_processor_id();
  rq = cpu_rq(cpu);
  rcu_note_context_switch(cpu);
  prev = rq->curr;

  schedule_debug(prev);

  if (sched_feat(HRTICK))
    hrtick_clear(rq);

  /*
   * Make sure that signal_pending_state()->signal_pending() below
   * can't be reordered with __set_current_state(TASK_INTERRUPTIBLE)
   * done by the caller to avoid the race with signal_wake_up().
   */
  smp_mb__before_spinlock();
  raw_spin_lock_irq(&rq->lock);

  switch_count = &prev->nivcsw;
  if (prev->state && !(preempt_count() & PREEMPT_ACTIVE)) {
    if (unlikely(signal_pending_state(prev->state, prev))) {
      prev->state = TASK_RUNNING;
    } else {
      deactivate_task(rq, prev, DEQUEUE_SLEEP);
      prev->on_rq = 0;

      /*
       * If a worker went to sleep, notify and ask workqueue
       * whether it wants to wake up a task to maintain
       * concurrency.
       */
      if (prev->flags & PF_WQ_WORKER) {
        struct task_struct *to_wakeup;

        to_wakeup = wq_worker_sleeping(prev, cpu);
        if (to_wakeup)
          try_to_wake_up_local(to_wakeup);
      }
    }
    switch_count = &prev->nvcsw;
  }

  if (task_on_rq_queued(prev) || rq->skip_clock_update < 0)
    update_rq_clock(rq);

  next = pick_next_task(rq, prev);
  clear_tsk_need_resched(prev);
  clear_preempt_need_resched();
  rq->skip_clock_update = 0;

  if (likely(prev != next)) {
    rq->nr_switches++;
    rq->curr = next;
    ++*switch_count;

    context_switch(rq, prev, next); /* unlocks the rq */
    /*
     * The context switch have flipped the stack from under us
     * and restored the local variables which were saved when
     * this task called schedule() in the past. prev == current
     * is still correct, but it can be moved to another cpu/rq.
     */
    cpu = smp_processor_id();
    rq = cpu_rq(cpu);
  } else
    raw_spin_unlock_irq(&rq->lock);

  post_schedule(rq);

  sched_preempt_enable_no_resched();
  if (need_resched())
    goto need_resched;
}

gdbb跟蹤分析

和前幾次實驗一樣編譯核心,開啟gdb:

使用gdb跟蹤schedule()函式,設定斷點:

(schedule(),context switch(),pick_next_task(),switch_to)

其中switch_to是巨集定義,不能設定斷點。
schedule()斷點:

尋找下一個程序及判斷上下文切換:

schedule 函式

  • 針對搶佔的處理
  • raw_spin_lock_irq(&rq->lock);
  • 檢查prev的狀態,並且重設state的狀態
  • next = pick_next_task(rq, prev); ////程序排程演算法都封裝這個函式內部
  • 更新就緒佇列的時鐘
  • context_switch(rq, prev, next); //程序上下文切換

schedule()函式選擇一個新的程序來執行,並呼叫context_switch進行上下文的切換,這個巨集呼叫switch_to來進行關鍵上下文切換switch_to利用了prev和next兩個引數:prev指向當前程序,next指向被排程的程序

switch_to中的彙編程式碼

#define switch_to(prev, next, last)                    
do {                                 

 unsigned long ebx, ecx, edx, esi, edi;              

 asm volatile("pushfl\n\t"      /* 儲存當前程序的flags */   
           "pushl %%ebp\n\t"        /* 把當前程序的當前的ebp壓入當前程序的棧   */ 
           "movl %%esp,%[prev_sp]\n\t"  /*儲存當前的esp到prev->thread.sp指向的記憶體中   */ 
           "movl %[next_sp],%%esp\n\t"  /* 重置esp,把下個程序的next->thread.sp賦予esp */ 
           "movl $1f,%[prev_ip]\n\t"    /*把1f放到[prev_ip]裡,儲存當前程序的EIP,當恢復prev程序時可從這裡恢復*/
           "pushl %[next_ip]\n\t"      /把next程序的起點,即ip的位置壓到堆疊中,next_ip一般是$1f*/  
           __switch_canary                   
           "jmp __switch_to\n" 
           "1:\t"                        
          "popl %%ebp\n\t"     /* 重置ebp  */    
           "popfl\n"         /* 重置flags*/  

           : [prev_sp] "=m" (prev->thread.sp),   
   /* =m 表示把變數放入記憶體,即把 [prev_sp] 儲存的變數放入記憶體,最後再寫入prev->thread.sp */
             [prev_ip] "=m" (prev->thread.ip),        
             "=a" (last),                 

         /*=a 表示把變數 last 放入 ax, eax = last */   
             "=b" (ebx), "=c" (ecx), "=d" (edx),    
         /* b 表示放入ebx, c 表示放入 ecx,d 表示放入 edx, S表示放入 si, D 表示放入 edi */  
             "=S" (esi), "=D" (edi)             

             __switch_canary_oparam                

           : [next_sp]  "m" (next->thread.sp),   /* next->thread.sp 放入記憶體中的 [next_sp] */        
             [next_ip]  "m" (next->thread.ip),       


             [prev]     "a" (prev),              
             [next]     "d" (next)               

             __switch_canary_iparam                

          "memory");                  
} while (0)

switch_to 從A程序切換到B程序的步驟如下:

eax <== prev_A
edx <== next_A

複製兩個變數到暫存器([prev]"a" (prev);[next]"d" (next))

pushfl /*將狀態暫存器eflags壓棧*/
pushl %ebp

儲存程序A的ebp和eflags(因為現在esp還在A的堆疊中,所以ebp和eflags被儲存到A程序的核心堆疊中)

movl%%esp, %[prev_sp]\n\t

儲存當前esp到A程序核心描述符中(prev_A->thread.sp<== esp_A)
在呼叫switch_to時,prev是指向A程序自己的程序描述符的。

movl %[next_sp], %%esp\n\t

從next(程序B)的描述符中取出之前從B切換出去時儲存的esp_B(esp_B <==next_A->thread.sp)
在A程序中的next是指向B的程序描述符的。從這個時候開始,CPU當前執行的程序已經是B程序了,因為esp已經指向B的核心堆疊。但是,現在的ebp仍然指向A程序的核心堆疊,所以所有區域性變數仍然是A中的區域性變數,比如next實質上是%n(%ebp_A),也就是next_A,即指向B的程序描述符。

movl $1f, %[prev_ip]\n\t

把標號為1的指令地址儲存到A程序描述符的ip域(prev_A->thread.ip<== %1f)
當A程序下次從switch_to回來時,會從這條指令開始執行。具體方法要看後面被切換回來的B的下一條指令。:

pushl %[next_ip]\n\t
jmp switch_to\n 

將返回地址儲存到堆疊,然後呼叫switch_to()函式,switch_to()函式完成硬體上下文切換。
注意,如果之前B也被switch_to出去過,那麼[next_ip]裡存的就是下面這個1f的標號,但如果程序B剛剛被建立,之前沒有被switch_to出去過,那麼[next_ip]裡存的將是ret_ftom_fork(參看copy_thread()函式)。

popl %%ebp\n\t  
popfl\n

從switch_to()返回後繼續從1:標號後面開始執行,修改ebp到B的核心堆疊,恢復B的eflags。如果從switch_to()返回後從這裡繼續執行,那麼說明在此之前B肯定被switch_to調出過,因此此前肯定備份了ebp_B和flags_B,這裡執行恢復操作。

此時ebp已經指向了B的核心堆疊,所以上面的prev,next等區域性變數已經不是A程序堆疊中的了,而是B程序堆疊中的(B上次被切換出去之前也有這兩個變數,所以代表著B堆疊中prev、next的值了),因為prev==%p(%ebp_B),而在B上次被切換出去之前,該位置儲存的是B程序的描述符地址。如果這個時候就結束switch_to的話,在後面的程式碼中(即context_switch()函式中switch_to之後的程式碼)的prev變數是指向B程序的,因此,程序B就不知道是從哪個程序切換回來。而從context_switch()中switch_to之後的程式碼中,我們看到finish_task_switch(this_rq(),prev)中需要知道之前是從哪個程序切換過來的,因此,我們必須想辦法儲存A程序的描述符到B的堆疊中,這就是last的作用。

"=a"(last)

將eax寫入last,以在B的堆疊中儲存正確的prev資訊(即last_B <== %eax)

從context_switch()中看到的呼叫switch_to的方法是:switch_to(prev,next, prev),這裡面的last實質上就是prev,因此在switch_to巨集執行完之後,prev_B就是正確的A的程序描述符了。這裡,last的作用相當於把程序A堆疊中的A程序描述符地址複製到了程序B的堆疊中。

至此,switch_to已經執行完成,A停止執行,而開始執行B。此後,可能在某一次排程中,程序A得到排程,就會出現switch_to(C,A)這樣的呼叫,這時,A再次得到排程,得到排程後,A程序從context_switch()中switch_to後面的程式碼開始執行,這時候,它看到的prev_A將指向C的程序描述符。