声明:本文为个人笔记性质类文章。去年毕业后参加工作至今已有一年多的时间,在工作过程中深刻认识到了过去自己认为大学课程中所学知识毕业后毫无用处这一想法的愚蠢之处,于是通过mooc网课等途径重新学习了几门大学期间学过并且在工作中经常接触到的课程,包括操作系统,计算机网络,数据库系统,数据结构与算法等。一是为了提升个人的技术能力,在了解底层原理的基础上能够知其然并知其所以然,写出更优秀的代码。二是为了弥补本科阶段过分贪玩没有把关键知识学好的遗憾。在学习过程中也发现了许多老系统在设计思想上的天才之处,这些天才的想法在后来发展出的其他新系统中也有应用,比如Redis内存淘汰策略的LRU算法,实际上就是操作系统页面调度算法之一LRU思想的迁移。之前的笔记都记在了本地文本文档里,由于分门类记录导致文件过多,为复习方便和防止丢失,将内容誊写于此,难免有不准确和个人理解有误之处,如有发现希望碰巧刷到的同学在评论区大胆指出不吝赐教。并且由于内容为学习笔记,故只粗略记录了个人觉得比较重要的知识点,并不适合系统性的学习。

 

1.寄存器,缓存内外存,虚拟内存技术
寄存器是中央处理器(cpu)的一个组成部分,cpu取数据操作数据都要放在寄存器里,而缓存是与cpu封装在一起的并列的部件,寄存器的速度远大于缓存,cpu首先从缓存里找他要操作的数据,常用的数据都被放在缓存里。缓存找不到才回去找内存(主存就是内存),内存是断电即失的(内存可以理解成电脑的内存条了。比如8G内存条,它虽然快但是里面的东西是断电即失的)。外存(磁盘,可以理解成硬盘)的内容需要先加载到内存中才会被cpu访问到。
虚存技术:由于程序的空间局部性,一个完整的程序在同一时间内实际上只有其中的一小段在运行,操作系统能够识别出哪一小段代码在运行的时候暂时用不到它上下相邻的代码段,如果把整个程序全放在内存里就有点浪费宝贵的内存空间了。所以就有了虚存技术,运行到某个段的程序就把这个段的程序加载到内存里,其他段放在硬盘里。这样通过内存+硬盘配合调度的方法就形成了一个虚拟的内存概念,给它命名为虚存,它把硬盘也当成了内存的一部分只不过里面存的是暂时运行不到的代码段。看以下的例子:
①for(i=0;i<1024;i++){
for(j=0;j<1024;j++){
a[j][i]=0;
}
}
由于c语言的行优先机制,一行1024个数每个整形数站4bit所以会生成一个4k的内存块来存一行的数据。这种写法第一次把a[0][0]放进去再想放a[1][0],发现当前的4k内存的内存块不是存放a[1][0]——a[1][1023]的块,就会发生缺页中断。由于虚拟内存技术,如果此时内存不够了放不下第二个4k的内存块,就要把第一个存放着a[0][0]的内存块从内存里拿出去放到硬盘里,再生成一个新的4k内存块放到内存里。如此循环下去,每赋值一个就要发生一次缺页中断,做一次页面调度,一共做了1024*1024-1次页面调度,超级费时。
虚存管理未必是基于分页存储的,也可以实基于分段存储的。
②for(i=0;i<1024;i++){
for(j=0;j<1024;j++){
a[i][j]=0;
}
}
这种写法就换了个思路:放完了a[0][0]之后要放a[0][1],不需要页面调度,只有在a[0][1023]放完了要放a[1][0]且内存不够的时候才有页面调度。一共发生了2013-1次页面调度,时间复杂度要比第一种写法好的多。

2.分段存储分页存储
分段和分页都是非连续空间内存分配,主要是为了解决连续空间分配产生的内存碎片问题。
分段:记录段号和段内偏移,其中段内偏移量是可变的。
分页:记录页号和页内偏移,区别是:页的偏移量是固定不变的。但是记录页号和页内偏移量的页表(页表是用来记录逻辑地址和物理地址之间映射关系的)过大,存储起来不方便,访问也很慢,解决这个问题的方式有两种。
第一个是缓存:建立一个快表放在缓存里,经常访问的页号放在快表里面。快表里查不到再去查页表,这就涉及 到了缓存命中率的问题,这是从时间方面解决。
第二个是多级页表,跟索引树的结构类似这是从空间方面解决。
缓存和多级页表是同时使用的。
分页和分段都是根据逻辑地址去寻找物理地址,反向页表是根据物理地址寻找逻辑地址。

