I/O输入输出
I/O硬件原理
I/O设备
I/O设备主要分两种(算上时钟是三种):块设备和字符设备。
块设备每次把信息存储在固定大小的块里,然后传输;字符设备以字符为单位发送或接收一个字符流。
设备控制器
I/O设备一般由两个部分组成:电子部件和机器部件。电子部件又称为设备控制器,它们通常包含了一个小的芯片,内存,小的存储单元,因此每个设备控制器就像一个微型计算机一样。
设备控制器负责把串行地把比特(位)流转换成字节块(一个字节=8比特),并进行有必要的错误纠正工作。
内存映射I/O
刚刚说了,设备控制器(一下简称设控)一般都有几个寄存器用来和CPU通信,比方说CPU可以借此命令设控发送数据,接收数据等。当然一般的设控都有一个可被操作系统读写地缓冲区。
有一个问题,CPU如何和设控通信?有两个方法,一个是对于每个设控寄存器,分配一个I/O端口号;第二个方法是把所有设控寄存器映射到主存里面去。
前者需要使用特殊的汇编命令才能访问,比方说:IN和OUT。
第二种方法又称为内存映射I/O。每个设控寄存器被分配一个唯一的,不会被再次分配地内存空间。第三种是混合实现,这种方案具有内存映射I/O地数据缓冲区,同时设控寄存器也拥有自己的I/O端口。这也是x86架构使用的方法。
来看看他们的优缺点:
对于内存映射I/O,不需要额外的汇编代码即可在C语言里访问设控寄存器地值(IN/OUT不属于C能嵌入的汇编);
而且不需要特殊的机制进行保护,这样就可以避免用户进程地读写。因为设控寄存器地值放在了内存里,只要不把这个页面分配到用户进程的地址空间就好了,而且正因如此,很容易做驱动程序唯一性定位,OS仅仅需要在驱动程序进程的页表包含相对应的设控寄存器页面就行了;
第三个好处就是,想要对数据操作,直接对内存进行操作就行,如果不是内存映射I/O,那还得读入到CPU,再进行操作,多了一步指令。
不过,万事皆有利有弊,内存映射I/O的缺点是,想要实现此操作,硬件和OS必须禁用相应页面的高速缓存,否则会造成I/O异常(比如重复读取,无限不可用等),这就需要复杂的硬件和OS操作;
另一点就是,当代OS大多拥有内存专线,所以I/O设备没法查看内存地址。此外,还必须采用特殊的机制使内存映射I/O工作在这样的模式下。一种方法是把全部引用发送给内存,若失败,转发至I/O;或者使用探查法,放过可能是I/O地址的引用;再或者,进行规定范围,范围之外发送到I/O。
直接存储器存取(DMA)
CPU直接存取I/O设备过于浪费时间,于是引入DMA方便处理,它就像CPU的小助手,独立于CPU,可以实现对几乎所有I/O设备的访问,读写。很多时候,都是主板自带一个DMA控制器,这样CPU直接访问它就可以了。
一个DMA包含多个可被CPU读取设置的寄存器,其中包含一个内存地址寄存器,字节计数器,一个或多个设控寄存器。设控寄存器指定要使用的I/O端口,传送方向(从哪个I/O设备读写到哪个I/O设备),传送单位,以及在一次突发传送中要传送的字节数。
来看一个使用DMA读取硬盘的例子:
首先,CPU设置DMA的控制器的寄存器,告诉他数据放到哪里,一会我去拿;读多少个;一次读几个之类的信息。然后CPU干其他事去了。(第一步)
接着,DMA向磁盘发送读请求(第二步),磁盘读取,把数据写入到内存(要写的位置,即内存地址在总线的地址线上),这是一个标准的总线周期(第三步)。
此时完成写入,磁盘发处应答信号(第四步),DMA收到,步增内存地址,步减字节计数器,若不为0,重复2-4步,当字节计数器为0时,DMA中断CPU,OS开始实际的工作。
DMA在不同的系统上差距颇大,有些DMA可以一次处理多个传输,这样的DMA控制器内部具有多组寄存器,每一通道一组,CPU在不同的通道上装载不同的寄存器值发出请求,每路传送对应一个不同的I/O设控,每传送一个字,DMA就会决定下一次为哪个设备提供服务,他可能使用轮转算法,也可能会考虑优先级,不过它也可以调用全部通道为同一个I/O设备服务,所以通常总线上的每个设控的应答线都与全部通道绑定。
DMA可以在两种模式下工作:每次一字模式和每次一块模式。在前一个模式中,DMA每次请求传输一个字并得到这个字,如果CPU也想用总线,它就得等待,这一机制称为周期窃取,因为设控偶尔偷偷地溜进并且偷走一个CPU地临时的总线周期,从而轻微地延迟CPU。
如果是在块模式下,则是突发模式,因为此时占用的时间更长,发送的数据更多;不过缺点就是,如果此次传输数据比较多,时间比较长,那就可能阻塞CPU和其他I/O设备。
不过还有一种,飞跃模式,在这种模式下,DMA同时设控直接把数据放到某处。但是还有的DMA是叫设控把数据给它,它来放,这样多了一个额外的总线周期,不过更加灵活。
大多数DMA使用物理地址传送,这就要求当OS给DMA的地址寄存器赋值时,记得把虚拟地址转换成物理地址;还有一些使用虚拟地址,这就要求内存自带MMU而不是CPU。
哦对,DMA处理磁盘时,磁盘要先把数据读到缓冲区,原因有二:一是进行数据校检,二是,一旦磁盘开始读,数据就是以固定速率读出,而不缓存的话,磁盘需要写道DMA或内存,万一没抢到总线使用权,就尴尬了,数据还得管理,无疑加大了设计难度。
中断
说中断前先说说陷阱,此陷阱非那个捕猎的陷阱,这个陷阱指的是“陷入其中的”意思,用户程序需要内核级调用时(内核可以访问全部硬件),就会陷入内核运行(深入内核运行硬件级调用更合适),这就叫陷阱。
中断在于可以让CPU转去运行触发中断的进程(如果是时钟中断就随机运行一个进程),在于触发进程上下文切换,避免一个进程吃死CPU(这一点由时钟实现,后面讲)。
来看看在I/O里中断的事情趴!
首先,某个I/O完成,在分配给它的总线信号上置起一个信号,引发中断。中断控制器检测到,决定怎么处理。
如果没有其他的中断等待处理,就处理它的中断,如果正在处理某个中断,或更高优先级的中断同时发出,它就得等等了。然后这个I/O不停地置起信号,直到得到请求(你不给我我就闹~)。
为了处理中断,中断控制器在地址线上放一个数字,表明哪个设备需要处理,并对CPU置起中断信号。
CPU被中断,进行上下文切换,根据数字作为下标索引在中断容器这张表里寻找一个程序计数器,此PC指向一个中断服务程序,陷阱一般和中断共享同一个中断容器。
中断服务程序向中断控制器地某个I/O端口写入一个确定的值,告诉它CPU收到啦!可以再次发起中断了。不过,可以让CPU晚点再告诉中断控制器,这样CPU才能安静地处理当前中断。
接下来就是保存刚刚运行的那个进程的信息,比如PC等,或者也包含某些寄存器。
但是保存到哪?一种方法是内部寄存器。可是这样只有OS完全读取完毕后才能对中断处理器应答,不然可能造成新的中断程序抹掉刚刚保存的信息,但是如果此时中断被禁止,将会导致死机并丢失中断,丢失数据。
或者保存在堆栈中,谁的堆栈?当前程序的?那样可能会造成野指针,或者因为缺页中断进行中断进而导致更大的问题。内核堆栈?那样要进行MMU重置和TLB重置,开销相当大。
现代CPU常采用超线程技术和指令重排序技术,所以对于指令,并不是某条指令前的指令全部执行完毕,某条指令后的指令完全执行。所以借此可以划分精确中断和不精确中断。一般来说,精确中断具有以下四个特性:
1. PC(程序计数器)保存在一个已知的地方
2. PC所指向的指令之前的指令全部完成
3. PC所指向的指令之后的指令全部未执行
4. PC所指向的指令的执行状态已知
不满足这些要求的称为不精确中断。拥有不精确中断的计算机通常将大量的指令状态推到堆栈里,因为此时程序计数器没法精确的说明指令的执行情况了,所以只能靠OS利用堆栈中的信息进行推算,这就造成了重启机器极其复杂,致使中断响应缓慢,所以超标量的计算机反而不适合实时操作。
有些计算机是针对某些中断和陷阱是精确的,某些则是不精确中断,比方说,I/O处理时精确中断而除零异常则不是,因为重启进程没意义,除零本来就是错的;还有些计算机可以设置它的某个位,使它对于所有中断都是精确的,只不过对开销很大,因为需要实时记录指令日志并维护寄存器影子副本。
还有些计算机,如x86,具有精确中断以兼容老的软件,这就给芯片架构和工艺带来了复杂度。如果不使用精确中断,那么CPU处理速度会慢,如果用了,OS会慢,所以这是一个值得权衡的问题。
I/O软件原理
I/O软件的目标
与I/O设计一个关键的概念是设备独立性,即无关于具体的设备,用户都可以通过同样的方式访问到。
与设备独立性密切相关的是统一命名。即一个文件或一个设备的名字应该是一个不依赖于设备的名称。做到所有的文件和设备都采用相同的方式寻址,也就是路径名寻址。
I/O软件的另一个重要问题是错误处理,对于错误的处理,应该尽可能地把错误在底层处理了,尽量不往上抛。
另一个关键问题是同步和异步传输。异步传输在CPU发出I/O请求后便忙于其他的事了,同步传输则会阻塞CPU直到数据可用。
I/O软件还有一个问题就是缓冲。缓冲涉及大量的复制工作,并且经常对I/O性能有重大影响。
最后还有一个概念是共享设备和独占设备的问题,有些设备,比如磁盘,可以被多个用户使用,多个用户同时读取一个文件是没什么问题的,但是对于类似打印机的设备,则只能在同一时刻允许一个用户独占,独占设备引入了各种各样的问题,比方说,死锁。
程序控制I/O
I/O可以采用根本上不同的三种方式实现:程序控制I/O,中断驱动I/O,使用DMA的I/O。
I/O最简单的形式,是让CPU做全部的工作,CPU负责I/O设备数据写入,调度先后顺序,等待操作完成,读取响应,再写入下一个数据。这一方法称为程序控制I/O。程序控制I/O简单易实现,但是缺点也很明显,就是直到全部I/O完成之前,要占用全部的CPU时间(因为可能发生轮询或忙等待来进行I/O调度操作),这在任务复杂的系统里是不可接受的。
中断驱动I/O
对于某些服务,比方说打印机,一个字符一个字符的打印,那么需要CPU一个字符一个字符的复制到打印机寄存器里面去,而每次等待打印机完成打印并复制下一个字符的过程对CPU来说是很漫长的,所以可以使用中断驱动I/O。
CPU复制第一个字符,然后启用调度程序,运行另一个进程。
每次打印机完成打印,产生一个中断,CPU运行中断处理程序,保存当前进程状态,处理引起中断的程序,对中断控制器应答,调用调度算法,运行新的进程(可能是刚刚那个也可能不是)。
使用DMA的I/O
介于每次处理中断,保存进程状态,进行上下文切换的开销比较大,那么可以考虑使用DMA控制的I/O。在这里,DMA作为CPU小助手存在,帮它完成I/O期间的工作,CPU对它进行编程,然后去忙其他的事,DMA负责整个I/O周期并在完成后通知CPU,这就是DMA的I/O。
不过,如果DMA比较慢的话(通常比CPU慢),那么可以考虑使用软件控制I/O或中断驱动I/O。
I/O软件层次
中断处理程序
对于中断,应该尽可能把它隐藏起来,其中最好的方法就是,把启动I/O操作的驱动程序阻塞起来,直到I/O完成产生一个中断。驱动程序可以通过多种方法阻塞自己,比方说对信号量执行down操作。对一个条件变量执行wait操作或在一个消息上执行receive操作。
当中断发生,中断处理程序进行工作以处理中断,然后,把触发中断且被阻塞的驱动程序解除阻塞。
不过实际的处理可能要复杂一些,对OS而言,在中断处理程序结束后,还有更多的工作去完成。这些工作是在硬件中断完成之后必须在软件层面完成的。
1. 保存左右没有被中断硬件保存的寄存器(包括PSW)
2. 为中断服务程序设置上下文,包括TLB,MMU和页表
3. 为中断服务程序设置堆栈
4. 应答中断控制器(表明可以再次接收中断),如果不存在集中的中断控制器,则再次开放中断
5. 将寄存器从他们被保存的地方复制到进程表里
6. 运行中断服务程序,从发出中断的设控的寄存器里提取信息
7. 根据调度程序选择接下来要运行的进程
8. 为下一次要运行的进程设置MMU,TLB等
9. 装入新进程的寄存器,包括PSW
10. 新进程开始运行
借此可见,中断处理还是蛮耗费CPU的。
设备驱动程序
设备驱动程序,用来联系实际的硬件控制器,作为CPU和设控之间的桥梁存在。驱动程序一般作为OS的一部分而被安装在OS内,所以OS开发者一般也会为它们保留接口。
一般操作系统会把驱动程序分为块驱动程序和字符驱动程序,OS会为他们分别预留一些接口供它们实现。对于驱动程序而言,它们必须是可重入的,这代表对于一个驱动程序而言,在第一次调用完成之前第二次被调用。
驱动程序一般不允许系统调用,但它们常常需要与内核的其余部分进行交互。
与设备无关的I/O软件
与设备无关的I/O软件的基本功能是执行那些所有I/O设备都有的功能,并向上暴露给用户层软件一个统一的接口。
驱动程序统一接口的主要目的是简化OS接口的设计,尽量让不同驱动程序可以更简单地装载进OS中。通过对不同的I/O设备驱动程序提供一致的,公共的接口实现。
驱动程序一般向OS提供一个函数表指针,表里面包含了此驱动支持的调用。OS可以通过这个指针来实现对某一驱动的某一方法的精确调用。
所有设备都具有主设备号和次设备号,并且所有的驱动程序都是通过使用主设备号来选择驱动程序而得到访问。
缓冲在数据处理上显得格外重要,无论是块设备还是字符设备。如果不使用缓冲,每次数据到来都会调用程序,这无疑是低效的。所以引入缓冲区这个概念,啥意思呢?就是每次把到来的数据提前的缓冲器起来。待到需要的时候再给用户,但是这就引发了几个问题。
如果把缓冲区放在用户进程,那么在处理时缓冲区被分页而调出了内存怎么办?锁定或许可以,但是进程多了之后就会导致内存池减少,系统性能降低。
再在内核建立一个缓冲区,每次满了后复制到用户?如果正在调入的过程,新的数据到达,怎么保存呢?那就再建立一个缓冲区,此时新的缓冲区接收,旧的复制到用户,而这,就是双缓冲。更进一步,可以做一个循环缓冲,让两个缓冲区交替接收,送回数据。
缓冲在输出上也很有用,对于输出,可以把要输出的内容复制到内核,交由内核处理,用户进程转而去忙其他的事。
但是过多的缓冲不一定是有益的,比方说一个典型的网络I/O:
对于错误报告,一般有两种,编程错误和实际的I/O错误。前者比如读打印机,写键盘等不可能的操作,仅需向调用者返回错误代码即可,后者可能需要驱动程序来决定应该做什么。
对于设备来说,还可以为他们设计一个专用的分配与释放程序用来管理I/O设备资源。
对于磁盘来说,不同的磁盘可能有不同的扇区大小,这一点应该由与设备无关的软件完成对OS的隐藏,以此来提供一个大小固定的块。
用户空间的I/O软件
对于I/O软件,有一些是存在于用户空间供用户调用的,比方说C语言的printf和scanf函数,就是典型的例子。当然,这属于用户层面的库过程的调用,并不是所有的都是这么实现的,可以假脱机机制。比方说打印机。但是这时就诞生了一个问题,那就是可能会发生某个进程长时间占用假脱机的情况。所以可以为它设立一个守护线程和假脱机目录,每次需要打印的文件放到假脱机目录就好,然后由守护进程负责处理,这里,守护进程是唯一可以访问假脱机的进程,这样就避免了刚刚的问题。
来看一个总结吧!
盘
盘硬件
磁盘是最常用的存储介质,即使是在SSD大行其道的今天依旧如此,机械硬盘有着不可替代的地位。
来看一下磁盘结构:
柱面是抽象出来的概念,是一个个同心同半径的磁道组成的。每个磁盘有上下两个面,从上向下依次为0号-n号磁盘面。
当代磁盘扇区并不同半径的,而是没向外一个磁道,大概增加4%的扇区,这是为了充分利用空间。比如下图的第一个。
磁盘控制器在今天来看,更像是一个微处理器,它可以接受一组高级指令,可以对数据缓存,坏块重映射等工作。对于磁盘驱动程序有一个很有意义的指标就是,磁盘控制器能否一次控制两个或多个驱动器(马达)进行寻道。这被称为重叠寻道。一个控制器可以在让某个驱动器读取数据的时候,命令另一个驱动器去寻道。不过,一般而言一个控制器只会和主存有一次传输,因为多次传输会降低平均速率。
RAID
磁盘格式化
在磁盘能使用前,必须经过由软件完成的低级格式化。
每一个扇区的组成如下所示。
其中,前导码以一定的位模式开始,位模式使硬件得以识别扇区的开始,前导码还包含柱面与扇区号,以及某些其他信息。数据部分的大小由格式化程序决定,一般为512字节。ECC(错误校正码)域包含冗余信息,用来恢复读错误,一般的硬盘都有备用扇区,用来重映射坏扇区。
当然,在低级初始化时,可能会进行柱面斜进,因为每次进行连续读取时,当前磁道读完了,可能需要读取下一个磁道,如果磁道起始终止位置一样的话,那么大概率在把磁头移过去时,第0号扇区已经过去了,因为移动磁头需要时间。所以可以把下一个磁道的0号扇区后移几个,这就叫做柱面斜进。
当然了,也存在磁头斜进,不过他的时间远小于一个柱面斜进的时间。
当控制器读取到扇区上的数据时,进行ECC校检之后,会把数据写回到主存,这个过程需要时间,所以可能下一个扇区就在磁头下溜走了,所以可以错开一个存放,给控制器一点时间,或者错开两个,前者是单交错,后者是双交错。
低级格式完成后,要对磁盘进行分区,逻辑上,每个分区就是一个独立的磁盘。一般来说,0号分区是主引导记录,现代操作系统也支持GPT,即GUID分区表,以实现更大的分区。
分区完毕后,对每个分区进行高级格式化。这一操作会设置引导块,空闲分区管理(空闲列表或位图),根目录,和一个空的文件系统。还要把一个代码设置在分区表的对应项里,用以表明在此分区中使用的是哪个文件系统。
当电源打开,BIOS运行,读入主引导记录,并进行跳转,然后这一引导程序试图找到活动的分区,接着找到存放操作系统内核的文件系统,载入OS。
磁盘臂调度算法
对于一个磁盘,读写一个块需要的时间主要由以下因素决定:
1. 寻道时间(将磁盘臂移动到适当的柱面上所需要的时间)
2. 旋转延迟(等到适当的扇区旋转到磁头下所需的时间)
3. 实际数据传输时间
前两个是影响时间的主要因素。
来看几个磁盘臂调度算法是怎么完成减少时间这一目标的。
先来先服务算法(FCFS),组织一个链表,把所有请求按照到达顺序组织起来,依次处理,但是时间消耗比较严重。
最短寻道优先算法(SSF),每次处理与当前磁头最近的请求,这一算法会导致请求分配不公平,边缘的请求被执行的机会会少很多。
电梯算法,此算法维护一个方向位,每次处理完一个请求,检查方向位是UP还是DOWN,如果是UP,则上移处理更高位置的请求,如果没有更高位置的请求,则方向位取反,然后重复刚刚的操作。如果没有请求,就停在此处。
略微改进的算法,即方向总是按照相同的方向进行扫描。当处理完最高编号柱面上的未完成的请求后,磁盘臂移动到具有未完成的请求的最低编号的柱面,然后继续沿向上的方向移动。这样就好像最低编号是最高编号的临近位一样。把整个编号看成了环状的。
当然,实际使用上,如果当前柱面还有其他的请求,就直接处理了,因为它们仅仅是磁道不同而已,此时不需要寻道时间也不需要旋转延迟(因为不是当前磁头,是同一柱面的另一个磁道上的磁头)。
不过了,某些磁盘旋转延迟更大的话,那就要重新设计算法了,比方说未完成的请求按照扇区号排序(因为寻道很快,所以可以很快切换到下一个磁道,那么这时按顺序组织扇区就显得尤为重要)。
对于磁盘来说,一般都有一个高速缓冲区,对于读,除了读取需要的内容,还会把当前磁道剩余的读出来以备使用;写入的话,把数据放在高速缓冲区,一次性写入更快一些。
当一个控制器上有多个驱动器时,操作系统会为每个驱动器维护一张表。一旦某个驱动器空闲了,就把它移动到准备请求的柱面下;当前传输结束时,会检查是否有驱动器位于正确的柱面下,如果有则开始下一次传输,如果没有,驱动程序则在刚刚完成传输的驱动器上发出一个新的寻道指令并等待,直到下一次中断,检查哪个磁臂先到了目标位置。
错误处理
磁盘可能出现坏扇区的情况,那么需要进行ECC修复,这是在错误仅有几位的情况下,如果扇区无法通过校检修复,就只能使用备用扇区以及重映射了。
一般有两个方法处理坏块,在控制器中对他们进行处理或者在OS中对他们进行处理。前一个方法是使用备用扇区进行替代,这是在出厂前进行的处理,有两种方法实现这个:
对于第一种修复方法,需要维护一张表格,记录坏块的替换块是谁,每个磁道一张表;对于第二种方法,则是通过设置前导码实现,不过这种方法需要坏块后面全部的前导码,好处就是性能好一些,不需要旋转一圈来找到坏块的备用块。
如果已经开始运行了,发生了坏块,那么使用第一种方法更好一些,因为第二种方法还要进行数据的复制,可能比较耗时,引起系统卡顿。
如果是操作系统做处理,就得获得一个坏扇区表,比方说通过读取磁道表,或者自己遍历获取全部块的状态,然后他们写到一个隐藏文件里去。并且保证坏块不能出现在任何文件,空闲表里。对于备份,实用软件如果是对文件备份,那么应该由OS隐藏坏块以避免被读到,如果是一个扇区一个扇区地备份,那只能期待写这个软件的程序员足够聪明了——在连续10次失败后放弃备份这个扇区。
稳定存储器
时钟
时钟在一个计算机系统里很重要。
时钟硬件
一般时钟使用晶体振荡器实现,加以一定的电压,石英晶体就能按一定的频率震动,产生周期性信号,把这个信号乘一个整数,就可以得到一个很大的频率。它给计算机的各个电路提供一个同步信号,这个信号被送到计数器,使其递减为0,引发一次时钟中断。
可编程时钟有几种编程模式。在一次完成模式下,当时钟启动时,他把存储在寄存器的值复制带计数器,每一次石英晶体信号都使计数器减1,当递减为零,产生一个中断,停止工作,直到软件显式地启动它。在方波模式下,当计数器变为0时,寄存器(所以这个寄存器可以用来设置中断周期)的值自动复制到计数器,整个过程无限期再次重复下去,这些周期性的中断称为时钟滴答。
可编程时钟的优点就是,中断频率可以由软件设置,一个可编程时钟芯片通常包含两个或三个独立的可编程时钟,并且还包含其他选项,比方说,屏蔽中断,用正计时代替倒计时。
为了防止电源关闭导致计算机丢失当前时间,主板上通常有一个低功耗时钟系统,每次系统启动时,会读取其时间,然后转换成自某个标准时间开始后的时钟滴答数。(Unix是1970/1/1/12:00 UTC时间,Win是1980/1/1)当然也可以联网获取,或者手动设置。
所以时钟啊,除了产生时钟中断,还用来计时。
时钟软件
时钟硬件所做的全部工作是根据已知的时间间隔产生中断。其他所有的全部工作都由软件——时钟驱动程序完成。一般而言,包含以下工作:
1. 维护实际时间(几点了几点了的意思)
2. 防止进程超时运行
3. 对CPU的使用情况记账
4. 处理用户进程提出的alarm系统调用
5. 为系统本身的各部分提供监视定时器
6. 完成概要剖析,监视和统计信息收集
维护实际时间有三种方法,一种是用64为计数器累计时钟滴答数,第二种是以秒为单位,每次时钟滴答了一秒的长度就加1,最后一种记录开机时备份时钟的滴答数,并从此时开始累加,需要时间时,把备份时间加上累加时间就可以了。
时钟的第二个功能是防止进程超时运行,每次调度一个进程时,就把计数器的值初始化为以时钟滴答为单位的该进程的时间片的取值(CPU时间片(假设为ms)/时钟滴答间隔(单位也是ms)=此计数器的取值),每次滴答将此计数器减一,为0时执行调度程序。
对CPU有两种方式进行记账,精确的方法是,每次进程启动时,就启动一个辅助定时器,开始计时,每次中断保存此辅助计数器,然后中断结束继续;最后读出此辅助计数器的值即可。另一个方法可能没那么精确,全局变量保存一个指向进程表的指针,每次时钟滴答,通过指针把该进程的计数器加1,相当于此进程在为它的运行付费。但是这里有一个不精确的地方就是,如果发生中断,那么即使这个进程没用CPU,他还是缴费了?!但是吧,精确记账代价太高了,一般都是第二种方法。
时钟的另一个用法是,接收进程请求然后再一定时间间隔后向它报警,可能是信号,中断,消息或者类似的东西。
如果时钟驱动程序有足够多的时钟,他就可以为每个请求设置一个时钟,如果不是,就必须用物理时钟来模拟多个时钟。比方说,维护一张表,把所有定时器的信号时刻计入表中,每次日时间更新时,驱动程序进行检查以了解最近的信号是否已经发生。如果是的话,则在表中搜索下一个要发生的信号的时刻。如果有多个信号,那么用一个链表把他们按时间顺序链接起来,每一次滴答时,下一个信号减1,当他变为0时,就引发与链表中第一个表项相对应的信号,然后删除此表项。
监视定时器可以被用来对系统死机进行复位,系统运行时,它会定期的复位定时器,所以定时器不会过期。所以一旦定时器过期,则证明系统很久没有运行了,这是可以考虑进行全系统复位。
最后,时钟机制可以帮助统计某一进程的运行情况,通过把该进程囊括的程序计数器的区间加1(起始地址,终止地址+1)。然后呈现出来,这是对于程序的剖析。
软定时器
一般而言,有两种方法管理I/O,一种是中断,一种是轮询,中断延迟低,响应及时(几乎是立即响应的),但是中断开销大;轮询需要忙等待,平均等待时间是轮询时间的一半,也不是很优秀。所以引入了软定时器这个概念。
软定时器避免了中断。当内核运行结束准备返回到用户态时,检查实时时钟,以了解软定时器是否到期,如果是,就执行被调度的事件。,这样就避免了内核态用户态切换。工作完成,复位软定时器,通过把当前时间加上时间间隔即可,以便再次闹响。
在这里,软定时器起到了实际时钟中断的作用——使CPU去执行其他进程,所以时间间隔的设置就显得很重要,因为软定时器随着其他进入内核的原因而改变频率,这些原因包括系统调用,TLB未命中,页面故障,I/O中断,CPU变成空闲等,所以一般设置2微秒(us)就可以。其实也可以这么理解,就是软定时器想运行,必须在它时间到了的时候(可超时),内核有程序在运行且已经运行完毕,此时才可以运行软定时器设置的进程,所以它会受进入内核的程序的影响。