IO - 同步,异步,阻塞,非阻塞
2012-05-04 15:57 iBlog 阅读(877) 评论(0) 编辑 收藏 举报读了林昊的书,有如下关于IO模式的描述:
很早之前就想弄清楚关于IO的一些概念跟原理,今日有时间不妨在网上搜罗了一番,有如下收获:
1、这篇文章《IO - 同步,异步,阻塞,非阻塞 (亡羊补牢篇)》细致的讲解了同步,异步,阻塞,非阻塞(其实是Richard Stevens的文章),图文并茂+作者的错误认知经验,值得阅读;看这篇文章的评论,发现还是有很多争议的点存在的。
首先我们来看几个socket相关的函数I/O模型
1. Blocking I/O 模型
这个模型是最普通和常见的, 以recvfrom为例. 这个函数在返回之前有两个过程 1). 等待数据到来. 2). 从内核空间copy数据到用户空间.
用图表示就是这样:
在这两个过程完成之前, 这个函数是不会返回的, 整个过程都是blocking的.
- #include <stdio.h>
- #include <sys/types.h>
- #include <sys/socket.h>
- #include <linux/in.h>
- #include <unistd.h>
- #include <errno.h>
- #define PORT 5789
- #define SERVERIP "122.225.*.*"
- int fd;
- struct sockaddr_in serverAddr;
- char* sendbuffer = "hello server";
- char recvbuffer[128];
- int main()
- {
- memset(&serverAddr, 0, sizeof(struct sockaddr_in));
- serverAddr.sin_family = AF_INET;
- serverAddr.sin_port = htons(PORT);
- serverAddr.sin_addr.s_addr = inet_addr(SERVERIP);
- if((fd = socket(AF_INET, SOCK_DGRAM, 0))== -1)
- {
- printf("socket error/n");
- exit(-1);
- }
- if(sendto(fd, sendbuffer, strlen(sendbuffer), 0, (struct sockaddr*)&serverAddr, sizeof(struct sockaddr_in)) == -1)
- {
- printf("sendto error: %d/n", errno);
- exit(-1);
- }
- if(recvfrom(fd, recvbuffer, sizeof(recvbuffer), 0, NULL, NULL) == -1)
- {
- printf("recvfrom error: %d/n", errno);
- exit(-1);
- }
- return 0;
- }
2. Nonblocking I/O 模型
当我们把socket设置为nonblocking, 如果内核没有数据, 函数返回时, 返回值为EWOULDBLOCK. 通过fcntl设置文件描述符O_NONBLOCK位.
- #include <stdio.h>
- #include <sys/types.h>
- #include <sys/socket.h>
- #include <linux/in.h>
- #include <unistd.h>
- #include <errno.h>
- #include <sys/time.h>
- #include <fcntl.h>
- #define PORT 5789
- #define SERVERIP "122.225.*.*"
- int fd;
- struct sockaddr_in serverAddr;
- char* sendbuffer = "hello server";
- char recvbuffer[128];
- int flags;
- int main()
- {
- memset(&serverAddr, 0, sizeof(struct sockaddr_in));
- serverAddr.sin_family = AF_INET;
- serverAddr.sin_port = htons(PORT);
- serverAddr.sin_addr.s_addr = inet_addr(SERVERIP);
- if((fd = socket(AF_INET, SOCK_DGRAM, 0))== -1)
- {
- printf("socket error/n");
- exit(-1);
- }
- if((flags = fcntl(fd, F_GETFL, 0)) == -1)
- {
- printf("fcntl error: %d/n", errno);
- exit(-1);
- }
- if(fcntl(fd, F_SETFL, flags | O_NONBLOCK) == -1)
- {
- printf("fcntl error: %d/n", errno);
- exit(-1);
- }
- if(sendto(fd, sendbuffer, strlen(sendbuffer), 0, (struct sockaddr*)&serverAddr, sizeof(struct sockaddr_in)) == -1)
- {
- printf("sendto error: %d/n", errno);
- exit(-1);
- }
- RECVFROM:
- if(recvfrom(fd, recvbuffer, sizeof(recvbuffer), 0, NULL, NULL) == -1)
- {
- if(errno == EWOULDBLOCK)
- {
- printf("waiting for data.../n");
- usleep(10 * 1000);
- goto RECVFROM;
- }
- printf("recvfrom error: %d/n", errno);
- exit(-1);
- }
- printf("successful recv data/n");
- return 0;
- }
3. I/O多路模型
上面两个模型是连个极端, 一个是完全blocking, 一个是完全nonblicking的. 对于blocking模型, 缺点就是不容易控制, 对nonblocking模型, 虽然容易控制, 但会增加内核调用次数. I/O多路模型克服了这两个缺点. 通过select函数可以实现I/O多路模型.
[cpp]
- #include <stdio.h>
- #include <sys/types.h>
- #include <sys/socket.h>
- #include <linux/in.h>
- #include <unistd.h>
- #include <errno.h>
- #include <sys/time.h>
- #include <fcntl.h>
- #define PORT 5789
- #define SERVERIP "122.225.0.0"
- int fd;
- struct sockaddr_in serverAddr;
- char* sendbuffer = "hello server";
- char recvbuffer[128];
- fd_set fdset;
- struct timeval tv;
- int selectret;
- int main()
- {
- memset(&serverAddr, 0, sizeof(struct sockaddr_in));
- serverAddr.sin_family = AF_INET;
- serverAddr.sin_port = htons(PORT);
- serverAddr.sin_addr.s_addr = inet_addr(SERVERIP);
- if((fd = socket(AF_INET, SOCK_DGRAM, 0))== -1)
- {
- printf("socket error/n");
- exit(-1);
- }
- if(sendto(fd, sendbuffer, strlen(sendbuffer), 0, (struct sockaddr*)&serverAddr, sizeof(struct sockaddr_in)) == -1)
- {
- printf("sendto error: %d/n", errno);
- exit(-1);
- }
- FD_ZERO(&fdset);
- FD_SET(fd, &fdset);
- tv.tv_sec = 0;
- tv.tv_usec = 100 * 1000;
- SELECT:
- selectret = select(fd + 1, &fdset, NULL, NULL, &tv);
- if(selectret == 0) //timeout.
- {
- printf("waiting for data.../n");
- FD_ZERO(&fdset);
- FD_SET(fd, &fdset);
- goto SELECT;
- }
- elseif(selectret == -1)
- {
- printf("select error: %d", errno);
- exit(-1);
- }
- //FD_ISSET(fd, &fdset);
- //now we can recv data.
- if(recvfrom(fd, recvbuffer, sizeof(recvbuffer), 0, NULL, NULL) == -1)
- {
- printf("recvfrom error: %d/n", errno);
- exit(-1);
- }
- printf("successful recv data/n");
- return 0;
- }
4. I/O信号驱动模型.
I/O多路模型尽管可以设置超时, 但是在select没有返回之前还是blocking的. nonblocking模型可以立即返回, 但还是有之后再次调用函数, 中间的时间不好把握. 信号模型可以克服这些缺点, 可以做到当有数据到来时候通知程序, 之后就可以调用recvfrom函数接收数据, 中间没有由于等待过程的blocking, 和决定多久再次接收数据的过程.
在利用信号I/O程序要做下面三步: 1). 注册SIGIO处理函数; 2). fcntl 设置F_SETOWN; 3). fcntl F_SETFL, O_ASYNC属性, 或者ioctl设置FIOASYNC.
5. 异步IO模型
和信号模型不同的是, 信号模型是内核告诉程序数据准备好了, 可以读取了, 异步模型是数据完成读取之后通知程序.
比较上面五种模型, 前面四种第二过程是相同的, 不同的只是第一过程; 五种中只有异步模型是完全nonblocking的. 我们可以根据程序的需要采用其中的一种或多种模型来设计程序.
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的两个阶段
- 等待数据准备好
- 将数据从内核缓冲区复制到用户进程缓冲区
同步,异步的区别
那么究竟什么是同步和异步的区别呢?请重点读一下原文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调用,那么此时是阻塞还是非阻塞,就非常有意义了,对吧!
结论
- 判断IO是同步还是异步,是看谁主动将数据拷贝到用户进程。
- select或者poll,epoll,是同步调用,进行此调用的用户进程也处于阻塞状态。
- javaScript或者nodejs中的读取网络(文件)数据,然后提供回调函数进行处理,是异步IO。
3、理论已经基本明确了,下面将从实战代码出发验证理论的真伪
本文基于署名 2.5 中国大陆许可协议发布,欢迎转载,演绎或用于商业目的,但是必须保留本文的署名时邵猛(包含链接)。如您有任何疑问或者授权方面的协商,请给我留言。如果您觉得文章很有用,欢迎捐赠!【通过】。