高级IO

  • 1. 五种IO模型
    • 1.1 阻塞IO
    • 1.2 非阻塞IO
    • 1.3 信号驱动IO
    • 1.4 多路复用、多路转接
    • 1.5 异步IO
  • 2. 非阻塞
  • 3. I/O多路转接之select
    • 3.1 select执行过程
    • 3.2 select完整代码实现

1. 五种IO模型

讲一个钓鱼的小故事:
钓鱼一共分为两步一步是还有一步是,那么真正有作用的其实是钓,可以把等待理解为无用的。

  • 张三:自己钓鱼的时候,等期间谁也不搭理,就专心的盯着自己的鱼漂是否有咬钩,如果咬钩了就将鱼捞起来。
  • 李四:自己钓鱼的时候,等待期间还不时的询问一下张三是否有钓上来鱼,即使张三一直也不搭理他但是他还是会不间断的过一会询问一下他,等到钓上鱼的时候就把鱼捞起来。
  • 王五:自己钓鱼的时候,给鱼竿上面绑了一个铃铛,他就直接拿起一本书看了起来,他知道当铃铛响的时候就是鱼上钩了,只要上钩就过来把鱼捞起来。
  • 赵六:是一个本地的小土豪很有钱,此时他一下子带了很多很多的鱼竿过来,然后将鱼竿全部撒饵放下去,然后自己就在跟前看有哪一个鱼竿咬钩了,就立马过去将鱼捞起来。
  • 田七:是一个超级大老板,他突然想要吃鱼,然后就给秘书小刘说:“我给你一个鱼竿和桶,你去帮我钓鱼,当桶都钓满的时候你就给我打电话,我过来把你接上,咱们一起去把这个鱼給烤掉”,然后就把小刘放在了鱼塘自己开着车去蒸桑拿了。

1.1 阻塞IO

阻塞IO就是上例中的张三在内核将数据准备好之前, 系统调用会一直等待. 所有的套接字, 默认都是阻塞方式。

阻塞IO是最常见的IO模型.

1.2 非阻塞IO

非阻塞IO就是上例中的李四: 如果内核还未将数据准备好, 系统调用仍然会直接返回, 并且返回EWOULDBLOCK错误码.非阻塞IO往往需要程序员循环的方式反复尝试读写文件描述符, 这个过程称为轮询. 这对CPU来说是较大的浪费, 一般只有特定场景下才使用.

1.3 信号驱动IO

信号驱动IO就是上例的王五: 内核将数据准备好的时候, 使用SIGIO信号通知应用程序进行IO操作。

1.4 多路复用、多路转接

IO多路转接就是上例的赵六: 虽然从流程图上看起来和阻塞IO类似. 实际上最核心在于IO多路转接能够同时等待多个文件描述符的就绪状态.

1.5 异步IO

异步IO就是上例的田七: 由内核在数据拷贝完成时, 通知应用程序(而信号驱动是告诉应用程序何时可以开始拷贝数据).

小结

  • 任何IO过程中, 都包含两个步骤. 第一是等待, 第二是拷贝. 而且在实际的应用场景中, 等待消耗的时间往往都远远高于拷贝的时间. 让IO更高效, 最核心的办法就是让等待的时间尽量少.
  • 同步IO和异步IO最大的区别就是,是否最终要自己进行拷贝数据,如果需要自己拷贝数据那就是同步IO,如果不需要自己进行拷贝数据那就是异步IO,所以前四种都是同步IO。
  • 高级IO(高效IO)本质:等是没有做贡献的,所以尽可能的要减少等的比重
    IO = 等待 + 拷贝
    recv(读IO) = 读时间就绪 + 内核数据拷贝到用户空间
    send(写IO)= 写时间就绪 + 用户数据拷贝到内核空间

同步通信 vs 异步通信
同步和异步关注的是消息通信机制.

  • 所谓同步,就是在发出一个调用时,在没有得到结果之前,该调用就不返回. 但是一旦调用返回,就得到返回值了; 换句话说,就是由调用者主动等待这个调用的结果;
  • 异步则是相反,调用在发出之后,这个调用就直接返回了,所以没有返回结果; 换句话说,当一个异步过程调用发出后,调用者不会立刻得到结果; 而是在调用发出后,被调用者通过状态、通知来通知调用者,或通过回调函数处理这个调用.

阻塞 vs 非阻塞
阻塞和非阻塞关注的是程序在等待调用结果(消息,返回值)时的状态.

  • 阻塞调用是指调用结果返回之前,当前线程会被挂起. 调用线程只有在得到结果之后才会返回.
  • 非阻塞调用指在不能立刻得到结果之前,该调用不会阻塞当前线程

