Linux0.11内核--进程的调度(运行态(就绪态)和睡眠态之间的转换)

一、Linux进程的状态

Linux0.11内核--进程的调度(运行态(就绪态)和睡眠态之间的转换)_第1张图片

Linux0.11内核--进程的调度(运行态(就绪态)和睡眠态之间的转换)_第2张图片

 

二、基本信息1---侧重C语言描述

当进程等待资源或者事件时,就进入睡眠状态。
有两种睡眠态:不可中断睡眠态( TASK_UNINTERRUPTIBLE)
                         可中断睡眠态( TASK_INTERRUPTIBLE)
处于不可中断睡眠态的进程:可以由 wake_up直接唤醒
处于可中断睡眠态的进程:不光可以由 wake_up直接唤醒,还可以由信号唤醒。

#define TASK_RUNNING            0  //进程正在运行或已准备就绪。 
#define TASK_INTERRUPTIBLE      1  //进程处于可中断等待状态。 
#define TASK_UNINTERRUPTIBLE    2  //进程处于不可中断等待状态,主要用于I/O操作等待。 
#define TASK_ZOMBIE             3  //进程处于僵死状态,已经停止运行,但父进程还没发信号。
#define TASK_STOPPED            4  //进程已停止。

系统资源

系统资源 :i节点中的 i_wait,高速缓冲块中的 b_wait,超级块中的 s_wait

struct m_inode {
	unsigned short i_mode;
	unsigned short i_uid;
	unsigned long i_size;
	unsigned long i_mtime;
	unsigned char i_gid;
	unsigned char i_nlinks;
	unsigned short i_zone[9];
/* these are in memory also */
	struct task_struct * i_wait;        //等待该i节点的进程
	unsigned long i_atime;
	unsigned long i_ctime;
	unsigned short i_dev;
	unsigned short i_num;
	unsigned short i_count;
	unsigned char i_lock;
	unsigned char i_dirt;
	unsigned char i_pipe;
	unsigned char i_mount;
	unsigned char i_seek;
	unsigned char i_update;
};
struct buffer_head {
    char * b_data;           
    unsigned long b_blocknr;  
    unsigned char b_uptodate;
    unsigned char b_dirt;       
    unsigned char b_count;        
    unsigned char b_lock;        
    struct task_struct * b_wait;   //指向等待该缓冲区解锁的任务
    struct buffer_head * b_prev;
    struct buffer_head * b_next;
    struct buffer_head * b_prev_free;
    struct buffer_head * b_next_free;
};
struct super_block {
	unsigned short s_ninodes;
	unsigned short s_nzones;
	unsigned short s_imap_blocks;
	unsigned short s_zmap_blocks;
	unsigned short s_firstdatazone;
	unsigned short s_log_zone_size;
	unsigned long s_max_size;
	unsigned short s_magic;
/* These are only in memory */
	struct buffer_head * s_imap[8];
	struct buffer_head * s_zmap[8];
	unsigned short s_dev;
	struct m_inode * s_isup;
	struct m_inode * s_imount;
	unsigned long s_time;
	struct task_struct * s_wait;        //等待该超级块的进程
	unsigned char s_lock;
	unsigned char s_rd_only;
	unsigned char s_dirt;
};

Linux 0.11的调度函数schedule()

#define NR_TASKS 64
#define LAST_TASK task[NR_TASKS-1]

#define SIGALRM		14   //实时定时器报警(在signil.h中定义)

extern struct task_struct *current;//定义在sched.h,给 switch_to用
#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])); 
}

