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恢复寄存器状
就绪态和运行态之间的切换
当前占用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; } /* 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; // 把队列头指针清空,这样失去了都其他等待进程的跟踪。 // 一般情况下这些进程迟早会得到运行。 } }
Linux0.11进程切换和TSS结构的更多相关文章
- Linux0.11进程分配时间片的策略
想知道内核什么时候给进程重新分配时间片,最好的办法就是阅读源代码(其中已经打了注释) /******************************************************** ...
- linux0.11改进之四 基于内核栈的进程切换
这是学习哈工大李治军在mooc课操作系统时做的实验记录.原实验报告在实验楼上.现转移到这里.备以后整理之用. 完整的实验代码见:实验楼代码 一.tss方式的进程切换 Linux0.11中默认使用的是硬 ...
- 在Linux-0.11中实现基于内核栈切换的进程切换
原有的基于TSS的任务切换的不足 进程切换的六段论 1 中断进入内核 2 找到当前进程的PCB和新进程的PCB 3 完成PCB的切换 4 根据PCB完成内核栈的切换 5 切换运行资源LDT 6 利用I ...
- Linux0.11内核源码——内核态线程(进程)切换的实现
以fork()函数为例,分析内核态进程切换的实现 首先在用户态的某个进程中执行了fork()函数 fork引发中断,切入内核,内核栈绑定用户栈 首先分析五段论中的第一段: 中断入口:先把相关寄存器压栈 ...
- linux0.11内核源码——进程各状态切换的跟踪
准备工作 1.进程的状态有五种:新建(N),就绪或等待(J),睡眠或阻塞(W),运行(R),退出(E),其实还有个僵尸进程,这里先忽略 2.编写一个样本程序process.c,里面实现了一个函数 /* ...
- 对Linux0.11 中 进程0 和 进程1分析
1. 背景 进程的创建过程无疑是最重要的操作系统处理过程之一,很多书和教材上说的最多的还是一些原理的部分,忽略了很多细节.比如,子进程复制父进程所拥有的资源,或者子进程和父进程共享相同的物理页面,拥有 ...
- Linux0.11 创建进程的过程分析--fork函数的使用
/* * linux/kernel/fork.c * * (C) 1991 Linus Torvalds */ /* 注意:signal.c和fork.c文件的编译选项内不能有vc变量优化选项/Og, ...
- Linux0.11之进程0创建进程1(1)
进程0是由linus写在操作系统文件中的,是预先写死了的.那么进程0以后的进程是如何创建的呢?本篇文章主要讲述进程0创建进程1的过程. 在创建之前,操作系统先是进行了一系列的初始化,分别为设备号.块号 ...
- linux-0.11内核 任务的堆栈切换
http://blog.163.com/di_yang@yeah/blog/static/86118492201212534924900/ 一直缠绕的两个问题:怎样标识的内核栈与用户栈?如何在内核态堆 ...
随机推荐
- ACM Super Jumping! Jumping! Jumping!
Nowadays, a kind of chess game called "Super Jumping! Jumping! Jumping!" is very popular i ...
- Node.js 字符串解码器
稳定性: 3 - 稳定 通过 require('string_decoder') ,可以使用这个模块.字符串解码器(StringDecoder)将缓存(buffer)解码为字符串.这是 buffer. ...
- Go 错误处理
Go 语言通过内置的错误接口提供了非常简单的错误处理机制. error类型是一个接口类型,这是它的定义: type error interface { Error() string } 我们可以在编码 ...
- Docker常见仓库Redis
Redis 基本信息 Redis 是开源的内存 Key-Value 数据库实现. 该仓库提供了 Redis 2.6 ~ 2.8.9 各个版本的镜像. 使用方法 默认会在 6379 端口启动数据库. $ ...
- ngx.ctx
https://github.com/openresty/lua-nginx-module#ngxctx 要点 生命周期和请求一致 每个请求的ngx.ctx是相互独立的,包括ngx.location. ...
- 安卓高级Fresco图片框架的时候
Fresco:2015FaceBook推出的 及其强大 支持webp图片格式 和渐进式图片加载 中文文档 使用方法 引入依赖 点击查看具体教程 基本使用步骤 在布局中使用其标签 <com.fac ...
- Eclipse调试(1)——基础篇
作为使用Eclipse的程序员都会使用它的Debug.但是有不少人只会用F6.F8,其他功能知之甚少.今天我就来总结一下我在使用eclipse的debug时的一些个人经验.水平有限,不足之处还请赐教. ...
- openresty 备忘
The problem with: apt-get --yes install $something is that it will ask for a manual confirmation if ...
- spring @Qualifier注解使用
@Autowired是根据类型进行自动装配的.如果当Spring上下文中存在多个UserDao类型的bean时,就会抛出BeanCreationException异常;如果Spring上下文中不存在U ...
- 【移动开发】binder阻塞/非阻塞与单向/双向的问题
The client thread calling transact is blocked by default until onTransact has finishedexecuting on t ...