2019-08-29 14:25:51 qq_36321889 阅读数 42

文章目录

第三章 数据缓冲区高速缓存

对文件系统的一切存取操作,核心都能通过每次直接从磁盘上读或往磁盘上写来实现。但是慢的磁盘传输速率会使系统响应时间加长、吞吐率降低;因此,核心通过保持一个称为数据缓冲区高速缓冲的内部缓冲区池来试图减小对磁盘的存取频率,高速缓冲含有最近被使用过的磁盘块的数据。

高层核心算法让高速缓冲模块把数据预先缓存起来,或延迟写数据以扩大高速缓冲的效果!

缓冲首部—buffer_head

好吧,Linux中的buffer.c就是完全盗版的这本书里的数据结构和算法···,没话说,不过Linux还是有非常多的过人之处的;
[外链图片转存失败(img-k4jBcOFg-1567059782827)(en-resource://database/3190:1)]
Linus将上图中的状态分成了三个分量:lock, uptodate, dirt

缓冲池的结构

核心遵循的算法的是:最近最少使用算法,即在空闲表中选取缓冲区时要选择最近最少使用的缓冲区; 即空闲表会维护一个最近最少使用的次序,空闲表的开头一般是最近最少使用的缓冲块;

当核心把一个缓冲区还给缓冲区池时,它通常把该缓冲区附到空闲表的尾部;

另外一点就是散列表和散列函数:
[外链图片转存失败(img-hoAHLmB5-1567059782827)(en-resource://database/3192:1)]
散列表中对应的队列也是双向链表

散列队列中各个缓冲区的位置是不重要的,没有两个缓冲区可以同时包含一个磁盘块上的内容,因此,缓冲区池中的每个磁盘块存在且仅存在于一个散列队列中,并且仅在那个散列队列中呆一阵;然而,如果一个缓冲区为空闲状态,则它也可以在空闲表中;因为一个缓冲区既可在一个散列队列中又可在一个空闲队列中,所以核心有两个办法可以找到它;如果要找一个特定缓冲区,则从散列表中找,如果要找空闲缓冲区,则遍历空闲表;

一个缓冲区总是在某个散列队列上,但是它可以在或不在空闲表中;??? 那么初始状态呢?

说起来,如果不读本书,那么Linux代码可能很难理解透彻;

缓冲区的检索-getblk

getblk:将一个缓冲区分配给磁盘块,下面讨论五种情况:

1 该块在散列队列中,并且它的缓冲区是空闲的

 将该缓冲区标记为忙,并将该缓冲块从空闲链表中摘下来;
 用完之后会调用brelse,其会将该块放置到空闲表尾部,但是并没有把散列表中的指针破坏,也就是说如果再次调用了相同的getblk仍然可以找到该缓冲区,但是如果其他缓冲区不够用了,也可以将刚才释放的缓冲区拿去用;

2 散列队列中找不到,从空闲表中分配一个

 注意从空闲表中找到的这个缓冲区也可能会在某个散列队列中,因为其曾经分配给另一个磁盘块;
 这时候会将其链接到对应的散列队列中,也会改变其在空闲表中的结构,将其从空闲表中摘下来;

3 散列队列中找不到,从空闲表中分配了一个缓冲区

 在空闲表中找到一个已经标上了延迟写标记的缓冲区,核心必须先将该缓冲区内容写到磁盘上,所以将它们从空闲表中摘下来,然后启动异步写操作,并从空闲表中寻找另一个缓冲区。
 当之前的延迟写完成后,核心将缓冲区释放,并把它放到空闲表头部;
 为什么会放到头部?
因为:延迟写的精髓在于追求最长时间的延迟写,这样才能尽可能多的命中缓存,减少IO次数,那么就明白了为什么要放置到头部了叭;
 再次思考为什么要将其从空闲表中摘下来再执行异步写操作呢?
因为:这些操作符合一致性的原则,即使用前都将其从空闲表中摘下来,不然会造成歧义;

4 散列队列中找不到,并且空表缓冲区表已空

 这时候进程A应该睡眠直至另一个进程执行算法berlse,当重新调用到进程A时,必须为该块重新搜索散列队列,不能立即从空闲表中分配缓冲区,因为互斥呗!!!

5 散列队列中找到,但它的缓冲区当前为忙

 这个有点复杂,在等待结束后仍需要判断之前等待的缓冲区还是不是符合条件的;

读磁盘块与写磁盘块

读块

的确很简单了,直接上图叭
[外链图片转存失败(img-5VvjNmKG-1567059782828)(en-resource://database/3194:1)]
[外链图片转存失败(img-vElbXsOI-1567059782829)(en-resource://database/3196:1)]
需要解释下breada,即buffer_read_ahead,即预先读,比如一个文件很大, 我目前只需要第一个块的数据,但是一会用第二个块的数据的可能性非常大,所以···需要预读了!!!

上面程序描述的非常清楚,不过稍微有点绕口,需要好好理解才能明白!!!

写块

[外链图片转存失败(img-QminFHuc-1567059782831)(en-resource://database/3198:1)]

延迟写的作用

  1. 比如我修改了某一个文件,刚好对应到磁盘中的某个块,其实我是修改的缓冲区里的内容,刚好这个缓冲区被标记为延迟写,也就是说我的修改目前没有同步到磁盘上;
  2. OK,我过了一会又读取了该文件并进行修改了,想想,如果本次读取的时候命中了缓冲,那么我的两次修改都对缓冲区进行,然后同步到磁盘中;
  3. 试想,如果没有延迟写,是不是需要写两次磁盘,有了延迟写只写了一次磁盘;
  4. 原来延迟写是有概率减少一次写磁盘的,真的比较神奇呢!

中断处理程序调用brelse的情形:提前读与延迟写磁盘这两个异步IO操作!提前读完和延迟写完都需要调用brelse函数;

brelse函数在Linux0.11中仅仅是唤醒因等待该缓冲区的进程;

在Unix中可能还有其他操作,比如:如果缓冲区无效将其放入到空闲表的头部,为什么缓冲区无效?可能发生IO错误!

看完这里buffer.c就可以完全理解了,毫无技术难点,而且Linus就是参考的这本书哈哈哈哈!

高速缓冲的优点和缺点

  1. 提供了统一的接口,即全部都和缓冲交互,使得程序更加模块化,系统设计也比较简单,接口比较单一,也体现了抽象的思维;
  2. 消除了字节对齐问题,没看明白是怎么消除的,?
  3. 最重要的一点是高速缓冲可以减少访问磁盘次数,从而提高整个系统的吞吐量,减少响应时间;高速缓冲的总大小和命中率息息相关,但是受到了总内存大小的限制,如果缓冲区占用了过多的内存,那么系统会由于过量的进程对换或调页而慢下来;
  4. 有助于确保文件系统的完整性,因为高速缓冲是唯一的、公共的;
  5. 缺点:发出写磁盘的请求但是从来不知道何时这些数据才能写到磁盘上,所以···如果断电···那么数据就丢失了鸭;
  6. 写时数据拷贝到核心,再由核心写到磁盘。读也是类似,中间的传输带来了性能的损耗!
  7. 总体来说,缓冲的思想是好的,给效率带来很大的提升;

习题

最好的散列函数是使块均匀分布在散列队列集合上的散列函数,什么是最理想的散列函数?在计算中应该使用逻辑设备号嘛?

dev^block 这个hash函数很普通啊,Linux0.11中就是使用的该hash函数,为什么还得加个dev呢,搞不明白;

应该使用逻辑设备号,这样编码的时候会比较方便;而且能体现抽象的思想;

getblk中,摘下一个缓冲区时必须提高处理机优先级,以便在检查空闲表之前封锁中断,为什么?

如果不封锁中断的话,检查过程中被其他程序抢占,如果其他程序修改了空闲表,那么恢复本程序执行时空闲表和之前的状态已经不同步了,所以需要封锁中断;

说到底还是那个核心的问题:数据同步;

getblk中,检查一个块是否处于忙状态之前,必须提高处理机优先级封锁中断,为什么?

同上

在算法brelse中,如果缓冲区内容无效,则核心把该缓冲区放入到空闲表队列头部,如果缓冲区内容无效,该缓冲区应该出现在散列队列中嘛?

该出现,下面对比两种情况:

  1. 如果将其移除出散列队列:首先涉及到至少两次的链表操作,会浪费时间,并且下次寻找的时候,可能需要从空闲队列中寻找缓冲区
  2. 如果其可以出现在散列队列:减少了两次链表操作的同时,下次寻找的时候不用遍历空闲表了,算是一个优点;而且不会影响空闲表剩余空间;
假设核心进行一个块的延迟写,当另一个进程从它的散列队列中取出那个块时会发生什么?从空闲表呢?
  1. 从散列队列中取:意味着要对该缓冲块进行读或者写,如果是读的话,那么给它即可;如果是写的话,让它直接覆盖缓冲区的内容即可,这样还能减少一次IO操作;
  2. 从空闲表:意味着该缓冲区与对应的设备号、块号不匹配,需要先将缓冲区内容异步写入磁盘,然后再找一个新的缓冲区,而且等其写完之后,将其放入到空闲表的头部;
  3. 总之都减少了IO次数;
重新设计getblk以保证一个进程最终能用上一个缓冲区

设计一个计数器?当等待次数到达阈值时便重新在空闲表中查找缓冲区?

或者比较合理的是引入优先级的概念,不过会提高算法复杂度,即等待次数越多优先级越高,高过某个级别时不再等待,去空闲表中寻找其他的缓冲区;

umount与sync等几个不同的系统调用,请求将核心把对一个特定的文件系统“延迟写”的所有缓冲区都清理到磁盘上;描述一个实现缓冲区清理的算法;作为清理的结果,对于空闲表上的缓冲区次序发生了什么?核心怎么样才能保证当清理进程因等候一个IO完成而睡眠时,没有别的进程偷偷溜进来用延迟写的方法把一个缓冲区写到这个文件系统中?

先来描述实现缓冲区清理的算法:

  1. 可以分为两类叭,一是连接在散列表中的,二是连接在空闲表中的;分别遍历它们?等下,回想下之前的过程,貌似延迟写的缓冲区都在空闲表中的鸭,好像的确是这样!
  2. 遍历到一个的时候,开启异步写操作后该进程进入睡眠,那么其他进程可能写了之前的缓冲区,所以需要从头开始扫描;
  3. 这样每次被唤醒后都从头开始扫描,知道全部扫描为止;
  4. 好像并不太优雅,不过因为多进程的原因,好像也没有其他办法;多几次遍历操作貌似也并不会太影响效率;

对于空闲表上的缓冲区次序发生了什么?

  1. 一般来讲,每次我们将延迟写的一个缓冲区写会磁盘后将其放置到空闲表的头部;
  2. 而且如果我们从空闲表的头开始遍历的话,那就会将空闲表中延迟写缓冲区的次序颠倒过来;
  3. 同理,如果我们从空闲表的尾部开始遍历的话,那空闲表中的延迟写缓冲区的次序不会发生变化;

如何保证睡眠期间没有别的进程偷偷溜进来用延迟写的方法把一个缓冲区写到这个文件系统中?

  1. 我怎么感觉没法保证啊~!@#
  2. 不如这样,该进程每次从睡眠状态苏醒后,都重新从开头开始遍历空闲表,如果有被锁住的缓冲区就等其解锁,如果等解锁的话,那么这个系统调用的响应时间可能会边长,如果不等,那就没法判断该算法是否完成;
  3. emmmm,到底等还是不等?即使是等也无法保证刚好能将所有延迟写同步到磁盘吧,所以干脆不用等,直接跳过OK!
把系统响应时间定义为它完成一个系统调用所占用的平均时间;把系统吞吐量定义为在一个给定时间段内系统所能执行的进程数目;描述高速缓冲能够怎么样有助于缩短响应时间;它必定有助于提高系统吞吐量嘛?

怎么样有助于缩短响应时间?

  1. 这个问题···感觉很难讲啊,如果缓冲命中,那么大大节省了IO的时间,可能节省很多倍;
  2. 所以需要统计的数学量有:64个系统调用发生的概率,以及有多少个需要IO操作,以及IO操作所需要的时间,以及缓冲命中所需要的时间,然后计算下就OK了;
  3. 所以,如果有用户数据,那么这些东西都可以有一个合理的数据来解释;
  4. 说到底,还是数据比较贴切;
  5. 而且随着各种硬件的发展,这些数据统计也需要不断更新,以适应新的需求;

必定有助于提高系统吞吐量?

  1. 这个不一定;
  2. 假定内存比较小,那么缓冲区比较小,那么命中率很低,那么很多系统调用就无端的执行了和缓冲有关的代码,而且还没命中,这些额外的和缓冲有关的代码造成了额外的开销;
2019-06-04 09:35:16 epubit17 阅读数 614

当前,介绍UNIX系统的书籍很多,然而论述UNIX系统内部结构的专著却屈指可数。《UNIX操作系统设计》是其中非常引人注目的一本。本书作者Maurice J.Bach多年来在AT&T公司的贝尔实验室工作,对UNIX系统的设计思想有深刻了解,又有讲授UNIX系统的丰富经验。作者在回顾UNIX操作系统的发展演变的基础上,描述了UNIX系统Ⅴ内核内部的数据结构和算法,并对其做了深入浅出的分析。在每章之后,本书还给出了大量富有启发性和实际意义的题目。因而,本书不仅可用作大学本科高年级和研究生操作系统课程的教科书和参考书,也为从事UNIX操作系统的研究人员或UNIX实用程序开发人员提供了极有价值的参考资料。

必读经典书名:《UNIX操作系统设计》

在这里插入图片描述

主要目录结构:

第1章 系统概貌
第2章 内核导言
第3章 数据缓冲区高速缓冲
第4章 文件的内部表示
第5章 文件系统的系统调用
第6章 进程结构
第7章 进程控制
第8章 进程调度和时间
第9章 存储管理策略
第10章 输入/输出子系统
第11章 进程间通信
第12章 多处理机系统
第13章 分布式UNIX系统

样章试读:内核导言

上一章给出了对UNIX系统环境的高层次的看法。本章重点放在内核上,对内核的体系结构提出一个总的看法,勾画出它的基本概念和结构,而这些对于了解本书的其余部分是必不可少的。

2.1 UNIX操作系统的体系结构

Christian曾提出,UNIX系统支持文件系统有“空间”而进程有“生命”的假象(见[Christian 83)第239页)。文件和进程这两类实体是UNIX系统模型中的两个中心概念。图2-1给出了内核框图,显示出了各种模块及它们之间的相互关系,尤其是,它显示出了内核的两个主要成分:左边的文件子系统和右边的进程控制子系统。虽然在实际上,由于某些模块同其他模块的内部操作进行交互而使内核偏离该模型,但该图仍可作为观察内核的一个有用的逻辑视图。

图2-1让我们看到了三个层次:用户级、内核级及硬件级。系统调用与库接口体现了图1-1中描绘的用户程序与内核间的边界。系统调用看起来像C程序中普通的函数调用,而库把这些函数调用映射成进入操作系统所需要的原语。这在第6章中有更详细的叙述。然而,汇编语言程序可以不经过系统调用库而直接引用系统调用。程序常常使用像标准I/O库这样的一些其他库程序以提供对系统调用的更高级的使用。在编译期间把这些库连接到程序上,因此,就这里所讨论的目的来说,这些库是用户程序的一部分。下面的一个例子将阐明这一点。

图2-1把系统调用的集合分成与文件子系统交互的部分以及与进程控制子系统交互的部分。文件子系统管理文件,其中包括分配文件空间、管理空闲空间、控制对文件的存取以及为用户检索数据。进程通过一个特定的系统调用集合,比如,通过系统调用open(为了读或写而打开一个文件)、close、read、write、stat(查询一个文件属性)、chown(改变文件所有者)及chmod(改变文件存取许可权)等与文件子系统交互。这些及另外一些有关的系统调用将在第5章介绍。

文件子系统使用一个缓冲机制存取文件数据,缓冲机制调节在内核与二级存储设备之间的数据流。缓冲机制同块I/O设备驱动程序交互,以便启动往内核去的数据传送及从内核来的数据传送。设备驱动程序是用来控制外围设备操作的内核模块。块I/O设备是随机存取存储设备,或者说,它们的设备驱动程序使得它们对于系统的其他部分来说好像是随机存取存储设备。例如,一个磁带驱动程序可以允许内核把一个磁带装置作为一个随机存取存储设备看待。文件子系统还可以在没有缓冲机制干预的情况下直接与“原始”I/O设备驱动程序交互。原始设备,有时被称为字符设备,包括所有不是块设备的设备。

..\18-1394改4.11\0201.tif{65%}

图2-1 系统内核框图

进程控制子系统负责进程同步、进程间通信、存储管理及进程调度。当要执行一个文件而把该文件装入存储器中时,文件子系统与进程控制子系统交互——进程子系统在执行可执行文件之前,把它们读到主存中。这些我们将在第7章看到。

用于控制进程的系统调用有fork(创建一个新进程)、exec(把一个程序的映象覆盖到正在运行的进程上)、exit(结束一个进程的执行)、wait(使进程的执行与先前创建的一个进程的exit相同步)、brk(控制分配给一个进程的存储空间的大小)及signal(控制进程对特别事件的响应)。第7章将介绍这些及其他系统调用。

存储管理模块控制存储分配。在任何时刻,只要系统没有足够的物理存储供所有进程使用,内核就在主存与二级存储之间对进程进行迁移,以便所有进程都得到公平的执行机会。第9章将描述存储管理的两个策略:对换与请求调页。对换进程有时被称为调度程序,因为它为进程进行存储分配的调度,并且影响到CPU调度程序的操作。然而,本书仍将称它为对换程序,以避免与CPU调度程序混淆。

调度程序(scheduler)模块把CPU分配给进程。该模块调度各进程依次运行,直到它们因等待资源自愿放弃CPU,或它们最近一次的运行时间超过一个时间量,从而内核抢占它们。于是调度程序选择最高优先权的合格进程投入运行;当原来的进程成为最高优先权的合格进程时,还会再次投入运行。进程间通信有几种形式,从事件的异步软中断信号到进程间消息的同步传输,等等。

最后,硬件控制负责处理中断及与机器通信。像磁盘或终端这样的设备可以在一个进程正在执行时中断CPU。如果出现这种情况,在对中断服务完毕之后内核可以恢复被中断了的进程的执行:中断不是由特殊的进程服务的,而是由内核中的特殊函数服务的,这些特殊函数是在当前运行的进程的上下文中被调用的。

2.2 系统概念介绍

本节将概述一些主要的内核数据结构,并且更详细地描述图2-1中给出的各模块的功能。

2.2.1 文件子系统概貌

一个文件的内部表示由一个索引节点(inode)给出,索引节点描述了文件数据在磁盘上的布局,并且包含诸如文件所有者、存取许可权及存取时间等其他信息。“索引节点”这一术语是index node的缩写,并且普遍地用于UNIX系统的文献中。每个文件都有一个索引节点,但是它可以有几个名字,且这几个名字都映射到该索引节点上。每个名字都被称为一个联结(link)。当进程使用名字访问一个文件时,内核每次分析文件名中的一个分量,检查该进程是否有权搜索路径中的目录,并且最终检索到该文件所对应的索引节点。例如,如果一个进程调用

open( “/fs2/mjb/rje/sourcefile”,1);

则内核检查“/fs2/mjb/rje/sourcefile”所对应的索引节点。当一个进程建立一个新文件时,内核分配给它一个尚未使用的索引节点。正如我们很快就会看到的那样,索引节点被存储在文件系统中,但是当操控文件时,内核把它们读到内存(in-core)[1]索引节点表中。

内核还包含另外两个数据结构,文件表(file table)和用户文件描述符表(user file descriptor table)。文件表是一个全局核心结构,但用户文件描述符表对每个进程分配一个。当一个进程打开或建立一个文件时,内核在每个表中为相应于该文件的索引节点分配一个表项。这样一共有三种结构表——用户文件描述符表、文件表和索引结点表(inode table),用这三种结构表中的表项来维护文件的状态及用户对它的存取。文件表保存着文件中的字节偏移量——下一次读或写将从那里开始,并保存着对打开的进程所允许的存取权限。用户文件描述符表标识着一个进程的所有打开文件。图2-2表明了这三张表及它们之间的相互关系。对于系统调用open和系统调用creat,内核返回一个文件描述符(file descriptor),它是在用户文件描述符表中的索引值。当执行系统调用read和write时,内核使用文件描述符以存取用户文件描述符表,循着指向文件表及索引节点表表项的指针,从索引节点中找到文件中的数据。第4章和第5章将详细地描述这些结构,此刻,我们只要说使用这三张表可以实现对一个文件的不同程度的存取共享就够了。

..\18-1394 图\0202.tif{55%}

图2-2 文件描述符表、文件表和索引节点表

UNIX系统把正规文件(regular file)及目录保存在诸如磁带或磁盘这样的块设备上。由于磁带和磁盘在存取时间上的差别,所以没有什么UNIX系统装置使用磁带实现它们的文件系统。今后,无盘工作站将用得很普遍。在无盘工作站中,文件被存放在一个远程系统上,并通过网络进行存取(见第13章)。然而,为简单起见,下面假设讨论的是有磁盘的系统。一套系统装置可以有若干物理磁盘设备,每个物理磁盘设备包含一个或多个文件系统。把一个磁盘分成几个文件系统可以使管理人员易于管理存储在那儿的数据。内核在逻辑级上只涉及文件系统,而不涉及磁盘,把每个文件系统都当作由一个逻辑设备号标识的逻辑设备(logical device)。由磁盘驱动程序实现逻辑设备(文件系统)地址与物理设备(磁盘)地址之间的转换。除非另有明确的说明,否则,本书在使用“设备”这一术语时总是意味着一个逻辑设备。

一个文件系统由一个逻辑块(logical block)序列组成,每个块都包含512、1024、2048 个字节或512个字节的任意倍数,这要依赖于系统实现。在一个文件系统中,逻辑块大小是完全相同的,但是在一个系统配置中的不同文件系统间逻辑块大小可以是不同的。使用大的逻辑块增加了在磁盘与主存之间的有效数据传送率,因为内核在每次磁盘操作中能传送较多的数据,所以只执行很少几次费时的操作。比如,一次从磁盘读1KB的读操作,会比读两次每次读512B的操作要快。然而,正如将在第5章中看到的那样,如果一个逻辑块太大,将失去有效的存储能力。为简单起见,本书将使用“块”这一术语表示一个逻辑块,并且它将假设一个逻辑块包含1KB数据,除非另有明确说明。

一个文件系统具有如下结构(图2-3):

..\18-1394改10.9\0203.tif{65%}

图2-3 文件系统布局

  • 引导块(boot block)占据文件系统的开头,典型地,是一个扇区。它可以含有被读入机器中起引导或初启操作系统作用的引导代码。虽然为了引导系统只需一个引导块,但每个文件系统都有一个(可能是空的)引导块。
  • 超级块(super block)描述了文件系统的状态——如它有多大,它能存储多少文件,在文件系统的何处可找到空闲空间,以及其他信息。
  • 索引节点表(inode list)是一张装有索引节点的表,它在文件系统中跟在超级块后面。当配置一个文件系统时,管理人员应指明索引节点表的大小。内核通过索引来访问索引节点表中的索引节点。有一个索引节点是文件系统的根索引节点(root inode):在执行了系统调用mount(见5.14节)之后,该文件系统的目录结构就可以从这个根索引节点开始进行存取了。
  • 数据块(data block)在索引节点表结束后开始,并且包含文件数据与管理数据。一个已被分配的数据块,能且仅能属于文件系统中的一个文件。

2.2.2 进程

本节将更进一步介绍进程子系统:先描述一个进程的结构及用于存储管理的若干进程数据结构;然后给出进程状态图的初步看法,并考虑状态转换中的各种问题。

一个进程是一个程序的执行,它是由一系列有格式字节组成的,这些有格式字节被解释成机器指令[以下被称为“正文(text)”]、数据和栈区(stack)。当内核调度各个进程使之执行时,这些进程看起来像是同时执行的。而且,可以有几个进程是一个程序的实例。一个进程循着一个严格的指令序列执行,这个指令序列是自包含的,并且不会跳转到别的进程的指令序列上。它读或写自己的数据和栈区,但它不能读或写其他进程的数据和栈区。进程通过系统调用与其他进程及外界进行通信。

用实际的术语来说,UNIX系统上的进程是被系统调用fork所创建的实体。除了0进程以外,每个进程都是被另一个进程执行系统调用fork时创建的。调用系统调用fork的进程是父进程(parent process),而新创建的进程是子进程(child process)。每个进程都有一个父进程,但一个进程可以有多个子进程。内核用各进程的进程标识号(process ID)来标识每个进程,进程标识号简称为进程ID(或PID,见第6章)。0进程是一个特殊进程,它是在系统引导时被“手工”创建的;当它创建了一个子进程(1进程)之后,0进程就变成对换进程。正如在第7章所解释的那样,1进程被称为init进程,是系统中其他每个进程的祖先,并且享有同它们之间的特殊关系。

用户对一个程序的源代码进行编译以建立一个可执行文件,可执行文件由以下几部分组成:

  • 描述文件属性的一组“头标(header)”;
  • 程序正文;
  • 数据的机器语言表示,它给出程序开始执行时的初值;一个空间指示,它指出内核应该为被称为bss[2]的未初始化数据分配多大的空间(在运行时内核把bss的初值置为0);
  • 其他段,诸如符号表信息。

对于图1-3中的程序,可执行文件的正文是函数main与copy所生成的代码,其中,变量version是初始化数据(放在本程序中仅仅是为让它有初始化数据),数组buffer是未初始化的数据。系统Ⅴ的C编译程序版本在缺省时创建一个分离的正文段,但它支持一种选择,该选择允许数据段中包含程序指令,这是在系统的较老的版本中使用的。

在系统调用exec期间,内核把一个可执行文件装入主存中,被装入的进程至少由被称为正文区、数据区及栈区的三部分组成。正文区和数据区相应于可执行文件中的正文段和数据bss段。但是栈区是自动创建的,而且它的大小在运行时是被内核动态调节的。栈区由逻辑栈帧(stack frame)组成,当调用一个函数时栈帧被压入,当返回时栈帧被弹出。一个称为栈指针(stack pointer)的特殊寄存器指示出当前栈深度。一个栈帧包含着用于函数调用的参数、它的逻辑变量及为恢复先前的栈帧所需要的数据——其中包括函数调用时程序计数器的值及栈指针的值。程序代码包含管理栈增长的指令序列,并且当需要时内核为栈区分配空间。在图1-3所示的程序中,当main被调用时(按惯例,在每个程序中被调用一次),函数main中的参数argc和argv、变量fdold和fdnew就会在栈区上出现。并且,无论何时函数copy被调用,copy中的参数old与new及变量count都在栈区上出现。

因为UNIX系统中的一个进程能在两种态——核心态(kernel mode)或用户态(user mode)下执行,所以UNIX系统中核心栈(kernel stack)与用户栈(user stack)是分开的。用户栈含有在用户态下执行时函数调用的参数、局部变量及其他数据。图2-4中的左半部表明一个进程在copy程序中做系统调用write时进程的用户栈。进程启动过程(此过程是包含在库中的)用两个参数调用函数main,并将第1帧压入用户栈;第1帧含有main的两个局部变量的空间。然后main用两个参数old与new调用copy,并将第2帧压入用户栈中,第2帧包含局部变量count的空间。最后,进程通过调用库函数write来引用系统调用write,每个系统调用都在系统调用库中有一个入口点;系统调用库按汇编语言编码并包含一个特殊的trap指令,当该指令被执行时,它引起一个“中断”,从而导致硬件转换到核心态。一个进程调用一个特定的系统调用库的入口点,正像它调用任何函数一样。对于库函数也要创建一个栈帧。当进程执行特定的指令时,它将处理机执行态转换到核心态,执行内核代码,并使用核心栈。

核心栈中含有在核心态下执行的函数的栈帧。核心栈上的函数及数据项涉及的是内核中的而不是用户程序中的函数和数据,但它的构成与用户栈的构成相同。当一个进程在用户态下执行时,它的核心栈为空。图2-4的右半部给出了一个在copy程序中执行系统调用write的进程的核心栈的表项。在以后的章节中对系统调用write进行详细讨论时,再叙述各算法名称。

..\18-1394改12.16\0204.tif{70%}

图2-4 程序copy的用户栈及核心栈

每个进程在内核进程表(process table)中都有一个表项,并且每个进程都被分配一个u区[3],u区包含仅被内核操纵的私用数据。进程表包含(或指向)一个本进程区表(per process region table),本进程区表的表项指向区表(region table)的表项。一个区是进程地址空间中连续的区域,如正文区、数据区及栈区等。区表登记项描述区的属性,诸如它是否包含正文或数据,它是共享的还是私用的,以及区的“数据”位于主存的何处,等等。从本进程区表到区表的额外伺接级允许彼此独立的进程对区的共享。当一个进程调用系统调用exec时,在释放了进程一直在使用着的老区之后,内核为它的正文、数据和栈分配新区。当一个进程调用系统调用fork时,内核拷贝老进程的地址空间,在可能时允许进程对区共享,否则再建立一个物理拷贝。当一个进程调用系统调用exit时,内核释放进程使用过的区。图2-5展示了一个运行中的进程的有关数据结构:进程表指向本进程区表,本进程区表有指向该进程的正文区、数据区或栈区的区表表项的指针。

..\18-1394 图\0205.tif{60%}

图2-5 进程的数据结构

进程表表项及u区包含进程的控制信息和状态信息。u区是进程表表项的扩展,第6章将介绍这两个表的区别。在后几章中讨论的进程表中的字段如下。

  • 状态字段。
  • 标识符——表示拥有该进程的用户(用户ID或UID,见第6章)。
  • 当一个进程被挂起(在sleep状态)时的事件描述符集合。

u 区包含的是用来描述进程的信息,这些信息仅当进程正在执行时才是可存取的。重要的字段如下。

  • 指向当前正在执行的进程的进程表项的指针。
  • 当前系统调用的参数,返回值及错误码。
  • 所有的打开文件的文件描述符。
  • 内部I/O参数。
  • 当前目录(current directory)和当前根(current root)(见第5章)。
  • 进程及文件大小的限制。

内核能直接存取正在执行的进程的u区的字段,但不能存取其他进程的u区的字段。在其内部,内核引用结构变量u以存取当前正在运行的进程的u区,并且当另一进程执行时,内核重新安排它的虚地址空间,以使结构变量u引用的是新进程的u区。由于这一实现方式给出了从u区到它的进程表表项的指针,所以内核很容易识别出当前进程。

1.进程上下文

一个进程的上下文(context)包括被进程正文所定义的进程状态、进程的全局用户变量和数据结构的值、它使用的机器寄存器的值、存储在它的进程表项与u区中的值以及它的用户栈和核心栈的内容。操作系统的正文和它的全局数据结构被所有的进程所共享,因而不是进程上下文的一部分。

当执行一个进程时,系统被说成在该进程的上下文中执行。当内核决定它应该执行另一个进程时,它做一次上下文切换(context switch),以使系统在另一个进程的上下文中执行。正如将要看到的,内核仅允许在指定条件下进行上下文切换。当进行上下文切换时,内核保留足够信息,为的是以后它能切换回第一个进程,并恢复它的执行。类似地,当从用户态移到核心态时,内核保留足够信息以便它后来能返回到用户态,并从它的断点继续执行。在用户态与核心态之间的移动是态的改变,而不是上下文切换。再看一下图1-5,当它把上下文从进程A变成进程B时,内核做的是上下文切换;当发生从用户态到核心态或从内核态到用户态的改变时,所改变的是执行态,但仍在同一个进程(例如进程A)的上下文中执行。

内核在被中断了的进程的上下文中对中断服务,即使该中断可能不是由它引起的。被中断的进程可以是正在用户态下执行的,也可以是正在核心态下执行的。内核保留足够的信息以便它在后来能恢复被中断了的进程的执行,并在核心态下对中断进行服务。内核并不产生或调度一个特殊进程来处理中断。

2.进程状态

一个进程的生存周期能被划分为一组状态,每个状态都具有一定的用来描述该进程的特点。第6章将描述所有的进程状态,但现在了解如下状态是重要的:

(1)进程正在用户态下执行。

(2)进程正在核心态下执行。

(3)进程未正在执行,但是它已准备好运行——一旦调度程序选中了它,它就可以投入运行。很多进程可以处于这一状态,而调度算法决定哪个进程将成为下一个执行的进程。

(4)进程正在睡眠。当进程再也不能继续执行下去的时候,如正在等候I/O完成时,进程使自己进入睡眠状态。

因为任何时刻一个处理机仅能执行一个进程,所以至多有一个进程可以处在第一种状态和第二种状态。这两个状态相应于两种执行态:用户态与核心态。

3.状态转换

以上描述的进程状态给出了进程的一种静态观点,但是,实际上,各个进程是按照明确定义的规则连续地在各种状态间移动的。状态转换图是一个有向图,它的节点表示一个进程能进入的状态,而它的边表示引起一个进程从一种状态到另一种状态的事件。如果从第一种状态到第二种状态存在着一条边,则这两种状态之间的状态转换是合法的。可以从一种状态发出多个转换,但是,就处于某种状态的一个进程来说,依赖于所发生的系统事件,完成一个且只完成一个转换。图2-6给出了上述定义的进程状态的状态转换图。

..\18-1394 图\0206.tif{60%}

图2-6 进程状态及转换

如前所述,在一个分时方式中,几个进程能同时执行,并且它们可能都要在核心态下运行。如果对它们在核心态下的运行不加以限制,则它们会破坏全局核心数据结构中的信息。通过禁止任意的上下文切换和控制中断的发生,内核可保护它们的一致性。

仅当进程从“核心态运行”状态转移到“在内存中睡眠”状态时,内核才允许上下文切换。在核心态下运行的进程不能被其他进程所抢占,因此内核有时被称为不可抢先(non-preemptive)的,虽然系统并不抢占处于用户态下的进程。因为内核处于不可抢先状态,所以内核可保持它的数据结构的一致性,从而解决了互斥(mutual exclusion)问题——保证在任何时刻至多一个进程执行临界区代码。

比如,让我们考虑图2-7中的示例代码。该代码段要把其地址在指针变量bp1中的数据结构,插入双向链表中地址在指针变量bp中的数据结构之后。如果当内核执行这一代码段时系统允许上下文切换,则会发生如下情形。假设直到注释出现之前内核执行该代码,然后做一个上下文切换,这时双向链表处于非一致性状态:结构bp1一半被插在该链表上,另一半在该链表外。如果进程沿着向前的指针,则它能在该链表上找到bp1;但如果沿着向后的指针,则它不能找到bp1(图2-8)。如果其他进程在原来的进程再次运行之前操控链表上的这些指针,则双向链表结构会被永久性地毁坏。UNIX系统通过一个进程在核心态下执行时不允许上下文切换来防止这种情况发生。如果一个进程进入睡眠从而允许上下文切换,则必须使内核算法的编码实现能够确保系统数据结构处于安全、一致的状态。

..\18-1394改(补画图4个)\0207.tif{45%}

图2-7 创建双链表的示例代码

..\18-1394改(补画图4个)\0208.tif{75%}

图2-8 由于上下文切换而造成的不正确链表

能引起内核数据的非一致性的有关问题是中断的处理。中断处理能改变内核状态信息。举例来说,如果内核正在执行图2-7中的代码,当执行到注释行时接收了一个中断,并且中断处理程序是如前所述的那样操纵指针,则中断处理程序就会破坏该链表中的信息。若规定在核心态下执行时系统禁止所有的中断,就可以解决这一问题。但是这可能会使中断的服务推迟,或者可能会损害系统吞吐量,为此,改为当进入代码临界区(critical region)时内核把处理机执行级提高,以禁止中断。如果任意的中断处理程序的执行会导致一致性问题的话,那么代码段是临界的。比如,如果一个磁盘中断处理程序操纵图中的缓冲区队列,则内核操纵缓冲区队列的那个代码段是关于磁盘中断处理程序的代码临界区。临界区应小且不经常出现,以便系统吞吐量不大会被它们的存在所影响。其他操作系统解决这一问题的方法是:规定在系统状态下执行时封锁所有的中断,或者采用完善的加锁方案以保证一致性。第12章将面对多处理机系统再回过头来讨论这一问题。这里所给出的解答在那时就不够了。

现在让我们回顾一下本节的内容:内核通过仅当一个进程使自己进入睡眠时才允许上下文切换,以及通过禁止一个进程改变另一个进程的状态来保护它的一致性。它还在代码临界区周围提高处理机执行级,以封锁其他能引起非一致性的中断。进程调度程序定期地抢占用户态下的进程执行,以使进程不能独占式地使用CPU。

4.睡眠与唤醒

一个在核心态下执行的进程在决定它对系统事件的反应上它打算做什么方面有很大的自主权。进程能互相通信并且“建议”各种可供选择的方法,但由它们自己做出最后的决定。正如我们将要看到的,存在着一组进程在面临各种情况时所应服从的规则,但是每个进程最终都是自主地遵循这些规则的。例如,当一个进程必须暂停它的执行(“进入睡眠”)时,它能自由地按自己的意图去做。然而,一个中断处理程序不能睡眠,因为如果中断处理程序能睡眠,就意味着被中断的进程会被投入睡眠。

进程会因为它们正在等待某些事件的发生而进入睡眠,例如:等待来自外围设备的I/O完成;等待一个进程退出;等待获得系统资源;等等。当我们说进程在一个事件上睡眠时,这意味着,直到该事件发生时,它们一直处于睡眠状态;当事件发生时它们被唤醒,并且进入“就绪”状态。很多进程能同时睡眠在一个事件上;当一个事件发生时,由于这个事件的条件再也不为真了,所以所有睡眠在这个事件上的进程都被唤醒。当一个进程被唤醒时,它完成一个从“睡眠”状态到“就绪”状态的状态转换,对于随后的调度来说,该进程就是个合格者了,但它并不立即执行。睡眠进程不耗费CPU资源;内核并不是经常去查看一个进程是否仍处于睡眠状态,而是等待事件的发生,那时把进程唤醒。

举例来说,一个在核心态执行的进程有时必须锁住一个数据结构,如果发生后来它进入睡眠的情况,其他企图操纵该上了锁的数据结构的进程必须检查上锁情况,并且因为别的进程已经占有该锁,则它们去睡眠。内核按如下方式实现这样的锁:

while(条件为真)
      sleep(事件:条件变为假);
置条件为真;

它按如下方式解锁并唤醒睡眠在该锁上的所有进程:

置条件为假;
wakeup(事件:条件变为假);

图2-9描绘了三个进程A、B、C为一个上了锁的缓冲区进行竞争的情况。睡眠的条件是缓冲区处于上锁状态。在任一时刻只能有一个进程在执行,它发现缓冲区是上了锁的,就在缓冲区变为开锁状态的事件上等待。终于,缓冲区的锁解开了,所有的进程被唤醒并且进入“就绪”状态。内核最终选择一个进程(比如说B)执行。进程B执行“while”循环,发现缓冲区处于开锁状态,于是为缓冲区上锁,并且继续执行。如果后来进程B在为缓冲区解锁之前再次去睡眠(例如等候I/O操作的完成),则内核能调度其他进程去运行。如果它选择了进程A,进程A执行“while”循环,发现缓冲区处于上锁状态,那么它就再次去睡眠。进程C可以做同样的事情。最后,进程B醒来并为缓冲区解锁,允许进程A也允许进程C存取缓冲区。因此,“while-sleep”循环保证至多一个进程能获得对资源的存取。

第6章将极其详细地介绍睡眠与唤醒的算法。在此期间它们应被考虑成是“原子的”:一个进程瞬时地进入睡眠状态,并停留在那儿直至它被唤醒。在它睡眠之后,内核调度另一个进程去运行,并切换成后者的上下文。

..\18-1394改12.16\0209.tif{80%}

图2-9 在一个锁上睡眠的多个进程

2.3 内核数据结构

大多数内核数据结构都占据固定长度的表而不是动态地分配空间,这一方法的优点是内核代码简单。但是它限制了一种数据结构的表项的数目,即为系统生成时原始配置的数目。如果在系统操作期间,内核用完了一种数据结构的表项,则它不能动态地为新的表项分配空间,而是必须向发出请求的用户报告一个错误。此外,如果内核被配置得具有不可能用完的表空间,则因不能用于其他目的而使多余的表空间浪费了。然而,一般都认为内核算法的简单性比挤出主存中每一个仅有的字节的必要性更重要一些。算法通常使用简单的循环来寻找表中的空闲表项,这是一个较易于理解的方法,而且有时比复杂的分配方案更为有效。

2.4 系统管理

管理进程可以非严格地归入为用户团体的公共福利提供各种功能的那类进程。这些功能包括磁盘格式化、新文件系统的创建、修复被破坏的文件系统、内核调试及其他。从概念上说,管理进程与用户进程没有区别:它们都使用为一般用户团体可用的相同的一组系统调用。它们仅在被允许的权限与特权上区别于一般用户进程。例如,文件存取权限允许管理进程操纵对一般用户来说禁止进入的文件。在内部,内核把一个称为超级用户(superuser)的特殊用户区别出来,赋予它特权,这一点我们即将看到。通过履行一次注册-口令序列或通过执行特殊程序可使一个用户成为超级用户。超级用户特权的其他用途将在随后的章节中研究。简而言之,内核不识别一个分离的管理进程类。

2.5 本章小结

本章描述了内核的体系结构:它的两个主要成分是文件子系统与进程子系统。文件子系统控制用户文件中数据的存储与检索。文件被组织到文件系统中,而文件系统被看作一个逻辑设备。像磁盘这样的一个物理设备能包含几个逻辑设备(文件系统)。每个文件系统都有一个用来描述文件系统的结构和内容的超级块,并且文件系统中的每个文件都由索引节点描述,索引节点给出了文件的属性。操控文件的系统调用通过索引节点来实现其功能。

进程有各种状态,并且按照明确定义的转换规则在这些状态之间转移。特别之处在于,在核心态下执行的进程能暂停它们的执行而进入睡眠状态,但是没有哪一个进程能把另一进程投入睡眠状态。内核是不可被抢占的,这意味着,一个在核心态下执行的进程将连续执行,直至它进入睡眠状态或直至它返回到用户态下执行时为止。内核通过实施不可抢占策略以及通过在执行代码临界区时封锁中断来维护它的数据结构的一致性。

本章的其余部分详细描述了图2-1所示的子系统及它们的交互作用。从文件子系统开始,继之以进程子系统。下一章将涉及高速缓冲问题,并描述在第4章、第5章和第7章要介绍的算法中所使用的缓冲区分配算法。第4章考查文件系统的内部算法,包括索引节点的操控、文件的结构及路径名到索引节点的转换。第5章解释若干系统调用,例如,系统调用open、close、read及write,这些系统调用使用了第4章中的算法来访问文件系统。第6章论述进程上下文的基本思想及其地址空间;第7章涉及有关进程管理及使用第6章的算法的系统调用;第8章介绍进程调度;第9章讨论存储管理算法;第10章讲的是设备驱动程序,直到这时终端驱动程序与进程管理之间的相互关系才能被解释;第11章介绍了进程间通信的某些形式;最后两章涉及若干高级专题,包括多处理机系统与分布式系统。

2.6 习题

1.考虑如下命令序列:

grep main a.c b.c c.c > grepout &
wc −1 < grepout &
rm grepout &

每一命令行尾部的“&”都通知shell在后台运行这些命令,并且它能并行地执行每个命令。为什么这不等价于如下的命令行?

grep main a.c b.c c.c| wc −1

2.考虑图2-7中的内核代码示例。假设当代码到达注释处时发生上下文切换,并且假设另一进程通过执行如下代码而从链表中摘掉一个缓冲区:

remove(qp)
    struct queue *qp;
{
    qp— >forp — >backp = qp — >backp; 
    qp— >backp— >forp=qp — >forp;
    qp— >forp=qp— >backp = NULL;
}

考虑三种情况:

  • 进程从链表中摘掉bp结构;
  • 进程从链表中摘掉bp1结构;
  • 进程摘掉链表上当前跟在bp1之后的结构。

这三种情况中哪种是原来的进程执行完注释以后的代码时链表的状态?

3.如果内核试图唤醒睡眠在一个事件上的所有进程,但是在唤醒时没有进程睡眠在那个事件上,那么会发生什么情况?


[1] “core”这一术语指的是机器的原始存储,不是指硬件技术。

[2] bss这一名字来自IBM 7090机的汇编伪运算符,它代表“block started by symbol”。

[3] u区中的u代表用户。u区的另一个名称是ublock;本书则总是称它为u区。

UNIX操作系统设计(各大网店已上架)

莫里斯·J.,巴赫(Maurice J.Bach) 著,陈葆钰,王旭,柳纯录,冯雪山 译

在这里插入图片描述

  • Linux之父Linux Torvalds曾捧读的经典著作
  • UNIX操作系统经典著作,畅销多年
  • 深度剖析UNIX操作系统内核的内部数据结构、算法和UNIX系统的问题

本书以UNIX系统为背景,全面、系统地介绍了UNIX操作系统内核的内部数据结构和算法。本书首先对系统内核结构做了简要介绍,然后分章节描述了文件系统、进程调度和存储管理,并在此基础上讨论了UNIX系统的问题,如驱动程序接口、进程间通信与网络等。在每章之后,还给出了大量富有启发性和实际意义的题目。

2007-04-28 11:55:00 kwiner 阅读数 2755

  一口气把<<UNIX操作系统设计>>一书读完了,这一口气大概是从06年6月开始呼入,于07年4月才呼出,哈哈。

  看完这本书非常受益,首先,解答一些朋友和同事的疑问,看这本书并不是为了研究LINUX内核才看的,当然,为了更好地理解UNIX和 LINUX系统,以及更好地理解操作系统是一个原因之一。更重要的是,通常阅读此类书藉,可以学习到超级大师们的软件设计方法,以及他们面对各种编程技术 难题时命使用的对策,软件开发的各种优化的策略以及高效率的算法等等。你会发现,其实平时我们在开发中遇到的一些技术实现的问题,有一些大师们也会遇到, 他们会找到一个巧妙的算法来解决或者绕过它,这也许是我们可以提高的地方。例如在前阵子的工作中,我在设计和实现列表框控件时就遇到了一个难题,当列表框 上的表项数非常大(上万条)的时候,用户的操作变得非常慢,幸运的是,我从该书中描述磁盘块缓冲区的分配算法中找到了灵感,通过缓冲可见区域的表项,极大 地优化用户操作的流畅性。我深信书中描述的UNIX操作系统的很多优秀的实现算法,很多都可以应用于平时的日常开发工作中去(特别是系统软件,或者是对效 率要求高的软件),比起那些专门去描述算法与数据结构的书藉,这本书描述得生动多了。

  推荐这本书给做软件开发的朋友们看,这本绝对是居家生活、杀人灭口必备之书!哈哈。

2019-08-29 14:22:35 qq_36321889 阅读数 68

第二章 核心导言

UNIX操作系统的体系结构

核心框图,给出各种模块及他们之间的相互关系,其中文件系统和进程管理是最核心部分:
在这里插入图片描述
翻译成系统调用界面有点low,应该是系统调用接口叭,为什么将存储管理放置到进程管理当中呢,应该切分出来叭,毕竟存储管理、内存管理、缓存管理那块重要程度也是挺高的,既然挺重要为什么不抽离出来呢!

什么是所谓的宏内核、微内核呢?
见知识补充-微内核与宏内核-笔记

系统概念介绍

文件子系统概述

一个文件的内部表示由一个索引节点(inode)给出,inode:index node,每个文件都有一个索引结点,但是它可以有几个名字,且这几个名字都映射到索引节点上,每个名字称为一个联结。

索引结点存储在文件系统中,但是当操纵文件时,核心把它们读到内存索引节点表中。

核心还包含另外两个数据结构,文件表和用户文件描述符表。文件表是一个全程核心结构,但是用户文件描述符表对每个进程分配一个。也就是说和文件系统有关的有三个表:文件表、用户文件描述符表、索引结点表,当一个进程打开或建立一个文件时,核心在每个表中都相应于该文件的索引节点分配一个表项。

文件表保存文件中的字节偏移,用户文件描述符表标识着一个进程的所有打开文件。

核心在逻辑上只涉及文件系统,不涉及磁盘,每个文件系统都当作由一个逻辑设备号标识的逻辑设备。

一个文件系统由一个逻辑块序列组成,每个块都包含512、1024、2048个字节或512个字节的任意倍数。下文都是以来讲解的。

一个文件系统具有如下数据结构:
[外链图片转存失败(img-xkGjHga9-1567059429772)(en-resource://database/3080:1)]
引导块:可以包含引导操作系统的引导代码,虽然为了引导系统只需要一个代码块,但每个文件系统都有一个引导块,当然可能是空的。

超级块:描述了文件系统的状态—它有多大,它能储存多少文件,在文件系统的何处可找到空闲空间,以及其他信息。

索引节点表:在执行了系统调用mount之后,该文件系统的目录结构就可以从这个根索引结点开始存取了。

数据块:包含文件数据与管理数据

进程

用实际的术语来说,UNIX系统上的进程是被系统调用fork所创建的实体。除了0进程以外,每个进程都是被另一个进程执行系统调用fork时创建的。0进程是系统引导后手动创建的,在它创建了一个子进程1后,0进程就变成了对换进程。1进程被称为init进程,是系统中其他每个进程的祖先,并且享有同它们之间的特殊关系。

可执行文件由以下几部分组成:

  1. 描述文件属性的一组“头标”
  2. 程序正文
  3. 数据的机器语言表示,它给出程序开始执行时的初值;一个空间指示,它指出核心应该为被称为bss的未初始化数据分配多大的空间;
  4. 其他段,诸如符号表信息;

一般都会有:代码段(程序正文)、数据段(bss段)、堆栈段

因为UNIX系统中的一个进程能在两种态—和心态和用户态下执行,所以UNIX系统中核心栈与用户栈是分开的。

系统调用的过程:每个系统调用都在系统调用库中有一个入口点,系统调用库按汇编语言编码并包含一个特殊的trap指令,当该指令被执行时,它引起一个中断,从而导致硬件转换到核心态。

执行核心态代码时,使用核心栈。

后面几章讨论的进程表中的字段是:

  1. 状态字段
  2. 标识符—指出拥有该进程的用户
  3. 当一个进程被挂起时的事件描述符集合

u区包含的是用来描述进程的信息,这些信息仅当进程正在执行时才需要被存取,重要的字段是:

  1. 指向当前正在执行的进程的进程表项的指针;
  2. 当前系统调用的参数,返回值以及错误码
  3. 所有的打开文件的文件描述符
  4. 内部IO参数
  5. 当前目录和当前根
  6. 进程以及文件大小的限度

u区在Linux内核中还存在嘛?

进程上下文

当执行一个进程时,系统被说成在该进程的上下文中执行,当核心决定它应该执行另一个进程时,它做一个上下文切换,以使系统在另一个进程的上下文中执行。

从用户态到核心态的转变并不涉及到上下文切换,上下文并没有切换,仅仅换了一个状态;

中断产生时,从用户态切换到核心态,并且保留足够的上下文信息,以便后续恢复,然后再核心态下对中断进行服务,核心并不产生或调度一个特殊进程来处理中断。

进程状态

用户态-执行
核心态-执行
就绪状态
进程正在睡眠

状态转换

[外链图片转存失败(img-0DCwvXVA-1567059429773)(en-resource://database/3082:1)]
核心少不了cli,sti,为了保证一致性叭!!!

在核心态下运行的进程不能被其他进程所抢占,因此核心有时被称为不可抢先的,所以核心能保证它的数据结构一致性,从而解决了互斥问题,不能抢占的方式包含:不允许调度、关掉中断!

这里需要指出Linux和Unix的一个区别,Linux2.6及更高版本的内核是可以被抢占的,Unix的内核不能被抢占;

什么叫核心态不能抢占呢?

  • 得先弄明白什么时候进入核心态,大多时候都是系统调用叭?
  • 先假设只讨论系统调用时进入核心态,emmm,不会讨论,之后查资料叭

但是如果关掉中断的话,可能导致中断的服务推迟,或者可能会损害系统吞吐量,为此,改为当进入代码临界区时,核心把处理机执行级提高,以禁止中断!,事实上,Linux0.11中直接关掉了中断
[外链图片转存失败(img-Fottu4y9-1567059429774)(en-resource://database/3084:1)]

睡眠与唤醒

一个中断处理程序不能去睡眠,因为如果中断处理程序去睡眠,就意味着将被中断的进程投入睡眠;

睡眠进程不耗费CPU资源,但是进行上下文切换的过程会影响CPU利用,核心并不是经常去查看一个进程是否仍处于睡眠状态,而是等待事件的发生,那时把进程唤醒。

下图所示的代码片段频繁出现在Linux内核程序中:
[外链图片转存失败(img-DDMZsqwo-1567059429775)(en-resource://database/3086:1)]

很关键的一点是不要使用:

if(条件为真)
    sleep(事件:条件变为假)
置条件为真

而应该使用

while(条件为真)
    sleep(事件:条件变为假)
置条件为真

有了上面基础再去看Linux内核源码就会感觉简单很多!!!

核心数据结构—思想层面

大多数核心数据结构都是占据固定长度的表,而不是动态地分配空间,这一方法的优点是核心代码简单高效。

一般认为核心算法的简单性比挤出内存中每一个仅有的字节的必要性更重要。典型地,算法使用简单的循环,以寻找表中的空闲表项,这是一个较易于理解的方法,而且有时比复杂的分配方案更为有效。

系统管理

管理进程和普通进程类似,只是管理进程的权限稍微大了一点,但是核心并不识别一个分立的管理进程。

本章小结

主要描述了文件系统和进程系统;

其中文件系统控制用户文件中数据的存储与检索,每个文件系统都有一个用来描述文件系统的结构和内容的超级块,并且文件的索引都是通过inode来完成的。inode:index node。

核心态不能被抢占意味着:核心态下的进程将连续执行,直至它进入睡眠状态或直至它返回到用户态下执行为止,维护数据的一致性是可以的,但是核心不可抢占是十分影响实时性的;

核心态通过实施不可抢占策略以及通过在执行代码临界区时封锁中断来维护它的数据结构的一致性。

还是需要去查资料,去了解Unix和Linux的区别,从本书的描述和Linus的代码中也能看出一些区别,不过查资料得到知识相对来讲能让看代码的过程更加明确;

2019-08-29 14:50:44 qq_36321889 阅读数 43

引言

为什么要读这本书,这本书可以说是非常有年代了,具体是哪年发布的我没有考证,不过Linus当年写Linux的时候就是参考的本书。

我是在读Linux0.11版源码的时候,发现buffer.c里的很多算法并不是很好理解,而且算法细节均没有注释,以及算法的原因也没有解释,所以就去读这本书了;

不读不知道,一读吓一跳,发现Linux0.11版的buffer.c就是将本书中的算法描述部分用C语言实现了,所以更加坚定了读本书的信念;

将这本书的pdf版分享给大家:
Unix操作系统设计.pdf-百度网盘链接
提取码:v9np

说明

本人一直使用印象笔记来记笔记,而且其中具有markdown功能,但是缺点是,图片问题不怎么好处理;

所以只能将文字复制上来,然后再重新上传图片,所幸图片并不是很多;

Unix操作系统设计一书中的每章节后面会附习题,我也尽可能的求解习题,当然仅仅是我个人的理解,至于对错不敢保证;

结构

由于印象笔记的缺点,我将每一章的读书笔记分拆开来,也就是一章对应一篇博客;

文笔可能不太好,语言也可能不太严谨,我的读书笔记也仅仅是本书的浓缩版,而且附加习题解读;

各章节目录

知识补充-微内核与宏内核
知识补充-抢占式和非抢占式
知识补充-用户、用户组的概念
第一章:概述
第二章:核心导言
第三章:数据缓冲区高速缓存
第四章:文件的内部表示
第五章:文件系统的系统调用-1
第5章 文件系统的系统调用-2



没有更多推荐了,返回首页