Linux进程管理:(七)进程的唤醒

文章说明:

1. 唤醒进程的流程

唤醒进程是操作系统中核心的操作之一,Linux内核提供了一个wake_up_process()接口函数来唤醒进程。唤醒进程涉及应该由哪个CPU来运行唤醒迸程,是本地CPU或当前CPU(称为 wakeup_cpu,因为它调用了 wake_up_process() 函数)还是该进程之前运行的CPU(称为 prev_cpu)呢?

为了更好地解决这个问题,我们先从宏观上大致了解下唤醒进程的流程,如下图所示:

在这里插入图片描述

相信大家已经对唤醒进程有了宏观的认识了,下面为了使读者有更真切的理解,下文将根据流程图围绕源代码进行讲解这个过程:

wake_up_process()->try_to_wake_up()->select_task_rq()->...->select_task_rq_fair()

// select_task_rq 方法在 CFS 调度类中的实现
// 参数 p 表示将要唤醒的进程
// 参数 prev_cpu 表示该进程上一次调度运行的 CPU
// 参数 sd_flag 表示调度域的标志位
// 参数 wake_flags 表示唤醒标志位
static int
select_task_rq_fair(struct task_struct *p, int prev_cpu, int sd_flag, int wake_flags)
{
	struct sched_domain *tmp, *sd = NULL;
	// 变量 CPU 指的是 wakeup_cpu
	int cpu = smp_processor_id();
	// 变量 new_cpu 指的是 prev_cpu
	int new_cpu = prev_cpu;
	int want_affine = 0;
	// sync 表示是否需要同步
	int sync = (wake_flags & WF_SYNC) && !(current->flags & PF_EXITING);

	// 若 sd_flag 包含 SD_BALANCE_WAKE 标志位,表示这是一个唤醒进程的动作
	if (sd_flag & SD_BALANCE_WAKE) {
		...

		// want_affine 表示有机会采用 wake_up 或者 prev_cpu 来唤醒这个进程,这是一个快速优化路径
		want_affine = !wake_wide(p) && !wake_cap(p, cpu, prev_cpu) &&
			      cpumask_test_cpu(cpu, &p->cpus_allowed);
	}

	rcu_read_lock();
	// for_each_domain() 由 wake_up 开始,从下至上遍历调度域
	for_each_domain(cpu, tmp) {
		if (!(tmp->flags & SD_LOAD_BALANCE))
			break;

		if (want_affine && (tmp->flags & SD_WAKE_AFFINE) &&
		    cpumask_test_cpu(prev_cpu, sched_domain_span(tmp))) {
			if (cpu != prev_cpu)
				// wake_affine() 函数会重新计算 wakeup_cpu 和 prev_cpu 的负载情况,并且比较使用哪个 CPU
				// 来唤醒进程是最合适的。如果 wakeup_cpu 的负载加上唤醒进程的负载比 prev_cpu 的负载小,
				// 那么使用 wakeup_cpu 来唤醒进程;否则,使用 prev_cpu。
				new_cpu = wake_affine(tmp, p, cpu, prev_cpu, sync);

			// 当找到合适的 CPU 来唤醒进程后,设置 sd 为 NULL,并退出 for 循环
			sd = NULL; /* Prefer wake_affine over balance flags */
			break;
		}

		...
	}

	// 若没找到合适的调度域(因为在之前代码里,找到合适调度域的情况会设置 sd 为 NULL),那么进入慢速优化路径
	if (unlikely(sd)) {
		// 慢速优化路径:调用 find_idlest_cpu() 来查找最悠闲的 CPU
		new_cpu = find_idlest_cpu(sd, p, cpu, prev_cpu, sd_flag);
	} else if (sd_flag & SD_BALANCE_WAKE) { /* XXX always ? */
		// 快速优化路径:调用 select_idle_sibling() 函数来选择一个合适的 CPU
		new_cpu = select_idle_sibling(p, prev_cpu, new_cpu);

		if (want_affine)
			current->recent_used_cpu = cpu;
	}
	rcu_read_unlock();

	// 返回找到的合适 CPU
	return new_cpu;
}

符合快速优化路径的3个条件:

  1. 这是一个唤醒进程的动作,即 sd_flag 包含 SD_BALANCE_WAKE 标志位
  2. wakeup_cpu 和 prev_cpu 在同一个调度域
  3. 调度域包含 SD_WAKE_AFFINE 标志位,表示运行唤醒进程的 CPU 可以运行这个被唤醒的进程

