代码改变世界

IO - 同步,异步,阻塞,非阻塞

2012-05-04 15:57  iBlog  阅读(873)  评论(0编辑  收藏  举报

读了林昊的书,有如下关于IO模式的描述:

很早之前就想弄清楚关于IO的一些概念跟原理,今日有时间不妨在网上搜罗了一番,有如下收获:

1、这篇文章《IO - 同步,异步,阻塞,非阻塞 (亡羊补牢篇)》细致的讲解了同步,异步,阻塞,非阻塞(其实是Richard Stevens的文章),图文并茂+作者的错误认知经验,值得阅读;看这篇文章的评论,发现还是有很多争议的点存在的。

首先我们来看几个socket相关的函数I/O模型

1. Blocking I/O 模型

这个模型是最普通和常见的, 以recvfrom为例. 这个函数在返回之前有两个过程 1). 等待数据到来. 2). 从内核空间copy数据到用户空间.

 

用图表示就是这样: Blocking IO model

在这两个过程完成之前, 这个函数是不会返回的, 整个过程都是blocking的.

[cpp]
  1. #include <stdio.h> 
  2. #include <sys/types.h> 
  3. #include <sys/socket.h> 
  4. #include <linux/in.h> 
  5. #include <unistd.h> 
  6. #include <errno.h> 
  7.  
  8. #define PORT 5789 
  9. #define SERVERIP "122.225.*.*" 
  10. int fd; 
  11. struct sockaddr_in serverAddr; 
  12. char* sendbuffer = "hello server"
  13. char recvbuffer[128]; 
  14.  
  15. int  main() 
  16.         memset(&serverAddr, 0, sizeof(struct sockaddr_in)); 
  17.         serverAddr.sin_family = AF_INET; 
  18.         serverAddr.sin_port = htons(PORT); 
  19.         serverAddr.sin_addr.s_addr = inet_addr(SERVERIP); 
  20.  
  21.         if((fd = socket(AF_INET, SOCK_DGRAM, 0))== -1) 
  22.         { 
  23.                 printf("socket error/n"); 
  24.                 exit(-1); 
  25.         } 
  26.  
  27.         if(sendto(fd, sendbuffer, strlen(sendbuffer), 0, (struct sockaddr*)&serverAddr, sizeof(struct sockaddr_in)) == -1) 
  28.         { 
  29.                 printf("sendto error: %d/n", errno); 
  30.                 exit(-1); 
  31.         } 
  32.         if(recvfrom(fd, recvbuffer, sizeof(recvbuffer), 0, NULL, NULL) == -1) 
  33.         { 
  34.                 printf("recvfrom error: %d/n", errno); 
  35.                 exit(-1); 
  36.         } 
  37.  
  38.         return 0; 

2. Nonblocking I/O 模型

当我们把socket设置为nonblocking, 如果内核没有数据, 函数返回时, 返回值为EWOULDBLOCK. 通过fcntl设置文件描述符O_NONBLOCK位.

Nonblocking IO model

[cpp]
  1. #include <stdio.h> 
  2. #include <sys/types.h> 
  3. #include <sys/socket.h> 
  4. #include <linux/in.h> 
  5. #include <unistd.h> 
  6. #include <errno.h> 
  7. #include <sys/time.h> 
  8. #include <fcntl.h> 
  9.  
  10. #define PORT 5789 
  11. #define SERVERIP "122.225.*.*" 
  12. int fd; 
  13. struct sockaddr_in serverAddr; 
  14. char* sendbuffer = "hello server"
  15. char recvbuffer[128]; 
  16. int flags; 
  17.  
  18. int  main() 
  19.         memset(&serverAddr, 0, sizeof(struct sockaddr_in)); 
  20.         serverAddr.sin_family = AF_INET; 
  21.         serverAddr.sin_port = htons(PORT); 
  22.         serverAddr.sin_addr.s_addr = inet_addr(SERVERIP); 
  23.  
  24.         if((fd = socket(AF_INET, SOCK_DGRAM, 0))== -1) 
  25.         { 
  26.                 printf("socket error/n"); 
  27.                 exit(-1); 
  28.         } 
  29.  
  30.         if((flags = fcntl(fd, F_GETFL, 0)) == -1) 
  31.         { 
  32.                 printf("fcntl error: %d/n", errno); 
  33.                 exit(-1); 
  34.         } 
  35.         if(fcntl(fd, F_SETFL, flags | O_NONBLOCK) == -1) 
  36.         { 
  37.                 printf("fcntl error: %d/n", errno); 
  38.                 exit(-1); 
  39.         } 
  40.  
  41.         if(sendto(fd, sendbuffer, strlen(sendbuffer), 0, (struct sockaddr*)&serverAddr, sizeof(struct sockaddr_in)) == -1) 
  42.         { 
  43.                 printf("sendto error: %d/n", errno); 
  44.                 exit(-1); 
  45.         } 
  46.  
  47. RECVFROM: 
  48.         if(recvfrom(fd, recvbuffer, sizeof(recvbuffer), 0, NULL, NULL) == -1) 
  49.         { 
  50.                 if(errno == EWOULDBLOCK) 
  51.                 { 
  52.                         printf("waiting for data.../n"); 
  53.                         usleep(10 * 1000); 
  54.                         goto RECVFROM; 
  55.                 } 
  56.  
  57.                 printf("recvfrom error: %d/n", errno); 
  58.                 exit(-1); 
  59.         } 
  60.  
  61.         printf("successful recv data/n"); 
  62.  
  63.         return 0; 

3. I/O多路模型

上面两个模型是连个极端, 一个是完全blocking, 一个是完全nonblicking的. 对于blocking模型, 缺点就是不容易控制, 对nonblocking模型, 虽然容易控制, 但会增加内核调用次数. I/O多路模型克服了这两个缺点. 通过select函数可以实现I/O多路模型.

 

IO multiplexing model

[cpp]

  1. #include <stdio.h> 
  2. #include <sys/types.h> 
  3. #include <sys/socket.h> 
  4. #include <linux/in.h> 
  5. #include <unistd.h> 
  6. #include <errno.h> 
  7. #include <sys/time.h> 
  8. #include <fcntl.h> 
  9.  
  10. #define PORT 5789 
  11. #define SERVERIP "122.225.0.0" 
  12. int fd; 
  13. struct sockaddr_in serverAddr; 
  14. char* sendbuffer = "hello server"
  15. char recvbuffer[128]; 
  16. fd_set fdset; 
  17. struct timeval tv; 
  18. int selectret; 
  19.  
  20.  
  21. int  main() 
  22.         memset(&serverAddr, 0, sizeof(struct sockaddr_in)); 
  23.         serverAddr.sin_family = AF_INET; 
  24.         serverAddr.sin_port = htons(PORT); 
  25.         serverAddr.sin_addr.s_addr = inet_addr(SERVERIP); 
  26.  
  27.         if((fd = socket(AF_INET, SOCK_DGRAM, 0))== -1) 
  28.         { 
  29.                 printf("socket error/n"); 
  30.                 exit(-1); 
  31.         } 
  32.  
  33.         if(sendto(fd, sendbuffer, strlen(sendbuffer), 0, (struct sockaddr*)&serverAddr, sizeof(struct sockaddr_in)) == -1) 
  34.         { 
  35.                 printf("sendto error: %d/n", errno); 
  36.                 exit(-1); 
  37.         } 
  38.  
  39.  
  40.         FD_ZERO(&fdset); 
  41.         FD_SET(fd, &fdset); 
  42.         tv.tv_sec = 0; 
  43.         tv.tv_usec = 100 * 1000; 
  44.  
  45. SELECT: 
  46.         selectret = select(fd + 1, &fdset, NULL, NULL, &tv); 
  47.         if(selectret == 0) //timeout. 
  48.         { 
  49.                 printf("waiting for data.../n"); 
  50.                 FD_ZERO(&fdset); 
  51.                 FD_SET(fd, &fdset); 
  52.                 goto SELECT; 
  53.         } 
  54.         elseif(selectret == -1) 
  55.         { 
  56.                 printf("select error: %d", errno); 
  57.                 exit(-1); 
  58.         } 
  59.  
  60.         //FD_ISSET(fd, &fdset); 
  61.         //now we can recv data. 
  62.         if(recvfrom(fd, recvbuffer, sizeof(recvbuffer), 0, NULL, NULL) == -1) 
  63.         { 
  64.                 printf("recvfrom error: %d/n", errno); 
  65.                 exit(-1); 
  66.         } 
  67.  
  68.         printf("successful recv data/n"); 
  69.  
  70.         return 0;

4. I/O信号驱动模型.

I/O多路模型尽管可以设置超时, 但是在select没有返回之前还是blocking的. nonblocking模型可以立即返回, 但还是有之后再次调用函数, 中间的时间不好把握. 信号模型可以克服这些缺点, 可以做到当有数据到来时候通知程序, 之后就可以调用recvfrom函数接收数据, 中间没有由于等待过程的blocking, 和决定多久再次接收数据的过程.

  Signal-Driven IO model

在利用信号I/O程序要做下面三步: 1). 注册SIGIO处理函数; 2). fcntl 设置F_SETOWN; 3). fcntl F_SETFL, O_ASYNC属性, 或者ioctl设置FIOASYNC.

5. 异步IO模型

和信号模型不同的是, 信号模型是内核告诉程序数据准备好了, 可以读取了, 异步模型是数据完成读取之后通知程序. Asynchronous IO model

 比较上面五种模型, 前面四种第二过程是相同的, 不同的只是第一过程; 五种中只有异步模型是完全nonblocking的. 我们可以根据程序的需要采用其中的一种或多种模型来设计程序.

Comparison of the five IO models

 

2、我们再来看看,第二篇文章《关于IO的同步,异步,阻塞,非阻塞》,看完这篇文章后或许就不再糊涂了

IO模型

目前unix存在五种IO模型(这也和上一篇文章:Unix IO 模型 中提到的一致),分别是:

  • 阻塞型 IO(blocking I/O)
  • 非阻塞性IO(nonblocking I/O)
  • IO多路复用(I/O multiplexing)
  • 信号驱动IO(signal driven I/O)
  • 异步IO(asynchronous I/O)

IO的两个阶段

  1. 等待数据准备好
  2. 将数据从内核缓冲区复制到用户进程缓冲区

同步,异步的区别

那么究竟什么是同步和异步的区别呢?请重点读一下原文6.2节中的信号驱动IO和异步IO中的比较。最后总结出来是:

  • 同步IO,需要用户进程主动将存放在内核缓冲区中的数据拷贝到用户进程中。
  • 异步IO,内核会自动将数据从内核缓冲区拷贝到用户缓冲区,然后再通知用户。

这样,同步和异步的概念就非常明显了。以上的五种IO模型,前面四种都是同步的,只有第五种IO模型才是异步的IO。

阻塞和非阻塞

那么阻塞和非阻塞呢?注意到以上五个模型。阻塞IO,非阻塞IO,只是上面的五个模型中的两个。阻塞,非阻塞,是针对单个进程而言的。

当对多路复用IO进行调用时,比如使用poll。需注意的是,poll是系统调用,当调用poll的时候,其实已经是陷入了内核,是内核线程在跑了。因此对于调用poll的用户进程来讲,此时是阻塞的。

因为poll的底层实现,是去扫描每个文件描述符(fd),而如果要对感兴趣的fd进行扫描,那么只能将每个描述符设置成非阻塞的形式(对于用户进程来讲,设置fd是阻塞还是非阻塞,可以使用系统调用fcntl),这样才有可能进行扫描。如果扫描当中,发现有可读(如果可读是用户感兴趣的)的fd,那么select就在用户进程层面就会返回,并且告知用户进程哪些fd是可读的。

这时候,用户进程仍然需要使用read的系统调用,将fd的数据,从内核缓冲区拷贝到用户进程缓冲区(这也是poll为同步IO的原因)。

那么此时的read是阻塞还是非阻塞呢?这就要看fd的状态了,如果fd被设置成了非阻塞,那么此时的read就是非阻塞的;如果fd被设置成了阻塞,那么此时的read就是阻塞的。

不过程序已经执行到了这时候,不管fd是阻塞还是非阻塞,都没有任何区别,因为之前的poll,就是知道有数据准备好了才返回的,也就是说内核缓冲区已经有了数据,此时进行read,是肯定能够将数据拷贝到用户进程缓冲区的。

但如果换种想法,如果poll是因为超时返回的,而我们又对一个fd(此fd是被poll轮询过的)进行read调用,那么此时是阻塞还是非阻塞,就非常有意义了,对吧!

结论

  1. 判断IO是同步还是异步,是看谁主动将数据拷贝到用户进程。
  2. select或者poll,epoll,是同步调用,进行此调用的用户进程也处于阻塞状态。
  3. javaScript或者nodejs中的读取网络(文件)数据,然后提供回调函数进行处理,是异步IO。

3、理论已经基本明确了,下面将从实战代码出发验证理论的真伪