linux调度器源码分析(三) - 新进程加入

这篇文章主要说明系统如何把一个进程加入到队列中。

1 加入时机

只有处于TASK_RUNNING状态下的进程才能够加入到调度器,其他状态都不行,也就说明了,当一个进程处于睡眠、挂起状态的时候是不存在于调度器中的,而进程加入调度器的时机如下:

  • 当进程创建完成时,进程刚创建完成时,即使它运行起来立即调用sleep()进程睡眠,它也必定先会加入到调度器,因为实际上它加入调度器后自己还需要进行一定的初始化和操作,才会调用到我们的“立即”sleep()。
  • 当进程被唤醒时,也使用sleep的例子说明,我们平常写程序使用的sleep()函数实现原理就是通过系统调用将进程状态改为TASK_INTERRUPTIBLE,然后移出运行队列,并且启动一个定时器,在定时器到期后唤醒进程,再重新放入运行队列。

2 调度初始化sched_fork

linux创建进程或线程的时候会调用copy_process这个函数,而里面有一个函数专门用于进程调度的初始化,就是sched_fork(),其代码如下

int sched_fork(unsigned long clone_flags, struct task_struct *p)
{
    unsigned long flags;
    /* 获取当前CPU,并且禁止抢占 */
    int cpu = get_cpu();
    
    /* 初始化跟调度相关的值,比如调度实体,运行时间等 */
    __sched_fork(clone_flags, p);
    /*
     * 标记为运行状态,表明此进程正在运行或准备好运行,实际上没有真正在CPU上运行,这里只是导致了外部信号和事件不能够唤醒此进程,之后将它插入到运行队列中
     */
    p->state = TASK_RUNNING;

    /*
     * 根据父进程的运行优先级设置设置进程的优先级
     */
    p->prio = current->normal_prio;

    /*
     * 更新该进程优先级
     */
    /* 如果需要重新设置优先级 */
    if (unlikely(p->sched_reset_on_fork)) {
        /* 如果是dl调度或者实时调度 */
        if (task_has_dl_policy(p) || task_has_rt_policy(p)) {
            /* 调度策略为SCHED_NORMAL,这个选项将使用CFS调度 */
            p->policy = SCHED_NORMAL;
            /* 根据默认nice值设置静态优先级 */
            p->static_prio = NICE_TO_PRIO(0);
            /* 实时优先级为0 */
            p->rt_priority = 0;
        } else if (PRIO_TO_NICE(p->static_prio) < 0)
            /* 根据默认nice值设置静态优先级 */
            p->static_prio = NICE_TO_PRIO(0);

        /* p->prio = p->normal_prio = p->static_prio,初始化时设置的默认优先级为120 */
        p->prio = p->normal_prio = __normal_prio(p);
        /* 设置进程权重 */
        set_load_weight(p);

         /* sched_reset_on_fork成员在之后已经不需要使用了,直接设为0 */
        p->sched_reset_on_fork = 0;
    }

    if (dl_prio(p->prio)) {
        /* 使能抢占 */
        put_cpu();
        /* 返回错误 */
        return -EAGAIN;
    } else if (rt_prio(p->prio)) {
        /* 根据优先级判断,如果是实时进程,设置其调度类为rt_sched_class */
        p->sched_class = &rt_sched_class;
    } else {
        /* 如果是普通进程,设置其调度类为fair_sched_class */
        p->sched_class = &fair_sched_class;
    }
    /* 调用调用类的task_fork函数 */
    if (p->sched_class->task_fork)
        p->sched_class->task_fork(p);

    /*
     * The child is not yet in the pid-hash so no cgroup attach races,
     * and the cgroup is pinned to this child due to cgroup_fork()
     * is ran before sched_fork().
     *
     * Silence PROVE_RCU.
     */
    raw_spin_lock_irqsave(&p->pi_lock, flags);
    /* 设置新进程的CPU为当前CPU */
    set_task_cpu(p, cpu);
    raw_spin_unlock_irqrestore(&p->pi_lock, flags);

#if defined(CONFIG_SCHEDSTATS) || defined(CONFIG_TASK_DELAY_ACCT)
    if (likely(sched_info_on()))
        memset(&p->sched_info, 0, sizeof(p->sched_info));
#endif
#if defined(CONFIG_SMP)
    p->on_cpu = 0;
#endif
    /* task_thread_info(p)->preempt_count = PREEMPT_DISABLED; */
    /* 初始化该进程为内核禁止抢占 */
    init_task_preempt_count(p); //preempt_count不为0则禁止抢占
#ifdef CONFIG_SMP
    plist_node_init(&p->pushable_tasks, MAX_PRIO);
    RB_CLEAR_NODE(&p->pushable_dl_tasks);
#endif
    /* 使能抢占 */
    put_cpu(); //每次使能抢占时都会检查,当前任务是否能被抢占,如果可以直接调用schedule
    return 0;
}

