Linux0.11进程切换和TSS结构
TSS 全称为task state segment,是指在操作系统进程管理的过程中,进程切换时的任务现场信息。
X86体系从硬件上⽀持任务间的切换。为此⽬的,它增设了⼀个新段:任务状态段(TSS),它和数据段、代码段⼀样也是⼀种段,记录了任务的状态信息。
与其它段⼀样,TSS也有描述它的结构:TSS描述符表,它记录了⼀个TSS的信息,同时还有⼀个TR寄存器,它指向当前任务的TSS。任务切换的时候,CPU会将原寄存器的内容写出到相应的TSS,同时将新TSS的内容填到寄存器中,这样就实现了任务的切换。
TSS在任务切换过程中起着重要作⽤,通过它实现任务的挂起和恢复。所谓任务切换是指挂起当前正在执⾏的任务,恢复或启动执⾏另⼀个任务。Linux任务切换是通过switch_to这个宏来实现的,它利⽤长跳指令,当长跳指令的操作数是TSS 描述符的时候,就会引起CPU的任务的切换,此时,CPU将所有寄存器的状态保存到当前任务寄存器TR所指向的TSS段中,然后利⽤长跳指令的操作数(TSS描述符)到新任务的TSS段,并将其中的内容填写到各个寄存器中,最后,将新任务的TSS选择符更新到TR中。这样系统就开始运⾏新切换的任务了。由此可见,通过在TSS中保存任务现场各寄存器状态的完整映象,实现了任务的切换。 task_struct中的tss成员就是记录TSS段内容的。当进程被切换前,该进程⽤tss_struct保存处理器的所有寄存器的当前值。当进程重新执⾏时,CPU利⽤tss恢复寄存器状linux下的sleep函数
就绪态和运⾏态之间的切换
当前占⽤CPU的进程,只有调⽤了schedule()函数,才会由运⾏态转变为就绪态,schedule()函数选择状态为
TASK_RUNNING的进程,
然后调⽤switch函数,将cpu切换到所选定的进程。
schedule()函数可能会在以下三种情况下调⽤:
(1) ⽤户态时发⽣时钟中断
如果当前进程是⽤户态进程,并且当前进程的时间⽚⽤完,那么中断处理函数do_timer()就会调⽤schedule()函数,这相当于⽤户态的进程被抢断了。
如果当前的进程属于内核态进程,那么该进程是不会被抢占的。schedule() 函数不是系统调⽤,⽤户程序不能直接调⽤,
但是放在时间中断函数中,就能够调⽤。所以在时间中断中调⽤schedule()是必要的,这样就保证⽤户进程不会永久地占有CPU。
(2)系统调⽤时,相应的sys_xxxx()函数返回之后。
这种情况是为了处理运⾏在内核态的进程,应⽤程序⼀般是通过系统调⽤进⼊内核态,因此,linux系统调⽤处理函数在结束的时候,
int 0x80 中断函数会检查当前进程的时间⽚和状态,如果时间⽚⽤完或者进程的状态不为RUNNING ,就会调⽤schedule()函数。
由此可见,如果系统的某个系统调⽤处理函数或者中断处理异常永远不退出,那么整个系统就会死锁,任何进程都⽆法运⾏。
(3)在睡眠函数内
当进程等待的资源还不可⽤的时候,它就进⼊了睡眠状态,并且调⽤schedule()函数再次调⽤CPU。
#define switch_to(n) {\
// __tmp⽤来构造ljmp的操作数。该操作数由4字节偏移和2字节选择符组成。当选择符
// 是TSS选择符时,指令忽略4字节偏移。
/
/ __tmp.a存放的是偏移,__tmp.b的低2字节存放TSS选择符。⾼两字节为0。
// ljmp跳转到TSS段选择符会造成任务切换到TSS选择符对应的进程。
// ljmp指令格式是 ljmp 16位段选择符:32位偏移,但如果操作数在内存中,顺序正好相反。
// %0 内存地址 __tmp.a的地址,⽤来放偏移
// %1 内存地址 __tmp.b的地址,⽤来放TSS选择符
// %2 edx 任务号为n的TSS选择符
// %3 ecx task[n]
struct {long a,b;} __tmp; \
__asm__("cmpl %%ecx,current\n\t" \ // 如果要切换的任务是当前任务
"je 1f\n\t" \ // 直接退出
"movw %%dx,%1\n\t" \ // 把TSS选择符放⼊__tmp.b中
"xchgl %%ecx,current\n\t" \ // 让current指向新进程的task_struct
"ljmp *%0\n\t" \ // 任务切换在这⾥发⽣,CPU会搞定⼀切
"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])); \
}
进程调度函数
/****************************************************************************/
/* 功能:进程调度。 */
/* 先对alarm和信号进⾏处理,如果某个进程处于可中断睡眠状态,并且收 */
/* 到信号,则把进程状态改成可运⾏。之后在处可运⾏状态的进程中挑选⼀个 */
/* 并⽤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已经到时间了
if ((*p)->alarm && (*p)->alarm < jiffies) {
// 向该进程发送SIGALRM信号
(*p)->signal |= (1<<(SIGALRM-1));
(*p)->alarm = 0; // 清除alarm
}
/
/可屏蔽信号位图BLOCKABLE定义在sched.c第24⾏,(~(_S(SIGKILL) | _S(SIGSTOP)))
// 说明SIGKILL和SIGSTOP是不能被屏蔽的。
// 可屏蔽信号位图 & 当前进程屏蔽的信号位图 = 当前进程实际屏蔽的信号位图
// 当前进程收到的信号位图 & ~当前进程实际屏蔽的信号位图
// = 当前进程收到的允许相应的信号位图
// 如果当前进程收到允许相应的信号,并且当前进程处于可中断睡眠态
// 则把状态改成运⾏态,参与下⾯的选择过程
if (((*p)->signal & ~(_BLOCKABLE & (*p)->blocked)) &&
(*p)->state==TASK_INTERRUPTIBLE)
(*p)->state=TASK_RUNNING;
(*p)->state=TASK_RUNNING;
}
/* this is the scheduler proper: */
// 下⾯是进程调度的主要部分
while (1) {
c = -1;
next = 0;
i = NR_TASKS;
p = &task[NR_TASKS];
while (--i) { // 遍历整个task[]数组
if (!*--p) // 跳过task[]中的空项
continue;
/
/ 寻剩余时间⽚最长的可运⾏进程,
// c记录⽬前到的最长时间⽚
// next记录⽬前最长时间⽚进程的任务号
if ((*p)->state == TASK_RUNNING && (*p)->counter > c)
c = (*p)->counter, next = i;
}
// 如果有进程时间⽚没有⽤完c⼀定⼤于0。这时退出循环,执⾏ switch_to任务切换
if (c) break;
// 到这⾥说明所有可运⾏进程的时间⽚都⽤完了,则利⽤任务优先级重新分配时间⽚。
// 这⾥需要重新设置所有任务的时间⽚,⽽不光是可运⾏任务的时间⽚。
// 利⽤公式:counter = counter/2 + priority
for(p = &LAST_TASK ; p > &FIRST_TASK ; --p)
if (*p)
(*p)->counter = ((*p)->counter >> 1) +
(*p)->priority;
// 整个设置时间⽚过程结束后,重新进⼊进程选择过程
}
// 当的上⾯的循环退出时,说明到了可以切换的任务
switch_to(next);
}
2 运⾏态和睡眠态之间的转化
当进程等待资源或者事件的时候,就进⼊了睡眠状态,有两种不同的睡眠状态,不可中断睡眠状态和可中断睡眠状态。
处于可中断睡眠状态的进程,不光可以由wake_up 唤醒,还可以由信号唤醒,在schedule()函数中,会把处于可中断睡眠状态的并且接收到信号的
进程变为运⾏状态。linux0.11的可中断睡眠状态可以由以下三中函数进⼊:
(1)调⽤interruptiable_sleep_on()函数、
(2)调⽤sys_pause()函数。
(3)调⽤sys_waitpid()函数。
进程要进⼊不可中断睡眠状态,必须调⽤sleep_on()函数。进程调⽤wake_up()函数,将处于不可中断状态的进程唤醒。
/****************************************************************************/
/* 功能:当前进程进⼊不可中断睡眠态,挂起在等待队列上 */
/* 参数: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;
}
以下是唤醒函数
/****************************************************************************/
/* 功能:唤醒等待队列上的头⼀个进程 */
/* 参数:p 等待队列头 */
/* 返回:(⽆) */
/****************************************************************************/
void wake_up(struct task_struct **p)
{
if (p && *p) {
(**p).state=0; // 把队列上的第⼀个进程设为运⾏态
*p=NULL; // 把队列头指针清空,这样失去了都其他等待进程的跟踪。
// ⼀般情况下这些进程迟早会得到运⾏。
}
}
版权声明:本站内容均来自互联网,仅供演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系QQ:729038198,我们将在24小时内删除。
发表评论