0%

linux的io模型

1. 基础术语

先抛出知乎的一个问题: https://www.zhihu.com/question/59975081

epoll技术属于IO复用,IO复用属于同步IO,所以epoll属于同步IO,这应该是没毛病的。

现在我用了一个框架,比如twisted,里面的reactor模式的实现是基于epoll或者poll的,在IO的范畴应该是属于同步IO,但是网上几乎所有的文章都说twisted是异步的。

我的问题是,异步与异步IO是不是一个东西?有没有可能异步可以由同步IO(epoll或poll)实现?

1.1 进程之间通信的层次

关于同步、异步、阻塞、非阻塞在《操作系统概念(第九版)》中有如下解释

img

进程间的通信是通过 send() 和 receive() 两种基本操作完成的。具体如何实现这两种基础操作,存在着不同的设计。 消息的传递有可能是阻塞的或非阻塞的 – 也被称为同步或异步的。

  • 阻塞式发送(blocking send). 发送方进程会被一直阻塞, 直到消息被接受方进程收到。
  • 非阻塞式发送(nonblocking send)。 发送方进程调用 send() 后, 立即就可以其他操作。
  • 阻塞式接收(blocking receive) 接收方调用 receive() 后一直阻塞, 直到消息到达可用。
  • 非阻塞式接受(nonblocking receive) 接收方调用 receive() 函数后, 要么得到一个有效的结果, 要么得到一个空值, 即不会被阻塞。

上述不同类型的发送方式和不同类型的接收方式,可以自由组合。

也就是说, 从进程级通信的维度讨论时, 阻塞和同步(非阻塞和异步)就是一对同义词, 且需要针对发送方和接收方作区分对待。

1.2 I/O 系统调用的层次

在处理 IO 的时候,阻塞和非阻塞都是同步 IO。

1

阻塞 IO

阻塞这个词是与系统调用 System Call 紧紧联系在一起的, 因为要让一个进程进入 等待(waiting) 的状态, 要么是它主动调用 wait() 或 sleep() 等挂起自己的操作, 另一种就是它调用 System Call, 而 System Call 因为涉及到了 I/O 操作, 不能立即完成, 于是内核就会先将该进程置为等待状态, 调度其他进程的运行, 等到 它所请求的 I/O 操作完成了以后, 再将其状态更改回 ready 。

1
2
3
4
5
6
7
8
9
listenfd = socket();   // 打开一个网络通信端口
bind(listenfd); // 绑定
listen(listenfd); // 监听
while(1) {
connfd = accept(listenfd); // 阻塞建立连接
int n = read(connfd, buf); // 阻塞读数据
doSomeThing(buf); // 利用读到的数据做些什么
close(connfd); // 关闭连接,循环等待下一个连接
}
非阻塞 IO

现在的大部分操作系统也会提供非阻塞I/O 系统调用接口(Nonblocking I/O system call)。 一个非阻塞调用不会挂起调用程序, 而是会立即返回一个值, 表示有多少bytes 的数据被成功读取(或写入)。

1
2
fcntl(connfd, F_SETFL, O_NONBLOCK);
int n = read(connfd, buffer) != SUCCESS);

1.3 前文问题解答

简单说一句话,你需要分层看这个事。

epoll 这个系统调用,是同步的,也就是必须等待操作系统返回值。而底层用了 epoll 的封装后的框架,可以是异步的,只要你暴露给外部的接口,无需等待你的返回值即可。

再多说些,epoll 这个系统调用的底层内核设计里,每个 IO 事件的通知等待,是异步的。但这不影响,epoll 这个系统调用对外部来说,是一个同步的接口。

所以你说,有的地方说同步,有的地方说异步,其实是不同分层的视角看。

2. Linux下的五种I/O模型(apue)

Linux下主要有以下五种I/O模型:

  1. 阻塞I/O(blocking IO)
  2. 非阻塞I/O (nonblocking I/O)
  3. I/O 复用 (I/O multiplexing)(select、poll、linux 2.6种改进的epoll)
  4. 信号驱动I/O (signal driven I/O (SIGIO))
  5. 异步I/O (asynchronous I/O)

2.1 阻塞IO 模型