看一下set_load_weight函数,可以看到weight是根据nice值来设定的,每个nice值对应一个weight,而inv_weight为weight的倒数,用于内核中方便计算使用:

static const int prio_to_weight[40] = {
 /* -20 */     88761,     71755,     56483,     46273,     36291,
 /* -15 */     29154,     23254,     18705,     14949,     11916,
 /* -10 */      9548,      7620,      6100,      4904,      3906,
 /*  -5 */      3121,      2501,      1991,      1586,      1277,
 /*   0 */      1024,       820,       655,       526,       423,
 /*   5 */       335,       272,       215,       172,       137,
 /*  10 */       110,        87,        70,        56,        45,
 /*  15 */        36,        29,        23,        18,        15,
};
static const int prio_to_weight[40] = {
 /* -20 */     88761,     71755,     56483,     46273,     36291,
 /* -15 */     29154,     23254,     18705,     14949,     11916,
 /* -10 */      9548,      7620,      6100,      4904,      3906,
 /*  -5 */      3121,      2501,      1991,      1586,      1277,
 /*   0 */      1024,       820,       655,       526,       423,
 /*   5 */       335,       272,       215,       172,       137,
 /*  10 */       110,        87,        70,        56,        45,
 /*  15 */        36,        29,        23,        18,        15,
};
static void set_load_weight(struct task_struct *p)
{
	int prio = p->static_prio - MAX_RT_PRIO;
	struct load_weight *load = &p->se.load;

	/*
	 * SCHED_IDLE tasks get minimal weight:
	 */
	if (p->policy == SCHED_IDLE) {
		load->weight = scale_load(WEIGHT_IDLEPRIO);
		load->inv_weight = WMULT_IDLEPRIO;
		return;
	}

	load->weight = scale_load(prio_to_weight[prio]);
	load->inv_weight = prio_to_wmult[prio];
}

在sched_fork()函数中,主要工作如下:

  • 获取当前CPU号
  • 禁止内核抢占(这里基本就是关闭了抢占,因为执行到这里已经是内核态,又禁止了被抢占)
  • 初始化进程p的一些变量(实时进程和普通进程通用的那些变量)
  • 设置进程p的状态为TASK_RUNNING(这一步很关键,因为只有处于TASK_RUNNING状态下的进程才会被调度器放入队列中)
  • 根据父进程和clone_flags参数设置进程p的优先级和权重。
  • 根据进程p的优先级设置其调度类(实时进程优先级:0~99  普通进程优先级:100~139)
  • 根据调度类进行进程p类型相关的初始化(这里就实现了实时进程和普通进程独有的变量进行初始化)
  • 设置进程p的当前CPU为此CPU。
  • 初始化进程p禁止内核抢占(因为当CPU执行到进程p时,进程p还需要进行一些初始化)
  • 使能内核抢占

  可以看出sched_fork()进行的初始化也比较简单,需要注意的是不同类型的进程会使用不同的调度类,并且也会调用调度类中的初始化函数。在实时进程的调度类中是没有特定的task_fork()函数的,而普通进程使用cfs策略时会调用到task_fork_fair()函数,我们具体看看实现:

2.1 普通非实时进程task_fork

