IO模式总结
网上看了好多IO,NIO的文字,参差不齐,每篇总是差一两个点没有讲到,所以这里对于我自己理解的做一个总结,也许有不对的地方。
1,基本概念
1.1)同步/异步,阻塞/非阻塞
同步异步主要针对C端:
所谓同步,就是在c端发出一个功能调用时,在没有得到结果之前,该调用就不返回。也就是必须一件一件事做,等前一件做完了才能做下一件事。
异步的概念和同步相对。当c端一个异步过程调用发出后,调用者不能立刻得到结果。实际处理这个调用的部件在完成后,通过状态、通知和回调来通知调用者。
阻塞非阻塞主要针对S端:
阻塞调用是指调用结果返回之前,当前线程会被挂起(线程进入非可执行状态,在这个状态下,cpu不会给线程分配时间片,即线程暂停运行)。函数只有在得到结果之后才会返回。
非阻塞和阻塞的概念相对应,指在不能立刻得到结果之前,该函数不会阻塞当前线程,而会立刻返回。
1.2)面向流,面向缓冲区
面向流:每次从流中读一个或多个字节,直至读取所有字节,它们没有被缓存在任何地方。
面向缓冲区:数据读取到一个它稍后处理的缓冲区,需要时可在缓冲区中前后移动。这就增加了处理过程中的灵活性。
1.4)内核态和用户态
内核空间可以访问受保护的内存(32位下高位1G为内核空间),剩下3G为用户空间
操作系统限制用户态不能直接访问硬件设备,所以数据从硬件移动到用户进程的内存时需要2步操作。
image.png1.3)mmap(内存映射)
将一个文件或者其它对象映射到进程的地址空间,实现文件磁盘地址和进程虚拟地址空间中一段虚拟地址的一一对映关系
image.png image2.png
2,linux下的5种IO模型
ea103750d79f9799947fdb5a99da11fb.jpg5种IO模式关于同步异步,阻塞非阻塞的关系
阻塞 | 非阻塞 | |
---|---|---|
同步 | BIO | nonblockingIO/多路复用IO(NIO1.0) |
异步 | X | AIO |
1,BIO:
阻塞当前线程,等待数据准备
image.png
2,nonblockingIO:
image.png
相当于轮训,将大片的等待时间切割成小片,
3,多路复用IO(IO mulitplexing)NIO
监听多个socket,当任何一个数据准备好后就返回,用户进程再调用read函数读取数据,极限情况,如果只监听一个socket那么和BIO是一样的,只有监听的socket多时,才会凸显效率。
重点,后面详细说明
image2.png4,信号驱动 I/O
5, asynchronous IO
需要操作系统内核支持
image.pngaio目前在linux下存在BUG,使用场景少
3,为什么说JAVA的标准IO是面向流,NIO面向缓冲区
标准IO:
SocketInputStream.read
int read(byte b[], int off, int length, int timeout) throws IOException {
int n;
// EOF already encountered
if (eof) {
return -1;
}
// connection reset
if (impl.isConnectionReset()) {
throw new SocketException("Connection reset");
}
// bounds check
if (length <= 0 || off < 0 || off + length > b.length) {
if (length == 0) {
return 0;
}
throw new ArrayIndexOutOfBoundsException();
}
boolean gotReset = false;
// acquire file descriptor and do the read
FileDescriptor fd = impl.acquireFD();
try {
n = socketRead(fd, b, off, length, timeout);//native函数
if (n > 0) {
return n;
}
} catch (ConnectionResetException rstExc) {
gotReset = true;
} finally {
impl.releaseFD();
}
/*
* We receive a "connection reset" but there may be bytes still
* buffered on the socket
*/
if (gotReset) {
impl.setConnectionResetPending();
impl.acquireFD();
try {
n = socketRead(fd, b, off, length, timeout);//native函数,这里从内核态中读取数据到数组b中
if (n > 0) {
return n;
}
} catch (ConnectionResetException rstExc) {
} finally {
impl.releaseFD();
}
}
/*
* If we get here we are at EOF, the socket has been closed,
* or the connection has been reset.
*/
if (impl.isClosedOrPending()) {
throw new SocketException("Socket closed");
}
if (impl.isConnectionResetPending()) {
impl.setConnectionReset();
}
if (impl.isConnectionReset()) {
throw new SocketException("Connection reset");
}
eof = true;
return -1;
}
面向缓冲区(NIO):
DatagramChannelImpl.receive
private int receive(FileDescriptor fd, ByteBuffer dst)
throws IOException
{
int pos = dst.position();
int lim = dst.limit();
assert (pos <= lim);
int rem = (pos <= lim ? lim - pos : 0);
if (dst instanceof DirectBuffer && rem > 0)
return receiveIntoNativeBuffer(fd, dst, rem, pos);
// Substitute a native buffer. If the supplied buffer is empty
// we must instead use a nonempty buffer, otherwise the call
// will not block waiting for a datagram on some platforms.
int newSize = Math.max(rem, 1);
ByteBuffer bb = Util.getTemporaryDirectBuffer(newSize);//申请一块newSize大小的缓冲区块
try {
int n = receiveIntoNativeBuffer(fd, bb, newSize, 0);//数据读取到缓冲区中,buffer可以做标记,操作指针等
bb.flip();
if (n > 0 && rem > 0)
dst.put(bb);
return n;
} finally {
Util.releaseTemporaryDirectBuffer(bb);
}
}
channel buffer 说明:
http://www.jianshu.com/p/052035037297
4,select,poll,epoll详解
select
int select (int n, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout);
监视所有的readFD,writeFD,exceptFD
select的一 个缺点在于单个进程能够监视的文件描述符的数量存在最大限制
poll
int poll (struct pollfd *fds, unsigned int nfds, int timeout);
pollfd并没有最大数量限制
select和poll没有太大区别,都是轮训所有的fd/pollfd来获取准备好的fd,当有大量连接的客户端时,效率会线性下降
epoll
1)int epfd = epoll_create(intsize); 创建一个ep句柄(/proc/进程id/fd/),用于监听所有注册的套接字,存在一个红黑树的数据结构中,这棵红黑树的存储通过mmap将内核态和用户态共享,减少用户态和内核态之间的数据交换,而select/poll每次轮训时都要将相关的句柄从内核态拷贝至用户态。
2)int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event) 添加/修改/删除注册的套接字,由于是在红黑树中,效率较高,当事件添加时,该事件会与相应的设备(网卡)驱动程序建立回调连接,一旦事件发生(文件fd改变)相应fd会回调这个函数,将事件添加到一个rdllist(双向链表)中
事件类型:
EPOLLIN :表示对应的文件描述符可以读(包括对端SOCKET正常关闭); EPOLLOUT:表示对应的文件描述符可以写; EPOLLPRI:表示对应的文件描述符有紧急的数据可读(这里应该表示有带外数据到来); EPOLLERR:表示对应的文件描述符发生错误; EPOLLHUP:表示对应的文件描述符被挂断;
3)int epoll_wait(int epfd, struct epoll_event * events, int maxevents, int timeout)
3.1)调用ep_poll,当rdllist为空时挂起,一直到rdllist不为空时唤醒
3.2)ep_events_transfer函数将rdlist中的epitem拷贝到txlist中,并将rdlist清空。ep_send_events函数(很关键),它扫描txlist中的每个epitem,调用其关联fd对用的poll方法。此时对poll的调用仅仅是取得fd上较新的events(防止之前events被更新),之后将取得的events和相应的fd发送到用户空间(封装在struct epoll_event,从epoll_wait返回)。
epoll.jpg3,asynchronous IO
IO详解:https://segmentfault.com/a/1190000003063859#articleHeader14
select、poll、epoll之间的区别总结[整理]:http://www.cnblogs.com/Anker/p/3265058.html
http://www.cnblogs.com/lojunren/p/3856290.html
http://www.smithfox.com/?e=191
这篇对IO流操作写的比较好 http://www.cnblogs.com/hapjin/p/5736188.html
补充:
创建Selectorprovider
当linux内核>2.6时使用epoll
public static SelectorProvider create() {
String osname = AccessController.doPrivileged(
new GetPropertyAction("os.name"));
if ("SunOS".equals(osname)) {
return new sun.nio.ch.DevPollSelectorProvider();
}
// use EPollSelectorProvider for Linux kernels >= 2.6
if ("Linux".equals(osname)) {
String osversion = AccessController.doPrivileged(
new GetPropertyAction("os.version"));
String[] vers = osversion.split("\\.", 0);
if (vers.length >= 2) {
try {
int major = Integer.parseInt(vers[0]);
int minor = Integer.parseInt(vers[1]);
if (major > 2 || (major == 2 && minor >= 6)) {
return new sun.nio.ch.EPollSelectorProvider();
}
} catch (NumberFormatException x) {
// format not recognized
}
}
}
return new sun.nio.ch.PollSelectorProvider();
}
select poll模式
int poll ( struct pollfd * fds, unsigned int nfds, int timeout);
通过程序去控制传入的监控fd
代码层面监视多个描述符,描述文件越多越慢
nio selector: