网上关于同步、异步,阻塞和非阻塞的文章可谓数不胜数,但是很可惜的是,很多文章没有说清楚这四个词之间到底有啥区别和联系,经常有人把epoll、select等IO复用当成是异步IO。这篇文章希望能做一下区分。
IO操作一般都要经过系统内核,一个完整的IO操作可以分为两个阶段:
- 等待内核将数据准备好
- 将数据从内核缓冲区拷贝到用户缓冲区
区分一个IO是同步还是异步只看一点:如果这两个阶段中任意一个阶段发生阻塞,我们就称之为同步IO;相应的,如果这两个阶段都不发生阻塞,那么我们称之为异步IO。下面分别看一下五种IO模型。
1. 阻塞式IO
以套接字的读事件为例,默认情况下所有套接字都是阻塞的,调用套接字的recv函数,就会通知内核准备数据,内核准备数据完成后会拷贝到用户缓冲区。这两个步骤完成之前,用户程序阻塞在recv()处等待返回,完成后系统调用返回。显然这种IO是同步阻塞的,因为在第一阶段,第二阶段都会发生阻塞。
2. 非阻塞式IO
注意,这里的非阻塞实际上指的是在第一个阶段不阻塞。还是以读套接字为例,假设有一个与客户端建立好连接的套接字s,我们把这个套接字设置为非阻塞式,调用s.recv()调用通知内核准备数据,与阻塞式IO不同的是,用户程序并不会在这里阻塞住,如果内核还没有准备好数据,那么s.recv()函数会马上返回一个错误码,告诉用户程序,数据还没准备好。应用这种IO,通常我们会写一个while循环,不断的调用s.recv(),等到内核准备好数据后,s.recv()会执行第二阶段,拷贝数据到用户空间,这个拷贝过程是会发生阻塞的。那么这种IO显然也是同步的。
3. I/O 复用
IO复用有多种,比如select、poll和epoll等。以select为例,select可以同时监听多个套接字是否可读。进程调用select函数会通知内核准备数据,此时进程发生阻塞,等待某一个套接字变为可读。等到数据准备好,select返回可读条件,某一个套接字变为可读,此时,用户程序调用s.recv()函数从内核拷贝数据至用户空间。同样这个数据拷贝过程也会发生阻塞。整个过程中会有两个地方发生阻塞,第一个是select调用,第二个是复制数据至用户空间,也就是第二个阶段,因此这种IO也是一种同步IO。其他的IO复用如epoll、poll等处理过程跟select是一样的,也属于同步IO。
4. 信号驱动式IO
使用信号驱动式IO首先要开启信号驱动IO功能,然后通过sigaction系统调用注册一个信号处理函数,并通知内核准备数据。这个系统调用将立即返回,用户进程继续运行。当内核将数据准备好之后,内核会为该进程产生一个SIGIO信号,随后我们可以在信号处理函数中调用recv()读取数据至用户空间,也可以立即通知主循环,在主循环中读取数据。同样的,这个读取数据的过程一样会阻塞。因此这种IO也是同步IO。这种IO的好处在于等待数据准备好的过程中不会发生阻塞,主循环可以继续运行,只要等待信号处理函数的通知即可:既可以是通知数据一准备好被读取,也可以是数据已经读取完成可以被处理。
5. 异步IO
异步IO的工作机制是我们调用异步IO如aio_read,通知内核启动某个IO操作(读或者写),并让内核在整个操作(即一阶段二阶段都完成)完成后通知我们。可能有人会把这种模型与信号驱动式IO搞混淆,这两种IO的区别在于第二阶段:信号驱动式IO的第二阶段是需要用户进程来操作的,需要用户去调用recv来读数据,这个过程会发生阻塞,因此是同步IO;异步IO的第二个阶段是内核完成的,完成之后才会通知用户进程,此时用户进程可以直接操作数据,在这个过程中没有发生阻塞,因此是异步IO。
总结一下五种IO如下图
6. 参考资料
[1].《UNIX网络编程卷1:套接字联网API(第3版)》第6章
[2].《unix环境高级编程》第14章