static void task_fork_fair(struct task_struct *p)
{
    struct cfs_rq *cfs_rq;
    
    /* 进程p的调度实体se */
    struct sched_entity *se = &p->se, *curr;
    
    /* 获取当前CPU */
    int this_cpu = smp_processor_id();
    
    /* 获取此CPU的运行队列 */
    struct rq *rq = this_rq();
    unsigned long flags;
    
    /* 上锁并保存中断记录 */
    raw_spin_lock_irqsave(&rq->lock, flags);
    
    /* 更新rq运行时间 */
    update_rq_clock(rq);
    
    /* cfs_rq = current->se.cfs_rq; */
    cfs_rq = task_cfs_rq(current);
    
    /* 设置当前进程所在队列为父进程所在队列 */
    curr = cfs_rq->curr;

    /*
     * Not only the cpu but also the task_group of the parent might have
     * been changed after parent->se.parent,cfs_rq were copied to
     * child->se.parent,cfs_rq. So call __set_task_cpu() to make those
     * of child point to valid ones.
     */
    rcu_read_lock();
    /* 设置此进程所属CPU */
    __set_task_cpu(p, this_cpu);
    rcu_read_unlock();

    /* 更新当前进程运行时间 */
    update_curr(cfs_rq);

    if (curr)
        /* 将父进程的虚拟运行时间赋给了新进程的虚拟运行时间 */
        se->vruntime = curr->vruntime;
    /* 调整了se的虚拟运行时间 */
    place_entity(cfs_rq, se, 1);

    if (sysctl_sched_child_runs_first && curr && entity_before(curr, se)) {
        /*
         * Upon rescheduling, sched_class::put_prev_task() will place
         * 'current' within the tree based on its new key value.
         */
        swap(curr->vruntime, se->vruntime);
        resched_curr(rq);
    }

    /* 保证了进程p的vruntime是运行队列中最小的(这里占时不确定是不是这个用法,不过确实是最小的了) */
    se->vruntime -= cfs_rq->min_vruntime;
    
    /* 解锁,还原中断记录 */
    raw_spin_unlock_irqrestore(&rq->lock, flags);
}

在task_fork_fair()函数中主要就是设置进程p的虚拟运行时间和所处的cfs队列,值得我们注意的是 cfs_rq = task_cfs_rq(current); 这一行,在注释中已经表明task_cfs_rq(current)返回的是current的se.cfs_rq,注意se.cfs_rq保存的并不是根cfs队列,而是所处的cfs_rq,也就是如果父进程处于一个进程组的cfs_rq中,新创建的进程也会处于这个进程组的cfs_rq中。

上面函数只是初始化了当前新建task调度相关的结构,但是还没有真正把该task放入调度队列。其中还有几个比较重要的跟调度时间相关的函数。

update_rq_clock

void update_rq_clock(struct rq *rq)
{
	s64 delta;

	if (rq->skip_clock_update > 0)
		return;

	delta = sched_clock_cpu(cpu_of(rq)) - rq->clock;
	rq->clock += delta;
	update_rq_clock_task(rq, delta);
}

一个tick对应一个clock,sched_clock_cpu获取系统运行至今的tick计数。该函数更新rq->clock和rq->clock_task,其中rq->clock记录总的tick计数,而clock_task会减去中断占用的tick计数,是真正进程执行所使用的tick计数。update_rq_clock_task去更新clock_task。

update_curr

static void update_curr(struct cfs_rq *cfs_rq)
{
	struct sched_entity *curr = cfs_rq->curr;
	u64 now = rq_of(cfs_rq)->clock_task;
	unsigned long delta_exec;

	if (unlikely(!curr))
		return;

	/*
	 * Get the amount of time the current task was running
	 * since the last time we changed load (this cannot
	 * overflow on 32 bits):
	 */
	delta_exec = (unsigned long)(now - curr->exec_start);
	if (!delta_exec)
		return;

	__update_curr(cfs_rq, curr, delta_exec);
	curr->exec_start = now;

	if (entity_is_task(curr)) {
		struct task_struct *curtask = task_of(curr);

		trace_sched_stat_runtime(curtask, delta_exec, curr->vruntime);
		cpuacct_charge(curtask, delta_exec);
		account_group_exec_runtime(curtask, delta_exec);
	}

	account_cfs_rq_runtime(cfs_rq, delta_exec);
}

