操作系统总结

337 阅读10分钟

进程和线程的区别

操作系统里有多个任务,每个任务就是一个进程,进程里又有多个任务,每个任务就是一个线程

  • 资源来看;进程是资源分配的最小单位,线程是程序执行的最小单位。

  • 分配内存来看;进程有自己的独立地址空间,每启动一个进程,系统就会为它分配地址空间,建立数据表来维护代码段、堆栈段和数据段,这种操作非常昂贵。而线程除了CPU之外,是共享进程中的数据的,使用相同的地址空间。因此CPU切换一个线程的花费远比进程要小很多,同时创建一个线程的开销也比进程要小很多。

  • 通信来看;线程之间的通信更方便,同一进程下的线程共享全局变量、静态变量等数据,而进程之间的通信需要以通信的方式(IPC)进行。不过如何处理好同步与互斥是编写多线程程序的难点。

  • 但是多进程程序更健壮,多线程程序只要有一个线程死掉,整个进程也死掉了,而一个进程死掉并不会对另外一个进程造成影响,因为进程有自己独立的地址空间。

为什么要设置线程

进程具有并发性,但是还会有些缺点

  1. 进程在同一时间内只能干一件事,
  2. 进程如果阻塞,那么整个进程都需要挂起,那么在进程中有些不相关的资源可能也会被中止利用

所以引入线程主要是提高程序的并发度,让程序并发的时候减少时间和空间的消耗

进程之间是如何通信的

同一台机器中:管道(有名和无名),消息队列,信号量,共享内存

不同机器中:socket,Stream

三种通信的方式

  1. 共享存储

    共享存储的做法就是在内存开辟一块共享的内存空间,两个进程访问的时候必须是互斥的,也就是说一个进程写的时候另外一个进程不能进行写操作,可以通过PV实现;也就是与信号量一起使用

    共享内存是最快的一种 IPC,因为进程是直接对内存进行存取。

    有基于数据结构的共享,比如在共享空间存放数组长度为10的数组,但是这种方式比较慢,第低级的通信方式,毕竟一次只能通信长度为10的数组的大小的内容

    有基于存储区域的共享,在内存中分配一块共享存储区,数据的形式和位置由各个进程之间相互决定,不是由操作系统,这种通信方式更加高级和速度更快。

  2. 消息队列

    进程之间以格式化的消息为单位进行传递,操作系统为进程提供了发送和接收的原语进行数据的交换

    每个进程都有一个消息缓冲队列,接收原语依次把消息从缓冲队列读取然后处理,发送队列就是用发送原语把消息发送到进程中

  3. 管道通信

  4. 信号量

    用于实现进程间的互斥与同步,而不是用于存储进程间通信数据。

    1)信号量用于进程间同步,若要在进程间传递数据需要结合共享内存。

    2)信号量基于操作系统的 PV 操作,程序对信号量的操作都是原子操作。

    3)每次对信号量的 PV 操作不仅限于对信号量值加 1 或减 1,而且可以加减任意正整数。

