第五章、线程
5.1、死锁概念
首先我们来看一个问题:
你需要使用线程Philosopher实现哲学家的生活。
我们来看一个代码:
上面的代码能保证都能吃到饭吗?
分析,我们是可以知道,当5个哲学家都并发运行到了P(S[i]);那么桌子上的筷子都被拿完了,则进行到下一行代码时,5个哲学家都陷入了阻塞。
这就是死锁现象。其他的例子还有如:
死锁的定义
同我们之前学的理论一致。
5.2、死锁起因
为什么会发生死锁呢?
- 系统资源有限
- 资源的数目不足,进程对资源的竞争而产生死锁。
- 并发进程的推进顺序不当
- 进程请求资源和释放资源的顺序不当,导致了死锁。
我们来看一个例子:
上面的这个例子是否会发生死锁呢?对于进程A来说,假设i在第五行运行完被释放,第七行运行完释放j。同理进程B在第5行释放j,第七行释放i。我们分析它:假设A进程运行到第5行,CPU停下来切换到进程B运行,第二行阻塞。当j被释放,才运行。上述过程继续分析下去,发现没有发生死锁。同理,假设B先运行到第五行,也没有发生死锁的风险。也就是说:
但是现在我们让程序A先运行到第四行,访问了i,准备取j这个时候切换到了进程B,也运行到了第四行,访问了j,准备取i,这个时候,两个进程互相争夺资源。A占有i争j,B占有j争i,陷入死锁了。
- 不正确的P-V操作也可能会带来死锁——生产者和消费者问题:现在的代码如下,两个P操作和两个V操作。
特殊情况下导致阻塞,交换了两行代码可能导致死锁:生产到第六个数据,这个时候P(mutex)——1-1=0使得缓冲区锁,P(empty)叫消费者进程来消耗数据,缓冲区满了。P(full)验证缓冲区不空,然后到P(mutex),mutex——0-1=-1,阻塞。则回到P(mutex),继续阻塞下去。
关于死锁的一些结论
5.3、死锁预防策略
死锁的必要条件
-
互斥条件
进程互斥使用资源,资源具有独占性。
-
不剥夺条件
进程在访问完资源前不能被其他进程强行剥夺。
-
部分分配条件
进程边运行边申请资源,临时需要临时分配。
- 区别于:全部分配
-
环路条件
那么如何破坏死锁?或者如何预防死锁?
上面的哲学家吃饭问题,假设限定最多四人同时吃饭,就可以避免因没有筷子而形成环路条件导致的死锁了。
解决死锁的策略
- 预防死锁
- 避免死锁
-
检测死锁
-
恢复死锁
方法:
- 预先静态分配法
- 有序资源分配法
Windows,Linux采用了何种死锁解决方案?
关于银行家算法:银行家算法——迪杰斯特拉设计
鸵鸟算法:
第六章 进程调度
6.1 进程调度概念
我们知道进程或者线程要处理同步或者死锁的情况。所以我们需要能够调度对应线程或者进程的策略。
- 在合适的时候以一定策略选择一个就绪进程运行
- 合适的调度时机是什么?
- 调度策略是什么?
- 调度的目标是什么?
所以我们需要一个一个问题的解决。
进程调度的目标
1.响应的速度要尽可能的快
2.进程处理的时间尽可能的短
3.系统吞吐量(能够运行或者处理的进程任务足够多)尽可能大
4.资源利用率尽可能高
5.对所有的进程要公平
6.避免饥饿
7.避免死锁
分析:
进程处理要尽可能短,到响应速度尽可能快,则无法做到对所有进程公平,资源利用率亦无法尽可能高。这个过程也可能出现饥饿或者死锁的情况。也就是说上述的部分原则之间存在自相矛盾的情况。
进程调度的目标(两个量化的衡量指标)
- 周转时间/平均周转时间——说明进程在系统中停留的时间长短。
- 带权周转时间/平均带权周转时间——意义就是进程在系统的相对停留时间。
分析,带权平均周转时间比较有参考意义
6.2 典型调度算法
我们目前分析了进程调度的一些概念。那么进程的调度策略到底是怎么个调度法?
典型调度算法
-
先来先服务调度(first come first serve)
-
短作业优先调度算法(short job first)
- 响应比高者优先调度算法
- 优先数调度算法
- 循环论调度法(Round-Robin)
-
可变时间片轮转调度法
将第四种调度算法改进,我们就得到了 可变时间片轮转调度法
-
多重时间片循环调度法
6.3 Linux进程调度
我们应该了解实际的操作系统的进程调度是如何是实现的。
- 普通的进程采用动态优先级来调度,调度程序周期性地修改优先级(避免饥饿)
- 实时进程由用户预先指定,以后不会改变。是采用静态优先级来调度的
Linux进程的优先级(priority)
-
静态优先级
进程创建时指定或由用户修改
-
动态优先级
- 在进程运行期间可以按调度策略改变
- 在实时进程采用动态优先级,由调度程序计算。
- 只要进程占用CPU,优先级就随时间而不断减小。
- task_struct的counter表示动态优先级。
我们来看一下task_struct的结构
调度策略
-
实时进程
两种策略
-
非实时进程(普通进程)
- SCHED_OTHER(动态优先级)
- counter成员表示动态优先级
进程调度的依据
注意,priority表示的是静态优先级,而rt_priority是动态优先级,表示实时进程特有。
动态优先级与counter
关于调度时机
进程的切换
具体的调度和切换的流程:
- 选择新的一个进程
- 调用宏切换进程上下文,实现进程的切换
- prev:当前的进程,next:被调用的新进程
- 调用switch_to(prev,next),切换上下文。
第七章、存储管理
操作系统的核心功能就是存储功能。
7.1、内存管理功能
首先我们对存储器的功能需求如下:
- 容量大
- 速度快
- 信息永久存储
- 多道程序并行
但是实现上面的某些功能时,需要解决一些问题,如多道程序带来的问题:
- 共享——代码和数据共享,节省内存
- 保护——不允许内存中的程序相互间非法访问
所以共享和保护相辅相成。下面我们来看一下存储器的实际体系
那么存储管理的功能有哪些呢?
- 1)地址映射
- 2)虚拟存储
- 3)内存分配
- 4)存储保护
下一节我们详细讲述内存管理的功能。
7.1.1、内存管理功能(一)
1、地址映射
程序中的地址是虚拟地址,而内存中的地址是实际地址,我们要进行地址重定位才能实现。
地址映射的方式有三种:
-
固定地址映射——编程或编译的时候确定了逻辑地址和物理地址映射关系。特点是程序加载时必须放在指定的内存区域,而且容易产生地址冲突,从而运行失败。比如大家都使用相同的地址编写程序,从而导致冲突的发生。
-
静态地址映射——程序装入的时候由操作系统完成逻辑地址到物理地址的映射。比如我们双击桌面应用时。程序装入内存时由一个计算公式如图所示:
特点:
-
动态地址映射——在程序执行的过程中把逻辑地址转换为物理地址。详细解析如图MOV AX,[500],这里学过操作系统就知道,这句话相当于:MOV AX,DS[500](默认使用的寄存器为DS)
特点:
应用:我们后面讲述的页式结构,分页系统都是采用动态地址映射。
2、虚拟存储
虚拟存储要解决的问题注意是以下两个:
- 程序过大或者过多时,内存不够,不能运行;
- 多个程序并发时地址冲突,不能运行;
概念:
关于和物理地址分离,意思就是虚拟地址和物理地址分离,没有冲突,你的虚拟地址使用了8000号,我的虚拟地址使用了8000号,但是映射到实际的物理地址可以不同从而没有冲突。那虚拟内存如何实现?这我们后面学。我们先理解虚拟内存的目标:
3、内存分配功能
我们知道,当一个作业进入内存时,操作系统会将其转变为进程,同时为其分配存储空间以供运行,而进程运行结束时,操作系统将进程所占有的存储空间回收。
如果操作系统带有虚拟存储管理功能,那么进程运行过程中一部分存在于内存,另一部分存在于外存。如果外存部分进入内存,则撤销外存空间,分配内存空间,反之,操作相反。
我们后面细学该功能。
4、存储保护
查询相关的博客:
存储共享中我们提到了PV(红绿灯机制)操作,那PV操作是为了限制多个进程出现同时进入临界区的情况所提出来的,也算是一种对共享变量的一种保护,不过在存储保护中这种保护则更甚一筹,即对于多个进程共享的存储区域的保护。
存储保护主要包括以下两个方面:
- 防止地址越界
这个比较容易理解,因为在我们写程序的时候也要注意的,一旦地址越界编译就会出错,无法通过,不过此时还能在编译失败时纠正。
而在操作系统中,每个进程具有相对独立的进程空间,一旦其中一个进程运行时产生的地址出现在其自身的进程空间之外,此时发生了地址越界,如果侵犯了其他进程空间,就会影响其他进程的正常执行,假如侵犯的进程空间属于操作系统,就可能导致系统崩溃。 - 防止操作越权
对于多个进程共享的存储空间,每个进程有自己的访问权限,如读,写,执行。如果该进程访问共享区域时违反了权限规定,就说这个进程发生了操作越权。
一般我们选用硬件来提供存储保护,软件作为辅助。
我们用Windows的时候,如果在系统盘里删一些东西,会有提示说需要提供权限,这就是说当前登录用户权限不够,不能做这种操作。
7.2、 物理内存管理
7.2.1、分区存储管理
分区内存管理是对物理存储进行管理,它可以分为:
- 单一区存储管理(不分区存储管理)
- 分区存储管理——把用户区分为若干大小不等的分区,供用户使用。
固定分区——分区大小位置都固定
- 定义
- 例子
- 特点
动态分区——要多大我就分配多大
-
定义和概念
-
动态分区的例子
进程进入内存,分配简单,但是当某些进程结束,撤回内存的时候,容易导致内存碎片,导致内存利用率下降。
7.2.2、分区放置策略——空闲区表如何排序原则
分区的分配
上面我们提到了分区的分配是存在问题的,我们需要解决。因为要提高内存的利用率,必然使用到动态分区,而要使用动态分区,就要解决相关的问题。所以如何解决分区的问题呢?
首先肯定要建立一些数据结构来解决分区的分配问题。
-
空闲区表
- 分配的过程——分割时注意是将分区表的底部(高地址部分)分割给用户使用,这样做的目的是为了使分割剩下的空闲区其位置不变。起始地址不变,方便更新空闲区表。
- 空闲区表如何排序呢?四种:
分区的回收
解决分区的分配,还要解决分区的回收。
7.2.3、内存覆盖技术
覆盖——Overlay
-
目的——在较小的内存控件中运行较大的程序
-
内存分区
-
常驻区:被某段单独且固定的占用,可划分多个。
-
覆盖区:能被多段共享使用,可划分多个。
-
例子如图所示,实现了大程序在小内存里面运行。前提是某个模块都不超过对应区的大小。
-
覆盖技术的缺点——编程复杂,程序员划分模块并确定覆盖关系。程序执行时间长,从外存装入内存耗时大,从上面我们看到程序是分模块的,但是每个模块的大小,如何划分这是程序员要解决的问题,比如上面的程序,如果C,F合并,80K大小,则无法装入覆盖1,则程序无法正常运行。
-
7.2.4、内存交换技术——Swapping
交换技术的原理:
- 内存不够时把进程写到磁盘(Swap Out/换出)
- 当程序要运行时重新写回内存(Swap In/换入)
图解:
优点:
- 增加进程并发数
- 不需要考虑程序的结构(对比覆盖技术)
缺点
- 增加CPU的开销(因为换入和换出整个进程涉及到I/O操作,消耗的时间大导致开销大)
- 交换的单位过大(整个进程,数据传输量大)
所以使用交换技术需要考虑的问题:
- 减少交换传送的信息量(比如以模块/段来换入换出)
- 外存交换空间的管理方法
- 程序交换时的地址重定位问题。
重定位问题
7.2.5、内存碎片
我们说过使用内存的过程里面,我们无法避免地产生内存碎片。
我们必须尽量减少内存碎片,解决内存碎片的问题。
前面我们说过现在的操作系统大都采用动态分区来管理内存。那么就涉及到三种分区放置策略
- 首次适应法
- 最佳适应法
- 最坏适应法
其中最佳适应法最容易产生内存碎片,因为它是使用最佳的空闲区来分割给进程,导致很有可能每次分割都产生内存碎片。最坏适应法最不容易产生碎片。
解决碎片问题的方法
- 1、规定门限值
- 2、内存拼接技术——实现起来难,管理难
- 3、解除程序占用连续内存才能运行的限制,从根本上解决问题。(类似的如段式,页式内存管理)
现在有一个进程需要3KB的内存,我们将该进程分割为1KB和2KB的组合,分别放入上图的空闲区,它也能运行,则能够充分使用内存,大大减少内存碎片的产生——非常高明手段,碎片不再是碎片。
7.3、虚拟内存管理
7.3.1 页式虚拟内存管理
上一小节我们已经学过了物理内存(即实内存)管理,它有如下的缺点:
基于物理内存的缺点进行改善的相关技术有:
但是还是有很多的问题,所以产生了虚拟内存管理。
虚拟内存
-
目标
- 使得大的程序能在较小的内存中运行
- 使得多个程序能够在较小的内存中运行(容纳性)
- 使得多个程序并发运行时地址不冲突(方便高效)
- 使得内存利用效率高;没有内存碎片,共享方便>。
-
实现的思路
实际的例子:
-
实现的原理——程序运行的局部性
-
程序在一个有限的时间段内访问的代码和数据往往集中在一个有限的地址内存区域内。
-
把程序的一部分装入内存在较大概率上能够让其运行一小段时间。
-
典型的虚拟内存管理方式
1、页式虚拟存储管理
概念:
把进程空间(虚拟)和内存空间划分为等大小的小片
-
小片的典型大小:1k,2k,4k…(2的几次幂,提升内存分配效率)
-
进程的小片——页(虚拟页或者页面)
-
内存小片——页框(物理页)
进程装入和使用内存的原则:
只要我们能够保证在上述内存空间里面,进程能够正常运行,则我们的目的则达到了——在较小的空间(只需用能够容纳你的三个页面)里面运行较大的程序。
实际的windows7的进程页如图:
我在win10上,无法找到页面错误这个属性栏。
2、段式虚拟存储管理
3、段页式虚拟存储管理
7.3.2、 页表和页式地址映射
页式系统中,我们需要知道虚拟内存地址它是如何映射实际的内存空间的:
基于上面的理解,我们必须要有一个结构来记录页面和页框等的映射关系——页面映射表。有了这个表,我们就知道哪个进程放在那一页那一个页框。
页面映射表
页表例子:
如何计算虚拟地址(页式地址)映射到的物理地址?
实际的例子:
7.3.3、 快表技术和页面共享技术
我们已经知道了虚拟内存地址是如何映射到物理内存的了,那么效率快不快?
快表机制(Cache,也叫高速缓冲,用于缓冲超快的CPU和慢主存的速度差,提升效率)
现代操作系统的命中率达到了95%左右。
读取数据的时候,先将数据复制一份到Cache,优先访问快表。
快表机制下的地址映射过程
思考题
页面共享——实现代码或者数据的共享问题
那么到底如何实现共享的?请看图解:
7.3.4、 缺页中断
缺页概念
分级存储体系:
- CACHE+内存+辅存
缺页到底是什么呢?这是页表扩充里面的。
页表扩充1——带中断位的页表
下面我们用例子来了解:
可以看到中断位为1的,该页不在内存中,而是辅存地址9000。
页表扩充2——带访问页和修改位的页表
缺页中断
-
定义
- 在地址映射过程中,当所要访问的目的页不在内存时,则系统产生异常中断——缺页中断。
-
缺页中断处理程序(响应和服务)
- 中断处理程序将所缺的页从页表所指出的辅存地址调入内存的某个页框中,并更新页表中该页对应的页框号以及修改中断位为0。
-
访存指令的执行过程(缺页中断处理)
如何选择要淘汰的页,这个是我们需要处理的。
- 缺页(中断)率
然后我们来看一个思考题:
看法:不准,因为如果MyFunc()是在外存里面的,则需要调度I/O操作将该函数装入,需要额外的时间,而得到的MyFunc()函数的执行可能仅仅是1ms,所以不准。当然,如果该函数的位置本身就在内存里面,则花费的是准确的时间。
7.3.5、 页面淘汰
上节讲到了缺页中断,涉及到了缺页淘汰的处理,那么到底如何进行缺页淘汰的选择?这就是淘汰策略
常用的淘汰策略
-
最佳算法(OPT算法,Optimal)
-
概念和思想
-
特点——理论最佳,实践中无法实现的算法,因为一个进程你无法预测它要装入的页面序列。
-
-
先进先出淘汰算法(FIFO算法)
-
定义和思想
-
特点分析
-
-
最久未使用淘汰算法(LRU,Least Recently Used)
- 定义及思想
- 特点分析
- 最不经常使用(LFU)算法
7.3.6、缺页因素与页式系统缺点
缺页因素
页式系统的不足之处
- 以固定大小分配,划分无逻辑含义可能导致了划分的页面所分配的代码不完整。
- 页面共享不灵活。我们知道页面的共享是将这个页面共享出去,假设需要共享的代码只占用了页面的一部分,那么我们无法实现页面的部分共享,导致共享不灵活。
- 页内碎片。假设一个文件 只达到了页面大小的99%,就是说产生了页内碎片的问题,空间被浪费了。
7.3.7、段式和段页式虚拟存储管理
段式存储管理
- 概念
- 段式内存管理系统的内存分配基本方法和虚拟内存
- 段式地址的映射机制(SMT-段表,Segment Memory Table)
映射过程我们使用例子来学习:
- 段表的简单扩充(添加附加属性)
R/W/X:读/写/执行。
-
段的共享——方便;共享段在内存里面只有一份存储;共享段被多个进程映射到各自的段表;需要共享的模块都可以设置为单独的段。
-
段式系统的缺点
对比段式系统和页式系统
段页式存储管理
段页式地址的映射机构:
7.4、 Intel CPU与Linux内存管理
学了内存管理的理论,我们要结合真正的cpu来学习。
7.4.1、 Intel CPU物理结构
1. X86的实模式(Real Mode)
2. 保护模式(Protect Mode)
保护模式又分为16位保护模式和32位保护模式。32位保护模式下,问题变复杂了。首先要明白,保护模式保护什么?保护的是:分清楚各个程序使用的存储区域,不允许随便跨界访问。然后,怎么保护?方式是:为内存里的每段地址空间定义一些安全上的属性,比如可以被多少优先级的代码写入,是不是允许执行等。>这个时候,段寄存器远远不能满足要求了。原因有二:段寄存器只有32位,保存不了这么多信息;段寄存器个数有限,不能保存内存中所有段的信息。
intel的工程师们就想出了用64位的段描述符表(descriptor table)来存储所有的段信息,段描述符表存放在内存的某个位置。段寄存器不再表示段首地址了,而是表示这个段在段描述符表的索引信息。通过段寄存器在段描述符表里找到关于这个段的所有信息。
但是,段描述符表不止一个。首先有一个全局段描述符表,简称GDT,每个程序都有自己的段描述符表,简称LDT。相应的,80386里面引入了两个新的寄存器,一个是48位的全局描述符表寄存器GDTR,指向全局描述符表GDT的首地址,一个是16位的局部描述符表寄存器LDTR,它的值随时变化,总是指向CPU当时正在执行的那个程序的局部描述符表LDT。>注意,这里不说指向LDT的首地址,是因为LDTR和CS、DS等段寄存器一样,存放的也是在GDT中的索引值,而不是实际地址。
那么段寄存器里的索引到底是GDT的索引还是LDT得索引呢?下面是实模式下段寄存器的结构:
TI位为0 表示从全局描述符表中找;TI位为1 表示从局部描述符表中找。
以保护模式下的虚拟地址xxxx:yyyyyyyy(16位段地址,32位偏移)为例,首先看xxxx的TI位,如果为0,那么在GDT中以xxxx的高13位作为索引找出段描述符,这样就得到了段基址、段限长、优先级等信息。如果TI位为1,那么从LDTR中得到当前程序的LDT在GDT中的索引,再在GDT里找到这个LDT的描述符,得到LDT的首址,然后再以xxxx的高13位为LDT里的索引找到段基址、段限长、优先级等信息。(LDTR,TR的内容只在第一次运行任务0的时候(init/main.c)需要“人工”加载,之后都由CPU自动完成)
在保护模式下,虚拟地址xxxx:yyyyyyyy(16位段地址,32位偏移),xxxx为段选择子,是段描述符相对于GDT(本质上是数组,段选择子则为其下标)首地址的偏移字节数。通过xxxx找到段描述符从而得到段首址,再加上偏移地址得到实际地址。
GDTR寄存器的结构:
GDT和LDT里的描述符的通用结构:
各属性位作用如下:
G:G=0时,段限长的20位为实际段限长,最大限长为220=1MB;G=1时,则实际段限长为20位段限长乘以212=4KB,最大限长达到4GB
D/B:当描述符指向的是可执行代码段时,这一位叫做D位,D=1使用32位地址和32/8位操作数,D=0使用16位地址和16/8位操作数。如果指向的是向下扩展的数据段,这一位叫做B位,B=1时段的上界为4GB,B=0时段的上界为64KB。如果指向的是堆栈段,这一位叫做B位,B=1使用32位操作数,堆栈指针用ESP,B=0时使用16位操作数,堆栈指针用SP。
AVL:available and reserved bit 通常为0
P:存在位,P=1表示段在内存中
DPL:特权级,0为最高特权级,3为最低,表示访问该段时CPU所需处于的最低特权级
S:S=1表示该描述符指向的是代码段或数据段;S=0表示系统端(TSS、LDT)和门描述符
TYPE:类型,和S结合使用
S=1且TYPE<8时,为数据段描述符。数据段都是可读的,但不一定可写。
S=0时,描述符可能为TSS、LDT和4种门描述符:
GDT和LDT的关系:
3、一些寄存器的介绍
4. X86下的三种地址及其转化
7.4.2、 Intel CPU段机制
- 段与段描述符(Descriptor),段描述符为8字节。
- 描述符的数据结构
- 段描述符表
- 选择子——要想知道某个描述符在描述符表的位置,我们需要一个结构。
- 把逻辑地址转化到线性地址(32位,4G)
段寄存器的高13位是索引,每个描述符是8位段寄存器:Index|Ti;根据Ti=0和1来选中GDT还是IDT实现逻辑地址转化为线性地址。
7.4.3、 Linux页面机制
7.4.3.1 英特尔cpu的分页
-
英特尔cpu支持分页机制,每个页的大小为4K
在80386下,一个页的固定大小是4K个字节,也就是4096,一个页的边界地址,不许是4K的倍数,所以4G大小的内存.就可以划分为1M个节,而我们的页的开始一般具有一个特点,比如我们的虚拟地址:
004010123,而页的首地址是00401000
后12位都是0,所以我们把页的高20位称为页码。
7.4.3.2、不同的页表机制
-
普通的一级页表(之前学的哪些都是)
-
二级页表(具体的Windows NT)
页目录实现了记录哪个小页表存放在内存哪个页框。
关于分页机制如果没有开启分页保护.那么虚拟地址就是线性地址,线性地址就是物理地址;微软是通过分页进行进程内存的隔离的;在保护模式下, 寄存器CR0的高位1表示开启分页.0表示不开启,不开启,那么访问虚拟内存,就等价于访问物理内存了。
具体的windows nt二级页表如何访问:
关于二级页表地址映射的特点:访问数据需要三次内存访问——页目录调入内存,找到对应的页表调入主存,访问对应的页表索引中的物理页面。最后得到地址,计算物理地址。页面、页表、页目录的大小都刚好是4K(占据一个页框)
-
Linux三级页表
-
7.4.4、 Linux对段的支持
7.4.4.1 段
前面的GDT和段机制历史,有一篇博客讲的挺好:Linux中的段。我摘抄了一部分:
Intel 微处理器的段机制是从8086 开始提出的, 那时引入的段机制解决了从CPU 内部16 位地址到20 位实地址的转换。为了保持这种兼容性,386 仍然使用段机制,但比以前复杂。因此,Linux 内核的设计并没有全部采用Intel 所提供的段方案,仅仅有限度地使用了一下分段机制。这不仅简化了Linux 内核的设计,而且为把Linux 移植到其他平台创造了条件,因为很多RISC 处理器并不支持段机制。但是,对段机制相关知识的了解是进入Linux内核的必经之路。
从2.2 版开始,Linux 让所有的进程(或叫任务)都使用相同的逻辑地址空间,因此就没有必要使用局部描述符表LDT。但内核中也用到LDT,那只是在VM86 模式中运行Wine 时,即在Linux 上模拟运行Windows 软件或DOS 软件的程序时才使用。
linux的GDT
Linux 在启动的过程中设置了段寄存器的值和全局描述符表GDT 的内容,段的定义在include/asm-i386/segment.h 中:
#define __KERNEL_CS 0x10 /* 内核代码段, index=2,TI=0,RPL=0 */
#define __KERNEL_DS 0x18 /* 内核数据段, index=3,TI=0,RPL=0 */
#define __USER_CS 0x23 /* 用户代码段, index=4,TI=0,RPL=3 */
#define __USER_DS 0x2B /* 用户数据段, index=5,TI=0,RPL=3 */
从定义看出,没有定义堆栈段,实际上,Linux 内核不区分数据段和堆栈段,这也体现了Linux 内核尽量减少段的使用。因为没有使用LDT,因此,TI=0,并把这4 个段都放在GDT中, index 就是某个段在GDT 表中的下标。内核代码段和数据段具有最高特权,因此其RPL为0,而用户代码段和数据段具有最低特权,因此其RPL 为3。可以看出,Linux 内核再次简
化了特权级的使用,使用了两个特权级而不是4 个。
全局描述符表的定义在arch/i386/kernel/head.S 中:
ENTRY(gdt_table)
.quad 0x0000000000000000 /* NULL descriptor */
.quad 0x0000000000000000 /* not used */
.quad 0x00cf9a000000ffff /* 0x10 kernel 4GB code at 0x00000000 index2 */
.quad 0x00cf92000000ffff /* 0x18 kernel 4GB data at 0x00000000 index3 */
.quad 0x00cffa000000ffff /* 0x23 user 4GB code at 0x00000000 index4*/
.quad 0x00cff2000000ffff /* 0x2b user 4GB data at 0x00000000 index5*/
.quad 0x0000000000000000 /* not used */
.quad 0x0000000000000000 /* not used */
/*
* The APM segments have byte granularity and their bases
* and limits are set at run time.
*/
.quad 0x0040920000000000 /* 0x40 APM set up for bad BIOS's */
.quad 0x00409a0000000000 /* 0x48 APM CS code */
.quad 0x00009a0000000000 /* 0x50 APM CS 16 code (16 bit) */
.quad 0x0040920000000000 /* 0x58 APM DS data */
.fill NR_CPUS*4,8,0 /* space for TSS's and LDT's */
从代码可以看出,GDT 放在数组变量gdt_table 中。按Intel 规定,GDT 中的第一项为空,这是为了防止加电后段寄存器未经初始化就进入保护模式而使用GDT 的。第二项也没用。从下标2~5 共4 项对应于前面的4 种段描述符值。对照图2.10,从描述符的数值可以得出:
- 段的基地址全部为0x00000000;
- 段的上限全部为0xffff(段界限=段长-1);
- 段的粒度G 为1,即段长单位为4KB,以页面为单位;
- 段的D 位为1,即对这4 个段的访问都为32 位指令;
- 段的P 位为1,即4 个段都在内存。
由此可以得出,每个段的逻辑地址空间范围为0~4GB。读者可能对此不太理解,但这种设置既简单又巧妙。因为每个段的基地址为0,因此,逻辑地址到线性地址映射保持不变,也就是说,偏移量就是线性地址,我们以后所提到的逻辑地址
(或虚拟地址)和线性地址指的也就是同一地址。看来,Linux 巧妙地把段机制给绕过去了,而完全利用了分页机制。
从逻辑上说,Linux 巧妙地绕过了逻辑地址到线性地址的映射,但实质上还得应付Intel所提供的段机制。只不过,Linux 把段机制变得相当简单,它只把段分为两种:用户态(RPL=3)的段和内核态(RPL=0)的段。另外,用户段和内核段的区别也仅仅在其RPL 不同,因此内核根本无需访问描述符投影寄存器,当然也无需访问GDT,而仅从段寄存器
的最低两位就可以获取RPL 的信息。Linux 这样设计所带来的好处是显而易见的,Intel 的分段部件对Linux 性能造成的影响可以忽略不计。
按Intel 的规定,每个进程有一个任务状态段(TSS)和局部描述符表LDT,但Linux 也没有完全遵循Intel 的设计思路。前所述,Linux 的进程没有使用LDT,而对TSS 的使用也非常有限,每个CPU 仅使用一个TSS。
通过上面的介绍可以看出,Intel 的设计可谓周全细致,但Linux 的设计者并没有完全陷入这种沼泽,而是选择了简洁而有效的途径,以完成所需功能并达到较好的性能为目标。
总结Linux的4个段
章节小测题
-
地址映射是指把程序中的虚拟地址变换成【 】中的真实地址的过程。——内存
-
分区存储管理包括固定分区和【 】两种。——动态分区
-
按空闲区位置(首址)递增排序的放置策略叫【 】适应算法。——首次
-
按空闲区大小的递增排序的放置策略叫【 】适应算法。——最佳
-
尽量保留较大空闲区的放置策略叫【 】适应算法。 ——最差
-
页面放置策略中,最容易产生页面碎片的算法是【 】适应算法——最佳
-
虚拟内存管理的实现思路用到了程序运行的【 】原理,即程序在一个有限的时间段内访问的代码和数据往往集中在有限的地址范围内。——局部
-
页式内存管理方案中,内存以【 】为单位分配使用。——页
-
假定虚拟地址是20200505,页面大小是4K字节。则该地址所在页号是【 】。(请写十进制数)——4931
P = 20200505/(4*1024) = 4931,W = 3129
-
假定虚拟地址是20200505,页面大小是4K字节。则该地址的页内偏移是【 】。(请写十进制数字)——3129
-
记录页面和页框之间对应关系的数据结构叫【 】——页表
-
页面共享原理是在不同进程的页表中填上相同的【 】,使得多个进程能访问相同的内存空间,从而实现页面共享。——页框号
-
在地址映射过程中,当所要访问的目的页不在内存时,系统产生的中断叫【 】。——缺页中断
-
好的淘汰策略应该具有具有较低的【 】且页面抖动较少。——缺页率
-
淘汰最长时间未被使用的页面的淘汰策略叫【 】算法。(请使用英文简写)——LRU
-
段表记录每段在内存中映射的位置,包括段号,【 】,段基地址等三个基本要素。——段长
-
采用二级页表的页式内存管理(不考虑快表)时,访问一个数据需要访问【 】 次内存才能最终获得存取数据。——3
-
如果发生缺页,引发缺页的线性地址保存在【 】寄存器中。(填写寄存器名字)——CR2
-
在CR3寄存中包含有页目录基址的高【 】位。——20
-
在X86 CPU 架构下的三种地址,逻辑地址先转化为【 】,再转化为物理地址。——线性地址
-
Cache是三级存储体系中速度最快,存储容量最大的一类。(判断)——错
-
固定地址映射由程序员或编译器完成地址映射,容易产生地址冲突,运行失败。(判断)——错
-
存储保护功能是指防止访问越界和防止访问越权。(判断)——对
-
静态地址映射和动态地址映射计算物理地址时都是用虚拟地址加上基址。(判断)——对
-
虚拟内存管理的目标之一是使得大的程序能在较小的内存中运行。(判断)——对
-
采用固定分区的系统在程序装入前,内存已被分区,且每个分区大小都相同,不再改变。(判断)——错,大小可以不同
-
动态分区比固定分区更容易产生碎片。(判断)——对,固定了程序大小,个数、装入顺序,内存利用率特别高
-
内存碎片是指内存损坏而导致不能使用的区域。(判断)——错
-
在页式地址映射过程中,快表的作用是尽量减少内存访问次数。(判断)——对
-
缺页中断处理程序的作用就是把相应页面的数据从写入到硬盘中。(判断)——错,写入到快表
-
最佳算法(OPT算法)淘汰以后不再需要或最远的将来才会用到的页面,是实际应用中性能最好的淘汰算法。(判断)——错
-
采用内存覆盖技术存储系统,调入一个模块时可以临时将其随意将其放在一个足够大的覆盖区上。(判断)——错
-
使用内存交换技术(Swapping)可以增加进程并发数。(判断)——对
-
提高程序的局部性可以有效降低系统的缺页率。(判断)——对
-
段页式系统的地址映射过程需要既需要段表,也需要页表,而且段表和页表都需要多个。(判断)——错
-
控制寄存器CR0的PG位作用是控制实模式和保护模式的选择。(判断)——错,是PE位
-
若在保护模式下,CS,DS等段寄存器的内容存储的是相应段的段基址。(判断)——错,不只是段基址
-
描述符表(Descriptor Table)以8字节为单位存储段的描述符。(判断)——对
-
选择子的作用是用于选择描述符表中的某个描述符。(判断)——对
-
二级页表机制中,页表和页目录的大小都是4K。(判断)——对