3.页面调度算法(现代计算机系统大多数都是页式存储而不是段式存储,下面的都是局部页面调度算法,局部页面调度算法在程序满足局部性原理的条件下才有意义)
关于前面提到的页面调度,实际上是发生了缺页中断(逻辑地址对应的物理地址映射不存在,需要调入新的页),但是在内存中空间不足的时候才会产生的,如果a[0][0]放完要放a[1][0]发现内存里剩下的空间超过4k那就不用调度了。
关于虚存的概念,朴素的想,肯定是希望虚存里的东西越少用越好,把它当成内存但是只要它不读入内存就可以冒充真的内存了!因为硬盘数据的读进内存比在内存里直接操作要慢得多,所以实际上页面调度算法的核心思想就是尽量减少调度发生的次数。页面调度算法主要的几种如下(作为衡量标准的最优思路是把将来一段时间最不可能用到的页给置换出去):
①先进先出算法FIFO:
先放进内存的页先被淘汰,用链表就可以实现了
②最近最久未使用算法(Least Recently Used,著名的LRU):
根据历史推测未来,遍历当前所有页,看他们的上次被访问时间,哪个距离现在最长把哪个替换出去。注意与FIFO的区别,先进来的未必先被淘汰因为它可能进来之后总是被访问。不过LRU算法实际实现起来数据结构比较复杂。
③时钟页面置换算法:把所有页连在一起形成一个环形链表,设置一个指针指着其中一个页,在页上有一个访问标志位,一个页被访问则把它的访问标志位设置为1。当发生页面调度的时候,看当前指针指着的页访问标志位是不是1,如果是1,就置为0往后走,直到找到第一个为0的页,把它置换出去,新的页放进来,这个页的访问位置为1,指针在往后走一步停住,直到下一次发生页面调度重复以上步骤。
④二次机会法:这个算法就非常有意思了,是对时钟算法的改进,这次不仅用访问标志位还用了脏数据位。因为对于被写了的数据脏数据位被置为1,这种页面如果要调度出去需要把它重新写回硬盘来保证数据一致性,显然我们希望没被写过的优先于被写过的被调出去。而如果是一个只是被读到的数据那么直接把这块内存清空就可以了,因为内存和硬盘里放的东西是一样的。被写过的页脏数据位设为1,就通过这个脏数据来给它二次机会。在时钟扫到它的时候,先把访问标志位由1设为0,如果转了一圈发现它访问标志位虽然是0但是写位是1,就再把写位置为0,指针往下走。这样这一条数据有两次不被调出去的机会,并且如果下一轮轮到这条数据之前它又被写了一次就又多了一条命,还可以再活一轮,这样的设计可以使脏数据总是比普通数据更不容易发生调度,优先调度普通数据(其实就是从内存里清空)会提高页面调度的效率。
⑤最不常用算法(Least Frequent Used,LFU):跟LRU想法类似,LRU是最久未被访问,LFU是访问频率最低。用一个访问次数来代表一个页面的使用次数,利用滑动窗口的思想,只统计过去固定时间段的次数当作频率,当发生缺页调度的时候把频率最低的替换出去。问题就在这个次数怎么计算,前面的访问标志位和脏数据标志位都可以用硬件的01表示,但次数不行。

4.进程和线程
(1)进程:进程是资源分配单位
进程的状态:5个状态:
创建状态:初始化完成后进入就绪态
就绪状态:一旦得到cpu就可以开始执行
运行状态:运行状态可以转入阻塞状态,也可以退化成准备状态(退化成就绪状态的原因主要是多个进程争抢时间片)
阻塞状态:阻塞状态结束后需要先进入就绪态才能再进入运行状态,不能直接从阻塞变成运行状态
结束状态

特殊状态--内存挂起:内存挂起跟阻塞不一样,挂起意味着进程不再占用内存空间,而是由于页面调度被放在了硬盘上。
分两种:阻塞挂起(等待某事件的出现才会被加载进内存,但不会加载如内存后立即执行)和就绪挂起(一进入内存就可以立即执行)。阻塞挂起有可能转换为就绪挂起状态

