《现代操作系统》笔记
进程与线程
一、进程
- 每一个被启动的进程都有一个启动该进程的用户UID,子进程拥有和父进程一样的UID。
- 用户可以是某个组的成员,每个组也有一个GID
- 在UNIX中的进程将其存储空间划分为三段:正文段(如程序代码)、数据段(如变量)以及堆栈段,数据向上增长而堆栈向下增长
- UNIX系统,只有一个系统调用可以创建新进程:fork
- fork产生的子进程,会和父进程拥有相同的内存映像、同样的环境字符串和同样的打开文件。
- UNIX中,子进程的初始地址空间是父进程的一个副本,但是涉及两个不同的地址空间,
不可写的内存区是共享的,可写的内存是不共享的
- 进程终止的四个原因:1、正常退出(自愿);2、出错退出(自愿);3、严重错误(非自愿);4、被其他进程杀死(非自愿)
- UNIX不会因为进程被杀死,而杀死该进程创建的所有进程
- UNIX中存在进程层次,WINDOWS因为可以将句柄移交给其他进程,因此所有进程的地位都是相同的
- 进程的三种状态:1、运行态(该时刻进程实际占用CPU);2、就绪态(可运行,但是因为其他进程正在运行而暂时停止);3、阻塞态(除非某种外部事件发生:例如shell等待输入,否则进程不能运行)
- 接收到中断信号后,将进程表中的程序计数器、程序状态字等压入堆栈,然后计算机跳转到中断向量所指示的地址(中断向量表共256个中断向量,由硬件定义)
二、线程
- 为什么要用多线程:如果在处理并发类事务时,程序频繁进入阻塞系统调用,让CPU空转,这是很浪费的
- 进程可以理解为将相关的资源集中在一起,进程有保存程序正文和数据以及其他资源的地址空间,这些资源包括打开的文件、子进程、即将发生的定时器、信号处理程序等
- 线程可以理解为真正在CPU上被调度的实体,它包含一个程序计数器、寄存器、堆栈等,
线程必须在某个进程中执行
- 进程拥有自己的地址空间(即可读地址共享,可写地址不共享);线程拥有一样的地址空间
- 每个线程拥有自己的堆栈
- 线程无法像进程一样利用时钟中断强制让出CPU,因此就出现了
yield
- 用户空间实现线程:1、线程切换时不需要陷入内核;2、允许每个进程有自己的调度算法;3、如果一个线程开始运行,则该进程中的其他线程就不能运行
- 互斥量用于线程的互斥,信号线用于线程的同步。这是互斥量和信号量的根本区别,也就是互斥和同步之间的区别
- 互斥量值只能为0/1,信号量值可以为非负整数
三、进程间通信
1、互斥方案
- 屏蔽中断:不现实,因为中断的权力不应该给用户,而且对多核处理器无效
- 锁变量:类似于monitor原理,但是会出错
- 严格轮换法:类似于自旋锁,不断检查变量
- Perterson算法:enter_region用于进入临界区,里面有一个空方法体的while循环,leave_region用于离开
- TSL指令:锁住内存总线,不同于屏蔽中断,屏蔽中断对多核处理器无效(XCHG与之类似),可以保证“检查-占锁”这一过程的原子性
2、生产者消费者模型
两个进程共享一个公共的固定大小的缓冲区,其中消费者从缓冲区读数据,生产者往缓冲区塞数据
3、信号量
信号量就是在一个叫做互斥区的门口放一个盒子,盒子里面装着固定数量的小球,每个线程过来的时候,都从盒子里面摸走一个小球,然后去互斥区里面浪(?),浪开心了出来的时候,再把小球放回盒子里。如果一个线程走过来一摸盒子,得,一个球都没了,不拿球不让进啊,那就只能站在门口等一个线程出来放回来一个球,再进去。这样由于小球的数量是固定的,那么互斥区里面的最大线程数量就是固定的,不会出现一下进去太多线程把互斥区给挤爆了的情况。这是用信号量做并发量限制。另外一些情况下,小球是一次性的,线程拿走一个进了门,就把小球扔掉了,这样用着用着小球就没了,不过有另外一些线程(一般叫做生产者)会时不时过来往盒子里再放几个球,这样就可以有新的线程(一般叫做消费者)进去了,放一个球进一个线程,这是信号量做同步功能。你截图里的例子就是这个情况,主线程是生产者,通过sem_post往盒子里放小球(信号量加一),而其他线程是消费者,通过sem_wait从盒子里拿小球(信号量减一),如果遇到盒子里一个小球都没有(信号量为0),就会开始等待信号量不为0,然后拿走一个小球(信号量减一)再继续。本质上来说信号量就是那个盒子,以及“摸不到球就不让进”这个机制。
PV原子操作
PV原子操作的具体定义如下:
● P操作:如果有可用的资源(信号量值>0),则此操作所在的进程占用一个资源(此时信号量值减1,进入临界区代码);如果没有可用的资源(信号量值=0),则此操作所在的进程被阻塞直到系统将资源分配给该进程(进入等待队列,一直等到资源轮到该进程)。
● V操作:如果在该信号量的等待队列中有进程在等待资源,则唤醒一个阻塞进程;如果没有进程等待它,则释放一个资源(即信号量值加1)。
4、互斥量
可以理解为二元信号量
5、管程
Java的互斥锁synchronized关键字就是基于管程,但是只有部分编程语言支持
6、消息传递
即网络传输的思想
7、屏障
屏障的作用是等待多个线程的完成,屏障允许任意线程的等待
8、避免锁:读-复制-更新(RCU)
和Copy-on-write类似
● 读标志
假设一个Reader企图占领一把RCU锁,它是不须要付出不论什么代价的,仅仅须要设置一个标志,让外界知道有Reader在占领这把RCU锁。多个Reader能够共同持有一把RCU锁。
● 写时拷贝
假设有一个Write企图更新RCU锁所保护的数据,那么它会首先查看该RCU锁的读标志,假设有该标志,说明有最少一个Reader持有了该RCU锁。它须要对原始数据make a copy,写这个副本并将更新过的副本保存在某处,等待时机用该副本更新原始数据。
● 更新时机
这个时机就是用副本更新原始数据的时间点。这个时间点怎样确定是RCU锁实现的算法核心,它直接能够确定全部的数据结构。确切来讲,Writer必须waitting for all readers leaving,方可Update原始数据,问题是,它是怎么知道全部的Reader都离开了呢?
调度
一、调度简介
1、调度类型
- 非抢占市式调度:根据调度算法挑选一个进程,然后让该进程运行直到被阻塞(阻塞在IO或等待另一个进程),或者直到该进程自动释放CPU。(时钟中断也不会发生调度,如没有更高优先级的进程在等待调度,则处理完时钟中断后该进程依旧会继续执行)
- 抢占式:根据调度算法挑选一个进程,并且让该进程运行某个固定时段的最大值,如果在该时段结束时,该进程仍在运行,它就会被挂起,然后调度程序挑选另一个进程运行
2、调度算法分类
2.1、批处理系统的调度算法
- 先来先服务:容易出现某一时间内只使用了CPU或者只使用了IO
- 最短作业优先:
- 最短剩余时间优先:调度程序总是选择剩余运行时间最短的那个进程,调度进程需要提前知道每个进程所运行的时间
2.2 交互式系统的调度算法
- 轮转调度:也就是时间片机制调度,缺点是需要进行频繁的上下文切换/进程切换。时间片设置得太短会导致过于频繁的进程切换,设置得太长会导致对短的交互请求响应时间变长。通常设置为20~50ms
- 优先级调度:与轮转调度的大家都是平等的进程不同,如果优先级相同,则会使用时间片机制调度
- 最短进程优先:类似于最短作业优先,估算出所需的运行时间
- 保证调度:有点类似时间片机制,但是多了一个均衡每个进程调度时间的机制
- 彩票调度:按照制定的比例分配调度时间
- 公平分享调度:将用户内的进程数加入考虑因素,而不是考虑总进程数,每个用户所得到的调度时间是平均的,但是每个进程不是
2.3 实时系统的调度算法
无
3、线程调度
- 用户级线程:由于内核并不知道线程的存在,所以内核还是和以前一样操作,进程调度程序选取一个进程A,并给予A固定的时间片。A中会有线程调度程序决定哪个线程运行,假设是A1,由于线程在运行过程中并不会遭遇时钟中断(因为一旦中断那这个进程旧被切走了),所以这个线程可以运行任意时间
- 内核级线程:选择一个特定的线程运行,并为之分配时间片,时间片超过了之后强制挂起该线程
内核级线程和用户级线程
内核级线程:内核会维护进程表和线程表,线程还是基于进程环境创建,但是不由进程管理,而是有内核管理,每个线程都有时间片概念
用户级线程:内核只维护进程表,线程是有运行时系统维护的,内核无法感知到线程的存在,无时间片概念
内存管理
一、存储器抽象
1、无存储器抽象
直接引用物理内存,只适用于运行固定某几个程序的场景
2、一种存储器抽象: 地址空间
地址空间为程序创造了一种抽象的内存。地址空间是一个进程可用于寻址内存的一套地址集合。每个进程都有一个自己的地址空间,并且与其他进程互相独立(除了特殊情况下需要共享)
2.1、基址寄存器和界限寄存器
(已不再使用)给每个CPU配置两个特殊的寄存器:基址寄存器和界限寄存器。分别用于代表程序的的起始物理地址和程序的长度。缺点是需要进行加法运算,会有耗时问题
2.2、如何解决内存超载?
- 交换技术:要将所有进程都放在RAM中是不现实的,因为需要的空间太大了。因此就出现了交换技术:即把一个进程完整调入内存,使进程运行一段时间,然后存回磁盘。
- 虚拟内存:
2.3、内存紧缩
交换技术会在内存中产生大量的空闲区,通过将所有的进程尽可能的向下移动,可以将这里内存碎片合成一大块,称之为内存紧缩,但是通常不进行这个操作,因为很耗费CPU时间
2.4、如何解决内存运行过程中的增长问题
如果进程毗邻空闲区,那还可以为它动态分配内存,但是如果是两个程序相邻的时候,就没办法了。这时候要么把增长的这个程序移到更大的空间中去,要么把增长的这个程序周围的程序交换到其他地方。通常的解决方法是分配内存的时候,分配多一点额外的内存,这样可以减少交换程序的次数
2.5、空闲内存管理
-
位图管理
将整个内存按照分配单位分割成若干个单元,每个单元对应位图中的一位 -
链表管理
维护一个记录已分配内存段和空闲内存段的链表。其中链表的每个节点代表其中之一:1、进程;2、两个进程间的空闲区。每个节点都包含以下信息:指示标识(P:进程,H:空闲区)、起始地址、长度、指向下一节点的指针
存储管理器为程序分配内存的算法
- 首次适配法:找到足够大的空闲区,除非刚好一样大,否则会将这个区域分成两部分:一部分供进程使用,另一部分成为新的空闲区
- 下次适配法:原理和首次适配法一样,只是每次找寻的时候都从上次结束的位置开始找起
- 最佳适配法:每次都会找整个链表。从而得到最适合的空间,但是会产生大量的小空闲区
- 快速适配法:每个链表节点一个大小,用于寻找非常方便,但是产生碎片内存的时候,合并会很麻烦
为什么会有虚拟内存?
因为程序越来越大,大到RAM无法装下,这时候交换技术也不生效
3、虚拟内存
思想:每个进程有自己的地址空间,每个空间被分割成多个块,每一块被称作一页或页面。每一页有连续的地址范围,这些页被映射到物理内存,但并不是所有的页都必须在内存中才能运行程序。当程序引用到一部分在物理内存中的地址空间时,由硬件离开执行必要的映射。当程序引用到一部分不在物理内存中的地址空间时,由操作系统负责将缺失的部分装入物理内存并重新执行失败的指令(也就是page fault)
,虚拟内存适合在多道程序设计系统中使用,许多程序的片段同时保存在内存中,当一个程序在等待它的一部分读入内存时,可以把CPU交给另一个进程使用
,(虚拟内存本质上是用来创造一个新的抽象概念 —— 地址空间)
3.1、分页
由程序产生的虚拟地址,构成一个虚拟地址空间,在没有虚拟内存的计算机上,系统直接将虚拟地址送到内存总线,读写操作使用具有同样地址的物理内存字;而在使用虚拟内存的情况下,虚拟地址不是被直接送到内存总线上,而是被送到内存管理单元MMU(在CPU内)
,MMU负责将虚拟地址映射为物理地址
页面和页框
页面:虚拟地址空间按照固定大小划分成为若干个单元,每个单元都称之为页面
页框:在物理内存中对应的单元称为页框
缺页错误Page Fault
当访问一个未映射的页面时,CPU会陷入操作系统,操作系统会找到一个很少使用的页框,然后将该页框内的内容写入磁盘,随后把需要访问的页面读到刚才回收的页框中,修改映射关系,然后重新启动触发这次陷阱的指令
3.2、页表
页表的目的是将虚拟页面映射为页框。
页表结构
- 页框号:最重要的一项
- “在/不在”位:1时代表表项有效,为0时代表该表项代表的虚拟页面不在物理内存中,如果访问这个页面的话会引起page fault
- 保护位:指出允许什么类型的访问,取值有rwx(可读、可写、可执行)
- 修改位:用于重新分配页框。如果一个页面被修改过,则必须将它写回磁盘
- 访问位:用于操作系统决定丢弃哪些页面
- 高速缓存禁止位:用于那些映射到设备寄存器而不是常规内存的场景,可以避免读的数据不是来自高速缓存中的旧数据副本
3.3、加速分页过程
TLB
称之为快表,其实就是页表的子集,用于存放使用率高的表项,如果快表不命中,还是需要走正常的页表搜索流程
软件TLB
现代很多机器,当TLB访问失效的时候,不需要再有MMU到页表中查找并取出需要的页表项,而是生成一个TLB失效,并直接将问题给操作系统处理。系统先找到页面,然后从TLB中删除一个项,接着装载新的项,最后执行先前出错的指令。只要TLB表足够大,就不会有频繁的TLB失效
TLB失效类型
- 软失效:当一个页面访问在内存而不在TLB中时,将产生软失效,此时所要做的就是更新一下TLB,不需要产生磁盘IO
- 硬失效:如果页面本身不在内存里面,将产生硬失效,此时需要一次磁盘操作以存入该页面
严重缺页错误和次要缺页错误
实际遇到的情况比软失效和硬失效更复杂,主要分为三种:
- 次要缺页错误:所需的页面就在内存,但未记录在页表里面,那只需要更新到页表中即可,不需要IO操作
- 严重缺页错误:需要从磁盘重新调入页面
- 段错误:程序访问了一个非法地址,这属于程序错误
前两种缺页情况都会被硬件或者操作系统以降低性能为代价而自动修复
3.4、针对大内存的页表
3.4.1、多级页表
进程内存结构
最底端的是程序正文段,中间是数据段,顶端是堆栈段
为什么需要多级页表
最主要的目的是避免将所有的页表都保存在主存中,本来分页的目的就是为了解决物理内存小于虚拟内存,现在还将内存都用在可能永远用不上的页面上,这是很浪费的.
多级页表的优势
多级页表对比单级页表最大的优势就在于不强求连续的内存空间(为了支持随机访问,也就是数组)
4、页面置换算法
4.1、最优页面置换算法
假设有1000万条指令,A页面在第800万条执行,B页面在第600万条执行,最优置换算法应该是置换A,因为A距离现在更久
4.2、最近未使用页面置换算法(NRU)
页面上有两个位:R位和M位,R位标记着被访问,M位标记着被修改。这两个位可以简单构造一个NRU算法:当启动一个进程时,它的RM位都被置为0,R位被定期(例如时钟中断)置为0,以区分访问和未访问
4.3、先入先出页面置换算法(FIFO)
即以队列的方式管理,找到存在时间最长的置换出去
4.4、第二次机会页面置换算法
因为FIFO有可能导致经常使用的页面被置换出去,为了避免这个问题而做出了改变:检查最老页面的R位,如果R位是0,说明又是最老又没有被使用过;如果是1,则将R位清0,然后放在链表最后一位(当作一个新的处理)
4.5、时钟页面置换算法
虽然第二次机会页面置换算法已经是一种比较合理的算法,但是频繁在链表上移动元素是很低性能的,改进方案是将链表形成一个链表环,将指针指向最老的页面,然后通过第二次机会页面置换算法的检查方式找到元素。因为链表元素以页框为单位,因此个数固定,效率也会高一些
4.6、最近最少使用页面置换算法
理论上可以实现,但是需要维护一个所有页面的链表,表头放最常使用的,表尾放最少使用的,每次访问页面都需要刷新整个表。然而有一种特殊硬件可以实现,它有64位的计数器,页面也要新增一个对应的项,每次访问这个页面的时候,对应域+1,为0或者值最小的就是最不常使用的
4.7、老化算法
每个页面有一个域用于保存8个位,每次时钟周期后,页面使用情况都添加到这个域的高位,例如:10000000 -> 01000000(未使用)
4.8、工作集页面置换算法
一个进程正在使用的所有页面称之为它的工作集。如果整个工作集都被装入内存中,那运行过程中产生的缺页中断就会少很多。在多道程序设计系统中,经常会把整个进程转移到磁盘中,下次再取出来,然而,执行指令是纳秒级别的,处理缺页中断是毫秒级别的,这样性能显然会有问题。所以分页系统会设法跟踪进程的工作集,确保在进程运行前,工作集已经在内存中
4.9、工作集时钟页面置换算法
结合时钟算法和工作集算法
5、分页系统中的设计问题
5.1、局部分配策略和全局分配策略
假设A、B、C构成了可运行进程的集合,假设A发生了缺页中断,那在寻找最少使用页面的时候是否会从BC里面寻找?不同的置换页面算法适用不同的策略
5.2、负载控制
正如PFF算法指出的:一些进程需要更多的内存,但是并没有进程需要更少的内存,在这种情况下,唯一的解决方法就是将某些进程放回磁盘(减少内存竞争)
5.3、页面大小
小页面更能利用TLB,但是小页面意味着页面数量多,将页面装载到寄存器的时间也会随之增长
5.4、分离的指令空间和数据空间
通常计算机只有一个地址空间,为了使用更加方便,需要将其拆分:为指令(程序正文)和数据设置分离的地址空间 —— I空间和D空间。它们之间分别独立,而且使用各自的页表
5.5、共享页面
几个用户同时运行一个程序是很常见的事情,因此共享程序可以通过共享I空间,因为I空间是只读的;如果共享数据可以通过共享D空间,但是必要时需要出发写时复制(Copy-on-write)
5.6、共享库
即so库,不是每个进程引用的时候都会复制一遍的
5.7、内存映射文件
共享库其实就是内存映射的一种特例。内存映射的含义:进程可以通过一个系统调用,将一个文件映射到它的虚拟内存空间中。需要注意的是,做这一个操作的时候,并不会实际将文件读入页框,只有访问的时候才会读入。当进程退出或者显示关闭内存映射时,会将内容写回磁盘文件中。
内存映射提供了一种I/O的可选模型,可以把文件当作内存中一个大字符数组来访问,而不需要通过读写操作。
通过内存映射文件实现进程间通信
如果两个或两个以上进程映射了同一个文件,它们就可以通过共享内存来实现通信。一个进程在文件上执行了写操作,另一个进程在执行读操作时立刻就可以看到前一个进程写操作的内容
6、与分页有关的工作
6.1、与分页有关的工作
操作系统在这四个时候会做跟分页有关的事情:进程创建时,进程执行时,缺页中断时,进程终止时。
- 进程创建时:当在分页系统中创建一个新进程时,操作系统要确定程序和数据在初始化时有多大,并为它们创建一个页表。操作系统还要在内存中为页表分配空间并对其进行初始化。操作系统还需要在磁盘交换区中分配空间,以便在一个进程换出时在磁盘上有放置此进程的空间。
- 进程执行时:当调度一个进程执行时,必须为新进程重置MMU,刷新TLB,以清除以前进程遗留的痕迹。新进程的页表必须成为当前页表,通常可以通过复制该页表或者把一个指向它的指针放进某个硬件寄存器来完成。在进程初始化时可以把进程的部分或全部页面装入内存中以减少缺页中断的发生,例如PC(程序计数器)所指向的页面是肯定需要的
- 缺页中断时:当缺页中断发生时,操作系统必须通过读硬件寄存器来确定是哪个虚拟地址发生了缺页中断。然后还要计算需要哪个页面,并在磁盘上对该页面进行定位。它必须找到合适的页框来存放新页面,必要时还要置换老的页面,然后把所需的页面读入页框。最后回退程序计数器,使程序计数器指向引起缺页中断的指令,并重新执行该指令。
- 进程终止时:当进程退出时,操作系统必须释放进程的页表、页面和页面在磁盘上所占用的空间(共享情况下另论)
7、分段
7.1、为什么要分段
分页是对内存空间按等长进行分割,目的是提高内存的利用率
;分段是对程序内容按语义进行分割,目的是保护代码段的内容不被修改,代码段的数据可被共享。
举例:现在要把一头猪(进程)放进冰箱(物理内存),分页就是将冰箱划分成大小一样的格子(页框),再把整头猪也按照空格大小来切块再放进去;分段就是先把猪按照猪头、猪肘、内脏等等分成一块块,再把这些块放入冰箱(不定长);而段页式就是将猪按照部位切块,同时又将冰箱按等大分格,再把每个部分切割,放在每个格子里。最终的结果就是同一格内只会有相同部位的肉,每一格拥有数量一样的肉,而不像分页或者分段
文件系统
一、文件
略
二、目录
略
三、文件系统的实现
1、文件系统布局
文件系统放置在磁盘中,通常磁盘都会分为多个分区,每个分区有一个独立的文件系统。磁盘的0号扇区称之为主引导记录(MBR),用来引导计算机。在MBR的结尾是分区表。该表给出了每个分区的起始和结束地址。
在计算机被引导时,BIOS读入并执行MBR,MBR做的第一件事是确定活动分区,读入它的第一个块,称为引导块,并执行之。引导块中的程序将装载该分区中的操作系统。
1.1、磁盘分区的结构
引导块 -> 超级块 -> 空闲空间管理 -> i节点 -> 根目录 -> 文件和目录
2、文件的实现
文件存储实现的关键是记录每个文件分别用到哪些磁盘块,主要有以下方式:
- 连续分配:以连续数据块的方式存储,优点是实现简单、读性能好;缺点是容易产生碎片
- 链表分配:解决了连续分配带来的碎片问题,缺点是访问很慢,指针本地需要占用字节,造成额外开销
- 文件分配表:将每个磁盘块的指针存放在内存的一张表中,缺点是数据庞大时会导致占用大量内存
- i节点:通过i节点去找到这个文件,因此只需要将i节点保存在内存中,而且只有文件打开的情况下才会占据内存。缺点是每个i节点只能保存固定数量的磁盘地址,如果超出的话需要通过其他方式解决:例如最后一个节点指向地址的地址
3、目录的实现
操作系统根据用户给出的路径名找到相应的目录项,目录项中提供了查找文件磁盘块所需要的信息:有可能是整个文件的磁盘地址(连续分配方案)、第一个块的编号(对于两种链表的分配方案)、i节点信息。
目录的主要功能是把ASCII文件名映射成定位文件数据所需的信息
4、日志结构文件系统
磁盘上的文件系统面临着这样一个问题:当crash的时候,如果保证数据的一致性。例如在某个场景下,需要同时更新文件系统中的A和B,因为磁盘每次都只能响应一次读写请求,肯定会造成A或B其中一方先接受到数据,如果在这时候crash的话,就会导致数据不一致。
传统UNIX对此的解决方案是使用fsck工具,但是fsck通常需要在crash重启后,扫描整个磁盘,繁琐且速度较慢。因此更多的是使用日志
原理:在真正更新磁盘之前,先往磁盘上写入一些信息,这些信息主要是描述接下来要更新什么,相当于write-ahead。这样,即使crash,也可以回溯crash前正在进行的操作,从而继续执行。(但是恢复操作必须是幂等的)
5、虚拟文件系统
一个操作系统内部往往有多个不同的文件系统共存着,因此需要抽象出一层,以方便替换各个文件系统
四、文件系统管理和优化
1、文件系统性能
访问磁盘比访问内存慢得多,为了改善性能,采取了以下措施:
- 高速缓存:逻辑上属于磁盘,但是出于性能考虑,高速缓存被保存在内存中。
文件系统
一、IO硬件原理
1、IO设备
IO设备大致可以分为两类:
- 块设备:把信息存储在固定大小的块中,每个块有自己的地址(可寻址操作)。所有传输以一个或多个完整的(连续的)块为单位。如硬盘、USB等
- 字符设备。以字符为单位发送或接收一个字符流,而不考虑任何块结构,即不可寻址,如网络接口、鼠标、打印机等
2、内存映射IO
每个设备控制器都有几个寄存器用来与CPU进行通信。通过写入这些寄存器,操作系统可以命令设备发送数据、接收数据、开启或关闭,或者执行某些其他操作。除此之外,许多设备还有一个供操作系统进行读写的数据缓冲区。于是问题来了:CPU如何与设备的控制寄存器和数据缓冲区进行通信?
- IO端口:每个控制寄存器被分配一个受保护的IO端口
- 内存映射IO:将这些寄存器地址映射到操作系统的内存空间。缺点是操作系统必须管理选择性高速缓存,硬件必须能够针对每个页面有选择地禁用高速缓存
3、直接存储器存取(DMA)
无论一个CPU是否具有内存映射IO,它都需要寻址设备控制器以便与它们交换数据,这样会导致CPU被浪费在这一操作上。引入DMA可以将对IO设备的读写交给DMA,操作完成后,再由DMA发送一个中断通知CPU
4、重温中断
由于在中断时,根据指令执行状态分成:
-
精确中断:
(1)程序计数器保存在一个已知的地方;
(2)PC所指的指令之前的所有指令均完全执行;
(3)之后的所有指令均未执行;
(4)所指向的指令的执行状态是已知的 -
不精确中断:
目前所指向的指令前后,有可能有执行到一半,且进度不一的指令,这种中断对于恢复很不友好
二、IO软件原理
1、IO软件的目标
IO软件的作用是统一对不同IO硬件的访问,主要需要遵循以下规则:
- 统一命名:命名不依赖具体设备
- 设备独立:不依赖设备的物理特征
- 错误处理:纠错应该从底到高,也就是应该由硬件去处理,处理不了再往上抛给驱动程序,还是处理不了才可以给操作系统
- 缓冲:为数据传输提供一个临时存放地,然后在方便的时候将数据拷贝到最后目的地
- 共享与独占:尽量将设备变为共享,以增大资源利用率和降低死锁发生的概率
2、程序控制IO
也就是通过程序代码去控制IO,例如写一个for循环去向打印机输出一串文字。这一行为称为轮询/忙等待。缺点是IO完成之前要占用CPU的全部时间
3、中断驱动IO
在不考虑缓冲区的情况下,假设打印机每秒打印100个字符,也就是每个字符是10ms,这意味着,当每个字符写到打印机的寄存器之后,这段时间CPU是空闲的,但是它却浪费了这段时间用来做无价值的循环。
因此中断驱动IO就是在每次循环完的时候,释放CPU给其他进程使用,等到打印机准备好接收下一个字符的时候,向CPU发出一个中断,从而让CPU继续处理
4、使用DMA的IO
中断驱动IO一个明显的缺点就是中断发生在每个字符上,而中断是需要花费时间的,所以这一方式会浪费一定的CPU时间。而DMA本质上还是程序控制IO,但是它可以解放CPU
三、IO软件层次
IO软件通常组织成四个层次,由下至上分别是:(硬件) -> 中断处理程序 -> 设备驱动程序 -> 与设备无关的操作系统软件 -> 用户级IO软件
1、中断处理程序
处理中断的流程:
- 保存没有被中断硬件保存的所有寄存器(包括PSW【程序状态寄存器】)
- 为中断服务过程设置上下文,可能包括设置TLB、MMU和页表
- 为中断服务过程设置堆栈
- 应答中断控制器,如果不存在集中的中断控制器,则再次开放中断
- 将寄存器从它们被保存的地方(可能是某个堆栈)复制到进程表
- 运行中断服务过程,从发出中断的设备控制器的寄存器中提取信息
- 选择下一次运行哪个进程,如果中断导致某个被阻塞的高优先级进程变成就绪状态,则可能选择它现在就运行
- 为下一次要运行的进程设置MMU上下文,也许还需要设置某个TLB
- 装入新进程的寄存器,包括其PSW
- 开始运行新进程
2、设备驱动程序
to be continue…