阻塞 / 非阻塞 VS 同步 / 异步

阻塞 I/O

阻塞 I/O 发起的 read 请求,线程会被挂起,一直等到内核数据准备好,并把数据从内核区域拷贝到应用程序的缓冲区中,当拷贝过程完成,read 请求调用才返回。接下来,应用程序就可以对缓冲区的数据进行数据解析。

image.png

非阻塞 I/O

非阻塞的 read 请求在数据未准备好的情况下立即返回,应用程序可以不断轮询内核,直到数据准备好,内核将数据拷贝到应用程序缓冲,并完成这次 read 调用。注意,这里最后一次 read 调用,获取数据的过程,是一个同步的过程。这里的同步指的是内核区域的数据拷贝到缓存区这个过程。

image.png

I/O 多路复用

通过 I/O 事件分发,当内核数据准备好时,再通知应用程序进行操作。这个做法大大改善了应用进程对 CPU 的利用率,在没有被通知的情况下,应用进程可以使用 CPU 做其他的事情。

注意,这里 read 调用,获取数据的过程,也是一个同步的过程。
**
image.png

对 “同步的说明”

无论是第一种阻塞 I/O,还是第二种非阻塞 I/O,第三种基于非阻塞 I/O 的多路复用都是同步调用技术。为什么这么说呢?因为同步调用、异步调用的说法,是对于获取数据的过程而言的,前面几种最后获取数据的 read 操作调用,都是同步的,在 read 调用时,内核将数据从内核空间拷贝到应用程序空间,这个过程是在 read 函数中同步进行的,如果内核实现的拷贝效率很差,read 调用就会在这个同步过程中消耗比较长的时间。

异步 I/O

当我们发起 aio_read 之后,就立即返回,内核自动将数据从内核空间拷贝到应用程序空间,这个拷贝过程是异步的,内核自动完成的,和前面的同步操作不一样,应用程序并不需要主动发起拷贝动作

image.png

image.png

aio_read 和 aio_write 的用法

  1. #include "lib/common.h"
  2. #include <aio.h>
  3. const int BUF_SIZE = 512;
  4. int main() {
  5. int err;
  6. int result_size;
  7. // 创建一个临时文件
  8. char tmpname[256];
  9. snprintf(tmpname, sizeof(tmpname), "/tmp/aio_test_%d", getpid());
  10. unlink(tmpname);
  11. int fd = open(tmpname, O_CREAT | O_RDWR | O_EXCL, S_IRUSR | S_IWUSR);
  12. if (fd == -1) {
  13. error(1, errno, "open file failed ");
  14. }
  15. char buf[BUF_SIZE];
  16. struct aiocb aiocb;
  17. //初始化buf缓冲,写入的数据应该为0xfafa这样的,
  18. memset(buf, 0xfa, BUF_SIZE);
  19. memset(&aiocb, 0, sizeof(struct aiocb));
  20. aiocb.aio_fildes = fd;
  21. aiocb.aio_buf = buf;
  22. aiocb.aio_nbytes = BUF_SIZE;
  23. //开始写
  24. if (aio_write(&aiocb) == -1) {
  25. printf(" Error at aio_write(): %s\n", strerror(errno));
  26. close(fd);
  27. exit(1);
  28. }
  29. //因为是异步的,需要判断什么时候写完
  30. while (aio_error(&aiocb) == EINPROGRESS) {
  31. printf("writing... \n");
  32. }
  33. //判断写入的是否正确
  34. err = aio_error(&aiocb);
  35. result_size = aio_return(&aiocb);
  36. if (err != 0 || result_size != BUF_SIZE) {
  37. printf(" aio_write failed() : %s\n", strerror(err));
  38. close(fd);
  39. exit(1);
  40. }
  41. //下面准备开始读数据
  42. char buffer[BUF_SIZE];
  43. struct aiocb cb;
  44. cb.aio_nbytes = BUF_SIZE;
  45. cb.aio_fildes = fd;
  46. cb.aio_offset = 0;
  47. cb.aio_buf = buffer;
  48. // 开始读数据
  49. if (aio_read(&cb) == -1) {
  50. printf(" air_read failed() : %s\n", strerror(err));
  51. close(fd);
  52. }
  53. //因为是异步的,需要判断什么时候读完
  54. while (aio_error(&cb) == EINPROGRESS) {
  55. printf("Reading... \n");
  56. }
  57. // 判断读是否成功
  58. int numBytes = aio_return(&cb);
  59. if (numBytes != -1) {
  60. printf("Success.\n");
  61. } else {
  62. printf("Error.\n");
  63. }
  64. // 清理文件句柄
  65. close(fd);
  66. return 0;
  67. }

这个程序展示了如何使用 aio 系列函数来完成异步读写。主要用到的函数有:

  1. aio_write:用来向内核提交异步写操作;
  2. aio_read:用来向内核提交异步读操作;
  3. aio_error:获取当前异步操作的状态;
  4. aio_return:获取异步操作读、写的字节数。

aiocb:

  1. struct aiocb {
  2. int aio_fildes; /* File descriptor */
  3. off_t aio_offset; /* File offset */
  4. volatile void *aio_buf; /* Location of buffer */
  5. size_t aio_nbytes; /* Length of transfer */
  6. int aio_reqprio; /* Request priority offset */
  7. struct sigevent aio_sigevent; /* Signal number and value */
  8. int aio_lio_opcode; /* Operation to be performed */
  9. };

接下来运行这个程序,我们看到屏幕上打印出一系列的字符,显示了这个操作是有内核在后台帮我们完成的。

./aio01
writing... 
writing... 
writing... 
writing... 
writing... 
writing... 
writing... 
writing... 
writing... 
writing... 
writing... 
writing... 
writing... 
writing... 
Reading... 
Reading... 
Reading... 
Reading... 
Reading... 
Reading... 
Reading... 
Reading... 
Reading... 
Success.

打开 /tmp 目录下的 aio_test_xxxx 文件,可以看到,这个文件成功写入了我们期望的数据。

image.png

Linux 下 socket 套接字的异步支持

aio 系列函数是由 POSIX 定义的异步操作接口,可惜的是,Linux 下的 aio 操作,不是真正的操作系统级别支持的,它只是由 GNU libc 库函数在用户空间借由 pthread 方式实现的,而且仅仅针对磁盘类 I/O,套接字 I/O 不支持。

Windows 下的 IOCP 和 Proactor 模式

和 Linux 不同,Windows 下实现了一套完整的支持套接字的异步编程接口,这套接口一般被叫做 IOCompletetionPort(IOCP)。

这样,就产生了基于 IOCP 的所谓 Proactor 模式

和 Reactor 模式一样,Proactor 模式也存在一个无限循环运行的 event loop 线程,但是不同于 Reactor 模式,这个线程并不负责处理 I/O 调用,它只是负责在对应的 read、write 操作完成的情况下,分发完成事件到不同的处理函数

  • 它只负责感知事件完成
  • 需要传入数据缓冲区的地址等信息,这样,系统内核才可以自动帮我们把数据的读写工作完成。

Reactor 模式是基于待完成的 I/O 事件,而 Proactor 模式则是基于已完成的 I/O 事件,两者的本质,都是借由事件分发的思想,设计出可兼容、可扩展、接口友好的一套程序框架。

总结