2. 非阻塞

fcntl

一个文件描述符, 默认都是阻塞IO,所来所接触的也都是阻塞IO,并没有接触过非阻塞的接口。

函数原型如下

#include <unistd.h>
#include <fcntl.h>
int fcntl(int fd, int cmd, … /* arg */ );

传入的cmd的值不同, 后面追加的参数也不相同.
fcntl函数有5种功能:

  • 复制一个现有的描述符(cmd=F_DUPFD
  • 获得/设置文件描述符标记(cmd=F_GETFD或F_SETFD).
  • 获得/设置文件状态标记(cmd=F_GETFL或F_SETFL)(最重要).
  • 获得/设置异步I/O所有权(cmd=F_GETOWN或F_SETOWN).
  • 获得/设置记录锁(cmd=F_GETLK,F_SETLK或F_SETLKW)
  • 我们此处只是用第三种功能, 获取/设置文件状态标记, 就可以将一个文件描述符设置为非阻塞

轮询方式读取标准输入

#include<stdio.h>
#include<fcntl.h>
#include<errno.h>
#include<unistd.h>void SetNonBlock(int fd)
{int f1 = fcntl(fd,F_GETFL); //获取文件描述符的属性if(f1 < 0){perror("fcntl error!\n");return;}fcntl(fd,F_SETFL,f1 | O_NONBLOCK);
}int main()
{char c = 0;SetNonBlock(0);while(1){sleep(1);ssize_t s = read(0,&c,1);if(s > 0){printf("%c\n",c);}else if(s < 0 && errno == EAGAIN){printf("read cond not ok!\n");}else {perror("read error!\n");}printf(".........................\n");}return 0;
}

  • 使用F_GETFL将当前的文件描述符的属性出来(这是一个位图).
  • 然后再使用F_SETFL将文件描述符设置回去. 设置回去的同时, 加上一个O_NONBLOCK参数
  • 要知道到底是出错了,还是内核的接受缓冲区没有就绪
  • EAGAIN是try again的意思,是一个错误码的宏定义

3. I/O多路转接之select

select的核心作用就是等,就绪事件通知方式,单进程内,同时等待多个文件描述符。

系统提供select函数来实现多路复用输入/输出模型.

  • select系统调用是用来让我们的程序监视多个文件描述符的状态变化的;
  • 程序会停在select这里等待,直到被监视的文件描述符有一个或多个发生了状态改变

select函数原型
select的函数原型如下:

#include <sys/select.h>

 int select(int nfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout)

参数解释:

  • 参数nfds是需要监视的最大的文件描述符值+1;(假设此时文件描述符是1和7,那么这个值就是8)
  • rdset,wrset,exset分别对应于需要检测的可读文件描述符的集合可写文件描述符的集合及异常文件描述符的集合;
  • 参数timeout为结构timeval,用来设置select()的等待时间

参数timeout取值:

  • NULL(阻塞方式等):则表示select()没有timeout,select将一直被阻塞,直到某个文件描述符上发生了事件;
  • 0(非阻塞方式等)仅检测描述符集合的状态,然后立即返回,并不等待外部事件的发生
  • 特定的时间值:如果在指定的时间段里没有事件发生,select将超时返回。

关于fd_set结构

  • 其实这个结构就是一个整数数组, 更严格的说, 是一个 “位图. 使用位图中对应的位来表示要监视的文件描述符.
  • fd_set:位图结构,比特位的位置代表是哪一个文件描述符,就以readfds为例,这个参数即使输入型参数也是输出型参数,比特位的内容(输入):是否需要关心特定文件描述符的读事件就绪(用户想告诉操作系统:你应该帮我关心那些文件描述符读事件就绪),比特位的内容(输出):所关心的特定文件描述符的读事件已经就绪(操作系统告诉用户) ,此时应该对返回的位图进行遍历检查那个比特位是1,说明该文件描述符读时间已经就绪,不好的地方就在于,下一次需要对这个参数重新设定,恢复你需要特别关心的是那几个文件描述符。

提供了一组操作fd_set的接口, 来比较方便的操作位图

  • void FD_CLR(int fd, fd_set *set); // 用来清除描述词组set中相关fd 的位
  • int FD_ISSET(int fd, fd_set *set); // 用来测试描述词组set中相关fd 的位是否为真
  • void FD_SET(int fd, fd_set *set); // 用来设置描述词组set中相关fd的位
  • void FD_ZERO(fd_set *set); // 用来清除描述词组set的全部位

关于timeval结构

  • timeval结构用于描述一段时间长度,如果在这个时间内,需要监视的描述符没有事件发生则函数返回,返回值为0

函数返回值

  • 执行成功则返回文件描述词状态已改变的个数
  • 如果返回0代表在描述词状态改变前已超过timeout时间,没有返回
  • 当有错误发生时则返回-1,错误原因存于errno,此时参数readfds,writefds, exceptfds和timeout的值变成不可预测。

错误值可能为(但是一般很少情况下才会出错,所以目前先不看):

  • EBADF 文件描述词为无效的或该文件已关闭
  • EINTR 此调用被信号所中断
  • EINVAL 参数n 为负值。
  • ENOMEM 核心内存不足

3.1 select执行过程

理解select模型的关键在于理解fd_set,为说明方便,取fd_set长度为1字节,fd_set中的每一bit可以对应一个文件描述符fd。则1字节长的fd_set最大可以对应8个fd.

  • (1)执行fd_set set; FD_ZERO(&set);则set用位表示是0000,0000。
  • (2)若fd=5,执行FD_SET(fd,&set);后set变为0001,0000(第5位置为1)
  • (3)若再加入fd=2,fd=1,则set变为0001,0011
  • (4)执行select(6,&set,0,0,0)阻塞等待
  • (5)若fd=1,fd=2上都发生可读事件,则select返回,此时set变为0000,0011。注意:没有事件发生的fd=5被清空

socket就绪条件

读就绪

  • socket内核中, 接收缓冲区中的字节数, 大于等于低水位标记SO_RCVLOWAT(并不是接收缓冲区只要一有数据,就立马读,而是设置了一个低水位标记,为了保证读取的效率). 此时可以无阻塞的读该文件描述符, 并且返回值大于0;
  • socket TCP通信中, 对端关闭连接, 此时对该socket读, 则返回0;
  • 监听的socket上有新的连接请求,这个请求是以读事件报告给操作系统的;
  • socket上有未处理的错误;

写就绪

  • socket内核中, 发送缓冲区中的可用字节数(发送缓冲区的空闲位置大小), 大于等于低水位标记SO_SNDLOWAT, 此时可以无阻塞的写, 并且返回值大于0;
  • socket的写操作被关闭(close或者shutdown). 对一个写操作被关闭的socket进行写操作, 会触发SIGPIPE信号;
  • socket使用非阻塞connect连接成功或失败之后;
  • socket上有未读取的错误;

3.2 select完整代码实现

main.cc

#include"SelectServer.hpp"void Usage(std::string proc)
{cout << "Usage :\n\t" << proc << "port"<< endl;
}//Server port
int main(int argc,char *argv[])
{if(argc != 2){Usage(argv[0]);exit(1);}SelectServer *ssvr = new SelectServer(atoi(argv[1]));ssvr->InitServer();ssvr->Start();return 0;
}

Sock.hpp

#pragma once #include<stdlib.h>
#include<strings.h>
#include<unistd.h>#include<sys/types.h>
#include<sys/socket.h>
#include<netinet/in.h>
#include<arpa/inet.h>
#include<sys/select.h>#include<iostream>
#include<string>
using namespace std;#define BACKLOG 5
class Sock
{public:static int Socket(){int sock = socket(AF_INET,SOCK_STREAM,0);if(sock < 0){cerr << "socket error!" << endl;exit(2);}return sock;}static void Bind(int sock,int port){struct sockaddr_in local;bzero(&local,sizeof(local));local.sin_family = AF_INET;local.sin_port = htons(port);local.sin_addr.s_addr = htonl(INADDR_ANY);if(bind(sock,(struct sockaddr*)&local,sizeof(local)) < 0){cerr << "bind error!"<< endl;exit(3);}}static void Listen(int sock){if(listen(sock,BACKLOG) < 0){cerr << "listen error!"<< endl;exit(4);}}static int Accept(int sock){struct sockaddr_in peer;socklen_t len = sizeof(peer);int fd = accept(sock,(struct sockaddr*)&peer,&len);if(fd < 0){cerr << "accept error!"<<endl;}return fd;}static void SetsockOpt(int sock){//因为对于server来说,主动断开连接的时候会进入time_wait状态,所以要端口复用int opt = 1;setsockopt(sock,SOL_SOCKET,SO_REUSEADDR,&opt,sizeof(opt));}
};

SelectServer.hpp

#pragma once#include"Sock.hpp"#define DFL_PORT 8080
#define NUM (sizeof(fd_set)*8)
#define DFL_FD -1class SelectServer
{private:int port;int lsock;//需要一个辅助的数组,需要把所有打开的文件描述符都保存起来int fd_array[NUM];public:SelectServer(int _p = DFL_PORT):port(_p){}void InitServer(){for(int i = 0;i < NUM;++i){fd_array[i] = DFL_FD;}lsock = Sock::Socket();Sock::SetsockOpt(lsock); //将listen socket 设置为端口复用Sock::Bind(lsock,port);Sock::Listen(lsock);fd_array[0] = lsock;}//找到数组中默认-1的,然后把这个位置给占掉void AddFd2Array(int sock){int i = 0;for( ; i < NUM; i++){if(fd_array[i] ==DFL_FD){break;}}//有可能不断的来新链接,然后把数组给弄满了if(i == NUM){cerr << "fd array is full,close sock" << endl;close(sock);}else {fd_array[i] = sock;cout << "fd: " << sock << " add to select..."<< endl;}}void DefFdFromArray(int index){if(index >=0 && index < NUM){fd_array[index] = DFL_FD;}}void HandlerEvents(fd_set *rfds){for(int i =0;i < NUM;++i){if(fd_array[i] == DFL_FD){continue;}//走到这里说明有文件描述符发生了改变,并判断该文件描述符是否属于原来的rfds集合中//只有你设置了关心的文件描述符,在返回的时候才会发生改变,没有设置的他压根不在乎if(FD_ISSET(fd_array[i],rfds)){//read ready//其实read ready就绪依旧存在两种情况,一种就是数据准备就绪了,还有一种情况是有了新链接if(fd_array[i] == lsock)//link event {int sock = Sock::Accept(lsock);if(sock >= 0){//sock ok//获得一个新的文件描述符,一定不敢直接的读取,如果对方不给你发数据,那就直接阻塞了cout << "get a new link ... "<< endl;AddFd2Array(sock);}}//date ready event 应该进行IOelse {char buffer[1024];ssize_t s = recv(fd_array[i],buffer,sizeof(buffer),0);if(s > 0){buffer[s] = 0;cout << "client# " << buffer << endl;}else if(s == 0){cout << "client quit" << endl;close(fd_array[i]);//还需要把这个文件描述符从全局数组fd_array中删除掉DefFdFromArray(i);}else{cout << "fd error!"<< endl;close(fd_array[i]);DefFdFromArray(i);}}}}}void Start(){int maxfd = DFL_FD;//服务器一启动的时候文件描述符就只有lsock一个//timeout 能够设置三种值 当NULL表示没有timeout,一直阻塞式的等,当0表示非阻塞状态,当是一个确切的数值的时候,表示等特定的时间,如果时间到了没有时间发送,那么就超时返回for(;;){//对于select来说,每一次都要重新的去设置这个位图结构的fd_setfd_set rfds;FD_ZERO(&rfds);cout << "fd_array: ";for(int i = 0;i < NUM; i++){if(fd_array[i] != DFL_FD){cout <<fd_array[i]<< " ";FD_SET(fd_array[i],&rfds);if(maxfd < fd_array[i]){maxfd = fd_array[i];}}}cout << endl;//这个timeout也是一个输入、输出型参数,第一次会等待5s,但是由于第一次没有事件发生timeout减为0,第二次及以后都不在等待,所以我们要每次进来都设置一次printf("begin select...\n");//struct timeval timeout = {5,0};switch(select(maxfd+1,&rfds,nullptr,nullptr, nullptr)){case 0:cout << "timeout"<< endl;break;case -1:cerr << "select error"<< endl;break;default://此时的select的文件描述符时大于0//successHandlerEvents(&rfds);break;}}}~SelectServer(){}
};


缺点

  1. 监视的文件描述符是由上限的。(可监控的文件描述符个数取决与sizeof(fd_set)的值. 每bit表示一个文件描述符,则我服务器上支持的最大文件描述符是128*8=1024)
  2. 每次都要进行文件描述符的主动添加,过于麻烦
  3. 每次调用select,都需要把fd集合从用户态拷贝到内核态,这个开销在fd很多时会很大
  4. 同时每次调用select都需要在内核遍历传递进来的所有fd,这个开销在fd很多时也很大
  5. select支持的文件描述符数量太小

网络编程---I/O多路转接之select相关推荐

  1. Linux下I/O多路转接之select --fd_set

    fd_set 你终于还是来了,能看到这个标题进来的,我想,你一定是和我遇到了一样的问题,一样的疑惑,接下来几个小时,我一定竭尽全力,写出我想说的,希望也正是你所需要的: 关于Linux下I/O多路转接 ...

  2. I/O多路转接之select

    I/O多路转接之select 文章目录 I/O多路转接之select 一.五种IO模型 二.I/O多路转接之select原理 一.五种IO模型 阻塞IO: 在内核将数据准备好之前, 系统调用会一直等待 ...

  3. Linux网络编程---I/O复用模型之select

    https://blog.csdn.net/men_wen/article/details/53456435 Linux网络编程-I/O复用模型之select 1. IO复用模型 IO复用能够预先告知 ...

  4. 详解I/O多路转接之select

    什么是多路转接IO 对大量的描述符进行I/O事件监控-可以告诉进程现在有哪些描述符就绪了,然后进行就可以只针对就绪了的描述符进行响应操作,避免对没有就绪的I/O操作所导致的效率降低和流程阻塞. IO事 ...

  5. UNIX网络编程:I/O复用技术(select、poll、epoll)

    http://blog.csdn.net/dandelion_gong/article/details/51673085 Unix下可用的I/O模型一共有五种:阻塞I/O .非阻塞I/O .I/O复用 ...

  6. 【Linux网络编程学习】I/O多路复用——select和poll

    此为牛客Linux C++课程和黑马Linux系统编程笔记. 0. I/O多路复用 所谓I/O就是对socket提供的内存缓冲区的写入和读出. 多路复用就是指程序能同时监听多个文件描述符. 之前的学习 ...

  7. I/O多路转接之 select

    系统提供select函数来实现多路复用输入/输出模型. 作用:select系统调用是用来让我们的程序监视多个文件句柄的状态变化的.程序会停在select这里等待,直到被监视的文件句柄有一个或多个发生了 ...

  8. Linux网络编程一步一步学-select详解

    select系统调用是用来让我们的程序监视多个文件描述符(file descriptor)的状态变化的.程序会停在select这里等待,直到被监视的文件描述符有某一个或多个发生了状态改变. selec ...

  9. Linux:I/O多路转接之select(有图有代码有真相!!!)

    一.select引入 一次 I/O 分为两个部分:1)等待数据就绪      2)进行数据转移 1.select 原理: select的原理就是减少等待数据就绪的比重,巧妙的利用等待队列机制让用户进程 ...

  10. 高级IO(多路转接之select、poll、epoll->核反应堆模式)

    ꧁ 大家好,我是 兔7 ,一位努力学习C++的博主~ ꧂ ☙ 如果文章知识点有错误的地方,请指正!和大家一起学习,一起进步❧

