16

linux学习13,进程调度

 4 years ago
source link: https://blog.popkx.com/linux%E5%AD%A6%E4%B9%A013-%E8%BF%9B%E7%A8%8B%E8%B0%83%E5%BA%A6/
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

前面两节介绍了一下 linux 中进程的资源,本节再来学习下 linux 中进程的调度。

linux 进程的时间记账

a79906c11a96b41c22030b5d24735cf8.png

上一节说到为了尽量让每个进程都有相对公平的机会运行,linux 在设计进程调度时,提出了 cpu使用比的概念,那么 linux 是如何统计每个进程的 cpu 使用比的呢?这就需要对每个进程进行时间记账了。

事实上,不仅是 linux,几乎所有调度器都需要对进程的运行时间记账。多数类 unix 系统都会分配给进程一个时间片,每当系统的时钟节拍增加时,进程的时间片就相应的减少一个节拍周期,进程的时间片减小到 0 就会被别的进程抢占。

虽然上一节说到 linux 的 cfs 调度法不再使用时间片(使用cpu使用比),但是它也仍然需要记录每一个进程的运行时间,因为 cfs 需要知道每个进程的运行状态,才能做出相对公平的调度。cfs 用于记录时间的结构体如下:

struct sched_entity { 
     struct load_weight  load;       /* for load-balancing */
     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         last_wakeup;
     u64         avg_overlap;
...
983fecfc9a5301ce796804f1693fda82.png

这是一个比较大的结构体,在之前的文章中提到 linux 中的进程资源都是记录在 struct task_struct 结构体内的,task_struct 结构体有个成员 se 就是 struct sched_entity 类型的:
1fc6759e4b86bb6081418dd3b1b76392.png

知道了每个进程的运行时间,要计算它的 cpu 使用比,还需要所有进程的运行时间总和,linux 内核是使用 vruntime 记录这一数值的,它的单位是 ns。为了让 vruntime 不与定时器相关,vruntime 实际上是所有进程运行时间经过加权处理的。

linux 的记账函数

现在知道了 linux 的时间账本记录在哪里了,那么谁负责记账呢?其实就是 update_curr() 函数了,它的代码如下,请看:

     430 static void update_curr(struct cfs_rq *cfs_rq)
-    431 {   
|    432     struct sched_entity *curr = cfs_rq->curr;
|    433     u64 now = rq_of(cfs_rq)->clock;
|    434     unsigned long delta_exec;
|    435     
|    436     if (unlikely(!curr))
|    437         return;
|    438 
|-   439     /*
||   440      * Get the amount of time the current task was running
||   441      * since the last time we changed load (this cannot
||   442      * overflow on 32 bits):
||   443      */ 
|    444     delta_exec = (unsigned long)(now - curr->exec_start);
|    445     
|    446     __update_curr(cfs_rq, curr, delta_exec);
|    447     curr->exec_start = now;
|    448 
|-   449     if (entity_is_task(curr)) {
||   450         struct task_struct *curtask = task_of(curr);
||   451 
||   452         cpuacct_charge(curtask, delta_exec);
||   453     }
|    454 }
fa0711763bd21fd11307309af5851829.png

可以看出,delta_exec 记录着进程的运行时间。它的计算方法也很简单:curr->exec_start 记录了进程的创建时间,delta_exec 就是当前时间减去创建时间的时间差。__update_curr() 函数则是负责统计 vruntime 的:
     412 static inline void
     413 __update_curr(struct cfs_rq *cfs_rq, struct sched_entity *curr,
     414           unsigned long delta_exec)
-    415 {   
|    416     unsigned long delta_exec_weighted;
|    417 
|    418     schedstat_set(curr->exec_max, max((u64)delta_exec, curr->exec_max));
|    419 
|    420     curr->sum_exec_runtime += delta_exec;
|    421     schedstat_add(cfs_rq, exec_clock, delta_exec);
|    422     delta_exec_weighted = delta_exec;
|-   423     if (unlikely(curr->load.weight != NICE_0_LOAD)) {
||   424         delta_exec_weighted = calc_delta_fair(delta_exec_weighted,
||   425                             &curr->load);
||   426     }
|    427     curr->vruntime += delta_exec_weighted;
|    428 }
e0b2e6408603c94f80aa549a5d09c1d5.png

进程调度,让每个进程都有相对公平的机会运行

现在 cfs 已经知道每个进程的执行状况了,那么挑选哪个进程投入运行呢?答案是显而易见的,cfs 为了给每个进程相对公平的机会运行,总是挑选具有最小 vruntime 的进程。

为了快速找到拥有 vruntime 的进程,linux 内核借用了红黑树的数据结构。最小的 vruntime 总是在树的最左侧叶子节点,请看如下代码:

