软件架构设计-操作系统

2021-10-04  本文已影响0人  孤独的死锁

操作系统

直接IO与缓冲IO

内存映射文件mmap与sendfile

内存映射文件mmap:用户空间不再有物理内存,直接拿应用程序的逻辑内存地址映射到了linux操作系统的内核缓冲区,应用程序虽然读写的是自己的内存,但这个内存只是一个”逻辑地址“,实际读写的是内核缓冲区。在Java中为MappedByteBuffer。注意,拷贝是把数据从一块内存中复制到另外一块内存里,映射相当于只是持有了数据的一个引用(或者叫地址),数据本身只有1份。
sendfile:直接映射内核缓冲区和socket缓冲区,数据无需在内核层进行拷贝。在java中对应的API为FileChannel.transferTo。

网络IO模型

image.png

阻塞和非阻塞是从函数调用角度来说的,而同步和异步是从”读写是谁完成的“角度来说的。
阻塞:如果读写没有就绪或者读写没有完成,则该函数一直等待。
非阻塞:函数立即返回,然后让应用程序轮询。
同步:读写由应用程序完成。
异步:读写由操作系统完成,完成之后,回调或者事件通知应用程序。
异步io一定是非阻塞io。

reactor模式与proactor模式

reactor模式:基于多路复用io产生的模式。主动模式。
proactor:基于异步io产生的模式。被动模式。

epoll的LT、ET

水平触发:又称为条件触发,读缓冲区只要不为空,就会一直触发读事件;写缓冲区只要不满,就会一直触发写事件。要避免”写的死循环“,写缓冲区为满的概率很小,即”写的条件“为一直满足,所以当用户注册了写事件却没有数据要写时,它会一直触发,因此在LT模式下写完数据一定要取消写事件。
边缘触发:读缓冲区从空转为非空的时候触发一次;写缓冲区的状态,从满转为非满的时候触发一次。要避免”short read“问题,例如用户收到了100个字节,它触发1次,但用户只读到了50个字节,剩下的50个字节不读,它也不会再次触发。因此在ET模式下,一定要把”读缓冲区“的数据一次性读完。
在实际开发中,大家一般都倾向于用LT,这也是默认的模式。Java NIO用的也是epoll的LT模式。因为ET容易漏事件,一次触发如果没有处理好,就没有第二次寄回来。虽然LT触发可能有少许的性能损耗,但代码写起来更安全。

服务器的1+n+m模型

image.png

监听线程:负责accept事件的注册和处理。和每一个新进来的客户端建立socket连接,然后把socket连接移交给IO线程,完成任务,继续监听新的客户端。
IO线程:负责每个socket连接上面read、write事件的注册和实际的socket的读写。把读到的request放入request队列,交由worker线程处理。
Worker线程:纯粹的业务线程,没有socket读写操作。对request队列进行处理,生成Response队列,由IO线程再回复给客户端。

进程、线程和协程


image.png

现代的编程语言像Go、Rust,原生就有协程的支持,但偏传统的Java、C++等语言没有原生支持。因此产生了一些第三方的方案,比如Java的Quasar Fiber、微信团队为C++研发的libco等,但普及程度还比较低,开发者还是习惯多线程的开发模型。

内存屏障

从用法来讲,内存屏障是在两行代码之间插入一个栅栏。基于内存屏障,有了Java中的volatile关键字,再加上但线程写的原则,就有了java中的无锁并发框架---Disruptor。其核心就是”一写多读,完全无锁“。

CAS

如果是多线程写,则内存屏障也不够用了,这时要用到CAS。CAS是在CPU层面提供的一个硬件原子指令,实现对同一个值的Compare和Set两个操作的原子化。基于CAS,上层可以实现乐观锁、无锁队列、无锁栈、无锁链表。

上一篇下一篇

猜你喜欢

热点阅读