概述
这里写目录标题
- 五种IO模型
- 高级IO的重要概念
- 同步通信 vs 异步通信(synchronous communication/ asynchronous communication)
- 阻塞 vs 非阻塞
- 其他高级IO
- 阻塞IO
- 非阻塞IO
- fcntl
- 实现函数SetNoBlock
- 轮询方式读取标准输入
- IO多路转接的实现
- IO多路转接之select
- IO多路转接之poll
- IO多路转接之epoll
- 总结
五种IO模型
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的重要概念
同步通信 vs 异步通信(synchronous communication/ asynchronous communication)
①:首先,同步和异步关注的是消息通信机制。
分别如下:
- 同步:所谓同步,就是发起一个调用,在没有得到结果之前,这个调用就不返回(堵塞住了),那就说明,只要调用一返回,就得到了结果。(也就是让主动调用者去等待这个调用结果)
- 异步:所谓异步,也就是和同步相反,它是发起一个调用,这个调用之后就不返回了,所以就没有返回结果。(也就是当一个异步过程的调用发出后,调用者不会立即得到结果;而是在调用发出后,被调用者通过状态,通知来通知调用者或者通过回调函数处理这个调用)
②注意:我们在多进程/多线程的时候也有同步和互斥,而上面的同步通信和进程之间的同步是没有任何关系的。
- 进程/线程之间的同步也是进程/线程之间直接制约的关系。
- 是为了完成每个任务,而建立了两个或多个线程,而这些线程在某些位置上协调它们的工作次序而等待、传递信息所产生的制约关系。(一般是对临界资源进行操作时)
阻塞 vs 非阻塞
阻塞和非阻塞我们在上面也有所了解。(关注的是程序在等待调用结果时的状态)
- 阻塞:阻塞调用是指调用结果返回之前,当前线程会被挂起。调用线程只有在得到结果之后才会返回。
- 非阻塞:非阻塞调用指在不能立刻得到结果之前,该调用不会阻塞当前线程。
其他高级IO
非阻塞IO,纪录锁,系统V流机制,I/O多路转接(也叫I/O多路复用),readv和writev函数以及存储映射IO(mmap),这些统称为高级IO。
阻塞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 }
然后我们运行该函数
然后此时我们从标准输入输入数据,如下:
每次输入都会堵塞,然后等到我们在输入数据后,才会堵塞结束,读取到内容进行打印。
非阻塞IO
1.非阻塞在上面也了解过,就是为了完成一个功能,然后直接查看目前有没有完成这件功能的条件,如果没有就直接报错返回。
2.非阻塞IO,就是用非阻塞的行为使用在IO操作上。
3.使用阻塞IO操作:
①:在打开文件的时的操作:我们使用的IO库函数操作基本上都是阻塞的操作(因为这是默认打开文件时的设置),而要想让我们在这些操作的基础上使用非阻塞的IO操作,需要在打开文件时使用的open函数的参数上使用O_NONBLOCK 或 O_NDELAY,如下:
②:文件已经打开的操作:这个时候就要使用fcntl这个函数了,下面就介绍。
fcntl
1.函数原型:int fcntl(int fd,int cmd,... /* arg */);
头文件:
①:#include<unistd.h>
②:#include<fcntl.h>
其中参数情况如下:
①fd:改变的文件描述符。
②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)。
我们此处只是用第三种功能, 获取/设置文件状态标记, 就可以将一个文件描述符设置为非阻塞。
返回值:成功返回进行的操作,失败返回-1。
实现函数SetNoBlock
基于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);
}
其中:
- 使用F_GETFL将当前的文件描述符的属性取出来(这是一个位图)。
- 然后再使用F_SETFL将文件描述符设置回去。设置回去的同时, 加上一个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多路转接基本上是使用以下三种模型实现的。
IO多路转接之select
主要功能:针对大量描述符进行IO就绪事件监控
1.首先我们了解一下IO就绪的条件:
①可读:一个描述符的接收缓冲区中的数据大小大于低水位标记。(一个基准判断值-默认1个字节)
- socket内核中, 接收缓冲区中的字节数, 大于等于低水位标SO_RCVLOWAT。此时可以无阻塞的读该文件描述符, 并且返回值大于0;
- socket TCP通信中, 对端关闭连接, 此时对该socket读, 则返回0;
- 监听的socket上有新的连接请求;
- socket上有未处理的错误;
②可写:一个描述符的发送缓冲区的剩余空间大小大于低水位标记。(一个基准判断值-默认1个字节)
- socket内核中, 发送缓冲区中的可用字节数(发送缓冲区的空闲位置大小), 大于等于低水位标记SO_SNDLOWAT, 此时可以无阻塞的写, 并且返回值大于0;
- socket的写操作被关闭(close或者shutdown). 对一个写操作被关闭的socket进行写操作, 会触发SIGPIPE信号;
- socket使用非阻塞connect连接成功或失败之后;
- socket上有未读取的错误;
③异常:一个描述符产生了异常。(例如:断开连接,描述符关闭了等等)
- socket上收到带外数据. 关于带外数据, 和TCP紧急模式相关(回忆TCP协议头中, 有一个紧急指针的字段)。
注意:低水位标记其实就是缓冲区中没有数据的情况(其实就是0)。
2.初识select
系统提供的select函数是用来实现多路复用输入/输出模型。
- select系统调用是用来让我们的程序监视多个文件描述符的状态变化的。
- 程序会在select这里等待,直到被监视的文件描述符有一个或多个发生了状态改变。
3.select函数原型
①:select函数原型如下:
int select(int nfds,fd_set* readfds,fd_set* writefds,fd_set* exceptfds,struct timeval* timeout);
头文件:#include<sys/select.h>
②:参数:
- nfds:要监视的最大描述符的数量+1。
- readfds:可读事件集合。
其中,fd_set结构如下:
其实这个结构就是一个整数数组,更严格的说,其实就是一个"位图",使用位图中对应的位来表示要监视的文件描述符。他还提供了一组的操作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的全部位
- writefds:可写事件集合。
- exceptfds:异常事件集合。
- timeout:所设置的监控超时事件。
其中,timeval的结构是:struct timeval timeout{time_t tv_sec;time_t tv_usec};
取值情况如下:
①NULL:则表示select函数没有timeout,就会一直堵塞,直到监控的某个描述符发生了事件。(永久阻塞)
②0:仅仅检测描述符集合的状态,然后立即返回,并不等待外部事件的发生。(非阻塞)
③特定的时间值:如果在指定的时间段里面没有事件发生,select则超时返回。
③两个参数,一个秒,一个是微秒
③:返回值
- 执行成功则返回文件描述词状态已经改变的个数。
- 如果返回0,则代表的是在描述词状态改变前已经超过timeout事件,没有返回。
- 如果有错误发生则返回-1,错误原因存在于errno中,此时参数readfds,writefds, exceptfds和timeout的值变成不可预测。
错误值可能为:
- EBADF 文件描述词为无效的或该文件已关闭。
- EINTR 此调用被信号所中断。
- EINVAL 参数n 为负值。
- ENOMEM 核心内存不足。
④原理:将集合中的数据拷贝到内核中,先遍历一遍,有就绪的则直接就遍历完毕后返回了。如果没有就绪则就将所有描述符添加到内核的事件队列中,当有描述符就绪或者超时进程就被唤醒,再次遍历集合中的所有描述符,将没有就绪的移除掉。(注意:对于select,在监控返回之前,那么返回就一定会删除集合中的所有没有就绪的,所以如果再次监控的话,就必须得重新在集合中进行加入)
3.select的操作流程
- 用户定义一个指定的描述符集合(三种-可读,可写,异常)进行初始化操作。(用到的函数:
void FD_ZERO(fd_set* set);
//初始化,清空集合) - 然后将需要监控指定事件的描述符添加到指定集合中。(例如:对描述符进行可读监控,则将描述符添加到可读事件的描述符集合中)(使用到的函数:
void FD_SET(int fd,fd_set* set)
//添加fd描述符到set集合中,其实也就是将fd所对应的bit位置为1,因为是位图操作) - 将集合数据拷贝到内核中,开始监控,当某个描述符就绪了某个指定的事件,或者监控的时间已经超时了,则监控返回。(使用的是select函数)
- 在监控返回之前,select会将事件描述符中未就绪的描述符从集合中删掉。(这时候,集合中的描述符就都是已经就绪了的描述符)
- 用户遍历所有描述符,看哪个还在集合中,证明该描述符就绪了什么事件,要进行操作。(使用的函数:
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 9000n";
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的特点
①特点:
- 可监控的文件描述符个数取决与sizeof(fd_set)的值. 我这边服务器上sizeof(fd_set)=512,每bit表示一个文件描述符,则我服务器上支持的最大文件描述符是512*8=4096。
- 将fd加入select监控集的同时,还要再使用一个数据结构array保存放到select监控集中的fd。
①:用于再select 返回后,array作为源数据和fd_set进行FD_ISSET判断。
②:select返回后会把以前加入的但并无事件发生的fd清空,则每次开始select前都要重新从array取得fd逐一加入(FD_ZERO最先),扫描array的同时取得fd最大值maxfd,用于select的第一个参数。
②缺点:
- 每次调用select, 都需要手动设置fd集合, 从接口使用角度来说也非常不便。
- 每次调用select,都需要把fd集合从用户态拷贝到内核态,这个开销在fd很多时会很大。
- 同时每次调用select都需要在内核遍历传递进来的所有fd,这个开销在fd很多时也很大。
- select支持的文件描述符数量太小。
IO多路转接之poll
1.poll函数原型
①:函数接口int poll(struct pollfd *fds, nfds_t nfds, int timeout);
头文件#include <poll.h>
注意:struct pollfd结构体:
struct pollfd {
int fd; //要监控的描述符
short events; //对应的fd描述符想要监控的事件
short revents; //监控返回后描述符实际就绪的事件
};
其中对于events:POLLIN是可读,POLLOUT是可写等等,如下:
结构体中的的操作流程:
- 首先,用户定义一个IO就绪事件结构体数组 struct pollfd。
- 向事件结构体中添加需要监控的描述符,以及要监控的事件信息。
- 调用监控接口,将数据拷贝到内核,开始监控,当监控超时或者有描述符就绪了对应事件则调用返回。
- 调用返回前,监控会将每个事件的结构体中revents成员进行置位,置为实际就绪的事件。(若没有就绪事件,则置为0)
- 当调用返回后,则遍历事件结构体数据,就能确定哪个描述符确定了哪个事件,进而进行对应的操作即可。
②参数认识:
- fds:事件结构体的地址。(fds是一个poll函数监听的结构列表,每一个元素中, 包含了三部分内容: 文件描述符, 监听的事件集合, 返回的事件集合)
- nfds:数组中有效元素的个数。(fds数组的长度)
- timeout:监控超时时间,单位是毫秒。
其中:
① -1:表示阻塞等待,没有就绪就一直等待。
② 0:表示非阻塞等待,没有就绪也会直接返回。
③返回值:
- 返回值小于0, 表示出错。
- 返回值等于0, 表示poll函数等待超时。
- 返回值大于0, 表示poll由于监听的文件描述符就绪而返回。
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的指针实现。
- pollfd结构包含了要监视的event和发生的event,不再使用select“参数-值”传递的方式。接口使用比select更方便。
- poll并没有最大数量限制 (但是数量过大后性能也是会下降)。
②缺点:
- 和select函数一样,poll返回后,需要轮询pollfd来获取就绪的描述符。
- 每次调用poll都需要把大量的pollfd结构从用户态拷贝到内核中。
- 同时连接的大量客户端在一时刻可能只有很少的处于就绪状态, 因此随着监视的描述符数量的增长, 其效率也会线性下降。
IO多路转接之epoll
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句柄操作
参数:
- size:最早用于确定要监控的最大描述符数量的上限,在linux2.6.8之后被忽略,但是必须大于0。
返回值:
- 成功:返回epoll描述符句柄。
- 失败:返回-1。
注意:用完之后必须调用close()关闭。
②:int epoll_ctl(int epfd,int op,int fd,struct epoll_event* event);
//epoll事件注册函数
参数:
- 它不同于select()是在监听事件时告诉内核要监听什么类型的事件, 而是在这里先注册要监听的事件类型。
- 第一个参数是epoll_create()的返回值(epoll的句柄)。
- 第二个参数表示动作,用三个宏来表示。
- 第三个参数是需要监听的fd。
- 第四个参数是告诉内核需要监听什么事。
第二个参数:
- 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队列里。
返回值:
- 成功:返回0。
- 失败:返回-1。
③:int epoll_wait(int epfd,struct epoll_event* events,int maxevents,int timeout);
//收集在epoll监控的事件中已经发送的事件。
参数:
- 参数events是分配好的epoll_event结构体数组。
- epoll将会把发生的事件赋值到events数组中 (events不可以是空指针,内核只负责把数据复制到这个events数组中,不会去帮助我们在用户态中分配内存)。
- maxevents告之内核这个events有多大,这个 maxevents的值不能大于创建epoll_create()时的size。
- 参数timeout是超时时间 (毫秒,0会立即返回,-1是永久阻塞)。
- 如果函数调用成功,返回对应I/O上已准备好的文件描述符数目,如返回0表示已超时, 返回小于0表示函数失败。
3.epoll的操作流程
①:在内核创建一个epoll句柄struct eventpoll
struct eventpoll{
....
/*红黑树的根节点,这颗树中存储着所有添加到epoll中的需要监控的事件*/
struct rb_root rbr;
/*双链表中则存放着将要通过epoll_wait返回给用户的满足条件的事件*/
struct list_head rdlist;
....
};
(其实,该结构体中有很多成员,但是基于我们对其原理的学习,目前使用的是这两个成员)
②:向内核的epoll句柄中添加需要监控的描述符以及对应的事件结构(添加到内核句柄的红黑树成员中)
③:开始监控,等到监控超时或者描述符就绪了则监控返回,返回的是实际就绪了指定事件的描述符对应的事件结构。
④:只需要根据返回的事件结构对对应的描述符进行对应的事件操作即可。
而当开始监控的时候,其实就是我们进程发出了一个异步操作:让操作系统完成监控。
- 操作系统是对每个描述符的事件监控都做了一个事件回调函数。
- 一旦某个描述符就绪了某个指定事件,就会自动调用回调函数,将这个就绪的描述符对应的事件结构。添加到eventpoll结构体中的双向链表中去。
- 而我们的进程是通过查看rdllist就绪链表,通过是否为NULL就能确定是否有描述符就绪,如果为NULL,则么有描述符就绪,反之则有就绪。(注意:rdllist就绪链表中就只有就绪的描述符)
- 这个时候会将就绪的描述符和其对应的事件结构拷贝到监控传入的数组中。
如上图:就是红黑树和链表的情况。
epitem结构:
struct epitem
{
struct rb_node rbn;//红黑树节点
struct list_head rdllink;//双向链表节点
struct epoll_filefd ffd; //事件句柄信息
struct eventpoll *ep; //指向其所属的eventpoll对象
struct epoll_event event; //期待发生的事件类型
}
总结,epoll的使用过程就是三部曲:
- 调用epoll_create创建一个epoll句柄。(也就是创建一个eventpoll结构体)
- 调用epoll_ctl, 将要监控的文件描述符进行注册。(将要监控的文件加入到eventpoll的红黑树中,而加入的信息是通过结构体epitem进行保存的,也就是通过epitem结构体加入到eventpoll结构体的红黑树中)
- 调用epoll_wait, 等待文件描述符就绪。(然后将就绪的epitem加入到eventpoll的链表中)
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 9000n";
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默认状态下的触发方式)
- 当epoll检测到socket上事件就绪的时候, 可以不立刻进行处理. 或者只处理一部分。
- 只要缓存管道有数据存在,就会直接返回就绪,直到缓冲区上没有数据了,才会等待继续监控。
- 支持阻塞读写和非阻塞读写。
这个触发方式其实也就是我们在select中讲的就绪条件相类似。
2.边缘触发(通过传入EPOLLET参数实现)
- 当epoll检测到socket上事件就绪时, 必须立刻处理。
- 就是当缓冲区有新数据到来的时候才会返回就绪,如果没有新数据到来,而此时缓冲区里面有数据还是会等待继续监控。
- ET的性能比LT性能更高( epoll_wait 返回的次数少了很多)。
- 只支持非阻塞的读写。
注意:select和poll其实也是工作在LT模式下。 epoll既可以支持LT, 也可以支持ET。
对比两者:
- LT是 epoll 的默认行为,一次响应就绪过程中就把所有的数据都处理完;而ET可以减少 epoll 触发的次数,但是会只有新数据到来的时候才会取出缓冲区里面的数据。
- ET相当于一个文件描述符就绪之后, 不会反复被提示就绪, 看起来就比 LT 更高效一些。
- LT 情况下如果也能做到每次就绪的文件描述符都立刻处理, 不让这个就绪被重复提示的话, 其实性能也是一样的。
- ET的实现会复杂一点。(因为每次到来数据只能触发一次,而我们要将这些数据读取完,就必须循环操作, 但是循环操作的话,我们又不知道什么时候停止,所以针对ET,我们要使用非阻塞操作)
6.epoll的优缺点
①优点:
- 监控描述符没有上限。
- 监控原理是异步操作,监控由内核完成,进程只需要判断就绪链表是否为空即可,效率不会随着描述符的增加而降低。
- 直接返回的全是就绪的描述符事件,可以针对就绪的描述符进行操作,没有空遍历。
②缺点:跨平台移植很差。
总结
1.总体对比:
- select和poll:效率会随着描述符的增多而降低,并且select的实现较为复杂,但是如果是单描述符的监控,或者单描述符的超时控制非常适用。
- epoll:性能不会随着描述符的增多而降低,适于针对大量描述符的监控场景,而不太适合单描述符的超时控制,因为它需要在内核中创建句柄,进行各种操作,如果不用了还要进行销毁操作。
2.多路转接模型的适用场景:
- 要么适用单个描述符的超时控制,要么针对大量描述符的事件监控。
- 在对于大量描述符事件监控的场景时,只适用于有大量描述符,但同一时间少量就绪的情况。
- 多路转接模型是一种单执行流的并发轮询操作(一个一个执行,执行完一个才到下一个),如果同一时间就绪的描述符过多,那么排在后面的描述符有可能就等待超时了。
- 所以通常是搭配线程池一块使用,有就绪的描述符直接扔到线程池中,这样还能避免描述符没有数据空占线程的情况。
最后
以上就是紧张老师为你收集整理的LInux学习------高级IO五种IO模型高级IO的重要概念其他高级IO阻塞IO非阻塞IOIO多路转接的实现的全部内容,希望文章能够帮你解决LInux学习------高级IO五种IO模型高级IO的重要概念其他高级IO阻塞IO非阻塞IOIO多路转接的实现所遇到的程序开发问题。
如果觉得靠谱客网站的内容还不错,欢迎将靠谱客网站推荐给程序员好友。
发表评论 取消回复