操作系统面经大全——双非上岸阿里巴巴系列

Posted 来老铁干了这碗代码

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了操作系统面经大全——双非上岸阿里巴巴系列相关的知识,希望对你有一定的参考价值。

双非本科,四面成功上岸阿里巴巴,在这里把自己整理的面经分享出来,欢迎大家阅读。


序号文章名超链接
1操作系统面经大全——双非上岸阿里巴巴系列2021最新版面经——>传送门1
2计算机网络面经大全——双非上岸阿里巴巴系列2021最新版面经——>传送门2
3面试阿里,你必须知道的背景知识——双非上岸阿里巴巴系列2021最新版面经——>传送门3

如有疏漏之处,还望各位大佬指出,感激不尽!内容持续更新中…



1. 进程线程篇

1. 进程、线程和协程

进程是对运行时程序的封装,是系统进行资源调度和分配的的基本单位,实现了操作系统的并发;
线程是进程的子任务,是CPU调度和分派的基本单位,用于保证程序的 实时性,实现进程内部的并发;
一个程序至少有一个进程,一个进程至少有一个线程,线程依赖于进程而存在;
进程在执行过程中拥有独立的内存单元,而多个线程共享进程的内存。

协程:相较于线程更加轻量级,不被操作系统内核管理,完全是由程序所控制。

协程在子程序内部是可中断的,转而执行别的子程序,在适当的时候再返回来接着执行。

好处

1、性能提升,不会像线程切换那样消耗资源。

2、不需要多线程锁的机制,因为只有一个线程,不存在同时写变量冲突。因此在协程中控制共享资源不加锁,只需判断状态,因此执行效率比线程高很多。



2. 进程有哪几种状态?

就绪状态:进程已获得除处理机以外的所需资源,等待分配处理机资源;
运行状态:占用处理机资源运行,处于此状态的进程数小于等于CPU数;
阻塞状态: 进程等待某种条件,在条件满足之前无法执行;



3. 进程间的通信的几种方式

管道(pipe)及命名管道(named pipe):管道可用于具有亲缘关系的父子进程间的通信,有名管道除了具有管道所具有的功能外,它还允许无亲缘关系进程间的通信;
信号(signal):信号是一种比较复杂的通信方式,用于通知接收进程某个事件已经发生;
消息队列:消息队列是消息的链接表,它克服了上两种通信方式中信号量有限的缺点,具有写权限得进程可以按照一定得规则向消息队列中添加新信息;对消息队列有读权限得进程则可以从消息队列中读取信息;
共享内存:可以说这是最有用的进程间通信方式。它使得多个进程可以访问同一块内存空间,不同进程可以及时看到对方进程中对共享内存中数据得更新。这种方式需要依靠某种同步操作,如互斥锁和信号量等;
信号量:主要作为进程之间及同一种进程的不同线程之间得同步和互斥手段;
套接字:这是一种更为一般得进程间通信机制,它可用于网络中不同机器之间的进程间通信,应用非常广泛。



4. 线程有几种状态?

  1. 新建(NEW):新创建了一个线程对象。
  2. 可运行(RUNNABLE):线程对象创建后,其他线程(比如main线程)调用了该对象的start()方法。该状态的线程位于可运行线程池中,等待被线程调度选中,获取cpu 的使用权 。
  3. 运行(RUNNING):可运行状态(runnable)的线程获得了cpu 时间片(timeslice) ,执行程序代码。
  4. 阻塞(BLOCKED):阻塞状态是指线程因为某种原因放弃了cpu 使用权,也即让出了cpu timeslice,暂时停止运行。直到线程进入可运行(runnable)状态,才有机会再次获得cpu timeslice 转到运行(running)状态。阻塞的情况分三种:
    (一). 等待阻塞:运行(running)的线程执行o.wait()方法,JVM会把该线程放入等待队列(waitting queue)中。
    (二). 同步阻塞:运行(running)的线程在获取对象的同步锁时,若该同步锁被别的线程占用,则JVM会把该线程放入锁池(lock pool)中。
    (三). 其他阻塞:运行(running)的线程执行Thread.sleep(long ms)或t.join()方法,或者发出了I/O请求时,JVM会把该线程置为阻塞状态。当sleep()状态超时、join()等待线程终止或者超时、或者I/O处理完毕时,线程重新转入可运行(runnable)状态。
  5. 死亡(DEAD):线程run()、main() 方法执行结束,或者因异常退出了run()方法,则该线程结束生命周期。死亡的线程不可再次复生。


