Linux内核对比学习系列(4)——进程休眠与唤醒

前言

进程休眠与唤醒也是内核管理的重要一部分。本是进程调度相关内容,笔者在此单拎出来进行梳理。同样的,主要对比 linux0.12 与 linux2.6 之间的差异。

流程梳理

自然地,让一个进程休眠,我们只需要将其状态更改为TASK_INTERRUPTIBLE或者TASK_UNINTERRUPTIBLE,接着再执行调度程序 schedule() 即可。由于调度程序只会调度状态为TASK_RUNNING的进程,因此被修改的进程不会被调度,看上去就像"休眠"了。而唤醒则更简单,只需要将待唤醒进程状态改为 TASK_RUNNING 即可,等待调度就能够被“唤醒”。

然而,由于休眠可能是为了等待某一类资源的使用或者某一条件的满足,当多个进程同时请求某类资源而都进入休眠状态时,需要使用一个等待队列进行管理。因此,我们可以简单地认为,实现进程的休眠和唤醒需要:

  1. 设计一个等待队列,用于关联正在休眠的进程
  2. 修改进程状态并执行调度

Linux0.12

该版本通过sleep_on()wake_up()进行进程的休眠与唤醒。代码中没有显式地使用一个等待队列关联休眠进程,而是通过指针直接串联task_struct结构体来实现。其原理在《Linux内核完全注释》中有详细说明,如下:

当刚进入该函数时,队列头指针*p 指向已经在等待队列中等待的任务结构(进程描述符)。当然,在系统刚开始执行时,等待队列上无等待任务。因此上图中原等待任务在刚开始时不存在,此时*p 指向NULL。通过指针操作,在调用调度程序之前,队列头指针指向了当前任务结构,而函数中的临时指针 tmp 指向了原等待任务。在执行调度程序并在本任务被唤醒重新返回执行之前,当前任务指针被指向新的当前任务,并且 CPU 切换到该新的任务中执行。这样本次 sleep_on()函数的执行使得 tmp 指针指向队列中队列头指针指向的原等待任务,而队列头指针则指向此次新加入的等待任务,即调用本函数的任务。
在这里插入图片描述
从而通过堆栈上该临时指针 tmp 的链接作用,在几个进程为等待同一资源而多次调用该函数时,内核程序就隐式地构筑出一个等待队列,参见图 8-7 中的等待队列示意图。图中示出了当向队列头部插入第三个任务时的情况。从图中我们可以更容易理解 sleep_on()函数的等待队列形成过程。在这里插入图片描述

linux-0.12\kernel\sched.c

void sleep_on(struct task_struct **p)
{
	__sleep_on(p,TASK_UNINTERRUPTIBLE);
}
static inline void __sleep_on(struct task_struct **p, int state)
{
	struct task_struct *tmp;

	if (!p)
		return;
	if (current == &(init_task.task))
		panic("task[0] trying to sleep");
	tmp = *p;
	*p = current;
	current->state = state;
repeat:	schedule();
	if (*p && *p != current) {
		(**p).state = 0;
		current->state = TASK_UNINTERRUPTIBLE;
		goto repeat;
	}
	if (!*p)
		printk("Warning: *P = NULL\n\r");
	if (*p = tmp)
		tmp->state=0;
}

通过wake_up()唤醒时,只需要修改对应进程状态

void wake_up(struct task_struct **p)
{
	if (p && *p) {
		if ((**p).state == TASK_STOPPED)
			printk("wake_up: TASK_STOPPED");
		if ((**p).state == TASK_ZOMBIE)
			printk("wake_up: TASK_ZOMBIE");
		(**p).state=0;
	}
}

如上所示,该版本对于进程休眠与唤醒实现十分简单,也没有考虑条件是否满足等因素。所谓麻雀虽小五脏俱全,在此基础上我们能够理解内核运行的本质也是极好的。

Linux2.6

该版本在实现上,除了显式的设计了等待队列,还进一步完善了整个休眠和唤醒的逻辑。并且由于目前内核处于多处理器且进程可抢占的环境下,需要考虑上锁,条件判断等问题。

