栏目分类:
子分类:
返回
名师互学网用户登录
快速导航关闭
当前搜索
当前分类
子分类
实用工具
热门搜索
名师互学网 > IT > 系统运维 > 运维 > Linux

linux内核设计核实现第四章---进程调度笔记

Linux 更新时间: 发布时间: IT归档 最新发布 模块sitemap 名妆网 法律咨询 聚返吧 英语巴士网 伯小乐 网商动力

linux内核设计核实现第四章---进程调度笔记

参考网址:

Linux CFS调度器之队列操作--Linux进程的管理与调度(二十七) - 云+社区 - 腾讯云

4.1多任务

多任务处理器上,能让多个进程处于阻塞或者睡眠状态,实际上不被调度,直到被唤醒

多任务操作系统分为抢占和非抢占两种多任务方式,linux是公平调度。

非抢占模式除非进程主动让出(yeild)否则会一直被执行,不会让出,但是绝大多操作系统是抢占式的。

4.2 linux进程的调度

时间片调度,通过alrm 闹钟信号
















void schedule(void)

{

int i,next,c;

struct task_struct ** p;



// 首先处理alarm信号,唤醒所有收到信号的可中断睡眠进程

for(p = &LAST_TASK ; p > &FIRST_TASK ; --p)

if (*p) {

// 如果进程设置了alarm,并且alarm已经到时间了

if ((*p)->alarm && (*p)->alarm < jiffies) {

// 向该进程发送SIGALRM信号

(*p)->signal |= (1<

(*p)->alarm = 0;// 清除alarm

}

//可屏蔽信号位图BLOCKABLE定义在sched.c第24行,(~(_S(SIGKILL) | _S(SIGSTOP)))

// 说明SIGKILL和SIGSTOP是不能被屏蔽的。

// 可屏蔽信号位图 & 当前进程屏蔽的信号位图 = 当前进程实际屏蔽的信号位图

// 当前进程收到的信号位图 & ~当前进程实际屏蔽的信号位图

//= 当前进程收到的允许相应的信号位图

// 如果当前进程收到允许相应的信号,并且当前进程处于可中断睡眠态

// 则把状态改成运行态,参与下面的选择过程

if (((*p)->signal & ~(_BLOCKABLE & (*p)->blocked)) &&

(*p)->state==TASK_INTERRUPTIBLE)

(*p)->state=TASK_RUNNING;

}



// 下面是进程调度的主要部分

while (1) {undefined

c = -1;

next = 0;

i = NR_TASKS;

p = &task[NR_TASKS];

while (--i) {// 遍历整个task[]数组

if (!*--p)// 跳过task[]中的空项

continue;

// 寻找剩余时间片最长的可运行进程,

// c记录目前找到的最长时间片

// next记录目前最长时间片进程的任务号

if ((*p)->state == TASK_RUNNING && (*p)->counter > c)

c = (*p)->counter, next = i;

}

// 如果有进程时间片没有用完c一定大于0。这时退出循环,执行 switch_to任务切换

if (c) break;

// 到这里说明所有可运行进程的时间片都用完了,则利用任务优先级重新分配时间片。

// 这里需要重新设置所有任务的时间片,而不光是可运行任务的时间片。

// 利用公式:counter = counter/2 + priority

for(p = &LAST_TASK ; p > &FIRST_TASK ; --p)

if (*p)

(*p)->counter = ((*p)->counter >> 1) +

(*p)->priority;

// 整个设置时间片过程结束后,重新进入进程选择过程

}

// 当的上面的循环退出时,说明找到了可以切换的任务

switch_to(next);

}

注意到,当系统中现在没有可以投入运行的进程,但是存在就绪态,不过其时间片为0,此时,就需要重新为进程分配时间片。

linux 采用的是cfs,公平调度,允许进程运行一段时间,循环旋转。先择运行最少的进程优先运行

4.5 linux调度的实现

四个重要组成部分:

时间记账、进程选择、调度器入口、睡眠和唤醒。

CFS使用调度器实体结构,来追踪进程运行记账

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;


#ifdef CONFIG_SCHEDSTATS
	struct sched_statistics statistics;
#endif


#ifdef CONFIG_FAIR_GROUP_SCHED
	int			depth;
	struct sched_entity	*parent;
	
	struct cfs_rq		*cfs_rq;
	
	struct cfs_rq		*my_q;
#endif


#ifdef CONFIG_SMP
	
	struct sched_avg	avg ____cacheline_aligned_in_smp;
#endif
};

调度器实体作为一个名字是se的成员变量,嵌入在进程描述符task_struct里面。

2.虚拟实时

update_curr实现了记账功能

