当进程等待资源或者事件时,就进入睡眠状态。
有两种睡眠态:不可中断睡眠态( 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;
};
#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任务并运行。
}
参数: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
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;
}
pause n.暂停; 停顿
// 转换当前任务状态为可中断的等待状态,并重新调度。
// 该系统调用将导致进程进入睡眠状态,知道收到一个信号。该信号用于终止进程或者使进程调用
// 一个信号捕获函数。只有当捕获了一个信号,并且信号捕获处理函数返回,pause()才会返回。此时
// pause()返回值应该是-1,并且errno被置为EINTR。这里还没有完全实现(直到0.95版)
int sys_pause(void)
{
current->state = TASK_INTERRUPTIBLE;
schedule();
return 0;
}
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;
}
}
/****************************************************************************/
/* 功能:唤醒等待队列上的头一个进程 */
/* 参数:p 等待队列头 */
/* 返回:(无) */ /****************************************************************************/
void wake_up(struct task_struct **p)
{ if (p && *p)
{
(**p).state=0; // 把队列上的第一个进程设为运行态或就绪态
*p=NULL; // 把队列头指针清空,这样失去了都其他等待进程的跟踪。
// 一般情况下这些进程迟早会得到运行。
}
}
1
处于可中断睡眠态的进程不光可以由 wake_up直接唤醒,还可以由信号唤醒。
所以: wake_up唤醒是正常天亮唤醒,信号唤醒是非正常半夜唤醒
在 schedule()函数中,会把处于可中断睡眠态(#define TASK_INTERRUPTIBLE 1 //进程处于可中断等待状态)并且收到信号的进程变成state为0的进程,使他参与调度选择。
Linux0.11中进入可中断睡眠状态的方法有 3种:
调用 interruptible_sleep_on()函数
调用 sys_pause()函数
调用 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先后进入。形成的队列如图。等待队列是堆栈式的,先进入队列的进程排在最后。
在调用了 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开头的队列游离了。
这时对同一个资源有两个进程是可运行状态,但是当前进程是 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队列上了。
从这个情况可以看到,虽然系统运行过程中,可能会把等待队列切分成很多游离队列,但是这些队列头上的进程的state都是0,这保证 schedule()函数最终还是会找到它。
假设目前进程等待资源的情况如下,某个进程占用资源不放,导致有 7个进程等待该资源。产生 3个队列,其中两个游离。
这时调度函数选中 Task E执行, Task E先唤醒 Task D但发现资源不能用,再次睡眠,把自己移到 wait队列,脱离了游离队列。调度再次发生。
假如这时 Task B得到运行,同样 Task B也只能唤醒 Task A,而把自己移动到等待队列
这样,只要游离队列头上的进程是运行态,游离队列可以再次合并到原先的等待队列上。