5. 创建线程的方式

创建线程有如下四种方式:

  • 继承Thread类

    //定义一个继承Thread类的Runner类
    class Runner extends Thread {
        //定义线程所拥有的方法
        public void run (){
            System.out.println("线程创建完毕");
        }
    }
    
    Runner r = new Runner();
    r.start();
    //如果使用r.run()仅相当于方法的调用
    
  • 实现Runnable接口

    //定义一个实现Runnable接口的类
    class MyThread implements Runnable {
        public void run (){
            System.out.println("创建成功");
        }
    }        
    //创建MyThread的对象,并用这个对象作为Thread类构造器的参数
    MyThread r = new MyThread();
    Thread t = new Thread(r);
    t.start();
    
  • 实现Callable接口

    做法:首先重写Call方法,启动线程时,需要新建一个Callable的实例,再用Future Task实例包装它,最终再包装成Thread实例,调用start方法启动。

    //创建线程的方式:Callable
    class MyThread implements Callable<Integer> {
        @Override
        public Integer call() throws Exception {
            System.out.println(Thread.currentThread().getName()+"\\t coming in");
            TimeUnit.SECONDS.sleep(3l);
            return 1024;
        }
    }
    
    //用Callable的方式创建线程,相比较于Runnable,可以获取返回的值
    MyThread thread = new MyThread();
    //需要一个FutureTask,而且在两个线程里传入同一个futureTask,只会执行一次
    FutureTask<Integer> futureTask = new FutureTask<>(thread);
    new Thread(futureTask,"A").start();
    
  • 使用Executors工具类创建线程池

    1. newFixedThreadPoo:固定大小线程池
    2. newCachedThreadPool:带缓冲线程池
    3. newSingleThreadExecutor:单线程线程池
    4. newScheduledThreadPool:任务调度线程池
    5. ForkJoinPool:分治线程池
// 创建单线程线程池的方法,创建其他线程池同理
public class t {
    public static void main(String[] args) {
        ExecutorService executorService = Executors.newSingleThreadExecutor();
        MyRunnable runnableTest = new MyRunnable();
        for (int i = 0; i < 5; i++) {
            // 每次循环,都创建一个线程
            executorService.execute(runnableTest);
        }
        System.out.println("线程任务开始执行");
        // 当线程池调用该方法时,线程池的状态则立刻变成SHUTDOWN状态。此时,则不能再往线程池中添加任何任务(串行执行)
        executorService.shutdown();
    }

}
class MyRunnable implements Runnable {
    @Override
    public void run() {
        System.out.println("执行中");
    }
}


6. Runnable、Callable、Future的异同

相同点:

  • 都是接口
  • 都可以编写多线程程序
  • 都采用Thread.start()启动线程

不同点

  • Runnable接口run方法无返回值。 Callable有,被线程执行后,可以返回值,这个返回值可以被Future拿到。

    Future表示异步任务,是一个可能还没有完成异步任务的结果。

    因此:Callable用于产生结果,Future用于获取结果

    FutureTask:表示异步运算任务,里面可以传入Callable具体实现类,可以对这个异步运算任务的结果进行等待获取,判断是否已经完成,取消任务等操作。

  • Runnable接口run只能抛出异常时运行,且无法捕获处理;Callable接口call方法允许抛出异常,且可以获取异常信息。



7. run()和start()区别

  • start()方法用于启动线程,run()方法用于执行线程的运行时代码,叫做线程体。

  • run()只是一个线程里的普通函数,可以重复调用,而start()只能调用一次

总结:调用start方法可以启动线程并使线程进入就绪状态,而run方法知识thread的一个普通方法调用,还是在主线程里执行。



