1. 五种IO模型
同步IO:
- 阻塞式IO (像 read , write ... 都是阻塞IO)
- 非阻塞式IO (非阻塞轮询等待)
- 信号驱动式IO (信号来了,才知道数据就绪了)
- 多路复用/多路转接 (多个读写端共同非阻塞轮询一起等待)
异步IO:
- 异步IO:发起IO,但是交给别人完成,完成的时候通知自己读取数据(由内核在数据拷贝完成时, 通知应用程序(而信号驱动是告诉应用程序何时可以开始拷贝数据)
注意:
- 任何IO过程中, 都包含两个步骤. 第一是等待 , 第二是拷贝
- 在实际的应用场景中, 等待消耗的时间往往都远远高于拷贝的时间. 让IO更高效, 最核心的办法就是让等待的时间尽量少
2. 高级IO重要概念
(一)同步通信 与 异步通信
同步和异步关注的是消息通信机制
同步:在发出一个调用时,没有得到结果之前,该调用就不返回. 但是一旦调用返回,就得到返回值了,即由调用者主动等待这个调用的结果
异步:调用在发出之后,这个调用就直接返回了,所以没有返回结果,即当一个异步过程调用发出后,调用者不会立刻得到结果,而是在调用发出后,被调用者通过状态、通知来通知调用者,或通过回调函数处理这个调用
(二)阻塞 vs 非阻塞
阻塞和非阻塞关注的是程序在等待调用结果(消息,返回值)时的状态
阻塞调用是指调用结果返回之前,当前线程会被挂起. 调用线程只有在得到结果之后才会返回
非阻塞调用指在不能立刻得到结果之前,该调用不会阻塞当前线程
3. 非阻塞IO
fcntl 函数
#include <unistd.h>
#include <fcntl.h>
int fcntl(int fd, int cmd, ... /* arg */ );
函数参数:
fd : 文件描述符
cmd : 可以设置不同值,有不同作用
cmd 设置:
复制一个现有的描述符(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)
代码(非阻塞IO)
void SetNoBlock(int fd) { int fl = fcntl(fd, F_GETFL); //得到 fd 中原来的文件状态标记 if (fl < 0) { perror("fcntl"); return; } fcntl(fd, F_SETFL, fl | O_NONBLOCK); // 既保留原来的文件状态标志,也增加设置了非阻塞 }
注意:
- cmd 设置 O_NONBLOCK 代表非阻塞
- cmd 设置一个fd成为非阻塞IO时,当 read读取数据为空时,不会阻塞,而是设置 errno 为 EWOULDBLOCK
4. IO多路转接 -- 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);
参数:
nfd : 需要监视的最大的文件描述符值+1
readfds , writefds , exceptfds :都是输入输出型参数
timeout : 输入输出型参数 ,表示等待的最长时间,是一个结构体,如果期间没有接收到有数据就绪,时间一到,函数会自动返回
返回值:返回值 > 0,代表有多少数据就绪 ; 返回值 = 0,在设置的等待最长时间内,没有数据就绪 ; 返回值 = -1,在等待过程中,有 IO出现错误
注意:
- readfds , writefds , exceptfds 都是输入输出型参数,实际上传入的类似一张位图,以readfds参数举例,传入的位图代表这张位图,所有设置过的位置(1代表被设置,0代表没被设置 ),都需要判断是否读就绪(读缓冲区是否有数据),传出的位图代表,只有就绪的位置会被设置,输入输出代表含义不一样 ,如果传入设置成NULL,即代表不关心读缓冲区是否有数据,writefds , exceptfds 的参数设置和前面的类似
- timeout 输入等待时间,输出代表剩下的时间,如:设置 5 s ,有一个数据就绪,函数就会返回,等待了 2 s , 输出得到的就是 3 s 。 如果设置成 NULL,即阻塞等待,直到有一个数据就绪
timeval 结构体:
提供了一组操作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的全部位
(三)select 使用
代码 (对连接和读做了处理)
#include"SelectServer.h" int main() { unique_ptr<select_server> str(new select_server()); str->Init(); str->start(); } #include"server.h" #include <sys/select.h> class select_server { public: select_server() { for(int i = 0;i < 1024;i++) { blank[i] = -1; } } void Init() { _listen.start(); } void Handler(fd_set& set) { if (FD_ISSET(blank[0], &set)) { int lietent_fd = _listen.ListenFd(); sockaddr_in x; socklen_t len = sizeof(x); int fd = accept(lietent_fd, (struct sockaddr *)&x, &len); if (fd < 0) { cerr << "accept fail" << endl; return; } else { int i = 1; for (; i < 1024; i++) { if (blank[i] == -1) { cout << "blank[i] = fd" << endl; blank[i] = fd; break; } } if (i == 1024) { cout << "over buff" << endl; close(fd); } } } for(int i = 1;i < 1024;i++) { if(blank[i] == -1) { continue; } if(FD_ISSET(blank[i],&set)) { char buff[1024]; int n = read(blank[i],buff,sizeof(buff) - 1); if(n > 0) { buff[n] = 0; cout << buff << endl; } else if(n == 0) { cout << "write close" << endl; close(blank[i]); blank[i] = -1; } else { cerr << " read fail " << endl; } } } } void start() { int max_fd = -1; int lietent_fd = _listen.ListenFd(); blank[0] = lietent_fd; fd_set set; while (true) { FD_ZERO(&set); for(int i = 0;i < 1024;i++) { if(blank[i] != -1) { max_fd = max(max_fd,blank[i]); FD_SET(blank[i], &set); } } timeval time({5, 0}); int n = select(max_fd + 1, &set, nullptr, nullptr, &time); if (n > 0) { cout << " get a link ... " << endl; Handler(set); } else if (n == 0) { cout << " wait ... ... " << endl; } else { cerr << "select fail " << endl; } } } ~select_server() {} private: int blank[1024]; server _listen; }; #include<iostream> #include<sys/types.h> #include<sys/socket.h> #include<netinet/in.h> #include<arpa/inet.h> #include<unistd.h> #include<signal.h> #include<memory> using namespace std; class server { public: server(const uint16_t port = 8080,const string& ip = "0.0.0.0") :server_port(port) ,server_ip(ip) {} void start() { //构建套接字 server_fd = socket(AF_INET,SOCK_STREAM,0); if(server_fd < 0) { cout << "server scoket fail" << endl; exit(1); } else { cout << "socket success" << endl; } //绑定端口号 sockaddr_in server; server.sin_family = AF_INET; server.sin_port = htons(server_port); server.sin_addr.s_addr = inet_addr(server_ip.c_str()); socklen_t len = sizeof(server); int tmp = bind(server_fd,(struct sockaddr*)&server,len); if(tmp < 0) { cout << "server bind fail" << endl; exit(2); } //开始监听 int n = listen(server_fd,10); if(n < 0) { cout << "server listen fail" << endl; exit(3); } } void run() { signal(SIGCHLD,SIG_IGN); while(true) { // 等待客户端 sockaddr_in client; socklen_t len = sizeof(client); int cilent_id = accept(server_fd, (struct sockaddr *)&client, &len); if(cilent_id < 0) { cout << "server : accept fail" << endl; } cout << "get a new link ...." << endl; int id = fork(); if (id == 0) { close(server_fd); while (true) { // 读取数据 char buff[1024]; int n = read(cilent_id, buff, sizeof(buff)); if (n < 0) { cout << "server read fail" << endl; break; } buff[n] = 0; cout << buff << endl; // 写入数据 string s = "server say : "; s += buff; write(cilent_id, s.c_str(), s.size()); } exit(0); } close(cilent_id); } } int ListenFd() { return server_fd; } private: int server_fd; uint16_t server_port; string server_ip; };
(四)select 缺点
- 等待的fd是有上限的(最多只能等待1024个)
- 输入输出型参数较多,数据拷贝频繁
- 输入输出型参数较多,每次都要对关心的数据进行事件重置(重新设置关心的读数据,写数据,或者出错数据)
- 用户层使用第三方数组管理用户fd,需要遍历很多次;内核检测fd是否就绪,也需要遍历很多次
5. IO多路转接 -- poll
(一)poll函数
poll 函数
#include <poll.h>
int poll(struct pollfd *fds, nfds_t nfds, int timeout);
参数:
fds:一个结构体地址,存放fd信息,关心数据信息,是否关心数据就绪信息
nfds:struct pollfd 结构体个数
timeout : 等待时间 (设置成 0 ,是非阻塞等待;设置成 - 1,是阻塞等待)
返回值:
返回值小于0, 表示出错; 返回值等于0, 表示poll函数等待超时; 返回值大于0, 表示poll由于文件描述符就绪而返回
注意:
timeout 参数代表的数据,单位是毫秒
// pollfd结构
struct pollfd {
int fd; /* file descriptor */
short events; /* requested events */
short revents; /* returned events */
};
struct pollfd
{
int fd; //fd信息
short events; /* requested events */
short revents; /* returned events */
};
注意:
这些取值实际上是宏
(二)poll 使用
代码 (对连接和读做了处理)
#include"PollServer.h" int main() { unique_ptr<poll_server> ptr(new poll_server()); ptr->Init(); ptr->start(); return 0; } #include"server.h" #include"poll.h" class poll_server { public: poll_server() { for(int i = 0;i < 1024;i++) { polls[i].fd = -1; } } void Init() { sv.Init(); } void Handler() { for(int i = 0;i < 1024;i++) { int fd = polls[i].fd; if(fd == -1) { continue; } else if(fd == sv.ListenFd()) { if(polls[i].revents & POLL_IN) { int client_fd = sv.Accept(); for(int j = 1;j < 1024;j++) { if(polls[j].fd == -1) { polls[j].events = POLL_IN;; polls[j].fd = client_fd; break; } } } } else { if(polls[i].revents & POLL_IN) { //读数据 char buff[1024]; int n = read(fd,buff,sizeof(buff) - 1); if(n > 0) { buff[n] = 0; cout << buff << endl; } else if(n == 0) { close(polls[i].fd); polls[i].fd = -1; cout << "client read close" << endl; } else { close(polls[i].fd); polls[i].fd = -1; cout << "read fail" << endl; } } } } } void start() { int fd = sv.ListenFd(); polls[0].fd = fd; polls[0].events = POLL_IN; while (true) { int n = poll(polls, 1024, 3000); if (n > 0) { Handler(); } else if (n == 0) { cout << "wait ..." << endl; } else { cerr << "poll fail" << endl; } } } ~poll_server() {} private: server sv; pollfd polls[1024]; }; #include<iostream> #include<sys/types.h> #include<sys/socket.h> #include<netinet/in.h> #include<arpa/inet.h> #include<unistd.h> #include<signal.h> #include<memory> using namespace std; class server { public: server(const uint16_t port = 8080,const string& ip = "0.0.0.0") :server_port(port) ,server_ip(ip) {} void Init() { //构建套接字 server_fd = socket(AF_INET,SOCK_STREAM,0); if(server_fd < 0) { cout << "server scoket fail" << endl; exit(1); } else { cout << "socket success" << endl; } //绑定端口号 sockaddr_in server; server.sin_family = AF_INET; server.sin_port = htons(server_port); server.sin_addr.s_addr = inet_addr(server_ip.c_str()); socklen_t len = sizeof(server); int tmp = bind(server_fd,(struct sockaddr*)&server,len); if(tmp < 0) { cout << "server bind fail" << endl; exit(2); } //开始监听 int n = listen(server_fd,10); if(n < 0) { cout << "server listen fail" << endl; exit(3); } } int Accept() { sockaddr_in client; socklen_t len = sizeof(client); int cilent_id = accept(server_fd, (struct sockaddr *)&client, &len); if (cilent_id < 0) { cout << "server : accept fail" << endl; } else { cout << "get a new link ...." << endl; } return cilent_id; } void run() { signal(SIGCHLD,SIG_IGN); while(true) { int cilent_id = Accept(); // 等待客户端 int id = fork(); if (id == 0) { close(server_fd); while (true) { // 读取数据 char buff[1024]; int n = read(cilent_id, buff, sizeof(buff)); if (n < 0) { cout << "server read fail" << endl; break; } buff[n] = 0; cout << buff << endl; // 写入数据 string s = "server say : "; s += buff; write(cilent_id, s.c_str(), s.size()); } exit(0); } close(cilent_id); } } int ListenFd() { return server_fd; } private: int server_fd; uint16_t server_port; string server_ip; };
(三)poll的缺点
poll中监听的文件描述符数目增多时,和select函数一样,poll返回后,需要轮询pollfd来获取就绪的描述符. 因此随着监视的描述符数量的增长, 其效率也会线性下降
6. IO多路转接 -- epoll
(一)epoll初识
是为处理大批量等待fd而作了改进的poll
优化了 select 和 poll 的缺点
(二)epoll的相关系统调用
epoll_create 函数
作用
创建一个epoll的模型
原型
int epoll_create(int size);
参数:
size : 填写的值大于0即可
注意:
用完之后, 必须调用close()关闭
epoll_ctl 函数
作用
epoll的事件注册函数,增减关心的数据
原型
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
参数:
epfd : epoll_create()的返回值(epoll的句柄).
op :表示动作, 用三个宏来表示
fd : 需要监听的fd
event : 告诉内核需要监听什么事
第二个参数的取值:
EPOLL_CTL_ADD :注册新的fd到epfd中
EPOLL_CTL_MOD :修改已经注册的fd的监听事件
EPOLL_CTL_DEL :从epfd中删除一个fd
struct epoll_event结构如图:
events可以是以下几个宏的集合:
EPOLLIN : 表示对应的文件描述符可以读 (包括对端SOCKET正常关闭)
EPOLLOUT : 表示对应的文件描述符可以写
EPOLLPRI : 表示对应的文件描述符有紧急的数据可读 (这里应该表示有带外数据到来)
EPOLLERR : 表示对应的文件描述符发生错误
EPOLLHUP : 表示对应的文件描述符被挂断
EPOLLET : 将EPOLL设为边缘触发(Edge Triggered)模式, 这是相对于水平触发(Level Triggered)来说的
EPOLLONESHOT:只监听一次事件, 当监听完这次事件之后, 如果还需要继续监听这个socket的话, 需要
再次把这个socket加入到EPOLL队列里
epoll_wait 函数
作用
收集在epoll监控的事件中已经发送的事件
原型
int epoll_wait(int epfd, struct epoll_event * events, int maxevents, int timeout);
参数:
epfd :epoll_create() 的返回值
events : 是分配好的epoll_event结构体数组
maxevents : 告诉内核这个events有多大
timeout :超时时间 (毫秒,0会立即返回,-1是永久阻塞)
返回值:
函数调用成功,返回对应IO上已准备好的文件描述符数目;返回0表示已超时;返回小于0表示函数失败
注意:
epoll将会把发生的事件赋值到events数组中 (events不可以是空指针,内核只负责把数据复制到这个events数组中,不会去帮助我们在用户态中分配内存).
(三)epoll 工作原理
实际上,创建epoll,等于创建一个文件,文件里面储存了上述信息
(四)epoll 使用
代码
#include"EpollServer.h" int main() { unique_ptr<epoll_server> ptr(new epoll_server()); ptr->Init(); ptr->start(); return 0; } #include"server.h" #include <sys/epoll.h> class epoll_server { public: epoll_server() { for(int i = 0;i < 1024;i++) { events[i].data.fd = -1; } } void Init() { sv.Init(); epoll_fd = epoll_create(2); if( epoll_fd < 0) { cerr << "epoll_create fail" << endl; exit(1); } cout << "epoll_fd :" << epoll_fd << endl; } void EpollCtl(int op,int fd,epoll_event* event) { if(op == EPOLL_CTL_DEL) { //删除最后一个传参是 nullptr,为了更方便显示细节,就不合并了 int n = epoll_ctl(epoll_fd,op,fd,nullptr); if(n < 0) { cerr << "epoll_ctl delete fail" << endl; } } else { int n = epoll_ctl(epoll_fd,op,fd,event); if(n < 0) { cerr << "epoll_ctl add fail" << endl; } } } void Handler(int n) { for (int i = 0; i < n; i++) { int fd = events[i].data.fd; cout << "fd: "<< fd << endl; if (fd == sv.ListenFd() && events[i].events & EPOLLIN) { int client_fd = sv.Accept(); epoll_event event; event.events = EPOLLIN; event.data.fd = client_fd; cout << "client_fd:" << client_fd << endl; EpollCtl(EPOLL_CTL_ADD,client_fd, &event); } else { if(events[i].events & EPOLLIN) { //写端 char buff[1024]; int n = read(fd,buff,sizeof(buff) - 1); if(n > 0) { buff[n] = 0; cout << buff << endl; } else if(n == 0) { EpollCtl(EPOLL_CTL_DEL,fd, nullptr); close(fd); cerr << "write close" << endl; } else { EpollCtl(EPOLL_CTL_DEL,fd, nullptr); close(fd); cerr << "read fail" << endl; } } } } } void start() { int fd = sv.ListenFd(); epoll_event event; event.events = EPOLLIN; event.data.fd = fd; EpollCtl(EPOLL_CTL_ADD,fd,&event); while (true) { int n = epoll_wait(epoll_fd, events, 1024, 3000); if (n > 0) { Handler(n); } else if (n == 0) { cout << "wait ..." << endl; } else { cerr << "epoll_wait fail" << endl; } } } ~epoll_server() {} private: int epoll_fd; epoll_event events[1024]; server sv; }; #include<iostream> #include<sys/types.h> #include<sys/socket.h> #include<netinet/in.h> #include<arpa/inet.h> #include<unistd.h> #include<signal.h> #include<memory> using namespace std; class server { public: server(const uint16_t port = 8080,const string& ip = "0.0.0.0") :server_port(port) ,server_ip(ip) {} void Init() { //构建套接字 server_fd = socket(AF_INET,SOCK_STREAM,0); if(server_fd < 0) { cout << "server scoket fail" << endl; exit(1); } else { cout << "socket success" << endl; } //绑定端口号 sockaddr_in server; server.sin_family = AF_INET; server.sin_port = htons(server_port); server.sin_addr.s_addr = inet_addr(server_ip.c_str()); socklen_t len = sizeof(server); int tmp = bind(server_fd,(struct sockaddr*)&server,len); if(tmp < 0) { cout << "server bind fail" << endl; exit(2); } //开始监听 int n = listen(server_fd,10); if(n < 0) { cout << "server listen fail" << endl; exit(3); } } int Accept() { sockaddr_in client; socklen_t len = sizeof(client); int cilent_id = accept(server_fd, (struct sockaddr *)&client, &len); if (cilent_id < 0) { cout << "server : accept fail" << endl; } else { cout << "get a new link ...." << endl; } return cilent_id; } void run() { signal(SIGCHLD,SIG_IGN); while(true) { int cilent_id = Accept(); // 等待客户端 int id = fork(); if (id == 0) { close(server_fd); while (true) { // 读取数据 char buff[1024]; int n = read(cilent_id, buff, sizeof(buff)); if (n < 0) { cout << "server read fail" << endl; break; } buff[n] = 0; cout << buff << endl; // 写入数据 string s = "server say : "; s += buff; write(cilent_id, s.c_str(), s.size()); } exit(0); } close(cilent_id); } } int ListenFd() { return server_fd; } private: int server_fd; uint16_t server_port; string server_ip; };
(五)epoll的优点(和 select 的缺点对应)
- 接口使用方便: 虽然拆分成了三个函数, 但是反而使用起来更方便高效. 不需要每次循环都设置关注的文件描述符, 也做到了输入输出参数分离开
- 数据拷贝轻量: 只在合适的时候调用 EPOLL_CTL_ADD 将文件描述符结构拷贝到内核中, 这个操作并不频繁(而select/poll都是每次循环都要进行拷贝)
- 事件回调机制: 避免使用遍历, 而是使用回调函数的方式, 将就绪的文件描述符结构加入到就绪队列中,
- epoll_wait 返回直接访问就绪队列就知道哪些文件描述符就绪. 这个操作时间复杂度O(1). 即使文件描述符数目很多, 效率也不会受到影响.
- 没有数量限制: 文件描述符数目无上限
7. 水平触发 和 边缘触发
(一)水平触发Level Triggered 工作模式
epoll默认状态下就是LT工作模式
当epoll检测到socket上事件就绪的时候, 可以不立刻进行处理. 或者只处理一部分,当发生上面两者情况,下一次调用 epoll_wait 时,事件依然处于就绪中,值得数据全部被处理完,epoll_wait 才不会提醒
如上面的例子, 由于只读了1K数据, 缓冲区中还剩1K数据, 在第二次调用 epoll_wait 时, epoll_wait
举例:
向一个接收缓冲区内发送数据,如果上层没有读,或者没读完,检测 epoll_wait 的时候仍然能检测到,直到这个接收缓冲区内的数据全部读完,就不会接收到提醒
(二)边缘触发Edge Triggered工作模式
如果我们在第1步将socket添加到epoll描述符的时候使用了EPOLLET标志, epoll进入ET工作模式.
当数据发生变化时(即从无到有,从有到多),检测该数据是否会就绪只会提醒一次,如果数据仍然没有读完,那么 epoll_wait 不再提醒,除非下次数据发生变化
举例:
向一个接收缓冲区内发送数据,第一次检测 epoll_wait 是能检测成功的,如果不读,或者只读一部分,后面再次检测 epoll_wait 是收不到提示的,除非再向这个接收缓冲区内发送数据,则下一次 epoll_wait 是能检测成功的
(三)对比LT和ET
LT是 epoll 的默认行为
ET看上去效率更高,因为使用 ET 能够减少 epoll 触发的次数. 但是代价是:为了防止数据不完整,必须一次响应就绪,就立刻把所有的数据都处理完
LT 也能提高效率:接收到一次提醒后,也把所有数据都进行处理
要做到上述,就必须使用处理数据使用非阻塞等待,即对 fd 使用 fcntl 函数,设置成O_NONBLOCK