本文说的NIO,是指操作系统层面的NIO,no blocking io,并不是指Java api层面的nio。操作系统并不是一开始就支持nio的,而是从传统的bio blocking io模型演变而来的,文本主要简单的介绍下nio的演变历程
首先,一开始,操作系统只支持bio,基于bio的网络编程格式一般如下,注意,本文的例子都是用伪代码的,但是并不影响我们对io模型的理解,其中fd指的是文件描述符,也可以指的是网络socket
int fd = socket()
bind(fd,8888)
listen(fd)
for(;;) {
//阻塞
int fd1 = accept(fd);
new Thread(() -> {
//阻塞
recv(fd1)
})
}
阻塞IO,主要有两个方面是阻塞的,一个是接受客户端的连接accept,另一个读取连接的数据recv。这种IO模型的优点是,编程简单容易理解,理论上可以开启不限制的线程数,但是缺点也很明显,就是开启过多的线程数,会带来频繁的线程上下文切换,使CPU一直浪费在线程切换上,资源浪费。
为了解决线程太多的问题,操作系统支持了非阻塞IO,也就是接受连接和读取数据都是非阻塞的,此时编程格式变为
int fd = socket()
bind(fd,8888)
listen(fd)
int[] fdArr = new int[]{};
for(;;) {
int fd1 = accept()
if (fd1 > 0) {
fdArr.add(fd1)
}
for(int fd : fdarr) {
read(fd)
}
}
由于接受连接和读取数据都是非阻塞的,因此应用程序需要不断的判断socket是否有连接到来,是否有数据可以读取。这种的优点是,可以用单线程来处理所有客户端的请求,不会像bio那样会产生大量的线程,但是缺点也同样明显,就是程序每次循环都需要调用n次(取决于文件描述符fd的个数)的系统调用,但是实际上有可能每次遍历都没有有效的数据,系统调用需要从用户态转到核心态,一样浪费大量的CPU资源。
为了解决上述的问题,操作系统出现了多路复用器select,利用select,编程格式如下
int fd = socket()
bind(fd,8888)
listen(fd)
int[] fdArr = new int[]{};
fdArr.add(fd)
for(;;) {
int[] selectArr = select(fdArr)
if (selectArr.length > 0) {
for (int fd : selectArr) {
if (fd.accept()) {
fdArr.add(fd.accept)
} else if (fd.isReadable()) {
read(fd)
}
}
}
}
利用select编程,应用程序不必每次循环都调用了N次的系统调用read和一次accept。而是将批量的文件描述符传递给select系统调用,由系统帮助应用程序挑选出那些准备好IO事件的文件描述符,这样,应用程序最终获取到肯定是有数据可以处理的socket。而不是像之前那样,对每个socket都调了一次read,看有没有数据,减少了大量无用的系统调用。
但是这种也有一个缺点,就是每次调用select都需要将所有的文件描述符都传一次给内核,频繁的进行数据拷贝。
因此,操作系统又进一步提供了性能更高的epoll系统调用,来减少数据拷贝。也就是通过在内核开辟一块空间,用来存储需要操作系统监听的文件描述符,而不是每次调用系统调用都拷贝一次数据,此时的编程模式如下
int fd = socket()
bind(fd,8888)]
listen()
//开辟内存空间,用于存储fd
int epfd = epoll_create(5)
//将fd添加到epoll_create开辟的内存空间epfd中
epoll_ctl(ADD, fd, epfd, accept)
for(;;) {
int[] selectfds = epoll_wait(epfd)
for(int fd : selectfds) {
if (fd.isAccept()) {
epoll_ctl(ADD, fd, epfd, read)
} else if (fd.isReadable) {
read(fd)
}
}
}
在epoll的模式下,首先应用程序需要系统调用epoll_create()开辟一块内存空间,用于存储需要让操作系统监听的fd,然后将监听的fd添加到epfd中,接着在一个死循环中,调用epoll_wait(),注意这个系统调用会阻塞(可以设置超时时间),一直阻塞到epfd中有处于ready状态的fd,然后再通过epoll_wait()返回的fd列表,对列表进行循环处理,若是获得一个新连接,再将其添加到epfd中进行监听,若是有数据到达的fd,再通过系统调用read,主动获取数据。
通过epoll模型,可以发现,不必要的系统调用和数据拷贝都不存在了,极大提高了处理网络IO的效率,但是也有一个缺点,就是对应用程序来说,编程模型变得更复杂一些。
总结,bio到nio大致经过主要的4个发展历程,从阻塞到非阻塞,有一个需要注意的是,不管是bio还是nio,读取数据都是应用程序主动发起系统调用的,因此上述的这几种IO模型都是同步的,当然了,现在有一些操作系统是支持异步的,有兴趣的可以自己去查相关资料
网友评论