8. 守护、用户线程

用户线程:运行在前台,执行具体任务,如程序的主线程,连接网络的子线程等都是用户线程。

守护线程:运行在后台,为其他前台线程服务。一旦所有用户线程都结束运行,守护线程会随JVM一起结束工作。(如垃圾回收线程)



9. 查看线程运行的方法

1. Windows

  • 任务管理器查看or杀死

  • 打开cmd命令行窗口,使用tasklist + taskkill命令杀死进程。具体如下:

    jps
    taskkill /F /PID 33736 (F是强烈杀死)
    

2. linux

  • ps -fe 查看所有进程
    • ID、当前状态、启动时间、占用CPU时间、CPU占用百分比、内存占用百分比、占用虚拟内存的大小、占用常驻内存的大小
  • ps -fT -p <PID> 查看某个进程(PID)的所有线程
  • kill杀死进程
  • top 按大写 H 切换是否显示线程
  • top -H -p <PID>查看某个进程(PID)的所有线程

3. Java

注意:需要到对应的java\\lib下面运行

  • jsp查看所有Java命令

  • jstack<PID>查看某个Java进程(PID)的所有线程状态

  • jconsole查看某个Java进程中线程的运行情况(图形界面)



10. 线程同步的方式

互斥量 Synchronized/Lock:采用互斥对象机制,只有拥有互斥对象的线程才有访问公共资源的权限。因为互斥对象只有一个,所以可以保证公共资源不会被多个线程同时访问
信号量 Semphare:它允许同一时刻多个线程访问同一资源,但是需要控制同一时刻访问此资源的最大线程数量
事件(信号),Wait/Notify:通过通知操作的方式来保持多线程同步,还可以方便的实现多线程优先级的比较操作



11. 什么是上下文切换

多线程编程中一般线程的个数都大于CPU核心的个数。

但一个线程时间片用完后,会重新处于就绪状态并让给其他线程使用,这个过程属于一次上下文切换。

概括:当前任务在执行完CPU时间片切换到另一个任务前会保存自己的状态,以便下一次再切换回这个任务时,可以再加载这个任务的状态。

上下文切换是计算密集型的。耗时。

Linux相比其他操作系统的优点之一是:上下文切换和模式切换的时间消耗很少。



12. 如何减少上下文切换

多线程竞争时,会引起上下文切换。

  • 无锁并发编程:用一些办法来避免使用锁,如将数据的ID按照Hash取模分段,不同线程处理不同段数据。

  • CAS算法:Java的Atomic包使用CAS算法来更新数据,而不需加锁

  • 使用最少线程:避免创建不需要的线程

  • 协程:在单线程里实现多任务的调度,维持多任务间的切换。



13. 线程调度策略

Linux下的线程调度策略:

1、SCHED_OTHER:普通任务调度策略。

2、SCHED_FIFO:实时任务调度策略,先到先服务。一旦占用cpu则一直运行,直到有更高优先级任务到达或自己放弃。

3、SCHED_RR:实时任务调度策略,时间片轮转。当任务的时间片用完,系统将重新分配时间片,并置于就绪队列尾。放在队列尾

Java的线程调度策略:

JVM采用抢占式调度模型。



14. 线程调度器

线程调度器是一个操作系统服务,负责为Runnable状态的线程分配CPU时间,一旦我们创建了一个线程并启动它,它的执行便依赖于线程调度器的实现



15. 什么是死锁?死锁产生的条件?

1). 死锁的概念

在两个或者多个并发进程中,如果每个进程持有某种资源而又等待其它进程释放它或它们现在保持着的资源,在未改变这种状态之前都不能向前推进,称这一组进程产生了死锁。通俗的讲,就是两个或多个进程无限期的阻塞、相互等待的一种状态。

2). 死锁产生

互斥:至少有一个资源必须属于非共享模式,即一次只能被一个进程使用;若其他申请使用该资源,那么申请进程必须等到该资源被释放为止;
占有并等待:一个进程必须占有至少一个资源,并等待另一个资源,而该资源为其他进程所占有;
非抢占:进程不能被抢占,即资源只能被进程在完成任务后自愿释放
循环等待:若干进程之间形成一种头尾相接的环形等待资源关系

