当进程等待资源或者事件时,就进入睡眠状态。有两种睡眠态,不可中断睡眠态(
TASK_UNINTERRUPTIBLE
)和可中断睡眠态(
TASK_INTERRUPTIBLE
)。
处于可中断睡眠态的进程不光可以由
wake_up
直接唤醒,还可以由信号唤醒。在
schedule()
函数中,会把处于可中断睡眠态并且收到信号的进程变成运行态,使他参与调度选择。
Linux0.11
中进入可中断睡眠状态的方法有
3
中
调用
interruptible_sleep_on()
函数
调用
sys_pause()
函数
调用
sys_waitpid()
函数。
第一种情况用于等待外设资源时(如等待
I/O
设备),这时当前进程会挂在对应的等待队列上。第二第三种情况用于事件,即等待信号。
进程要进入不可中断睡眠态,只能通过
sleep_on()
函数。要使处于不可中断睡眠态的进程进入运行态,只能由其他进程调用
wake_up()
将它唤醒。当进程等待系统资源(比如高速缓冲块,文件
i
节点或者文件系统的超级块)时,会调用
sleep_on()
函数,使当前进程挂起在相关资源的等待队列上。
这部分代码很短,一共三个函数
sleep_on()
,
wake_up()
和
interruptible_sleep_on()
。在
sched.c
中。但是代码比较难理解,因为构造的等待队列是一个隐式队列,利用进程地址空间的独立性隐式地连接成一个队列。这个想法很奇妙。
sleep_on()
/****************************************************************************/
/* 功能:当前进程进入不可中断睡眠态,挂起在等待队列上*/
/* 参数: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;//管理的。后到的进程等在前面
current->state = TASK_UNINTERRUPTIBLE;// 进程进入不可中断睡眠状态
schedule();// 进程放弃CPU使用权,重新调度进程
// 当前进程被wake_up()唤醒后,从这里开始运行。
// 既然等待的资源可以用了,就应该唤醒等待队列上的所有进程,让它们再次争夺
// 资源的使用权。这里让队列里的下一个进程也进入运行态。这样当这个进程运行
// 时,它又会唤醒下下个进程。最终唤醒所有进程。
if (tmp)
tmp->state=0;
}
这个函数牵涉到
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
先后进入。形成的队列如图。等待队列是堆栈式的,先进入队列的进程排在最后。
在调用了
sleep_on()
的地方,我们可以发现
sleep_on()
往往是放在一个循环中的(比如
wait_on_buffer()
,
wait_on_inode()
,
lock_inode()
,
lock_super()
,
wait_on_super()
等函数)。当进程从
sleep_on()
返回时,并不能保证当前进程取得了资源使用权,因为调用
wake_up()
进程切换到从
sleep_on()
中苏醒的过程中,发生了进程调度,中间很可能有别的进程取得了资源。
wake_up()
/****************************************************************************/
/* 功能:唤醒等待队列上的头一个进程*/
/* 参数:p 等待队列头*/
/* 返回:(无)*/
/****************************************************************************/
void wake_up(struct task_struct **p)
{
if (p && *p) {
(**p).state=0;// 把队列上的第一个进程设为运行态
*p=NULL;// 把队列头指针清空,这样失去了都其他等待进程的跟踪。
// 一般情况下这些进程迟早会得到运行。
}
}
下面分析
sleep_on()
和
wait_up()
配合使用的情况
情况一 游离队列的产生
先分析一下
sleep_on()
和
wake_up()
在通常情况下的工作原理。考虑一个非常简单的情况,假设目前系统只有
3
个进程,且都等在队列上,队列的头指针设为
wait
。
然后系统资源得到释放,当前进程调用
wake_up(wait)
。这时
Task C
变成了运行态。
之后进程调度发生,
Task C
被选中,开始运行。
Task C
是从
sheep_on()
中的
schedule()
的后一条语句开始运行,它把
Task B
的状态变成运行态。随后
Task C
退出
sheep_on()
函数,堆栈中的局部变量
tmp
消失,这样再没有指向
Task B
的指针,
Task B
开头的队列游离了。
情况
1-1
这时对同一个资源有两个进程是可运行状态,但是当前进程是
Task C
,只要它不调用
schedule
,它是不会被抢断的。因此
Task C
继续运行,取得了它想要的资源,这时
Task C
可以完成它的任务了。当进程调度再次发生时,
Task B
会被选中,同样,
Task B
会把
Task A
变成可运行态,而它自己得到了资源。最终
Task A
也会得到执行。这样,等待在一个资源上的三个任务最终都得到运行。
情况
1-2
假设
Task C
在得到资源后,又主动调用了
schedule()
,进程调度程序这时选中了
Task B
。
Task B
从上次中断的地方开始运行,即从
sleep_on()
中
schedule()
后面的语句开始运行。它会把
Task A
也变成可运行状态。然后退出
sleep_on()
,
tmp
变量消失。但是不幸的是它发现资源仍然被占用,所以再次进入睡眠,又连接到
wait
队列上了。
从这个情况可以看到,虽然系统运行过程中,可能会把等待队列切分成很多游离队列,但是这些队列头上的进程都是运行态,这保证
schedule()
函数最终还是会找到它。
情况二 游离队列的合并
假设目前进程等待资源的情况如下,某个进程占用资源不放,导致有
7
个进程等待该资源。产生
3
个队列,其中两个游离。
这时调度函数选中
Task E
执行,
Task E
先唤醒
Task D
但发现资源不能用,再次睡眠,把自己移到
wait
队列,脱离了游离队列。调度再次发生。
假如这时
Task B
得到运行,同样
Task B
也只能唤醒
Task A
,而把自己移动到等待队列
p { margin-bottom: 0.08in; }
这样,只要游离队列头上的进程是运行态,游离队列可以再次合并到原先的等待队列上。
p { margin-bottom: 0.08in; }
interruptible_sleep_on()
/****************************************************************************/
/* 功能:当前进程进入可中断睡眠态,挂起在等待队列上*/
/* 参数: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;
}