curr->exec_start记录当前进程最近一次执行的时间点。
curr->sum_exec_runtime记录当前进程运行的总的时间。 

__update_curr()函数主要完成了三个任务:1.更新当前进程的实际运行时间(抽象模型中的runtime)。2.更新当前进程的虚拟时间vruntime。3.更新cfs_rq->min_vruntime。

place_entity

static void
place_entity(struct cfs_rq *cfs_rq, struct sched_entity *se, int initial)
{
	u64 vruntime = cfs_rq->min_vruntime;

	/*
	 * The 'current' period is already promised to the current tasks,
	 * however the extra weight of the new task will slow them down a
	 * little, place the new task so that it fits in the slot that
	 * stays open at the end.
	 */
	if (initial && sched_feat(START_DEBIT))
		vruntime += sched_vslice(cfs_rq, se);

	/* sleeps up to a single latency don't count. */
	if (!initial) {
		unsigned long thresh = sysctl_sched_latency;

		/*
		 * Halve their sleep time's effect, to allow
		 * for a gentler effect of sleepers:
		 */
		if (sched_feat(GENTLE_FAIR_SLEEPERS))
			thresh >>= 1;

		vruntime -= thresh;
	}

	/* ensure we never gain time by being placed backwards. */
	se->vruntime = max_vruntime(se->vruntime, vruntime);
}

place_entity()函数的功能是调整进程的虚拟时间。当新进程被创建或者进程被唤醒时都需要调整它的vruntime值

3 wake_up_new_task

到这里新进程关于调度的初始化已经完成,但是还没有被调度器加入到队列中,其是在do_fork()中的wake_up_new_task(p);中加入到队列中的,我们具体看看wake_up_new_task()的实现:

void wake_up_new_task(struct task_struct *p)
{
    unsigned long flags;
    struct rq *rq;

    raw_spin_lock_irqsave(&p->pi_lock, flags);
#ifdef CONFIG_SMP
    /*
     * Fork balancing, do it here and not earlier because:
     *  - cpus_allowed can change in the fork path
     *  - any previously selected cpu might disappear through hotplug
     */
     /* 为进程选择一个合适的CPU */
    set_task_cpu(p, select_task_rq(p, task_cpu(p), SD_BALANCE_FORK, 0));
#endif

    /* Initialize new task's runnable average */
    /* 这里是跟多核负载均衡有关 */
    init_task_runnable_average(p);
    /* 上锁 */
    rq = __task_rq_lock(p);
    /* 将进程加入到CPU的运行队列 */
    activate_task(rq, p, 0);
    /* 标记进程p处于队列中 */
    p->on_rq = TASK_ON_RQ_QUEUED;
    /* 跟调试有关 */
    trace_sched_wakeup_new(p, true);
    /* 检查是否需要切换当前进程,如果需要则设置当前线程的调度flag */
    check_preempt_curr(rq, p, WF_FORK);
#ifdef CONFIG_SMP
    if (p->sched_class->task_woken)
        p->sched_class->task_woken(rq, p);
#endif
    task_rq_unlock(rq, p, &flags);
}

在wake_up_new_task()函数中,将进程加入到运行队列的函数为activate_task()。

void activate_task(struct rq *rq, struct task_struct *p, int flags)
{
	if (task_contributes_to_load(p))
		rq->nr_uninterruptible--;

	enqueue_task(rq, p, flags);
}
static void enqueue_task(struct rq *rq, struct task_struct *p, int flags)
{
	update_rq_clock(rq);
	sched_info_queued(p);
	p->sched_class->enqueue_task(rq, p, flags);
}

可以看到,最后根据调度类的不同,会使用具体调度类的enqueue_task函数加入到调度队列中。

3.1 cfs调度类的enqueue_task

static void
enqueue_task_fair(struct rq *rq, struct task_struct *p, int flags)
{
    struct cfs_rq *cfs_rq;
    struct sched_entity *se = &p->se;

    /* 这里是一个迭代,我们知道,进程有可能是处于一个进程组中的,所以当这个处于进程组中的进程加入到该进程组的队列中时,要对此队列向上迭代 */
    for_each_sched_entity(se) {
        if (se->on_rq)
            break;
        /* 如果不是CONFIG_FAIR_GROUP_SCHED,获取其所在CPU的rq运行队列的cfs_rq运行队列
         * 如果是CONFIG_FAIR_GROUP_SCHED,获取其所在的cfs_rq运行队列
         */
        cfs_rq = cfs_rq_of(se);
        /* 加入到队列中 */
        enqueue_entity(cfs_rq, se, flags);

        /*
         * end evaluation on encountering a throttled cfs_rq
         *
         * note: in the case of encountering a throttled cfs_rq we will
         * post the final h_nr_running increment below.
        */
        if (cfs_rq_throttled(cfs_rq))
            break;
        cfs_rq->h_nr_running++;

        flags = ENQUEUE_WAKEUP;
    }

    /* 只有se不处于队列中或者cfs_rq_throttled(cfs_rq)返回真才会运行这个循环 */
    for_each_sched_entity(se) {
        cfs_rq = cfs_rq_of(se);
        cfs_rq->h_nr_running++;

        if (cfs_rq_throttled(cfs_rq))
            break;

        update_cfs_shares(cfs_rq);
        update_entity_load_avg(se, 1);
    }

    if (!se) {
        update_rq_runnable_avg(rq, rq->nr_running);
        /* 当前CPU运行队列活动进程数 + 1 */
        add_nr_running(rq, 1);
    }
    /* 设置下次调度中断发生时间 */
    hrtick_update(rq);
}

在enqueue_task_fair()函数中又使用了enqueue_entity()函数进行操作,如下: 

static void
enqueue_entity(struct cfs_rq *cfs_rq, struct sched_entity *se, int flags)
{
    /*
     * Update the normalized vruntime before updating min_vruntime
     * through calling update_curr().
     */
    if (!(flags & ENQUEUE_WAKEUP) || (flags & ENQUEUE_WAKING))
        se->vruntime += cfs_rq->min_vruntime;

    /*
     * Update run-time statistics of the 'current'.
     */
    /* 更新当前进程运行时间和虚拟运行时间 */
    update_curr(cfs_rq);
    enqueue_entity_load_avg(cfs_rq, se, flags & ENQUEUE_WAKEUP);
    /* 更新cfs_rq队列总权重(就是在原有基础上加上se的权重) */
    account_entity_enqueue(cfs_rq, se);
    update_cfs_shares(cfs_rq);

    /* 新建的进程flags为0,不会执行这里 */
    if (flags & ENQUEUE_WAKEUP) {
        place_entity(cfs_rq, se, 0);
        enqueue_sleeper(cfs_rq, se);
    }

    update_stats_enqueue(cfs_rq, se);
    check_spread(cfs_rq, se);
    
    /* 将se插入到运行队列cfs_rq的红黑树中 */
    if (se != cfs_rq->curr)
        __enqueue_entity(cfs_rq, se);
    /* 将se的on_rq标记为1 */
    se->on_rq = 1;

    /* 如果cfs_rq的队列中只有一个进程,这里做处理 */
    if (cfs_rq->nr_running == 1) {
        list_add_leaf_cfs_rq(cfs_rq);
        check_enqueue_throttle(cfs_rq);
    }
}

看一下__enqueue_entity

static void __enqueue_entity(struct cfs_rq *cfs_rq, struct sched_entity *se)
{
	struct rb_node **link = &cfs_rq->tasks_timeline.rb_node;
	struct rb_node *parent = NULL;
	struct sched_entity *entry;
	int leftmost = 1;

	/*
	 * Find the right place in the rbtree:
	 */
	while (*link) {
		parent = *link;
		entry = rb_entry(parent, struct sched_entity, run_node);
		/*
		 * We dont care about collisions. Nodes with
		 * the same key stay together.
		 */
		if (entity_before(se, entry)) {
			link = &parent->rb_left;
		} else {
			link = &parent->rb_right;
			leftmost = 0;
		}
	}

	/*
	 * Maintain a cache of leftmost tree entries (it is frequently
	 * used):
	 */
	if (leftmost) //更新rb_leftmost节点,后面选择下一个需要调度的fair task的时候会使用
		cfs_rq->rb_leftmost = &se->run_node;

	rb_link_node(&se->run_node, parent, link);
	rb_insert_color(&se->run_node, &cfs_rq->tasks_timeline);
}