cpu调度算法

  • 先进先出算法FIFO((First In First Out)

    • 原理:按照进程就绪的先后顺序来使用CPU,处理器被分配给最先进入就绪队列的进程,进程一单分到CPU的使用权,就一直执行到晋城结束或阻塞时才结束。
    • 优点:这种进程按照时间顺序来使用,非抢占内存的方式,公平公正,实现起来也相对简单
    • 算法的实际效果不佳,比如在长进程后的短进程,必须等到长进程执行完毕后才能执行,不利于有效的提升用户体验
  • 短作业优先算法SCBF(Shortest CPU Burst First)非剥夺式

  • 原理:总是选取预计计算时间最短的作业投入运行。
  • 缺点:执行效率不高,因为要预先知道作业所需的cpu运行时间,很难精确运算,如果估值过低,系统可能提前终止作业,第二点是忽略作业等待的时间,由于系统不断接受新作业,每次选计算时间短的作业运行,就容易造成进入系统时间久但是计算时间长的进程得不到运行,也会出现饥饿现象。
  • 最短剩余时间优先 剥夺式

  • 最高响应比优先

    • 原理:响应比=作业周转时间/作业处理时间作业处理时间由用户给出,是一个常量,作业等待时间从0开始,最后备队列中等待时间的增加而增加,每当调度作业运行时,计算后备队列中每个作业的响应比作为她的优先级,选择优先级最高的运行
    • 优点:这是fifo和短作业有限的折中,既考虑了作业等待时间也考虑了长作业的问题
  • 时间片轮转调度算法RR(Round Robin)

    • 原理:cpu划分了一个一个的时间片,每个进程都被分了一个时间片,在时间片里给进程运行,如果在时间片轮完的情况下进程还没运行完,cpu就会剥夺她运行下一个进程,她就只能等下一轮轮到他的时候,如果进程在这个时间片内提前结束了或者阻塞了,cpu就会切换进程。优点:响应的速度快,公平公正,有利于交互式计算
    • 缺点是:进程的频繁切换要话费较多的内存开销

不同的进程应该用不同的调度算法:前台任务关注的是和用户交互的响应时间,我们的word文档,我们打一个字,需要立马显示在文档中,这就是word文档这个任务关注的是响应时间,那么他就应该用时间片轮转。而后台任务中,例如我们的javac编译java代码,它的周转时间要小,即该任务从进入到结束所花的时间要小,即编译完成的时间要小,她应该用短作业优先。

对于两类进程同时存在的问题:可以用两个队列,前台使用RR算法,后台使用SJF算法,只有前台任务没有时才调度后台任务。如果后台一直得不到执行可以讲后台的任务优先级动态升高

死锁

定义:a和b方法都需要获得锁A和锁B,当一个线程执行a方法且获得锁A,正在等待锁B;另一个线程执行b方法获得锁B两个线程,正在等待锁A。此时双方都无法再获得相应的锁,就会原地僵持,造成死锁。

public void a() {
        synchronized (lockA) {
            synchronized (lockB) {
                test1();
            }
        }
    }

    public void b() {
        synchronized (lockB) {
            synchronized (lockA) {
                test2();
            }
        }
    }

解决:解决上面所示的静态锁顺序的死锁方法:所有需要多个锁的线程,都必须通过相同的顺序来获得锁。即:

public void a() {
        synchronized (lockA) {
            synchronized (lockB) {
                test1();
            }
        }
    }

    public void b() {
        synchronized (lockA) {
            synchronized (lockB) {
                test2();
            }
        }
    }

如何避免死锁

银行家算法

​ 当进程首次申请获取资源的时候,需要测试该进程的最大需求量,如果系统中存在的系统资源可以满足,那么就分配,否则就延迟分配;

​ 当进程再次申请资源的时候,先判断这个进程还需要的系统资源和现在系统所剩余的资源的大小关系,如果系统剩余的资源不够,则拒绝申请;否则可以申请;

​ 银行家的算法主要是去对资源分配的尝试,因为只有进程获得所有资源之后然后处理才会释放所有资源,如果没到达最大的申请量,就不会释放资源,所以如果分配的序列不正确的话可能会导致死锁

安全序列

​ 银行家算法主要是找到一个安全序列,如果是安全序列就不会产生死锁;

如何解除死锁

  1. 资源剥夺。也就是把某个死锁进程进行挂起,并且抢占它的资源,将这些资源分配给其他的死锁进程。但是需要防止被挂起的进程会产生进程饥饿;
  2. 撤销进程法:强制撤销部分,甚至全部进程,并且剥夺资源,但是代价会很大,因为其他进程之前所处理的一些事情或者环境都被破坏掉了,重来一次的话可能会花费比较多的时间
  3. 进程回退法:让一个或者多个进程回退到足以避免死锁的地步,但是需要系统记录进程的历史信息,设置还原点;

如何检查死锁

虚存

虚拟内存就是一个内存优化的方案,操作系统能把一些散乱分在内存或者磁盘里的多个存储部分有效利用起来。操作系统完成了虚拟内存地址到真实内存地址或者磁盘地址之间的映射工作 当使用虚拟内存时,并不是把全部的进程页全部调用内存,只需要在内存中调用够用的页就可以了,当需要访问外存中的程序页时,再调用即可。

类比你在食堂吃饭,你用勺子一口一口吃光了所有的米饭,别人过来看到你以为你的嘴和碗一样大

虚存页面调度算法

  • 最近最久未使用算法——(LRU) 根据页面调入内存后的使用情况,选择内存中最久未使用的页面被置换。这是局部性原理的合理近似,性能接近最佳算法

  • 先进先出淘汰算法——FIFO 把进程中已调入内存的页面,按先后次序链成一个队列,并设置一个替换指针,使它总是指向内存中最老的页面。

局部性原理

分为空间局部性和时间局部性

时间局部性:如果程序中的某条指令开始执行,则不久之后该指令可能会再次被执行;如果某数据被访问,则不久之后该数据可能再次被访问。 应用场景:可以把常用的数据加cache来优化访存。 空间局部性:一旦程序访问了某个存储单元,则不久之后,其附近的存储单元也会被访问。我们大部分情况下代码都是顺序执行,数据也是顺序访问的。。

局部性原理可以用来提升代码性能: 让最常见的情况运行的快,程序大部分的运行实际都花在少数核心函数上,而这些函数把大部分时间都花在少量循环上,把注意力放在这些代码上。