因为项目需要,接触和使用了Netty,Netty是高性能NIO通信框架,在业界拥有很好的口碑,但知其然不知其所以然。
所以本系列文章将从基础开始学起,深入细致的学习NIO。本文主要是介绍五种I/O模型,概念是枯燥的,不过还是得理解才行。
在网络管理,Linux UNIX很相似.UNIX系统一直被用做高端应用或服务器系统,因此拥有一套完善的网络管理机制和规则, Linux沿用了这些出色的规则,使网络的可配置能力很强,为系统管理提供了极大的灵活性.
通俗一点讲,就是在网络方面Linux和UNIX是非常相似的,网络模型大可借鉴UNIX网络编程中的描述。
这里介绍四个概念,方便五种I/O模型的理解:
1.所有外部设备皆文件
Linux的内核将所有的外部设备都看作是一个文件来操作,对一个文件的读写操作会调用内核提供的系统命令,返回一个file descriptor(fd,文件描述符)。
面对一个socket也会有相应的描述符,成为socketfd(socket描述符),描述符就是一个数字,他指向内核中的一个结构体(文件路径,数据区等一些属性)。
2.recvfrom()函数
ssize_t recvfrom(int sockfd, void *buff, size_t nbytes, int flags, strut sockaddr *from, socklen_t *addrlen);
该函数执行成功,则返回读或写的字节数,如出错则为-1。
*from参数,指向一个将由该函数在返回时填写数据包发送者的协议地址的套接字地址结构,
*addrlen参数,套接字地址结构,并且该结构体中填写的则放在addrlen所指的整数中返回给调用者
通过这两个参数,我们可以知道是谁发送了数据包(udp情况下),或是谁发送了数据包(TCP情况下);
3.应用进程与内核
应用进程就是常规的程序,用户程序,打开任务管理器,在应用分组就可以看到应用进程,如下图所示:
图中红框内的,都是应用进程。所有的应用进程都是运行在用户态中。(用户态的概念直接戳链接),运行时所处空间是用户空间
内核就是操作系统的内核,它的作用是将应用进程与硬件分开。可以这么理解,所有涉及到I/O的操作都直接或者间接的经过内核程序。
如果应用进程可以直接操作硬件,那么一些病毒就会蓄意的对计算机硬件进行破坏,那就不可控制了。这样的机制就保证了系统的安全性。运行时是处于内核态,所处空间是内核空间。
网络传输数据,首先是内核先接收到数据,然后内核将数据拷贝到用户态中供应用进程使用。
请先理解上面的基本概念,接下来将介绍五种传统的I/O模型。
最传统的一种IO模型,即在读写数据过程中会发生阻塞现象。
在应用进程通过内核调用recvfrom()函数,其系统调用直到数据包到达且被复制到应用进程的缓冲区或者发生错误时才会返回,在此期间会一直等待。
这句话太晦涩难懂了,简单点说就是:应用进程通过内核调用recvfrom(),收到数据的话则将数据从内核态复制到用户态,没有收到就一直阻塞。
应用系统还是调用recvfrom,但是他不会阻塞与此,而是不断的去轮询的是否有数据准备好,如果没有准备好,就直接返回一个EWOULDBLOCK错误。
总结:
与同步阻塞I/O相比,如果数据准备好,不会一直阻塞与此,而是直接返回错误,接收到错误之后,就可以干点别的事,这是他的优点。但是缺点也很明显,
任务完成的响应延迟增大了。因为很可能在两次轮询之间,socketfd就处于read状态了,所以导致整体的吞吐量下降了。
I/O多路复用技术的最大优势是系统开销小,系统不必创建进程/线程,也不必维护这些进程/线程。
与同步非阻塞I/O不断轮询不同的是,I/O复用是使用一个线程循环轮询socketfd集合是否处于read状态。
Linux提供select/epoll,进程通过一个或者多个socketfd传递给select或poll系统调用,阻塞在select上,这样select/poll可以侦测到多个socketfd是否处于就绪状态。
select/poll是顺序扫描socketfd是否就绪,而且支持的fd很有限。
Linux还提供了一个epoll系统调用,epoll基于事件驱动方式代替顺序扫描,因此性能更高。当有fd就绪时,立即回调函数rollback。
关于select/poll,epoll
select/poll
该函数允许进程指示内核等待多个事件中的任何事件发生,并且只在有一个或多个事件发生或经历一段时间指定的时间才唤醒它。 举个例子,也就是说进程可以通知内核在socketfd集合{1,2,3}进行侦听,知道socketfd集合中任何一个可读的话,就返回。这个等待的过程是阻塞的,它可以侦听多个,但是侦听的数量是有限的。
看下官方关于epoll的解释
The epoll API performs a similar task to poll: monitoring multiple file descriptors to see if I/O is possible on any of them.
The epoll API can be used either as an edge-triggered or a level-triggered interface and scales well to large numbers of watched file descriptors.
epoll和poll执行类似的任务,监控多个fd,如果多个fd中任何一个有I/O时间,即可及时发现。epoll的API既可以作为边触发,也可以作为水平触发接口和可扩展到大量的监视fd。
epoll是Linux下多路复用IO接口select/poll的增强版本,它能显著提高程序在大量连接中只有少量活跃的情况下的系统CPU利用率.
epoll与select/poll的对比
一个进程能够打开socketfd的限制
select一个进程能够打开的FD是由FD_SETSIZE限制的,默认是2048,可以选择修改宏然后重新编译服务器代码,相关资料表明这样会带来网络效率的下降。
epoll没有打开FD数量的限制,在1GB内存的机器上大约是10万左右,具体数目可以cat /proc/sys/fs/file-max查看,一般来说这个数目和系统内存关系很大。
IO效率不会因为socketfd数量提高而线性下降
select和poll拥有一个很大的socketfd集合和,由于每次都会调用线性扫描全部的集合,带来的后果就是效率线性下降
epoll有着相对更好的解决方案,在很大的socketfd集合中,它只会对活跃的socketfd进行操作。epoll是根据每个fd上面的callback函数实现的,只有活跃的socketfd才会去调用callback函数
mmap加速内核与用户空间的传递
无论是select/poll和epoll,都是需要通过内核将FD消息拷贝到用户空间,拷贝是费时的。epoll通过内核与用户控件mmap同一块内存实现不必要的拷贝,从而加快效率。
总结:
进程通过调用内核中的select/poll/epoll,监听socketfd集合的读写就绪状态,多个socketfd都能在一个线程中交替完成,所谓的复用就是指使用的同一个线程。
I/O复用实际还是同步I/O,归根到底还是应用进程主动向内核查询状态。
I/O多路复用是OS提供的最稳定的IO模型,大部分主流的应用都是基于此种IO模型构建的,比如NodeJS,Netty框架。
首先开启套接字信号驱动I/O功能,并通过系统调用sigaction执行一个信号处理函数,此时系统继续运行,并不会阻塞。
当数据准备就绪时,就为该进程生成一个SIGIO信号,通过信号回调通知,通知应用进程调用recvfrom来读取数据。
一句话简单说:产品经理让你改一个需求,并且让你改好了告诉他,给他看一下,于是你就吭哧吭哧的做了,(产品经理就去忙别的事情了,比如又去改需求了)并且做好了叫了产品经理来看。
用户进程进行aio_read系统调用之后,就去干别的事情了。当socketfd数据准备好之后,内核直接复制数据到用户空间,然后内核向用户进程发送通知,数据准备好了。
总结:
整个I/O过程都是非阻塞的,这个是真正的异步非阻塞。
理解五种I/O模型,有助于理解网络I/O,写出更健壮的代码。
在实际工程项目中,普遍使用I/O复用模型,本章重点介绍了I/O复用模型,Java中的NIO也是基于此。学习I/O模型有助于更好的理解NIO,学习Netty框架。
勿在浮沙筑高楼
参考:
《UNIX网络编程》
《Netty权威指南》