埃氏筛 & 欧拉筛
素数的定义:指在大于1的自然数中,除了1和该数自身外,无法被其他自然数整除的数。大于1的自然数若不是素数,则称之为合数。
素数的定义:指在大于1的自然数中,除了1和该数自身外,无法被其他自然数整除的数。大于1的自然数若不是素数,则称之为合数。
matplotlib 绘制的图线有自己的显示窗口,有时候希望在其他的UI设计中使用其绘制的图,比如PyQt,官方有一个支持QT的显示窗口类,但配置很麻烦,在这里记录一种简便的导出方式
主要思路为使用matplotlib的print_png
函数将其图片数据导出到二进制流中,然后numpy从此二进制流中取出数据即可
Github地址:笔记-操作系统精髓与设计原理第8版
操作系统是管理计算机硬件与软件资源的系统软件,同时也是计算机系统的内核与基石。操作系统需要处理如管理与配置内存、决定系统资源供需的优先次序、控制输入与输出设备、操作网络与管理文件系统等基本事务。操作系统也提供一个让用户与系统交互的操作界面。—— 维基百科
前几章为引言部分 略
程序代码 + 数据集
轨迹:进程执行的指令序列
非运行态 \(\Leftrightarrow\) 运行态
原因:当所有进程都处于阻塞态时,处理器处于休闲状态。此时将某个进程的一部分或者全部移入磁盘,然后从挂起队列加载一个新进程,放入内存中运行
进程创建的原因:
进程终止的原因:
进程映像:程序 + 数据 + 栈 + 属性
进程标识信息:存储在PCB中的数字标识符,包括: 进程 ID,父进程 ID,用户 ID
进程状态信息(处理器状态信息):存储所有的程序状态字(PSW)
进程控制信息:操作系统协调各种活动进程的额外信息
PCB 是操作系统中最重要的数据结构,包含操作系统所需进程的全部信息
PCB 集合定义了 OS 的状态
如何在发生错误和变化时,保护 PCB,具体表现为两个问题:
特权模式称为系统模式,控制模式或者内核模式,非特权模式又称为用户模式
原因:保护操作系统和重要的操作系统表 不受用户程序的干扰
ELSE:PCB 中有指示执行模式的位,因事件变化而变化,当用户调用OS服务或中断触发系统例程时,执行模式变为内核模式,返回到用户进程时变为用户模式
##### 何时切换进程
可在OS从当前正在运行的进程获得控制器的任何时刻发生
出现中断时,处理器将:
第二章指出操作系统的两个特殊事实:
- OS 和普通软件以相同的方式运行,也是一个程序
- OS 会频繁的释放控制权,并依赖于处理器来恢复控制权
在所有进程外部执行操作系统内核,进程概念只适用于用户程序,操作系统则是则是在特权模式下单独运行的实体
操作系统是用户调用的一组例程,在用户进程的环境中执行并实现各种功能。进程映像不仅包括自己的程序,数据,栈还包括内核程序的程序,数据,和栈区域。操作系统代码和数据位于共享地址空间中,并被所有用户进程所共享。只需要在同一进程中切换模式,而不需要切换进程
把操作系统作为一组系统进程来实现
优点:
这两个特点是独立的,为了区分这两个特点,通常将分派的单位称为线程(轻量级进程 LWP )
而将资源所有权的单位称为进程(任务)
指OS在单个进程内支持多个并发执行路径的能力
在多线程环境中,进程定义为资源分配单元和一个保护单元,与进程相关联的有:
每个线程都有:
使用线程的几个例子:
线程的优点
Why : 因为线程共享一个地址,内存,文件空间,ULT中不用切换到内核,进程切换需要内核
线程状态:就绪态,运行态,阻塞态
基本操作:派生,阻塞,解除阻塞,结束
线程同步:同步线程的活动是它们互不干扰且不破坏数据结构,如两个线程向一个链表加入元素,则可能会丢失
管理线程的所有工作都由应用程序完成,==内核意识不到线程的存在==(线程在内核看来和进程是一致的)。任何应用程序都可以设计成多线程程序。线程库提供了所有关于线程的操作
ULT相较于KLT的优点:
ULT 相较于 KLT 的缺点:
管理线程的所有操作由内核完成,应用级只有一个到内核线程实施的应用编程接口(API)
KLT 的优点:
KLT 的缺点:
结合两者优点
并不是核越多越好,管理起来越麻烦,会有更多多余的开销
影响多核系统上软件性能的因素
操作系统的核心问题是进程和线程的管理:
并发是所有问题的基础,也是操作系统设计的基础(设计问题:进程通信,资源共享和竞争)
出现的环境:
1 | zvoid echo() { |
这个打印字符的程序很容易发生数据的丢失,出现这种问题的原因是中断可能在进程的任何地方发生,解决方案是控制对共享资源的访问
竞争条件发生在多个进程或线程读写数据时,其最终结果取决于进程的指令执行顺序
有几个基本概念:
临界资源 :就是上面谈到的一个不可分享的资源
临界区:使用这一部分资源的程序称为程序的临界区
死锁:两个进程互相控制两个资源,但又还需要对方持有的资源才可以继续工作,这样就产生了死锁(两个进程都不能继续工作)
饥饿:有三个进程 ABC ,每个进程都需要访问资源 R,资源被 AC 交替访问,却始终没有分配给 B 这样 B 就处于饥饿状态
互斥:简单来说,就是两个或多个进程需要访问一个不可分享的资源的保护机制
1 | while(true) { |
临界区不能被中断,所以可以保证互斥,为了保证互斥只需要保证一个进程在访问资源的时候不被中断。
但是,这种方法的代价非常高。由于处理器被限制得只能交替执行程序,因此执行的效率会明显降低。而且它不能用于多处理器体系结构。当一个计算机系统含有多个处理器时,通常可能有多个进程同时执行。这种情况下,中断不能保证互斥。因为在多处理器配置中,几个处理器对内存的访问不存在主从关系,处理器之间的行为是无关的,表现出一种对等的关系,处理器之间没有支持互斥的中断机制。
在硬件级别上,对存储单元的访问排斥对相同单元的其它访问,因此处理器的设计人员提出了一些机器指令,用与保证两个动作的原子性(不能被中断的指令),在这个指令执行的过程中,任何其它指令访问内存都将被总之,而且这些动作在一个指令周期中完成
定义如下:
1 | int compare_and_swap(int *word, int testval, int newval) |
使用一个测试值检查一个内存单元,如果内存单元的当前值是
testval
,就使用 newval
取代该值,否则保持不变,并返回旧内存值。因此如果返回值和测试值相同,表示内存单元已经被更新,整个过程按原子操作执行,不接受中断。这个过程的另一个版本为返回
bool
值,判断是否完成交换
定义如下:
1 | void exchange(int *register, int *memory) |
忙等待(自旋等待):进程在得到临界区访问权之前,它只能继续执行测试变量的指令来得到访问权,除此之外不能做任何事情
对于上图 a ,唯一可以进入临界区的进程是发现 bolt
为0的那个进程,并把bolt置为1在它访问临界区的时候,此时其它的进程都处于忙等待中,访问结束后继续将
bolt 置为 0,此时下一个可以进入临界区的进程就是在这之后最早执行
compare&swap
指令的进程
对于上图b ,工作原理和 a 几乎一致。由于变量初始化的方式和交换算法的本质,下面的表达式恒成立:
\[ bolt + \sum_ikey_i = n \]
若 bolt = 0
,则没有任何一个进程在它的临界区中,若
bolt = 1
,则只有一个进程在临界区中,且为 key为
0 的那个进程
有如下的优点:
但也有一些严重的缺点:
讨论的是提供并发性的操作系统和设计语言的机制
常用的并发机制:
基本原理如下:
两个或多个进程可以通过简单的信号进行合作。强迫一个进程在某个位置停止,直到它接受到一个特定的信号,其中使用了一个称为信号量的特殊变量。通过信号量
s 传送信号,进程须执行原语 semSignal(s)
要通过信号量 s
接受信号需要执行原语 semWait(s)
若相应信号未发送则阻塞进程,知道发送完为止
为达到预期效果,可把信号量视为一个值为整数的变量,定义了三个操作:
semWait
使信号量减 1,若值变成负数,则阻塞执行
semWait
的进程,否则继续执行semSignal
操作使信号量加 1 ,若值小于等于 0 ,则被
semWait
操作阻塞的进程解除阻塞信号量为正数时,代表发出 semWait
后可以继续执行的进程数量,信号量为负数时,每个 semSignal
操作都会将等待进程中的一个进程解除阻塞
对于信号量有三个重要结论:
信号量原语的定义:
1 | struct semaphore { |
二元信号量的定义:
1 | struct binary_semaphore { |
理论上二元信号量更易于实现,且可以证明==与普通信号具有同样的表达能力==,非二元信号量也称作计数信号量或一般信号量
与二元信号量有关的还有互斥锁(Mutex)。互斥是一个编程标志位,用来获取和释放一个对象。可以对一个资源进行加锁和解锁操作,即为置0和置1,可以由互斥量和二元信号量实现,二者区别在于,互斥量解锁和加锁的进程必须是同一个进程,二元信号量进行加锁操作,而由另一个进程解锁
可以理解==强信号量不会导致饥饿,而弱信号量可能导致饥饿==
信号量机制示例
使用信号量的互斥:
1 | const int n = /*进程数*/; |
问题描述:有一个或多个生产者生产某种类型的数据,并放置在缓冲区中;有一个消费者从缓冲区中取数据,每次取一项,任何时候只有一个主体访问缓冲区。问题是要确保:当缓冲区已满时,生产者不会继续向其中添加数据,当缓冲区为空时,消费者不会从中移走数据
首先假设缓冲区是无限的,且是一个线性数组,可以使用二元信号量和计数信号量实现
二元信号量错误的方法:
1 | int n; // 缓冲区剩余生产量 |
为什么是错误的,可能造成消费完之后继续取
也就是==在消费者消费之后已经不属于互斥资源保护区,发生中断之后不能保护原有变量的值==,正如上图第10行,本来应该阻塞消费者进程,但是由于中断使 n++,并且有重新将 delay 置1,而后恢复消费者进程消费完缓冲区之后 delay 信号仍然为1所以,此时缓冲区为空但是并不会阻塞进程,所以还会继续从已经为空的缓冲区拿东西(也就是 delay 信号并不能匹配了)
二元信号量正确的方法:
1 | int n; // 缓冲区剩余生产量 |
使用一般信号量(计数信号量),可得到一种更好的解决方法,如下
1 | // 直接把n和信号量联系起来 |
如果是有限缓冲区的话,只需要对缓冲区大小也设置信号量保护即可
1 | const int sizeofBuffer = //缓冲区大小 |
即使用 s.flag
的互斥原语实现了信号量操作的原子性
管程是一种程序设计语言结构( C/C++ 语言没有 JAVA 支持)
它提供的功能与信号量相同但是更易于控制
感觉就是把信号量的一些操作给封装了
管程通过使用条件变量来支持同步,这些条件变量包含在管程中,并且只有在管程中才能被访问
有两个函数可以操作条件变量:
暂略
这个例子表明,与信号量相比,管程担负的责任不同。对于管程,它有自己的互斥机制:两个进程不能同时访问缓冲区,但是 cwait csignal 原语的位置需要注意。管程优于信号量之处在于,所有的同步机制都被限制在管程内部,因此不但易于验证同步的正确性,而且易于检测出错误。此外若一个管程被正确的编写,则所有进程对受保护资源的访问都是正确的,而对于信号量,只有当所有资源的进程都被正确编写时,资源访问才是正确的。
上述方法有两个缺陷
csignal
的进程在管程内还未结束,则需要两次额外的进程切换:阻塞进程需要一次切换,管程可用时又需要一次切换在新的管程规则(Mesa)中,csignal
原语被
cnotify
代替,
cnotify
可以解释如下:当一个正在管程中的进程执行
cnotify(x)
中,会使得x
条件队列得到通知,但发信号的进程还在继续执行。但是由于不能保证在它之前没有其它进程进入管程,因而这个等待进程必须重新检查条件。
cbroadcast
原语:广播可以使所有在该条件上等待的进程置于就绪态,当一个进程不知道有多少进程被激活时,这种方法非常方便
进程交互式必须满足两个基本要求:同步和通信,为实施互斥,进程间需要同步;为实现合作,进程需要交换信息,提供这一方法之一就是消息传递
注:互斥和同步的联系:——摘自百度知道:
相交进程之间的关系主要有两种,同步与互斥。所谓互斥,是指散布在不同进程之间的若干程序片断,当某个进程运行其中一个程序片段时,其它进程就不能运行它 们之中的任一程序片段,只能等到该进程运行完这个程序片段后才可以运行。所谓同步,是指散布在不同进程之间的若干程序片断,它们的运行必须严格按照规定的某种先后次序来运行,这种先后次序依赖于要完成的特定的任务。 显然,==同步是一种更为复杂的互斥,而互斥是一种特殊的同步==。 也就是说互斥是两个线程之间不可以同时运行,它们会相互排斥,必须等待一个线程运行完毕,另一个才能运行,而同步也是不能同时运行,但它是必须要安照某种次序来运行相应的线程(也是一种互斥)! 总结:互斥:是指某一资源同时只允许一个访问者对其进行访问,具有唯一性和排它性。但互斥无法限制访问者对资源的访问顺序,即访问是无序的。 同步:是指在互斥的基础上(大多数情况),通过其它机制实现访问者对资源的有序访问。在大多数情况下,同步已经实现了互斥,特别是所有写入资源的情况必定是互斥的。少数情况是指可以允许多个访问者同时访问资源。
特点(优点):可以在分布式系统、共享内存的多处理器系统和单处理器系统中实现
消息传递原语:
send(destination, message)
receive(source, message)
两个进程之间的消息通信隐含着某种同步的信息:只有当一个进程发送消息后,接受者才能接受消息
一个进程发出send
或者receive原语
后,我们需要确定会发生什么:有三种组合:
阻塞send
,阻塞receive
:
发送者和接收者都被阻塞,直到完成信息的投递,也叫做会合,考虑进程间的紧密同步
无阻塞send
,阻塞receive:
发送者可以继续,但接收者会被阻塞直到请求的消息到达,适用于服务器给其它的进程提供服务和资源
无阻塞send
,无阻塞receive:
不要求任何一方等待
两个原语的中确定源进程或目标的方案有两类:直接和间接寻址
直接寻址:
send
原语包含目标进程的标识号,而 receive
有两种处理方式,一种是显示的指定源进程,该进程需要事先直到希望接受来自哪一个进程的消息。另一种是不指定所期望的源进程,例如打印机接受其它进程的打印请求。
间接寻址:
消息不直接从发送者发送到接收者,而是发送到一个共享数据结构,由临时保存消息的队列组成,称为信箱,具有一对一,多对一,一对多,多对多三种形式。其中多对一的信箱又叫做端口。
进程和信箱的关联可以是静态的,也可以是动态的。
还有就是所有权问题。对于端口来说,信箱的所有几乎都是接受进程(多对一),由接受进程创建,对于通用信箱,可以视信箱为创建它的进程所有和该进程一起终止,或是为操作系统所有,这时销毁信箱需要一个显示命令
最简单的排队原则是先进先出,还有优先级原则,以及允许接收者检查消息队列并选择下一次接受哪个消息
1 | const int n = /* 进程数 */ |
使用无阻塞 send
和阻塞 receive
实现互斥,如果消息被一个进程收取,则另外一个执行 receive
操作的进程将被阻塞
生产者消费者问题:
利用了消息传递的能力,除了传递信号之外,它还传递数据。它使用了两个信箱。当生产者产生数据后,数据将作为
消息发送到信箱 mayconsume
,只要该信箱中有一条消息,消费者就可开始消费。从此之后
mayconsume
用做缓冲区,缓冲区中的数据被组织成消息队列,缓冲区的大小由全局变量
capacity 确定。信箱 mayproduce
最初填满空消息,空消息的数量等于信箱的容量,每次生产使得
mayproduce
中的消息数减少,每次消费使得
mayproduce
中的消息数增多。
暂略
死锁定义为一组相互竞争系统资源或进行通信的进程间的“永久”阻塞,所有死锁都涉及两个或者多个进程之间对资源需求的冲突。
简单来说两个进程都希望获得已经掌握的资源才能继续执行,就产生了死锁。
操作系统中死锁检测、预防和避免方法小结:
表征进程资源分配的有效工具是 Holt 引入的资源分配图,如下:
其中方块中原点表示资源的一个实例,边表示请求资源和占有资源
如果==资源分配图中出现环,并且环中存在资源实例个数小于环中进程的个数==,则可能导致死锁
死锁有三个必要条件:
这三个为必要条件并非充分条件,要产生死锁还需要第四个条件:
循环等待:存在一个闭合的进程链,每个进程至少占有此链中下一个进程所需的一个资源
第四个条件是前三个条件的潜在结果,满足前三个条件,然后在特定顺序的进程调度下就有可能产生死锁。
死锁预防策略是设计一种系统来排除发生死锁的可能性,死锁预防分为两类
此条件不可能禁止,对于多进程的并发执行调度中,互斥是必须满足的条件
预防此条件,可以==要求进程一次性地请求所有需要的资源,并阻塞这个进程直到所有请求都同时满足==,显然,这个方法是低效的
循环等待条件可通过定义资源类型的线行顺序来预防,若一个进程分配了R类型的资源,则接下来请求的资源只能是排在R类型之后的资源
类似占有且等待的预防方法,循环等待的预防方法是低效的,会使进程执行速度变慢,且在没必要的情况下拒绝资源访问
死锁避免允许三个必要条件,但通过特定的选择,确保永远不会到达死锁点,死锁避免可允许更多的并发,死锁避免通过当前的资源分配采取措施,所以需要直到未来进程资源请求的情况
书本给出两种死锁避免方法:
考虑 n 个进程和 m 种不同类型资源的系统,有以下定义:
从中可以得知:
资源分配拒绝策略,即 银行家算法,定义了安全状态和不安全状态,进程请求一组资源时,查看同意此请求之后的状态,若还为安全状态,则分配资源,否则拒绝
但在此处,不可能真的对所有资源分配序列进行探查,判断是否存在此分配序列,所以通常根据下面的关系式判断是否是安全序列:
\(C_{ij} - A_{ij} \le V_j , 对所有的j\)
一个安全状态的例子:
死锁避免的优点:无须死锁预防的抢占和回滚进程,且与死锁预防相比限制较少
死锁避免的限制:
死锁预防策略非常保守,它们通过限制访问资源和进程上强加约束来解决死锁问题,而 死锁检测不限制资源访问或约束进程行为,只要有可能就会给进程分配其所需要的资源,操作系统周期性的执行一个算法来检测前面的条件(4)(循环等待条件)
书本中死锁检测的算法,在之前定义的基础上还存在一个请求矩阵 \(Q\),其中 \(Q_{ij}\) 表示进程 \(i\) 请求资源 \(j\) 的数量,此算法主要是一个标记未死锁进程的过程,最初所有进程都是未标记的,然后执行以下步骤:
简单来说就是查找一个可以在当前可用资源条件下完成的进程,然后释放该进程占用的资源(即此进程可以正常执行,结束后回收资源),然后查找下一个,当不存在此进程的时候,剩余的所有进程都不可能在当前资源条件下执行,所以这些进程是死锁的。
检测到死锁后就需要某种策略来恢复死锁,下面为按复杂度递增的顺序列出可能的方法:
对于(3)(4)可参考以下原则:
以上解决死锁的策略都各有优缺点,所以操作系统可以在不同的情况下使用不同的策略
其中资源可分为:
在每一类资源中,可采取一下策略确定次序:
有五位哲学家,它们的就餐在一张圆桌上,圆桌上有5个盘子,盘子之间有一把叉子,每位想吃饭的哲学家就餐时使用盘子两侧的叉子
为避免死锁的风险,可再买5把叉子,另一种方法是只允许四位哲学家同时进入餐厅,由于最多有4位哲学家就座,因而至少有一位哲学家可以拿到两把叉子
两种方案的解决代码如下:(第一种解决方案会导致死锁)
在单道程序设计系统中(本书主要讨论单道),内存划分为两部分
简单的内存管理术语:
为了使处理器利用率最大化,程序换出到磁盘后,下次换入到换出之前的内存区域很困难,相反,我们需要把进程重定位到内存的不同区域。这样就会带来寻址的问题。处理器硬件和操作系统软件必须能以某种方式把程序代码中的内存访问转换为实际的物理内存地址,并反映程序在内存中的当前位置。
每个进程都应受到保护,以免其它进程有意或无意地干扰。
通常用户进程不能访问操作系统的任何部分,无论是程序还是数据。此外,一个进程中的程序通常不能跳转到另一个进程中的指令,若无特别许可,一个进程的程序不能访问其它进程的数据区。
内存保护需求必须由处理器(硬件)而非操作需要(软件)来满足,因为操作系统不能预测程序可能产生的所有内存访问,即使可以预测检查也非常费时。
任何保护机制都必须具有一定的灵活性,以允许多个进程访问内存的同一部分。内存管理系统在不损害基本保护的前提下,必须允许对内存共享区域进行受控访问。
计算机系统的内存和外存总是被组织成线性的地址空间。大多数程序被组织成模块,某些模块是不可修改的,若操作系统和计算机硬件能够有效地处理以某种模块形式组织的用户程序与数据,则会带来许多好处:
最易于满足这些需求的根据是分段
计算机系统分为两级,内存和外存,内存提供快速的访问,成本高,易失性;外存较慢且便宜,非易失性。
在这种两级方案中,系统主要关注的是内存和外存之间信息流的组织,组织这一信息流是由系统负责的,而不能由程序员负责。
内存管理的主要操作是处理器把程序装入内存中执行,内存管理技术由以下几种:
管理用户内存空间的最简方案就是对它分区,以形成若干边界固定的区域
分区大小:
内部碎片:装入的数据块小于分区大小,因而导致分区内部存在空间浪费,这种现象称为内部碎片
放置算法:
对于大小相等的分区,只需要把每个进程分配到能够容纳它的最小分区中,每个分区都需要维护一个调度队列,用于保存从这个分区换出的进程
对于大小不等的分区,也可以采取上面这种方式,对于单个分区来说是最优的,可以达到最小的内部碎片,但是从整个系统看不是最佳的,小内存的进程可能被阻塞即使有大的空闲分区,所以一种更可取的方式是为所有的进程只提供一个队列。如果所有都被占据,则必须进行交换,一般优先考虑一些诸如优先级之类的其它因素,或者优先选择换出阻塞的进程而非就绪进程
固定分区方案简单,但存在以下缺点:
对于动态分区,分区长度和数量是可变的,进程装入内存时,系统会给它分配一块与其所需容量完全相等的内存空间,动态分区方法会在内存中形成许多小空洞(外部碎片),内存利用率随之下降
克服外部碎片的一种方法是压缩,操作系统不时地移动进程,使得进程占用的空间连续,使得所有空闲空间连成一片。
压缩是一个非常耗时的过程,另外,压缩需要动态重定位的能力,能够把程序从内存的一块区域移动到另一块区域,且不会使程序中的内存访问无效
放置算法:
可供考虑的放置算法有三种:
伙伴系统中内存块大小为 \(2^K\) 个字,\(L \le K \le U\),\(2^L\) 表示分配的最小块尺寸,\(2^U\) 表示分配的整个内存的大小,伙伴系统简单来说就是,给定大小为 \(2^i\) (i为不小于此进程大小的最小整数),然后寻找一个大小为 \(2^i\) 的空闲块,每个大小为 \(2^i\) 的块都有维护列表,空闲块可以由对半分裂从大小为 \(2^{i+1}\) 的列表移出,并在 \(2^i\) 列表中产生两个伙伴,当 \(2^i\) 列表一对伙伴都未分配时,则合并移入到 \(2^{i+1}\) 中,可以由下面算法找到一个 \(2^i\) 大小的空闲块
1 | void get_hole(int i) { |
例子:
释放B后的二叉树:
在内存中放置进程需要的一种技术。进程在重新换入到内存后其地址是不确定的,所以需要逻辑地址和物理地址的转换
重定位的硬件支持如下:
基址寄存器为程序在内存中的地址,通过与相对地址相加转换为绝对地址,然后与界限寄存器(即程序的终止位置)比较,如超过界限寄存器则发送错误,产生中断
将 内存和进程都划分为大小固定,相等且比较小的块,在进程中的称为页,在内存中的称为页框。使用分页技术,每个进程在内存中浪费的空间,仅是进程最后一页一小部分形成的内部碎片,没有外部碎片。
它和固定分区不同的是:采用分页技术的分区相当小,一个程序可以占据多个分区,并且这些分区不需要是连续的。
实现上述的方法之一是,每个进程维护一个页表,页表给出了该进程每页对应页框的位置。在程序中,每个逻辑地址包括一个页号和该页中的偏移量。
为了使分页方案更加方便,规定页和页框的大小必须是2的幂,以便容易地表示出相对地址,有以下两个好处:
简单分页的图形表示如下:
采用分段技术,可以把程序和与其相关的数据划分到几个段,尽管段的最大长度有限制的,但不要求长度都相等,和分页一样,其逻辑地址由段号和偏移量组成。
同样会产生外部碎片,但是由于块可以设置的很小所以外部碎片也很小,分段也不要求分区是连续的。
分页对程序员是透明的,分段则是可见的。为实现模块化设计,程序或数据分段或进一步分段。
采用大小不等的段的另一个结果是,逻辑地址和物理地址不再是简单的对应关系,在简单的分段方案中,每一个进程都有一个段表,系统也会维护一个内存中的空闲块列表,段表项必须给出相应段在内存中的起始地址,还必须指明段的长度,以确保不会使用无效地址。
考虑一个 n+m 位的地址,左侧 n 位是段号,右侧 m 位是偏移量,进行地址转换有以下步骤:
简单分段的图形表示:
这里讨论的还是简单分页和简单分段,进程必须把所有全部加载到内存中,如果采用了覆盖或者虚存技术,则可以部分加载内存中,这部分在下一章讨论。
覆盖:所谓覆盖,就是把一个大的程序划分为一系列覆盖,每个覆盖就是一个相对独立的程序单位,把程序执行时并不要求同时装入内存的覆盖组成一组,称为覆盖段。一个覆盖段内的覆盖共享同一存储区域,该区域成为覆盖区,它与覆盖段一一对应。显然,为了使一个覆盖区能为相应覆盖段中的每个覆盖在不同时刻共享,其大小应由覆盖段中的最大覆盖来确定。
分段的段表给出了基地址和页号本质上是一致的,分页中页号乘以页大小即为基地址所以直接和偏移量拼接即可,段地址长度不一,所以需要给出长度过滤无效地址
简单的虚拟内存相关定义:
虚拟内存:
虚拟内存是计算机系统内存管理的一种技术。它使得应用程序认为它拥有连续的可用的内存(一个连续完整的地址空间),而实际上,它通常是被分隔成多个物理内存碎片,还有部分暂时存储在外部磁盘存储器上,在需要时进行数据交换。目前,大多数操作系统都使用了虚拟内存,如 Windows 家族的“虚拟内存”;Linux 的“交换空间”等。
虚拟地址:
在虚拟内存中分配给某一位置的地址,使得该位置可被访问,如同内存
虚拟地址空间:分配某进程的虚拟存储
地址空间:用户某进程的内存地址范围
实地址:内存中存储位置的地址
分页和分段存在着这样的特点:
这样的特点可以使得一个进程在执行的过程中,该进程不需要所有页或段都在内存中,只需要在内存保存下一条指令所在块,以及将访问的数据块即可。(这里的块都代表页或段)
进程执行过程中任何时刻都在内存中的部分称为进程的 常驻集(resident set),只要所有的内存访问都是常驻集中的单元,执行就可以顺利进行,并且处理器可以判断是否如此。当访问一个不再内存中(驻留集)的逻辑地址,会产生一个中断,操作系统会将此进程置于 阻塞态 ,为此操作系统产生一个 磁盘I/O读请求,在执行此 I/O 期间,操作系统可以调度另一个进程运行,在读入内存后(按某种置换策略),产生一个 I/O 中断,操作系统则将原来被阻塞的进程置为 就绪态。
内存又称为 实存储器,简称实存,但程序员或用户感觉到的是一个更大的内存,且通常分配在磁盘上,称为 虚拟内存(virtual memory),简称虚存。虚存支持更有效的系统并发度,解除用户与内存之间没有必要的紧密联系。
分页和分段的特点:
虚拟内存的开销收到 系统抖动(thrashing)的影响。在虚存的机制下操作系统读取一块到内存,通常要将另一块换出,如果这块正好在将要用到之前换出,操作系统不得不很快的将它收回,会 导致处理器大部分的时间都用于交换块而非执行指令。
避免系统抖动的算法都根据最近的历史来猜测将来最可能用到的块。这类推断基于局部性原理(一个进程中程序和数据引用的集簇倾向)
局部性原理表明虚存方案是可行的,要使虚存比较实用且有效,需要两方面因素:
主要有二级页表、倒排页表、转换检测缓冲区等结构
虚存分页和简单分页一样都有页表,其中 页表项(Page Table Entry,PTE) 相比简单分页也多了一些内容,如下:
其中的 P 表示它所对应的页当前是否在内存中,如果在内存中,则还包括页框号,另一位是修改位(M)表示相应的内容装入内存后是否发生变化,若没有改变则无需重新写入辅存,否则需要用该页更新原来的页。
同简单分页类似,逻辑地址依然由页号和偏移量组成,而物理地址由页框号和偏移量组成,页表的长度基于进程长度的变化而变化,以下给出了一种硬件实现:
上述页表的简单处理在虚存空间大的时候会导致页表项的也十分大,一种解决方案是在虚存中保存页表,这意味着 页表和其它页一样服从分页管理,一个进程在运行时,它的页表至少有一部分在内存中,这一部分包括正在运行的页的页表项,有一种 两级层次页表结构 来组织大型页表,典型情况如下:
以上是 32 位地址两级方案的例子,假设采用字节级寻址,页尺寸为 \(4KB (2^{12})\) ,则 \(4GB (2^{32})\) 虚拟地址空间由 \(2^{20}\) 页组成,若这些页的每一页都由一个4字节的页表项映射,则可创建由 \(2^{20}\) 页表项组成的页表,这时需要 \(4MB(2^{22})\) 的内存空间。这个由 \(2^{10}\) 页组成的巨大用户页表可以保留在虚存中,并由一个包括 \(2^{10}\) 个页表项的根页表映射,根页表占据的内存为 \(4KB(2^{12})\),二级页表的地址转换如下图:(根页表的页表项存4KB页表每一页的基地址)
即使是二级页表,页表大小与虚拟地址空间的大小成正比,可以使用倒排页表替代这种结构,==称为“倒排”的原因是,它使用页框号而非虚拟页号来索引页表项,它的大小是固定的==,在虚存空间特别大的时候开销比多级页表少。
在这种方法中,虚拟地址的页号部分使用一个散列函数映射到散列表中。散列表包含指向倒排表的指针,而倒排表中含有页表项,页表项包含以下内容:
下图是一个n位页号m位数索引倒排表的页表结构图:
每次虚存访问都可能会引起两次物理内存访问:
因此简单的虚拟内存方案会导致内存访问时间加倍,为克服这个问题,可以采用 转换检测缓冲区(Translation Lookaside Buffer,TLB)(一个特殊的高速缓存,包含最近用过的页表项)。给定一个虚拟地址,处理器首先检查TLB,若需要的页表项在其中,则检索页框号形成实地址,若未找到则使用页号检索检查页表。然后查看其”存在位“状态,若不在内存中,则会产生一次内存访问故障,称为缺页(page fault)中断,此时由操作系统负责装入所需要的页,并更新页表。基本机理如下:
TLB使用流程如下,图中未显示磁盘I/O过程中可以调度另外进程执行。根据局部性原理大多数虚存访问都位于最近使用过的页中,所以此方案可以提高性能。
TLB 的实际组织还有许多额外细节,由于 TLB 仅包含整个页表中的部分表项,因此不能简单地把页号编入 TLB 的索引,所以 TLB 的项必须包含页号和完整的页表项(和倒排表一样),这两种技术对应直接映射(索引)和关联映射,如下所示:
虚存机制须与高速缓冲系统(内存高缓)进行交互。首先,内存系统查看 TLB 是否存在匹配的页表项,若不存在则从页表中读取页表项。产生一个 TAG 标记和其余部分组成的实地址后,查看高速缓存中是否存在,若有则返回给 CPU,若没有,则从内存中检索这个字。若被访问的字在磁盘中,则包含该字的页必须装入内存,且所在的块须装入高速缓存,且其页表项必须更新。
页尺寸是一个重要的硬件设计决策,页越小,内部碎片总量越少;另一方面,页越小,每个进程需要的页数量越多,页表也会变得更大。大页表不容易存储(二级页表)会导致产生两次缺页中断(读取页表,读取页),但大页表又利于数据块传送。大体来说,缺页率和页尺寸和分配的页框数有关系。
页尺寸的设计问题还与物理内存大小和程序大小有关。
分段组织与非段式有许多优点:
类似的虚存分段和简单分段一样也为每个进程维护一个段表,段表项包含存在位和修改位,以及该段的起始地址和长度。分段的地址转换如下图:(这里虚拟地址是段号,图片有误)
分段和分页各有所长,在段页式系统中,用户地址空间被程序员划分为许多段,每段划分为和内存页框大小相同的页。逻辑地址仍然由段号和偏移量组成,段偏移量可视为指定段中的页号和页偏移量。其地址转换如下图:
操作系统内存管理设计取决于三个基本的选择:
前两者取决于所用的硬件平台(当前计算机主流提供了虚存的支持,且纯分段的系统也越来越少,结合分段分页后操作系统内存管理问题都是面向分页的)第三个就是操作系统软件领域的问题,也是本节所述。
虚拟内存的操作系统策略:
上表不存在一种绝对的最佳策略,在所有的策略中都要做到通过适当的安排,使得一个进程在执行时,访问一个未命中的页中的字的概率最小。
读取策略决定某页何时取入内存,常用的两种方法是请求分页和预先分页
在纯分段系统中,放置策略并不是重要的设计问题(最佳适配、首次适配等均可)对于分页和段页式系统,如何放置通常无关紧要,因为地址转换硬件和内存访问硬件能以相同的效率为任何页框组合执行相应的功能
置换策略涉及到的问题有:
前两个为驻留集管理,之后讨论,置换策略专指第三个概念
页框锁定:注意的是对于被锁定的页框,则不能被用于置换,锁定是给每个页框关联一个锁定位实现的,可以包含在页框表和当前的页表中
基本算法有:
最佳(Optimal,OPT):
选择置换下次访问距当前时间最长的那些页,这种方法是最优的,但是操作系统必须直到将来的事件,因此不可能实现,仅作为一种衡量标准
最近最少使用(Least Recently Used,LRU):
置换内存中最长时间未被引用的页,根据局部性原理,这也是最近最不可能访问到的页,其性能接近OPT策略,但较难实现,开销大
先进先出(First In First Out,FIFO):
将分配给进程的页框视为一个循环缓冲区,并按循环方式移动页,需要一个指针在页框中循环,这种策略实际是置换驻留在内存时间最长的页,但通常导致频繁的换入换出页
时钟(CLock):
最简单的时钟策略给每个页框附加一个使用位,每当该页 装入内存,或被访问时使用位置为 1,并有一个指针与之相关联,当 一页被置换时,该指针指向位下一个页框。需要置换一页时,操作系统扫描缓冲区,查找一个使用位为 0 的页框,扫描过程中遇到使用位为 1 时,将其置为 0。
这个时钟策略类似 FIFO,不过它跳过了 1 的项,1 说明它最近访问过,时钟策略都是尽可能用少的开销达到 LRU 的效率
四种方法的比较:
对固定页框数量且为局部页面置换,有如下关系:
一种更有效的时钟算法采取了使用位和修改位,此时每个页框状态有:
此时的时钟算法执行过程如下:
==此策略查找自被取入至今未被修改且未访问的页==(如果有的话),由于未被修改,则不需要写回辅存。
还有一种称为 页缓冲 的方法允许使用较简单的页面置换策略(FIFO)。这种算法不丢弃置换出的页而是分配到两个表中(不移动页移动对应的页表项):
这种方法的特定是,被置换的页仍然留在内存中,若进程访问该页,则可迅速返回该进程的驻留集且代价很小,实际上就是充当了高速缓存的角色
对于驻留集(操作系统给进程分配的内存空间)大小,需要考虑以下几个因素:
当代操作系统通常采取两种策略:
可变分配策略的 难点在于要求操作系统评估活动进程的行为,会增加操作系统的软件开销
对于 置换范围 有局部和全局两类
置换范围和驻留集大小之间的关系:
注意固定分配没有全局置换策略,因为如果替换其它进程的页框,因为需要保持页框数固定,则它也需要重新置换一个,则是无意义的
工作集策略:略
清除策略用于确定何时将已修改的一页写回辅存,通常有两种选择:
完全使用一种策略都存在危险,预约式请求可能在写回后又发生更改,这样就没太大的意义,请求式则意味着写回修改页和读入新页,且读入在写回前,会降低处理器的利用率
一种较好的方法是结合 页缓冲技术:只清除可用于置换的页,去除了清除和置换操作之间的成对关系,被置换页可放于修改表和未修改表。修改表中的页可以周期性的成批写出,并移到未修改表中。未修改表的一页要么被访问到而回收,要么在其页框分配给另一页时被淘汰。
加载控制会影响到驻留在内存中的进程数量,着称之为系统并发度。如果驻留进程过少,那么所有进程都处于阻塞态概率就较大,因而许多时间花费在交换上。另一方面,进程过多,那么驻留集大小可能会不够用,会发生频繁的缺页中断,导致系统抖动
解决上述冲突,可以使用工作集策略或 PFF 算法。还有人提出了 L=S 准则,通过调整系统并发度,来使缺页中断之间的平均时间等于处理一次缺页中断所需的平均时间。这样处理器的利用率达到最大。
也可以采用时钟页面置换算法,监视该算法中扫描页框的指针循环缓冲区的速度。速度低于某个给定的最小阈值时,表明出现了如下的一种或两种情况:
在上述情况下,系统并发度可以安全的增加,另一方面,指针扫描速度超过某个阈值,表明缺页率很高,要么难以找到可置换页,说明系统并发度过高
系统并发度减小时,一个或多个当前驻留进程须被挂起(换出),可根据以下标准换出进程:
多道程序设计系统中,需要提高处理器处理效率,所以需要合理的调度策略以达到效率的最大化。多道程序涉及的关键就是调度,典型的调度类型有四种,处 I/O 调度外其它三种调度类型都属于处理器调度
长程调度和中程调度主要由于系统并发度相关的性能驱动,这些在前面的章节就讨论过
处理器调度的目的是,以满足系统目标(响应时间、吞吐率、处理器效率)的方式,把进程分配到一个或多个处理器上执行。
调度的层次结构以及进程状态和其所属调度种类如下:
调度决定哪个进程须等待、哪个进程能继续运行,因此会影响系统的性能。本质上说调度属于队列管理,用于在排队环境中减少延迟并优化性能。
长程调度决定哪个程序可以进入系统中处理,因此它控制了系统的并发度。调度程序必须决定操作系统 何时 才能接纳一个进程或多个进程;同时,调度程序必须决定接受哪个作业或哪些作业,并将其转变为进程。
何时创建一个新进程,通常由要求的系统并发度驱动,下次允许哪个作业进入决策可基于简单的先来先服务(FCFS)原则,或者其它基于管理系统性能的根据。
中程调度是交换功能的一部分。典型情况下,换入决定取决于管理系统并发度的需求,在不使用虚存的系统中,存储管理也是个问题。因此换入决策将考虑换出进程的存储需求。
长程调度程序执行的频率相对较低。短程调度程序,也称为分派程序(dispatcher)执行最为频繁,精确的决定下次执行哪个进程。
导致当前进程阻塞或抢占当前运行进程的事件发生时,调用短程调度程序,包括:
短程调度的主要目标是按照优化系统一个或多个方面行为的方式,来分配处理器时间。
常用的规则可按两个维度来分类
下表总结了几种重要的调度规则,相互依赖,不可能同时达到最优,比如提供较好的响应时间可能需要调度算法在进程间频繁切换,但也会增加系统开销,降低吞吐量。
可以为每个进程指定一个优先级,调度程序总是优先选择具有较高优先级的进程,纯优先级调度方案可能导致低优先级进程可能会长期处于饥饿状态。可以让一个进程的优先级随时间或执行历史而变化,例如调度算法中的 反馈 法
有以下三个参数:
与非抢占策略相比,抢占策略虽然会导致较大的开销,但能为所有进程提供较好的服务,因为它们避免了任何一个进程长时间独占处理器的情形。
周转时间(turnaround time):就是驻留时间 \(T_r\) ,或这一项在系统中花费的总时间(等待时间+服务时间),另外有用的是归一化周转时间,为周转时间与服务时间的比值,表示一个进程的相对延迟情况。
下面是一个例子:
调度策略的比较如下:
各个策略的效率的简单度量:
FCFS 是长进程友好的,而且相对于I/O密集进程,更有利于处理器密集型进程,其它进程,FCFS可能导致处理器和I/O设备都未得到充分利用。
FCFS 自身对于单处理器系统而言并不是很有吸引力的选择,但它与优先级策略结合后通常能提供一种更有效的调度方法,如反馈。
根据时间片(time slicing)周期性的产生时钟中断,出现中断后将进程放置到就绪队列中,然后基于FCFS策略选择下一个就绪作业运行。
轮转法对处理器密集型进程和I/O密集进程的处理不同。处理器密集型进程在执行过程中通常会使用大部分处理器时间,导致I/O密集型进程性能降低,使用I/O设备低效,响应时间变化较大。
虚拟轮转法,这种方法可以避免上述不公平性,此方法的不同之处在于,接触了I/O阻塞的进程都会转移到一个FCFS辅助队列中。进行调度决策时,辅助队列中的决策优先于就绪队列中的进程,这种方法在公平性方面确实优于轮转法
SPN是非抢占的,短进程友好的,SPN的难点在于需要直到或至少需要估计每个进程所需的处理时间。对于批处理作业,系统要求程序员给出估计值,若执行时间远高于实际运行时间,系统可能终止该作业。实际中操作系统可为每个进程保留一个运行平均值,计算方法如下: \[ S_{n+1} = \frac{1}{n}T_n+\frac{n-1}{n}S_n \\ T_i:第i个实例的处理器执行时间 \\ S_i:第i个实例的预测值 \] 这属于指数平均法的一种,SPN的风险在于,长进程可能饥饿。
相当于是在SPN中增加了抢占机制的策略,和SPN一样,调度程序在执行选择函数的时候,必须具备关于处理时间的估计,并且具有长进程饥饿的风险。
SRT不像FCFS那样偏向长进程,也不像轮转法那样产生额外的中断,所以降低了开销。但是它必须记录过去的服务时间,又增加了开销,对于SPN,从周转时间上看,SRT性能更好,因为相当于一个正在运行的长作业而言,短作业可以立即选择执行。
调度规则见之前的对比图。HRRN偏向短作业(小分母产生大比值),长进程由于得不到服务等待的时间会不断增加,因此比值变大。所以是比较公平的。
调度基于抢占原则(按时间片)并使用动态优先级机制。一个进程首次进入系统时,会放在RQ0中当它首次被抢占并返回就绪态时,会放在RQ1中,在随后的时间内,每当它被抢占就降级一次直到最低级。因此,新到的进程和短进程会优于老进程和长进程。这种方法称为多级反馈(Multilevel Feedback)。
但存在一个问题,可能会造成长进程的饥饿。可以按以下办法解决:
略
公平共享调度考虑了进程组调度的基本原则。每个用户(进程组)被指定了某种类型的权值,此权值定义了用户对系统资源的共享,而且是作为在所有使用资源中所占的比例来体现的,如处理器资源。
调度是根据优先级进行的,它会考虑三方面因素:
优先级数值越大,所表示的优先级越低,适用于组k中进程j的公式如下:
计算机系统中参与I/O的外设可以分为以下三类:
其中主要差别包括:
执行I/O的三种技术:
可选的DMA配置如下:
两个最重要的目标:
为避免多余的开销和低效操作,在输入请求发出前就开始执行输入传送,并且在输出请求发出一段时间后才开始执输出传送,这样的技术称为缓冲。
两类I/O设备:
I/O缓冲方案:
缓冲的作用:
缓冲是用来平滑I/O需求的峰值的一种技术,但在进程的平均需求大于I/O设备的服务能力时,再多的缓冲也无法让I/O设备与进程一直并驾齐驱。
在多道程序设计中,当存在多种I/O活动和多种进程活动时,缓冲是提高操作系统效率和单个进程性能的一种方法
磁盘驱动器工作时,磁盘以某个恒定的速度旋转,为了读或写,磁头必须定位于指定磁道和该磁道中指定扇区的开始处。磁头定位到磁道所需要的时间称为寻道时间,之后,磁盘控制器开始等待直到适当的扇区旋转到磁头处,这段时间称为旋转延迟,寻道时间和旋转延迟的总和为存取时间,这是达到读或写位置所需要的时间,之后便开始执行读操作或写操作,这段时间就是传输时间。
总平均存取时间包括上面的三个,可用公式表示 \[ T_a = T_s + \frac{1}{2r}+\frac{b}{rN} \\ T_s 为平均寻道时间,b表示传输的字节数,N表示一个磁道的字节数,r表示旋转速度(转/秒) \] 时序比较的经典例子:
磁盘调度算法及其比较:
访问的磁道序列为:55、58、39、18、90、160、150、38、184
除此之外,还有基于优先级(PRI)和后进先出的算法
基于优先级并不会优化磁盘的利用率,但能满足操作系统的其它目标。通常较短的批作业和交互作业的优先级较高,而较长计算时间的长作业优先级较低。对于数据库系统这类策略往往使得性能较差。
后进先出在事务处理系统中,由于顺序读取文件,可减少磁壁的运动,可以提高吞吐量并缩短队列长度,但可能会导致饥饿。
略
磁盘高速缓存是内存中为磁盘扇区设置的一个缓冲区,包含磁盘中某些扇区的副本
进程从磁盘高速缓存中获取数据可以采取两种方式
后一种方法节省了内存到内存的传输时间
对于置换策略可以以下两种:
简单的LFU不能判断那些整体很少访问但是近期访问频繁的块,可以使用LFU的改进版
使用分区,新区老区(中间区),对不同区的对应措施不同
典型情况下,文件管理系统由系统实用程序组成,它们可以作为具有特权的应用程序来运行。一般来说,整个文件管理系统都被当做操作系统的一部分
文件有以下理想的属性:
文件的操作如下:
文件系统架构:
文件管理的要素:
文件管理系统作为一个单独的系统实用程序和操作系统关注的是不同两方面的内容,之间的共同点是记录的处理。
在选择文件组织,有以下重要原则:
这些原则的优先级取决于将要使用这些文件的应用程序
基本文件组织:
目录包含关于文件的信息,如属性、位置、所有权,都由操作系统管理。
目录的结构通常是树状结构:
文件共享会产生两个问题:访问权限和同时访问的管理。
访问权限有如下:
话可以按面向用户分组:特定用户,用户组,全部
对于同时访问,必须解决互斥和死锁问题
记录是访问结构化文件的逻辑单元,而块是与辅存进行I/O操作的基本单位,为执行I/O,记录必须组织成块。
对于给定的块大小,有三种组块方法:
定长组块是记录定产顺序文件最常用的方式,变长跨越式存储效率高,但难以实现。跨越两个块需要两次I/O操作,文件很难修改。变长非跨越式浪费空间,且存在记录大小不能超过块大小的限制。
在辅存中,文件是由许多块组成。操作系统负责为文件分配块。
对于分配给文件的分区大小,我们需要考虑下面四项内容:
有以下两个选择:
选择策略有:
连续分配:创建文件时,给文件分配一组连续的块。需要在创建文件时声明文件的大小,会出现外部碎片,所以需要紧缩算法
链式分配:链式分配基于单个块,使用指针建立联系,不必担心外部碎片的出现,但是局部性原理不再适用,为了克服这个问题,有些系统会周期性的合并文件
索引分配:解决了连续分配和链式分配中的许多问题。每个问价在文件分配表中都有一个一级的索引,分配给该文件的每个分区在索引中都有一个表项。
除了文件分配表外,还需要磁盘分配表(Disk Allocation Table,DAT)
卷:一组分配在可寻址的扇区的集合,操作系统或应用程序用卷来存储数据。卷中的扇区在物理存储设备上不需要连续,只需要对操作系统或应用程序来说连续即可。卷可能由更小的卷合并或组合而成。最简单的情况下,一个单独的磁盘就是一个卷,通常一个磁盘会分为几个分区,每个分区都作为一个单独的卷来工作。
Github: https://github.com/nothings/stb/
stb的库像素数据都是从左到右,从上到下存储
使用 stbi_set_flip_vertically_on_load(true); 上下翻转
使用 stbi_flip_vertically_on_write(true); 在写数据的时候翻转 (在stb_write_image中)
摘自百度百科
虚拟机(Virtual Machine)指通过软件模拟的具有完整硬件系统功能的、运行在一个完全隔离环境中的完整计算机系统。 虚拟系统通过生成现有操作系统的全新虚拟镜像,它具有真实windows系统完全一样的功能,进入虚拟系统后,所有操作都是在这个全新的独立的虚拟系统里面进行,可以独立安装运行软件,保存数据,拥有自己的独立桌面,不会对真正的系统产生任何影响 ,而且具有能够在现有系统与虚拟镜像之间灵活切换的一类操作系统。虚拟系统和传统的虚拟机(Parallels Desktop ,Vmware,VirtualBox,Virtual pc)不同在于:虚拟系统不会降低电脑的性能,启动虚拟系统不需要像启动windows系统那样耗费时间,运行程序更加方便快捷;虚拟系统只能模拟和现有操作系统相同的环境,而虚拟机则可以模拟出其他种类的操作系统;而且虚拟机需要模拟底层的硬件指令,所以在应用程序运行速度上比虚拟系统慢得多。
A Song To Share And Save
图的基础,最短路径的几种解答
单源最短路: Bellman-Ford & Dijkstra 及其简单优化 以及负圈的判断
多源最短路:Floyd-Warshall 算法的简单理解
路径还原问题
算法代码及思路主要参考:《挑战程序设计竞赛》
在此之前读者应对图已经有基础的概念,以及图的邻接表 & 邻接矩阵的表示方法
在学习BMP位图的构成时,对网上的收费16进制查看器很是烦躁,notepad查看时卡到放弃人生
因为只是为了初步学习图片知识,以及查看2进制文件内部构成的话,可以自己实现一个
使用C++的文件操作进行二进制的读操作,这里默认以1个字节为单位(2位16进制)
每次读一个字节,就将其转化位16进制,读取的时候需要注意有符号数和无符号数的区别,我这里用unsigned Char 来存取每一个字节的内容
对于C++的文件读写网上很多介绍
对于每一个读取的数写入文本文件内(.txt文件)便于查看
计算文件大小 通过C++的文件指针移动即首尾文件指针差除以1024即文件大小(KB)