(2)线程:线程是CPU调度单位
线程的状态:可以理解成跟进程一样
用户态线程和内核态线程:用户态线程用户能看到操作系统看不到,内核态线程用户看不到操作系统能看见。内核线程就是内核中实际获得cpu资源并执行的。用户线程想要跑必须被调度和某个内核线程绑定,然后内核线程再去竞争cpu资源,
用户线程和内核线程的绑定有一对一,多对一,多对多三种模型。
线程可以直接通过共享进程内资源进行通信

5.进程调度算法
(1)先来先服务算法:等待时间波动不稳定,容易发生短时服务等待超长时间的情况,平均等待时间很长。
(2)短时先服务算法:平均等待时间是最少的,但是容易导致一个时间长的进程一直在等待(饥饿现象)
(3)最高响应比优先算法:通过一个进程的执行历史来预估它下一次的执行时间。(执行时间+等待时间)/执行时间,可以看到这个公式执行时间越短或者等待时间长则值越大,值越大越优先执行,相较前两种算是比较好的算法。稍微有个小问题就是执行时间是无法完美预测的。
(4)轮询:每个线程轮流占有一个相同的时间片,如果在时间片内执行完了就提前交出使用权。时间片的设置很讲究,因为时间片长了就退化成先来先服务了,短了的话进程切换开销又过大。优点是公平性较强,每个进程都能在短时间内得到响应。
(5)多级反馈:把进程放在不同的优先级队列里,优先级越高的队列里的进程越先执行。同时有反馈机制,执行时间很长的进程优先级越来越低,而等待时间长的进程优先级越来越高,进程可以在不同的优先级队列之间升级或降级。
(6)最公平的算法--公平共享调度算法

6.进程同步(进程通信也用了)
(1)信号量法,P()操作使信号量-1,V()操作使信号量+1,可以控制多个进程共同进入临界区。P操作可以呗阻塞,但是V操作不会阻塞(有些类似生产者消费者问题)通信的过程是:P操作把信号量减1之后如果信号量<0了则进程等待。而V操作把信号量+1之后如果信号量<=0(注意此处是小于等于),则说明之前有一个进程把信号量减成负数了,则唤醒一个进程。
(2)管程法

7.进程通信
(1)信号法,只用一个bit来作为信号,通过软件通知事件处理
(2)管道法,一个进程的结果输出作为另外一个进程的输入,传递的是字节流,有缓冲区buffersize有限,是间接通信
(3)消息队列,传递的不是无意义的字节流,而是有意义的消息可以被解析,也有缓冲区buffersize有限,也是间接通信
(4)共享内存,有一个内存空间是所有进程都能访问到的,是最快的一种通信方式,需要同步机制来保证正确性,保证不会发生两个进程在同一个地址写的情况
(5)socket


8.磁盘调度
磁臂前后移动找磁道,在一个磁道上的扇区中读数据。时间开销最大的不是扇区的旋转而是磁臂的前后移动,圆形磁盘从内到外的磁道编上号,磁盘调度是希望对于随机的请求散落在不同的磁道上,尽量让磁臂的挪动较小,有以下几种算法。
(1)先到先扫描,最朴素的想法,磁臂移动长度巨大。
(2)距离最近先扫描,每次只取扫描离当前磁臂距离最小的磁道,致命缺点是离得远的要等很久很久才被扫描到。
(3)来回扫,从最外圈向最内圈挪动,在某个磁道有请求就扫描然后继续往里走,走到最里圈再从里往外走。
(4)单向扫描,和来回扫的区别就是走到最里圈之后直接挪到最外圈再从外往里扫,比来回扫更加公平一些
(5)C-SCAN:是对上面的方法的改进,每次从外往里走的时候不走到头,而是走到最靠里的那个请求的位置就停下,瞬移到最外层再往里扫描,重复这个过程
(6)最后一个F-SCAN是综合了C-SCAN和FIFO,把磁盘分区,每个区之间是FIFO的,区内是C-SCAN的。这样可以防止磁臂粘着。

9.死锁产生的四个条件(缺一不可,消除一个解除死锁)
(1)资源同时只能被一个进程使用
(2)一个进程占有资源未完成任务之前不释放资源
(3)一个进程不能抢占其他进程还没使用完成的资源
(4)发生循环调用,互相等待