为什么要有虚拟地址
程序可以使用一系列相邻的虚拟地址来访问物理内存中不相邻的大内存缓冲区。
程序可以使用一系列虚拟地址来访问大于可用物理内存的内存缓冲区。
不同进程使用的虚拟地址彼此隔离,一个进程中的代码无法更改正在由另一进程或操作系统使用的物理内存。
虚拟地址转化为物理地址
对于每个程序,内存管理单元MMU都为其保存一个页表,该页表中存放的是虚拟页面到物理页面的映射。
每当为一个虚拟页面寻找到一个物理页面之后,就在页表里增加一条记录来保留该映射关系,当然,随着虚拟页面进出物理内存,页表的内容也会不断更新变化。
请求分页存储管理
请求分页是目前最常用的一种实现虚拟存储器的方法,请求分页存储管理系统中,在作业开始运行之前,仅装入当前要执行的部分段即可运行,假如在作业运行的过程中发现要访问的页面不在内存,则由处理器通知操作系统按照对应的页面置换算法将相应的页面调入到主存,同时操作系统也可以将暂时不用的页面置换到外存中。
页面置换算法
OPT 页面置换算法(最佳页面置换算法) :该置换算法所选择的被淘汰页面将是以后永不使用的,或者是在最长时间内不再被访问的页面,这样可以保证获得最低的缺页率,但由于人们目前无法预知进程在内存下的若千页面中哪个是未来最长时间内不再被访问的,因而该算法无法实现,一般作为衡量其他置换算法的方法。
FIFO(First In First Out) 页面置换算法(先进先出页面置换算法) : 总是淘汰最先进入内存的页面,即选择在内存中驻留时间最久的页面进行淘汰。
LRU (Least Currently Used)页面置换算法(最近最久未使用页面置换算法) :LRU算法赋予每个页面一个访问字段,用来记录一个页面自上次被访问以来所经历的时间 T,当须淘汰一个页面时,选择现有页面中其 T 值最大的,即最近最久未使用的页面予以淘汰。
LFU (Least Frequently Used)页面置换算法(最少使用页面置换算法) : 该置换算法选择在之前时期使用最少的页面作为淘汰页。
局部性原理:
时间局部性 :如果程序中的某条指令一旦执行,不久以后该指令可能再次执行;如果某数据被访问过,不久以后该数据可能再次被访问,产生时间局部性的典型原因,是由于在程序中存在着大量的循环操作。
空间局部性 :一旦程序访问了某个存储单元,在不久之后,其附近的存储单元也将被访问,即程序在一段时间内所访问的地址,可能集中在一定的范围之内,这是因为指令通常是顺序存放、顺序执行的,数据也一般是以向量、数组、表等形式簇聚存储的。
快表
为了解决虚拟地址到物理地址的转换速度,操作系统在页表方案基础之上引入了快表来加速虚拟地址到物理地址的转换
我们可以把快表理解为一种特殊的高速缓冲存储器(Cache),其中的内容是页表的一部分或者全部内容,作为页表的 Cache,它的作用与页表相似,但是提高了访问速率,由于采用页表做地址转换,读写内存数据时 CPU 要访问两次主存,有了快表,有时只要访问一次高速缓冲存储器,一次主存,这样可加速查找并提高指令执行速度。
内存分配算法
首次适应算法
空闲分区以地址递增的次序链接,分配内存时顺序查找,找到大小满足要求的第一个空闲分区就进行分配
近邻适应算法
又称循环首次适应法,由首次适应法演变而成,不同之处是分配内存时从上一次查找结束的位置开始继续查找
最佳适应算法
空闲分区按容量递增形成分区链,找到第一个能满足要求的空闲分区就进行分配
最坏适应算法
又称最大适应算法,空闲分区以容量递减的次序链接,找到第一个能满足要求的空闲分区(也就是最大的分区)就进行分配
进程与线程
区别
进程是CPU资源分配的最小单位,线程是CPU调度的最小单位
创建进程或者撤销进程,系统都要为之分配或者回收资源,开销远大于线程的创建和撤销
进程相对独立,不同的进程有不同的地址空间,而同一个进程内的线程共享一个地址空间,进程之间不会相互影响,而一个线程挂了可能导致进制内其他线程也挂了
多线程的优点
- 可以更加高效的内存共享。
- 较轻的上下文切换,不用切换地址空间
多进程的优点
进程之间相互独立,容错性更强不会因为一个进程出问题导致整个系统崩溃,可伸缩性更好,因为都有自己的内存空间,且有隔离,能够更好的伸缩
什么是用户态和内核态
用户态和内核态是操作系统的两种运行状态
内核态:处于内核态的进程可以访问任意数据和资源,如外围设备,网卡,硬盘等。处于内核态的CPU可以从一个程序切换到另一个程序,并且不会发生抢占情况。
用户态:处于用户态的进程只能受限的访问内存,并且不允许访问外围设备,CPU不能被独占,可以被其他程序获取
为什么进程切换比线程慢
进程切换与线程切换的一个最主要区别就在于进程切换涉及到虚拟地址空间的切换而线程切换则不会。因为每个进程都有自己的虚拟地址空间,而线程是共享
所在进程的虚拟地址空间的,因此同一个进程中的线程进行线程切换时不涉及虚拟地址空间的转换。
线程的切换会触发内核态和用户态的切换吗?
所谓“时间片用完”,其实是指时钟中断发生(以某个固定频率发生,由硬件自动触发),中断处理程序检查当前线程设置的时间片是否到期,如果到期就发生线程切换。
线程切换的实现一般放在内核,中断处理程序也是放在内核。从这个角度来说,如果线程当前处于用户态,若要发生线程切换,必然是要先要先进入内核态,发生状态切换的。线程切换的原因还可能是其他类型的中断,或者线程自身主动进入等待或者睡眠,这些情况无一例外都是要进入内核的。
现在我们已经知道了进程都有自己的虚拟地址空间,把不腻地址转化为物理地址需要查找页表,页表查找是一个很慢的过程,因此通常使用Cache来缓存常用
的地址映射,这样可以加速页表查找,这个Cache就是TLB,Translation Lookaside Buffer,我们不需要关心这个名字,只需要知道TLB本质上就是一
个cache,是用来加速页表查找的。由于每个进程都有自己的虚拟地址空间,那么显然每个进程都有自己的页表,那么当进程切换后页表也要进行切换,页表
切换后TLB就失效了,cache失效导致命中率降低,那么虚拟地址转换为物理地址就会变慢,表现出来的就是程序运行会变慢,而线程切换不会导致TLB失效,
因为线程无需切换地址空间,因此我们通常说线程切换比进程切换快,原因就在这里。
进程间通讯的方式
-
管道/匿名管道:用于两个相关进程之间的通信,是一种半双工的通信方式,如果要全双工,则需要两个管道
只能用于父子进程或者兄弟进程之间(具有亲缘关系的进程);
单独构成一种独立的文件系统:管道对于管道两端的进程而言,就是一个文件,但它不是普通的文件,它不属于某种文件系统,而是自立门户,单独构成一种文件系统,并且只存在与内存中。
数据的读出和写入:一个进程向管道中写的内容被管道另一端的进程读出,写入的内容每次都添加在管道缓冲区的末尾,并且每次都是从缓冲区的头部读出数据。
-
有名管道:
匿名管道,由于没有名字,只能用于亲缘关系的进程间通信。
为了克服这个缺点,提出了有名管道(FIFO)。
有名管道不同于匿名管道之处在于它提供了一个路径名与之关联,以有名管道的文件形式存在于文件系统中,这样,即使与有名管道的创建进程不存在亲缘关系的进程,只要可以访问该路径,就能够彼此通过有名管道相互通信,因此,通过有名管道不相关的进程也能交换数据。
-
消息队列:发送数据时会按照一个个独立单元的消息体进行发送,同时发送方和接收方约定好消息类型或者是正文的格式,可以实现不同进程以消息队列的形式发送给任意的进程
-
共享内存:通过不同的虚拟地址空间,映射到相同的物理地址空间上,实现内存共享,不需要再讲消息拷贝过来拷贝过去
-
信号量:本质是一个计数器, 用来协同共享资源的竞争问题
-
套接字:不同主机上的进程之间通信,就需要套接字来实现
进程的状态模型
三态模型
五态模型
进程的调度算法
-
先来先服务
-
时间片轮转
-
短作业优先
-
最短剩余时间优先
当一个进程加入到就绪队列时,他可能比当前运行的进程具有更短的剩余时间,因此只要新进程就绪,调度程序就能可能抢占当前正在运行的进程。像最短进程优先一样,调度程序正在执行选择函数是必须有关于处理时间的估计,并且存在长进程饥饿的危险。
- 优先级调度算法
守护进程
守护进程是脱离于终端并且在后台运行的进程,脱离终端是为了避免在执行的过程中的信息在终端上显示,并且进程也不会被任何终端所产生的终端信息所打断。
守护进程一般的生命周期是系统启动到系统停止运行。
Linux系统中有很多的守护进程,最典型的就是我们经常看到的服务进程。
当然,我们也经常会利用守护进程来完成很多的系统或者自动化任务。
孤儿进程
父进程早于子进程退出时候子进程还在运行,子进程会成为孤儿进程,Linux会对孤儿进程的处理,把孤儿进程的父进程设为进程号为1的进程,也就是由init进程来托管,init进程负责子进程退出后的善后清理工作
僵尸进程
子进程执行完毕时发现父进程未退出,会向父进程发送 SIGCHLD 信号,但父进程没有使用 wait/waitpid 或其他方式处理 SIGCHLD 信号来回收子进程,子进程变成为了对系统有害的僵尸进程
子进程退出后留下的进程信息没有被收***导致占用的进程控制块PCB不被释放,形成僵尸进程,进程已经死去,但是进程资源没有被释放掉
问题及危害
如果系统中存在大量的僵尸进程,他们的进程号就会一直被占用,但是系统所能使用的进程号是有限的,系统将因为没有可用的进程号而导致系统不能产生新的进程
任何一个子进程(init除外)在exit()之后,并非马上就消失掉,而是留下一个称为僵尸进程(Zombie)的数据结构,等待父进程处理,这是每个子进程在结束时都要经过的阶段,如果子进程在exit()之后,父进程没有来得及处理,这时用ps命令就能看到子进程的状态是Z。
如果父进程能及时处理,可能用ps命令就来不及看到子进程的僵尸状态,但这并不等于子进程不经过僵尸状态
产生僵尸进程的元凶其实是他们的父进程,杀掉父进程,僵尸进程就变为了孤儿进程,便可以转交给 init 进程回收处理
死锁
什么是死锁
导致线程卡死的所冲突
死锁的必要条件
互斥条件:
一个资源每次只能被一个进程使用,即在一段时间内某资源仅为一个进程所占有,此时若有其他进程请求该资源,则请求进程只能等待
请求与保持条件:
进程已经保持了至少一个资源,但又提出了新的资源请求时,该资源已被其他进程占有,此时请求进程被阻塞,但对自己已获得的资源保持不放
不可剥夺条件:
进程所获得的资源在未使用完毕之前,不能被其他进程强行夺走,即只能由获得该资源的进程自己来释放(只能是主动释放)
循环等待条件:
若干进程间形成首尾相接循环等待资源的关系
解决死锁的基本方法
破坏互斥
破坏非抢占
破坏循环等待
避免死锁
银行家算法产生一个进程推进的安全序列
银行家算法找到一个安全序列, 来顺序进行资源的分配
怎么解除死锁
- 资源剥夺:挂起某些死锁进程,并抢占它的资源
- 撤销进程:强制撤销部分、甚至全部的死锁进程,释放这些进程的资源
- 进程回退:让一个或者多个进程回退到足以避免死锁的地步
I/O模型
网络I/O什么时候会发生阻塞
- connect:需要阻塞等待三次握手的完成。
- accept:需要等待可用的已完成的连接,如果已完成连接队列为空,则被阻塞。
read 为读数据,从服务端来看就是等待客户端的请求,如果客户端不发请求,那么调用 read 会处于阻塞等待状态,没有数据可以读,这个应该很好理解。
因为我们用的是 TCP 协议,TCP 协议需要保证数据可靠地、有序地传输,并且给予端与端之间的流量控制。
所以说发送不是直接发出去,它有个发送缓冲区,我们需要把数据先拷贝到 TCP 的发送缓冲区,由 TCP 自行控制发送的时间和逻辑,有可能还有重传什么的。
如果我们发的过快,导致接收方处理不过来,那么接收方就会通过 TCP 协议告知:别发了!忙不过来了。发送缓存区是有大小限制的,由于无法发送,还不断调用 write 那么缓存区就满了,满了就不然你 write 了,所以 write 也会发生阻塞。
综上,read 和 write 都会发生阻塞。
BIO
NIO
多路复用
SELECT模型
使用一个bitmap表示一些文件描述符是否有io事件到来,但是bitmap有上限,只能表示1024个
缺点:
- rset,会被重置,不能重用,需要反复的写rset
- bitmap有上限,1024
- 要反复将所有的fd拷贝到内核
- 每次select出来之后,还需要o(n)的时间来遍历,知道到底是那个fd可以读了
POLL模型
将文件描述符添加了状态,来表示是否有io事件的到来
解决了SELECT中rset不能重用的问题,以及上限问题
EPOLL模型
n返回的是有多少个socket接收到了数据
epoll_wait之后会将有数据的socket进行重排,让有数据的在前面,这样在遍历的时候,就之后遍历到有数据的socket,这样的就绪队列是使用双向链表实现的,且维护了一个红黑树索引方便搜索,防止重复添加。