操作系统级别的NIO概念
BIO:Blocking IO
NIO: Nonblocking IO
语言/架构层面的NIO概念
NIO: new IO,基于新的操作系统层面接口的应用接口
如Java的Socket编程里调用accept()实际的系统调用是使用的poll(多路复用的系统调用接口)而不是旧的IO接口accept(不支持多路复用)。
AIO:async IO,异步IO
graph TD
Server --> Kernel --> Client
Server端创建一个Socket监听客户端连接,这个Socket对应一个文件描述符(假如为fd5),Socket先绑定(bind)端口号,监听(listen)这个端口的状态,再阻塞在接收客户端连接上(accept/poll),此时如果有客户端Client1连接,accept的系统调用返回一个fd6,指向连入的客户端。此时Server端调用系统调用(recvfrom),传入fd6,接收客户端的输入。以上无论哪一个系统调用都是阻塞执行的,当Client2想要接入Server时,Server已经阻塞在recvfrom。解决办法是,当accept返回客户端fd时就新开一个线程处理客户端的输入操作,主线程则再阻塞到accept方法接收其他客户端的连接。
以上为多线程服务端的模型。
Kernel提供Socket-NonBlocking类别的接口,指定fd为非阻塞的。则fd的操作(accept,recv,read...)要么拿到结果返回,要么直接返回错误码,不会阻塞。
graph TD
Server --> Kernel --> Client1,Client2,Client3...
while(true){
bind->listen->accept->recvfrom
}
服务端的while循环中,recvfrom会立刻返回,则accept还可以接收其他客户端的连接,每次循环都遍历已经连接的客户端链表,检查是否需要读取内容。
以上非阻塞的方案,存在的问题是系统调用(recvfrom)的次数和客户端数量正相关。
最好的方案是系统调用的次数和客户端数量无关,即不管客户端连接多少,都只需要一次系统调用就可以得知哪个客户端需要输入数据。
此时Kernel提供新的系统调用select(/poll),传入需要监听读入的fd set和需要写入的fd set等等,select的返回值就会告诉用户空间哪些文件描述符可以写入、哪些可以读取...,用户空间再分别去调用这些fd的系统调用,则可以大幅度减少系统调用的次数。
select就称作多路复用,即复用一次系统调用得到多次单路系统调用需要的结果。
以上多路复用存在的问题是内核依然要去遍历客户端fd检查是否有读入写入的事件,另一方面select的参数是所有客户端文件描述符的集合,客户端数量多的时候从用户空间拷贝文件描述符占用的内存到内核空间依然是耗费CPU性能的。
此时Kernel提供新的系统调用epoll(event poll),epoll相关的三个系统调用分别为epoll_create,会在内核空间创建一个内存空间,并返回指向该内存空间的fd;epoll_ctl(fd1,op,fd2,event),fd1是epoll_create返回的fd,op是创建、删除、移动等控制fd1中fd的操作,fd2是放入fd1中的,event是fd2监听的事件,如读入事件、写入事件等等,当Server阻塞在accept时,会在内核空间创建fd1,再调用epoll_wait()监听内核空间的事件,当Client建立连接时,Kernel会有回调事件从fd1中取出fd2通过epoll_wait返回给用户空间,用户空间得知有Client连接就会创建fd3指向Client再通过epoll_ctl(fd1,add,fd3,recvfrom)把fd3添加到fd1指向的内核空间,再如此循环执行下去。
此时的方案不需要再每次select时拷贝fd set,也不需要遍历fd set,客户端的事件都是通过CPU中断感知的,CPU会根据中断号回调不同的回调事件,因此Kernel可以感知文件描述符的状态变化。