Linux中的进程调度(四)

先把我们最初开始的函数写上,前面写的太长,不再看下原始代码可能回不去了。
/*
 * Share the fairness runtime between parent and child, thus the
 * total amount of pressure for CPU stays equal - new tasks
 * get a chance to run but frequent forkers are not allowed to
 * monopolize the CPU. Note: the parent runqueue is locked,
 * the child is not running yet.
 */
static void task_new_fair(struct rq *rq, struct task_struct *p)
{
	struct cfs_rq *cfs_rq = task_cfs_rq(p);
	struct sched_entity *se = &p->se, *curr = cfs_rq->curr;
	int this_cpu = smp_processor_id();

	sched_info_queued(p);

	update_curr(cfs_rq);
	place_entity(cfs_rq, se, 1);

	/* 'curr' will be NULL if the child belongs to a different group */
	if (sysctl_sched_child_runs_first && this_cpu == task_cpu(p) &&
			curr && curr->vruntime < se->vruntime) {
		/*
		 * Upon rescheduling, sched_class::put_prev_task() will place
		 * 'current' within the tree based on its new key value.
		 */
		swap(curr->vruntime, se->vruntime);
	}

	enqueue_task_fair(rq, p, 0);
	resched_task(rq->curr);
}
上次已经分析过update_curr的代码,现在接着往下走,去看place_entity()
static void
place_entity(struct cfs_rq *cfs_rq, struct sched_entity *se, int initial)
{
	u64 vruntime;

	vruntime = cfs_rq->min_vruntime;

	if (sched_feat(TREE_AVG)) {
		struct sched_entity *last = __pick_last_entity(cfs_rq);
		if (last) {
			vruntime += last->vruntime;
			vruntime >>= 1;
		}
	} else if (sched_feat(APPROX_AVG) && cfs_rq->nr_running)
		vruntime += sched_vslice(cfs_rq)/2;

	/*
	 * 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_add(cfs_rq, se);

	if (!initial) {
		/* sleeps upto a single latency don't count. */
		if (sched_feat(NEW_FAIR_SLEEPERS) && entity_is_task(se))
			vruntime -= sysctl_sched_latency;

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

	se->vruntime = vruntime;
}
其中的if else if语句块在默认情况下是都不成立的,不成立的原因很简单,初始化时就这么写的,这里就不啰嗦了。 其中
if (initial && sched_feat(START_DEBIT))
在默认情况下还是成立的,所以进去看一下
static u64 sched_vslice_add(struct cfs_rq *cfs_rq, struct sched_entity *se)
{
	return __sched_vslice(cfs_rq->load.weight + se->load.weight,
			cfs_rq->nr_running + 1);
}
注释会对我们有帮助~
/*
 * We calculate the vruntime slice.
 *
 * vs = s/w = p/rw
 */
static u64 __sched_vslice(unsigned long rq_weight, unsigned long nr_running)
{
	u64 vslice = __sched_period(nr_running);

	vslice *= NICE_0_LOAD;
	do_div(vslice, rq_weight);

	return vslice;
}
再进入到__sched_period中去
/*
 * The idea is to set a period in which each task runs once.
 *
 * When there are too many tasks (sysctl_sched_nr_latency) we have to stretch
 * this period because otherwise the slices get too small.
 *
 * p = (nr <= nl) ? l : l*nr/nl
 */
static u64 __sched_period(unsigned long nr_running)
{
	u64 period = sysctl_sched_latency;
	unsigned long nr_latency = sched_nr_latency;

	if (unlikely(nr_running > nr_latency)) {
		period *= nr_running;
		do_div(period, nr_latency);
	}

	return period;
}
注释里写的已经很清楚了,如果进程太多的话,一个调度周期就被分的太细,这样纯粹调度花费的时间所占的比重就已经很大,所以要适当把调度周期放大些,放大是根据进程数量按比例来的。 然后根据新调整的调度周期,将新建进程的虚拟运行时间增加一点(个人感觉这也是考虑到如果不增加的话,如果一个进程不断的fork子进程,会造成CPU垄断) 由于在调用place_entity时最后一个参数为1,所以if(!initial)不成立,直接将计算出来的虚拟时间赋给p的虚拟运行时间。 这便是place_entity完成的功能。 主要就是给当前进程分配一个初始的虚拟运行时间,虽然他还没有运行,就像它的名字,place_entity,给新进程找到一个合适的地方,以便接下来将其放入红黑树。 接下来再回到task_+new_fair中去,place_entity执行完毕后进入一个if条件语句,当满足以下条件时该语句块会被执行: 1)系统定义了创建进程后子进程先执行 2)当前进程所在的CPU就是子进程所被分配到的CPU 3)curr不为空(注释中说明是如果父子进程不属于同一个调度组时curr可能为空,这个在后面会研究) 4)curr的虚拟运行时间小于新进程的虚拟运行时间 如果满足这几个条件,会将父子进程的虚拟运行时间交换,为什么要交换呢? 很明显,条件中说,子进程要先运行,而此时子进程的虚拟运行时间又比父进程的运行时间大,调度器每次都会选一个虚拟运行时间最小的进程来运行,所以当然要将他们两个交换了。 这一切准备好之后(主要是记录更新了当前进程的虚拟运行时间,给新创建的子进程增加了适当的虚拟运行时间),便可以将新创建的进程插入红黑树以待调度了。于是在task_new_fair里就进程了enqueue_task函数。
/*
 * The enqueue_task method is called before nr_running is
 * increased. Here we update the fair scheduling stats and
 * then put the task into the rbtree:
 */
