酒店机票最便宜的网站建设,陕西住建电子证书查询,wordpress用户后台,网站做百度推广划算吗I/O模型的一些理解 一些基本的概念同步阻塞、同步非阻塞、异步阻塞、异步非阻塞总结概念 I/O模型一些例子 从源头解释从TCP发送数据的流程说起阻塞I/O | 非阻塞I/OI/O多路复用信号驱动I/O异步I/O再谈IO模型里面的同步异步 参考连接
参考链接
参考链接
一些基本的概念 阻塞(b… I/O模型的一些理解 一些基本的概念同步阻塞、同步非阻塞、异步阻塞、异步非阻塞总结概念 I/O模型一些例子 从源头解释从TCP发送数据的流程说起阻塞I/O | 非阻塞I/OI/O多路复用信号驱动I/O异步I/O再谈IO模型里面的同步异步 参考连接
参考链接
参考链接
一些基本的概念 阻塞(blocking)、非阻塞non-blocking可以简单理解为需要做一件事能不能立即得到返回应答比如read函数 同步(synchronous)、异步(asynchronous) 你总是做完一件再去做另一件不管是否需要时间等待这就是同步(就是在发出一个功能调用时在没有得到结果之前该调用就不返回,即此时不能做下一件事情)异步则反之你可以同时做几件事并非一定需要一件事做完再做另一件事(当一个异步过程调用发出后调用者不能立刻得到结果,此时可以接着做其它事情)。同步可以理解为多个相互之间一定联系的并发执行的进程有序执行异步则没有约定一定的次序。
举个更加形象的例子如果主线程上执行了一系列函数某个函数A需要与相关线程交互调用了另一个函数a时 如果立即去执行函数 a这称为同步。 如果没有去执行函数 a而是将执行函数 a 的时机安排在未来的某个时间然后马上继续执行函数 A这称为异步。 当执行函数 a 时直至获得完整的资源之前都暂停执行当前函数这称为阻塞。 当执行函数 a 时立即获得瞬时的结果然后马上继续执行当前函数。如果获得的瞬时资源不是完整的资源之后周期性发送类似的请求直至获得完整的资源这称为非阻塞。
同步阻塞、同步非阻塞、异步阻塞、异步非阻塞 同步阻塞在需要某资源时马上发起请求并暂停本线程之后的程序直至获得所需的资源。 同步非阻塞在需要某资源时马上发起请求且可以马上得到答复然后继续执行之后的程序。但如果得到的不是完整的资源之后将周期性地的请求直至获得所需的资源。 异步阻塞在需要某资源时不马上发起请求而安排一个以后的时间再发起请求。当到了那时发出请求时将暂停本线程之后的程序直至获得所需的资源。在获取资源之后使用共享信号量、异步回调等方式将结果异步反馈。 异步非阻塞在需要某资源时不马上发起请求而安排一个以后的时间再发起请求。当到了那时发出请求时可以马上得到答复然后继续执行之后的程序。但如果得到的不是完整的资源之后将周期性地的请求。在最终获取到资源之后使用共享信号量、异步回调等方式将结果异步反馈。
总结概念 异步把事情推到以后去做 阻塞专心做一件事情 同步阻塞马上专心做一件事情 同步非阻塞一边做一件事情一边做另一件事情一心二用 异步阻塞把问题推到以后专心处理 异步非阻塞把问题推到以后时不时处理一下
I/O模型
IO (Input/Output输入/输出)即数据的读取接收或写入发送操作通常用户进程中的一个完整IO分为两阶段用户进程空间–内核空间、内核空间–设备空间磁盘、网络等。IO有内存IO、网络IO和磁盘IO三种通常我们说的IO指的是后两者。
针对网络I/O可以分成两个阶段准备阶段和操作阶段。
准备阶段判断是否能够操作(即等待数据是否可用)在内核进程完成的操作阶段执行实际的IO调用数据从内核缓冲区拷贝到用户进程缓冲区。
比如对于一个read操作它会经历下面两个阶段 等待数据准备数据是否拷贝到内核缓冲区; 将数据从内核拷贝到用户进程空间 重点理解上面这两个阶段
UNP中提到5种IO模型分别是阻塞IO模型、非阻塞IO模型、IO复用模型、信号驱动的IO模型、异步IO模型前4种为同步IO操作只有异步IO模型是异步IO操作。
同步I/O向应用程序通知的是I/O就绪事件异步I/O向应用程序通知的是I/O完成事件
1、阻塞IO在准备阶段即同步阻塞应用进程调用I/O操作时阻塞只有等待要操作的数据准备好并复制到应用进程的缓冲区中才返回
2、非阻塞IO当应用进程要调用的I/O操作会导致该进程进入阻塞状态时该I/O调用返回一个错误一般情况下应用进程需要利用轮询的方式来检测某个操作是否就绪。数据就绪后实际的I/O操作会等待数据复制到应用进程的缓冲区中以后才返回
3、IO复用多路IO共用一个同步阻塞接口任意IO可操作都可激活IO操作这是对阻塞IO的改进(主要是select和poll、epoll,关键是能实现同时对多个IO端口进行监听)。此时阻塞发生在select/poll的系统调用上而不是阻塞在实际的I/O系统调用上。IO多路复用的高级之处在于:它能同时等待多个文件描述符而这些文件描述符套接字描述符其中的任意一个进入读就绪状态select等函数就可以返回。
4、信号驱动IO注册一个IO信号事件在数据可操作时通过SIGIO信号通知线程这应该算是一种异步机制
以上四种模型在第一阶段即判断是否可操作阶段各不相同但一旦数据可操作则切换到同步阻塞模式下执行IO操作所以都算是同步IO。
5、异步IO: 应用进程通知内核开始一个异步I/O操作并让内核在整个操作包含将数据从内核复制到应该进程的缓冲区完成后通知应用进程。
在UNP里面根据上述两个阶段可以进行归类 阻塞IO:在两个阶段上面都是阻塞的; 非阻塞IO:在第1阶段程序不断的轮询直到数据准备好第2阶段还是阻塞的; IO复用:在第1阶段当一个或者多个IO准备就绪时通知程序第2阶段还是阻塞的在第1阶段还是轮询实现的只是所有的IO都集中在一个地方这个地方进行轮询; 信号IO:当数据准备完毕的时候信号通知程序数据准备完毕第2阶段阻塞; 异步IO:12都不阻塞
一些例子
阻塞、非阻塞、I/O多路复用socket可以设置为阻塞和非阻塞两种结合select、poll、epoll可以实现I/O多路复用
信号I/O
client.cpp
#include stdio.h
#include stdlib.h
#include string.h
#include unistd.h
#include fcntl.h
#include signal.h
#include sys/types.h
#include sys/socket.h
#include netinet/in.h
#include arpa/inet.h
#include iostreamvolatile sig_atomic_t io_ready 0;
int sockfd -1;void io_handler(int sig) {io_ready 1;
}int main()
{struct sigaction sa;char buffer[1024];// 创建套接字int sockfd socket(AF_INET, SOCK_STREAM, 0);// 向服务器特定的IP和端口发起请求struct sockaddr_in serv_addr;memset(serv_addr, 0, sizeof(serv_addr)); // 每个字节都用0填充serv_addr.sin_family AF_INET; // 使用IPv4地址serv_addr.sin_addr.s_addr inet_addr(127.0.0.1); // 具体的IP地址serv_addr.sin_port htons(1234); // 端口connect(sockfd, (struct sockaddr *)serv_addr, sizeof(serv_addr));// 设置超时时间为5秒struct timeval timeout;timeout.tv_sec 5;timeout.tv_usec 0;setsockopt(sockfd, SOL_SOCKET, SO_RCVTIMEO, (char *)timeout, sizeof(timeout));// 设置套接字为非阻塞模式int flags fcntl(sockfd, F_GETFL, 0);if (fcntl(sockfd, F_SETFL, flags | O_NONBLOCK) -1){perror(fcntl);close(sockfd);exit(EXIT_FAILURE);}// 设置信号处理函数sa.sa_handler io_handler;sa.sa_flags 0;sigemptyset(sa.sa_mask);if (sigaction(SIGIO, sa, NULL) -1){perror(sigaction);close(sockfd);exit(EXIT_FAILURE);}// 设置套接字的所有者if (fcntl(sockfd, F_SETOWN, getpid()) -1){perror(fcntl F_SETOWN);close(sockfd);exit(EXIT_FAILURE);}// 启用信号驱动 I/Oif (fcntl(sockfd, F_SETFL, O_ASYNC) -1){perror(fcntl F_SETFL);close(sockfd);exit(EXIT_FAILURE);}// 等待接收数据while (!io_ready){// 模拟进程正在执行其他任务std::cout 模拟进程正在执行其他任务 std::endl;sleep(1);}// 接收数据ssize_t len recv(sockfd, buffer, sizeof(buffer) - 1, 0);if (len -1){perror(recv);close(sockfd);exit(EXIT_FAILURE);}buffer[len] \0;printf(Received: %s\n, buffer);// 关闭套接字close(sockfd);return 0;
}
server.cpp
#include stdio.h
#include string.h
#include stdlib.h
#include unistd.h
#include arpa/inet.h
#include sys/socket.h
#include netinet/in.h
#include iostream
int main()
{// 创建套接字int serv_sock socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);// 将套接字和IP、端口绑定struct sockaddr_in serv_addr;memset(serv_addr, 0, sizeof(serv_addr)); // 每个字节都用0填充serv_addr.sin_family AF_INET; // 使用IPv4地址serv_addr.sin_addr.s_addr inet_addr(127.0.0.1); // 具体的IP地址serv_addr.sin_port htons(1234); // 端口// 设置端口复用int opt 1;setsockopt(serv_sock, SOL_SOCKET, SO_REUSEADDR, opt, sizeof(opt));bind(serv_sock, (struct sockaddr *)serv_addr, sizeof(serv_addr));// 进入监听状态等待用户发起请求listen(serv_sock, 20);// 接收客户端请求struct sockaddr_in clnt_addr;socklen_t clnt_addr_size sizeof(clnt_addr);int clnt_sock accept(serv_sock, (struct sockaddr *)clnt_addr, clnt_addr_size);// 打印收到的客户端地址端口信息std::cout Client address: inet_ntoa(clnt_addr.sin_addr) , port: ntohs(clnt_addr.sin_port) std::endl;sleep(5);for (int i 0; i 1; i){// 向客户端发送数据char str[] hello client!!!;write(clnt_sock, str, sizeof(str));}sleep(10000);// 关闭套接字close(clnt_sock);close(serv_sock);return 0;
}
上面的例子中server就只做一件事监听客户端连接一旦建立以后等待5s再发送数据
而client首先绑定了信号处理函数当套接字有数据可以读的时候内核会发出SIGIO信号给进程然后执行信号处理函数。将io_ready变量设置为1表示可以读取数据了。然后主循环中的进程可以跳出循环执行接收数据的操作。
一般来说都会设置套接字是非阻塞的但我感觉这里recv实际上已经不会阻塞了因为肯定是有了数据才会执行到这一步的
从源头解释
从TCP发送数据的流程说起
以两个应用程序通讯为例我们来了解一下当“A”向B 发送一条消息简单来说会经过如下流程
第一步应用A把消息发送到 TCP发送缓冲区。
第二步 TCP发送缓冲区再把消息发送出去经过网络传递后消息会发送到B服务器的TCP接收缓冲区。
第三步B再从TCP接收缓冲区去读取属于自己的数据。 阻塞I/O | 非阻塞I/O
我们把视角切换到上面图中的第三步 也就是应用B从TCP缓冲区中读取数据。 思考一个问题
因为应用之间发送消息是间断性的也就是说在上图中TCP缓冲区还没有接收到属于应用B该读取的消息时那么此时应用B向TCP缓冲区发起读取申请TCP接收缓冲区是应该马上告诉应用B 现在没有你的数据还是说让应用B在这里等着直到有数据再把数据交给应用B。
把这个问题应用到第一个步骤也是一样应用A在向TCP发送缓冲区发送数据时如果TCP发送缓冲区已经满了那么是告诉应用A现在没空间了还是让应用A等待着等TCP发送缓冲区有空间了再把应用A的数据访拷贝到发送缓冲区。
什么是阻塞IO
如果上面的问题你已经思考过了那么其实你已经明白了什么是阻塞IO了所谓阻塞IO就是当应用B发起读取数据申请时在内核数据没有准备好之前应用B会一直处于等待数据状态直到内核把数据准备好了交给应用B才结束。
术语描述在应用调用recvfrom读取数据时其系统调用直到数据包到达且被复制到应用缓冲区中或者发送错误时才返回在此期间一直会等待进程从调用到返回这段时间内都是被阻塞的称为阻塞IO
流程
1、应用进程向内核发起recfrom读取数据。
2、准备数据报应用进程阻塞。
3、将数据从内核负责到应用空间。
4、复制完成后返回成功提示。 什么是非阻塞IO
我敢保证如果你已经理解了阻塞IO那么必定已经知道了什么是非阻塞IO。按照上面的思路所谓非阻塞IO就是当应用B发起读取数据申请时如果内核数据没有准备好会即刻告诉应用B不会让B在这里等待。
术语非阻塞IO是在应用调用recvfrom读取数据时如果该缓冲区没有数据的话就会直接返回一个EWOULDBLOCK错误不会让应用一直等待中。在没有数据的时候会即刻返回错误标识那也意味着如果应用要读取数据就需要不断的调用recvfrom请求直到读取到它要的数据为止。即需要自己实现一个轮询比如while又比如epoll ET时候必须设置非阻塞判断EAGAIN
流程
1、应用进程向内核发起recvfrom读取数据。
2、没有数据报准备好即刻返回EWOULDBLOCK错误码。
3、应用进程向内核发起recvfrom读取数据。
4、已有数据包准备好就进行一下 步骤否则还是返回错误码。
5、将数据从内核拷贝到用户空间。
6、完成后返回成功提示。
I/O多路复用
如果你已经明白了非阻塞IO的工作模式那么接下来我们继续了解IO复用模型的产生原因和思路。
思考一个问题
我们还是把视角放到应用B从TCP缓冲区中读取数据这个环节来。如果在并发的环境下可能会N个人向应用B发送消息这种情况下我们的应用就必须创建多个线程去读取数据每个线程都会自己调用recvfrom 去读取数据。那么此时情况可能如下图 如上图一样并发情况下服务器很可能一瞬间会收到几十上百万的请求这种情况下应用B就需要创建几十上百万的线程去读取数据同时又因为应用线程是不知道什么时候会有数据读取为了保证消息能及时读取到那么这些线程自己必须不断的向内核发送recvfrom 请求来读取数据
那么问题来了这么多的线程不断调用recvfrom 请求数据先不说服务器能不能扛得住这么多线程就算扛得住那么很明显这种方式是不是太浪费资源了线程是我们操作系统的宝贵资源大量的线程用来去读取数据了那么就意味着能做其它事情的线程就会少。
所以有人就提出了一个思路能不能提供一种方式可以由一个线程监控多个网络请求我们后面将称为fd文件描述符linux系统把所有网络请求以一个fd来标识这样就可以只需要一个或几个线程就可以完成数据状态询问的操作当有数据准备就绪之后再分配对应的线程去读取数据这么做就可以节省出大量的线程资源出来这个就是IO复用模型的思路。 正如上图IO复用模型的思路就是系统提供了一种函数可以同时监控多个fd的操作这个函数就是我们常说到的select、poll、epoll函数有了这个函数后应用线程通过调用select函数就可以同时监控多个fdselect函数监控的fd中只要有任何一个数据状态准备就绪了select函数就会返回可读状态这时询问线程再去通知处理数据的线程对应线程此时再发起recvfrom请求去读取数据。
术语描述进程通过将一个或多个fd传递给select阻塞在select操作上select帮我们侦测多个fd是否准备就绪当有fd准备就绪时select返回数据可读状态应用程序再调用recvfrom读取数据。 总结复用IO的基本思路就是通过slect或poll、epoll 来监控多fd 来达到不必为每个fd创建一个对应的监控线程从而减少线程资源创建的目的。
信号驱动I/O
复用IO模型解决了一个线程可以监控多个fd的问题但是select是采用轮询的方式来监控多个fd的通过不断的轮询fd的可读状态来知道是否有可读的数据而无脑的轮询就显得有点暴力因为大部分情况下的轮询都是无效的所以有人就想能不能不要我总是去问你是否数据准备就绪能不能我发出请求后等你数据准备好了就通知我所以就衍生了信号驱动IO模型。
于是信号驱动IO不是用循环请求询问的方式去监控数据就绪状态而是在调用sigaction时候建立一个SIGIO的信号联系当内核数据准备好之后再通过SIGIO信号通知线程数据准备好后的可读状态当线程收到可读状态的信号后此时再向内核发起recvfrom读取数据的请求因为信号驱动IO的模型下应用线程在发出信号监控后即可返回不会阻塞所以这样的方式下一个应用线程也可以同时监控多个fd。 术语描述首先开启套接口信号驱动IO功能并通过系统调用sigaction执行一个信号处理函数此时请求即刻返回当数据准备就绪时就生成对应进程的SIGIO信号通过信号回调通知应用线程调用recvfrom来读取数据。 总结 IO复用模型里面的select虽然可以监控多个fd了但select其实现的本质上还是通过不断的轮询fd来监控数据状态 因为大部分轮询请求其实都是无效的所以信号驱动IO意在通过这种建立信号关联的方式实现了发出请求后只需要等待数据就绪的通知即可这样就可以避免大量无效的数据状态轮询操作。
异步I/O
其实经过了上面两个模型的优化我们的效率有了很大的提升但是我们当然不会就这样满足了有没有更好的办法通过观察我们发现不管是IO复用还是信号驱动我们要读取一个数据总是要发起两阶段的请求第一次发送select请求询问数据状态是否准备好第二次发送recevform请求读取数据。
思考一个问题
也许你一开始就有一个疑问为什么我们明明是想读取数据而却非得要先发起一个select询问数据状态的请求然后再发起真正的读取数据请求,能不能有一种一劳永逸的方式我只要发送一个请求我告诉内核我要读取数据然后我就什么都不管了然后内核去帮我去完成剩下的所有事情
当然既然你想得出来那么就会有人做得到有人设计了一种方案应用只需要向内核发送一个read 请求,告诉内核它要读取数据后即刻返回内核收到请求后会建立一个信号联系当数据准备就绪内核会主动把数据从内核复制到用户空间等所有操作都完成之后内核会发起一个通知告诉应用我们称这种一劳永逸的模式为异步IO模型。 术语描述 应用告知内核启动某个操作并让内核在整个操作完成之后通知应用这种模型与信号驱动模型的主要区别在于信号驱动IO只是由内核通知我们合适可以开始下一个IO操作而异步IO模型是由内核通知我们操作什么时候完成。 **总结**异步IO的优化思路是解决了应用程序需要先后发送询问请求、发送接收数据请求两个阶段的模式在异步IO的模式下只需要向内核发送一次请求就可以完成状态询问和数拷贝的所有操作。
再谈IO模型里面的同步异步
我们通常会说到同步阻塞IO、同步非阻塞IO异步IO几种术语通过上面的内容那么我想你现在肯定已经理解了什么是阻塞什么是非阻塞了所谓阻塞就是发起读取数据请求的时当数据还没准备就绪的时候这时请求是即刻返回还是在这里等待数据的就绪如果需要等待的话就是阻塞反之如果即刻返回就是非阻塞。
我们区分了阻塞和非阻塞后再来分别下同步和异步在IO模型里面如果请求方从发起请求到数据最后完成的这一段过程中都需要自己参与那么这种我们就称为同步请求反之如果应用发送完指令后就不再参与过程了只需要等待最终完成结果的通知那么这就属于异步。
我们再看同步阻塞、同步非阻塞他们不同的只是发起读取请求的时候一个请求阻塞一个请求不阻塞但是相同的是他们都需要应用自己监控整个数据完成的过程。而为什么只有异步非阻塞 而没有异步阻塞呢因为异步模型下请求指定发送完后就即刻返回了没有任何后续流程了所以它注定不会阻塞所以也就只会有异步非阻塞模型了。从常规意义上看异步阻塞不存在。