要讲清楚 IO 这坨东西. 将按如下章节说明。
1. user space & kernel space
2. multiplexing (select , epoll,AIO[reactor & proactor ] )
3. 各种 IO 的名称
4. 结语
user space & kernel space
这两跟 IO 有很密切的关系。所以放在最前面说明, 中文翻译为用户空间与内核空间,啥意思呢?
内核空间:系统管理的内存。用户可以通过 system call 操作内核空间。
用户空间:用户自己写的代码管理的内存。
你把 linux 想像成一个超大的类,它暴露了 api 给你操作它的数据。但你不能直接修改它的数据。
而对 IO 的操作一般为分两步,比方说 Input。
1. 内核将输入放入kernel space。
2. 用户从kernel space。 获得输入数据到 user space。
当然,除非你有非常特殊的原因,你也可以使用 mmap。直接操作 kernel space。
multiplexing
multiplexing 翻成中文技术术语叫多路复用。plex 是堆叠的意思。在 IO 相关的文章里,很常见到。
维基这张图很能说明 multiplexing 这个概念
让 1 个 socket 能够同时为多个 socket 服务,可以理解为 socket 具有 multiplexing 的能力。
怎么实现呢?解决的方法有很多,比如:
1. 你可以为每一个连接开一个线程。但缺点是连接一多,内存压力太大。
2. 不用线程,也可以在非阻塞的 socket 上一直轮询,但缺点是连接一多,cpu 压力太大。
上面两种方法,在不改 linux kernel 的情况下都能达到 multiplexing 目的。比如经典的 tomcat 在很早前就是用 thread 去实现 multiplexing。
而什么可以解决以上的提到的缺点,我们看看 select
select
select 其实是一个函数的名字。但为交流方便,它现在更像是一种 IO 术语了。
select 在 application 与 kernel 之间放了一块空间 。
1. application 将 IO 设备写到这,然后阻塞。 IO 设备的状态也会被清空。
2. kernel 也会在某个 IO 设备可读可写,或出错时,在这块空间上扫一遍,看有没有相同的 IO 设备。有的话将事件写上,并给一个 syscall 让 application 等的这个线程醒来。
3. application醒了也需要再扫一遍这块空间,看哪个 IO 设备有新事件了。
4. 如此循环。。
所以,两边都瞎.都要扫描。
select 最多 FD_SETSIZE 个 fd(fd 是一个数字,当它是一个 key,关联着对应的 value,通常是一个 file). FD_SETSIZE 是一个编译器的常量,是在 glibc/bits/typesizes.h 里定义的,描述了能放多少 个 fd 到 fd_set。在 32位下,FD_SETSIZE 通常是 1024. 我们看它的这块 buffer 的数据结构:
struct fd_set {
int count;
struct fd_record {
int fd;
bool check;
} fds[FD_SETSIZE];
};
看一下 c 里的接口方法:
# nfds: 你要监控的最大的 fd 号。作用是扫描时,可以确定最大值。 从0 到 ndfs,能少则少点。。
# readfds writefds exceptfds: 分别为需要监控的 fd们(读,写,异常)。
# timeout: select 为阻塞函数,timeout可指定阻塞超时时间。
# 返回值为发生事件的 fd值。
int select(int nfds, fd_set *readfds, fd_set *writefds,
fd_set *exceptfds, struct timeval *timeout);
select 缺点很明显:
1. 连接数量支持有限.
2. 连接越多,性能越差.性能为 O(n).
3. 而且每次都得重置这块空间.
4. 需要用户自己读数据。
而 poll 跟 select 很像,解决了 select 的第 1 个缺点(连接数量限制). 其他两个缺点依旧. select/poll 的性能是 O(N),能不能到 O(1)呢?
epoll
要规避 select 的第 2 个缺点,就差一个通知了!某个 IO 设备可读可写或出问题,kernel 你直接通知 application 是哪个 IO 设备不就好了!而且顺带着能优化第 3 个缺点。
AIO
IO 发展滚滚向前。AIO 出现了。 名字酷炫,异步 IO。
因为前面说的 select / poll / epoll 都有一个共同的缺点。
需要用户自己读数据。
而 AIO 可以看起来像是要解决这个问题。
为什么这么说。
现在的 AIO 看起来像是内核的实现,其实不然,它是在 glibc 里实现的, 内核的版本貌似还在开发中。
虽然glibc 并不属于内核,但做为普通用户来说,也只是用 glibc 的接口而已。
AIO 的实现可以有很多种,可以使用信号,可以通过线程,也可以通过回调。
讲 AIO 不能不讲一下 reactor & proactor pattern
reactor & proactor pattern
reactor 和 proactor 它们都是 multiplexing 的架构模型。 意思也就是,要实现 multiplexing 这个功能。有哪种架构。
它们的最大的区别,在于系统是否利用了 AIO。
在上面我们写微缩版的 EventLoop, 就是基于 select 实现的 reactor 模型。
所以 AIO 到底带来了啥好处?以致于可以多一个 multiplexing 模型命名出来。我们复习一下
前面说的 IO 的操作一般为分两步,比方说 Input:
1. 内核将输入放入kernel space。
2. 用户从kernel space。 获得输入数据到 user space。
第 1 步由内核完成。
第 2 步是要占用用户(application)时间的! 如果第 2 步要用很久,select 实现的 reactor 模型虽然可以支持 multiplexing,但一样很慢。但如果是 AIO 机制,第 2 步会也交给内核。是不是就会快很多!内核帮你把数据都写好了,你直接用就行了。
所以 AIO是什么。简单点讲,就是第 2 步由”内核“完成。而不占用 application 响应时间。
前面也说了 aio 现在是 glibc 实现的。最大区别在于它占用的不是 application 的时间。而并不是整机性能就比 select/ poll/ epoll 强多少。但对于 application 来说,那可是性能强了很多。
关于 reactor & proactor pattern 最初的定义可看看这个 Proactor.pdf,这里面作者就有 ACE 的贡献者,靠谱点。拿几张图。
我们先看一下 reactor client 连接的图:
各种 IO 的名称
现在,我们来看一下,因为上面的技术出现的各种花哨的名字!
sync blocking IO
sync non-blocking IO
async blocking IO
async non-blocking IO
我一直觉得这 4 个组合好牵强.对它们的来源有些好奇。查阅网上资料,维基里有解释,然后IBM developerworks也有篇解释,然后 stackoverflow 上也各种解释.百花齐放.然而各自精彩。。这是wiki的