static void enqueue_task_fair(struct rq *rq, struct task_struct *p, int wakeup)
{
	struct cfs_rq *cfs_rq;
	struct sched_entity *se = &p->se;

	for_each_sched_entity(se) {
		if (se->on_rq)
			break;
		cfs_rq = cfs_rq_of(se);
		enqueue_entity(cfs_rq, se, wakeup);
		wakeup = 1;
	}
}
如果在有组调度的情况下,这个函数不仅会将当前子进程加入红黑树,具体我们后面再分析。总之,它的主要动作就是调用enqueue_entity:
static void
enqueue_entity(struct cfs_rq *cfs_rq, struct sched_entity *se, int wakeup)
{
	/*
	 * Update run-time statistics of the 'current'.
	 */
	update_curr(cfs_rq);

	if (wakeup) {
		place_entity(cfs_rq, se, 0);
		enqueue_sleeper(cfs_rq, se);
	}

	update_stats_enqueue(cfs_rq, se);
	check_spread(cfs_rq, se);
	if (se != cfs_rq->curr)
		__enqueue_entity(cfs_rq, se);
	account_entity_enqueue(cfs_rq, se);
}
在做了一些必要的更新信息后,主要动作是调用__enqueue_entity
/*
 * Enqueue an entity into the rb-tree:
 */
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;
	s64 key = entity_key(cfs_rq, se);
	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 (key < entity_key(cfs_rq, 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)
		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);
}
这个函数很容易读懂,先是找到要插入的位置,然后将其插入,(如果插入位置是最左边的叶子结点,那么需要更新缓冲),然后链入结点,并修改结点颜色以保证红黑树的性质,以防子树高度过于不平衡。具体算法可参见算法导论,很容易懂。不过在红黑树的结点表示上,这里用到了一个技巧: 把所有结点的地址按4字节对齐,这样,每个节点的首地址最后两个比特位肯定为零,由于红黑树的颜色状态只有两个,要么红要么黑,所以编码者就利用了最低位来表示当前节点的颜色。这种用内存的方式~~ 将进程插入红黑树后,就可以通过内核在系统调用返回用户空间时进行一次调度了。
	enqueue_task_fair(rq, p, 0);
	resched_task(rq->curr);
进入resched_task中去:
/*
 * resched_task - mark a task 'to be rescheduled now'.
 *
 * On UP this means the setting of the need_resched flag, on SMP it
 * might also involve a cross-CPU call to trigger the scheduler on
 * the target CPU.
 */
#ifdef CONFIG_SMP

#ifndef tsk_is_polling
#define tsk_is_polling(t) test_tsk_thread_flag(t, TIF_POLLING_NRFLAG)
#endif

static void resched_task(struct task_struct *p)
{
	int cpu;

	assert_spin_locked(&task_rq(p)->lock);

	if (unlikely(test_tsk_thread_flag(p, TIF_NEED_RESCHED)))
		return;

	set_tsk_thread_flag(p, TIF_NEED_RESCHED);

	cpu = task_cpu(p);
	if (cpu == smp_processor_id())
		return;

	/* NEED_RESCHED must be visible before we test polling */
	smp_mb();
	if (!tsk_is_polling(p))
		smp_send_reschedule(cpu);
}
先加锁.
if (unlikely(test_tsk_thread_flag(p, TIF_NEED_RESCHED)))
		return;
这一句很有意思,如果当前进程的TIF_NEED_RESCHED标志已经置位,那么便可以直接返回了,test_tsk_thread_flag是怎么工作的呢?一步步往下深入:
 static inline int test_tsk_thread_flag(struct task_struct *tsk, int flag)
 {
     return test_ti_thread_flag(task_thread_info(tsk), flag);
 }
  static inline int test_ti_thread_flag(struct thread_info *ti, int flag)
  {
      return test_bit(flag,&ti->flags);
  }
 #define test_bit(nr,addr) 
 (__builtin_constant_p(nr) ? 
  constant_test_bit((nr),(addr)) : 
  variable_test_bit((nr),(addr)))
当前,flag标志应该是一个变量,(谁说的C语言里面没有多态?),接着往里面走:
 static inline int variable_test_bit(int nr, const volatile unsigned long * addr)
 {
     int oldbit;

     __asm__ __volatile__(
         "btl %2,%1ntsbbl %0,%0"
         :"=r" (oldbit)
         :"m" (ADDR),"Ir" (nr));
     return oldbit;
 }
汇编语句。 btl的功能是测试某个数的特定位是零还是1,测试结果放到CF标志位里。(具体可见intel的用户手册).然后,sbbl %0, %0,也就是说,让oldbit与oldbit的值带位相减,即oldbit-oldbit-CF,如果CF标志位是零,也就是刚才位测试的结果是零的话,当然最后返回的oldbit也是零了,如果CF标志位是1,那么返回的就不一样了。返回的值为-1,多么巧妙。 如果经测试,TIF_NEED_RESCHED还没有置位,那么就将其置位。  
      set_tsk_thread_flag(p, TIF_NEED_RESCHED);
 
 static inline void set_tsk_thread_flag(struct task_struct *tsk, int flag)
 {
     set_ti_thread_flag(task_thread_info(tsk), flag);
 }
 
  static inline void set_ti_thread_flag(struct thread_info *ti, int flag)
  {
      set_bit(flag,&ti->flags);
  }
这里所有的涉及平台相关的汇编指令全是以Intel x86为例
  static inline void set_bit(int nr, volatile unsigned long * addr)
  {
      __asm__ __volatile__( LOCK_PREFIX
          "btsl %1,%0"
          :"+m" (ADDR)
          :"Ir" (nr));
  }
btsl就不说了,这个指令的功能就是置位,而且是原子的。 置好位后,resched_task的任务只剩下对多处理器存在的情况下任务的处理了,先留着,把整个调度过程走一遍再回过头来分析。  
原文地址:https://www.cnblogs.com/yangce/p/2910093.html