void schedule(void)      //调度就是从就绪队列中选择一个让它来执行
{
	int i,next,c;
	struct task_struct ** p;

    // 从任务数组中最后一个任务开始循环检测alarm(实时定时器报警)。在循环时跳过空指针项。
	for(p = &LAST_TASK ; p > &FIRST_TASK ; --p)
	  if (*p) 
         {

            // 如果设置过任务的定时值alarm,并且已经过期(alarmalarm && (*p)->alarm < jiffies)
               {
 					(*p)->signal |= (1<<(SIGALRM-1)); //向任务发送SIGALARM信号(信号竟然是 
                                                      //这样发送的)
					(*p)->alarm = 0;
			   }

            // 如果信号位图中除被阻塞的信号外还有其他信号,并且任务处于可中断状态(支持信号            
            // 唤醒),则置任务为就绪状态。其中'~(_BLOCKABLE & (*p)->blocked)'用于忽略被阻塞
            // 的信号,但SIGKILL 和SIGSTOP不能被阻塞。
			if (((*p)->signal & ~(_BLOCKABLE & (*p)->blocked)) &&
			(*p)->state==TASK_INTERRUPTIBLE)
				(*p)->state=TASK_RUNNING;

		 }


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任务并运行。
}


不可中断睡眠态sleep_on() 

  参数:p 等待队列头 (队列头有:i节点中的 i_wait;高速缓冲块中的 b_wait;超级块中的 s_wait)

extern struct m_inode inode_table[NR_INODE];      //i节点表数组(32项)
extern struct buffer_head * start_buffer;         //缓冲区起始内存位置(缓冲块是比i节点,超级块处理复杂一点的哈希表)
extern struct super_block super_block[NR_SUPER];  //超级块数组(8项)

/****************************************************************************/ 
/* 功能:当前进程进入不可中断睡眠态,挂起在等待队列上 */ 
/* 参数:p 等待队列头 */ 
/* 返回:(无) */ /****************************************************************************/
 void sleep_on(struct task_struct **p) 
{ 

  struct task_struct *tmp;           // tmp用来指向等待队列上的下一个进程 
  if (!p)                           // 无效指针,退出 
    return; 
  if (current == &(init_task.task)) // 进程0不能睡眠
    panic("task[0] trying to sleep");

  tmp = *p;                        // 下面两句把当前进程放到等待队列头,等待队列是以堆栈方式管理的。后到的进程等在前面  
  *p = current;        //将i节点中的i_wait或超级块中的s_wait置成"当前进程current",
                       //"当前进程current"是下面schedule执行witch_to(next)中相当于crrent=next的语句前,哪个进程,即没有被置为next前的“当前进程current"     
  current->state = TASK_UNINTERRUPTIBLE; // 进程进入不可中断睡眠状态 
  schedule();                     // 进程放弃CPU使用权,重新调度进程

 // 当前进程被wake_up()唤醒后,从这里(代码schedule()后)开始运行。 
 // 既然等待的资源可以用了,就应该唤醒等待队列上的所有进程,让它们再次争夺 
 // 资源的使用权(schedule调度就是从就绪队列中选择一个让它来执行,state不为0没有资格争夺)。这里让队列里的下一个进程也进入运行态。这样当这个进程运行 
 // 时,它又会唤醒下下个进程。最终唤醒所有进程。
 
  if (tmp) 
     tmp->state=0;// 唤醒队列中的上一个(tmp)睡眠进程。0 换作 TASK_RUNNING 更好
 }

下图红色8箭头是 sleep_on中的struct task_struct **p和current

Linux0.11内核--进程的调度(运行态(就绪态)和睡眠态之间的转换)_第3张图片

Linux0.11内核--进程的调度(运行态(就绪态)和睡眠态之间的转换)_第4张图片

进入可中断睡眠态可调用interruptible_sleep_on() 

interruptible  adj.可中断的

/****************************************************************************/
 /* 功能:当前进程进入可中断睡眠态,挂起在等待队列上 */
 /* 参数:p 等待队列头 */
 /* 返回:(无) */ /****************************************************************************/
 void interruptible_sleep_on(struct task_struct **p)
 {
  struct task_struct *tmp; // tmp用来指向等待队列上的下一个进程
  if (!p) // 无效指针,退出 
     return; 
  if (current == &(init_task.task)) // 进程0不能睡眠
     panic("task[0] trying to sleep");
 
  tmp=*p; // 和sleep_on()一样,构建隐式队列
 *p=current; 
repeat: current->state = TASK_INTERRUPTIBLE; // 当前进程状态变成可中断睡眠态 

  schedule(); // 重新调度进程 
              // 当进程苏醒后,从这里继续运行 

  if (*p && *p != current) 
     { 
      // 如果当前进程之前还有进程,这把头进程唤醒, 
      (**p).state=0; // 自己进入睡眠态。这样做为了保证队列栈式管理 
      goto repeat;
     } 

 *p=NULL; // 和wake_up()一样 

 if (tmp) // 产生了游离队列,需要把头进程唤醒 
    tmp->state=0;
 }