static void update_curr(struct cfs_rq *cfs_rq)
{
	struct sched_entity *curr = cfs_rq->curr;
	u64 now = rq_clock_task(rq_of(cfs_rq));
	u64 delta_exec;
 
	if (unlikely(!curr))
		return;
     
	delta_exec = now - curr->exec_start;
	if (unlikely((s64)delta_exec <= 0))
		return;
 
	curr->exec_start = now;
    
	schedstat_set(curr->statistics.exec_max, max(delta_exec, curr->statistics.exec_max));
 
	curr->sum_exec_runtime += delta_exec;// (1) 累计当前进程的实际运行时间
// 更新cfs_rq的实际执行时间cfs_rq->exec_clock
	schedstat_add(cfs_rq, exec_clock, delta_exec);

	curr->vruntime += calc_delta_fair(delta_exec, curr);// (2) 累计当前进程的vruntime
	update_min_vruntime(cfs_rq);

	if (entity_is_task(curr)) {
		struct task_struct *curtask = task_of(curr);
      
		trace_sched_stat_runtime(curtask, delta_exec, curr->vruntime);
        // 更新task所在cgroup之cpuacct的某个cpu运行时间ca->cpuusage[cpu]->cpuusage
		cpuacct_charge(curtask, delta_exec);
        // 统计task所在线程组(thread group)的运行时间:
        // tsk->signal->cputimer.cputime_atomic.sum_exec_runtime
		account_group_exec_runtime(curtask, delta_exec);
	}

	account_cfs_rq_runtime(cfs_rq, delta_exec);
}

_update_curr()完成后,exec_start被设置为rq的时间 

pdate_curr()函数只负责计算delta_exec以及更新exec_start。其它工作由__update_curr()函数完成:
        1、更新当前进程的实际运行时间(抽象模型中的runtime)。
        2、更新当前进程的虚拟时间vruntime。
        3、更新cfs_rq->min_vruntime。
           在当前进程和下一个将要被调度的进程中选择vruntime较小的值。然后用该值和cfs_rq->min_vruntime比较,如果比min_vruntime大,则更新cfs_rq为的min_vruntime为所求出的值。

4.5.2 进程选择

红黑树上存储了所有可以运行的进程

static struct sched_entity *
pick_next_entity(struct cfs_rq *cfs_rq, struct sched_entity *curr)
{
    
    struct sched_entity *left = __pick_first_entity(cfs_rq);
    struct sched_entity *se;

    
    if (!left || (curr && entity_before(curr, left)))
    {
        left = curr;
    }

    
    se = left; 

    
    if (cfs_rq->skip == se)
    {
        struct sched_entity *second;

        if (se == curr) 
        {
            second = __pick_first_entity(cfs_rq);
        }
        else    
        {
            
            second = __pick_next_entity(se);
            
            if (!second || (curr && entity_before(curr, second)))
                second = curr;
        }

        
        if (second && wakeup_preempt_entity(second, left) < 1)
            se = second;
    }

    
    if (cfs_rq->last && wakeup_preempt_entity(cfs_rq->last, left) < 1)
        se = cfs_rq->last;

    
    if (cfs_rq->next && wakeup_preempt_entity(cfs_rq->next, left) < 1)
        se = cfs_rq->next;

    
    clear_buddies(cfs_rq, se);

    return se;
}

向树中加入进程

enqueue_entity完成了进程真正的入队操作, 其具体流程如下所示

  • 更新一些统计统计量, update_curr, update_cfs_shares等
  • 如果进程此前是在睡眠状态, 则调用place_entity中首先会调整进程的虚拟运行时间
  • 最后如果进程最近在运行, 其虚拟运行时间仍然有效, 那么则直接用__enqueue_entity加入到红黑树

首先如果进程最近正在运行, 其虚拟时间时间仍然有效, 那么(除非它当前在执行中)它可以直接用__enqueue_entity插入到红黑树, 该函数徐娅萍处理一些红黑树的机制, 这可以依靠内核的标准实现, 参见__enqueue_entity函数, 

static void
enqueue_entity(struct cfs_rq *cfs_rq, struct sched_entity *se, int flags)
{
    
    if (!(flags & ENQUEUE_WAKEUP) || (flags & ENQUEUE_WAKING))
        se->vruntime += cfs_rq->min_vruntime;

    
    update_curr(cfs_rq);
    enqueue_entity_load_avg(cfs_rq, se);
    account_entity_enqueue(cfs_rq, se);
    update_cfs_shares(cfs_rq);

    
    if (flags & ENQUEUE_WAKEUP)
    {
        
        place_entity(cfs_rq, se, 0);
        if (schedstat_enabled())
            enqueue_sleeper(cfs_rq, se);
    }

    check_schedstat_required();
    if (schedstat_enabled()) {
        update_stats_enqueue(cfs_rq, se);
        check_spread(cfs_rq, se);
    }

    
    if (se != cfs_rq->curr)
        __enqueue_entity(cfs_rq, se);
    se->on_rq = 1;

    if (cfs_rq->nr_running == 1) {
        list_add_leaf_cfs_rq(cfs_rq);
        check_enqueue_throttle(cfs_rq);
    }
}

