文章目录
进程
Linux 0.11的调度函数schedule()
//综合优先级与时间片的算法
void Schedule(void)
{while(1) {c=-1;next=0;i=NR_TASKS;
//Linux 0.11中将PCB做成数组
P=&task[NR_TASKS];//P设成最后一个地址
while(--i){if((*P->state==TASK_RUNNING&&(*P) ->counter>c)//--i表示从后往前移
//如果状态=就绪,并且counter>c
C=(*P)->counter,next=i;}
//返回给C的就是最大的counter
if(C) break;//c不等于0找到了最大的counter,就break;c都=0,就是现在就绪态的时间片都用完了,所有就绪态进程的时间片都用完了此时就执行下面的for语句
//找到最大counter就跳转执行switch_to(next),即优先级方法;counter就是优先级,counter本身也作为时间片轮转调度
for(P=&LAST_TASK;P>&FIRST_TASK;--P)
(*P)->counter=((*P)->counter>>1)//所有进程置counter等于当前counter右移一位即为当前counter除2,就绪态进程是0的0除2得0再加上counter的初值。(右移比除法快)
//若为就绪态进程在此处会设成就绪态初值,而阻塞的进程是折了一半再加初值,所以这个阻塞态进程恢复就绪就一定比非阻塞态进程的大。
//即因为IO阻塞的进程在回来后其counter一定会大,优先级也就更高。阻塞时间越大,counter就会改的越大
+(*P)->priority;)
switch_to(next);
}
counter的两个作用
时间片作用
时间片轮转
在时钟中断里要修改counter
void do timer(…)//在kernel/sched.c
{ if((–current->counter>0) return;//让counter–
current->counter=0;//如果减完=0
schedule();}//调度切换
_timer_interrupt://在kernel/system_call.s中
…
call_do_timer
//do time放在时钟中断中
void
sched_init
(void) {
set_intr_gate(0x20,&timer_interrupt);
优先级作用
while(–i){if((*P->state==TASK_RUNNING&&(*P) ->counter>c)
C=(*P)->counter,next=i;}
//找counter最大的任务调度,conter表示了优先级
for(P=&LAST_TASK;P>&FIRST_TASK;–P)
(*P)->counter=((*P)->counter>>1)+(*P)->priority;)
//将优先级进行了动态调整,IO时间越长,优先级就越大
- 受到Io约束的阻塞越长优先级就越大,而受到IO约束(IO需要多的)的正好是前台进程的特征,
前台进程要求响应要快(优先级算法响应快,但周转时间大)
- counter保证了响应时间的界,每个进程时间片最大是2P。最大周转时间也就2np
- 后台进程一直按照counter轮转,近似SJF调度
- 每个进程只用维护一个counter变量,简单、高效。后台特征是CPU约束型(CPU需要多的)则
后台进程关注周转时间(短作业优先周转时间最小,但响应时间没有限制)
- 时间片轮转,保证响应时间有最大值限制,可以保证响应时间的速度
- 要求后台周转时间小,且前台进程可以快速响应,schedul函数就做到了这两点,并结合轮转算法
- 前台任务时间片轮转,后台任务短作业优先,前后台任务之间采用优先级调度,同时要让前台任务优先级较大。但前台任务的优先级不能固定,这样可能会导致某个后台任务一直无法执行,故而`还需要后台任务动态变化任务的优先级,同时后台也需要一定的时间片,否则某个后台任务优先度若高,它就会一直执行下去(后台任务对cpu需求大),应该让他执行一部分再切换.
即需要以轮转调度为核心,在轮转的基础上增加优先级,优先级同时要考虑到短作业先做和前台任务先做---这就是简单高效的schedule函数算法。(该算法可以学习到哪个是前台任务,哪个是后台任务从而做出相应的变换调整)
进程同步与信号量
可能有多个进程,仅一个信号是无法表示多个进程的状态(是否睡眠及是否需要唤醒);故引入信号量。
用临界区保护信号量,用信号量实现同步
struct semaphore
{
int value;//记录资源个数
PCB *queue;//记录等待在该信号量上的进程
}
P(semaphore s);//消费资源
V(semaphore s);//产生资源
P(semaphore s)
{
s.value--;//使用资源
if(s.value<0)//资源数小于0
sleep(s.queue;)//等待睡眠
}
V(semaphore s)
{
s.value++;//释放资源
if(s.value<=0)//有等待睡眠的进程(因没有资源)
wake up(s.queue;)//唤醒进程
}
用信号量解决生产者消费者问题
int fd=open("buffer.tet");
//用文件定义共享缓冲区
write(fd,0,sizeof(int));//写in
write(fd,0,sizeof(int));//写out
//信号量的定义和初始化
semaphore full=0;//生产的内容的个数
semaphore empty=BUFFER_SIZE;//empty空闲缓冲区个数
semaphore mutex=1;//互斥信号量,只能一个进程进去
//******
Producer(item){
P(empty);//测试空闲缓冲区个数是否为0
P(mutex);//测试其值是否为1
//读入in;将item写入到in位置上
V(mutex);//进程离开,则释放
V(full);}//生产者增加内容
//******
Consumer(){
P(full);//测试生产内容个数是否没有
P(mutex);
//读入out;从文件中的out位置读出到item;打印item
V(mutex);
V(empty);}//消费者增加空闲缓冲区个数
信号量临界区保护
信号量是整型变量,对其访问修改实现进程同步。
1.为什么要对信号量进行保护?
多个进程共同修改信号量(并发操作共享数据),使得信号量的值出错。这与调度顺序有关,无法得知时间片何时到时。
2.如何对信号量进行保护(基本思路)?
生产者P1检查并给其上锁;生产者P2检查发现有锁则空转或阻塞;P1执行完开锁,P2再检查上锁。
临界区:一次仅允许一个进程进入的那段代码是临界区(读写信号量的那段代码,需要对其进行保护)
- 基本的临界区
- 互斥进入:一个进入,其他进程则不准进
- 更好的临界区
- 有空让进:临界区没有其他进程就必须让进
- 有限等待
在进入区进行"上锁"操作;在退出区进行"开锁操作"
临界区的进入
软件的方式
1.轮转法存在问题
(类似值日,轮到的进入)
//对于P0来说,turn=0时轮到它P0
//对于P1来说,turn=1时轮到它P1
//进程P0
while(turn!=0);//表示没轮到P0,则P0空转;
临界区
turn=1//轮到P1,P1进入
剩余区
存在问题:当P1阻塞,P0完成后不能接着再次进入,即使P1不再临界区,不满足有空让进。
2.标记法存在问题
加一个标记(便条),表示现在临界区是否有进程;当P0想进入时,就打个标记,若发现有标记了,则空转等待。
//进程P0
flag[0]=true//P0打标记
while(flag[1]);//若发现当有P1打的flag[1]标记(即flag[1]=true),则P0等待;若flag[1]=false则进入
临界区
flag[0]=false;//一旦进去并退出后,标记置空
剩余区
存在问题:可能两个进程同时进行标记,后两个进程发现有对方的标记都进行等待,没有进程进入,造成无限等待
3.非对称标记(peterson算法)
让其中一个进程足够勤劳,过一会就看一下。结合标记和轮转的思想
//进程P0
flag[0]=true;//P0做标记
turn=1;//当P0跑完,则将turn置为1表示该轮到P1
while(flag[1]&&turn==1);//发现P1做了标记,并且也轮到P1;则P0空转
临界区
flag[0]=false;
剩余区
4.多个进程(面包店算法)
仍然标记与轮转结合
面包店:每个进店的客户都获得一个号码,号码最小的先服务;号码相同时,名字靠前的先服务。
- 轮转方式:每个进程都获得一个序号,序号最小的进入
- 标记方式:获得了序号的进程都表示想进,不为0的序号即标记,进程离开时序号为0
- 每次取得的号是当前号+1
- 每次取得的号是当前号+1
//进程Pi
choosing[i]=ture;num[i]=max(num[0],....,num[n-1])+1)//取号,取当前号最大的+1的号
choosing[i]=false;for(j=0;j<n;j++){while(choosing[j]);//当有人正在选号,则需要等待
while((num[j]!=0)&&(num[j],j)<(num[i],i));}//当不再有人选号,则开始判断:若进程j想进入(不为0则是想进入),则进程j先与进程i的号码比较,j的序号小则先进
临界区
num[i]=0;
剩余区
一直往后取号,存在溢出问题,需要做处理
硬件的方式
临界区只允许一个进程进入,另一个进程只有被调度才能执行想要进入临界区。可以利用硬件阻止另一个进程被调度,而要调度必须要中断,可以阻止中断。即硬件提供关中断命令。
cli();//关中断
sti();//开中断
在多CPU情况下,不好
临界区保护的硬件原子指令法
上锁就是将一个变量置0或置1。就像一个信号量mutex。mutex=1表示有资源;当一个进程进入后就没有资源,mutex再置0。但是用mutex信号量去实现,那么修改mutex也需要保护。
在执行时不可中断的操作称为原子操作。
将修改mutex的指令做成原子指令,使得它要么执行完成,要么不执行。
boolean
TestAndSet(boolean &x)//已经被锁上,x就为true
{
boolean rv=x;//若锁上x=ture被赋给rv;若没锁上rv为false
x=ture;//没锁的情况下将x锁上
return rv;//锁了返回rv,则返回true;没锁的情况下返回false
//即锁了返回true;没锁则返回false且将x锁上
}
while(TestAndSet(&lock));//如果已经被锁上,则空转;没锁上则直接进入,此时x已经被锁中途不会停下TestAndSet(&lock)就会被完整执行
临界区
lock=false;//退出时开锁
剩余区
信号量的代码实现
Producer(item){
P(empty);
...
V(full);
}
sem.c//进入内核
typedef struct{
char name[20];
int value;
task_struct*queue;
}semtable[20];//定义了一个全局数组,这个数组定义了信号量的名字、值、队列
sys_sem_open(char *name)
{
在semtable中寻找name对上的;
没找到则创建;
返回对应的下标;}
}
//用户态程序producer.c
main(){
sd=sem_open("empty");//打开名字为empty的信号量(获取信号量)
for(i=1 to 5)
sem_wait(sd);
write(fd,&i,4)
}
sys_sem_wait(int sd){
cli();
if(semtable[sd].value)--<0){
设置自己为阻塞,将自己加入semtable[sd].queue队列中;schedule;}//自己睡眠了
sti();}
//在磁盘上读磁盘块
bread(int dev,int block){
struct buffer_head*bh;//申请空闲缓冲
ll_rw_block(READ,bh);//启动读命令
wait_on_buffer(bh);//缓冲区带着一个信号量阻塞
启动磁盘后睡眠,等待磁盘读完由磁盘中断将其唤醒并加入就绪队列,也是一种同步
lock_buffer(buffer_head*bh)
{
cli();
while(bh->b_lock)???//判断lock,如果等于1则要sleep,不为1则上锁(没有负值)
sleep_on(&bh->b_wait);
bh->b_lock=1;//上锁
sti();}
void sleep_on(struct task_struct **P){//P是一个指向task_struct结构体的指针的指针
struct task_struct *p;
//***********
//以下两句话是将自己放入阻塞队列
tmp=*p;//
*p=current;//
//**********
current->stste=TASK_UNINTERRUPTIBLE;//将自己的状态变为阻塞态
schedule();//实现进程调度函数
//tmp指向阻塞队列中下一个进程
if(tmp)//如果有下一个进程
tmp->state=0;}//将这下一个进程状态置0变为就绪态
//依次类推,将阻塞队列中进程依次全部唤醒
//将其全唤醒后由schedule依照优先级决定谁先执行
struct task_struct *tmp;
tmp=*p;
*p=current;//current指向的是当前正在执行的进程
//此时新的阻塞队列的队首指向current指向当前正在执行的进程
//当前进程的PCB放在队首,队首指向新放入的PCB
//tmp指向阻塞队列中下一个进程
//磁盘中断
static void read_intr(void){
...
end_request(1);
end_request(int uptodate){
...
unlock_buffer(CURRENT->bh);
unlock_buffer(struct buffer_head*bh){
bh->b_lock=0;
wake_up(&bh->b_wait);}//唤醒
wake_up(struct task_struct **P){
if (p && *p){
(**p).state=0;//将PCB状态置0即为就绪
*p=NULL;
}
}
内存管理
第一步:分段,建立段表
//在Linux/kerbel/fork.c中
int copy_process(int nr,long edp,...)
{
...
copy_mem(nr,p);
int copy_mem(int nr,task_struct*p)
{
unsigned long new_data_base;
new_data_base=nr*0x4000000;//64M*nr,n就是第几个进程就是几;此处就是分割虚拟内存
set_base(p->ldt[1],new_data_base);//设置基址,建段表(p是PCB),进程切换段表跟着切换
set_base(p->ldt[2],new_data_base);//同上建段表
代码示例图如下
- 每个进程的代码段、数据段都是一个段
- 每个进程占64M虚拟空间,互不重叠(不重叠意味着可以各进程可以共用一套页表),实际操作系统中页很有可能重叠,则每个进程都有自己的页表与段表。
第二步:分配内存(物理页),建立页表
int copy_mem(int nr,task_struct*p)
{
unsigned long new_data_base;
old_data_base=get_base(current->ldt[2]);
copy_page_tables(old_data_base,new_data_base,data_limit);//共用父进程的内存(因为是fork的父进程的,而父进程已经分配好了),建立页表(只需要拷贝父进程的页表即可),参数为两个虚拟地址
//old_data_base是父进程的虚拟地址
int copy_page_tables(unsigned long from,unsigned long to,long size)
{
from_dir=(unsigned long*)((from>>20)&0xffc);//from是一个32位的虚拟地址,将它右移20位(也就是右移22位再乘4的结果;右移22位得到目录项编号,每项4字节)
to_dir=(unsigned long *)((to>>20)&0xffc);
size=(unsigned long)(size+0x3fffff)>>22;
for(;size-->0;from_dir++,to_dir++){
from_page_table=(0xfffff000&*from_dir);//size是可能有好多页目录需要处理,from_dir是父进程的页目录,to_dir是子进程的页目录
to_page_table=get_free_page();//要用了才建立映射,才分配页表
*to_dir=((unsigned long)to_page_table)|7;//申请新页后将页的地址赋给它
将子进程指针与父进程指向同一页,然后将其变为只读,他们共享这一页
使用内存
内存的换入换出
- 实现虚拟内存必须要有换入换出,虚拟内存是连接分段和分页的关键所在
- 请求调页的时候才换入并且建立逻辑到物理的映射
请求访问(地址)——>发现没有映射、缺页,则中断——>调页(从磁盘上),映射
- 缺页造成的中断,应使PC指针不动,不进行自动加1,以便调页后中断结束后继续执行该指令,而不是自动加1后执行了下一条指令
中断号 | 名称 | 说明 |
---|---|---|
12 | Segment not Present | 描述符所指的段不存在 |
14 | Page fault | 页不存在内存 |
void trap_init(void)
{set_trap_gate(14,&page_fault);}//系统初始化时设置14号中断由page_fault去处理
#define set_trap_gate(n,addr)\ //宏定义中要换行必须用'\'结尾
_set_gate(&idt[n],15,0,addr);//初始化修改idt表
//do_no_page
//这页不在,从磁盘中把这页读进来,并且完成映射
//在linux/ mm/memory.c中
void do_ no_ page (unsigned long error_ code ,
unsigned long address )//这里是虚拟地址
{ address&=0xfffff000; //页面地址,得到虚拟页号
tmp= address- current->start_ code; //页面対应的偏移
if (!current->executable N tmp>=current->end data) {//不是代码和数据
get_ empty_ page (address) ; return;}
page=get_ free_ page();//得到物理空闲页
bread_page(page,current->executable->i_dev,nr);//在磁盘上读,读到空闲页上,读系统文件
put_ page (page, address) ;//添写页表,建立映射
void get_ empty_ page (unsigned long address)
unsigned long tmp = get_ free page () ;
put_ page (tmp,address) ;}
//put_page
//建立映射,修改页表
//在linux/mm/memory.c中
unsigned long put_ page (unsigned long page, //物理地址
unsigned long address)
{unsigned long tmp, *page_ table;
page_ table= (unsigned long *) ( (address>>20) &ffc) ;//页目录项
if((*page_ table) &1)//页表项
page_ table= (unsigned long*) (0xfffff000&*page_ table) ;
else{
tmp=get_ free_ page() ;
*page_ table= tmp|7;
page_ table= (unsigned long*) tmp;}
page_ table[ (address>>12) &0x3ff] = page|7;//页表项与物理页建立映射
return page ;
置换算法之LRU
- A在第一个时刻使用;
- B在第二个时刻使用;
- C在第三个时刻使用;
- 又来的A在第四个时刻使用;
- 又来的B在第五个时刻使用;
- 来的D,选择数值最小的C换出,在第6个时刻使用;
- 来的A在第七个时刻使用;
- 来的D在第8个时刻使用;
- 来的B在第九个时刻使用;
- 来的C,选择当前数值最小的A换出,然后在第十个时刻使用;
- 来的B在第十一个时刻使用;
…以此类推
数值大是当前的,选择数值最小的换出
栈的顶部是最近使用的 - 再来A则把B和C往下沉,把A浮上来
- 淘汰替换时选择栈底的淘汰
- 如果最近访问过这一页,则将它置为1;每访问一页将它置为1,表示访问过。
- 如何淘汰?使用再给一次机会算法(SCR(也叫CLOCK算法))
- 如果R是1表示最近访问过,再给一次机会,则将它置为0不淘汰它,但要是转了一圈后还是0(转的这段时间中没有被访问过),则要淘汰替换它;R是0则没访问过,要淘汰它
- 设置1或0,可以硬件自动设置
缺页很少,现在用到的页一直在用,退化为FIFO了
- 用扫描指针清除R位。指针转动速度快
- 另一个指针用来选择淘汰页,转动速度慢
分配的页框数:指针在多少个页之间循环的数
页框分配太少,急剧下降那里的现象称为颠簸
页框分配太少会颠簸
局部:应该分配的页框数——>求工作集来求一个局部或也可以试着分配几个后做动态调整
IO与显示器