1. 程式人生 > 其它 >【第3版emWin教程】第14章 emWin6.x的2D圖形庫之基本繪圖

【第3版emWin教程】第14章 emWin6.x的2D圖形庫之基本繪圖

排程策略

在 Linux 裡面,程序大概可以分成兩種。

一種稱為實時程序,也就是需要儘快執行返回結果的那種。另一種是普通程序,大部分的程序其實都是這種。

優先順序其實就是一個數值,對於實時程序,優先順序的範圍是 0~99;對於普通程序,優先順序的範圍是 100~139數值越小,優先順序越高

從這裡可以看出,所有的實時程序都比普通程序優先順序要高。

/****** task_struct 程序排程相關 ******/
// 是否在執行佇列上
int        on_rq;
// 優先順序
int        prio;
int        static_prio;
int        normal_prio;
unsigned 
int rt_priority; // 排程器類,封裝了排程策略的執行邏輯 const struct sched_class *sched_class; // 排程實體 struct sched_entity se; // 完全公平演算法排程實體 struct sched_rt_entity rt; // 實時排程實體 struct sched_dl_entity dl; // Deadline 排程實體 // 排程策略 unsigned int policy; // 可以使用哪些CPU int nr_cpus_allowed; cpumask_t cpus_allowed;
struct sched_info sched_info; // 排程策略定義 #define SCHED_NORMAL 0 #define SCHED_FIFO 1 // 先來先服務 #define SCHED_RR 2 #define SCHED_BATCH 3 #define SCHED_IDLE 5 #define SCHED_DEADLINE 6 // 完全公平演算法排程實體 struct sched_entity { struct load_weight load; struct rb_node run_node; struct
list_head group_node; unsigned int on_rq; u64 exec_start; u64 sum_exec_runtime; u64 vruntime; u64 prev_sum_exec_runtime; u64 nr_migrations; struct sched_statistics statistics; ...... };

實時程序的排程策略:SCHED_FIFO, SCHED_RR, SCHED_DEADLINE

SCHED_FIFO 先來先服務,高優先順序的程序可以搶佔低優先順序的程序,而相同優先順序的程序,我們遵循先來先得。

SCHED_RR 輪流排程演算法,採用時間片,相同優先順序的任務當用完時間片會被放到佇列尾部,以保證公平性,而高優先順序的任務也是可以搶佔低優先順序的任務。

SCHED_DEADLINE,是按照任務的 deadline 進行排程的。當產生一個排程點的時候,DL 排程器總是選擇其 deadline 距離當前時間點最近的那個任務,並排程它執行。

普通程序的排程策略:SCHED_NORMAL、SCHED_BATCH、SCHED_IDLE

SCHED_NORMAL 是普通的程序。

SCHED_BATCH 是後臺程序,幾乎不需要和前端進行互動。

SCHED_IDLE 是特別空閒的時候才跑的程序。

sched_class 有幾種實現:

stop_sched_class 優先順序最高的任務會使用這種策略,會中斷所有其他執行緒,且不會被其他任務打斷;

dl_sched_class 對應上面的 deadline 排程策略;

rt_sched_class 對應 RR 演算法或者 FIFO 演算法的排程策略,具體排程策略由程序的 task_struct->policy 指定;

fair_sched_class 普通程序的排程策略;

idle_sched_class 空閒程序的排程策略。

每個 CPU 都有自己的 struct rq 結構,用於描述在此 CPU 上所執行的所有程序,包括一個實時程序佇列 rt_rq 和一個 CFS 執行佇列 cfs_rq在。

排程時,排程器首先會先去實時程序佇列找是否有實時程序需要執行,如果沒有才會去 CFS 執行佇列找是否有程序需要執行。

struct rq {
  /* runqueue lock: */
  raw_spinlock_t lock;
  unsigned int nr_running;
  unsigned long cpu_load[CPU_LOAD_IDX_MAX];
  ......
  struct load_weight load;
  unsigned long nr_load_updates;
  u64 nr_switches;


  struct cfs_rq cfs;
  struct rt_rq rt;
  struct dl_rq dl;
  ......
  struct task_struct *curr, *idle, *stop;
  ......
};


/* CFS-related fields in a runqueue */
struct cfs_rq {
  struct load_weight load;
  unsigned int nr_running, h_nr_running;

  u64 exec_clock;
  u64 min_vruntime;
#ifndef CONFIG_64BIT
  u64 min_vruntime_copy;
#endif
  struct rb_root tasks_timeline; // 指向紅黑樹的根節點
  struct rb_node *rb_leftmost; // 指向最左面的節點

  struct sched_entity *curr, *next, *last, *skip;
  ......
};

排程類的定義如下:

struct sched_class {
  const struct sched_class *next; // 指向下一個排程類
  
  // 向就緒佇列中新增一個程序,當某個程序進入可執行狀態時,呼叫這個函式
  void (*enqueue_task) (struct rq *rq, struct task_struct *p, int flags);
  // 將一個程序從就緒佇列中刪除
  void (*dequeue_task) (struct rq *rq, struct task_struct *p, int flags);
  void (*yield_task) (struct rq *rq);
  bool (*yield_to_task) (struct rq *rq, struct task_struct *p, bool preempt);

  void (*check_preempt_curr) (struct rq *rq, struct task_struct *p, int flags);

  // 選擇接下來要執行的程序
  struct task_struct * (*pick_next_task) (struct rq *rq,
            struct task_struct *prev,
            struct rq_flags *rf);
  // 用另一個程序代替當前執行的程序
  void (*put_prev_task) (struct rq *rq, struct task_struct *p);

  // 用於修改排程策略
  void (*set_curr_task) (struct rq *rq);
  // 每次週期性時鐘到的時候,這個函式被呼叫,可能觸發排程
  void (*task_tick) (struct rq *rq, struct task_struct *p, int queued);
  void (*task_fork) (struct task_struct *p);
  void (*task_dead) (struct task_struct *p);

  void (*switched_from) (struct rq *this_rq, struct task_struct *task);
  void (*switched_to) (struct rq *this_rq, struct task_struct *task);
  void (*prio_changed) (struct rq *this_rq, struct task_struct *task, int oldprio);
  unsigned int (*get_rr_interval) (struct rq *rq,
           struct task_struct *task);
  void (*update_curr) (struct rq *rq)
}

排程類分為下面這幾種:

extern const struct sched_class stop_sched_class;
extern const struct sched_class dl_sched_class;
extern const struct sched_class rt_sched_class;
extern const struct sched_class fair_sched_class;
extern const struct sched_class idle_sched_class;

它們其實是放在一個連結串列上的。這裡我們以排程最常見的操作,取下一個任務為例,來解析一下。

可以看到,這裡面有一個 for_each_class 迴圈,沿著上面的順序,依次呼叫每個排程類的方法。

這就說明,排程的時候是從優先順序最高的排程類到優先順序低的排程類,依次執行。而對於每種排程類,有自己的實現,例如,CFS 就有 fair_sched_class。

/*
 * Pick up the highest-prio task:
 */
static inline struct task_struct *
pick_next_task(struct rq *rq, struct task_struct *prev, struct rq_flags *rf)
{
  const struct sched_class *class;
  struct task_struct *p;
  ......
  for_each_class(class) {
    p = class->pick_next_task(rq, prev, rf);
    if (p) {
      if (unlikely(p == RETRY_TASK))
        goto again;
      return p;
    }
  }
}


const struct sched_class fair_sched_class = {
  .next      = &idle_sched_class,
  .enqueue_task    = enqueue_task_fair,
  .dequeue_task    = dequeue_task_fair,
  .yield_task    = yield_task_fair,
  .yield_to_task    = yield_to_task_fair,
  .check_preempt_curr  = check_preempt_wakeup,
  .pick_next_task    = pick_next_task_fair,
  .put_prev_task    = put_prev_task_fair,
  .set_curr_task          = set_curr_task_fair,
  .task_tick    = task_tick_fair,
  .task_fork    = task_fork_fair,
  .prio_changed    = prio_changed_fair,
  .switched_from    = switched_from_fair,
  .switched_to    = switched_to_fair,
  .get_rr_interval  = get_rr_interval_fair,
  .update_curr    = update_curr_fair,
};

對於同樣的 pick_next_task 選取下一個要執行的任務這個動作,不同的排程類有自己的實現。

fair_sched_class 的實現是 pick_next_task_fair,rt_sched_class 的實現是 pick_next_task_rt。

我們會發現這兩個函式是操作不同的佇列,pick_next_task_rt 操作的是 rt_rq,pick_next_task_fair 操作的是 cfs_rq。

static struct task_struct *
pick_next_task_rt(struct rq *rq, struct task_struct *prev, struct rq_flags *rf)
{
  struct task_struct *p;
  struct rt_rq *rt_rq = &rq->rt;
  ......
}


static struct task_struct *
pick_next_task_fair(struct rq *rq, struct task_struct *prev, struct rq_flags *rf)
{
  struct cfs_rq *cfs_rq = &rq->cfs;
  struct sched_entity *se;
  struct task_struct *p;
  ......
}

這樣整個執行的場景就串起來了,在每個 CPU 上都有一個佇列 rq,這個佇列裡面包含多個子佇列,例如 rt_rq 和 cfs_rq,不同的佇列有不同的實現方式,cfs_rq 就是用紅黑樹實現的。當有一天,某個 CPU 需要找下一個任務執行的時候,會按照優先順序依次呼叫排程類,不同的排程類操作不同的佇列。當然 rt_sched_class 先被呼叫,它會在 rt_rq 上找下一個任務,只有找不到的時候,才輪到 fair_sched_class 被呼叫,它會在 cfs_rq 上找下一個任務。這樣保證了實時任務的優先順序永遠大於普通任務。

我們重點看下 fair_sched_class 對於 pick_next_task 的實現 pick_next_task_fair,獲取下一個程序。

呼叫路徑如下:pick_next_task_fair->pick_next_entity->__pick_first_entity。

struct sched_entity *__pick_first_entity(struct cfs_rq *cfs_rq)
{
  struct rb_node *left = rb_first_cached(&cfs_rq->tasks_timeline);

  if (!left)
    return NULL;

  return rb_entry(left, struct sched_entity, run_node);
}

主動排程

舉例:從 Tap 網路裝置等待一個讀取。Tap 網路裝置是虛擬機器使用的網路裝置。當沒有資料到來的時候,它也需要等待,所以也會選擇把 CPU 讓給其他程序。

static ssize_t tap_do_read(struct tap_queue *q,
         struct iov_iter *to,
         int noblock, struct sk_buff *skb)
{
  ......
  while (1) {
    if (!noblock)
      prepare_to_wait(sk_sleep(&q->sk), &wait,
          TASK_INTERRUPTIBLE);
    ......
    /* Nothing to read, let's sleep */
    schedule();
  }
  ......
}

schedule 函式的呼叫過程

asmlinkage __visible void __sched schedule(void)
{
  struct task_struct *tsk = current;


  sched_submit_work(tsk);
  do {
    preempt_disable();
    __schedule(false);
    sched_preempt_enable_no_resched();
  } while (need_resched());
}


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

  // 1. 在當前的 CPU 上取出任務佇列 rq
  cpu = smp_processor_id();
  rq = cpu_rq(cpu);
  prev = rq->curr;
  ......
  // 2. 獲取下一個任務,即繼任
  next = pick_next_task(rq, prev, &rf);
  clear_tsk_need_resched(prev);
  clear_preempt_need_resched();
  ......
  // 3. 當選出的繼任者和前任不同,就要進行上下文切換,繼任者程序正式進入執行
  if (likely(prev != next)) {
    rq->nr_switches++;
    rq->curr = next;
    ++*switch_count;
    ......
    rq = context_switch(rq, prev, next, &rf);
    ......
}

pick_next_task 的實現如下:

static inline struct task_struct *
pick_next_task(struct rq *rq, struct task_struct *prev, struct rq_flags *rf)
{
  const struct sched_class *class;
  struct task_struct *p;
  /*
   * Optimization: we know that if all tasks are in the fair class we can call that function directly, 
   * but only if the @prev task wasn't of a higher scheduling class, 
   * because otherwise those loose the opportunity to pull in more work from other CPUs.
   */
  if (likely((prev->sched_class == &idle_sched_class ||
        prev->sched_class == &fair_sched_class) &&
       rq->nr_running == rq->cfs.h_nr_running)) {
    p = fair_sched_class.pick_next_task(rq, prev, rf);
    if (unlikely(p == RETRY_TASK))
      goto again;
    /* Assumes fair_sched_class->next == idle_sched_class */
    if (unlikely(!p))
      p = idle_sched_class.pick_next_task(rq, prev, rf);
    return p;
  }
again:
  for_each_class(class) {
    p = class->pick_next_task(rq, prev, rf);
    if (p) {
      if (unlikely(p == RETRY_TASK))
        goto again;
      return p;
    }
  }
}

again 就是依次呼叫排程類。但是這裡有了一個優化,因為大部分程序是普通程序,所以大部分情況下會呼叫上面的邏輯,呼叫的就是 fair_sched_class.pick_next_task。

根據 fair_sched_class 的定義,它呼叫的是 pick_next_task_fair。

static struct task_struct *
pick_next_task_fair(struct rq *rq, struct task_struct *prev, struct rq_flags *rf)
{
  struct cfs_rq *cfs_rq = &rq->cfs;
  struct sched_entity *se;
  struct task_struct *p;
  int new_tasks;

對於 CFS 排程類,取出相應的佇列 cfs_rq,這就是那棵紅黑樹。

    struct sched_entity *curr = cfs_rq->curr;
    if (curr) {
      if (curr->on_rq)
        update_curr(cfs_rq);
      else
        curr = NULL;
        ......
    }
    se = pick_next_entity(cfs_rq, curr);

取出當前正在執行的任務 curr,如果依然是可執行的狀態,也即處於程序就緒狀態,則呼叫 update_curr 更新 vruntime。

update_curr 會根據實際執行時間算出 vruntime 來。

接著,pick_next_entity 從紅黑樹裡面,取最左邊的一個節點。

  // 得到下一個排程實體對應的 task_struct
  p = task_of(se);

  // 如果發現繼任和前任不一樣,這就說明有一個更需要執行的程序了,就需要更新紅黑樹了。
  if (prev != p) {
    struct sched_entity *pse = &prev->se;
    ......
    // 前面前任的 vruntime 更新過了,put_prev_entity 放回紅黑樹,會找到相應的位置
    put_prev_entity(cfs_rq, pse);
    // 將繼任者設為當前任務
    set_next_entity(cfs_rq, se);
  }

  return p

程序上下文切換

上下文切換主要幹兩件事情,一是切換程序空間,也即虛擬記憶體;二是切換暫存器和 CPU 上下文。

/*
 * context_switch - switch to the new MM and the new thread's register state.
 */
static __always_inline struct rq *
context_switch(struct rq *rq, struct task_struct *prev,
         struct task_struct *next, struct rq_flags *rf)
{
  struct mm_struct *mm, *oldmm;
  ......
  mm = next->mm;
  oldmm = prev->active_mm;
  ......
  switch_mm_irqs_off(oldmm, mm, next);
  ......
  /* Here we just switch the register state and the stack. */
  switch_to(prev, next, prev);
  barrier();
  return finish_task_switch(prev);
}

switch_to 是暫存器和棧的切換,它呼叫到了 __switch_to_asm。這是一段彙編程式碼,主要用於棧的切換。

// 32 位作業系統
/*
 * %eax: prev task
 * %edx: next task
 */
ENTRY(__switch_to_asm)
  ......
  /* switch stack */
  movl  %esp, TASK_threadsp(%eax)
  movl  TASK_threadsp(%edx), %esp
  ......
  jmp  __switch_to
END(__switch_to_asm)


// 64 位作業系統
/*
 * %rdi: prev task
 * %rsi: next task
 */
ENTRY(__switch_to_asm)
  ......
  /* switch stack */
  movq  %rsp, TASK_threadsp(%rdi)
  movq  TASK_threadsp(%rsi), %rsp
  ......
  jmp  __switch_to
END(__switch_to_asm)

最終,都返回了 __switch_to 這個函式。這個函式對於 32 位和 64 位作業系統雖然有不同的實現,但裡面做的事情是差不多的。所以這裡僅僅列出 64 位作業系統做的事情。

__visible __notrace_funcgraph struct task_struct *
__switch_to(struct task_struct *prev_p, struct task_struct *next_p)
{
  struct thread_struct *prev = &prev_p->thread;
  struct thread_struct *next = &next_p->thread;
  ......
  int cpu = smp_processor_id();
  struct tss_struct *tss = &per_cpu(cpu_tss, cpu);
  ......
  load_TLS(next, cpu);
  ......
  this_cpu_write(current_task, next_p);


  /* Reload esp0 and ss1.  This changes current_thread_info(). */
  load_sp0(tss, next);
  ......
  return prev_p;
}

所謂的程序切換,就是將某個程序的 thread_struct 裡面的暫存器的值,寫入到 CPU 的 TR 指向的 tss_struct,對於 CPU 來講,這就算是完成了切換。

例如 __switch_to 中的 load_sp0,就是將下一個程序的 thread_struct 的 sp0 的值載入到 tss_struct 裡面去。