操作系统
1. 进程和线程有什么区别
- 根本区别
- 进程是操作系统进行资源分配的基本单位,用于执行计算机程序
- 线程是系统进行调度的基本单位,是进程的执行单元
- 切换开销
- 进程有独立的数据空间,切换开销大
- 线程共享进程的数据空间,切换开销小
2. 多线程和多进程的区别:优缺点
- 多线程:同一进程的线程共享进程数据
- 优点:内存占用小,cpu利用率高,切换开销小,创建销毁开销小
- 缺点:一个线程挂掉会导致整个进程挂掉,因为线程没有独立的内存地址,同一进程的线程可以相互访问对方的栈空间造成数据读写错误,为了保护同一进程的其他线程,只能将整个进程关掉
- 多进程:进程间数据隔离
- 优点:进程间数据隔离,一个进程挂掉不会影响其他进程
- 缺点:内存占用大,cpu利用率低,切换开销大,创建销毁开销大
3. 进程间通信方式:管道IO、消息队列、共享内存、socket
- 管道IO:半双工通信,有固定的读写端,读写阻塞,不能一边读一边写。有匿名IO和有名IO,匿名IO只能用于有关进程,比如父子进程。有名IO可以用于无关进程
- 消息队列:面向记录,有独特的格式和优先级,进程从消息队列中读取消息后,消息被删除,通信效率比较高,只需要将消息存入队列后就可以返回,写入消息时,将用户态的数据拷贝到内核态,读取消息时,将内核态数据拷贝到用户态
- 共享内存:最快的一种通信方式,解决了消息队列中数据拷贝的开销问题,通过虚拟内存映射出的物理内存来进行读写,进程直接从内存中读取,不需要进行数据的拷贝,多个进程可以同时读取,需要信号量进行同步
- socket:用于不同服务器之间通信,比如跨网络的客户端和服务器
4. 进程调度算法:先来先去、短作业、高响应、时间片轮转、高优先级
- 先来先去服务:选择等待时间最久的进程
- 非抢占式
- 优点:公平
- 缺点:在长进程后的短进程等待时间长
- 短作业优先:选择服务时间最短的进程
- 非抢占式
- 优点:系统响应快
- 缺点:不公平,长作业长时间无法获得调度
- 高响应优先:选择响应速度最快的进程
- 非抢占
- 优点:解决了长作业长时间无法获得调度的问题
- 时间片轮转:轮流让队列的进程获得时间片,时间片用完或者被剥夺就重新回到队列等待下一次调度
- 如果时间片未使用完就发生切换,是抢占式
- 优点:公平,实现简单
- 缺点:频繁切换开销大
- 高优先级:选择优先级最高的进程
- 抢占
- 优点:系统运行速度快
- 缺点:低优先级的进程长时间无法获得调度
5. 进程的三种状态及其切换过程:就绪、运行、阻塞
- 就绪ready:获得资源,等待CPU调度
- 运行running:获得CPU调度,获得时间片
- 阻塞block:时间片被剥夺
6. 死锁产生的原因和四条件:互斥、不可剥夺、请求和保持、循环等待
系统资源不足,无法分配资源
请求和释放资源的顺序不当,导致无法获取资源
互斥条件:进程互相持有对方所需的资源
不可剥夺:资源未使用完不释放
请求和保持:请求资源时不会释放持有的资源
循环等待:无法获取资源时,进入循环等待
四条件是必要条件,也就是说发生死锁,必定满足这四个条件,满足这四个条件,不一定发生死锁
7. 死锁预防:破环不可剥夺、破环请求和保持、破环循环等待
- 破环不可剥夺:先释放资源才能请求
- 破环请求和保持:一次性分配所有资源或者有一个资源就不请求
- 破环循环等待:对资源进行编号,请求时必须按照编号升序请求
8. 虚拟内存换页算法:FIFO、LFU、LRU
FIFO:最先被加载到内存的最先被置换出去
LRU:最少被访问的被置换
LFU:最近一段时间内最少被访问的被置换
OPT:最佳置换算法,不会再被访问的页面被置换,但是这只是一个理论的算法,因为没办法预知这个页面是否真的不会再被访问,目前无法实现
手撕LRU
class LRUCache { /* 分析:访问时,先弹出key,再存入。添加时,如果有,先弹出,再存入,map更新。如果容量为0,先弹出队首,再存入。 */ Map<Integer,Integer> map; Queue<Integer> queue; int capacity; public LRUCache(int capacity) { map = new HashMap<>(); queue = new LinkedList<>(); this.capacity = capacity; } public int get(int key) { if(queue.contains(key)){ queue.remove(key); queue.add(key); return map.get(key); }else{ return -1; } } public void put(int key, int value) { if(queue.contains(key)){ queue.remove(key); queue.add(key); map.put(key,value); }else if(capacity == 0){ map.remove(queue.poll()); queue.add(key); map.put(key,value); }else{ queue.add(key); map.put(key,value); capacity--; } } }
9. 高CPU占用排查
- top查看进程
- top -H -P [PID]查看进程中线程的cpu使用情况
- printf "%x\n" [线程ID]转换成十六进制
- jstack [PID] | grep [线程ID]查看线程
10. 孤儿进程和僵尸进程
- 孤儿进程:父进程退出后,子进程还在运行,子进程变成孤儿进程,被init进程(pid=1)收养,完成后续的资源释放
- 僵尸进程:子进程退出后,父进程没有调用wait或者waitpid释放子进程的资源,子进程pid依旧被占用,因为系统的pid是有限的,一个32767个,会限制进程数量
- 解决僵尸进程:将父进程kill掉,僵尸进程变成孤儿进程,被init进程收养完成资源释放
11. Linux内核IO模型:阻塞IO、非阻塞IO、多路复用IO、信号驱动IO、异步IO
阻塞io:对应Java中的BIO。用户线程向内核发起IO请求,内核查看数据是否就绪,如果数据未就绪,阻塞线程,用户线程交出CPU时间片。当数据就绪后,内核将数据拷贝到用户线程,用户线程解除阻塞状态并处理数据
非阻塞io:用户线程向内核发起IO请求,内核查看数据是否就绪,如果数据未就绪,用户线程可以先去处理其他操作,同时用户线程不断询问内核,当数据就绪后,内核将数据拷贝到用户线程,用户线程处理数据
多路复用io:对应Java中的NIO。可以调用select、poll、epoll函数进行处理,用户线程调用函数向内核发起IO请求,用户线程可以先去执行其他操作,当数据就绪后,内核将数据拷贝到用户线程,函数返回大于0,通知用户线程可以处理数据
信号驱动io:用户线程注册一个信号函数,函数向内核发起IO请求,用户线程可以去执行其他操作,内核查看数据是否就绪,当数据就绪后,内核将数据拷贝到用户线程,信号函数返回一个信号,通知用户线程可以处理数据
异步io:对应Java中的AIO,用户线程启动一个read函数,然后就可以去处理其他操作,内核查看数据是否就绪,当数据就绪后,内核将数据拷贝到用户线程,用户线程只需要注册函数,整个操作由read函数完成
select
- 操作方式:将用户态的fd_set拷贝到内核态,内核态遍历fd_set,时间复杂度On
- 数据结构:数组,1024和2048
- 成功返回大于0,失败返回小于0,超时返回0
- 优:兼容性好,支持多种系统
- 缺:效率低,需要遍历fd_set,有最大连接上限
poll
- 操作方式:将用户态的fd_set拷贝到内核态,内核态遍历fd_set,时间复杂度On
- 数据结构:链表
- 成功返回大于0,失败返回小于0,超时返回0
- 优:没有最大连接上限
- 缺:效率低
epoll
- 操作方式:事件触发,当fd就绪后,通知进程进行处理,只关心活跃的连接,时间复杂度O1
- 数据结构:红黑树
- 两种模式
- LT水平:当epoll_wait检测到fd触发后,可以等待下一次再次触发再处理
- ET边缘:当epoll_wait检测到fd触发后,必须马上处理
- 优:没有最大连接上限,事件触发,效率高,只关心活跃的连接
- 缺:兼容性差,只能在Linux上使用
12. Java IO模型:同步和异步、BIO、NIO、AIO
- 同步异步、阻塞非阻塞
- 同步:必须等待结果
- 异步:通过回调函数返回结果
- 阻塞:线程在进行io时不能执行其他操作
- 非阻塞:线程在进行io时可以执行其他操作
- BIO:同步阻塞,基于stream流,一个客户端请求对应一个服务器线程
- 传统单线程模式,一个Acceptor线程while true循环调用accept方法监听连接请求,监听和处理都由单个线程完成,线程必须处理完一个请求才能处理下一个请求
- 伪异步多线程模式:使用线程池处理请求
- NIO:同步非阻塞,基于buffer,将监听和处理分开进行,由selector同时监听多个客户端,然后再将事件交给线程进行处理,可以让单个线程高效处理请求
- selector:选择器,同时监听多个channel的状态,判断channel中是否有事件发生
- channel:双向通道,可读可写,不能直接读写客户端事件,必须通过buffer进行读写
- buffer:数据缓冲区,是一块可读可写的内存,用于简化数据的读写
- AIO:异步非阻塞,基于回调函数实现