     295 static inline struct rb_node *first_fair(struct cfs_rq *cfs_rq)
-    296 {   
|    297     return cfs_rq->rb_leftmost;
|    298 }
     299     
     300 static struct sched_entity *__pick_next_entity(struct cfs_rq *cfs_rq)
-    301 {   
|    302     return rb_entry(first_fair(cfs_rq), struct sched_entity, run_node);
|    303 }   
5d341c1420f8777f3746e10bfdb9c55f.png

看到这里,知道 linux 内核在往树中加入进程时,就应该将树处理好,这一过程由 enqueue_entity 函数完成:

     622 static void
     623 enqueue_entity(struct cfs_rq *cfs_rq, struct sched_entity *se, int wakeup)
-    624 {
|-   625     /*
||   626      * Update run-time statistics of the 'current'.
||   627      */
|    628     update_curr(cfs_rq);
|    629     account_entity_enqueue(cfs_rq, se);
|    630 
|-   631     if (wakeup) {
||   632         place_entity(cfs_rq, se, 0);
||   633         enqueue_sleeper(cfs_rq, se);
||   634     }
|    635 
|    636     update_stats_enqueue(cfs_rq, se);
|    637     check_spread(cfs_rq, se);
|    638     if (se != cfs_rq->curr)
|    639         __enqueue_entity(cfs_rq, se);
|    640 }
98dfaa60ac70a826dd899ca30e089c16.png

可以看出,enqueue_entity 函数更新了进程的运行时间,最终将数据插入树的实际上是__enqueue_entity() 函数:
     227 static void __enqueue_entity(struct cfs_rq *cfs_rq, struct sched_entity *se)
-    228 {
|    229     struct rb_node **link = &cfs_rq->tasks_timeline.rb_node;
|    230     struct rb_node *parent = NULL;
|    231     struct sched_entity *entry;
|    232     s64 key = entity_key(cfs_rq, se);
|    233     int leftmost = 1;
|    234 
|-   235     /* 
||   236      * Find the right place in the rbtree:
||   237      */ 
|-   238     while (*link) {
||   239         parent = *link;
||   240         entry = rb_entry(parent, struct sched_entity, run_node);
||-  241         /*
|||  242          * We dont care about collisions. Nodes with
|||  243          * the same key stay together.
|||  244          */
||-  245         if (key < entity_key(cfs_rq, entry)) {
|||  246             link = &parent->rb_left; 
|||  247         } else {
|||  248             link = &parent->rb_right;
|||  249             leftmost = 0;
|||  250         }
||   251     }
|    252 
|-   253     /*
||   254      * Maintain a cache of leftmost tree entries (it is frequently
||   255      * used):
||   256      */
|-   257     if (leftmost) {
||   258         cfs_rq->rb_leftmost = &se->run_node;
||-  259         /*
|||  260          * maintain cfs_rq->min_vruntime to be a monotonic increasing
|||  261          * value tracking the leftmost vruntime in the tree.
|||  262          */
||   263         cfs_rq->min_vruntime =
||   264             max_vruntime(cfs_rq->min_vruntime, se->vruntime);
||   265     }
|    266 
|    267     rb_link_node(&se->run_node, parent, link);
|    268     rb_insert_color(&se->run_node, &cfs_rq->tasks_timeline);
|    269 }
6a9cf9068347fdeb304417a822bccd9b.png

可以看出,linux 内核一直将树的最左节点维护在缓存 cfs_rq->rb_leftmost 里,这样一来,cfs 在选择哪一个进程应该被投入运行时,能够非常快速的做出决定。

至此,linux 内核中经典的 cfs 进程调度设计,就比较熟悉了。


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK