linux操作系统进程调度与进程切换

Posted 西邮菜

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了linux操作系统进程调度与进程切换相关的知识,希望对你有一定的参考价值。

一、进程调度是由schedule函数来实现的

首先进程有五个状态:运行状态、可中断睡眠状态、不可中断睡眠状态、暂停状态、僵死状态。

#define TASK_RUNNING		0 可以运行状态
#define TASK_INTERRUPTIBLE	1 可以使用信号来唤醒变为可运行状态(例:父进程在等待子进程时)
#define TASK_UNINTERRUPTIBLE	2 只能利用wakeup唤醒变为可运行状态
#define TASK_ZOMBIE		3 收到SIGSTOP、SIGTSTP、SIGTTIN三种信号暂停
#define TASK_STOPPED		4 进程停止运行,但父进程未收尸

         在schedule中,首先给设立闹钟时间且已到点的进程发送信号,如果进程有非阻塞信号、且进程状态是可中断状态,则将其状态设置为就绪态。在while中遍历每个进程,找出可运行且时间片最大的那个进程,最后判断c的值,如果最大c的值为0,那么代表所有进程时间片全用完了,用for循环根据任务优先级重新分配时间片,如果最大c的值不为0,则退出循环,并执行switch_to()函数进行任务切换。

void schedule(void)

	int i,next,c;
	struct task_struct ** p;

/* check alarm, wake up any interruptible tasks that have got a signal */

    // 从任务数组中最后一个任务开始循环检测alarm。在循环时跳过空指针项。
	for(p = &LAST_TASK ; p > &FIRST_TASK ; --p)
		if (*p) 
            // 如果设置过任务的定时值alarm,并且已经过期(alarm<jiffies),则在
            // 信号位图中置SIGALRM信号,即向任务发送SIGALARM信号。然后清alarm。
            // 该信号的默认操作是终止进程。jiffies是系统从开机开始算起的滴答数(10ms/滴答)。
			if ((*p)->alarm && (*p)->alarm < jiffies) 
					(*p)->signal |= (1<<(SIGALRM-1));
					(*p)->alarm = 0;
				
            // 如果信号位图中除被阻塞的信号外还有其他信号,并且任务处于可中断状态,则
            // 置任务为就绪状态。其中'~(_BLOCKABLE & (*p)->blocked)'用于忽略被阻塞的信号,但
            // SIGKILL 和SIGSTOP不能呗阻塞。
			if (((*p)->signal & ~(_BLOCKABLE & (*p)->blocked)) &&
			(*p)->state==TASK_INTERRUPTIBLE)
				(*p)->state=TASK_RUNNING;
		

/* this is the scheduler proper: */

	while (1) 
		c = -1;
		next = 0;
		i = NR_TASKS;
		p = &task[NR_TASKS];
        // 这段代码也是从任务数组的最后一个任务开始循环处理,并跳过不含任务的数组槽。比较
        // 每个就绪状态任务的counter(任务运行时间的递减滴答计数)值,哪一个值大,运行时间还
        // 不长,next就值向哪个的任务号。
		while (--i) 
			if (!*--p)
				continue;
			if ((*p)->state == TASK_RUNNING && (*p)->counter > c)
				c = (*p)->counter, next = i;
		
        // 如果比较得出有counter值不等于0的结果,或者系统中没有一个可运行的任务存在(此时c
        // 仍然为-1,next=0),则退出while(1)_的循环,执行switch任务切换操作。否则就根据每个
        // 任务的优先权值,更新每一个任务的counter值,然后回到while(1)循环。counter值的计算
        // 方式counter=counter/2 + priority.注意:这里计算过程不考虑进程的状态。
		if (c) break;
		for(p = &LAST_TASK ; p > &FIRST_TASK ; --p)
			if (*p)
				(*p)->counter = ((*p)->counter >> 1) +
						(*p)->priority;
	
    // 用下面的宏把当前任务指针current指向任务号Next的任务,并切换到该任务中运行。上面Next
    // 被初始化为0。此时任务0仅执行pause()系统调用,并又会调用本函数。
	switch_to(next);     // 切换到Next任务并运行。

二、进程切换是由switch_to()函数实现的

        一段宏定义代码,首先比较切换的任务是不是当前进程,如果不是,则将current与ecx寄存器转化,

进程切换两步:

        1、将任务指针付给current指针。

        2、执行上下文切换。

#define switch_to(n) \\
struct long a,b; __tmp; \\
__asm__("cmpl %%ecx,current\\n\\t" \\
	"je 1f\\n\\t" \\
	"movw %%dx,%1\\n\\t" \\
	"xchgl %%ecx,current\\n\\t" \\
	"ljmp *%0\\n\\t" \\
	"cmpl %%ecx,last_task_used_math\\n\\t" \\
	"jne 1f\\n\\t" \\
	"clts\\n" \\
	"1:" \\
	::"m" (*&__tmp.a),"m" (*&__tmp.b), \\
	"d" (_TSS(n)),"c" ((long) task[n])); \\

三、休眠进程sleep_on()

        当某个进程想访问CPU资源时,但访问不到时,调用该函数将进程休眠等待资源被释放。该函数可以形成一个进程等待链表。

        函数执行过程为如果为0号进程则进行提示,将 进程插入等待队列中,并将进程状态改为不可中断睡眠状态(只有wakeup可以唤醒)。然后进行任务调度。在这函数里面需要注意的是等待队列的形成,里面tmp指针是局部变量,每个进程都拥有。

形成类似下图的等待队列:

 

// 把当前任务置为不可中断的等待状态,并让睡眠队列指针指向当前任务。
// 只有明确的唤醒时才会返回。该函数提供了进程与中断处理程序之间的同步机制。函数参数P是等待
// 任务队列头指针。指针是含有一个变量地址的变量。这里参数p使用了指针的指针形式'**p',这是因为
// C函数参数只能传值,没有直接的方式让被调用函数改变调用该函数程序中变量的值。但是指针'*p'
// 指向的目标(这里是任务结构)会改变,因此为了能修改调用该函数程序中原来就是指针的变量的值,
// 就需要传递指针'*p'的指针,即'**p'.
void sleep_on(struct task_struct **p)

	struct task_struct *tmp;

    // 若指针无效,则退出。(指针所指向的对象可以是NULL,但指针本身不应该为0).另外,如果
    // 当前任务是任务0,则死机。因为任务0的运行不依赖自己的状态,所以内核代码把任务0置为
    // 睡眠状态毫无意义。
	if (!p)
		return;
	if (current == &(init_task.task))
		panic("task[0] trying to sleep");
    // 让tmp指向已经在等待队列上的任务(如果有的话),例如inode->i_wait.并且将睡眠队列头的
    // 等等指针指向当前任务。这样就把当前任务插入到了*p的等待队列中。然后将当前任务置为
    // 不可中断的等待状态,并执行重新调度。
	tmp = *p;
	*p = current;
	current->state = TASK_UNINTERRUPTIBLE;
	schedule();
    // 只有当这个等待任务被唤醒时,调度程序才又返回到这里,表示本进程已被明确的唤醒(就
    // 续态)。既然大家都在等待同样的资源,那么在资源可用时,就有必要唤醒所有等待该该资源
    // 的进程。该函数嵌套调用,也会嵌套唤醒所有等待该资源的进程。这里嵌套调用是指一个
    // 进程调用了sleep_on()后就会在该函数中被切换掉,控制权呗转移到其他进程中。此时若有
    // 进程也需要使用同一资源,那么也会使用同一个等待队列头指针作为参数调用sleep_on()函数,
    // 并且也会陷入该函数而不会返回。只有当内核某处代码以队列头指针作为参数wake_up了队列,
    // 那么当系统切换去执行头指针所指的进程A时,该进程才会继续执行下面的代码,把队列后一个
    // 进程B置位就绪状态(唤醒)。而当轮到B进程执行时,它也才可能继续执行下面的代码。若它
    // 后面还有等待的进程C,那它也会把C唤醒等。在这前面还应该添加一行:*p = tmp.
	if (tmp)                    // 若在其前还有存在的等待的任务,则也将其置为就绪状态(唤醒).
		tmp->state=0;

四、wakeup()

// 唤醒*p指向的让任务。*p是任务等待队列头指针。由于新等待任务是插入在等待队列头指针处的,
// 因此唤醒的是最后进入等待队列的任务。
void wake_up(struct task_struct **p)

	if (p && *p) 
		(**p).state=0;          // 置为就绪(可运行)状态TASK_RUNNING.
		*p=NULL;
	

以上是关于linux操作系统进程调度与进程切换的主要内容,如果未能解决你的问题,请参考以下文章

进程调度与切换简单总结

Linux 操作系统原理 — 用户进程与内核线程的调度策略与切换开销

linux操作系统进程调度与进程切换

理解进程调度时机跟踪分析进程调度与进程切换的过程(Linux)

进程调度

Linux 内核Linux 内核体系架构 ( 进程调度 | 内存管理 | 中断管理 | 设备管理 | 文件系统 )