Loading... # BIO与NIO ## 第一阶段 ![第一阶段][1] 每个client发来的请求都对应一个I/O标识符,fd 8,fd 9,多个线程或进程去读对应标识符的请求,该阶段socket是blocking(阻塞)的,也就是说如果一个线程读fd 8,但是数据还没有返回,它就会一直在那里等,我想读fd 9,只能再开一个线程,当请求很多时,就要开更多的线程,但是cpu线程是有时间片的,如果有某个请求数据返回了,还没轮到它的线程处理,它就只能阻塞着,造成资源的浪费,因为blocking,所以叫做bio。 ## 第二阶段 该阶段通过内核的fd nonblock系统调用,实现非阻塞I/O,由一个线程或进程,去轮询内核的I/O请求,如果有数据就返回,实现了同步非阻塞I/O(NIO),但是该模式有一个问题,如果有N个fd,会导致用户进程轮训N次内核,成本很大。 ## 第三阶段 该阶段通过内核的select系统调用,将轮询过程在内核中实现,用户进程只需要将N个要获取数据的文件标识符给内核,内核会负责轮询找出有数据的文件标识符,返回给用户进程,用户进程就可以通过返回的文件标识符去读取数据了。这种模式叫做多路复用,但是依然存在问题,就是用户进程需要传给内核一个fd数组,内核也会返回一个fd数组,(用户态与内核态)造成fd相关数据拷来拷去。 ## 第四阶段 epoll系统调用,采用回调的方式处理I/O操作,避免轮询,内部使用红黑树记录添加进来的socket,并将返回事件结果放入一个双向链表中,通过epoll_create函数创建该结构体,新传入socekt监听事件会调用epoll_ctl函数将其添加到红黑树上,这些事件都会与网卡驱动程序建立回调关系,这个回调方法在内核中叫ep_poll_callback,它会将发生的事件添加到双链表中。当有程序调用epoll_wait函数时,就会立刻返回双向链表中的数据,该操作不用传递socket句柄给内核,因为epoll_ctl的时候内核已经拿到了要监控的句柄列表。 有人说epoll用到了mmap避免了用户态与内核态之间的拷贝,mmap是一种映射关系,可以将对象或文件映射到进程地址空间。将文件的一段直接映射到内存,内核和应用进程共用一块内存地址,这样就不需要拷贝了,但是查看linux的epoll代码发现`extern int epoll_create(int __size) __THROW;`, `extern int epoll_ctl(int __epfd, int __op, int __fd,struct epoll_event *__event) __THROW;`, `extern int epoll_wait(int __epfd, struct epoll_event *__events,int __maxevents, int __timeout);`均没有涉及到mmap的相关操作,那么epoll到底有没有用到mmap?首先,linux实现零拷贝的方式有mmap和sendfile两种,观察源码,epoll确实没有用到mmap。那么mmap在哪?查看java的nio源码发现,FileChannel的map()方法 ``` private native long map0(int prot, long position, long length) throws IOException; ``` 调用了map0这个本地方法,观察FileChannelImpl.c的map0实现发现: ``` mapAddress = mmap64( 0, /* Let OS decide location */ len, /* Number of bytes to map */ protections, /* File permissions */ flags, /* Changes are shared */ fd, /* File descriptor of mapped file */ off); /* Offset into file */ ``` 使用了mmap这个API 再看FileChannel的transferTo()方法,有行代码 ``` // Attempt a mapped transfer, but only to trusted channel types if ((n = transferToTrustedChannel(position, icount, target)) >= 0) return n; ``` 尝试一个映射传输,但只用在被信任的channel类型,跟进去发现: ``` MappedByteBuffer dbb = map(MapMode.READ_ONLY, position, size); ``` 查看map方法: ``` addr = map0(imode, mapPosition, mapSize); ``` 依然调用的map0这个本地方法。 FileChannel的transferTo()方法中还有 ``` // Attempt a direct transfer, if the kernel supports it if ((n = transferToDirectly(position, icount, target)) >= 0) return n; ``` 尝试一个直接传输,如果内核支持它,跟进去: ``` return transferToDirectlyInternal(position, icount,target, targetFD); ``` 查看transferToDirectlyInternal()方法,发现它最终调用了transferTo0()这个本地方法,查看transferTo0()的实现: ``` JNIEXPORT jlong JNICALL Java_sun_nio_ch_FileChannelImpl_transferTo0(JNIEnv *env, jobject this, jint srcFD, jlong position, jlong count, jint dstFD) { off64_t offset = (off64_t)position; jlong n = sendfile64(dstFD, srcFD, &offset, (size_t)count); if (n < 0) { if (errno == EAGAIN) return IOS_UNAVAILABLE; if ((errno == EINVAL) && ((ssize_t)count >= 0)) return IOS_UNSUPPORTED_CASE; if (errno == EINTR) { return IOS_INTERRUPTED; } JNU_ThrowIOExceptionWithLastError(env, "Transfer failed"); return IOS_THROWN; } return n; } ``` 发现用到的是sendfile。 **因此,epoll并没有用到mmap,真正用到mmap的是java实现的nio** [1]: https://www.princelei.club/usr/uploads/2019/11/1396312223.png Last modification:June 11th, 2020 at 06:15 pm © 允许规范转载