【知识强化】第五章 输入/输出(I/O)管理 5.2 I/O核心子系统I
学习I/O核心子系统相关的一系列功能。
设备独立性软件、设备驱动程序、中断处理程序这三层其实是属于操作系统的内核部分的,所以它们也称作“I/O核心子系统”,又可以简称为“I/O系统”。在考研当中我们需要重点掌握的是:I/O调度、设备保护、假脱机技术(SPOOLing技术)、设备分配与回收、缓冲区管理(即缓冲与高速缓存)这几种功能的原理和实现。
那我们来看一下这些事情分别需要在哪些层次实现。所有和硬件直接相关的,那肯定是设备驱动程序和中断处理程序需要负责的。但是咱们刚才提到的那些功能当中都没有直接与硬件相关的。
这个地方大家可能会比较奇怪,刚才不是说I/O核心子系统(I/O系统)要实现的是这些技术吗?而假脱机技术(SPOOLing技术)看起来并不是I/O核心子系统(I/O系统)来实现的。一般来说假脱机技术(SPOOLing技术)都需要使用到磁盘这种设备的设备独立性软件这一层的服务,所以假脱机技术(SPOOLing技术)一般来说都是在用户层软件实现的。
但是在咱们408的考研大纲中,又把假脱机技术(SPOOLing技术)把它归为I/O核心子系统(I/O系统)要实现的一个功能。所以在考试的时候如果我们遇到了,那还是以这个大纲为准。不过大家也需要知道,在实际应用中,其实假脱机技术是用户层软件这儿实现的。
那这个小节中我们先简单地介绍两个我们很熟悉的功能的实现,一个是I/O调度,一个是设备保护。首先来看I/O调度,其实I/O调度和处理机调度是很类似的,就是用某一种算法来确定一个好的顺序,来处理各个I/O请求。就比如说咱们在上一章学过的磁盘调度,其实就是用某一种算法,来确定应该先满足哪些磁盘的访问请求。那由于磁盘其实也是一种I/O设备,所以其实磁盘调度也是I/O调度的问题。
那除了磁盘之外,像什么打印机等等这些设备,也同样是可以采用类似于先来先服务啊,优先级算法或者短作业优先等等这些算法来确定一个合适的I/O调度顺序。那这些咱们之前已经训练过很多次了,所以这儿就不再赘述。大家只需要根据题目中提供的算法的名字,再结合咱们之前学习过的那些调度算法的思想来分析、做题就可以了,所以I/O调度其实是我们很熟悉的一个内容。
那接下来我们再来看一下设备保护。这一点其实咱们在上小节也简单地提及过,其实怎么实现设备保护这一点咱们也已经很熟悉了。因为在UNIX系统当中,设备会被看作是一种特殊的文件,因此其实系统也会为各个设备建立一个相应的FCB,也就是文件控制块。那就像咱们之前在文件管理的章节中学到的一样,不同的用户对于不同的文件是有不同的访问权限的。但它其实上其实也是文件保护那一块所需要做的事情。那这个知识点如果回忆不起来的话,可以再参考咱们在第四章中文件保护那个小节当中讲到的内容。
好的,那么这个小节我们只是简单地对I/O核心子系统(I/O系统)需要实现哪些功能进行了一个简要的概述,并且简单提了一下I/O调度和设备保护应该怎么实现,这都是咱们很熟悉的内容。那之后的小节中我们还会展开介绍设备分配与回收、内存缓冲区管理(缓冲与高速缓存)应该怎么实现。而假脱机技术(或者叫SPOOLing技术)它在实际当中本来是在用户层软件当中实现的,但是在408考研大纲中是在设备独立性软件中实现,也把它归为I/O核心子系统(I/O系统)要实现的功能之一,所以我们按照从上至下的顺序在下个小节会先介绍假脱机技术(或者叫SPOOLing)的实现。
学习假脱机技术(又叫SPOOLing技术)。
假脱机技术(又叫SPOOLing技术)其实是用软件的方式来实现了脱机技术。那最后我们会以共享打印机的案例来分析一下就是假脱机技术的具体实现和应用。
咱们在第一章操作系统的发展历程那个小节当中学过,在刚开始就是手工输入的这个没有操作系统的阶段,都是需要用这样的纸带机,很慢速的纸带机,来把这些程序员的这些程序读入,然后CPU需要直接和这个纸带机交互。所以由于纸带机的速度特别慢,因此这个输入还有把结果输出的过程都会很慢。所以虽然CPU的处理速度很快,但是在数据输入和数据输出的时候,CPU依然需要迁就纸带机的这个慢速的输入输出。
所以在这个阶段就暴露出了很严重的这种速度矛盾,这对CPU这种昂贵的资源是一种极大的浪费。
所以在之后的批处理阶段人们引入了脱机技术,那刚开始的脱机技术是用磁带来实现的。引入了脱机技术之后,程序员可以先用纸带机把自己的这些程序输进数据输入到磁带当中,而磁带的速度要比纸带机要快的很多。而这个输入的过程是由一台专门的外围控制机来实现的。那由于这些程序的数据首先被输入到了这个更快速的磁带当中,而之后CPU可以直接从这个磁带中读取想要的这些输入数据。因此这就很大地缓解了这种速度的矛盾。也就是说有了脱机输入技术之后,在数据输入的时候速度就快了很多。那在输出的时候其实也一样。主机首先会把数据输出到一个很快速的磁带上,之后又会由一个外围控制机控制要把这磁带当中的数据依次地输出到这些这个慢速的磁带机上。那同样的,由于磁带的速度比纸带机的速度快很多,因此CPU在输出的时候就可以节省很多等待输出完成的这个时间了。那这样的话就大大地提升了CPU的利用率。其实这个输入和输出它是由一个外围控制机来实现的,那显然这个输入和输出的过程其实并不需要主机或者说CPU的干预和控制,这样的话就可以让CPU有更多的时间去处理别的那些计算任务了。所以其实引入了脱机技术之后,除了缓解了I/O设备还有快速的CPU之间的速度矛盾之外,还有一个好处就是,即使CPU正在忙碌,但是也可以在外围控制机的控制下,先把数据预先地把它输入到这个磁带当中。而在输出的时候,即使这个纸带机现在正在忙碌,但是CPU也可以先把数据输出到这个磁带当中。
那基于脱机思想,人们又发明了所谓的假脱机技术,又叫“SPOOLing技术”。假脱机技术当中的脱机,其实是由软件的方式来模拟实现的。那一个SPOOLing系统或者叫假脱机系统,一般来说由这样的一些部分组成。
那这是输入井和输出井的一个作用。那除了这个磁带之外,外围控制机其实也是实现脱机技术当中一个很重要的一个部件。
那外围控制机就是由一个输入进程和一个输出进程来实现的。输入进程就是模拟脱机输入时候的外围控制机,而输出进程是模拟脱机输出时候的外围控制机。那显然这个输入进程和输出进程肯定需要和用户进程并发地执行才可以完成这种模拟脱机输入和模拟脱机输出的过程。因此SPOOLing技术肯定是需要有多道程序技术的支持的。
输入进程就是模拟这个外围控制机的功能。也就是说,当有数据需要从设备输入到计算机的时候,这个输入进程其实是由软件的方式模拟外围控制机。这个输入进程会把这些要输入的数据放到磁盘的输入井当中,而输出进程在模拟脱机输出的时候原理也类似,无非就是从磁盘的输出井当中取出数据,然后输出到这个设备上。
那这个地方大家还需要注意,在内存当中也会开辟这样的两个缓冲区,一个叫输入缓冲区,一个输出缓冲区。其实它们的作用就是在模拟脱机输入还有模拟脱机输出的时候作为一个数据的中转站这样的一个角色。所以输入进程在实现模拟脱机输入的时候,其实是先接收了这个输入设备的数据,然后把这些数据先放到输入缓冲区里,之后再把输入缓冲区当中的数据放到磁盘的输入井当中。而数据输出的时候,其实是由输出进程从输出井当中取出数据然后放到这个内存的输出缓冲区当中。之后再由输出缓冲区往输出设备写出数据。那这就是假脱机技术的一个原理。大家再结合刚才的图再自己理解一下。
那接下来我们来看一个具体的假脱机技术(SPOOLing技术)的应用——共享打印机的实现。打印机它肯定是一种“独占式的设备”,因为打印机的这个速度很慢,这个这一点大家在生活当中也有所体会。所以打印机这种设备在一段时间内肯定是只能为一个用户、一个进程服务的。如果打印机设备同时处理多个进程的请求的话,那么就有可能会导致各个进程的打印输出结果相互串行,就是混杂在一起的这种情况,那这显然不是我们期待的。
只有第一个进程使用完了打印机并且释放了这个资源之后,第二个进程才可以被激活然后继续发出这个打印请求。那既然打印机它是一种独占式的设备,为什么我们这儿还把它称作共享打印机呢?
那其实这儿所谓的共享就是用SPOOLing技术,也就是假脱机技术来实现的。而是由一个叫做假脱机管理进程的东西为每个用户进程做这样的两件事。
也就是说其实这个缓冲区是在磁盘里的缓冲区,而不是内存中的缓冲区。
那之后假脱机管理进程会把用户进程想要打印输出的数据
把它先放到给它申请的这个缓冲区当中。这是这个管理进程做的第一件事。
第二件事它还会为这个用户进程申请一张空白的打印请求表,并且把用户的打印请求各种参数填入表中。所以其实这个表就是用来说明用户的打印数据放在哪个缓冲区里,存放在什么地方等等这一系列信息的。所以其实不要被这个浮华的名词所迷惑,它的本质无非就是一个打印任务的说明书。那接下来这个打印任务的说明书会被挂到一个叫做脱机文件队列的队列上。
那这个文件队列其实就是一系列的打印任务的队列。所以之后这个输出进程会从这个任务队列当中取出某一个任务,然后根据这个打印请求表的说明来执行相应的那些打印任务。
那比如说刚才这个用户进程提出的打印任务此时就是挂在这个队列的队头的。所以此时输出进程应该先满足的是队头的这个任务。因此,根据这个打印任务表它就可以知道此时要打印输出的这些数据是在这一块缓冲区当中的。
所以它会从这块缓冲区当中读出这个数据,然后把这个数据首先读入到内存的输出缓冲区当中。
之后再从输出缓冲区把这个数据输出到打印机上,让打印机执行具体的打印任务。
那当这个打印任务完成了之后,这个打印文件表就可以从这个队列当中删除了。那之后只要打印机空闲,输出进程就可以从这个队列中再取出下一个任务,然后根据这个任务的指示来执行后续的那些打印任务。
所以在采用了SPOOLing技术之后,虽然系统当中只有一台打印机,但是每个进程在提出打印请求的时候,系统都会为这个进程在输出井当中申请一个缓冲区、一个存储区。其实这就相当于为这个进程分配了一个逻辑上的设备,所以这样的话就可以让每个用户进程都感觉到似乎都是自己独占了一个打印机设备一样,这样的话就实现了对打印机的共享使用。
所以SPOOLing技术可以把一台物理上的设备虚拟成若干个逻辑上的多台设备。可以把这种独占式的设备改造成共享设备,这就是SPOOLing技术的一个具体应用。
脱机技术它可以实现数据的预输入和缓输出。那在现代的操作系统当中,一般来说使用软件的方式来模拟实现脱机技术,也就实现了所谓的假脱机技术,也叫SPOOLing技术。那我们需要对假脱机技术当中的输入井、输出井、输入进程、输出进程等等这些概念和脱机技术当中的那些部件一一地对应起来,来理解和记忆。那最后我们又介绍了SPOOLing技术的一个具体应用,把独占式的打印机改造成一个共享打印机。
在之前的小节中我们介绍了I/O核心子系统(I/O系统)需要实现哪些功能,那上一小节我们介绍了在用户层软件需要实现的SPOOLing技术,从这个小节开始我们会开始介绍设备独立性软件这一层需要实现的两个功能。一个是设备的分配与回收,下一节会讲内存缓冲区的管理(缓冲与高速缓存)。
之后会介绍两种很常用的分配策略,静态分配与动态分配。再之后我们介绍设备分配的是时候操作系统用来管理这些设备资源需要使用的数据结构,并且介绍设备分配的具体步骤,最后会介绍一种设备分配步骤的改进方法。那我们会按从上至下的顺序依次讲解。
独占设备就是一个时间段内只能分配给一个进程使用的那些设备,就比如说咱们很熟悉的打印机。
那共享设备呢就是可以同时分配给多个进程使用的那种设备,比如说像磁盘。但是所谓的这种共享,其实是宏观上共享,但微观上各个进程有可能是交替地使用着这个设备的。
那虚拟设备呢,其实就是我们上个小节介绍的,采用SPOOLing技术把独占设备改造成虚拟的共享设备。在采用了SPOOLing技术之后,我们可以把独占式的打印机改造成共享打印机,并且把它同时分配给多个进程使用。那这就是我们需要考虑的设备的固有属性。
但是如果从设备的分配算法这个角度来看的话,可以有很多我们很熟悉的算法来进行设备的分配。比如说先来先服务,或者优先级高者优先,短任务优先等等。那这些算法的具体策略其实通过算法的名字就可以判断,所以这儿就不再展开赘述。
但是如果从设备分配的安全性这个角度来考虑的话,可以有这样的两种分配方式,一种叫安全分配方式。也就是为一个进程分配了设备之后,就一定会把这个进程阻塞,直到这个进程释放这个I/O设备之后才会把它唤醒让它继续往下执行。那我们来考虑一下进程请求打印机打印输出的例子。如果采用的是安全分配方式的话,那么一个进程在请求使用打印机并且把打印机分配给它之后,这个进程就必须先阻塞等待。虽然按理来说这个进程只需要把数据丢给打印机它其实就不用再管打印机那个设备,它还可以继续往下执行。但是由于我们采用的是安全分配的这种方式,所以这个进程不得不阻塞,一直到打印结束之后进程才会被再次唤醒,继续往下执行。
所以采用安全分配方式的话,在一个时间段内,每个进程只能使用一个设备。它的优点呢就是破坏了我们之前学过的“请求和保持”条件,所以不会导致死锁,所以它才叫安全分配方式。但是缺点呢也很明显,就是对于一个进程来说它的CPU和I/O设备只能串行地工作,因此系统资源的利用率低,并且进程的执行效率也会降低。
那与这种分配方式相反的,另一种叫做不安全分配方式。就是进程发出I/O请求之后,系统为其分配I/O设备,这个进程不会被阻塞,它可以继续往下执行。之后还可以发出更多的新的I/O请求。一直到某一个I/O请求得不到满足的时候才会把这个进程阻塞。那还是以进程请求打印机打印输出的例子。比方说采用的是不安全分配方式的话,那么一个进程在请求打印机的服务之后,系统会把打印机资源分配给它。但是这个进程把数据丢给打印机之后,它其实就不需要再管打印机的这个打印输出过程,它可以继续往下执行。甚至在之后的执行过程中它还可以申请使用别的I/O设备,比如说像扫描仪啊等等。所以如果采用的是不安全分配方式的话,那么一个进程可以同时使用多个设备。这种方式的优点就是效率高,进程的计算任务和I/O任务可以并行地执行。但缺点呢就像这个名字所说的这样,它不安全,有可能会发生死锁。那我们在第二章中学过,如果采用的是这种方式的话,可以用死锁避免也就是银行家算法来避免系统进入不安全状态。那这是在设备分配的时候需要考虑的三个角度的问题。
那接下来我们来看两种我们很熟悉的设备分配方式——静态分配和动态分配。所谓静态分配就是指进程运行之前就为其分配它所需要的全部资源,一直到进程运行结束之后才把资源归还给系统。而如果说进程在运行之前它所需要的全部资源暂时得不到完全的满足的话,那么这个进程就暂时不能投入运行的。那这也是我们在死锁那个章节学过的内容。
其实,静态分配方式就是破坏了“请求和保持”条件,所以采用这种分配方式的话是不会发生死锁的。那讲到这里,希望大家也可以再回忆一下咱们死锁发生的四个必要条件,分别可以用什么样的方法破坏这四个必要条件呢。这是大家需要思考和复习的地方。
那与静态分配方式相对的,另一种分配方式叫做动态分配方式。就是进程的运行过程当中,动态地申请设备资源,并不是刚开始就得到全部的资源。那这两种方式咱们在死锁那个小节当中也讲过,所以这儿就不再展开。
接下来我们来看一下,设备分配管理当中,需要用到哪些数据结构。那在讲这些数据结构之前我们先来复习一下设备、控制器还有通道之间的关系。一个通道可以控制多个控制器,而一个控制器又可以控制多个设备,所以我们可以把它们之间的关系理解为一个树的这种形状。那从另外一个角度来说,每一个设备它肯定会有一个它所从属的控制器,而每一个控制器也肯定会有一个它所从属的通道。这儿需要注意啊,一个系统中,可能会有多个通道。那由于要控制一个设备,肯定需要找到这个设备的控制器。而要控制一个控制器,肯定又需要找到这个控制器所从属的通道。所以设备分配管理中的数据结构,需要表示出这种从属的关系。
那系统会为每一个设备配置一张设备控制表,英文缩写叫DCT,用于记录设备的使用情况。
一个设备分配表当中,可能会有这样的一些常用的字段。
设备类型指的是这个设备到底是打印机,还是扫描仪还是键盘等等等等。
而设备标识符其实就是我们之前提到过很多次的所谓的物理设备名,它是这个设备的唯一ID。那系统中各个不同的设备它的设备标识符都是不同的。
那设备状态这个字段呢又是用来区分这个设备此时是处于忙碌啊、空闲啊还是故障啊等等这些不同的状态的。
而指向控制器表的指针这个就是咱们刚才提到的,用于找到一个设备它所从属的控制器到底是哪一个。
重复执行次数或时间。这个字段是用来干什么的呢?一个设备有很多机械零件,所以设备故障的几率其实是很大的。比如说使用打印机或者复印机的时候经常会出现卡纸的现象,不过并不是说执行一次I/O操作失败之后就认为这次I/O操作真的失败了。系统会重复执行多次这个I/O操作。那这个字段呢就是用于记录它到底失败了几次。只有重复执行多次之后仍然不成功,那才认为这次I/O是失败的。
最后一个字段——设备队列的队首指针。这个字段其实就是用于指向此时正在等待着使用这个设备的进程队列的。那这个队列由进程的PCB组成。
还记不记得咱们在“进程管理”章节当中曾经提到过,系统会根据各个进程阻塞原因的不同,将进程的PCB挂到不同的阻塞队列当中。那假如说我们的进程此时正在等待某一个I/O设备的分配,而这个I/O设备又暂时没法分配给它的话,那么这个进程就会挂到那个I/O设备设备控制表所指向的这个等待队列的队尾。所以,这个队列指针其实指向的就是相应的阻塞队列。那这是系统中需要配置的第一种数据结构——设备控制表(DCT)。
第二种数据结构叫做控制器控制表,英文缩写是COCT。每一个设备控制器都会对应一张设备控制表。系统会根据这个设备控制表当中的信息对设备控制器进行控制和管理。那这个表当中也会有一些很常用的字段。
比如说控制器的标识符,那这个就是各个控制器的唯一ID。
另外还需要记录设备控制器的状态,比如说忙碌还是空闲。
再者呢,每个设备控制器都会有一个它所从属的通道,所以在设备控制器的控制表当中,也会有一个指向通道表的指针。系统可以根据这个指针找到它所从属的通道。
那最后还需要有一个队首指针和队尾指针。这个队列其实就是指向了正在等待这个设备控制器的进程。那这是需要配置的第二个数据结构——控制器控制表。
第三个数据结构是通道控制表,英文缩写是CHCT。每个通道都会对应一张通道控制表。当操作系统会根据这个表的信息对通道进行相应的操作和管理。
那与之前的两个数据结构很类似,通道标识符指的是这个通道的唯一ID。
通道状态就是指明它是忙碌、空闲还是故障。
而第三个与通道连接的控制器表的首址。操作系统可以根据这个字段找到这个通道控制的所有的控制器相关的信息,也就是找到这些控制器控制器表(COCT)的集合。
那如果说通道此时暂时不能为一个进程服务,那么这个进程一样是需要等待这个通道的服务。所以最后的这两个字段就是用于指向等待这个通道的那些进程队列的。那这是通道控制表的一个作用。
那第四个需要设置的数据结构叫系统设备表,英文缩写是SDT。这个表当中记录了系统中全部设备的情况。
每一个设备会对应一个表目。那每个表目中又会记录这个表目所对应的设备的设备类型。比如说,它是打印机还是扫描仪还是键盘。又会记录这个设备的标识符,也就是它的物理设备名,唯一的ID。那当然这个表目中还包含着这个设备的控制表DCT。最后,还记录了这个设备的驱动程序的入口地址。再次强调,系统设备表当中记录的是系统中全部设备的情况。所以其实当用户用设备名请求使用某一个设备的时候,操作系统是可以从这个系统控制表当中,来找到用户指定的设备到底是哪一个的。
那接下来我们来分析一下设备分配的具体步骤。那用户在编程的时候需要提供物理设备名作为这个系统调用的参数。
那操作系统用某种方式来查找系统控制表的时候就会把各个表目当中记录的这个设备标识符和用户提供的物理设备名进行比对,然后找到这两个参数相匹配的一个表项,之后就可以找到这个设备对应的设备控制表DCT了。
于是接下来会根据DCT当中记录的信息,来判断此时这个设备是否空闲。如果设备空闲的话,就可以把这个设备分配给那个进程。而如果这个设备此时是忙碌的话,那么就需要把这个进程挂到这个设备对应的等待队列、阻塞队列当中。一直到这个设备空闲,并且把设备分配给该进程之后才会把这个进程重新唤醒。那除了分配这个设备之外,还需要把这个设备对应的控制器也分配给这个进程。所以系统会根据指向控制器表的指针这个字段找到这个设备对应的控制器控制表,也就是COCT。
那与刚才类似,首先会检查这个控制器此时是不是空闲。如果空闲的话,可以把这个控制器分配给这个进程。如果控制器忙碌,那就需要把这个进程放到控制器的阻塞队列当中。那分配了控制器之后,还需要给这个进程分配相应的通道。于是又会根据这个指针(控制器队列的队首指针和控制器队列的队尾指针),找到相应的通道控制表,也就是CHCT。
这个过程也和刚才类似,首先会判断通道的状态,如果通道空闲的话,那么可以把通道分配这个进程。如果通道忙碌的话,就需要把进程挂到通道的阻塞队列当中。
那只有设备、控制器和通道这三个东西都分配成功的时候,这次设备分配才算成功。之后就可以开始启动I/O设备进行数据传送了。那这是设备分配的一个步骤。
那我们来思考一下,这个分配过程有什么缺点呢?
还记不记得我们刚才说的,采用这种分配方式的话,用户在编程的时候其实是需要提供物理设备名作为系统调用的参数的,所以这种方式对用户编程来说是很不方便的。假如说用户使用的物理设备名叫A,而之后这个物理设备又进行了更换,把物理设备A换成了物理设备B。但是由于用户在编程的时候使用的依然是A这个物理设备名,因此只要物理设备更换了,那么用户之前所写的程序就无法运行了。它在请求使用物理设备A的时候肯定是没办法给它分配成功的。那除了这个缺点之外,当一个进程它请求的物理设备正在忙碌的时候,即使系统当中还有同类型的设备,但这个进程依然是必须阻塞等待的。比如说我们一台电脑上连了三台打印机,那如果说这个进程请求使用的是第一台打印机,那么虽然第二台和第三台打印机此时有可能是空闲的,但只要第一台打印机此时是忙碌的,那这个进程依然是需要阻塞等待。但显然其实可以把进程的这个打印任务把它分配给第二台或者第三台打印机进行。因此,采用这种方式的话也会导致设备的利用率不高的问题。
那解决这个问题的方法,其实咱们在之前谈到过,就是可以建立逻辑设备名与物理设备名之间的映射机制。用户编程的时候使用的是逻辑设备名,然后由操作系统来负责完成逻辑设备名到物理设备名的转换。
那引入这种机制之后,进程在请求使用某种设备的时候,只需要提供逻辑设备名。所谓的逻辑设备名,其实就是要指明它所使用的设备类型。
比如说它想使用的是打印机这种类型的设备。
那由于系统设备表(SDT)当中有一个字段它就是记录的设备的类型。因此操作系统可以根据用户提供的逻辑设备名也就是设备类型来依次查找这个系统设备表(SDT),然后找到一个指定类型的,并且空闲的设备把它分配给进程。那只有这个类型的设备全部都处于忙碌状态的时候,才需要把这个进程阻塞。
那把这个设备分配给进程之后,操作系统还需要在逻辑设备表(LUT)当中新增一个表项。那逻辑设备表(LUT)就是用于记录逻辑设备名到物理设备名的一个映射关系的一张表。除了这个映射关系之外,还需要记录这个设备对应的驱动程序的入口地址。
那之后的操作就和之前一样了。可以根据这个设备控制表(DCT)找到相应的控制器控制表(COCT),然后把控制器分配给设备。之后又再根据控制器控制表(COCT),找到通道控制表(CHCT),把通道分配给设备。那只有进程第一次通过逻辑设备名申请使用一个设备的时候,操作系统才会来查询这个系统设备表。
如果之后进程再次以相同的逻辑设备名来请求使用设备的话,那么操作系统首先做的是是会在这个逻辑设备表(LUT)当中查找这个逻辑设备对应的物理设备,然后找到相应的表项之后,就可以找到这个设备对应的驱动程序了。
那有这样的两种方式设置逻辑设备表。第一种就是整个系统中只有一张逻辑设备表。这样的话各个用户所使用的逻辑设备名是不允许重复的,所以这种方式只适合用于单用户操作系统。第二种的话就是每个用户设置一张逻辑设备表。那这样的话不同用户的逻辑设备名可以重复,它可以适用于多用户的操作系统。那这一点咱们在之前的小节当中也提到过。
好的那个这个小节我们介绍了设备的分配与回收问题。主要介绍的是设备的分配。其实设备的回收无非就是把那些数据结构再把它改回来就行了。那在设备分配的时候我们应该考虑这样的三种因素。
其中不太容易记忆的是安全分配方式和不安全分配方式。大家在复习到相应概念的时候,需要留个心眼,在选择题当中有可能进行考查。
另外静态分配和动态分配这两个术语要有印象。其实静态分配方式就是破坏了“请求和保持”这一个产生死锁的必要条件,而动态分配方式呢效率更高,但是有可能产生死锁。那一般来说我们可以利用银行家算法来避免死锁或者结合资源分配图来对死锁进行检测和解除。那银行家算法的规则还有死锁的检测和解除这也是大家需要回顾的知识点。
那我们之后还介绍了设备分配管理当中所需要用到的数据结构。那这些数据结构其实并不需要死记硬背。设备控制表、控制器控制表、通道控制表它们分别对应设备、控制器还有通道这三种东西,那其实我们只需要理解这三种东西的一个对应关系我们就可以知道这些数据结构它们大致是需要做什么事情的了。一个通道会控制多个控制器,一个控制器又会控制多个设备,所以它们之间的关系是一种树型的关系。
而无论是设备还是控制器还是通道它们都会有一个字段叫做等待队列的指针。其实就是当这种资源暂时不能分配给某个进程的时候,需要把这个进程的PCB插入到相应资源的等待队列当中。那系统设备表呢集中了所有的设备相关的信息,操作系统可以通过这个表来统一地对设备进行查询检索。
那基于这些数据结构,我们介绍了设备分配的具体步骤。我们需要知道的是,如果采用的是物理设备名来请求使用一个设备的话,它会存在一些很明显的缺点。并且这些缺点可以用逻辑设备名和物理设备名映射的这种方式来解决。那这个地方是大家需要着重理解的点,很有可能在选择题当中进行考查。
这门课的最后一个知识点——缓冲区管理。
那我们会首先介绍什么是缓冲区,缓冲区有什么作用,之后会介绍几种常见的、常考的缓冲区的解决方案。
像咱们之前学到过的联想寄存器,也就是快表,它就是一种由硬件来实现的缓冲区。那由于对快表的这个访问频率是很高的,所以如果快表的速度高的话,那么整个系统的性能提升会很明显。因此在这种情况下,使用这种成本高的解决方案是值得的。
但一般情况下更多的是用内存作为缓冲区。那像设备独立性软件这一层的缓冲区管理主要其实指的是对内存作为缓冲区的这种这种缓冲区进行组织和管理。
所以我们这一小节介绍的所有的这些缓冲区,主要还是围绕内存作为缓冲区这种类型。
首先来看一下缓冲区有什么作用。
在内存中可以开辟一小片区域作为缓冲区,
那么如果要输出数据的话,那么CPU产生的这些数据首先会被放到内存的缓冲区当中。不过CPU的速度很快,所以它很快就可以把这个缓冲区给充满。那当缓冲区放满了之后,CPU就可以转头去做别的事情。
之后I/O设备可以慢慢地从这个缓冲区当中取走数据。
而在数据输入的时候其实也是类似的。I/O设备可以慢慢地把数据放到缓冲区当中,当缓冲区满了之后,CPU再很快速地从缓冲区中取走数据。所以采用这种方式的话,很明显可以缓和CPU和I/O设备之间的速度不匹配的矛盾。
那如果说没有采用缓冲区这种策略的话,I/O设备每输入或者每输出一定单位的数据之后,就需要对CPU发出一个中断信号请求CPU介入处理。那假设这个I/O设备是一种字符型的设备的话,那每输入完一个字符或者每输出一个字符I/O设备都会打断CPU向CPU发出中断信号。而之前咱们强调过,对中断的处理是需要付出一定的时间代价的,因此CPU频繁地处理这些中断显然会降低系统的性能。而如果采用采用缓冲区这种策略的话,只有缓冲区中的数据被全部取走了,或者输入的数据充满了缓冲区的时候,CPU才需要介入来处理中断。因此,采用这种方式可以减少CPU的中断频率,放宽CPU对中断响应时间的限制。
那缓冲区还有一个作用就是解决数据粒度不匹配的问题。比如说此时在CPU上运行的输出进程,每次可以生成一整块的数据。但是I/O进程每次只能输出一个字符,那如果没有采用缓冲区策略的话,输出进程只能一个字符一个字符地给这个I/O设备来传输数据。但如果采用的缓冲区这种策略的话,输出进程可以直接把一整块的数据放到缓冲区里,让I/O设备从这个缓冲区当中一个字符一个字符地往外取。那在输入的时候其实也是类似的,缓冲区同样可以解决这种数据粒度不匹配的问题。那另外呢,采用了缓冲区之后,很显然是可以提高CPU和I/O设备之间的并行性的。那接下来我们介绍几种我们需要掌握的缓冲区管理的策略。
首先来看一下什么是单缓冲。而这个缓冲区的大小和块是相等的。那我们首先来看一下对一个数据块的处理需要经过哪些步骤。首先系统在主存当中为这个用户进程分配了一块大小的缓冲区。
那么这个块设备会产生一块大小的数据,
把它输入到这个缓冲区当中。
这个过程我们假设所耗费的时间为T。
那之后呢这块数据需要传送到用户进程的工作区当中才可以被用户进程所处理的。还记得我们之前小节中提到的例子吗?C语言的scanf这个函数读入一个数字的时候,其实我们从键盘输入的这个数据最终是要被传送到用户进程的内存空间当中的。
所以在这个地方,数据块,被传入了缓冲区还不够,它还需要把这个数据块把它传送到用户进程的工作区当中。那同样的,在考研当中,我们默认用户进程的工作区也是刚好可以放得下这样一块数据。
所以接下来会用M这么长的时间,把缓冲区当中的数据传送到用户进程的工作区中。
那之后用户进程就可以开始对这一块数据进行处理。那我们假设对一块数据进行计算处理所需要的时间为C这么多。那当它处理完之后,这个用户进程的工作区就可以被腾空了。
那我们在考研当中经常考查的题型是,会让我们计算每处理一块数据平均需要多长的时间。
那咱们的书上给出了一个很好用的技巧。
那来看第一种情况。假设输入时间T>处理时间C。那根据我们假设的条件,刚开始工作区是满的,缓冲区是空的。所以刚开始,CPU就可以处理工作区中的这块数据了。
那这个处理的过程,需要花C这么长的时间。
另外呢,由于刚开始缓冲区就是空的,而我们刚才说过,只要缓冲区空的话,那其实是可以往缓冲区当中充入数据的。
所以刚开始块设备就往缓冲区当中充入一块数据,那这个时间总共是耗费了T这么久。那把缓冲区充满总共需要花费T这么长的时间。
那由于这个数据充入的时间大于处理数据的时间,因此CPU在花了C这么长的时间处理完数据之后,它并不可以紧接着继续处理下一块的数据。因为当它处理完这个数据之后,其实缓冲区当中的数据是还暂时没有充满的,因此必须先等到缓冲区的数据充满并且把这些数据传送到工作区之后才可以进行下一块数据的处理。那显然在T这个时间点,缓冲区充满了。
接下来可以紧接着把这一块数据传送到工作区当中,这个过程又需要花费M这么长的时间。还记得我们刚才假设的初始状态吗?工作区满,缓冲区空。
其实在经历T+M这么长的时间之后,就再一次回到了我们刚刚假设的这种初始状态,也就是工作区满,缓冲区空。
那接下来的数据处理其实无非也就是重复刚才我们分析的这个过程。
因此,通过刚才的分析我们发现,平均每处理一块数据需要花的时间是T+M这么多。
那再来看第二种情况,假设输入一块数据的时间要小于处理一块数据的时间的话,
那么由于刚开始缓冲区就是空的,
所以从0这个时刻块设备就可以开始往缓冲区当中充入数据,那这个过程需要花费T这么长的时间,可以把缓冲区充满。
同时呢,由于刚开始工作区是满的,所以CPU可以在0这个时刻就开始处理这块数据。但这一块数据处理的时间需要C这么长,而C是大于T的。因此虽然缓冲区当中的数据充满了,
但是由于工作区中的这些数据暂时还没有处理完,所以在T这个时刻,并不可以直接把缓冲区中的数据把它继续传送到工作区当中。
必须等到工作区中的这块数据处理完了,也就是到C这个时刻,才可以开始把这块数据传送到工作区当中。
所以接下来这个传送的过程需要耗费M这么长的时间。那到这一步,又回到了我们刚才假设的这种初始状态,工作区是满的,缓冲区是空的。
所以这个过程总共耗费了C+M这么长的时间。那经过M这么长的时间之后,缓冲区的数据就全部被取空了。
因此接下来块设备又可以开始往里边输入数据,并且CPU也可以开始处理工作区当中的这块数据。
那接下来的过程其实无非也就是在重复我们刚才所说的这个过程。
因此可以发现,如果T<C的话,那么每处理一块数据的平均用时应该是C+M这么多。
所以经过刚才的分析我们得到一个结论,如果采用的是单缓冲区的策略的话,那么每处理一块数据,平均需要耗时C和T当中更大的那个再加M这么多的时间,这是对单缓冲策略的一个分析过程。
那接下来我们再来看双缓冲策略。如果采用的是双缓冲策略的话,操作系统会在主存当中为进程分配两个缓冲区。那同样的,每个缓冲区的大小也是一个块。
类似的,双缓冲问题也经常会考查每处理一块数据平均需要耗时多久这样一个问题。那我们和刚才的思路是类似的,我们可以假设一个初始状态,然后分析一下下次到达这个初始状态总共需要耗时多少。我们假设刚开始工作区是空的,其中一个缓冲区是满的,而另一个缓冲区是空的。
那先来看第一种情况。假设T>C+M,那根据我们假设的初始状态,缓冲区1当中此时是有1块数据的,缓冲区2是空的,工作区也是空的。
所以在0这个时刻,可以把缓冲区1当中的数据传送到工作区当中。那这个过程耗时应该是M这么多。
而接下来CPU就可以开始处理工作区当中的这个数据,耗时是C这么多。
另一方面,在刚开始缓冲区2就是空的,
所以刚开始块设备就可以往缓冲区2当中输入数据。
那充满这个数据总共需要耗时T这么多。那由于T>C+M,所以虽然在这个时间点CPU已经把工作区当中的这块数据处理完了,工作区已经空了,但是由于缓冲区2此时这个时刻还没有充满,所以暂时不能把缓冲区2当中的数据把它传送到工作区当中,必须等到T这个时间点,缓冲区2才可以被充满。所以到T这个时刻,其实就回到了我们假设的那种工作状态。工作区此时是空的,而这两个缓冲区当中,其中一个是空的,另一个是满的。
因此当T>C+M的时候,处理一块数据平均用时应该是T这么多。
而接下来无非就是再对刚才咱们分析的这一系列过程再进行重复。那整体来看,每处理完一个数据块,耗时都是T这么多。
那接下来再来看第二种情况,T<C+M。
刚开始缓冲区2是空的,
所以刚开始块设备可以往缓冲区2当中充入数据,耗时是T秒这么多。
另一方面,由于刚开始缓冲区1当中是充满数据的,
所以刚开始就可以把缓冲区1当中的数据传送到工作区当中,到M这个时刻工作区会被充满,然后CPU就可以开始处理这些数据,处理数据总共耗时C这么长。那处理完工作区当中的这块数据之后,此时缓冲区2当中其实也已经充满了下一块的数据了。
因此接下来可以紧接着把缓冲区2当中的数据传送到工作区当中,接着继续处理工作区中的这块数据。
那我们再回头来看数据输入的这个过程,在T这个时刻缓冲区2已经被充满,然后设备开始空闲,并且由于缓冲区1当中的数据在M这个时刻就已经被取空了,因此缓冲区2的数据被充满了之后,设备就可以紧接着往缓冲区1当中充入数据,这个耗时也是T这么多。
那我们假设在这个时间点,缓冲区2当中的数据还没有完全被取走,所以在这个时间点这儿,虽然设备空闲了,但是由于缓冲区2还没有被取空,而缓冲区1又刚刚被充满的,因此在这个时间点,设备并不能紧接着往缓冲区2当中充入下一块数据。
那只有缓冲区2当中的数据被取空之后,这个设备才可以继续往缓冲区2当中写入下一块的数据。所以其实这样一步一步分析下来大家会发现,如果采用的是双缓冲结构并且T<C+M的话,那么我们很难找到一个和刚开始的这种初始状态一模一样的这种状态。比如说像T这个时刻,虽然其中缓冲区2是满的,但是由于此时工作区当中的数据还没有全部被处理完,因此工作区又不是空的,这和我们刚开始的初始状态是不一样的。而在M+C这个时刻,虽然我们的工作区是空的,但是缓冲区2当中的数据是满的,并且缓冲区1当中也充入了一部分数据。也就是说在M+C这个时刻,工作区是空的,然后一个缓冲区是满的,另一个缓冲区是半满的。所以如果T<C+M的话,那课本当中介绍的这种方法其实就不太好使了。所以自己用这种甘特图的方式多往下分析几次就会发现,其实每经过M+C这么长的时间,就会有一块数据被处理完毕。
因此当T<C+M的时候,每处理完一个数据块,平时耗时应该是C+M这么多。
所以经过刚才的这两种情况的分析我们发现,如果T>M+C的话,那么平均处理一个数据块需要T这么长的时间。而如果T<M+C的话,那么平均处理一个数据块需要的是M+C这么长的时间。
那像单缓冲和双缓冲策略,其实不仅仅是在主机和设备之间的这种数据传送当中可以使用。
其实在两台主机通信时,也可以采用这种缓冲的策略,而用于数据的发送和接收。
那如果说为两台通信的这个主机配置单缓冲区的话,
那A主机想要发送数据,首先需要把这些数据放入到自己的这个缓冲区当中,
等缓冲区充满之后就可以往B主机的缓冲区当中一个一个地发送数据。
那之后B主机可以开始处理这些数据。
等这些数据处理完之后,这个缓冲区才会变空。只有等这些数据全部被取走,全部处理完之后,那B主机的缓冲区才可以被腾空。而这个缓冲区中的数据被全部取走之后,B主机要向A主机发送的数据才可以开始放入到这个缓冲区当中。
所以如果两台通信的机器只设置单缓冲区的话,那么在任何一个时刻都只能实现数据的单向传输。那这其中的原因呢,其实就是咱们之前强调过的缓冲区的特性决定的。缓冲区只有充满之后才可以取走数据,而只有取空之后才可以往里边放入数据。
那为了实现同一时刻双向传输,
我们可以给两台机器配置双缓冲区,其中一个缓冲区用来暂存即将发送的那些数据,而另一个缓冲区用于接收输入的数据。
所以如果采用双缓冲的结构的话,那么这两台主机都可以同时往自己的发送缓冲区当中充入自己想要发送出去的那个数据,
接下来可以同时往对方的接收缓冲区当中充入数据。那这样的话就实现了同一时刻双向传输的功能。
那讲到这个地方,大家可以再回忆一下咱们在进程通信那个小节当中提到过的管道通信这种方式。有很多同学都有疑问说,为什么管道通信当中只有管道充满的时候才可以取出数,只有管道取空的时候才可以往管道里放入数据。其实管道通信当中的管道它就是一种缓冲区,而由缓冲区的特性就决定了管道通信就应该是那样的。所以在管道通信中我们也强调过,如果要实现双向传输的话,必须设置两个管道,也就是设置两个缓冲区。那这是缓冲区这一块的知识点和管道通信的一个联系。
接下来我们再来学习下一种常见的缓冲区,叫做循环缓冲区。很多时候只有两个缓冲区依然不能满足进程的实际需要,
所以操作系统可以给一个进程分配多个大小相等的缓冲区,让这些缓冲区连成一个循环的队列。那在这个图当中橙色表示的是已经被充满数据的缓冲区,而绿色表示的是此时为空的缓冲区。
那系统会保持两个指针用于缓冲区的管理。in指针需要指向下一个可以充入数据的空缓冲区,而out指针可以指向下一个可以取出数据的满的缓冲区。那当这个缓冲区的数据被取空之后,out这个指针可以开始指向下一个缓冲区了。类似的,如果in这个指针所指向的缓冲区被充满之后,那么in指针也需要指向下一个空的缓冲区。那这是循环缓冲区的方式。那一般来说只有单缓冲和双缓冲需要我们分析处理一块数据平均所需要消耗的时间。循环缓冲区我们只需要了解一个它的大致原理就可以了。
接下来我们再来看一个不太容易理解的缓冲池这种方案。缓冲池其实是由一系列的缓冲区组成的,就像它这个名字一样。缓冲池其实就是放满了各种各样缓冲区的一个池子,当我们需要使用一个缓冲区或者要归还一个缓冲区的时候,就可以对这个池子当中的各种缓冲区进行操作就可以了。
所以缓冲池当中的缓冲区可以按照使用状况分为空缓冲队列,还有装满了输入数据的输入队列,还有装满了输出数据的输出队列这样三个队列组成。
另外呢根据一个缓冲区在实际运算当中所扮演的功能不同,又设置了四种工作缓冲区。
一个是用于收容输入数据的工作缓冲区,还有用于提取输入数据的工作缓冲区,还有用于收容输出数据的工作缓冲区,和用于提取输出数据的工作缓冲区。这些名词都很奇怪并且很拗口,不过其实它们的原理并不复杂。
如果一个输入进程要请求输入一块数据的话,
那么系统会从空缓存队列的队头当中取下这一块空的缓冲区,
把它作为用于收容输入数据的缓冲区。
那当这块缓冲区被充满之后,
就会被挂到输入队列的队尾上。
那如果计算进程想要取得一块之前已经输入了的数据的话,
那么操作系统会从输入队列的队头取下一个缓冲区,把它作为提取输入的工作缓冲区。
那接下来这块缓冲区当中的数据会被传送到计算进程的工作区当中。所以这块缓冲区当中的数据就被取空了,那当它取空了之后,
这个空缓冲区又会被挂回到空缓冲队列的队尾。
第三种情况,如果此时计算进程已经准备好了数据,想要把这些数据充入到缓冲区的话,
那么系统会从空缓冲队列的队列的队头取下一个空闲的缓冲区,把这个缓冲区作为收容这个进程想要输出数据的工作缓冲区。
因此接下来这个缓冲区慢慢地会被充满,那由于这块缓冲区当中的数据接下来是要输出到I/O设备上的,
所以这块数据会被挂到输出数据的队尾。
第四种操作,如果某个输出进程请求输出一块数据的话,
那么系统会从输出队列的队头取下一块缓冲区,把它作为提取输出数据的工作缓冲区。
接下来这块缓冲区当中的输出数据会被慢慢取走,
当缓冲区被取空之后,就可以把这个缓冲区挂回到空缓冲区队列的队尾了。所以这是缓冲池相关的几种操作和原理。
介绍了缓冲区管理相关的知识点。我们需要理解缓冲区的各种优点,需要对这些优点有一些大体的印象,可以应付选择题。另外呢,大家需要重点重点注意的是,单缓冲和双缓冲结构当中,每处理一块数据平均需要耗时多少。那这种题型是经常在选择题当中出现的。那最后我们还介绍了循环缓冲和缓冲池,对于这两种缓冲管理方案来说,我们并不需要去研究太深的那些细节,我们只需要对它们的原理有一个大体的印象,理解就可以了。
【知识强化】第五章 输入/输出(I/O)管理 5.2 I/O核心子系统I的更多相关文章
- 【知识强化】第五章 输入/输出(I/O)管理 5.1 I/O管理概述
这门课的最后一个章节——设备管理. 操作系统它作为系统资源的管理者,既需要对上层的软件进行管理,也需要对下层的硬件进行管理.操作系统它需要对处理机还有存储器这些硬件进行管理,但是这些硬件其实是在计算机 ...
- 【知识强化】第七章 输入/输出系统 7.1 I/O系统基本概念
那么下面,我们将要进入计算机组成原理的最后一章,也就是我们的第七章,输入输出系统的学习.那么这一部分内容呢,我们之前呢一直在提,但是并没有详细地讲解,那么进入到我们第七章输入输出系统这一部分,我们就要 ...
- 第15章-输入/输出 --- 理解Java的IO流
(一)理解Java的IO流 JAVA的IO流是实现输入/输出的基础,它可以方便地实现数据的输入/输出操作,在Java中把不同的输入/输出(键盘.文件.网络连接等)抽象表述为"流"( ...
- 【知识强化】第七章 输入/输出系统 7.3 I/O接口
下面我们进入第七章的第三节,I/O接口. I/O接口呢就是解决了外设和主机之间的一个连接的问题.那么我们这一节就要来看一下I/O接口它有哪些功能,以及它是怎么组成的,还有就是我们主机如何来定位到那样一 ...
- 【知识强化】第二章 数据的表示和运算 2.4 算术逻辑单元ALU
从本节开始我们就进入到本章的最后一节内容了,也就是我们算术逻辑单元的它的实现.这部分呢是数字电路的一些知识,所以呢,如果你没有学过数字电路的话,也不要慌张,我会从基础开始给大家补起.那么在计算机当中, ...
- 第15章-输入/输出 --- File类
(一) Java的IO通过java.io包下的类和接口来支持,在java.io包下主要包括输入.输出两种IO流. 每种输入.输出流又分为字节流和字符流两大类: (1)字节流以字节为单位来处理输入.输出 ...
- 第四章输入/输出(I/O)4.1I/O涉及的设备及相关概念简介
PCL中所有的处理都是基于点云展开的,利用不同的设备获取点云.存储点云等都是点云处理前后必须做的流程,PCL中有自己设计的内部PCD文件格式,为此,设计读写该该格式以及与其他3D文件格式之间进行转化的 ...
- Android初级教程理论知识(第五章页面跳转和数据传递)
总体概述: Android四大组件 Activity BroadCastReceiver Service ContentProvider 创建第二个activity 新创建的activity,必须在清 ...
- 第四章输入/输出(I/O)4.2PCL中I/O模块及类介绍
PCL中I/O库提供了点云文件输入输出相关的操作类,并封装了OpenNI兼容的设备源数据获取接口,可直接从众多感知设备获取点云图像等数据.I/O模块利用21个类和28个函数实现了对点云的获取.读入.存 ...
随机推荐
- Redis 设置权限密码,以及如何开启关闭设置
linux redis 设置密码: 在服务器上,这里以linux服务器为例,为redis配置密码. 1.第一种方式 (当前这种linux配置redis密码的方法是一种临时的,如果redis重启之后 ...
- SourceTree 这是一个无效源路径/URL的 解决方法
看网上的教程都解决不了,这是一个大坑,折腾了很久. 如果说你的项目存在,而不是url真的无效,那就是因为你的权限问题. 因为你的sourcetree登过其他账号,在sourceTree设置里面记录了他 ...
- RAD介绍及实战,LVM介绍及实战,磁盘常见故障
目录 一.RAID 1.RAID好处: 2.RAID的运行方式: 3.RAID的级别: 二.RAID实战 软RAID 1.RAID0 2.RAID1 3.RAID5 4.RAID10 三.LVM介绍 ...
- mysql基于Altas读写分离并实现高可用
实验环境准备: master:192.168.200.111 slave1:192.168.200.112 slave2:192.168.200.113 Altas:192.168.200.114 c ...
- go语言从例子开始之Example16.函数递归
Go 支持 递归.这里是一个经典的阶乘示例. Example: package main import "fmt" func fact(n int) int{ //先设置退出条件 ...
- Java高级应用(一)
下面来介绍一下Java的高级应用有哪些. Java高级应用 第一讲 类加载 (一).类加载 类加载器是一个特殊的类,负责在运行时寻找和加载类文件.Java允许使用不同的类加载器,甚至是自定义类加载器. ...
- oracle11g rename user导致物化视图失效的处理
在上一篇文章中,已经点到了数据库改名时,引起该schema下物化视图会失效的问题.从表面上看,该物化视图是删也删不掉,那当然就无法重建了.以下是实验过程: Oracle Database 11g En ...
- 【leetcode】947. Most Stones Removed with Same Row or Column
题目如下: On a 2D plane, we place stones at some integer coordinate points. Each coordinate point may h ...
- PHP disk_free_space() 函数
定义和用法 disk_free_space() 函数返回指定目录的可用空间,以字节为单位. 语法 disk_free_space(directory) 参数 描述 directory 必需.规定要检查 ...
- apue 第6章 系统数据文件和信息
在给出用户登录名或数值用户ID后,这两个函数就能查看相关项. #include <sys/types.h> #include <pwd.h> struct passwd *ge ...