最新文章

  1. 微信开放平台 公众号第三方平台开发 教程一 平台介绍
  2. HTML5对音频的支持
  3. matlab矩阵连接图解
  4. 协方差、协方差矩阵的解释意义
  5. 全球及中国焦炉气制天然气用催化剂行业前景动态与未来可行性研究报告2022版
  6. git如何强制用远程分支更新本地
  7. 最优化课堂笔记08——非线性规划中的一些其他方法(考试你懂得)
  8. 洛谷P3803 【模板】多项式乘法(FFT)
  9. Java-volatile是如何实现的
  10. android极光推送声音,Android 极光推送JPush---自定义提示音
  11. 关于AJAX的安全性
  12. python 的基础 学习 第四天 基础数据类型
  13. (转)如何看待美国监管机构要求文艺复兴基金提交源码?
  14. Python识别中国工作日,节假日,调休日。—已更新2022年
  15. 甩开炎热去15℃的四川秘境度假,这里有藏于田园风景的纯白民宿
  16. android第三方库适配鸿蒙,鸿蒙第三方适配rom
  17. 大学计算机课桌面弄毛玻璃,高校换上新课桌,同学表示“世界观被颠覆”,网友:黑科技的诞生...
  18. 二叉排序树详解及实现
  19. Linux Kernel 6.0 CXL Core pci.c 详解
  20. c语言间隔输出菱形图案,c语言输出菱形图案

热门文章

  1. python extract_convert.py对应代码解读抽取式提取+生成式提取摘要代码解读------摘要代码解读1
  2. 43. TA镜像文件的签名
  3. C语言求 阶乘 5!
  4. 详细游戏建模,入门要领及学习方法。
  5. CommonAPI 使用说明文档
  6. 开源看板 wekan docker-compose部署
  7. 关于广告投放系统:竞价策略(2018)
  8. mysql 重置密码_mysql忘记密码如何重置密码,以及修改root密码的三种方法
  9. 西工大计算机课程表,工大、高新、交大、爱知等7所名校初一作息时间表课表新鲜出炉!...
  10. init cloudchannel failerr:10102 - message:参数无效