IO其实就是代表的输入和输出,而过程一般就分为两步:等待IO就绪和进行响应的操作(数据拷贝)
而五种IO模型分别如下:
1.阻塞IO
①阻塞:为了完成一件事情,就会一直堵塞在这里,等待这个事情被完成。
②阻塞IO:在内核将数据准备好之前, 系统调用会一直等待。所有的套接字, 默认都是阻塞方式。(阻塞IO就是为了完成IO操作,发起了IO调用,如果说此时不具备完成IO操作的条件,那么就一直堵塞着。)
如下图:
③优缺点
优点:操作很简单,清晰明了。
缺点:没有充分利用好资源,如果这个条件不满足,就会一直堵塞着,其实这段时间就被浪费掉了。
2.非阻塞IO
①非阻塞:为了完成一件事,如果完成这件事情的资源还不够,那么就先返回去干其他的事情,等过一段时间再来看看是否满足要完成的条件了。
②非阻塞IO:如果内核还未将数据准备好, 系统调用仍然会直接返回, 并且返回EWOULDBLOCK错误码。(为了完成IO操作,发起IO调用,如果说当前不具备完成IO的条件,则调用立即报错返回)
如图:
③:优缺点
优点:效率相较于阻塞有些提高,并且提高了资源的利用效率,我发现内核没有准备好资源,就直接返回然后去完成别的事情,然后再回来看看。
缺点:流程相较于阻塞IO复杂,并且加入了循环条件(因为需要循环到调用成功,反复尝试读写文件描述符,这个过程称为轮询,对CPU来说是非常浪费的,一般只有特定的场合才使用),并且操作不够实时,因为我们去干另一件事的时候,必须干完才回来看看。
3.信号驱动IO
①信号驱动:为了完成一件事情,因为我不知道什么时候会满足完成这个事情就条件,我就设置一个信号函数,当满足这个事情完成的条件时,就给我发一个信号,然后我就知道了,然后就去开始处理。
②信号驱动IO:内核将数据准备好的时候, 使用SIGIO信号通知应用程序进行IO操作。(为了提高资源利用率,先自定义一个IO就绪信号,当收到IO就绪信号,则表示IO就绪,然后直接进行IO操作)
如图:
③:优缺点
优点:资源利用率高,实时性高。
缺点:操作流程相对复杂。
4.异步IO
①异步:为了完成一件事情,我这下彻底不管了,直接把东西交个别人来完成(别人等待完成这个事情需要的条件,等待好了去完成),别人完成好了给我说一下就好了。
②异步IO:由内核在数据拷贝完成时, 通知应用程序(而信号驱动是告诉应用程序何时可以开始拷贝数据)。(发起一个异步IO调用,IO操作由系统完成,完成后通知我。)
如图:
③:优缺点
优点:资源利用率最高,效率最高。
缺点:流程最为复杂。
5.IO多路转接
①多路转接:就是我们一次可以有很多条路去放这些事情,当有一个事情准备的条件可以满足完成的时候,那么就返回这个事情。
②IO多路转接:虽然从流程图上看起来和阻塞IO类似。实际上最核心在于IO多路转接能够同时等待多个文件描述符的就绪状态。(有就绪的IO操作,然后将该操作的描述符返回,然后进行IO操作)
如图:
③:优缺点
优点:可以同时进行多个事件的处理,对资源利用率颇高。
缺点:操作复杂。
总结:在任何IO操作中,只有两步操作,就是等待和拷贝;并且在一般情况下,等待的时间是大于拷贝的时间的,所以让IO操作更高效,就是减少等待的时间。
①:首先,同步和异步关注的是消息通信机制。
分别如下:
②注意:我们在多进程/多线程的时候也有同步和互斥,而上面的同步通信和进程之间的同步是没有任何关系的。
阻塞和非阻塞我们在上面也有所了解。(关注的是程序在等待调用结果时的状态)
非阻塞IO,纪录锁,系统V流机制,I/O多路转接(也叫I/O多路复用),readv和writev函数以及存储映射IO(mmap),这些统称为高级IO。
1.阻塞的定义,我们在上面也说了,就是要完成一件事,如果目前不能完成,那么就会一直堵塞在在这里等待着完成,那么阻塞IO也就是在IO操作时进行阻塞。
2.一个文件描述符,默认都是阻塞IO。(例如:read等函数接口)
3.阻塞IO的例子:
如下代码是一个read函数的例子。
1 #include<iostream>
2 #include<stdio.h>
3 #include<unistd.h>
4 #include<fcntl.h>
5 int main()
6 {
7 char buf[4096] = {0};
8 while(1)
9 {
10 int ret = read(0,buf,4095);//使用read函数将标准输入的数据读入到buf中
11 std::cout << "堵塞中" << std::endl;//用来看是否为堵塞,如果为堵塞,运行该程序时,这个堵塞中就不会打印,直到我们标准输入数据后才打印。
12 if(ret < 0)
13 {
14 perror("read error");
15 }
16 std::cout << buf << std::endl; //打印buf中的内容
17 }
18 return 0;
19 }
然后我们运行该函数
然后此时我们从标准输入输入数据,如下:
每次输入都会堵塞,然后等到我们在输入数据后,才会堵塞结束,读取到内容进行打印。
1.非阻塞在上面也了解过,就是为了完成一个功能,然后直接查看目前有没有完成这件功能的条件,如果没有就直接报错返回。
2.非阻塞IO,就是用非阻塞的行为使用在IO操作上。
3.使用阻塞IO操作:
①:在打开文件的时的操作:我们使用的IO库函数操作基本上都是阻塞的操作(因为这是默认打开文件时的设置),而要想让我们在这些操作的基础上使用非阻塞的IO操作,需要在打开文件时使用的open函数的参数上使用O_NONBLOCK 或 O_NDELAY,如下:
②:文件已经打开的操作:这个时候就要使用fcntl这个函数了,下面就介绍。
1.函数原型:int fcntl(int fd,int cmd,... /* arg */);
头文件:
①:#include
②:#include
其中参数情况如下:
①fd:改变的文件描述符。
②cmd:需要进行的操作(一共有五种)。
我们此处只是用第三种功能, 获取/设置文件状态标记, 就可以将一个文件描述符设置为非阻塞。
返回值:成功返回进行的操作,失败返回-1。
基于fcntl函数,我们设置一个SetNoBlock函数,将文件描述符设置为非阻塞。
如下:
void SetNoBlock(int fd)
{
int f1 = fcntl(fd,F_GETFL);
if(f1 < 0)
{
perror("fcntl error");
return;
}
fcntl(fd,F_SETFL,f1 | O_NONBLOCK);
}
其中:
此时,当一个已经被打开的文件,调用该函数,就会让该文件的IO操作成为非阻塞模式。
1.轮询方式:就是以固定的时间去询问是否已经存在完成IO操作的条件了,如果没有,那么就隔一段时间再来问。
2.以轮询方式读取标准输入
1 #include<iostream>
2 #include<unistd.h>
3 #include<stdio.h>
4 #include<fcntl.h>
5
6 void SetNoBlock(int fd)
7 {
8 int f1 = fcntl(fd,F_GETFL);
9 if(f1 < 0)
10 {
11 perror("fcntl error");
12 return;
13 }
14 fcntl(fd,F_SETFL,f1 | O_NONBLOCK);
15 }
16
17 int main()
18 {
19 SetNoBlock(0);
20 while(1)
21 {
22 char buf[4096] = {0};
23 int ret = read(0,buf,4095);
24 if(ret < 0)
25 {
26 std::cout << "read error" << std::endl;;
27 sleep(1);
28 continue;
29 }
30 std::cout << buf << std::endl;
31 }
32 return 0;
33 }
然后运行如下:
因为此时我们没有进行标准输入,所以就会一直报错返回,然后按照上面的代码而言,轮询方式为1秒,然后1秒询问一次。
此时我们使用标准输入,则情况如下:
emmmm,设置时间有点太短了,不能连续的打出来,如果需要,可以将轮询时间设置的长一点。
IO多路转接我们在上面已经进行介绍,而实现IO多路转接基本上是使用以下三种模型实现的。
主要功能:针对大量描述符进行IO就绪事件监控
1.首先我们了解一下IO就绪的条件:
①可读:一个描述符的接收缓冲区中的数据大小大于低水位标记。(一个基准判断值-默认1个字节)
②可写:一个描述符的发送缓冲区的剩余空间大小大于低水位标记。(一个基准判断值-默认1个字节)
③异常:一个描述符产生了异常。(例如:断开连接,描述符关闭了等等)
注意:低水位标记其实就是缓冲区中没有数据的情况(其实就是0)。
2.初识select
系统提供的select函数是用来实现多路复用输入/输出模型。
3.select函数原型
①:select函数原型如下:
int select(int nfds,fd_set* readfds,fd_set* writefds,fd_set* exceptfds,struct timeval* timeout);
头文件:#include
②:参数:
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的全部位
struct timeval timeout{time_t tv_sec;time_t tv_usec};
③:返回值
错误值可能为:
④原理:将集合中的数据拷贝到内核中,先遍历一遍,有就绪的则直接就遍历完毕后返回了。如果没有就绪则就将所有描述符添加到内核的事件队列中,当有描述符就绪或者超时进程就被唤醒,再次遍历集合中的所有描述符,将没有就绪的移除掉。(注意:对于select,在监控返回之前,那么返回就一定会删除集合中的所有没有就绪的,所以如果再次监控的话,就必须得重新在集合中进行加入)
3.select的操作流程
void FD_ZERO(fd_set* set);
//初始化,清空集合)void FD_SET(int fd,fd_set* set)
//添加fd描述符到set集合中,其实也就是将fd所对应的bit位置为1,因为是位图操作)int FD_ISSET(int fd,fd_set* set);
//判断fd描述符是否在set集合中)void FD_CLR(int fd,fd_set* set);
//吧fd描述符从set集合中删掉)其实:对于这个流程也就是将要监控的描述符的类别加入到对应监控类别的集合中,然后遍历一遍将这些需要监控的描述符拷贝到内核中,如果监控返回了,那么就删掉集合中没有就绪的,然后再遍历一遍,找到就绪的描述符进行操作即可。
例如:这是对read集合描述符进行监控的模拟实现
1 #include<iostream>
2 #include<stdio.h>
3 #include<unistd.h>
4 #include<sys/select.h>
5
6 int main()
7 {
8 fd_set read_fds; //建立一个read集合的位图(一个监控)
9 FD_ZERO(&read_fds);//初始化清空
10 int max_fd = 0; //标准输出的描述符
11 FD_SET(max_fd,&read_fds); //因为监控的是读,所以将读描述符添加到read集合中。
12 while(1)
13 {
14 struct timeval tv;//select每次都会重置超时时间为0
15 tv.tv_sec = 3;
16 tv.tv_usec = 0;
17 FD_SET(max_fd,&read_fds);//因为select会删除集合中没有就绪的描述符
18 int ret = select(max_fd+1,&read_fds,NULL,NULL,&tv);//我们只监控读集合,所以将另外两个集合> 置为NULL
19 if(ret < 0)
20 {
21 perror("select error");
22 return -1;
23 }
24 else if(ret == 0)
25 {
26 std::cout << "select timeout" << std::endl;
27 continue;
28 }
29 else
30 {
31 for(int i = 0;i <= max_fd; ++i)
32 {
33 if(!FD_ISSET(i,&read_fds))
34 {
35 continue;//该描述符没有就绪
36 }
37 std::cout << "描述符" <<i<<"就绪了事件" << std::endl;
38 char buf[1024] = {0};
39 read(0,buf,1023);
40 std::cout << buf << std::endl;
41 }
42 }
43 }
44 return 0;
45 }
运行如下:
4.select与tcp服务器的使用
①:封装select类
1 #include"tcp_socket.hpp"
2 #include<sys/select.h>
3 #include<vector>
4 class Select
5 {
6 private:
7 int _max_fd;//当前集合中的最大描述符
8 fd_set _rfds;//备份所有已经添加过的描述符的集合
9 public:
10 Select():_max_fd(-1)
11 {
12 FD_ZERO(&_rfds);
13 }
14 bool Add(TcpSocket &sock)//添加描述符 15 {
16 int fd = sock.Getfd();
17 FD_SET(fd,&_rfds);
18 _max_fd = _max_fd < fd ? fd : _max_fd;
19 return true;
20 }
21 bool Del(TcpSocket &sock)//删除描述符
22 {
23 int fd = sock.Getfd();
24 FD_CLR(fd,&_rfds);
25 int i = _max_fd;
26 for(;i > 0; --i)
27 {
28 if(FD_ISSET(i,&_rfds))
29 {
30 _max_fd = i;
31 break;
32 }
33 }
34 if(i < 0)
35 {
36 _max_fd = -1;
37 }
38 return true;
39 }
40 bool Wait(vector<TcpSocket>* array,int s = 3)//开始监控,返回就绪描述符
41 {
42 array->clear();
43 struct timeval tv;
44 tv.tv_sec = s;
45 tv.tv_usec = 0;
46 fd_set tmp_set = _rfds; //因为select函数操作完后会删除集合中所有没有就绪的操作符,所以使> 用一个替代品进行操作。
47 int ret = select(_max_fd+1,&tmp_set,NULL,NULL,&tv);
48 if(ret < 0)
49 {
50 perror("select error");
51 return false;
52 }
53 else if(ret == 0)
54 {
55 cout << "select timeout" << endl;
56 return true;
57 }
58 for(int i = 0;i < _max_fd;++i)
59 {
60 if(FD_ISSET(i,&tmp_set))
61 {
62 TcpSocket sock;
63 sock.Setfd(i);
64 array->push_back(sock);
65 }
66 }
67 return true;
68 }
69
70 };
②:客户端的写法:
1 #include "select.hpp"
2 #include <unordered_map>
3
4 std::unordered_map<std::string, std::string> table = {
5 {"hello", "你好"},
6 {"hi", "雷猴"},
7 {"吃了吗", "油泼面"}
8 };
9
10 std::string get_response(const std::string &key) {
11 std::string rsp;
12 auto it = table.find(key);
13 if (it == table.end()) {
14 rsp = "未知请求";
15 return rsp;
16 }
17 rsp = it->second;
18 return rsp;
19 }
20 int main(int argc, char *argv[])
21 {
22 if (argc < 2) {
23 std::cout << "usage: ./tcp_srv 9000\n";
24 return -1;
25 }
26 int port = std::stoi(argv[1]);
27 TcpSocket lst_sock;
28 //创建套接字
29 CHECK(lst_sock.Socket());
30 lst_sock.Setsocopt();
31 //绑定地址信息, "0.0.0.0"会被识别为本机上任意网卡IP地址--绑定0.0.0.0就表示绑定了本机上所有 网卡
32 CHECK(lst_sock.Bind("0.0.0.0", port));
33 //开始监听
34 CHECK(lst_sock.Listen());
35
36 Select s;
37 s.Add(lst_sock);
38 while(1) {
39 std::vector<TcpSocket> arry;
40 bool ret = s.Wait(&arry);
41 if (ret == false) {
42 return -1;
43 }
44 for (auto &a : arry) {
45 if (a.Getfd() == lst_sock.Getfd()) {
46 //就绪的就是监听套接字
47 TcpSocket new_sock;
48 std::string cli_ip;
49 int cli_port;
50 CHECK(a.Accept(&new_sock, &cli_ip, &cli_port));
51 std::cout << "new connect: " << cli_ip << ":" << cli_port << "\n";
52 s.Add(new_sock);
53 }else {
54 //就绪的就是普通的新建的通信套接字
55 std::string buf;
56 a.Recv(&buf);
57 std::string rsp = get_response(buf);
58 a.Send(rsp);
59 }
60 }
61 }
62 //关闭套接字
63 lst_sock.Close();
64 return 0;
65 }
③:服务端写法:
1 #include"tcp_socket.hpp"
2
3 int main(int argc,char* argv[])
4 {
5 if(argc < 3)
6 {
7 cout<<"Usage ./dict_client [ip] [port]"<<endl;
8 return -1;
9 }
10 string ip = argv[1];
11 int port = atoi(argv[2]);
12 TcpSocket socket; //实例化套接字类
13 CHECK(socket.Socket()); //套接字初始化
14 //由于是客户端,所以不需要绑定地址信息,只要指定服务端的地址和ip即可。
15 CHECK(socket.Connect(ip,port));//请求与客户端连接
16 while(1) //连接成功后,进入通信
17 {
18 string data;
19 cout<<"客户输入数据:";
20 fflush(stdout);
20 fflush(stdout);
21 cin>>data;
22 CHECK(socket.Send(data));
23 string rec;
24 CHECK(socket.Recv(&rec));//接收数据
25 cout<<"服务端发来的消息:"<<rec<<endl;
26 }
27 socket.Close();//关闭套接字
28 return 0;
29 }
5.select的特点
①特点:
②缺点:
1.poll函数原型
①:函数接口int poll(struct pollfd *fds, nfds_t nfds, int timeout);
头文件#include
注意:struct pollfd结构体:
struct pollfd {
int fd; //要监控的描述符
short events; //对应的fd描述符想要监控的事件
short revents; //监控返回后描述符实际就绪的事件
};
其中对于events:POLLIN是可读,POLLOUT是可写等等,如下:
结构体中的的操作流程:
②参数认识:
③返回值:
2.使用poll监控标准输入
1 #include<iostream>
2 #include<unistd.h>
3 #include<stdio.h>
4 #include<poll.h>
5
6 int main()
7 {
8 struct pollfd poll_fd;
9 poll_fd.fd = 0;
10 poll_fd.events = POLLIN;
11
12 while(1)
13 {
14 int ret = poll(&poll_fd,1,3000);
15 if(ret < 0)
16 {
17 perror("poll error");
18 return -1;
19 }
20 else if(ret == 0)
21 {
22 std::cout << "poll timeout" <<std::endl;
23 continue;
24 }
25 if(poll_fd.revents == POLLIN)
26 {
27 char buf[4096] = {0};
28 read(0,buf,4095);
29 std::cout << buf << std::endl;
30 }
31 }
32 return 0;
33 }
运行如下:
3.poll的优缺点
①优点:不同与select使用三个位图来表示三个fdset的方式,poll使用一个pollfd的指针实现。
②缺点:
1.epoll初识
背景:按照man手册的说法: 是为处理大批量句柄而作了改进的poll.
它是在2.5.44内核中被引进的(epoll(4) is a new API introduced in Linux kernel 2.5.44)它几乎具备了之前所说的一切优点,被公认为Linux2.6下性能最好的多路I/O就绪通知方法。
2.epoll的相关系统调用
①:int epoll_create(int size);
//在内核中,创建epoll句柄操作
参数:
返回值:
注意:用完之后必须调用close()关闭。
②:int epoll_ctl(int epfd,int op,int fd,struct epoll_event* event);
//epoll事件注册函数
参数:
第二个参数:
struct epoll_event结构体如下:
注意,events可以是以下几个宏的集合:
返回值:
③:int epoll_wait(int epfd,struct epoll_event* events,int maxevents,int timeout);
//收集在epoll监控的事件中已经发送的事件。
参数:
3.epoll的操作流程
①:在内核创建一个epoll句柄struct eventpoll
struct eventpoll{
....
/*红黑树的根节点,这颗树中存储着所有添加到epoll中的需要监控的事件*/
struct rb_root rbr;
/*双链表中则存放着将要通过epoll_wait返回给用户的满足条件的事件*/
struct list_head rdlist;
....
};
(其实,该结构体中有很多成员,但是基于我们对其原理的学习,目前使用的是这两个成员)
②:向内核的epoll句柄中添加需要监控的描述符以及对应的事件结构(添加到内核句柄的红黑树成员中)
③:开始监控,等到监控超时或者描述符就绪了则监控返回,返回的是实际就绪了指定事件的描述符对应的事件结构。
④:只需要根据返回的事件结构对对应的描述符进行对应的事件操作即可。
而当开始监控的时候,其实就是我们进程发出了一个异步操作:让操作系统完成监控。
如上图:就是红黑树和链表的情况。
epitem结构:
struct epitem
{
struct rb_node rbn;//红黑树节点
struct list_head rdllink;//双向链表节点
struct epoll_filefd ffd; //事件句柄信息
struct eventpoll *ep; //指向其所属的eventpoll对象
struct epoll_event event; //期待发生的事件类型
}
总结,epoll的使用过程就是三部曲:
4.epoll与tcp服务器的使用
①epoll类的实现:
1 #include"tcp_socket.hpp"
2 #include<sys/epoll.h>
3 #include<vector>
4 class Epoll
5 {
6 private:
7 int _epfd;
8 public:
9 Epoll():_epfd(-1)
10 {
11 //创建epoll句柄
12 _epfd = epoll_create(1);
13 if(_epfd < 0)
14 {
15 perror("epoll_create error");
16 exit(-1);
17 }
18 }
19 bool Add(TcpSocket &sock)//添加描述符
20 {
21 //epoll_ctl(epoll句柄,操作类型,描述符,事件结构)
22 int fd = sock.Getfd();
23 struct epoll_event ev;
24 ev.data.fd = fd;
25 ev.events = EPOLLIN; //可读事件
26 int ret = epoll_ctl(_epfd,EPOLL_CTL_ADD,fd,&ev);
27 if(ret < 0)
28 {
29 perror("epoll_ctl error");
30 return false;
31 }
32 return true;
33 }
34 bool Del(TcpSocket &sock)//删除描述符
35 {
36 int fd = sock.Getfd();
37 int ret = epoll_ctl(_epfd,EPOLL_CTL_DEL,fd,NULL);
38 if(ret < 0)
39 {
40 perror("epoll_ctl error");
41 return false;
42 }
43 return true;
44 }
45 bool Wait(vector<TcpSocket>* array,int s = 3000)//开始监控,返回就绪描述符
46 {
47 //epoll_wait(句柄,事件结构数组,数组大小,超时时间)
48 array->clear();
49 struct epoll_event evs[10];
50 int ret = epoll_wait(_epfd,evs,10,s);
51 if(ret < 0)
52 {
53 perror("epoll_wait error");
54 return false;
55 }
56 else if(ret == 0)
57 {
58 std::cout<< "epoll timeout!" << std::endl;
59 return true;
60 }
61 for(int i = 0;i < ret;++i)
62 {
63 if(evs[i].events & EPOLLIN)
64 {
65 TcpSocket sock;
66 sock.Setfd(evs[i].data.fd);
67 array->push_back(sock);
68 }
69 }
70 return true;
71 }
72 };
②tcp服务器的实现:
1 #include "epoll.hpp"
2 #include <unordered_map>
3
4 std::unordered_map<std::string, std::string> table = {
5 {"hello", "你好"},
6 {"hi", "雷猴"},
7 {"吃了吗", "油泼面"}
8 };
9
10 std::string get_response(const std::string &key) {
11 std::string rsp;
12 auto it = table.find(key);
13 if (it == table.end()) {
14 rsp = "未知请求";
15 return rsp;
16 }
17 rsp = it->second;
18 return rsp;
19 }
20 int main(int argc, char *argv[])
21 {
22 if (argc < 2) {
23 std::cout << "usage: ./tcp_srv 9000\n";
24 return -1;
25 }
26 int port = std::stoi(argv[1]);
27 TcpSocket lst_sock;
28 //创建套接字
29 CHECK(lst_sock.Socket());
30 lst_sock.Setsocopt();
31 //绑定地址信息, "0.0.0.0"会被识别为本机上任意网卡IP地址--绑定0.0.0.0就表示绑定了本机上所有 网卡
32 CHECK(lst_sock.Bind("0.0.0.0", port));
33 //开始监听
34 CHECK(lst_sock.Listen());
35
36 Epoll s;
37 s.Add(lst_sock);
38 while(1) {
39 std::vector<TcpSocket> arry;
40 bool ret = s.Wait(&arry);
41 if (ret == false) {
42 return -1;
43 }
44 for (auto &a : arry) {
45 if (a.Getfd() == lst_sock.Getfd()) {
46 //就绪的就是监听套接字
47 TcpSocket new_sock;
48 std::string cli_ip;
49 int cli_port;
50 CHECK(a.Accept(&new_sock, &cli_ip, &cli_port));
51 std::cout << "new connect: " << cli_ip << ":" << cli_port << "\n";
52 s.Add(new_sock);
53 }else {
54 //就绪的就是普通的新建的通信套接字
55 std::string buf;
56 a.Recv(&buf);
57 std::string rsp = get_response(buf);
58 a.Send(rsp);
59 }
60 }
61 }
62 //关闭套接字
63 lst_sock.Close();
64 return 0;
65 }
③:客户端的实现和select的实现相同。
其实这个客户端和服务端的实现和select的客户端和服务端的实现相类似,只不过进行监控使用的方式不同,一个select模型,一个是epoll模型。
5.epoll事件的触发方式
epoll有两种触发方式-水平触发(LT)和边缘触发(ET)
1.水平触发方式(epoll默认状态下的触发方式)
这个触发方式其实也就是我们在select中讲的就绪条件相类似。
2.边缘触发(通过传入EPOLLET参数实现)
注意:select和poll其实也是工作在LT模式下。 epoll既可以支持LT, 也可以支持ET。
对比两者:
6.epoll的优缺点
①优点:
②缺点:跨平台移植很差。
1.总体对比:
2.多路转接模型的适用场景: