【哈工大操作系统】期末考试复习大纲

操作系统接口

Q:什么是接口? A:连接两个东西,信号转换,屏蔽细节。

eg:命令行、图形界面

接口表现为函数调用,由系统提供,所以称为系统调用(system call)

分类:(POSIX:IEEE指定的一个标准族)

  • 任务管理(fork,execl)
  • 文件系统(open,EACCES)

系统调用的实现

不能随意jmp,系统调用提供了一种进入内存的手段。

将内核程序和用户程序隔离,区分用户态和内核态:

  • CS的最低两位当前特权级CPL:00内核态,11用户态
  • DPL>=CPL时可以访问,DPL为表项中对应的段的特权级

硬件提供的主动进入内核方法:中断(进入内核的唯一方法

因此,系统调用的核心为:

  • 用户程序中包含一段包含 int 指令的代码
  • 操作系统写中断处理,获取想调用程序的编号
  • 操作系统根据编号执行相应代码

CPU管理的直观想法

CPU的工作原理:取址,执行

管理CPU最直观的方法:设置好PC的初值

处理一条IO命令的时间相当于很多计算指令的时间:多道程序,交替执行

一个CPU上交替执行多个程序:并发

怎么做到并发:

  • 控制PC寄存器切换
  • 记录切出去时进程的状态:每个进程有一个存放信息的结构–PCB

进程:进行中的程序,有开始有结束,走走停停,需要记录寄存器状态

多进程图像

多个进程使用CPU的图像,从启动开始到关机结束。

PCB:用来记录进程信息的数据结构

多进程如何组织:PCB+状态+队列

  • 一些进程等待执行:就绪队列
  • 一些进程等待某个事件:磁盘等待队列

多进程如何交替:队列操作+调度+切换(schedule( )完成切换)

schedule()
{
   pNew=getNext(ReadyQueue);
   switch_to(pCur,pNew);
}

进程调度:FIFO 先入先出;Priority 优先级。

会出现多个进程同时访问同一个内存位置的问题:要限制对内存的读写,通过映射表映射到不同的物理地址

多个进程如何合作:生产者-消费者实例

  • 核心在于进程同步(合理的推进顺序)
  • 生产者给counter上锁
  • 消费者检查counter锁
  • 生产者给counter开锁
  • 消费者给counter上锁

用户级线程

进程=资源+指令执行序列

  • 将资源和指令执行分开,线程切换就是只针对指令的切换
  • 一个资源+多个指令执行序列

线程thread:保留了并发的优点,避免了进程切换代价

从一个栈到两个栈:使用TCB和Yield

内核级线程

和用户级相比,核心级有哪些不同:

  • ThreadCreate时系统调用,内核管理TCB,内核负责切换线程
  • 如何让切换成型?内核栈,TCB
    • 执行的代码仍然在用户态,还要进行函数调用
    • 一个栈到一套栈,两个栈到两套栈

用户栈和内核栈之间的关联:

返回时弹栈,跳回到发生中断前的位置。

开始内核中的切换:switch_to,通过TCP找到内核栈指针,通过ret切到某个内核程序,最后再用CS:IP切换到用户程序。

sys_read()
{
   //启动磁盘读;将自己变成阻塞;找到next
   switch_to(cur,next);
}

CPU调度策略

调度就是获得next,然后switch to next

调度需要折中,需要中和:

  • 吞吐量和响应时间之间有矛盾;
  • 前台任务(时间)和后台任务(周转时间)关注点不同;
  • IO约束型任务和CPU约束型任务有各自的特点。

几种调度算法:

1.FCFS-先来先服务

  • 平均周转时间:进入到离开的时间的平均
  • 短的作业提前的话平均周转时间会变小→\rightarrowSJF短作业优先

2.SJF-短作业优先

  • 平均周转时间最小
  • 响应时间该怎么办?→\rightarrowRR按时间片轮转调度

3.RR-轮转调度

  • 时间片大:响应时间太长
  • 时间片小:吞吐量小
  • 折中:时间片10-100ms,切换时间0.1-1ms(1%)

想要使用优先级,但会产生如下的问题:

  • 如果一直有前台任务,后台任务就得不到执行,因此需要后台任务优先级动态升高
  • 但后台任务一旦执行,前台任务的响应时间得不到保证,因此前后台都需要使用时间片,但这就又退化到了RR,体现不出SJF

因此引出如下的这个实际的schedule函数

Linux 0.11的schedule函数

求最大的counter;counter就代表了优先级,本身也作为时间片进行轮转调度。

找到最大的counter然后跳出去执行;

//没有找到最大counter,对所有任务的counter进行如下修改
for(p=$Last_Task;p>$First_Task;--p){
  (*p)->counter = ((*p)->counter >> 1)+(*p)->priority;//当前counter除以2加上初值
}

counter作用的整理:

  • counter保证了响应时间的界限;
  • 经过IO后counter会变大,IO时间越长counter越大,照顾了前台进程;
  • 后台进程按照counter轮转,近似于SJF;
  • 每个进程只维护一个counter变量,简单高效。

进程同步与信号量

进程合作:多个进程共同完成一个任务

以生产者-消费者为实例:

//生产者进程
while(true){
  while(counter == BUFFER_SIZE)
   sleep();//缓存满,生产者停
  buffer[in] = item;
  in = (in+1) % BUFFER_SIZE;
  counter++;
}
//消费者进程
while(true){
  while(counter == 0)
    sleep();//缓存空,消费者停
  item = buffer[out];
  out = (out+1) % BUFFER_SIZE;
  counter--;
}

只发信号不能解决所有问题,如果两个生产者同时睡眠,第二个永远不会被唤醒。

因此需要使用信号量来记录更多详细的信息,来决定睡眠还是唤醒。

信号量开始工作:

  • 缓冲区满,P1执行,P1 sleep sem=-1
  • P2执行,P2 sleep sem=-2
  • C执行1次循环, wakeup P1 sem=-1
  • C再执行1次循环, wakeup P2 sem=0
  • C再执行一次循环 sem=1
  • P3执行 sem=0

定义信号量:Dijkstra提出

P在荷兰语中代表检查,而V代表增加。

struct semaphore{
  int value;//记录资源个数
  PCB *queue;//记录等待在该信号量上的进程
}
P(semaphore s);//消费资源
V(semaphore s);//产生资源

P(semaphore s){
  s.value--;//消费一个资源
  if(s.value < 0)
    sleep(s.queue);//没有资源则等待
}
V(semaphore s){
  s.value++;//生产一个资源
  if(s.value <= 0)//说明有等待的进程,需要进行唤醒
    wakeup(s.queue);
}

用信号量解决生产者-消费者问题:

int fd = open("buffer.txt");//用文件定义共享缓冲区
write(fd,0,sizeof(int));//写in
write(fd,0,sizeof(int));//写out

//信号量的定义和初始化
semaphore full = 0;
semaphore empty = BUFFER_SIZE;
semaphore mutex = 1;//互斥信号量,只能有一个人进入缓冲区写文件

Producer(item){
  P(empty);//测试空闲缓冲区是不是0
  P(mutex);
  /*读入in,将item写入到in的位置*/
  V(mutex);
  V(full);
}
Consumer(){
  P(full);//测试生产出的资源数量
  P(mutex);
  /*读入out,从文件中的out位置读出到item,打印item*/
  V(mutex);
  V(empty);
}

信号量临界区保护

共同修改信号量引出的问题:

  • 竞争条件:和调度有关的共享数据语义错误
  • 错误和调度顺序有关,难于发现和调试

解决竞争条件的直观想法:

  • 在写共享变量时阻止其他进程访问它(上锁,开锁)
  • 临界区:一次只允许一个进程进入的该进程的那一段代码
  • 一个非常重要的工作:找出进程中的临界区代码

剩余区–进入区–临界区–退出区–剩余区

临界区代码的保护原则:

  • 基本原则:互斥进入
  • 有空让进
  • 有限等待

轮换法:相当于值日

//进程P0
while(turn != 0);
/*临界区*/
turn = 1;
/*剩余区*/

//进程P1
while(turn != 1);
/*临界区*/
turn = 0;
/*剩余区*/

//能够满足互斥进入,但是不能满足有空让进原则

标记法:相当于留了便条

//进程P0
flag[0] = true;
while(flag[1]);
/*临界区*/
flag[0] = false;
/*剩余区*/

//两边同时留条的话会造成都认为对方会进入,结果没有人能进入

非对称标记:带名字的便条+一个人更加勤劳

进入临界区Peterson算法:结合了标记和轮转两种思想

//进程P0
flag[0] = true;
turn = 1;
while(flag[1] && turn == 1);
/*临界区*/
flag[0] = false;
/*剩余区*/

//满足互斥进入,有空让进,有限等待

多个进程:

1.面包店算法(软件方法)

  • 每个进程获得一个序号,序号最小的进入
  • 进程离开时序号为0,不为0的序号即为标记
  • 很明显这个算法虽然可以,但是太复杂了

2.阻止调度(硬件方法)

  • 多CPU(多核)时不好使
cli();//关中断
/*临界区*/
sti();//开中断
/*剩余区*/

3.临界区保护的硬件原子指令法(硬件方法)

  • 中间不能被打断
boolean
  TestAndSet(boolean &x)
{
  //一次执行完毕
  boolean rv = x;
  x = true;
  return rv;
}

while(TestAndSet(&lock));//查看锁是否被锁上
/*临界区*/
lock = false;
/*剩余区*/

死锁处理

再考虑生产者-消费者问题,如下所示调换信号量的位置:

Producer(item){
  P(empty);//改为mutex
  P(mutex);//改为empty
  /*读入in,将item写入到in的位置*/
  V(mutex);
  V(full);
}
Consumer(){
  P(full);//改为mutex
  P(mutex);//改为full
  /*读入out,从文件中的out位置读出到item,打印item*/
  V(mutex);
  V(empty);
}

生产者和消费者同时阻塞,互相等待对方持有的资源,称为死锁。

死锁的成因:

  • 资源互斥使用,一旦别人占有无法使用
  • 进程占有了一些资源,又不去释放,再去申请其他资源
  • 各自占有资源并互相申请

死锁的四个必要条件:

  • 互斥使用
  • 不可抢占:资源只能自愿放弃
  • 请求和保持:进程必须占有资源再去申请
  • 循环等待:存在一个回路

死锁处理方法概述:

  • 死锁预防:破坏死锁出现的条件
  • 死锁避免:检查每个资源请求,如果会造成死锁就拒绝—银行家算法
  • 死锁检测+恢复:检测到死锁时让一些进程回滚,让出资源
  • 死锁忽略:重启

内存使用与分段

内存使用:将程序放到内存中,PC指向开始地址。

重定位:修改程序中的地址(是相对地址)

  • 编译时完成重定位程序只能放在固定的位置
  • 载入时重定位程序一旦载入内存就不能动了

重定位最合适的时机:运行时重定位

  • 在运行每条指令时才完成重定位
  • 每执行一条指令都要从逻辑地址算出物理地址
  • 每个指令的基地址放在PCB里,执行指令的第一步先从PCB中取出这个基地址
  • 物理地址 = base + offset

引入分段:是将整个程序一起载入内存中吗?

  • 程序由若干段组成,每个段有各自的特点和用途,代码段只读,代码/数据段不会动态增长
  • 用户可以独立考虑每个段
  • 定位具体指令(数据):<段号,段内偏移>

不是将程序,而是将各段分别放入内存中。

进程段表:GDT表(操作系统对应的段表)+LDT表(每个进程自己的段表)

段号基址长度保护
0180K150KR

内存分区与分页

三个步骤:

  • 将一个程序分成多个段落
  • 在内存中找到空闲的区域
  • 将程序读入内存

可变分区的管理–按需分配+再次申请:又有一个段提出内存请求,此时同时存在多个空闲段

  • 首先适配:表查的最快 O(1)
  • 最佳适配:会割出很多外碎片 O(n)
  • 最差适配:会割出均匀的块 O(n)

引入分页:解决内存分区导致的内存效率问题

  • 因为会产生大量的内存碎片;将空闲分区合并需要移动段(内存紧缩)
  • 把面包切成片=把内存分成页,每4K分成一页
  • 每个进程最多浪费一页,不用进行内存紧缩

页载入内存,此时要用到页表使程序运行起来:

  • 页中的仍然是逻辑地址 Page(第几页)+Offset(页面尺寸)
  • 由硬件MMU完成由逻辑地址找到物理地址
页号页框号保护
05R
11R/W
23R/W

若此时逻辑地址为0x02|0x240,查找表内对应页框号为3,3*4K即为3左移12位,加上偏移就得到了物理地址为0x3240

多级页表与快表

为了提高内存的空间利用率,页就应该小一点,但这样的话页表项增加,页表就大了。

实际上大部分的逻辑地址根本不会用到,很自然的想法:用到的逻辑页才有页表项

32位地址空间+4K页面+页号必须连续→\rightarrow2202^{20}220个页表项→\rightarrow大页表占用内存,造成浪费

Q:既要连续又要让页表占用内存少,怎么办? A:多级页表

逻辑地址:

10bits10bits12bits
页目录号页号Offset

多级页表增加了访存的次数,尤其是64位系统:

  • TLB是一组相联快速存储(使用硬件一步找到),是寄存器
有效页号修改保护页框号
11400R56
1201R/W23
0190R/X29

根据逻辑地址的页号查找快表中的对应序号,如果TLB命中直接拿到对应的页框号;若TLB未命中则失效,查看多级页表,查看完成后将结果放入TLB中。

TLB命中时效率会很高,未命中时效率会降低。

TLB的条目数在64-1024之间:

  • 程序的地址访问具有局部性
  • 空间局部性

段页结合的实际内存管理

程序员希望用段,物理内存希望用页。

核心想法:先将用户程序的段映射到地址空间(虚拟内存),再将地址空间(虚拟内存)的段映射到实际的内存上,即完成了段页的结合。

cs:ip逻辑地址→\rightarrow用户代码段→\rightarrow虚拟地址(对用户是透明的)→\rightarrow物理地址

段页同时存在时的重定位:

  • 段号+偏移(cs:ip)(逻辑地址)→\rightarrow页号+偏移(虚拟地址)→\rightarrow物理页号+偏移(物理地址)

使用内存的换入换出来实现虚拟内存。

内存换入-请求调页

用户眼中的内存:

  • 32位机器即4GB的(大而规整的)内存,用户可以随意使用,实际上就是用地址,就好像单独拥有4G内存(此时是虚拟内存)
  • 这个“内存”如何映射到物理内存,用户全然不知
  • 当物理内存的大小小于虚拟内存时:用换入、换出可以实现”大内存”

请求调页:

  • 请求的时候才建立映射
  • MMU查页表发现缺页,发起中断,进行页错误处理程序
  • 从磁盘中把页面放入物理内存,重新执行指令

整个调入过程对用户透明,用户只会感觉运行慢了一点。

中断号名称说明
12Segment not Present描述符所指的段不存在
14Page fault页不在内存

内存换出

并不能总是获得新的页,内存是有限的,需要选择一页淘汰,换出到磁盘。

page = get_free_page();
bread_page(page, current->exectable->i_dev, nr);

1.FIFO页面置换

  • 评价准则:缺页次数

  • 置换方法不合适,可能造成刚换出又要换入的情况,缺页次数多。

2.MIN页面置换

  • 选最远将使用的页淘汰,是最优方案

  • 但是MIN需要知道将来发生的事,理论可行但是实际无法使用。

3.LRU页面置换

  • 用过去的历史预测将来:选择最近最长一段时间没有使用的页淘汰(最近最少使用);

  • LRU是公认的很好的页面置换算法,怎么实现?

  • LRU的准确实现:使用时间戳(time stamp)

    • 每页维护一个时间戳,选择具有最小时间戳的页换出;
    • 每次地址访问需要修改时间戳,需要维护一个全局时钟,需要找到最小值,实现的代价太大,故不可行。
  • LRU的准确实现:使用页码栈

    • 维护一个页码栈,选择栈底页淘汰;
    • 每次访问需要修改栈,实现代价仍然较大→\rightarrowLRU的准确实现使用较少
  • LRU的近似实现:将时间计数更改为是和否

    • 每次访问一页时,硬件自动设置该位;
    • 选择淘汰页:扫描该位,是1时清零,并继续扫描;是0则淘汰该页(二次机会算法);
    • 组织成循环队列较为合适;
    • 二次机会算法也称为Clock算法

Clock算法的分析和改造:

  • 当缺页很少时,1很少有机会变成0,最终会导致R全为1,发生缺页时所有1变为0,然后将当前指向的页换出,退化为了FIFO算法;
  • 发生以上问题的原因:记录了太长的历史信息,因此需要定时清除R位;
  • 再加入一个扫描指针,用来清除R位,移动速度要快;
  • 原来的指针用于选择淘汰页,移动速度慢;两指针一快一慢,因此称为Clock算法。

给一个进程分配多少页框(帧frame):

  • 分配的多会导致请求调页导致的内存高效利用无效;
  • 分配太少会导致程序数量到达一定量时CPU利用率急剧下降(颠簸);

I/O与显示器

让外设工作起来:

  • CPU向控制器中的寄存器读写数据;
  • 控制器完成真正的工作,并向CPU发送中断信号。

一段操纵外设的程序:操作系统为用户提供了统一的接口

int fd = open("/dev/xxx");
for(int i = 0; i < 10; i++){
  write(fd,i,sizeof(int));
}
close(fd);

printf的整个过程:

  • 库函数(printf)
  • 系统调用(write)
  • 字符设备接口(crw_table[])
  • tty设备写(tty_write)
  • 显示器写(con_write)

生磁盘的使用

磁盘的IO过程:控制器→\rightarrow寻道→\rightarrow旋转→\rightarrow数据传输;

最直接的使用磁盘:

  • 只要往控制器中写(使用out指令)柱面、磁头、扇区、缓存位置。

通过盘块号读写磁盘(一层抽象):

  • 磁盘驱动负责从block计算出cyl、head、sec(CHS);
  • 磁盘访问时间=写入控制器时间+寻道时间+旋转时间+传输时间
  • block相邻的盘块可以快速读出,因此相邻的盘块尽量放在同一个磁道里;
  • 由CHS得到的扇区号:C×\times×(Heads×\times×Sectors)+H×\times×Sectors+S
  • 从扇区到盘块:每次读写1K:碎片0.5K;读写速度100K/秒;每次读写1M:碎片0.5M;读写速度约40M/秒。因此把连续的几个扇区认定为一个盘块,一次读写单位变大,虽然空间浪费变多但是读写速度变快。

多个进程通过队列使用磁盘(第二层抽象):

  • 磁盘中断时由磁盘驱动从队列中取出block,换算成CHS后out输出;
  • 需要考虑到调度算法。

1.FCFS磁盘调度算法:最公平、最直观的调度。

2.SSTF(Shortest-seek-time First)磁盘调度:短寻道优先;离中心较远的请求难以执行,存在饥饿问题。

3.SCAN磁盘调度(电梯算法):

  • SSTF+中途不回折:每一个请求都有处理机会;
  • SCAN+直接移到另一端:两端请求都能够很快处理。 ←\leftarrow真实算法

生磁盘的使用整理:

  • 进程“得到盘块号”,算出扇区号(sector);
  • 用扇区号make req,使用电梯算法add_request;
  • 进程sleep_on;
  • 磁盘中断处理;
  • do_hd_request算出CHS;
  • hd_out调用outp(…)完成端口写。

从生磁盘到文件

引入文件,对磁盘使用的第三层抽象:

  • 文件:建立字符流到盘块集合的映射关系;
  • 连续结构来实现文件:处理起来快,但是不适合用于动态增长;
  • 在文件的FCB中存放文件名、起始块以及块数。

链式结构也可以实现文件:

  • 读写过程长,存取比较慢,但是动态变换比较快。

索引结构Index:

  • FCB中记录文件名与索引块;

  • 读写速度和动态变换速度都不慢,现实中较为常用。

目录与文件系统

文件系统,抽象整个磁盘(第四层抽象)。

在其他计算机上:应用结构+存储的数据可以得到那棵文件树,找到文件,读取文件…

引入目录树:

  • 将划分后的集合再次进行划分:k次划分后,每个集合中的文件数为O(log⁡kN)O(\log_kN)O(logkN)
  • 这一树状结构扩展性好、表示清晰,最常用;
  • 引入目录,代表一个文件集合。

如何实现目录成为关键问题:

  • 如何使用? 用"/my/data/a"定位文件a;即为根据路径名"/my/data/a"得到a的FCB;

树状目录的完整实现:

  • 磁盘:FCB数组+数据盘块集合;
  • 目录项(字符串+一个号):文件名+对应的FCB的地址。

要使整个系统能自举,还需要存一些信息:

引导块超级块i节点位图盘块位图i节点数据区
固定的记录两个位图有多大等信息哪些inode空闲,哪些被占用表示哪些块是空闲的根目录放在第一项
  • 根目录放在inode数组的第一项;
  • 通过读超级块,解析里面的内容,就可以找到根目录,进行其他的操作;
  • mount就是读超级块的操作。

完成全部映射下的磁盘使用:

  • 用户:读test.c 202-212个字节
  • open(/xx/test.c):目录解析到/,读入/内容找到xx,再找到test.c的inode
  • read(fd):根据找到的FCB和file中的202-212字节找盘块789
  • 写入电梯队列:add_request(789)
  • 磁盘中断:从队列中取出789,算出CHS
  • 写磁盘控制器:outp(cyl,head,sector)
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值