操作系统
操作系统基础
进程和线程
进程和线程的区别 +5
进程(Process)和线程(Thread)是操作系统中的概念,是计算机中的基本执行单位。它们的主要区别如下:
- 进程是资源分配的最小单位,线程是程序执行的最小单位;
- 每个进程都有独立的内存空间,而线程共享所属进程的内存空间;
- 进程间通信需要使用进程间通信的机制,如管道、消息队列等,而线程可以直接访问所属进程的共享内存空间;
- 进程的创建和销毁都需要操作系统的介入,而线程的创建和销毁可以由所属进程自己控制;
- 进程上下文切换需要保存和恢复进程的所有状态,而线程的上下文切换只需要保存和恢复部分状态。
一个进程中可以有多个线程,多个线程共享进程的堆和方法区 (JDK1.8 之后的元空间)资源,但是每个线程有自己的程序计数器、虚拟机栈 和 本地方法栈。
线程有自己的哪些东西
每个线程拥有自己的栈段和寄存器组。
追问栈里有什么
我理解栈里存的是局部变量
线程是依赖进程的资源的,具体是哪些资源呢?(问的是线程的私有和共享部分吗,我是照这样回答的)
协程和线程、进程的联系?协程了解吗?说一下和线程的区别?
线程是操作系统中最小的调度单位,它可以运行在进程中,并与同一进程中的其他线程共享内存和文件句柄等资源。线程的切换比进程的切换更快,因为不需要切换地址空间。
进程是操作系统中的一个执行单位,它由操作系统负责管理。每个进程有自己独立的地址空间和文件句柄等资源,进程之间不能直接共享内存和文件句柄等资源。进程的切换比线程的切换更慢,因为需要切换地址空间。
协程是一种轻量级的用户态线程,由程序员自己实现调度。协程的切换是在用户态下完成的,因此比线程的切换更快。协程本质上是在一个线程内部实现多个执行流,因此可以共享该线程的资源。
在实现上,协程通常通过“协作式调度”来实现,即一个协程在执行时,需要主动让出 CPU 的控制权,让其他协程来执行。而线程是“抢占式调度”,即操作系统会在一定的时间片后自动把 CPU 的控制权交给其他线程。
总之,协程和线程、进程一样,都是并发编程的重要概念,但是在实现和应用上有一些不同点。
进程有哪几种状态? 线程的状态及其转化 +2线程有哪些状态 内核级线程的线程生命周期:线程的创建和销毁的过程:
- 创建状态(new) :进程正在被创建,尚未到就绪状态。
- 就绪状态(ready) :进程已处于准备运行状态,即进程获得了除了处理器之外的一切所需资源,一旦得到处理器资源(处理器分配的时间片)即可运行。
- 运行状态(running) :进程正在处理器上上运行(单核 CPU 下任意时刻只有一个进程处于运行状态)。
- 阻塞状态(waiting) :又称为等待状态,进程正在等待某一事件而暂停运行如等待某资源为可用或等待 IO 操作完成。即使处理器空闲,该进程也不能运行。
- 结束状态(terminated) :进程正在从系统中消失。可能是进程正常结束或其他原因中断退出运行。
用户态和内核态?为什么分为用户态和内核态?
用户态是指应用程序在执行时所处的环境,包括应用程序代码、数据以及栈等。在用户态下,应用程序可以访问进程空间内的数据、执行指令,但不能直接访问操作系统的资源,如硬件设备、内核代码、驱动程序等。所有的系统调用都是通过陷入内核来实现的。
内核态是指操作系统内核执行的环境,拥有最高的权限,可以直接访问系统的硬件资源和内存,执行敏感操作,如读写硬盘、修改内存等。内核态运行的代码通常由操作系统自身提供,也可以由驱动程序、内核扩展或其他内核模块提供。
分为用户态和内核态的原因是为了保证操作系统的安全性和稳定性。通过限制用户态程序对系统资源的直接访问,防止非法访问和破坏操作系统的稳定性。同时,操作系统内核拥有最高的权限,可以直接访问和控制系统资源,保证系统的正确运行。
有什么方法暂停一个线程 +1
造成线程不安全的原因
锁的诞生解决什么问题:
锁的诞生主要是为了解决并发访问共享资源时可能出现的数据竞争和不一致性问题。
在并发编程中,多个线程同时对同一个共享资源进行读写操作,如果没有控制好并发访问,可能会导致数据不一致的情况出现,即一个线程正在修改共享资源时,另一个线程也在同时访问该资源,导致资源被破坏,数据出现混乱。
锁的引入可以保证同一时刻只有一个线程可以对共享资源进行访问和修改,从而避免了数据竞争和不一致性问题。锁可以通过互斥锁、读写锁、自旋锁等方式实现,不同的锁有不同的适用场景和性能特点。
乐观锁和悲观锁
乐观锁和悲观锁是并发编程中的两个重要概念,用来解决多个线程同时访问共享资源时可能出现的数据不一致的问题。
悲观锁是指在访问共享资源时,认为其他线程可能会修改数据,因此在操作之前先加锁,防止其他线程访问,操作完成后再释放锁。例如,Java 中的 synchronized 和 ReentrantLock 就是悲观锁的实现方式。
相比之下,乐观锁是指在访问共享资源时,认为其他线程不会修改数据,因此不加锁直接进行操作。如果操作完成后发现数据已经被其他线程修改,那么乐观锁会进行回滚或者重试等操作。乐观锁的实现方式比较灵活,可以使用版本号或者时间戳等机制来实现。在 Java 中,乐观锁的实现方式包括 CAS(Compare And Swap)、AtomicInteger 等。
悲观锁适用于写操作比较频繁的场景,因为加锁可以保证数据的一致性,但是可能会导致线程阻塞从而影响性能。乐观锁适用于读操作比较频繁,写操作比较少的场景,因为不需要加锁,不会影响线程的并发性,但是可能需要进行回滚或者重试等操作,影响性能。
操作系统:cpu cache,false sharing,gdb
- CPU Cache:
CPU Cache 是一种快速访问数据的内存,用于缓存 CPU 所需要的数据。它存储了 CPU 运行时经常使用的数据,使得 CPU 可以更快地访问内存。
CPU Cache 通过多级别的缓存来实现,一般分为三级:L1 Cache、L2 Cache 和 L3 Cache。L1 Cache 是 CPU 中最小、最快的缓存,L2 Cache 是次之,L3 Cache 是最大、最慢的缓存。
- False Sharing:
False Sharing,即伪共享,指的是多个线程在访问同一个 CPU Cache 行的不同变量时,由于变量之间共享同一缓存行,会导致频繁地缓存失效,从而影响程序的性能。
False Sharing 通常会在多线程程序中出现,可以通过使用 padding 或将变量放置到不同的缓存行中来避免。
- GDB:
GDB(GNU Debugger)是一种功能强大的调试工具,可以用来监视和调试正在运行的程序。它可以跟踪程序运行时的变量值、寄存器状态等信息,从而帮助开发人员解决程序中的错误。
GDB 可以在命令行界面中使用,也可以通过图形化界面进行使用。在使用 GDB 进行调试时,可以通过设置断点、监视变量、查看程序堆栈等方式来了解程序的执行情况,从而找到程序中的错误。
死锁
死锁描述的是这样一种情况:多个进程/线程同时被阻塞,它们中的一个或者全部都在等待某个资源被释放。由于进程/线程被无限期地阻塞,因此程序不可能正常终止。
死锁是指两个或多个进程或线程在执行过程中,因争夺资源而造成的一种互相等待的现象,若无外力干涉那它们都将无法推进下去,称这种状态为死锁。
死锁必要条件?
- 互斥:资源必须处于非共享模式,即一次只有一个进程可以使用。如果另一进程申请该资源,那么必须等待直到该资源被释放为止。
- 占有并等待:一个进程至少应该占有一个资源,并等待另一资源,而该资源被其他进程所占有。
- 非抢占:资源不能被抢占。只能在持有资源的进程完成任务后,该资源才会被释放。
- 循环等待:有一组等待进程
{P0, P1,..., Pn}
,P0
等待的资源被P1
占有,P1
等待的资源被P2
占有,......,Pn-1
等待的资源被Pn
占有,Pn
等待的资源被P0
占有。
如何解除避免死锁?
产⽣死锁的有四个必要条件:互斥条件、持有并等待条件、不可剥夺条件、环路等待条件。
避免死锁,破坏其中的一个就可以。
消除互斥条件
这个是没法实现,因为很多资源就是只能被一个线程占用,例如锁。
消除请求并持有条件
消除这个条件的办法很简单,就是一个线程一次请求其所需要的所有资源。
消除不可剥夺条件
占用部分资源的线程进一步申请其他资源时,如果申请不到,可以主动释放它占有的资源,这样不可剥夺这个条件就破坏掉了。
消除环路等待条件
可以靠按序申请资源来预防。所谓按序申请,是指资源是有线性顺序的,申请的时候可以先申请资源序号小的,再申请资源序号大的,这样线性化后就不存在环路了。
描述一个死锁情景
假设有两个线程 A 和 B,以及两个共享资源 X 和 Y,它们的使用顺序如下:
- 线程 A 占用资源 X,等待访问资源 Y。
- 线程 B 占用资源 Y,等待访问资源 X。
此时,线程 A 无法继续执行,因为它需要访问资源 Y 才能继续。但是,线程 B 正在占用资源 Y,无法释放,因此线程 A 一直处于等待状态。同样,线程 B 也无法继续执行,因为它需要访问资源 X 才能继续,但是资源 X 正在被线程 A 占用,线程 B 也一直处于等待状态。这就形成了死锁,两个线程互相等待对方释放资源,导致无法继续执行。
cas乐观锁
分布式锁
怎么实现乐观锁(√)
具体说版本号、CAS怎么实现乐观锁(√)
知道哪些锁?(乐观锁悲观锁、独占锁共享锁)
6.悲观锁乐观锁各适用什么场景
场景题:读多写少和写少读多这两种场景分别用什么锁?
内存管理
操作系统的内存管理机制?详细说一下并说明其优缺点?
简单分为连续分配管理方式和非连续分配管理方式这两种。连续分配管理方式是指为一个用户程序分配一个连续的内存空间,常见的如 块式管理 。同样地,非连续分配管理方式允许一个程序使用的内存分布在离散或者说不相邻的内存中,常见的如页式管理 和 段式管理。
-
块式管理 : 远古时代的计算机操作系统的内存管理方式。将内存分为几个固定大小的块,每个块中只包含一个进程。如果程序运行需要内存的话,操作系统就分配给它一块,如果程序运行只需要很小的空间的话,分配的这块内存很大一部分几乎被浪费了。这些在每个块中未被利用的空间,我们称之为碎片。
-
页式管理 :把主存分为大小相等且固定的一页一页的形式,页较小,相比于块式管理的划分粒度更小,提高了内存利用率,减少了碎片。页式管理通过页表对应逻辑地址和物理地址。
-
段式管理 : 页式管理虽然提高了内存利用率,但是页式管理其中的页并无任何实际意义。 段式管理把主存分为一段段的,段是有实际意义的,每个段定义了一组逻辑信息,例如,有主程序段 MAIN、子程序段 X、数据段 D 及栈段 S 等。 段式管理通过段表对应逻辑地址和物理地址。
-
段页式管理机制 。段页式管理机制结合了段式管理和页式管理的优点。简单来说段页式管理机制就是把主存先分成若干段,每个段又分成若干页,也就是说 段页式管理机制 中段与段之间以及段的内部的都是离散的。
什么是内存分段?
程序是由若⼲个逻辑分段组成的,如可由代码分段、数据分段、栈段、堆段组成。不同的段是有不同的属性的,所以就⽤分段(Segmentation)的形式把这些段分离出来。
分段机制下的虚拟地址由两部分组成,段号和段内偏移量。
虚拟地址和物理地址通过段表映射,段表主要包括段号、段的界限
。
什么是内存分页?
分⻚是把整个虚拟和物理内存空间切成⼀段段固定尺⼨的⼤⼩。这样⼀个连续并且尺⼨固定的内存空间,我们叫⻚(Page)。在 Linux 下,每⼀⻚的⼤⼩为 4KB 。
访问分页系统中内存数据需要两次的内存访问 :一次是从内存中访问页表,从中找到指定的物理页号,加上页内偏移得到实际物理地址,第二次就是根据第一次得到的物理地址访问内存取出数据。
多级页表知道吗?
操作系统可能会有非常多进程,如果只是使用简单分页,可能导致的后果就是页表变得非常庞大。
所以,引入了多级页表的解决方案。
所谓的多级页表,就是把我们原来的单级页表再次分页,这里利用了局部性原理
,除了顶级页表,其它级别的页表一来可以在需要的时候才被创建,二来内存紧张的时候还可以被置换到磁盘中。
什么是块表?
同样利用了局部性原理
,即在⼀段时间内,整个程序的执⾏仅限于程序中的某⼀部分。相应地,执⾏所访问的存储空间也局限于某个内存区域。
利⽤这⼀特性,把最常访问的⼏个⻚表项存储到访问速度更快的硬件,于是计算机科学家们,就在 CPU 芯⽚中,加⼊了⼀个专⻔存放程序最常访问的⻚表项的 Cache,这个 Cache 就是 TLB(Translation Lookaside Buffer) ,通常称为⻚表缓存、转址旁路缓存、快表等。
快表(Translation Lookaside Buffer,TLB)是一种高速缓存,用于存储虚拟地址到物理地址的映射关系。在计算机系统中,CPU访问内存时会使用虚拟地址,这些虚拟地址需要转换为物理地址,然后才能访问实际的内存。由于每次内存访问都需要进行地址转换,如果每次都要访问内存中的页表,那么访问的速度将会很慢。因此,快表被用来缓存最近访问过的页表项,以提高地址转换的速度。
快表通常由硬件实现,在 CPU 的芯片上内置了快表。当 CPU 访问内存时,它首先检查快表,看是否有所需的映射关系。如果在快表中找到了映射关系,则直接访问物理地址。如果在快表中没有找到映射关系,则需要访问内存中的页表,找到映射关系并更新快表。由于快表的命中率通常很高,所以它可以显著提高地址转换的速度,从而提高计算机系统的性能。
什么是虚拟内存?
虚拟内存是一种计算机内存管理技术,它使得应用程序认为它拥有连续的可用内存空间,而实际上,这些内存空间可以分散在不同的物理内存中,还可以利用硬盘空间模拟出更多的内存。虚拟内存将内存地址空间分为多个虚拟页面,每个虚拟页面都有一个唯一的地址,应用程序可以将数据存储在这些虚拟页面中,而操作系统负责将虚拟页面映射到物理内存或硬盘空间上,以便应用程序能够访问到它们。虚拟内存使得应用程序能够访问比物理内存更多的内存空间,从而提高了系统的性能和可靠性。
虚拟内存的技术实现
- 请求分页存储管理 :建立在分页管理之上,为了支持虚拟存储器功能而增加了请求调页功能和页面置换功能。请求分页是目前最常用的一种实现虚拟存储器的方法。请求分页存储管理系统中,在作业开始运行之前,仅装入当前要执行的部分段即可运行。假如在作业运行的过程中发现要访问的页面不在内存,则由处理器通知操作系统按照对应的页面置换算法将相应的页面调入到主存,同时操作系统也可以将暂时不用的页面置换到外存中。
- 请求分段存储管理 :建立在分段存储管理之上,增加了请求调段功能、分段置换功能。请求分段储存管理方式就如同请求分页储存管理方式一样,在作业开始运行之前,仅装入当前要执行的部分段即可运行;在执行过程中,可使用请求调入中断动态装入要访问但又不在内存的程序段;当内存空间已满,而又需要装入新的段时,根据置换功能适当调出某个段,以便腾出空间而装入新的段。
- 请求段页式存储管理
局部性原理 +1
局部性原理是虚拟内存技术的基础,正是因为程序运行具有局部性原理,才可以只装入部分程序到内存就开始运行。
- 时间局部性 :如果程序中的某条指令一旦执行,不久以后该指令可能再次执行;如果某数据被访问过,不久以后该数据可能再次被访问。产生时间局部性的典型原因,是由于在程序中存在着大量的循环操作。
- 空间局部性 :一旦程序访问了某个存储单元,在不久之后,其附近的存储单元也将被访问,即程序在一段时间内所访问的地址,可能集中在一定的范围之内,这是因为指令通常是顺序存放、顺序执行的,数据也一般是以向量、数组、表等形式簇聚存储的。
时间局部性是通过将近来使用的指令和数据保存到高速缓存存储器中,并使用高速缓存的层次结构实现。空间局部性通常是使用较大的高速缓存,并将预取机制集成到高速缓存控制逻辑中实现。虚拟内存技术实际上就是建立了 “内存一外存”的两级存储器的结构,利用局部性原理实现髙速缓存。
分页机制和分段机制的共同点和区别
-
共同点
- 分页机制和分段机制都是为了提高内存利用率,减少内存碎片。
- 页和段都是离散存储的,所以两者都是离散分配内存的方式。但是,每个页和段中的内存是连续的。
-
区别
- 页的大小是固定的,由操作系统决定;而段的大小不固定,取决于我们当前运行的程序。
- 分页仅仅是为了满足操作系统内存管理的需求,而段是逻辑信息的单位,在程序中可以体现为代码段,数据段,能够更好满足用户的需要。
-
抢占是如何做到的?比如当前执行的中断,中断如何实现的?
-
中断之后,上下文如何保存?恢复之后,上下文如何恢复?
页面置换算法有哪些?
- OPT (Optimal Replacement Algorithm)(最佳页面置换算法) :选择淘汰未来最长时间内不会被访问的页面,但需要未来页面访问情况的预测,该算法无法实现。一般作为衡量其他置换算法的方法。
- FIFO(First In First Out) 页面置换算法(先进先出页面置换算法) : 总是淘汰最先进入内存的页面,即选择在内存中驻留时间最久的页面进行淘汰。
- LRU (Least Recently Used)页面置换算法(最近最久未使用页面置换算法) :LRU 算法赋予每个页面一个访问字段,用来记录一个页面自上次被访问以来所经历的时间 T,当须淘汰一个页面时,选择现有页面中其 T 值最大的,即最近最久未使用的页面予以淘汰。
- LFU (Least Frequently Used)页面置换算法(最少使用页面置换算法) : 该置换算法选择在之前时期使用最少的页面作为淘汰页。
线程创建方式
java线程的创建方式主要有以下几种:
- 继承 Thread 类:通过继承 Thread 类并重写 run() 方法来创建线程。
public class MyThread extends Thread {
@Override
public void run() {
// 线程执行的代码
}
}
MyThread myThread = new MyThread();
myThread.start();
- 实现 Runnable 接口:通过实现 Runnable 接口并重写 run() 方法来创建线程。
public class MyRunnable implements Runnable {
@Override
public void run() {
// 线程执行的代码
}
}
MyRunnable myRunnable = new MyRunnable();
Thread thread = new Thread(myRunnable);
thread.start();
- 实现 Callable 接口:通过实现 Callable 接口并重写 call() 方法来创建线程。
public class MyCallable implements Callable<String> {
@Override
public String call() throws Exception {
// 线程执行的代码
return "线程执行结束";
}
}
MyCallable myCallable = new MyCallable();
FutureTask<String> futureTask = new FutureTask<>(myCallable);
Thread thread = new Thread(futureTask);
thread.start();
- 使用线程池:通过 Executors 工具类提供的方法来创建线程池,然后通过线程池来执行线程任务。
ExecutorService executorService = Executors.newFixedThreadPool(5);
executorService.submit(() -> {
// 线程执行的代码
});
executorService.shutdown();