概述
高级IO:5种典型的IO模型/多路转接模型---三种实现
四种IO模型:阻塞IO/非阻塞IO/信号驱动IO/异步IO
IO的过程:发起IO调用,等待IO条件就绪,然后将数据拷贝到缓冲区进行处理---等待/拷贝
阻塞IO:为了完成IO,发起调用,若当前不具备IO条件,则一直等待
类似于钓鱼,没有鱼上钩就一直等待.
一个IO完毕后才能进行下一个,对于资源没有充分利用,大部分时间都在等待.
非阻塞IO:完了完成IO,发起调用,若当前不具备IO条件,则立即返回(通常干其他事情需要循环操作重新发起IO)
类似于钓鱼,鱼竿抛下去,有鱼则上钩,没鱼也不想一直等待,去干点其他事情.
流程相对于阻塞操作来说复杂一点,对资源利用更加充分,IO操作不够实时.
信号驱动IO:定义IO信号处理方法,在处理方式中进行IO操作,IO就绪时信号通知进程,进程在IO就绪时进行IO.(IO更加实时,对资源利用更加充分,流程更加复杂)
异步IO:通过异步IO调用告诉操作系统,IO哪些数据拷贝到哪里,IO的等待与拷贝都由操作系统完成.(资源利用更加充分,流程更加复杂)
阻塞:为了完成一个功能,发起调用,若当前不具备完成条件,则一直等待
非阻塞:为了完成一个功能,发起调用,若当前不具备完成条件,则立即报错返回
阻塞与非阻塞的区别:发起的调用在不具备完成条件的情况下是否立即返回
同步:处理流程中,顺序处理,一个完成之后再完成下一个,并且所有功能都由进程自身完成
异步:处理流程中,顺序不定,因为功能都由操作系统完成
同步与异步的区别:功能是否由进程自己完成,完成的顺序是否是确定.
异步阻塞:功能由别人完成,调用中等着别人完成.
异步非阻塞:功能由别人完成,调用是立即返回的.
多路转接IO:对大量的描述符进行IO事件监控.可以告诉进程现在有哪些描述符就绪了哪些事件,然后进程可直接只针对就绪了对应事件的描述符进行响应操作即可.避免对没有就绪的描述符进行IO操作所导致的效率降低/程序流程阻塞
只要需要对描述符进行监控的场景都可以使用多路转接模型
IO就绪事件:可读事件/可写事件/异常事件
例如:基本的TCP服务器程序,一个执行流中,既有accept,也有recv/send;然而每种操作都有可能在不满足条件的时候阻塞,若在大量的描述符中对一个没有就绪的描述符进行操作(对没有新连接的监听套接字调用accept/对没有数据到来的新的通信套接字recv都会导致流程阻塞,其他描述符就算就绪,也无法操作)
多路转接IO的实现:select/poll/epoll
监控的好处:让进程可以只针对就绪了指定事件的描述符进行操作,提高效率性能,避免了因为对没有就绪的描述符操作导致的阻塞。
select模型:
操作流程:
1.程序员定义某个事件的描述符集合(可读事件的描述符集合/可写事件的描述符集合/异常事件的描述符集合),初始化清空集合,对哪个描述符关心什么事件,就把这个描述符添加到相应事件的描述符集合中
2.发起监控调用,将集合拷贝到内核中进行监控,监控的原理是轮询遍历判断
可读事件的就绪:接收缓冲区数据大小大于低水位标记(量化标准--通常默认1个字节)
可写事件的就绪:发送缓冲区中剩余空间的大小大于低水位标记(量化标准--通常默认1个字节)
异常事件的就绪:描述符是否产生了某个异常
3.监控的调用返回,表示监控出错/有描述符就绪/监控等待超时
并且调用返回时,将事件监控的描述符集合中的未就绪描述符从集合中移除--(集合中仅仅保留就绪的描述符)
4.程序员轮询判断哪个描述符仍然还在哪个集合中,就确定这个描述符是否就绪了某个事件,然后进行对应事件的操作即可
select并不会直接返回给用户就绪的描述符直接操作,而是返回了就绪的描述符集合,因此需要程序员进行判断
代码操作:
1.定义集合--struct fd_set--成员只有一个数组--当作二进制位图使用--添加描述符就是将描述符的值对应的比特位置1
因此select能够监控的描述符数量,取决于二进制位图的比特位多少--而比特位多少取决于宏-FD_SETSIZE,默认等于1024
2.int select(int nfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout);
参数:
nfds 是需要监视的最大的文件描述符值+1,减少遍历次数;
rdset,wrset,exset 可读/可写/异常事件的描述符集合;
参数timeout为结构struct timeval{tv_sec;tv_usec;},用来设置select()的等待时间,决定select阻塞/非阻塞/限制超时的阻塞
若timeout为NULL,表示阻塞监控,知道有描述符就绪,或者监控出错才会返回;
若timeout中的成员数据为0,则表示非阻塞,监控的时候若没有描述符就绪,则立即超时返回
若timeout中成员数据不为0,则在指定时间内,没有就绪则超时返回
返回值:返回值大于0表示就绪的描述符个数;
返回值=0表示没有描述符就绪,超时返回,
返回值小于0表示监控出错
3.调用返回,返回给程序员,就绪的描述符集合,程序员遍历判断哪个描述符还在哪个集合中,就是就绪了哪个事件
int FD_ISSET(int fd, fd_set *set); // 用来测试描述set中相关fd 的位是否为真,fd是否在集合中
因为select返回时会修改集合,因此每次监控时候都要重新添加描述符
4.若对描述符不想监控了,则从集合中移除描述符
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对标准输入进行监控:
1 #include <stdio.h>
2 #include <unistd.h>
3 #include <string.h>
4 #include <fcntl.h>
5 #include <time.h>
6 #include <sys/select.h>
7
8 int main(){
9
10 //对标准输入进行监控--标准输入描述符-0
11 //1.定义指定事件的描述符集合
12
13 fd_set rfds;
14 while(1){
15 printf("开始监控n");
16 //select(maxfd+1,可读事件集合,可写事件集合,异常事件集合,超时时间)
17 //开始监控,超时/有就绪则调用返回,返回的时候将集合中未就绪的描述符移除
18 //超时:在tv指定的时间内都一直没有描述符就绪
19 //有就绪:有描述符就绪的指定的事件
20 struct timeval tv;//因为select会重置集合和事件结构,因此每次需要重新设置
21 tv.tv_sec=3;
22 tv.tv_usec=0;
23 FD_ZERO(&rfds);//初始化清空集合
24 FD_SET(0,&rfds);//将0号描述符添加到集合中
25 int ret=select(0+1,&rfds,NULL,NULL,&tv);
26 if(ret<0){
27 perror("select errorn");
28 return -1;
29
30 }else if(ret==0){
31 printf("wait timeoutn");
32 continue;
33 }
34 if(FD_ISSET(0,&rfds)){//判断描述符是否在集合中,判断是否就绪了事件
35 printf("准备从标准输入读取数据...n");
36 char buf[1024]={0};
37 int ret=read(0,buf,1023);
38 if(ret<0){
39 perror("read error");
40 FD_CLR(0,&rfds);//从集合中移除描述符
41 return -1;
42 }
43 printf("read buf:[%s]n",buf);
44 }
45
46
47 }
48 return 0;
49 }
select.hpp
1 #include <cstdio>
2 #include <vector>
3 #include <sys/select.h>
4 #include <time.h>
5 #include "tcpsocket.hpp"
6
7 #define MAX_TIMEOUT 3000
8 //通过类的实例化对象来实现select的简单操作
9 class Select
10 {
11 public:
12 Select():_maxfd(-1){
13 FD_ZERO(&_rfds);
14 }
15 bool Add(TcpSocket &sock){//添加描述符的操作
16 //获取到套接字描述符
17 int fd=sock.GetFd();
18 //添加到事件的描述符集合中
19 FD_SET(fd,&_rfds);
20 //判断重新确定当前集合中的最大描述符
21 _maxfd=_maxfd>fd?_maxfd:fd;
22 return true;
23 }
24
25 bool Del(TcpSocket &sock){//移除描述符的监控
26 //获取到套接字描述符
27 int fd=sock.GetFd();
28 //移除到事件的描述符集合中
29 FD_CLR(fd,&_rfds);
30 //判断重新确定当前集合中的最大描述符
31 if(fd!=_maxfd) return true;
32 //假设集合中以前是8,8移除之后,从7开始判断,还在集合中的第一个就是最大
的
33 for(int i=_maxfd-1;i>=0;i--){
34 if(FD_ISSET(i,&_rfds)){
35 _maxfd=i;
36 break;
37 }
38 }
39 return true;
40 }
41 //开始监控,转接向外部返回就绪的TcpSocket
42 bool Wait(std::vector<TcpSocket> *list,int timeout=MAX_TIMEOUT){
43 //select开始监控,定义超时时间,添加描述符到集合中
44 struct timeval tv;
45 tv.tv_sec=timeout/1000;
46 tv.tv_usec=(timeout%1000)*1000;
47 fd_set tmp_set=_rfds;//每次使用临时的集合进行监控
48 int ret=select(_maxfd+1,&tmp_set,NULL,NULL,&tv);
49 if(ret<0){
50 perror("select error");
51 return false;
52 }else if(ret==0){
53 list->clear();
54 printf("wait timeoutn");
55 return true;
56 }
57 //判断哪些描述符就绪了,组织TcpSocket对象,添加到list中
58 //从0~maxfd逐个进行判断哪个数字在集合中哪个数据就是就绪的描述符的值
59 for(int i=0;i<=_maxfd;i++){
60 if(!FD_ISSET(i,&tmp_set)){
61 continue;
62 }
63 TcpSocket sock;
64 sock.SetFd(i);
65 list->push_back(sock);
66
67 return true;
68 }
69 }
70 private:
71 fd_set _rfds;//可读事件的描述符集合
72 int _maxfd;//保存集合每次集合操作后的最大描述符
73 };
74
main.cpp
1 #include "select.hpp"
2 #include <iostream>
3 //select实现的一个并发服务器
4 int main(int argc,char *argv[]){
5
6 if(argc!=3){
7 printf("usage: ./main ip portn");
8 return -1;
9 }
10 std::string srv_ip=argv[1];
11 uint16_t srv_port=std::stoi(argv[2]);
12 TcpSocket lst_sock;
13 CHECK_RET(lst_sock.Socket());//创建套接字
14 CHECK_RET(lst_sock.Bind(srv_ip,srv_port));//绑定地址信息
15 CHECK_RET(lst_sock.Listen());//开始监听
16
17 //多路转接IO
18 Select s;
19 s.Add(lst_sock);
20
21 while(1){
22 std::vector<TcpSocket>list;
23 bool ret=s.Wait(&list);
24 if(ret==false){
25 return -1;
26 }
27 for(auto sock :list){
28 //遍历就绪TcpSocket进行操作,获取新连接/接收数据
29 if(sock.GetFd()==lst_sock.GetFd()){
30 TcpSocket new_sock;
31 bool ret=lst_sock.Accept(&new_sock);//获取新套接字
32 if(ret==false){
33 continue;
34 }
35 s.Add(new_sock);
36 }else {
37 std::string buf;
38 ret=sock.Recv(&buf);//
39 if(ret==false){
40 sock.Close();
41 continue;
42 }
43 printf("client say:%sn",buf.c_str());
44 buf.clear();
45 std::cout<<"server say: ";
46 std::cin>>buf;
47 ret=sock.Send(buf);
48 if(ret==false){
49 sock.Close();
50 continue;
51 }
52 }
53 }
54 }
55
56 lst_sock.Close();//关闭套接字
57 return 0;
58 }
Select的优缺点分析:主要看应用场景
缺点:
1.select对描述符进行监控有最大数量上限,上限取决于宏--_FD_SETSIZE,默认大小1024.
2.在内核中进行监控,通过轮询遍历判断实现,性能会随着描述符的增多而下降
3.只能返回就绪的集合,需要进程进行轮询遍历判断才能得知哪个描述符就绪了哪个事件
4.每次监控都需要重新添加描述符到集合中,每次监控都需要将集合重新拷贝到内核中
优点:
遵循posix(可移植操作系统接口)标准,跨平台移植性比较好
poll模型:
操作流程:
1.定义监控的描述符事件结构体数组,将需要监控的描述符以及事件标识信息,添加到数组的各个节点中
2.发起调用开始监控,将描述符事件结构体数组,拷贝到内核中进行轮询遍历判断,若有就绪/等待超时则调用返回,并且在每个描述符对应的事件结构体中,标识当前就绪的事件
3.进程轮询遍历数组,判断数组中的每个节点中的就绪事件是哪个事件,决定是否就绪了以及如何对描述符进行操作
接口认识
int poll(struct pollfd *fds, nfds_t nfds, int timeout);
poll监控采用事件结构体的形式
struct pollfd(int fd--要监控的描述符;short events--要监控的事件 POLLIN/POLLOUT; short revents--调用返回时填充的就绪事件;)
arry_fds:事件结构体数组,填充要监控的描述符以及事件信息
nfds:数组中的有效节点个数(数组可能很大,但需要监控的节点只有前nfds个)
timeout:监控的超时等待时间--单位:毫秒
返回值:返回值大于0表示就绪的描述符事件个数;返回值等于0表示等待超时;小于0表示监控出错
优缺点分析:
优点:
1.使用事件结构体进行监控,简化了select中三种事件集合的操作流程
2.监控的描述符数量,不做最大数量限制
3.不需要每次重新定义事件节点
缺点:
1.跨平台移植性差
2.每次监控依然需要向内核中拷贝监控数据
3.在内核中监控依然采用轮询遍历,性能会随着描述符增多而下降
epoll模型:Linux下最好用的性能最高的多路转接模型
操作流程:
1.发起调用在内核中创建epoll句柄epollevent结构体(这个结构体中包含很多信息,红黑树+双向链表)
2.发起调用对内核中的epollevent结构添加/删除/修改所监控的描述符监控信息
3.发起调用开始监控,在内核中采用异步阻塞操作实现监控,等待超时/有描述符就绪了事件则调用返回,返回给用户就绪描述的事件结构信息
4.进程直接对就绪的事件结构体中的描述符成员进行操作即可。
接口信息:
int epoll_create(int size); --创建epoll句柄
size:在linux2.6.2之后被忽略,只要大于0即可。
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
epfd:epoll_create返回的操作句柄
cmd:针对fd描述符的监控信息要进行的操作--添加/删除/修改
EPOLL_CTL_ADD(注册新的fd到epfd)/EPOLL_CTL_DEL(删除一个fd)/EPOLL_CTL_MOD(修改已经注册的fd监听事件)
fd:要监控操作的描述符
ev:fd描述符对应的事件结构体信息
struct epoll_event{
uint32_t events;//对fd描述符要监控的事件-EPOLLIN/EPOLLOUT
unoin{int fd; void *ptr;}data;//要填充的描述符信息
}
一旦epoll开始监控,描述符就绪了进程关心的事件,则就会给用户返回所添加的对应事件结构体信息,通过事件结构体信息中包含的描述符进行操作---因此第三个单数fd与结构体中的fd描述符通常是同一个描述符。
3.int epoll_wait(int epfd, struct epoll_event * events, int maxevents, int timeout);
epfd:epoll操作句柄
evs: struct epoll_event结构体数组的首地址,用于接收就绪描述符对应的事件结构体信息
max_event:本次监控想要获取的就绪事件最大数量,不大于evs数组的节点个数,防止访问越界
timeout:等待超时时间,单位:毫秒
返回值:返回值大于0表示就绪的描述符事件个数;返回值等于0表示等待超时;小于0表示监控出错
epoll的监控原理:异步阻塞操作
监控由系统完成,用户添加监控的描述符以及对应事件结构体会被添加到内核的wvwntpoll的结构体中的红黑树中。
一旦发起调用开始监控,则操作系统为每个描述符事件做了一个回调函数,功能是当描述符就绪了关心的事件,则将描述符对应的事件结构体添加到双向链表中
进程自身,只是每隔一段时间,判断双向链表是否为NULL,决定是否有就绪
1 #include <cstdio>
2 #include<vector>
3 #include<sys/epoll.h>
4 #include <iostream>
5 #include <stdlib.h>
6
7 #define MAX_TIMEOUT 3000
8 class Epoll
9 {
10 public:
11 Epoll():_epofd(-1){
12 //1.创建epoll
13 _epfd=epoll_create(1);
14 if(_epfd<0){
15 perror("epoll create error");
16 exit(-1);
17
18 }
19 }
20 bool Add(TcpSocket &sock){
21 //2.添加描述符监控事件信息
22 //(1).获取描述符
23 int fd=sock.GetFd();
24 //(2).定义描述符对应的事件结构体
25 //EPOLLIN-可读事件 EPOLLOUT-可写事件
26 struct epoll_event ev;
27 ev.events=EPOLLIN;
28 ev.data.fd=fd;
29 //(3).添加到内核中
30 //epoll_ctrl(epoll句柄,操作类型,监控描述符,描述符事件结构)
31 int ret=epoll_ctrl(_epfd,EPOLL_CTL_ADD,fd,&ev);
32 if(ret<0){
33 perror("epoll ctl add error");
34 return false;
35 }
36 return true;
37 }
38 bool Del(TcpSocketk &sock){//移除epoll
39 int fd=sock.GetFd();//获取描述符
40 int ret=epoll_ctl(_epfd,EPOLL_CTL_DEL,fd,NULL);//移除
41 if(ret<0){
42 perror("epoll ctl del error");
43 return false;
44 }
45 return true;
46 }
47 //开始监控
48 bool Wait(std::vector<TcpSocket> *list,int timeout=MAX_TIMEOUT){
49 //(1)开始监控
50 struct epoll_event evs[10];
51 int nfds=epoll_wait(_epfd,evs,10,timeout);
52 if(nfds<0){
53 perror("epoll wait error");
54 return false;
55 }else if(nfds==0){
56 printf("epoll wait timeoutn");
57 list->clear();
58 return true;
59 }
60 //(2)监控调用返回后,为每一个就绪的描述符组织Tcpsocket对象
61 for(int i=0;i<nfds;i++){
62 if(evs[i].events&EPOLLIN){//可读事件的操作
63 TcpSocket sock;
64 sock.SetFd(evs[i].data.fd);
65 //(3)将Tcpsocket对象添加到list中,进行返回
66 list->push_back(sock);
67 }//else if()其他事件的判断操作
68 }
69 return true;
70 }
71 private:
72 int _epfd;
73
74 };
epoll中就绪事件的触发方式:
可读事件:接收数据的缓冲区数据大小大于水位标记,就会触发可读事件
可写事件:发送缓冲区剩余空间大小大于低水位标记,就会触发可写事件
低水位标记:基准衡量值,默认1个字节
边缘触发方式:EPOLLET
可读事件:只有新数据到来的时候,才会触发一次事件
可写事件:发送缓冲区中剩余空间从无到有的时候才会触发一次事件
边缘触发因为触发方式的不同,因此要求进程中事件触发进行数据接收的时候,要求最好能够一次将所有数据全部读取(因为剩余数据不会触发第二次事件,只有新数据到来才会触发)
然而循环读取能够保证读完缓冲区中的所有数据,但是在没有数据的时候会造成阻塞,因此边缘触发方式中,描述符的操作都采用非阻塞操作--非阻塞描述符操作在没有数据/超时情况下会报错返回:EAGAIN or EWOULDBLOCK
如何将描述符设置为非阻塞(描述符的所有操作都为非阻塞)
int fcntl(int fd,int cmd,.../*arg*/);---
fd:指定的描述符
cmd:F——GETFL/F_SETFL---获取/设置一个描述符的属性信息--O_NONBLOCK-非阻塞属性
arg:要设置的属性信息/获取的属性信息 F_GETFL使用的时候,arg被忽略,默认设置即可
边缘触发:为了防止一些事件不断触发(接收数据后(按条取指定长度)),缓冲区留有半条,就会不断触发事件,这种情况要不然上层操作,将半条数据读取出来,外部维护,要不然就使用边缘触发,等待新数据到来数据完整在触发事件。
epoll的优缺点分析:
1.没有描述符监控数量的上限
2.监控信息只需要向内核添加一次
3.监控使用异步阻塞操作完成,性能不会随着描述符的增多而下降
4.直接向用户返回就绪的事件信息(包含描述符在内),进程直接可以针对描述符以及事件进行操作,不需要判断有没有就绪了
1.跨平台移植性差。
多路转接模型
使用场景:只要对描述符有(可读/可写/异常)事件监控的需求就都可以使用多路转接模型
适用场景:适用于对大量描述符进行监控,但是同一时间只有少量描述符活跃的场景
理解多路转接模型的并发(用户之间的)/多进程多线程的并发(基于操作系统时间片的均衡并发)不同之处:
多路转接模型可以和线程池搭配适用
最后
以上就是舒心口红为你收集整理的高级IO--5种及select、poll、epoll的原理、执行以及优缺点的全部内容,希望文章能够帮你解决高级IO--5种及select、poll、epoll的原理、执行以及优缺点所遇到的程序开发问题。
如果觉得靠谱客网站的内容还不错,欢迎将靠谱客网站推荐给程序员好友。
发表评论 取消回复