以网络编程为例,默认情况下socket 是 blocking 的,即函数 accept , recvfrom 等,需等待函数执行结束之后才能够返回(此时操作系统切换到其他进程执行) 。accpet 等待到有 client 连接请求并接受成功之后,recvfrom 需要读取完client 发送的数据之后才能够返回
// 创建套接字int serv_sock = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);// IP 和端口配置 ...// ...// 绑定 IP 和端口bind(serv_sock, ...);// 监听来自监听型套接字的请求listen(serv_sock, ...);int clnt_sock;while (1) {// 创建新的通信型套接字用于接收来自客户端的请求,此时会阻塞程序执行,直到有请求到来clnt_sock = accept(serv_sock, ...);// 接收客户端的数据,同步阻塞 I/O,等待数据就绪recvfrom(clnt_sock, ...);// 处理数据handle(data);}?同步非阻塞?图片来源:https://www.51cto.com/article/693213.html
同步非阻塞 I/O 的特点是当用户进程发起网络读请求时,如果内核缓冲区还没接收到客户端数据,会立即返回 EWOULDBLOCK 错误码,而不会阻塞用户进程,用户进程可结合轮询调用方式继续发起 recvfrom 调用,直到数据就绪,然后同步等待数据从内核缓冲区复制到用户空间,然后用户进程进行数据处理 。
同步非阻塞 I/O 的优势在于当发起 I/O 请求时不会阻塞用户进程,一定程度上提升了程序的性能,但是为了及时获取数据的就绪状态,需要频繁轮询,这样也会消耗不小的 CPU 资源 。
以网络编程为例,可设置 socket 为 non-blocking 模式,使用 socket()创建的 socket 默认是阻塞的;可使用函数 fcntl 可设置创建的 socket 为非阻塞的,这样使用原本 blocking 的各种函数(accept、recvfrom),可以立即获得返回结果 。通过判断返回的errno了解状态:
这样就实现同步非阻塞 I/O 请求:
// 创建套接字int serv_sock = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);// 设置 socket 为非阻塞 I/Oint flags = fcntl(serv_sock, F_GETFL, 0);fcntl(serv_sock, F_SETFL, flags | O_NONBLOCK);// IP 和端口配置 ...// ...// 绑定 IP 和端口bind(serv_sock, ...);// 监听来自监听型套接字的请求listen(serv_sock, ...);// 创建新的通信型套接字用于接收来自客户端的请求int clnt_sock;while (1) {// 在non-blocking模式下,如果返回值为-1,且 errno == EAGAIN 或errno == EWOULDBLOCK 表示no connections 没有新连接请求;clnt_sock = accept(serv_sock, ...);if (clnt_sock == -1 && errno == EAGAIN) {fprintf(stderr, "no client connectionsn");continue;} else if (clnt_sock == -1) {perror("accept failed");}// 接收客户端的数据,同步非阻塞 I/O,在non-blocking模式下,如果返回值为-1,且 errno == EAGAIN表示没有可接受的数据或正在接受尚未完成;while (1) {int ret = recvfrom(clnt_sock, ...);if (ret == -1 && errno == EAGAIN) {fprintf(stderr, "no data readyn");continue;} else if (ret == -1) {perror("read failed");}// 处理数据handle(data);}}?I/O 多路复用?图片来源:https://www.51cto.com/article/693213.html
上述两种 I/O 模型均是面向单个客户端连接的,同一时间只能处理一个 client 请求,虽然可以通过多进程/多线程的方法解决,但是多进程/多线程需要考虑额外的资源消耗以及同步互斥的相关问题 。
为了高效解决多个 fd 的状态监听,I/O 多路复用技术应运而生 。
I/O 多路复用的核心思想是可以同时监听多个不同的 fd(网络环境下即是网络套接字),当套接字中的任何一个数据就绪了,就可以通知用户进程,此时用户进程再发起 recvfrom 请求去读取数据 。
以网络编程为例,可通过维护一个需要监听的所有 socket 的 fd 列表,然后调用 select/epoll 等监听函数,如果 fd 列表中所有 socket 都没有数据就绪,则 select/epoll 会阻塞,直到有一个 socket 接收到数据,然后唤醒进程 。
int serv_sock = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);// 设置 socket 为非阻塞 I/Oint flags = fcntl(serv_sock, F_GETFL, 0);fcntl(serv_sock, F_SETFL, flags | O_NONBLOCK);// IP 和端口配置 ...// ...// 绑定 IP 和端口bind(serv_sock, ...);// 监听来自监听型套接字的请求listen(serv_sock, ...);// 存放需要监听的 socket 列表fd_set readfds;// 添加 需要监听的 socket 到 readfdsFD_SET(serv_sock, readfds);// 创建新的通信型套接字用于接收来自客户端的请求int clnt_sock;// 调用 select 返回的结果值int res;// 预计可接受的最大连接数量,select 最大支持 1024 个 fdint maxfd = 1000;while (1) {// 调用 select 阻塞监听 fd 列表,直到有一个 socket 接收到请求,唤醒进程res = select(maxfd + 1, &readfds, ...);if (res == -1) {perror("select failed");exit(EXIT_FAILURE);} else if (res == 0) {fprintf(stderr, "no socket ready for readn");}// 遍历每个 socket,如果是 serv_sock 则 accept,否则进行读操作for (int i = 0; i <= maxfd; i++) {// 是否 socket 是否在 监听的 fd 列表中if (!FD_ISSET(i, &readfds)) {continue;}if (i == serv_sock) {// 当前请求是 server sock,则建立 accept 连接clnt_sock = accpet(serv_sock, ...);// 将新建立的客户端连接添加进行 readfds 监听列表中FD_SET(clnt_sock, &readfds);} else {// 当请求是客户端的 socket,接收客户端的数据,此时数据已经就绪,将数据从内核空间复制到用户空间int ret = recvfrom(i, ...);if (ret == -1 && errno == EAGAIN) {fprintf(stderr, "no data readyn");continue;} else if (ret == -1) {perror("read failed");}// 处理数据handle(data);}}}
推荐阅读
- 十个优质的基于Node.js的CMS 内容管理平台
- Linux系统下如何设置开机自动运行脚本?
- 如何寻找并删除系统里的重复文件,快速释放磁盘空间?
- 用了三年MySQL,还不知道Server层和引擎层是如何交互的?
- 如何提升HTTPS访问速度?网站HTTPS性能优化技巧
- Ngnix如何配置强制HTTPS访问?
- 机器学习系统架构的十个要素
- 从如何更好的监控Oracle共享池谈起
- 如何以及为什么衡量网络安全
- Java 中为什么要设计 throws 关键词,是故意的还是不小心
