Linux内核源代码情景分析-wait()、schedule()

父进程执行wait4,并调用schedule切换到子进程:

wait4(child, NULL, 0, NULL);

像其他系统调用一样,wait4()在内核中的入口是sys_wait4(),代码如下:

asmlinkage long sys_wait4(pid_t pid,unsigned int * stat_addr, int options, struct rusage * ru)//pid为子进程的进程号
{
	int flag, retval;
	DECLARE_WAITQUEUE(wait, current);
	struct task_struct *tsk;

	if (options & ~(WNOHANG|WUNTRACED|__WNOTHREAD|__WCLONE|__WALL))
		return -EINVAL;

	add_wait_queue(current->wait_chldexit,&wait);
repeat:
	flag = 0;
	current->state = TASK_INTERRUPTIBLE;//父进程设置为可中断等待状态
	read_lock(&tasklist_lock);
	tsk = current;
	do {//第一层循环
		struct task_struct *p;
	 	for (p = tsk->p_cptr ; p ; p = p->p_osptr) {//第二层循环,从最年轻的子进程开始沿着由各个task_struct结构中的指针p_osptr所形成的链,找寻与所等待对象的pid相符的子进程、或符合其他一些条件的子进程
			if (pid>0) {
				if (p->pid != pid)//找到pid相符的子进程
					continue;
			} else if (!pid) {
				if (p->pgrp != current->pgrp)
					continue;
			} else if (pid != -1) {
				if (p->pgrp != -pid)
					continue;
			}
			/* Wait for all children (clone and not) if __WALL is set;
			 * otherwise, wait for clone children *only* if __WCLONE is
			 * set; otherwise, wait for non-clone children *only*.  (Note:
			 * A "clone" child here is one that reports to its parent
			 * using a signal other than SIGCHLD.) */
			if (((p->exit_signal != SIGCHLD) ^ ((options & __WCLONE) != 0))
			    && !(options & __WALL))
				continue;
			flag = 1;//说明pid是当前进程的子进程号
			switch (p->state) {
			case TASK_STOPPED:
				if (!p->exit_code)
					continue;
				if (!(options & WUNTRACED) && !(p->ptrace & PT_PTRACED))
					continue;
				read_unlock(&tasklist_lock);
				retval = ru ? getrusage(p, RUSAGE_BOTH, ru) : 0;
				if (!retval && stat_addr)
					retval = put_user((p->exit_code << 8) | 0x7f, stat_addr);
				if (!retval) {
					p->exit_code = 0;
					retval = p->pid;
				}
				goto end_wait4;//子进程处于停止状态,goto end_wait4
			case TASK_ZOMBIE:
				current->times.tms_cutime += p->times.tms_utime + p->times.tms_cutime;
				current->times.tms_cstime += p->times.tms_stime + p->times.tms_cstime;
				read_unlock(&tasklist_lock);
				retval = ru ? getrusage(p, RUSAGE_BOTH, ru) : 0;
				if (!retval && stat_addr)
					retval = put_user(p->exit_code, stat_addr);
				if (retval)
					goto end_wait4;
				retval = p->pid;
				if (p->p_opptr != p->p_pptr) {
					write_lock_irq(&tasklist_lock);
					REMOVE_LINKS(p);
					p->p_pptr = p->p_opptr;
					SET_LINKS(p);
					do_notify_parent(p, SIGCHLD);
					write_unlock_irq(&tasklist_lock);
				} else
					release_task(p);
				goto end_wait4;////子进程处于僵死状态,goto end_wait4
			default:
				continue;//否则继续第二层循环
			}
		}
		if (options & __WNOTHREAD)
			break;
		tsk = next_thread(tsk);//从同一个thread_group队列中找到下一个线程的task_struct结构
	} while (tsk != current);
	read_unlock(&tasklist_lock);
	if (flag) {//如果pid不是当前进程的子进程,直接到end_wait4
		retval = 0;
		if (options & WNOHANG)
			goto end_wait4;
		retval = -ERESTARTSYS;
		if (signal_pending(current))
			goto end_wait4;
		schedule();
		goto repeat;
	}
	retval = -ECHILD;
end_wait4:
	current->state = TASK_RUNNING;
	remove_wait_queue(¤t->wait_chldexit,&wait);
	return retval;
}

下列条件之一得到满足时才结束,goto end_wait4:

1、所等待的子进程的状态变成TASK_STOPPED,TASK_ZOMBIE;

2、所等待的子进程存在,可不在上述两个状态,而调用参数options中的WHONANG标志位为1,或者当前进程接受到了其他的信号;

3、进程号pid的那个进程根本不存在,或者不是当前进程的子进程。