3). 死锁的处理

解决死锁的基本方法主要有 预防死锁、避免死锁、检测死锁、解除死锁 等。

(1) 死锁预防

死锁预防的基本思想是 只要确保死锁发生的四个必要条件中至少有一个不成立,就能预防死锁的发生,具体方法包括:
打破互斥条件:允许进程同时访问某些资源。但是,有些资源是不能被多个进程所共享的,这是由资源本身属性所决定的,因此,这种办法通常并无实用价值。
打破占有并等待条件:可以实行资源预先分配策略(进程在运行前一次性向系统申请它所需要的全部资源,若所需全部资源得不到满足,则不分配任何资源,此进程暂不运行;只有当系统能满足当前进程所需的全部资源时,才一次性将所申请资源全部分配给该线程)或者只允许进程在没有占用资源时才可以申请资源(一个进程可申请一些资源并使用它们,但是在当前进程申请更多资源之前,它必须全部释放当前所占有的资源)。但是这种策略也存在一些缺点:在很多情况下,无法预知一个进程执行前所需的全部资源,因为进程是动态执行的,不可预知的;同时,会降低资源利用率,导致降低了进程的并发性。
打破非抢占条件:允许进程强行从占有者哪里夺取某些资源。也就是说,但一个进程占有了一部分资源,在其申请新的资源且得不到满足时,它必须释放所有占有的资源以便让其它线程使用。这种预防死锁的方式实现起来困难,会降低系统性能。
打破循环等待条件:实行资源有序分配策略。对所有资源排序编号,所有进程对资源的请求必须严格按资源序号递增的顺序提出,即只有占用了小号资源才能申请大号资源,这样就不回产生环路,预防死锁的发生。也就是:设置超时锁。利用ReentrantLock的tryLock解决哲学家就餐问题。

(2) 死锁避免

死锁避免的基本思想是动态地检测资源分配状态,以确保循环等待条件不成立,从而确保系统处于安全状态。所谓安全状态是指:如果系统能按某个顺序为每个进程分配资源(不超过其最大值),那么系统状态是安全的,换句话说就是,如果存在一个安全序列,那么系统处于安全状态。资源分配图算法和银行家算法是两种经典的死锁避免的算法,其可以确保系统始终处于安全状态。其中,资源分配图算法应用场景为每种资源类型只有一个实例(申请边,分配边,需求边,不形成环才允许分配),而银行家算法应用于每种资源类型可以有多个实例的场景。

(3) 死锁解除

1. 进程终止

​ 所谓进程终止是指简单地终止一个或多个进程以打破循环等待,包括两种方式:终止所有死锁进程和一次只终止一个进程直到取消死锁循环为止;

2. 资源抢占

指从一个或多个死锁进程那里抢占一个或多个资源,此时必须考虑三个问题:
  (I). 选择一个牺牲品
  (II). 回滚:回滚到安全状态
  (III). 饥饿(在代价因素中加上回滚次数,回滚的越多则越不可能继续被作为牺牲品,避免一个进程总是被回滚)

4). 定位死锁

使用jconsole工具,或使用jps定位进程id,再用jstack定位死锁

如果不知道是哪个进程使造成了死锁,可以优先测试CPU占用高的JAVA线程

1. jps+jstack

jps 						//查看运行的进程
jstack pid				    //查看某进程id下的线程状态

对上述代码执行检查,得:

2. jconsole

一种基于JMX的可视化监视、管理工具。

1. jconsole查看当前进程、线程的全局情况:

在这里插入图片描述

2. 单独查看内存引用和GC情况

在这里插入图片描述

3. 查看进程中的线程情况

在这里插入图片描述

5. 单独查看程序中类的加载和卸载情况

在这里插入图片描述

6. 查看VM的概要情况以及相关运行参数

在这里插入图片描述



16. 进程调度策略?

1. FCFS