2. 快速优化路径

下面来看一下快速优化路径处理函数select_idle_sibling()的实现:

// 快速优化路径处理函数,该函数优先选择空闲CPU,如果没找到空闲CPU,那么只能选择 prev_cpu 或 wakeup_cpu
// 参数 p 表示要唤醒的进程
// 参数 prev 表示 prev_cpu
// 参数 target 表示前面通过计算推荐的 CPU 
static int select_idle_sibling(struct task_struct *p, int prev, int target)
{
	...

	// available_idle_cpu() 检查 target 指向的 CPU 是否为空闲 CPU
	if (available_idle_cpu(target))
		return target;

	// cpus_share_cache() 函数判断两个 CPU 是否具有高速缓存的亲和性
	// 若 prev_cpu 和 target_cpu 不是同一个 CPU,但是它们具有高速缓存的亲和性并且 prev_cpu现在是空闲 CPU,
	// 那么也会优先选择 prev_cpu
	if (prev != target && cpus_share_cache(prev, target) && available_idle_cpu(prev))
		return prev;

	// p->recent_used_cpu 记录了进程最近经常使用的 CPU
	recent_used_cpu = p->recent_used_cpu;
	if (recent_used_cpu != prev &&
	    recent_used_cpu != target &&
	    // recent_used_cpu 和 target_cpu 具有共享高速缓存的亲和性
		cpus_share_cache(recent_used_cpu, target) &&
		// recent_used_cpu 现在是空闲 CPU
	    available_idle_cpu(recent_used_cpu) &&
		// recent_used_cpu 在进程允许运行的 CPU 位图里
	    cpumask_test_cpu(p->recent_used_cpu, &p->cpus_allowed)) {
            p->recent_used_cpu = prev;
            // 若满足上述条件,那么选择 recent_used_cpu 作为候选者,并且返回 recent_used_cpu
            return recent_used_cpu;
	}

	// 若不能找到合适的 CPU 来唤醒进程,那么只能从调度域里找了
	sd = rcu_dereference(per_cpu(sd_llc, target));
	if (!sd)
		return target;

	i = select_idle_core(p, sd, target);
	if ((unsigned)i < nr_cpumask_bits)
		return i;

	i = select_idle_cpu(p, sd, target);
	if ((unsigned)i < nr_cpumask_bits)
		return i;

	i = select_idle_smt(p, sd, target);
	if ((unsigned)i < nr_cpumask_bits)
		return i;

	// 如果上述遍历过程都没找到合适的CPU,那么只能返回 target_cpu
	return target;
}

3. 慢速优化路径

下面来看一下慢速优化路径处理函数find_idlest_cpu()的实现:

// 慢速优化路径
// 参数 sd 表示从这个调度域里查找的最合适的候选者
// 参数 p 表示将要唤醒的进程
// 参数 cpu 表示 wakeup_cpu
// 参数 prev_cpu 表示唤醒进程之前运行的 CPU
// 参数 sd_flag 表示调用者传递下来的调度域标志
static inline int find_idlest_cpu(struct sched_domain *sd, struct task_struct *p,
				  int cpu, int prev_cpu, int sd_flag)
{
	int new_cpu = cpu;

	// 若 sd 里的 cpu 都不在进程允许运行的 CPU 位图里,则直接返回 prev_cpu
	if (!cpumask_intersects(sched_domain_span(sd), &p->cpus_allowed))
		return prev_cpu;

	// 若 sd_flag 标志没有包含 SD_BALANCE_FORK,则说明不是因为 fork() 系统调用而调用到本函数的,
	// 调用 sync_entity_load_avg() 函数更新系统的负载信息
	if (!(sd_flag & SD_BALANCE_FORK))
		sync_entity_load_avg(&p->se);

	// 从 sd 开始,自上而下地遍历调度域
	while (sd) {
		...

		// 调用 find_idlest_group() 函数查找一个最空闲的调度组。遍历 sd 中所有的调度组,通过比较每个
		// 调度组中的量化负载来找出负载最小的一个调度组。
		group = find_idlest_group(sd, p, cpu, sd_flag);
		if (!group) {
			sd = sd->child;
			continue;
		}

		// find_idlest_group_cpu() 从上述调度组中找出一个负载最小的 CPU 作为最佳候选者
		new_cpu = find_idlest_group_cpu(group, p, cpu);
		if (new_cpu == cpu) {
			sd = sd->child;
			continue;
		}

		...
	}

	return new_cpu;
}

