Java NIO的核心类库多路复用器Selector就是基于epoll的多路复用技术实现的
相比select、poll系统调用,epoll有如下优点:
1.支持一个进程打开的socket描述符(FD)不受限制,仅受限于操作系统的最大文件句柄数。
select最大的缺陷是单个进程所打开的FD是有一定限制的,它由FD_SETSIZE设置,默认值是1024。可以选择修改这个宏后重新编译内核,但这对带来网络效率的下降。也可以选择多进程的方案(传统的Apache方案)来解决这个问题,但进程的创建成本,以及进程间的数据交换需要进行考虑。
epoll具体支持的FD上线值可以通过cat /proc/sys/fs/file-max查看,这个值和系统的内存关系比较大。
2.I/O效率不会随着FD数目的增加而线性下降。
当拥有一个很大的socket集合时,由于网络延时或者链路空闲,任一时刻只有少部分的socket是“活跃”的,但select/poll每次调用都会线性扫描全部的集合,导致效率线性下降。而epoll只会对活跃的socket进行操作-只有活跃的socket才会主动调用callback函数,所以只需要遍历那些被内核I/O事件异步唤醒而加入Ready队列的FD集合。
3.使用mmap加速内核与用户空间的消息传递
epoll通过内核和用户空间mmap同一块内存来实现的。mmap()系统调用允许存放在块设备上的文件或信息的一部分映射到进程的部分地址空间。
看到这里,或许会产生一个疑问,为什么内核要和用户空间进行消息传递呢?
下面我来进行深入解析:
首先,操作系统必须完成的两个主要目标:
1,与硬件部分交互
2,为运行在计算机系统上的应用程序(即所谓的用户程序)提供执行环境。
而现代操作系统是禁止用户程序直接与底层硬件进行交互的,或者禁止直接访问任意的物理地址。为此,硬件为CPU引入了至少两种不同的执行模式:用户程序的非特权模式和内核的特权模式。Unix把它们分别称为用户态(User Mode)和内核态(Kernel Mode)。所以,每个实际的文件I/O操作必须在内核态下进行。
cpu既可以运行在用户态,也可以运行在内核态。一些CPU可以有两种以上的执行状态,如Intel80x86微处理器有四种不同的执行状态。但是,所有标准的Unix内核都仅仅利用了内核态和用户态。
一个程序执行时,大部分时间都处在用户态,只有需要内核所提供的服务时才切换到内核态。当内核满足了用户程序的请求后,它让程序又回到用户态下。
然后,访问文件的模式有多种:
1,规范模式
规范模式下文件打开后,标志O_SYNC与O_DIRECT清0,而且它的内容是由系统调用read()和write()来存取。系统调用read()将阻塞调用进程,直到数据被拷贝进用户态地址空间。但系统调用write()不同,它在数据被拷贝到页高速缓存(延迟写)后就马上结束。
2,同步模式
同步模式下文件打开后,标志O_SYNC置1。这个标志只影响写操作(读操作总是会阻塞),它将阻塞调用进程,直到数据被有效的写入磁盘。
3,内存映射模式
内存映射模式下文件打开后,应用程序发出系统调用mmap()将文件映射到内存中。因此,文件就称为RAM中的一个字节数组,应用程序就可以直接访问数组元素,而不需用系统调用read()、write()或lseek()。
4,直接I/O模式
直接I/O模式下文件打开后,标志O_DIRECT置1。任何读写操作都将数据在用户态地址空间与磁盘间直接传送而不通过页高速缓存。
5,异步模式
异步模式下,文件的访问可以有两种方法,即通过一组POSIX API或Linux特有的系统调用来实现。所谓的异步模式就是数据的传输请求不阻塞调用进程,而是在后台执行,同时应用程序继续它的正常运行。
说道内存映射,就要谈谈操作系统的内存管理了。
以80X86微处理器为例,我们必须区分以下三种不同的地址:
逻辑地址,包含在机器语言指令中用来指定一个操作数或一条指令的地址。每一个逻辑地址都由一个段(segment)和偏移量(offset)组成。
线性地址,也称虚拟地址,是一个32位无符号整数,可以用来表示高达4GB的地址。
物理地址,用于内存芯片级内存单元寻址。
内存控制单元(MMU)通过一种称为分段单元的硬件电路把一个逻辑地址转换成线性地址;接着,第二个称为分页单元的硬件电路把线性地址转换成一个物理地址。
注:
Linux采用4KB页框大小作为标准的内存分配单元。
内存碎片的产生主要是由于请求内存的大小与分配给它的大小不匹配而造成的。一种典型的解决办法是提供按照几何分布的内存区大小,即内存区大小取决于2的幂而不取决于所存放的数据大小。这样,不管请求内存的大小是多少,都可以保证内存碎片小于50%。
内核使用一种新的资源-线性区,实现了对进程动态内存的推迟分配。当用户态进程请求动态内存时,并没有获得请求的页框,而仅仅获得对一个新的线性地址区间的使用权,而这一线性地址区间就成为进程地址空间的一部分。这一区间叫“线性区”。
进程的地址空间由允许进程使用的全部线性地址组成。内核通过所谓线性区的资源来表示线性地址区间,线性区是有起始线性地址、长度和一些访问权限来描述的。
内存映射,内核给这个进程分配一个新的线性区来映射这个文件。一个新建立的内存映射就是一个不包含任何页的线性区,当进程引用线性区中的一个地址时,缺页异常发生。
与进程地址空间有关的全部信息都包含在一个叫做内存描述符的数据结构中,类型为mm_struct。
Linux通过类型为vm_area_struct的对象实现线性区
进程所拥有的所有线性区是通过一个简单的链表按照内存地址升序链接在一起的,其中mmap字段指向链表中的第一个线性区描述符。当进程中的线性区很多时,Linux2.6使用红黑树来存储内存描述符。
进程的地址空间、它的内存描述符以及线性区链表三者之间的关系,如下图所示:
每个Unix进程都拥有一个特殊的线性区-堆,堆用于满足进程的动态内存请求。
For most operating systems, mapping a file into memory is more expensive than reading or writing a few tens of kilobytes of data via the usual read and write methods. From the standpoint of performance it is generally only worth mapping relatively large files into memory.
JDK1.7升级了NIO类库,升级后的NIO类库被称为NIO2.0。
NIO2.0正式提供了异步文件通道和异步套接字通道的实现。对应于Unix网络编程中的事件驱动I/O - AIO。它不需要通过多路复用器-Selector对注册的通道进行轮询即可实现异步读写,从而简化了NIO的编程模型。