从红黑树中删除,进程变为了挂起等待的进程

static void
dequeue_entity(struct cfs_rq *cfs_rq, struct sched_entity *se, int flags)
{
    
    update_curr(cfs_rq);
    dequeue_entity_load_avg(cfs_rq, se);

    if (schedstat_enabled())
        update_stats_dequeue(cfs_rq, se, flags);

    clear_buddies(cfs_rq, se);

    if (se != cfs_rq->curr)
        __dequeue_entity(cfs_rq, se);
    se->on_rq = 0;
    account_entity_dequeue(cfs_rq, se);

    
    if (!(flags & DEQUEUE_SLEEP))
        se->vruntime -= cfs_rq->min_vruntime;

    
    return_cfs_rq_runtime(cfs_rq);

    update_min_vruntime(cfs_rq);
    update_cfs_shares(cfs_rq);
}

static void __dequeue_entity(struct cfs_rq *cfs_rq, struct sched_entity *se)
{
    if (cfs_rq->rb_leftmost == &se->run_node) {
        struct rb_node *next_node;

        next_node = rb_next(&se->run_node);
        cfs_rq->rb_leftmost = next_node;
    }

    rb_erase(&se->run_node, &cfs_rq->tasks_timeline);
}

4.5.3 调度器入口

调度器的核心函数是schedule,调用pick_next_task选择下一个要被调度的进程

schedule->__schedule->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;

    if (likely((prev->sched_class == &idle_sched_class ||
            prev->sched_class == &fair_sched_class) &&
           rq->nr_running == rq->cfs.h_nr_running)) {     ........1

        p = fair_sched_class.pick_next_task(rq, prev, rf);  .....2
        if (unlikely(p == RETRY_TASK))
            goto again;

        
        if (unlikely(!p))
            p = idle_sched_class.pick_next_task(rq, prev, rf);

        return p;
    }

again:
    for_each_class(class) {            ...........................3
        p = class->pick_next_task(rq, prev, rf);
        if (p) {
            if (unlikely(p == RETRY_TASK))
                goto again;
            return p;
        }
    }
}
4.5.4 函数唤醒和休眠

只记几个核心

//定义一个等待队列
DEFINE_WAIT

//加入等待队列
add_wait_queue

while (!concition) {

    //设置当前进程可被唤醒状态
    prepare_to_Wait

    //调度
    schedule()
}

//移除等待队列
finish_wait


唤醒使用wakeup系列就可以

4.6 上下文切换和 抢占

上下文切换核心是context_switch

static __always_inline struct rq *
context_switch(struct rq *rq, struct task_struct *prev,
	       struct task_struct *next, struct rq_flags *rf)
{
	prepare_task_switch(rq, prev, next);
 
	
	arch_start_context_switch(prev);
 
	
	if (!next->mm) {                                // to kernel
		enter_lazy_tlb(prev->active_mm, next);
 
		next->active_mm = prev->active_mm;
		if (prev->mm)                           // from user
			mmgrab(prev->active_mm);
		else
			prev->active_mm = NULL;
	} else {                                        // to user
		membarrier_switch_mm(rq, prev->active_mm, next->mm);
		
		switch_mm_irqs_off(prev->active_mm, next->mm, next);
 
		if (!prev->mm) {                        // from kernel
			
			rq->prev_mm = prev->active_mm;
			prev->active_mm = NULL;
		}
	}
 
	rq->clock_update_flags &= ~(RQCF_ACT_SKIP|RQCF_REQ_SKIP);
 
	prepare_lock_switch(rq, next, rf);
 
	
	switch_to(prev, next, prev);
	barrier();
 
	return finish_task_switch(prev);
}

上下文切换分为

根据引发context switch的原因,又分为两种情况——CSWCH(自愿上下文切换)和NVCSWCH(非自愿上下文切换),查看man pidstat中:

  1. 症状:上下文切换过多,会导致sys系统态CPU增加,换句话说CPU资源都被内核用了
    1. 如果是CSWCH过多,那要定位是什么资源短缺导致频繁context switch影响系统性能
    2. 如果是NVCSWCH过多,那要看是哪里导致那么多R态进程等待调度运行

switch_ mm负责把虚拟内存从旧进程切换到新进程,switch_to负责切换处理器状态

转载请注明:文章转载自 www.mshxw.com
本文地址:https://www.mshxw.com/it/603894.html
我们一直用心在做
关于我们 文章归档 网站地图 联系我们

版权所有 (c)2021-2022 MSHXW.COM

ICP备案号:晋ICP备2021003244-6号