进入可中断睡眠态可调用sys_pause()     

pause  n.暂停; 停顿

// 转换当前任务状态为可中断的等待状态,并重新调度。
// 该系统调用将导致进程进入睡眠状态,知道收到一个信号。该信号用于终止进程或者使进程调用
// 一个信号捕获函数。只有当捕获了一个信号,并且信号捕获处理函数返回,pause()才会返回。此时
// pause()返回值应该是-1,并且errno被置为EINTR。这里还没有完全实现(直到0.95版)
int sys_pause(void)
{
	current->state = TASK_INTERRUPTIBLE;
	schedule();
	return 0;
}

进入可中断睡眠态可调用sys_waitpid()             

pid是task_struck的成员,表示进程号

 系统调用waipid().挂起当前进程,直到pid指定的子进程退出(终止)或收到要求终止该进程的信号,
// 或者是需要调用一个信号句柄(信号处理程序)。如果pid所指向的子进程早已退出(已成所谓的僵死进程),
// 则本调用将立刻返回。子进程使用的所有资源将释放。
// 如果pid > 0,表示等待进程号等于pid的子进程。
// 如果pid = 0, 表示等待进程组号等于当前进程组号的任何子进程。
// 如果pid < -1,表示等待进程组号等于pid绝对值的任何子进程。
// 如果pid = -1,表示等待任何子进程。
// 如 options = WUNTRACED,表示如果子进程是停止的,也马上返回(无须跟踪)
// 若 options = WNOHANG, 表示如果没有子进程退出或终止就马上返回。
// 如果返回状态指针 stat_addr不为空,则就将状态信息保存到那里。
// 参数pid是进程号,*stat_addr是保存状态信息位置的指针,options是waitpid选项。
int sys_waitpid(pid_t pid,unsigned long * stat_addr, int options)
{
	int flag, code;             // flag标志用于后面表示所选出的子进程处于就绪或睡眠态。
	struct task_struct ** p;

	verify_area(stat_addr,4);
repeat:
	flag=0;
    // 从任务数组末端开始扫描所有任务,跳过空项、本进程项以及非当前进程的子进程项。
	for(p = &LAST_TASK ; p > &FIRST_TASK ; --p) {
		if (!*p || *p == current)
			continue;
		if ((*p)->father != current->pid)
			continue;
        // 此时扫描选择到的进程p肯定是当前进程的子进程。
        // 如果等待的子进程号pid>0,但与被扫描子进程p的pid不相等,说明它是当前进程另外的
        // 子进程,于是跳过该进程,接着扫描下一个进程。
		if (pid>0) {
			if ((*p)->pid != pid)
				continue;
        // 否则,如果指定等待进程的pid=0,表示正在等待进程组号等于当前进程组号的任何子进程。
        // 如果此时被扫描进程p的进程组号与当前进程的组号不等,则跳过。
		} else if (!pid) {
			if ((*p)->pgrp != current->pgrp)
				continue;
        // 否则,如果指定的pid < -1,表示正在等待进程组号等于pid绝对值的任何子进程。如果此时
        // 被扫描进程p的组号与pid的绝对值不等,则跳过。
		} else if (pid != -1) {
			if ((*p)->pgrp != -pid)
				continue;
		}
        // 如果前3个对pid的判断都不符合,则表示当前进程正在等待其任何子进程,也即pid=-1的情况,
        // 此时所选择到的进程p或者是其进程号等于指定pid,或者是当前进程组中的任何子进程,或者
        // 是进程号等于指定pid绝对值的子进程,或者是任何子进程(此时指定的pid等于-1).接下来根据
        // 这个子进程p所处的状态来处理。
		switch ((*p)->state) {
            // 子进程p处于停止状态时,如果此时WUNTRACED标志没有置位,表示程序无须立刻返回,于是
            // 继续扫描处理其他进程。如果WUNTRACED置位,则把状态信息0x7f放入*stat_addr,并立刻
            // 返回子进程号pid.这里0x7f表示的返回状态是wifstopped()宏为真。
			case TASK_STOPPED:
				if (!(options & WUNTRACED))
					continue;
				put_fs_long(0x7f,stat_addr);
				return (*p)->pid;
            // 如果子进程p处于僵死状态,则首先把它在用户态和内核态运行的时间分别累计到当前进程
            // (父进程)中,然后取出子进程的pid和退出码,并释放该子进程。最后返回子进程的退出码和pid.
			case TASK_ZOMBIE:
				current->cutime += (*p)->utime;
				current->cstime += (*p)->stime;
				flag = (*p)->pid;                   // 临时保存子进程pid
				code = (*p)->exit_code;             // 取子进程的退出码
				release(*p);                        // 释放该子进程
				put_fs_long(code,stat_addr);        // 置状态信息为退出码值
				return flag;                        // 返回子进程的pid
            // 如果这个子进程p的状态既不是停止也不是僵死,那么就置flag=1,表示找到过一个符合
            // 要求的子进程,但是它处于运行态或睡眠态。
			default:
				flag=1;
				continue;
		}
	}

wake_up()唤醒等待队列上的头一个进程

/****************************************************************************/
 /* 功能:唤醒等待队列上的头一个进程 */
 /* 参数:p 等待队列头 */
 /* 返回:(无) */ /****************************************************************************/ 
void wake_up(struct task_struct **p)
{ if (p && *p)
   { 
     (**p).state=0;  // 把队列上的第一个进程设为运行态或就绪态 
     *p=NULL;        // 把队列头指针清空,这样失去了都其他等待进程的跟踪。
                     // 一般情况下这些进程迟早会得到运行。
   } 
}

三、基本信息2---侧重汉语描述

1

处于可中断睡眠态的进程不光可以由 wake_up直接唤醒,还可以由信号唤醒。

所以: wake_up唤醒是正常天亮唤醒,信号唤醒是非正常半夜唤醒

在 schedule()函数中,会把处于可中断睡眠态(#define TASK_INTERRUPTIBLE     1  //进程处于可中断等待状态)并且收到信号的进程变成state为0的进程,使他参与调度选择。 

Linux0.11中进入可中断睡眠状态的方法有 3种:

  1. 调用 interruptible_sleep_on()函数

  2. 调用 sys_pause()函数

  3. 调用 sys_waitpid()函数

第一种情况用于等待外设资源时(如等待 I/O设备),这时当前进程会挂在对应的等待队列上。第二第三种情况用于事件,即等待信号。

2

进程要进入不可中断睡眠态,只能通过 sleep_on()函数。要使处于不可中断睡眠态的进程进入运行态,只能由其他进程调用 wake_up()将它唤醒。当进程等待系统资源(比如高速缓冲块,文件 i节点或者文件系统的超级块)时,会调用 sleep_on()函数,使当前进程挂起在相关资源的等待队列上。

这部分代码很短,在 sched.c中,一共三个函数 sleep_on(), wake_up()和 interruptible_sleep_on()。但是代码比较难理解,因为构造的等待队列是一个隐式队列,利用进程地址空间的独立性隐式地连接成一个队列。这个想法很奇妙。

3

这个函数牵涉到 3个指针, p, tmp和 current。

p是指向指针的指针,实际上 *p指向的是等待队列头。系统资源(高速缓冲块,文件 i节点或者文件系统的超级块)的数据结构中都一个 struct task_struct *类型的指针,指向的就是等待该资源的进程队列头。比如 i节点中的 i_wait,高速缓冲块中的 b_wait,超级块中的 s_wait。 *p对于等待队列上的所有进程都是一样的。

current指向的是当前进程指针,是全局变量。

tmp位于当前进程的地址空间内,是局部变量。不同的进程有不同 tmp变量。等待队列就是利用这个变量把所有等待同一个资源的进程连接起来。具体的说,所有等待在队列上的进程,都是在 sleep_on()中 schedule()中被切换出去的,这些进程还停留在 sleep_on()函数中,在函数的堆栈空间里面,存放了局部变量 tmp。

假如当前进程要进入某个高速缓冲块的等待队列,而且该等待队列上已经有另外两个进程 task1和 task2先后进入。形成的队列如图。等待队列是堆栈式的,先进入队列的进程排在最后。

Linux0.11内核--进程的调度(运行态(就绪态)和睡眠态之间的转换)_第5张图片

Linux0.11内核--进程的调度(运行态(就绪态)和睡眠态之间的转换)_第6张图片

在调用了 sleep_on()的地方,我们可以发现 sleep_on()往往是放在一个循环中的(比如 wait_on_buffer(), wait_on_inode(), lock_inode(), lock_super(), wait_on_super()等函数)。当进程从 sleep_on()返回时,并不能保证当前进程取得了资源使用权,因为调用 wake_up()进程切换到从 sleep_on()中苏醒的过程中,发生了进程调度,中间很可能有别的进程取得了资源。

wait_on_buffer()

进程要什么资源直接调用的是wait_on_buffer(), wait_on_inode(), lock_inode(), lock_super(), wait_on_super()等函数等待解锁。

这些函数又都调用了 sleep_on()挂起在等待队列上,资源一解锁,由于是while,队列上的进程会被全都唤醒。

 等待指定缓冲块解锁
// 如果指定的缓冲块bh已经上锁就让进程不可中断地睡眠在该缓冲块的等待队列b_wait中。
// 在缓冲块解锁时,其等待队列上的所有进程将被唤醒。虽然是在关闭中断(cli)之后
// 去睡眠的,但这样做并不会影响在其他进程上下文中影响中断。因为每个进程都在自己的
// TSS段中保存了标志寄存器EFLAGS的值,所以在进程切换时CPU中当前EFLAGS的值也随之
// 改变。使用sleep_on进入睡眠状态的进程需要用wake_up明确地唤醒。
static inline void wait_on_buffer(struct buffer_head * bh)
{
        cli();                          // 关中断
        while (bh->b_lock)              // 如果已被上锁则进程进入睡眠,等待其解锁
                sleep_on(&bh->b_wait);
        sti();                          // 开中断
}

wait_on_inode()

 等待指定的i节点可用
// 如果i节点已被锁定,则将当前任务置为不可中断的等待状态,并添加到该
// i节点的等待队列i_wait中。直到该i节点解锁并明确地唤醒本地任务。
static inline void wait_on_inode(struct m_inode * inode)
{
        cli();
        while (inode->i_lock)
                sleep_on(&inode->i_wait);
        sti();
}

 lock_inode()

 等待指定的i节点可用
// 如果i节点已被锁定,则将当前任务置为不可中断的等待状态,并添加到该
// i节点的等待队列i_wait中。直到该i节点解锁并明确地唤醒本地任务。
static inline void wait_on_inode(struct m_inode * inode)
{
        cli();
        while (inode->i_lock)
                sleep_on(&inode->i_wait);
        sti();
}

lock_super()

wait_on_super()

// 以下3个函数(lock_super()、free_super()和wait_on_super())的作用与inode.c文件中头
// 3个函数的作用雷同,只是这里操作的对象换成了超级块。
 锁定超级块
// 如果超级块已被锁定,则将当前任务置为不可中断的等待状态,并添加到该超级块等待队列
// s_wait中。直到该超级块解锁并明确地唤醒本地任务。然后对其上锁。
static void lock_super(struct super_block * sb)
{
        cli();                          // 关中断
        while (sb->s_lock)              // 如果该超级块已经上锁,则睡眠等待。
                sleep_on(&(sb->s_wait));
        sb->s_lock = 1;                 // 会给超级块加锁(置锁定标志)
        sti();                          // 开中断
}
 对指定超级块解锁
// 复位超级块的锁定标志,并明确地唤醒等待在此超级块等待队列s_wait上的所有进程。
// 如果使用ulock_super这个名称则可能更妥贴。
static void free_super(struct super_block * sb)
{
        cli();
        sb->s_lock = 0;             // 复位锁定标志
        wake_up(&(sb->s_wait));     // 唤醒等待该超级块的进程。
        sti();
}

 睡眠等待超级解锁
// 如果超级块已被锁定,则将当前任务置为不可中断的等待状态,并添加到该超级块的等待
// 队列s_wait中。知道该超级块解锁并明确的唤醒本地任务.
static void wait_on_super(struct super_block * sb)
{
        cli();
        while (sb->s_lock)
                sleep_on(&(sb->s_wait));
        sti();
}

四、分析队列---这里是核心要点---也是难点

下面分析 sleep_on() 和 wait_up()配合使用的情况

情况一 游离队列的产生

先分析一下 sleep_on()和 wake_up()在通常情况下的工作原理。考虑一个非常简单的情况,假设目前系统只有 3个进程,且都等在队列上(3个进程都执行了sleep_on()),队列的头指针设为 wait(不管是i节点中的 i_wait;高速缓冲块中的 b_wait;超级块中的 s_wait)。

然后系统资源得到释放,当前进程(可能是进程D)调用 wake_up(wait)。这时 Task C的state变成了0。

之后进程调度发生, Task C被选中,开始运行。 Task C是从 sheep_on()中的 schedule()的后一条语句开始运行,它把 Task B的state变成了0。随后 Task C退出 sheep_on()函数,堆栈中的局部变量 tmp消失,这样再没有指向 Task B的指针, Task B开头的队列游离了。

Linux0.11内核--进程的调度(运行态(就绪态)和睡眠态之间的转换)_第7张图片

这时对同一个资源有两个进程是可运行状态,但是当前进程是 Task C,只要它不调用 schedule,它是不会被抢断的。因此 Task C继续运行,取得了它想要的资源,这时 Task C可以完成它的任务了。当进程调度再次发生时, Task B会被选中,同样, Task B会把 Task A变成可运行态,而它自己得到了资源。最终 Task A也会得到执行。这样,等待在一个资源上的三个任务最终都得到运行。

假设 Task C在得到资源后,又主动调用了 schedule()(但是Task C的state早已为0),进程调度程序这时选中了 Task B。 Task B从上次中断的地方开始运行,即从 sleep_on()中 schedule()后面的语句开始运行。它会把 Task A也变成可运行状态。然后退出 sleep_on(), tmp变量消失。但是不幸的是它发现资源仍然被占用,所以再次进入睡眠,又连接到 wait队列上了。

Linux0.11内核--进程的调度(运行态(就绪态)和睡眠态之间的转换)_第8张图片

从这个情况可以看到,虽然系统运行过程中,可能会把等待队列切分成很多游离队列,但是这些队列头上的进程的state都是0,这保证 schedule()函数最终还是会找到它。

情况二 游离队列的合并

假设目前进程等待资源的情况如下,某个进程占用资源不放,导致有 7个进程等待该资源。产生 3个队列,其中两个游离。

Linux0.11内核--进程的调度(运行态(就绪态)和睡眠态之间的转换)_第9张图片

这时调度函数选中 Task E执行, Task E先唤醒 Task D但发现资源不能用,再次睡眠,把自己移到 wait队列,脱离了游离队列。调度再次发生。

Linux0.11内核--进程的调度(运行态(就绪态)和睡眠态之间的转换)_第10张图片

假如这时 Task B得到运行,同样 Task B也只能唤醒 Task A,而把自己移动到等待队列

Linux0.11内核--进程的调度(运行态(就绪态)和睡眠态之间的转换)_第11张图片

这样,只要游离队列头上的进程是运行态,游离队列可以再次合并到原先的等待队列上。

你可能感兴趣的:(李志军老师实验,p2p,网络协议,网络)