先来先服务。先请求CPU的进程先分配到CPU

2. SJF

短作业优先调度。平均等待时间最短,但难以知道下一个CPU区间长度

3. 优先级调度算法

可以是抢占的,也可以是非抢占的

优先级越高越先分配到CPU。

相同优先级先到先服务,存在的主要问题是:低优先级进程无穷等待CPU,会导致无穷阻塞或饥饿;解决方案:老化

4. 时间片轮转调度算法

队列中的进程被分配时间片,时间片到则退出对CPU资源的使用

5. 多级队列调度算法

将就绪队列分成多个独立的队列,每个队列都有自己的调度算法,队列之间采用固定优先级抢占调度。

其中,一个进程根据自身属性被永久地分配到一个队列中。

6. 多级反馈队列调度算法

与多级队列调度算法相比,其允许进程在队列之间移动:若进程使用过多CPU时间,那么它会被转移到更低的优先级队列;在较低优先级队列等待时间过长的进程会被转移到更高优先级队列,以防止饥饿发生。



17、进程同步的机制

1. 进程同步概念

1、临界资源:在系统中有许多硬件和软件资源,如打印机、公共变量等,这些资源在一段时间内只允许一个进程访问或者使用,这种资源称之为临界资源。

2、临界区:作为临界资源,不论硬件临界资源还是软件临界资源,多个并发的进程都必须互斥地访问或者使用,这时候,把每个进程访问临界资源的那段代码称为临界区。

3、进程同步:进程同步是指多个相关进程在执行次序上的协调这些进程相互合作,在一些关键点上需要相互等待或者通信。通过临界区可以协调进程间的合作关系,这就是同步。

4、进程互斥:进程互斥是指当一个程序进入临界区使用临界资源时,另一个进程必须等待。当占用临界资源的进程退出临界区后,另一个进程才被允许使用临界资源。通过临界区可以协调程序间资源共享关系,就是进程互斥。进程互斥是同步的一种特例。


2. 进程同步机制遵循的原则

空闲让进:当无进程处于临界区时,临界区处于空闲状态,可以允许一个请求进入临界区的进程进入临界区,有效地使用临界资源。

忙则等待:当有进程进入自己的临界区时,意味着临界资源正在被访问,因而其他的试图进入临界区的进程必须等待,以保证进程互斥地使用临界资源。

有限等待:对要求访问临界资源的进程,必须保证该进程在有效的时间内进入自己的临界区,以免出现死等的情况。

让权等待:当进程不能进入自己的临界区时,应该立即释放处理器,以免陷入“忙等”


3. 几种进程同步机制

原子操作、信号量机制、自旋锁管程、会合、分布式系统



2. 存储篇

1. 分页和分段有什么区别?

1. 段式存储管理

用户视角。

将程序的地址空间划分为若干段(segment),如代码段,数据段,堆栈段;

这样每个进程有一个二维地址空间,相互独立,互不干扰。

段式管理的优点是:没有内碎片(因为段大小可变,改变段大小来消除内碎片)。但段换入换出时,会产生外碎片(比如4k的段换5k的段,会产生1k的外碎片)


2. 页式存储管理

存储器视角。

将程序的逻辑地址划分为固定大小的页(page),而物理内存划分为同样大小的帧,程序加载时,可以将任意一页放入内存中任意一个帧,这些帧不必连续,从而实现了离散分离。

页式存储管理的优点是:没有外碎片(因为页的大小固定),但会产生内碎片(一个页可能填充不满)。


3. 二者区别

目的不同:分页是系统管理的需要,是信息的物理单位;分段是满足用户的需要,它是信息的逻辑单位,它含有一组其意义相对完整的信息;
大小不同:页的大小固定且由系统决定,而段的长度却不固定,由其所完成的功能决定;
地址空间不同: 段向用户提供二维地址空间;页向用户提供的是一维地址空间;
信息共享:段是信息的逻辑单位,便于存储保护和信息的共享,页的保护和共享受到限制;
内存碎片:页式存储管理的优点是没有外碎片(因为页的大小固定),但会产生内碎片(一个页可能填充不满);而段式管理的优点是没有内碎片(因为段大小可变,改变段大小来消除内碎片)。但段换入换出时,会产生外碎片(比如4k的段换5k的段,会产生1k的外碎片)。