最傻,不能忍。

进程会一直阻塞,直到数据拷贝完成 应用程序调用一个IO函数,导致应用程序阻塞,等待数据准备好。数据准备好后,从内核拷贝到用户空间,IO函数返回成功指示。

1

2.2 非阻塞IO模型

循环调用询问, OK了开始拷贝,拷贝中需要等待。

通过进程反复调用IO函数,在数据从内核拷贝到用户空间过程中,进程是阻塞的。

1

2.3 IO复用模型

ok了被通知, 拷贝中需要等待。

主要是select和epoll。一个线程可以对多个IO端口进行监听,当socket有读写事件时分发到具体的线程进行处理。

1

2.4 信号驱动IO模型

ok了被通知,拷贝中需要等待。

首先我们允许Socket进行信号驱动IO,并安装一个信号处理函数,进程继续运行并不阻塞。当数据准备好时,进程会收到一个SIGIO信号,可以在信号处理函数中调用I/O操作函数处理数据。

1

2.5 异步IO模型

ok了被通知,拷贝完成后被通知。

相对于同步IO,异步IO不是顺序执行。用户进程进行aio_read系统调用之后,无论内核数据是否准备好,都会直接返回给用户进程,然后用户态进程可以去做别的事情。等到socket数据准备好了,内核直接复制数据给进程,然后从内核向进程发送通知。IO两个阶段,进程都是非阻塞的。

1

3. 非阻塞IO为什么是同步IO?

3.1 同步IO和异步IO的区别

A synchronous I/O operation causes the requesting process to be blocked until that I/O operation completes;
An asynchronous I/O operation does not cause the requesting process to be blocked;

两者的区别就在于同步IO 做 IO操作的时候会将process阻塞。

按照这个定义,之前所述的blocking IO,non-blocking IO,IO multiplexing都属于synchronous IO。注意到non-blocking IO会一直轮询(polling),这个过程是没有阻塞的,但是recvfrom阶段blocking IO,non-blocking IO和IO multiplexing都是阻塞的。

而asynchronous IO则不一样,当进程发起IO 操作之后,就直接返回再也不理睬了,直到kernel发送一个信号,告诉进程说IO完成。在这整个过程中,进程完全没有被block。

3.2 非阻塞IO在内核拷贝数据中是阻塞的

POSIX把I/O操作划分成两类:

  • 同步I/O: 同步I/O操作导致请求进程阻塞,直至操作完成

  • 异步I/O: 异步I/O操作不导致请求阻塞

所以Unix的前四种I/O模型都是同步I/O, 只有最后一种才是异步I/O。

  • 阻塞IO模型,非阻塞IO模型,IO复用模型,信号驱动IO模型都是同步IO。
  • 非阻塞之所以是同步,是因为recvfrom会将数据从kernel拷贝到用户内存中,这个时候进程是被block了,在这段时间内,进程是被block的。

1

4.1 Java IO 和 Linux IO

1. Java IO

  • BIO(Blocking I/O)

  • NIO(Non-blocking I/O)

    在Java领域,也称为New I/O。

    Java NIO (JSR 51)定义了Java new I/O API,提案2000年提出,2002年正式发布。 JDK 1.4起包含了相应的API实现。

  • AIO(Asynchronous I/O)

    JAVA NIO2 (JSR 203)定义了更多的 New I/O APIs, 提案2003提出,直到2011年才发布, 最终在JDK 7中才实现。

当前很多的项目还停留在JAVA NIO的实现上, 对JAVA AIO(asynchronous I/O)着墨不多。

2. 和Linux IO 对应关系

再次重复linux的前四种I/O模型都是同步I/O, 只有最后一种才是异步I/O。

  • 传统的Java BIO (blocking I/O)是Unix I/O模型中的第一种。
  • Java NIO中如果不使用select模式,而只把channel配置成nonblocking则是第二种模型。
  • Java NIO select实现的是一种多路复用I/O。 底层使用epoll或者相应的poll系统调用。
  • 第四种模型JDK应该是没有实现。
  • Java NIO2增加了对第五种模型的支持,也就是AIO。

5. 参考资料

给作者打赏,可以加首页微信,咨询作者相关问题!