操作系统
直接IO与缓冲IO
- 缓冲io又称作标准I/O,大多数文件系统的默认IO操作都是缓冲IO。在linux的缓冲IO机制中,数据先从磁盘复制到内核空间的缓冲区,然后从内核空间缓冲区复制到应用程序的地址空间。内核缓冲区即pagecache,一个page一般为4K。
- 直接io是由应用程序直接访问磁盘数据,而不经过内核缓冲区,这样做的目的是减少一次从内核缓冲区到用户程序缓存的数据复制。比如说数据库管理系统这类应用,它们更倾向于选择它们自己的缓存机制,因为数据库管理系统往往比操作系统更了解数据库中存放的数据,数据库管理系统可以提供一种更加有效的缓存机制来提高数据库中数据的存取性能。通常直接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,上层可以实现乐观锁、无锁队列、无锁栈、无锁链表。
网友评论