2. 什么是虚拟内存?

1). 内存的发展历程

没有内存抽象(单进程,除去操作系统所用的内存之外,全部给用户程序使用) —>

有内存抽象(多进程,进程独立的地址空间,交换技术,内存大小不可能容纳下所有并发执行的进程) —>

连续内存分配(固定大小分区(多道程序的程度受限),可变分区(首次适应,最佳适应,最差适应),碎片) —>

不连续内存分配(分段,分页,段页式,虚拟内存)


2). 虚拟内存

虚拟内存允许执行进程不必完全在内存中。

基本思想

每个进程拥有独立的地址空间,这个空间被分为大小相等的多个块,称为页(Page),每个页都是一段连续的地址。

这些页被映射到物理内存,但并不是所有的页都必须在内存中才能运行程序。

当程序引用到一部分在物理内存中的地址空间时,由硬件立刻进行必要的映射。

当程序引用到一部分不在物理内存中的地址空间时,由操作系统负责将缺失的部分装入物理内存并重新执行失败的命令。

这样,对于进程而言,逻辑上似乎有很大的内存空间,实际上其中一部分对应物理内存上的一块(称为帧,通常页和帧大小相等),还有一些没加载在内存中的对应在硬盘上

注意

1、请求分页系统、请求分段系统和请求段页式系统都是针对虚拟内存的,通过请求实现内存与外存的信息置换。

2、如果虚拟内存的页并不存在于物理内存中(如图5的3,4),会产生缺页中断,从磁盘中取得缺的页放入内存,如果内存已满,还会根据某种算法将磁盘中的页换出。


3). 页面置换算法

FIFO先进先出算法:在操作系统中经常被用到,比如作业调度(主要实现简单,很容易想到);
LRU(Least recently use)最近最少使用算法:根据使用时间到现在的长短来判断;
LFU(Least frequently use)最少使用次数算法:根据使用次数来判断;
OPT(Optimal replacement)最优置换算法:理论的最优,理论;就是要保证置换出去的是不再被使用的页,或者是在实际内存中最晚使用的算法。


4). 虚拟内存的应用与优点

适合多道程序设计系统,许多程序的片段同时保存在内存中。

当一个程序等待它的一部分读入内存时,可以把CPU交给另一个进程使用。

优点

1、在内存中可以保留多个进程,系统并发度提高
2、解除了用户与内存之间的紧密约束,进程可以比内存的全部空间还大



3. 颠簸

本质:频繁的页调度行为

具体来讲:进程发生缺页中断,这时,必须置换某一页。然而,其他所有的页都在使用,它置换一个页,但又立刻再次需要这个页。因此,会不断产生缺页中断,导致整个系统的效率急剧下降,这种现象称为颠簸(抖动)。
解决策略

  • 如果是因为页面替换策略失误,可以修改替换算法来解决这个问题;
  • 如果是因为运行的程序太多,造成程序无法同时将所有频繁访问的页面调入内存,则要降低多道程序的数量;
  • 否则,还剩下两个办法:终止该进程或增加物理内存容量。


4. 局部性原理

(1). 时间上的局部性:最近被访问的页在不久的将来还会被访问;
(2). 空间上的局部性:内存中被访问的页周围的页也很可能被访问。

以上是关于操作系统面经大全——双非上岸阿里巴巴系列的主要内容,如果未能解决你的问题,请参考以下文章

Java并发编程面经大全——双非上岸阿里巴巴系列

Java虚拟机(JVM)面经大全——双非上岸阿里巴巴系列

硬核!Java集合面经大全——双非上岸阿里巴巴系列

硬核!Java集合面经大全——双非上岸阿里巴巴系列

计算机网络面经大全——双非上岸阿里巴巴系列

Java虚拟机(JVM)面经大全——双非上岸阿里巴巴系列