操作系统
1、进程和线程的区别
进程:是资源分配的最小单位,是程序的执行过程,一个进程可以有多个线程,多个线程共享进程的堆和方法区资源,但每个线程又有自己的虚拟机栈,本地方法栈,程序计数器
线程:是任务调度和执行的最小单位,线程间可能存在相互影响,执行开销较小,不利于资源的管理和保护,线程间是共享进程中的资源的。
2、协程
是一种比线程更加轻量级的存在,一个线程可以有多个协程。
3、进程间通信方式IPC
参考:https://www.jianshu.com/p/c1015f5ffa74
- 匿名管道
- 命名管道
- 信号
- 消息队列
- 共享内存
- 信号量
- 套接字
匿名管道pipe:
匿名管道是半双工的,数据只能单向通信;需要双方通信时,需要建立起两个管道;只能用于父子进程或者兄弟进程之间(具有亲缘关系的进程)。
命名管道FIFO:
不同于匿名管道之处在于它提供一个路径名与之关联,以FIFO的文件形式存在于文件系统中。这样,即使与FIFO的创建进程不存在亲缘关系的进程,只要可以访问该路径,就能够彼此通过FIFO相互通信(能够访问该路径的进程以及FIFO的创建进程之间),因此,通过FIFO不相关的进程也能交换数据。值得注意的是,FIFO严格遵循先进先出(first in first out),对管道及FIFO的读总是从开始处返回数据,对它们的写则把数据添加到末尾。
信号:
信号是一种比较复杂的通信方式,信号产生的条件:按键、硬件异常、进程调用kill函数将信号发送给另一个进程、用户调用kill命令将信号发送给其他进程,信号传递的消息比较少,主要用于通知接收进程某个事件已经发生。
消息队列:
消息队列是消息的链表,存放在内核中并由消息队列标识符标识,消息队列克服了信号传递信息少,管道只能承载无格式字节流以及缓冲区大小受限等特点。消息队列起信箱作用,到了就挂在那里,需要的时候去取。消息队列提供了一种在两个不相关进程间传递数据的简单有效的方法。与命名管道相比:消息队列的优势在于,它独立于发送和接收进程而存在,这消除了在同步命名管道的打开和关闭时可能产生的一些困难。消息队列提供了一种从一个进程向另一个进程发送一个数据块的方法。而且,每个数据块被认为含有一个类型,接收进程可以独立地接收含有不同类型值的数据块。
优点:
A. 我们可以通过发送消息来几乎完全避免命名管道的同步和阻塞问题。
B. 我们可以用一些方法来提前查看紧急消息。
缺点:
A. 与管道一样,每个数据块有一个最大长度的限制。
B. 系统中所有队列所包含的全部数据块的总长度也有一个上限。
共享内存(share memory):
使得多个进程可以可以直接读写同一块内存空间,是最快的可用IPC形式。是针对其他通信机制运行效率较低而设计的。
为了在多个进程间交换信息,内核专门留出了一块内存区,可以由需要访问的进程将其映射到自己的私有地址空间。进程就可以直接读写这一块内存而不需要进行数据的拷贝,从而大大提高效率。
由于多个进程共享一段内存,因此需要依靠某种同步机制(如信号量)来达到进程间的同步及互斥。
信号量(Semaphores) :
信号量是⼀个计数器,⽤于多进程对共享数据的访问,信号量的意图在于进程间同步。这种通信⽅式主要⽤于解决与同步相关的问题并避免竞争条件。
套接字(Sockets) :
此⽅法主要⽤于在客户端和服务器之间通过⽹络进⾏通信。套接字是⽀持TCP/IP 的⽹络通信的基本操作单元,可以看做是不同主机之间的进程进⾏双向通信的端点,简单的说就是通信的两⽅的⼀种约定,⽤套接字中的相关函数来完成通信过程。
4、用户态和核心态
在计算机系统中,分两种程序:系统程序和应用程序,为了保证系统程序不被应用程序有意或无意地破坏,为计算机设置了两种状态——用户态、核心态
用户态:只能受限的访问内存,运行所有的应用程序
核心态:运行操作系统程序,CPU可以访问内存的所有数据,包括外围设备
为什么要有用户态和内核态:
限制不同程序之间的访问能力,防止他们获取别的程序的内存数据,或者外围设备的数据,并发送到网络。
用户态切换到内核态的3种方式:
a、系统调用
这是用户态进程主动要求切换到内核态的一种方式,用户态进程通过系统调用申请使用操作系统提供的服务程序完成工作,比如前例的fork()实际上就是执行了一个创建新进程的系统调用。而系统调用的机制其核心还是使用了操作系统为用户特别开放的一个中断来实现,例如Linux的int 80h中断。
b、异常
当CPU在执行运行在用户态下的程序时,发生了某些事先不可知的异常,这时会触发由当前运行进程切换到处理此异常的内核相关程序中,也就 转到了内核态,比如缺页异常。
c、外围设备的中断
当外围设备完成用户请求的操作后,会向CPU发出相应的中断信号,这时CPU会暂停执行下一条即将要执行的指令转而去执行与中断信号对应的处理程序,如果先前执行的指令是用户态下的程序,那么这个转换的过程自然也就发生了由用户态到内核态的切换。比如硬盘读写操作完成,系统会切换到硬盘读写的中断处理程序中执行后续操作等。
这3种方式是系统在运行时由用户态转到内核态的最主要方式,其中系统调用可以认为是用户进程主动发起的,异常和外围设备中断则是被动的。
5、操作系统分配的进程空间是怎样的?线程能共享哪些?
栈区:由编译器自动分配释放,存放函数的参数值,局部变量的值等
堆区:一般由程序员分配释放,若程序员不释放,程序结束时可能由OS回收静静态区:存放全局变量和静态变量
代码区:存放函数体的二进制代码
线程共享堆区、静态区
6、操作系统内存管理方式,分页分段以及段页式的优缺点
内存管理方式:块式管理、页式管理、段式管理、段页式管理
分段管理:
在段式存储管理中,将程序的地址空间划分为若干段(segment),如代码段,数据段,堆栈段;这样每个进程有一个二维地址空间,相互独立,互不干扰。段式管理的优点是:没有内碎片(因为段大小可变,改变段大小来消除内碎片)。但段换入换出时,会产生外碎片(比如4k的段换5k的段,会产生1k的外碎片)
分页管理:
在页式存储管理中,将程序的逻辑地址划分为固定大小的页(page),而物理内存划分为同样大小的页框,程序加载时,可以将任意一页放入内存中任意一个页框,这些页框不必连续,从而实现了离散分离。页式存储管理的优点是:没有外碎片(因为页的大小固定),但会产生内碎片(一个页可能填充不满)
段页式管理:
段⻚式管理机制结合了段式管理和⻚式管理的优点。简单来说段⻚式管理机制就是把主存先分成若⼲ 段,每个段⼜分成若⼲⻚,也就是说 段⻚式管理机制 中段与段之间以及段的内部的都是离散的。
7、页面置换算法有哪些,FIFO为什么不好?如何改进?LRU思想,手写LRU
置换算法:先进先出FIFO,最近最久未使用LRU,最佳置换算法OPT
先进先出FIFO:
- 原理:把内存中驻留时间最久的页面置换算法予以淘汰
- 优点:实现简单、直观
- 缺点:没有考虑到实际的页面使用频率,性能差、与通常页面使用的规则不符合,实际应用较少
- 改进:给每个页面增加一个R位,每次先从链表头开始查找,如果R置位,清除R位并且把该页面节点放 到链表结尾;如果R是0,那么就是又老又没用到,替换掉。
最近最久未使用LRU: - 原理:选择最近且最久未使用的页面进行淘汰
- 优点:考虑到了程序访问的时间局部性,有较好的性能,实际应用也比较多
- 缺点:实现需要比较多的硬件支持,会增加一些硬件成本
最佳置换算法OPT: - 原理:每次选择当前物理块中的页面在未来长时间不被访问的或未来不再使用的页面进行淘汰
- 优点:具有较好的性能,可以保证获得最低的缺页率
- 缺点:过于理想化,但是实际上无法实现(没办法预知未来的页面)
LRU算法实现:
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.Set;
/**
* @author :lzh
* @createTime :2021/8/30 10:01
*/
public class LRUcache {
private LinkedHashMap<Integer,Integer> map;
private int capacity;
public LRUcache(int capacity){
map=new LinkedHashMap<>(capacity);
this.capacity=capacity;
}
public int get(int key){
if(!map.containsKey(key)){
return -1;
}
int x = map.get(key);
map.remove(key);
map.put(key,x);
return x;
}
public void set(int key,int value){
if(map.containsKey(key)){
map.remove(key);
}
if(capacity==map.size()){
Set<Integer> set = map.keySet();
Iterator<Integer> iterator = set.iterator();
map.remove(iterator.next());
}
map.put(key,value);
}
}
通过LinkedHashMap内部removeEldestEntry方法实现
class LRUCache {
private Map<Integer, Integer> map;
private int capacity;
/**
*初始化构造函数
* @param capacity
*/
public LRUCache(int capacity) {
this.capacity = capacity;
map = new LinkedHashMap<Integer, Integer>(capacity, 0.75f, true) {
@Override
protected boolean removeEldestEntry(Map.Entry eldest) {
return size() > capacity; // 容量大于capacity 时就删除
}
};
}
public int get(int key) {
//返回key对应的value值,若不存在,返回-1
return map.getOrDefault(key, -1);
}
public void put(int key, int value) {
map.put(key, value);
}
}
8、死锁条件,解决方式。
死锁是指两个或两个以上进程在执行过程中,因争夺资源而造成的相互等待的现象。
死锁条件:
- 互斥条件:进程所分配到的资源不允许其它进程访问,若其它进程访问该资源,只能等待,直至占有该资源的进程释放该资源。
- 请求与保持条件:进程获得一定的资源后,又对其它资源发出请求,但是该资源可能被其它资源占有,此时请求阻塞,但改进程不会释放自己已经占有的资源
- 非剥夺条件:进程已获得的资源,在未完成使用之前,不可被剥夺,只能在使用后自己释放
- 循环等待条件:系统中若干进程组成环路,环路中每个进程都在等待相邻进程占有的资源。
解决方法:破坏死锁的任一条件
- 资源一次性分配,从而剥夺请求和保持条件
- 可剥夺资源:即当进程心得资源未得到满足时,释放已占有的资源,从而破坏不可剥夺的条件
- 资源有序分配法:系统给每类资源赋予一个序号,每个进程按编号递增的请求资源,释放则相反,破坏了环路等待的条件