在该版本中,休眠的入口变成了wait_event(),并且是个宏定义。由下述代码可知,休眠当前进程需要执行几个步骤:

  1. 通过 DEFINE_WAIT 为当前进程构建一个等待队列项 __wait
  2. 进入 for 循环中,通过 prepare_to_wait() 将 __wait 插入 wq 等待队列中,并将当前进程状态变为TASK_UNINTERRUPTIBLE
  3. 执行调度前,判断condition是否已经满足,如果满足则无需调度直接结束循环。否则进行调度
  4. 结束循环后,通过 finish_wait() 从 __wait 中获取当前进程,并将其状态置为 TASK_RUNNING

\include\linux\wait.h

#define wait_event(wq, condition) 					\
do {									\
	if (condition)	 						\
		break;							\
	__wait_event(wq, condition);					\
} while (0)

#define __wait_event_timeout(wq, condition, ret)			\
do {									\
	DEFINE_WAIT(__wait);						\
									\
	for (;;) {							\
		prepare_to_wait(&wq, &__wait, TASK_UNINTERRUPTIBLE);	\
		if (condition)						\
			break;						\
		ret = schedule_timeout(ret);				\
		if (!ret)						\
			break;						\
	}								\
	finish_wait(&wq, &__wait);					\
} while (0)

上述过程涉及了内核对于队列的使用,可以发现,Linux内核并不会将进程描述符 task_struct 直接插入链表中,而是为其构建了一个队列项,在这里是等待队列项 wait_queue_t ,通过 list_head 进行链表串联。这么做的好处是,无需为 task_struct 添加多种队列相关的 list_head

struct __wait_queue {
	unsigned int flags;
#define WQ_FLAG_EXCLUSIVE	0x01
	void *private;
	wait_queue_func_t func;
	struct list_head task_list;
};

#define DEFINE_WAIT_FUNC(name, function)				\
	wait_queue_t name = {						\
		.private	= current,				\
		.func		= function,				\
		.task_list	= LIST_HEAD_INIT((name).task_list),	\
	}

#define DEFINE_WAIT(name) DEFINE_WAIT_FUNC(name, autoremove_wake_function)

具体地,prepare_to_wait() 函数涉及同步操作,利用锁,完成链表的插入。后续 schedule() 的逻辑就不重复了,前篇文章有介绍

void
prepare_to_wait(wait_queue_head_t *q, wait_queue_t *wait, int state)
{
	unsigned long flags;

	wait->flags &= ~WQ_FLAG_EXCLUSIVE;
	spin_lock_irqsave(&q->lock, flags);
	if (list_empty(&wait->task_list))
		__add_wait_queue(q, wait);
	set_current_state(state);
	spin_unlock_irqrestore(&q->lock, flags);
}

wake_up() 用于根据队列头,将某个等待队列上所有进程唤醒。实际上就是上锁,修改链表上进程的状态,并将包含进程的队列项从链表队列种删除
kernel\sched.c

#define wake_up(x)			__wake_up(x, TASK_NORMAL, 1, NULL)

void __wake_up(wait_queue_head_t *q, unsigned int mode,
			int nr_exclusive, void *key)
{
	unsigned long flags;

	spin_lock_irqsave(&q->lock, flags);
	__wake_up_common(q, mode, nr_exclusive, 0, key);
	spin_unlock_irqrestore(&q->lock, flags);
}

__wake_up_common() 为主唤醒函数,通过宏定义遍历整个链表,并执行队列项的 func。这里有点像回调的实现。而 func 在上述休眠过程中,构建队列项时设置,即DEFINE_WAIT 将autoremove_wake_function() 作为默认的唤醒方法。

static void __wake_up_common(wait_queue_head_t *q, unsigned int mode,
			int nr_exclusive, int wake_flags, void *key)
{
	wait_queue_t *curr, *next;

	list_for_each_entry_safe(curr, next, &q->task_list, task_list) {
		unsigned flags = curr->flags;

		if (curr->func(curr, mode, wake_flags, key) &&
				(flags & WQ_FLAG_EXCLUSIVE) && !--nr_exclusive)
			break;
	}
}

autoremove_wake_function() 进一步调用了 default_wake_function() 进行处理,该函数封装了try_to_wake_up()

int autoremove_wake_function(wait_queue_t *wait, unsigned mode, int sync, void *key)
{
	int ret = default_wake_function(wait, mode, sync, key);

	if (ret)
		list_del_init(&wait->task_list);
	return ret;
}

try_to_wake_up() 该函数比较长(省略了smp相关代码)。此时的输入已经是从队列项中取出的 task_struct 进程描述符。可以直接对其进行状态的修改。调用 activate_task() 将当前进程放入调度器的就绪队列中。最后将状态改为 TASK_RUNNING

static int try_to_wake_up(struct task_struct *p, unsigned int state,
			  int wake_flags)
{
	int cpu, orig_cpu, this_cpu, success = 0;
	unsigned long flags;
	struct rq *rq;

	if (!sched_feat(SYNC_WAKEUPS))
		wake_flags &= ~WF_SYNC;

	this_cpu = get_cpu();

	smp_wmb();
	rq = task_rq_lock(p, &flags);
	update_rq_clock(rq);
	if (!(p->state & state))
		goto out;

	if (p->se.on_rq)
		goto out_running;

	cpu = task_cpu(p);
	orig_cpu = cpu;


	schedstat_inc(p, se.nr_wakeups);
	if (wake_flags & WF_SYNC)
		schedstat_inc(p, se.nr_wakeups_sync);
	if (orig_cpu != cpu)
		schedstat_inc(p, se.nr_wakeups_migrate);
	if (cpu == this_cpu)
		schedstat_inc(p, se.nr_wakeups_local);
	else
		schedstat_inc(p, se.nr_wakeups_remote);
	activate_task(rq, p, 1);
	success = 1;
out_running:
	trace_sched_wakeup(rq, p, success);
	check_preempt_curr(rq, p, wake_flags);

	p->state = TASK_RUNNING;
out:
	task_rq_unlock(rq, &flags);
	put_cpu();

	return success;
}

activate_task() 通过 enqueue_task() 将进程放入就绪队列中,对于CFS调度器而言,就绪队列为红黑树

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

	enqueue_task(rq, p, wakeup, false);
	inc_nr_running(rq);
}

梳理到此,突然有问题,即唤醒需要放回就绪队列,休眠时不需要从就绪队列拿出吗?以及condition如何被修改呢?不同进程能够同时访问condition吗?不然休眠进程如何才能被其他进程唤醒呢

思考记录

Q: 进程休眠时需从就绪队列中拿出吗?
A: 需要的。之前分析schedule()时没注意,该函数会判断当前进程状态是否非运行态,若是则通过 deactivate_task() 将其从就绪队列中摘出

asmlinkage void __sched schedule(void)
{
	struct task_struct *prev, *next;
	unsigned long *switch_count;
	struct rq *rq;
	int cpu;

need_resched:
	preempt_disable();
	cpu = smp_processor_id();
	rq = cpu_rq(cpu);
	rcu_sched_qs(cpu);
	prev = rq->curr;
	//-------省略部分代码---------

	if (prev->state && !(preempt_count() & PREEMPT_ACTIVE)) {
		if (unlikely(signal_pending_state(prev->state, prev)))
			prev->state = TASK_RUNNING;
		else
			deactivate_task(rq, prev, 1);
		switch_count = &prev->nvcsw;
	}

	//-------省略部分代码---------
}

Q:condition可以被多进程共享吗?
A:如果 condition 在内核空间,则能够被进程共享。不管是 Linux0.12 还是 Linux2.6 ,尽管两个对于用户空间与内核空间分布上的设计不相同,但内核地址空间指向的物理地址是相同的。Linux0.12在内核学习系列中以及对此进行了分析,而 Linux2.6 中,目前个人水平较低,只能根据资料书进行判断(但本质应该都是差不多的,无外乎内核页表映射),如:

无论当前哪个用户进程处于活动状态,虚拟地址空间内核部分的内容总是同样的。取决于具体的硬件,这可能是通过操作各用户进程的页表,使得虚拟地址空间的上半部看上去总是相同的。也可能是指示处理器为内核提供一个独立的地址空间,映射在各个用户地址空间之上。
------《深入Linux内核架构》4.2节内容

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值