最近在读一本<<软件架构设计:大型网站技术架构与业务融合之道>>,它就像是把你平时一点点积累的知识有条理且有深度的整合。一步一步的将读者断断续续的知识接起来。以下文章是记录书本中的一些知识并加以拓展。
操作系统
对于开发者来说,I/O 是绕不过去的一个基本问题。从文件 I/O 到网络 I/O,存在着各式各样的概念和 I/O 模型,所以这里首先把涉及 I/O 的各种概念和原理厘清。
IO
先了解几个概念;
应用程序内存:是通常写代码用 malloc/free、new/delete 等分配出来的内存。
用户缓冲区:C 语言的 FILE 结构体里面的 buffer。FILE 结构体的定义如下,可以看到里面有定义的 buffer;
内核缓冲区:Linux 操作系统的 Page Cache。为了加快磁盘的 I/O,Linux 系统会把磁盘上的数据以 Page 为单位缓存在操作系统的内存里,这里的 Page 是 Linux 系统定义的一个逻辑概念,一个 Page 一般为 4K。
缓存 I/O (Buffered I/O)/直接IO
缓存 I/O 又被称作标准 I/O,大多数文件系统的默认 I/O 操作都是缓存 I/O。在 Linux 的缓存 I/O 机制中,操作系统会将 I/O 的数据缓存在文件系统的页缓存( page cache )中,也就是说,数据会先被拷贝到操作系统内核的缓冲区中,然后才会从操作系统内核的缓冲区拷贝到应用程序的地址空间。缓存 I/O 有以下这些优点:
- 缓存 I/O 使用了操作系统内核缓冲区,在一定程度上分离了应用程序空间和实际的物理设备。
- 缓存 I/O 可以减少读盘的次数,从而提高性能。
当应用程序尝试读取某块数据的时候,如果这块数据已经存放在了页缓存中,那么这块数据就可以立即返回给应用程序,而不需要经过实际的物理读盘操作。当然,如果数据在应用程序读取之前并未被存放在页缓存中,那么就需要先将数据从磁盘读到页缓存中去。对于写操作来说,应用程序也会将数据先写到页缓存中去,数据是否被立即写到磁盘上去取决于应用程序所采用的写操作机制:如果用户采用的是同步写机制( synchronous writes ), 那么数据会立即被写回到磁盘上,应用程序会一直等到数据被写完为止;如果用户采用的是延迟写机制( deferred writes ),那么应用程序就完全不需要等到数据全部被写回到磁盘,数据只要被写到页缓存中去就可以了。在延迟写机制的情况下,操作系统会定期地将放在页缓存中的数据刷到磁盘上。与异步写机制( asynchronous writes )不同的是,延迟写机制在数据完全写到磁盘上的时候不会通知应用程序,而异步写机制在数据完全写到磁盘上的时候是会返回给应用程序的。所以延迟写机制本身是存在数据丢失的风险的,而异步写机制则不会有这方面的担心。
在缓存 I/O 机制中,DMA 方式可以将数据直接从磁盘读到页缓存中,或者将数据从页缓存直接写回到磁盘上,而不能直接在应用程序地址空间和磁盘之间进行数据传输,这样的话,数据在传输过程中需要在应用程序地址空间和页缓存之间进行多次数据拷贝操作,这些数据拷贝操作所带来的 CPU 以及内存开销是非常大的。
对于某些特殊的应用程序来说,避开操作系统内核缓冲区而直接在应用程序地址空间和磁盘之间传输数据会比使用操作系统内核缓冲区获取更好的性能,下边这一小节中提到的自缓存应用程序就是其中的一种。
标准访问文件的方式
在 Linux 中,这种访问文件的方式是通过两个系统调用实现的:read() 和 write()。当应用程序调用 read() 系统调用读取一块数据的时候,如果该块数据已经在内存中了,那么就直接从内存中读出该数据并返回给应用程序;如果该块数据不在内存中,那么数据会被从磁盘上读到页高缓存中去,然后再从页缓存中拷贝到用户地址空间中去。如果一个进程读取某个文件,那么其他进程就都不可以读取或者更改该文件;对于写数据操作来说,当一个进程调用了 write() 系统调用往某个文件中写数据的时候,数据会先从用户地址空间拷贝到操作系统内核地址空间的页缓存中去,然后才被写到磁盘上。但是对于这种标准的访问文件的方式来说,在数据被写到页缓存中的时候,write() 系统调用就算执行完成,并不会等数据完全写入到磁盘上。Linux 在这里采用的是我们前边提到的延迟写机制( deferred writes )。
同步访问文件的方式
同步访问文件的方式与上边这种标准的访问文件的方式比较类似,这两种方法一个很关键的区别就是:同步访问文件的时候,写数据的操作是在数据完全被写回磁盘上才算完成的;而标准访问文件方式的写数据操作是在数据被写到页高速缓冲存储器中的时候就算执行完成了。
内存映射方式
在很多操作系统包括 Linux 中,内存区域( memory region )是可以跟一个普通的文件或者块设备文件的某一个部分关联起来的,若进程要访问内存页中某个字节的数据,操作系统就会将访问该内存区域的操作转换为相应的访问文件的某个字节的操作。Linux 中提供了系统调用 mmap() 来实现这种文件访问方式。与标准的访问文件的方式相比,内存映射方式可以减少标准访问文件方式中 read() 系统调用所带来的数据拷贝操作,即减少数据在用户地址空间和操作系统内核地址空间之间的拷贝操作。映射通常适用于较大范围,对于相同长度的数据来讲,映射所带来的开销远远低于 CPU 拷贝所带来的开销。当大量数据需要传输的时候,采用内存映射方式去访问文件会获得比较好的效率。
直接 I/O 方式
凡是通过直接 I/O 方式进行数据传输,数据均直接在用户地址空间的缓冲区和磁盘之间直接进行传输,完全不需要页缓存的支持。操作系统层提供的缓存往往会使应用程序在读写数据的时候获得更好的性能,但是对于某些特殊的应用程序,比如说数据库管理系统这类应用,他们更倾向于选择他们自己的缓存机制,因为数据库管理系统往往比操作系统更了解数据库中存放的数据,数据库管理系统可以提供一种更加有效的缓存机制来提高数据库中数据的存取性能。
零拷贝
零拷贝的意思是说不需要将数据从某处复制到特定的某一个区域,可以减少CPU在数据复制的消耗还有内存内存带宽。
它马的发现自己说不清楚,大家去这个网站认真看吧!!
详见:https://juejin.im/post/5d84bd1f6fb9a06b2d780df7
网络IO
IO模型
第一种模型:同步阻塞 I/O。
这种很简单,就是 Linux 系统的 read 和 write 函数,在调用的时候会被阻塞,直到数据读取完成,或者写入成功。
第二种模型:同步非阻塞 I/O。
和同步阻塞 I/O 的 API 是一样的,只是打开 fd 的时候带有 O_NONBLOCK 参数。于是,当调用 read 和 write 函数的时候,如果没有准备好数据,会理解返回,不会阻塞,然后让应用程序不断地去轮询。
第三种模型:I/O 多路复用(IO Multiplexing)。
前面两种 I/O 都只能用于简单的客户端开发。但对于服务器程序来说,需要处理很多的 fd (连接数可以达几十万甚至百万)。如果使用同步阻塞 I/O,要处理这么多的 fd 需要开非常多的线程,每个线程处理一个 fd;如果用同步非阻塞 I/O,要应用程序轮询这么大规模的 fd。这两种办法都不行,所以就有了 I/O 多路复用。
在 Linux 系统中,有三种 I/O 多路复用的办法:select、poll、epoll,
I/O 多路复用是现在 Linux 系统上最成熟的网络 I/O 模型,在三种方式中,epoll 的效率最高,所以目前主流的网络模型都是 epoll。
第四种模型:异步 I/O。
熟悉 Windows 系统开发的人会知道 Windows 系统的 IOCP,这是一种真正意义上的异步 I/O。所谓异步 I/O,是指读写都是由操作系统完成的,然后通过回调函数或者某种其他通信机制通知应用程序。
在 Linux 系统上,也有异步 I/O 的实现,就是 aio。但由于 aio 并不成熟,所以现在主要还是用 epoll。
Reactor 模式与 Preactor 模式
(1)Reactor 模式:主动模式。所谓主动,是指应用程序不断地轮询,询问操作系统或者网络框架、I/O 是否就绪。Linux 系统下的 select、poll、epoll 就属于主动模式,需要应用程序中有一个循环一直轮询;Java 中的 NIO 也属于这种模式。在这种模式下,实际的 I/O 操作还是应用程序执行的。
(2)Proactor 模式:被动模式。应用程序把 read 和 write 函数操作全部交给操作系统或者网络框架,实际的 I/O 操作由操作系统或网络框架完成,之后再回调应用程序。asio 库就是典型的 Proactor 模式。
参考:
Linux 中直接 I/O 机制的介绍
《软件架构设计:大型网站技术架构与业务融合之道》
深入剖析Linux IO原理