操作系统
作者:互联网
1. CPU缓存
- CPU缓存分为3级结构: 寄存器 -> L1缓存(数据缓存 + 指令缓存) -> L2缓存 -> L3共享缓存
- 缓存的最小单位: 缓存行(64kb), 这意味着对于内存连续的数据结构, 一次会将64kb的元素载入数据缓存
- 好处是: 可以用来提升缓存命中率, 比如二维数组的行优先好于列优先遍历, 比如将同种类型的运算符聚类, 以充分利用指令缓存加速分支预测器
- 坏处是: 造成伪共享, 降低CAS的效率, 可以使用padding消除
- 对于多核CPU, 由于L1L2缓存不能共享, 因此也造成多线程被调度算法分到不同核上, 缓存失效的问题
- 正如DB会在合适的时间将内存页刷回磁盘一样, CPU也会将缓存刷回内存
- 写直达: 实时双写, 保证CPU缓存和内存的强一致性, 影响性能
- 写回: 保证最终一致性, 缓存命中时标记为脏行但不刷内存, 缓存不命中且缓存行为脏才刷内存
- 那么在不一致的窗口, 若其他CPU核上的线程读取内存脏数据, 就造成不一致
- 基于总线嗅探的MESI协议: 对所有CPU核, 保证缓存更改的可见性和有序性
2. 硬中断 vs 软中断
- 中断处理是内核的一个功能, 负责处理南向硬件的请求, 与文件, 网络, 内存管理等平级
- 由于中断会阻塞用户进程, 因此有两种方法减小影响:
- 中断屏蔽: 中断可以嵌套, 这样切换上下文次数多了影响效率, 中断屏蔽指令可以让中断不被抢占, 减少中断总时间
- 中断拆分: 类似标记脏数据思想, 先被动接收中断, 但不立即处理, 然后由内核挑时间延迟集中处理
3. 巨内核 vs 微内核
- 内核是对硬件指令的统一封装
- linux是巨内核, 这意味着所有封装动作都在内核态完成
- 缺点是封装内部的各功能耦合度高, 可移植性差, 优点是核内inline调用无需频繁切换, 性能高
- 微内核只在内核态封装最基本的调度, 虚拟内存, 中断等硬件功能, 其他功能如驱动, 文件系统全部交给用户态实现
- 优点是可移植性高, 自己在用户态实现的功能可以解耦, 缺点是用户请求某些硬件功能时需频繁切换到内核态, 性能差
- 类比TCP的强治理 vs UDP的弱治理
4. 虚拟内存
-
通过内存管理单元MMU, 将不同进程的物理内存空间做屏蔽
-
分段
- 会产生段外碎片和段内碎片
- 段外碎片: 需要将零散的段放到硬盘swap区, 再写回内存, 类似GC的复制算法
- 由于需要IO与硬盘交互, 速度很慢, 产生抖动
- 段内碎片无法避免
- 会产生段外碎片和段内碎片
-
分页
- 没有外部碎片, 内部碎片不超过单页大小
- 额外需要MMU管理页表, 页表占一定空间
- 根据局部性原理, 使用多级页表动态加载需要的页
-
添加快表做缓存
-
段页式
- 在按进程逻辑块分段的基础上, 将每个段分页
- 在按进程逻辑块分段的基础上, 将每个段分页
5. 页面置换算法
- 在缺页时, 假如第4步无法找到内存空闲页, 就需要换出脏页
- 目标: 减少总体置换次数
- 最佳置换, 未来最少使用
- FIFO
- LRU, 最久未使用
- LFU, 最少使用
- 时钟, 沿途访问过的标为未访问, 直到遇到未访问的
6. 进程 vs 线程
-
单核CPU通过中断实现进程切换, 从而达到并发效果
-
多核CPU并行执行
-
挂起: 大量处于阻塞或就绪状态的进程会占据内存空间, 因此将其换出到磁盘
-
使用链表实现阻塞队列, 就绪队列, 单元是PCB
线程之于进程, 就好比容器之于虚拟机, Runnable之于Thread:
- 进程是资源(包括内存、打开的⽂件等)分配的单位,线程是 CPU 调度的单位;
- 进程拥有⼀个完整的资源平台,⽽线程只独享必不可少的资源,如寄存器和栈;
- 线程同样具有就绪、阻塞、执⾏三种基本状态,同样具有状态之间的转换关系;
- 线程能减少并发执⾏的时间和空间开销;
- 线程的创建时间⽐进程快,因为进程在创建的过程中,还需要资源管理信息,⽐如内存管理信息、⽂件管理信息,⽽线程在创建的过程中,不会涉及这些资源管理信息,⽽是共享它们;
- 线程的终⽌时间⽐进程快,因为线程释放的资源相⽐进程少很多;
- 同⼀个进程内的线程切换⽐进程切换快,因为线程具有相同的地址空间(虚拟内存共享),这意味着
- 同⼀个进程的线程都具有同⼀个⻚表,那么在切换的时候不需要切换⻚表。⽽对于进程之间的切换,切换的时候要把⻚表给切换掉,⽽⻚表的切换过程开销是⽐较⼤的;
- 由于同⼀进程的各线程间共享内存和⽂件资源,那么在线程之间数据传递的时候,就不需要经过内核了,这就使得线程之间的数据交互效率更⾼了
进程1, 4: 内核线程
进程2: 用户线程
进程3: 轻量级线程
进程5: 混合线程
7. 僵尸进程 vs 孤儿进程 vs 守护进程
- 正常情况下, 子进程结束后, 其大部分资源会被内核回收, 只留下现场信息(PID, timestamp, status)用于父进程调用wait()时获取子进程状态
- 僵尸进程: 父进程未调用wait(), 子进程的现场信息删不掉
- 解决方法, 杀掉父进程, 让init统一处理
- 孤儿进程: 父进程结束时, 调用wait()发现子进程还在运行, 那么父进程通知init进程去回收子进程的资源, 然后挂掉. 在init收到通知并杀死子进程前的时间里, 称为孤儿进程
- 守护进程: 开机就运行, 一直在跑, 杀不死(杀死后立即重生)
8. 进程线程调度算法
-
FIFO
-
SJF
-
高响应比优先
-
RR
-
高优先级
-
多级反馈队列 = RR + 高优先级
9. 磁盘调度算法
- 先来先服务 = FIFO
- 最短寻道时间 = SJF
- 扫描/循环扫描/LOOK/循环LOOK = RR
10. 进程通信
FIFO管道
- 父进程创建管道 -> fork -> 父子进程间通信
- shell是所有进程的父进程
消息队列
共享内存
- 虚拟内存技术将不同进程的内存空间隔离
- 共享内存则共享内存空间
- 比如static变量, 临界资源
信号
- kill -l
信号量
- P消费, V生产
Socket
- 分TCP/UDP
11. 单点锁模型
- 生产消费者
- 哲学家进餐
- 读写者
12. 死锁
- 互斥访问
- 不可剥夺
- 持有等待(当线程被阻塞时, 不会释放持有的临界资源, 即使该线程现在不使用)
- 循环等待
13. 阻塞 vs 非阻塞
- 以上两种都是同步式调用, 因为最后的拷贝阶段(内核缓冲区->用户进程缓冲区)需要用户进程阻塞, 区别仅在于内核准备数据阶段(磁盘->内核缓冲区)是阻塞还是轮询
14. I/O 多路复⽤
- 上边仍然是同步式调用, 因为最后的拷贝阶段需要用户进程阻塞
- 真正的异步式调用, 用户进程将最后的拷贝阶段主动权让给内核, 由内核拷贝完后通知, 用户进程在全部时间无阻塞
15. 直接内存访问DMA
-
传统IO, 无DMA, CPU需承担数据桥的拷贝工作, 拷贝期间CPU一直被占用
-
DMA, 将数据桥的角色分离出来, 但将数据从磁盘⾼速缓存拷贝到用户缓冲区仍经过CPU, 因为涉及到上下文切换, 只有CPU有最高权限
-
涉及2次用户态到内核态的上下文切换, 1次DMA拷贝(磁盘 -> 磁盘⾼速缓存), 1次CPU拷贝(磁盘⾼速缓存 -> 用户进程缓冲区)
-
DMA类似MMU, 那么磁盘高速缓存则对应快表, 假如命中, 则只需要1次CPU拷贝
16. 零拷贝
- 业务场景:
- 下载文件, 先从磁盘read到用户进程, 再拷到socket发给客户端, 不在用户进程允许对载入的磁盘里的文件进行再加工, 比如压缩, 转义
- Kafka -> NIO -> Channel.transferTo() -> sendfile
- Nginx
-
共享内存mmap, 减少1次CPU拷贝
-
sendfile -> 减少1次CPU拷贝 + 2次上下文切换
-
若网卡支持SG-DMA, 则能再减少1次CPU拷贝
- 若磁盘高速缓存命中, 则能再减少1次DMA拷贝①
- 但假如传输的是大文件, 那么磁盘高速缓存区很快被占满, 而且命中率很低, 则 ①DMA拷贝 不可回避
- 解决方案: 直接绕过磁盘高速缓存, 重新启用用户缓存取而代之. 同时, 使用异步IO, 让内核拷完后通知用户进程, 减少用户阻塞
- 解决方案: 直接绕过磁盘高速缓存, 重新启用用户缓存取而代之. 同时, 使用异步IO, 让内核拷完后通知用户进程, 减少用户阻塞
-
-
Nginx大小文件传输阈值:
location /path/ {
sendfile on; // 启用零拷贝
aio on; // 启用异步IO
directio 1024m; // 阈值, 小于时用零拷贝
}
17. 多线程socket模型
-
问题在于, 线程池里的子线程与socket队列一一对应, 无法支撑C10K
-
思路: 一个子线程对应多socket, 采用类似并发的方式快速切换, 产生多路复用效果.
- 这要求socket是非阻塞的, 否则在read -> 业务处理 -> socket send时一旦发生阻塞, 线程也阻塞, 无法响应对应的其他socket
-
多路复用框架, 非阻塞同步式: 将所有socket放入内核, 然后等待事件, 最后将产生事件的socket返回给对应单一的用户态子线程处理
- select, 在内核态进行循环轮询, 发生事件后标记该socket为可读/写, 然后将所有socket拷回用户态, 再通过轮询找到被标记socket, 最后开始处理
- 底层数据结构bitMap, 最大1024个socket
- poll, 将底层数据结构改为链表, 数量不受限制
- epoll, 底层数据结构改为红黑树, 同时维护一个链表, 用来记录被触发的socket
- select, 在内核态进行循环轮询, 发生事件后标记该socket为可读/写, 然后将所有socket拷回用户态, 再通过轮询找到被标记socket, 最后开始处理
-
多路复用框架, 非阻塞异步式: Windows下的IOCP
18. Reactor/Dispatcher vs Proactor
- Reactor/Dispatcher即对非阻塞同步式IO进行封装
-
reactor负责监听和分发事件
-
单线程/线程池负责处理事件
-
单Reactor + 单线程
- handler负责处理事件
- Redis采用此方案
-
单Reactor + 线程池<Handler>
- handler负责转发事件到processor子线程
-
单Reactor即负责北向监听, 又负责南向Acceptor建立连接, 还要负责将处理结果返回给客户端, 易单点瓶颈, 因此提出多Reactor + 单线程
- 主Reactor只负责监听和建立连接, 处理结果返回交给各个子Reactor
- Netty, Memcache, Nginx(子Reactor建立连接)
-
- Proactor
标签:缓存,操作系统,线程,内核,进程,CPU,socket 来源: https://www.cnblogs.com/rellik96/p/16645107.html