4. wake affine 特性

select_task_rq_fair()函数中的 wake affine 特性希望把被唤醒进程尽可能地运行在 wakeup_cpu 上,这样可以让—些有相关性的进程尽可能地运行在具有高速缓存共享的调度域中,获得一些高速缓存命中带来的性能提升。在task_struct数据结构中增加了两个成员——last_wakeewakee_flipswakee_flips 表示 waker(唤醒进程) 切换不同的 wakee(被唤醒进程) 的个数,即如果 waker 发现上次唤醒的进程不是 wakee 则 wakee_flips++。

但这也是一把“双刃剑”,如果一个 wakee 的 wakee_flips 值比较大,那么 waker 把这种 wakee 放到自身的 CPU 中来运行是比较危险的事情(类似于“引狼入室”),把 wakee 的下线 wakee 进程都放到自身的CPU上,加剧了CPU调度的竞争。另外, waker 的 wakee_flips 值比较大,说明很多进程依赖它来唤醒,waker的调度延迟会增大,再把新的 wakee 放进来显然不是好办法。因此代码中会通过判断来过滤上述情况。

static int
select_task_rq_fair(struct task_struct *p, int prev_cpu, int sd_flag, int wake_flags)
{
    ...
		// want_affine 表示有机会采用 wake_up 或者 prev_cpu 来唤醒这个进程,这是一个快速优化路径
		want_affine = !wake_wide(p) && !wake_cap(p, cpu, prev_cpu) &&
			      cpumask_test_cpu(cpu, &p->cpus_allowed);
    ...
}


static int wake_wide(struct task_struct *p)
{
	// 从当前任务结构体中获取 wakee_flips 值,存储在 master 变量中
    unsigned int master = current->wakee_flips;
	// 从传入的任务结构体中获取 wakee_flips 值,存储在 slave 变量中
    unsigned int slave = p->wakee_flips;
	// 从当前 CPU 的 sd_llc_size 变量中读取值,存储在 factor 变量中
    int factor = this_cpu_read(sd_llc_size); 

	// 如果当前任务的 wakee_flips 值小于传入任务的 wakee_flips 值,则交换它们的值
    if (master < slave)
		swap(master, slave);
	// 如果传入任务的 wakee_flips 值小于 factor,或者当前任务的 wakee_flips 值小于传入任务的 wakee_flips 值乘以 factor,则返回 0
    if (slave < factor || master < slave * factor)
		return 0;
	 // 否则返回 1
    return 1;
}
  • 18
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
Linux中的进程等待(Process Waiting)是指一个进程在执行时需要等待某些条件满足后才能继续执行的情况。 在Linux中,进程等待通常有以下几种情况: 1. I/O等待:当一个进程需要进行输入输出操作时,比如读写文件或者网络通信,由于这些操作是相对慢速的,进程需要等待数据的读取或写入完成才能继续执行。 2. 锁等待:多个进程访问临界资源时,为了避免竞态条件,需要使用锁来实现同步。当一个进程试图获取已经被其他进程占用的锁时,它会被阻塞,并等待锁被释放。 3. 睡眠等待:当一个进程调用了sleep()或wait()等系统调用后,它会主动释放CPU资源,并进入睡眠状态,等待指定的时间或者某个事件发生后才会被唤醒。 4. 信号等待:当一个进程正在等待某个信号的到来时,它会进入阻塞状态,直到该信号被发送给该进程进程才会被唤醒并继续执行。 针对进程等待的情况,Linux提供了一些机制来管理这些等待的进程,比如使用信号量、条件变量、管道等方式来实现进程间的同步与通信。此外,Linux还提供了一些工具和命令来查看进程等待的状态,比如top命令可以查看每个进程的等待时间,ps命令可以查看进程的状态等。 总之,Linux中的进程等待是一个重要的概念,合理管理进程等待可以提高系统的性能和资源利用率。进程等待是多任务操作系统中的常见现象,对于了解和掌握Linux进程管理至关重要。

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值