否则,当前进程将其自身的状态设成TASK_INTERRUPTIBLE,并调用schedule()。

schedule,代码如下:

asmlinkage void schedule(void)
{
	struct schedule_data * sched_data;
	struct task_struct *prev, *next, *p;
	struct list_head *tmp;
	int this_cpu, c;

	if (!current->active_mm) BUG();//如果当前进程是个内核线程,那就没有用户空间,所以其mm指针为0,运行时就要暂时借用在它之前运行的那个进程的active_mm,所以active_mm一定不等于0
need_resched_back:
	prev = current;//当前进程赋值给prev
	this_cpu = prev->processor;

	if (in_interrupt())//只能由进程在内核中主动调用,或者在当前进程从系统空间返回用户空间的前夕被动地发生,而不能在一个中断服务程序的内部发生
		goto scheduling_in_interrupt;

	release_kernel_lock(prev, this_cpu);

	/* Do "administrative" work here while we don‘t hold any locks */
	if (softirq_active(this_cpu) & softirq_mask(this_cpu))//处理软中断
		goto handle_softirq;
handle_softirq_back:

	/*
	 * ‘sched_data‘ is protected by the fact that we can run
	 * only one process per CPU.
	 */
	sched_data = & aligned_data[this_cpu].schedule_data;

	spin_lock_irq(&runqueue_lock);

	/* move an exhausted RR process to be last.. */
	if (prev->policy == SCHED_RR)//见注释1
		goto move_rr_last;
move_rr_back:

	switch (prev->state) {
		case TASK_INTERRUPTIBLE://TASK_UNINTERRUPTIBLE和TASK_INTERRUPTIBLE的主要区别就在于此,TASK_UNINTERRUPTIBLE即使有信号等待处理,也不将其修改成TASK_RUNNING
			if (signal_pending(prev)) {//有信号等待处理时要将其改成TASK_RUNNING
				prev->state = TASK_RUNNING;
				break;
			}
		default:
			del_from_runqueue(prev);//sys_wait4中调用schedule时的状态为TASK_INTERRUPTIBLE,所以这里把这进程从可执行队列中撤下来
		case TASK_RUNNING://如果是TASK_RUNNING,即继续运行,那么这里不需要有什么特殊处理
	}
	prev->need_resched = 0;//刚开始need_reshced清0

	/*
	 * this is the scheduler proper:
	 */

repeat_schedule:
	/*
	 * Default process to select..
	 */
	next = idle_task(this_cpu);//目前是进程0,指向已知最佳的候选进程
	c = -1000;//目前是最低的权值,指向这个进程的综合权值
	if (prev->state == TASK_RUNNING)//如果当前进程想要继续运行
		goto still_running;

still_running_back:
	list_for_each(tmp, &runqueue_head) {//遍历可执行队列runqueue中的每个进程
		p = list_entry(tmp, struct task_struct, run_list);
		if (can_schedule(p, this_cpu)) {//单cpu中can_schedule永远为1
			int weight = goodness(p, this_cpu, prev->active_mm);//进程所具有的权值
			if (weight > c)//挑选出权值最大的
				c = weight, next = p;
		}
	}

	/* Do we need to re-calculate counters? */
	if (!c)//如果当前已经选择的进程(权值最高的进程)权值为0,那么就要重新计算各个进程的时间配额,参考注释2
		goto recalculate;
	/*
	 * from this point on nothing can prevent us from
	 * switching to the next task, save this fact in
	 * sched_data.
	 */
	sched_data->curr = next;
        ......
	spin_unlock_irq(&runqueue_lock);

	if (prev == next)//挑选出来的next就是当前进程
		goto same_process;

        ......

	kstat.context_swtch++;
	/*
	 * there are 3 processes which are affected by a context switch:
	 *
	 * prev == .... ==> (last => next)
	 *
	 * It‘s the ‘much more previous‘ ‘prev‘ that is on next‘s stack,
	 * but prev is set to (the just run) ‘last‘ process by switch_to().
	 * This might sound slightly confusing but makes tons of sense.
	 */
	prepare_to_switch();
	{
		struct mm_struct *mm = next->mm;
		struct mm_struct *oldmm = prev->active_mm;
		if (!mm) {
			if (next->active_mm) BUG();
			next->active_mm = oldmm;
			atomic_inc(&oldmm->mm_count);
			enter_lazy_tlb(oldmm, next, this_cpu);
		} else {
			if (next->active_mm != mm) BUG();
			switch_mm(oldmm, mm, next, this_cpu);
		}

		if (!prev->mm) {
			prev->active_mm = NULL;
			mmdrop(oldmm);
		}
	}

	/*
	 * This just switches the register state and the
	 * stack.
	 */
	switch_to(prev, next, prev);
	__schedule_tail(prev);

same_process:
	reacquire_kernel_lock(current);
	if (current->need_resched)//前面已经把当前进程的need_resched清0,如果现在又成了非0,则一定发生了中断并且情况发生了变化
		goto need_resched_back;

	return;

recalculate:
	{
		struct task_struct *p;
		spin_unlock_irq(&runqueue_lock);
		read_lock(&tasklist_lock);
		for_each_task(p)//对所有进程的循环,对不在runqueue的进程,也提升其时间配额,参考注释3
			p->counter = (p->counter >> 1) + NICE_TO_TICKS(p->nice);
		read_unlock(&tasklist_lock);
		spin_lock_irq(&runqueue_lock);
	}
	goto repeat_schedule;

still_running:
	c = goodness(prev, this_cpu, prev->active_mm);//那么挑选候选进程时以当前进程此刻的权值开始。这意味着,相对于权值相同的其它进程来说,当前进程优先
	next = prev;
	goto still_running_back;

handle_softirq:
	do_softirq();
	goto handle_softirq_back;

move_rr_last:
	if (!prev->counter) {//如果时间配额用完了
		prev->counter = NICE_TO_TICKS(prev->nice);
		move_last_runqueue(prev);//从可执行进程队列runqueue中当前的位置上移到队列的末尾,同时恢复其最初的时间配额,对于相同优先级的进程,调度的时候排在前面的进程优先,所以这使队列中具有相同优先级的其它进程有了优势
	}
	goto move_rr_back;

scheduling_in_interrupt:
	printk("Scheduling in interrupt\n");
	BUG();
	return;
}

注释1:

为了适应各种不同应用的需要,内核在此基础上实现了三种不同的政策:SCHED_FIFO、SCHED_RR以及SCHED_OTHER。每个进程都有自己使用的调度政策,并且进程还可以通过系统调用sched_setscheduler()设定自己使用的调度政策。其中SCHED_FIFO适合于时间性要求比较强、但每次运行所需的时间比较短的进程,实时的应用大都具有这样的特点。SCHED_RR中的“RR”表示“Round Robin”,是轮流的意思,这种政策适合比较大、也就是每次运行需时较长的进程。而除此二者之外的SCHED_OTHER,则为传统的调度政策,比较适合于交互式的分时应用。

当前进程prev的调度政策为SCHED_RR,即轮换调度。SCHED_RR和SCHED_FIFO都是基于优先级的调度政策,可是在怎样调度具有相同优先级的进程这个问题上二者有区别。调度策略为SCHED_FIFO的进程一旦受到调度而开始运行之后,就要一直运行到自愿让出或被优先级更高的进程剥夺为止。对于每次受到调度时要求运行时间不长的进程,这样并没有什么不妥。可是,如果是受到调度后可能会长时间运行的进程,那样就不公平了。这种不公正性是对具有相同优先级的进程而言。所以,对这样的进程应该实行SCHED_RR调度政策,这种政策在相同的优先级上实行轮换调度。

注释2:

此时所有runqueue的进程权值都为0,由于除init进程和调用了sched_yield()的进程以外,每个进程的权值最低为0,所以只要队列中有其他就绪进程存在就不可能为负数。这里要指出,队里中所有其他进程的权限都已降到0,说明这些进程的调度政策都是SCHED_OTHER,因为若有政策为SCHED_FIFO或SCHED_RR的进程存在,则权值至少也有100。

注释3:

for_each_task()是对所有进程的循环,而不是仅对就绪进程队列的循环。对于不在就绪进程队列中的非实时进程,这里得到了提升其时间配额、从而提升其综合权值的机会。不过,对综合权值的这种提升是很有限的,每次重新计算都将原有的时间配额减半,再与NICE_TO_TICKS(p->nice)相加,这样就决定了重新计算以后的综合权值永远也不可能达到NICE_TO_TICKS(p->nice)的两倍。因此,即使经过很长时间的"韬光养晦",也不能达到可与实时进程竞争的地步(综合权值至少是1000),所以只是对非实时进程之间的竞争有意义。至于实时进程,时间配额的增加并不会提升其综合权值,而且对于SCHED_FIFO进程则连时间配额也是没有意义的。

时间: 2024-10-10 10:03:43

Linux内核源代码情景分析-wait()、schedule()的相关文章

Linux内核源代码情景分析-mmap后,文件与虚拟区间建立映射

一.文件映射的页面换入 在mmap后,mmap参考Linux内核源代码情景分析-系统调用mmap(),当这个区间的一个页面首次受到访问时,会由于见面无映射而发生缺页异常,相应的异常处理程序do_no_page(). static inline int handle_pte_fault(struct mm_struct *mm, struct vm_area_struct * vma, unsigned long address, int write_access, pte_t * pte) {

Linux内核源代码情景分析-强制性调度

Linux内核中进程的强制性调度,也就是非自愿的.被动的.剥夺式的调度,主要是由时间引起的.前面讲过这种调度发生在中断,异常,系统调用从系统空间返回用户空间的前夕,也就是在ret_with_reschedule可以看出,此时是否真的调用schedule(),最终还要取决于当前进程task_struct结构中的need_resched是否为1(非0),因此,问题就结为当前进程的need_resched是在什么情况下才置成1的.主要有如下几种情况: 1.在时钟中断的服务程序中,发现当前进程(连续)运

Linux内核源代码情景分析-进程的创建,执行,等待,消亡

我们先看下面的程序: #include <stdio.h> int main() { int child; char *args[] = {"/bin/echo", "Hello", "World!", NULL}; if(!(child = fork())) { /* child */ execve("/bin/echo", args, NULL}); printf("I am back, someth

Linux内核源代码情景分析-内存管理之用户页面的定期换出

我们已经看到在分配页面时,如果页面数不够,那么会调用page_launder,reclaim_page,__free_page将页面换出,并重新投入分配. 为了避免总是在CPU忙碌的时候,也就是在缺页异常发生的时候,临时再来搜寻可供换出的内存页面并加以换出,Linux内核定期地检查并且预先将若干页面换出,腾出空间,以减轻系统在缺页异常发生时的负担. 为此,在Linux内核中设置了一个专司定期将页面换出的"守护神"kswapd和kreclaimd. static int __init k

Linux内核源代码情景分析-内存管理之slab-回收

在上一篇文章Linux内核源代码情景分析-内存管理之slab-分配与释放,最后形成了如下图的结构: 图 1 我们看到空闲slab块占用的若干页面,不会自己释放:我们是通过kmem_cache_reap和kmem_cache_shrink来回收的.他们的区别是: 1.我们先看kmem_cache_shrink,代码如下: int kmem_cache_shrink(kmem_cache_t *cachep) { if (!cachep || in_interrupt() || !is_chaine

Linux内核源代码情景分析-系统调用mknod

普通文件可以用open或者create创建,FIFO文件可以用pipe创建,mknod主要用于设备文件的创建. 在内核中,mknod是由sys_mknod实现的,代码如下: asmlinkage long sys_mknod(const char * filename, int mode, dev_t dev) //比如filename为/tmp/server_socket,dev是设备号 { int error = 0; char * tmp; struct dentry * dentry;

Linux内核源代码情景分析-文件系统的安装

执行sudo mount -t ext2 /dev/sdb1 /mnt/sdb,将文件系统挂在到/mnt/sdb上.系统调用mount,映射到内核层执行的是sys_mount.假设/dev/sdb1和/mnt/sdb都位于ext2文件系统中. asmlinkage long sys_mount(char * dev_name, char * dir_name, char * type, unsigned long flags, void * data)//dev_name指向了"/dev/sdb

Linux内核源代码情景分析-访问权限与文件安全性

在Linux内核源代码情景分析-从路径名到目标节点,一文中path_walk代码中,err = permission(inode, MAY_EXEC)当前进程是否可以访问这个节点,代码如下: int permission(struct inode * inode,int mask) { if (inode->i_op && inode->i_op->permission) { int retval; lock_kernel(); retval = inode->i_

Linux内核源代码情景分析-文件系统安装后的访问

在Linux内核源代码情景分析-文件系统的安装,一文中,已经调用sudo mount -t ext2 /dev/sdb1 /mnt/sdb,在/mnt/sdb节点上挂载了文件系统,那么我们接下来访问/mnt/sdb/hello.c节点.我们来看一下path_walk的执行有什么不同? int path_walk(const char * name, struct nameidata *nd) { struct dentry *dentry; struct inode *inode; int er

Linux内核源代码情景分析-内存管理

用户空间的页面有下面几种: 1.普通的用户空间页面,包括进程的代码段.数据段.堆栈段.以及动态分配的"存储堆". 2.通过系统调用mmap()映射到用户空间的已打开文件的内容. 3.进程间的共享内存区. 这些页面的的周转有两方面的意思. 1.页面的分配,使用,回收.如进程压栈时新申请的页面,这类页面不进行盘区交换,不使用时释放得以回收. 这部分通过一个场景来解释: Linux内核源代码情景分析-内存管理之用户堆栈的扩展. 2.盘区交换.如要执行硬盘上的对应代码段.把硬盘上的代码段换入内