3.2 rt调度类的enqueue_task

static void
enqueue_task_rt(struct rq *rq, struct task_struct *p, int flags)
{
	struct sched_rt_entity *rt_se = &p->rt;

	if (flags & ENQUEUE_WAKEUP)
		rt_se->timeout = 0;

	enqueue_rt_entity(rt_se, flags & ENQUEUE_HEAD);

	if (!task_current(rq, p) && p->nr_cpus_allowed > 1)
		enqueue_pushable_task(rq, p);

	inc_nr_running(rq);
}
static void enqueue_rt_entity(struct sched_rt_entity *rt_se, bool head)
{
	dequeue_rt_stack(rt_se); //先dequeue,因为这边本来就不再队列里,所以什么都没做
	for_each_sched_rt_entity(rt_se)
		__enqueue_rt_entity(rt_se, head);
}

__enqueue_rt_entity函数是真正加入rt队列的函数:

static void __enqueue_rt_entity(struct sched_rt_entity *rt_se, bool head)
{
	struct rt_rq *rt_rq = rt_rq_of_se(rt_se);
	struct rt_prio_array *array = &rt_rq->active;
	struct rt_rq *group_rq = group_rt_rq(rt_se);
	struct list_head *queue = array->queue + rt_se_prio(rt_se);//根据优先级值算出要放入的hash表的queue

	/*
	 * Don't enqueue the group if its throttled, or when empty.
	 * The latter is a consequence of the former when a child group
	 * get throttled and the current group doesn't have any other
	 * active members.
	 */
	if (group_rq && (rt_rq_throttled(group_rq) || !group_rq->rt_nr_running))
		return;

	if (!rt_rq->rt_nr_running)
		list_add_leaf_rt_rq(rt_rq);

	if (head)
		list_add(&rt_se->run_list, queue);
	else
		list_add_tail(&rt_se->run_list, queue);//插入queue中
	__set_bit(rt_se_prio(rt_se), array->bitmap); //并把该hash表对应的位图置1

	inc_rt_tasks(rt_se, rt_rq);
}

 

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
Linux内核进程调度是一个非常复杂的系统,主要由可调度进程队列、进程调度策略、调度负责。 在Linux中,进程调度是一个单独的内核线程,其主要工作是在可调度进程队列中选择下一个要运行的进程,并将CPU分配给该进程进程调度根据进程的优先级和调度策略来选择下一个要运行的进程Linux进程调度的核心代码位于sched目录下,主要包括以下文件: 1. sched.h:定义了调度的数据结构和函数原型。 2. sched.c:实现了进程调度的主要功能,包括进程加入、删除、更等操作。 3. rt.c:实时调度策略相关代码。 4. fair.c:CFS调度策略相关代码。 5. idle.c:空闲进程相关代码。 6. deadline.c:DEADLINE调度策略相关代码。 下面我们以CFS调度策略为例,简单介绍一下进程调度的实现过程: CFS调度策略是一种完全公平的调度策略,它通过动态优先级来保证进程的公平性。在CFS调度策略中,每个进程都有一个虚拟运行时间(virtual runtime),该时间是进程已经运行的时间和优先级的函数。 CFS调度策略的核心代码位于fair.c文件中,主要包括以下函数: 1. enqueue_task_fair():将一个进程添加到可调度进程队列中。 2. dequeue_task_fair():将一个进程从可调度进程队列中删除。 3. update_curr_fair():更当前进程的虚拟运行时间。 4. pick_next_task_fair():选择下一个要运行的进程。 以上函数的实现过程中,都涉及到了对进程调度的数据结构的操作,如可调度进程队列、进程控制块等。具体实现过程需要结合代码进行分析。 总体来说,Linux内核进程调度的实现非常复杂,需要涉及到很多的数据结构和算法,同时还需要考虑到性能、公平性等因素。因此,对于想要深入了解Linux内核的人来说,进程调度是必须要掌握的一个重要部分。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值