当前位置: 首页 > news >正文

Linux相关概念和易错知识点(47)(五种IO模型、多路转接、select、poll、epoll)

目录

  • 1.五种IO模型
    • (1)理解IO和高效IO
    • (2)形象认识五种IO模型
      • ①阻塞IO
      • ②非阻塞IO
      • ③信号驱动式IO
      • ④多路转接 / 多路复用IO
      • ⑤异步IO
    • (3)不同IO的效率对比、同步IO和异步IO
    • (4)非阻塞IO的实现
  • 2.多路转接
    • (1)select
      • ①select代码逻辑
      • ②select函数
      • ③fd_set、timeval
      • ④select的优缺点
    • (2)poll
      • ①poll代码逻辑
      • ②poll函数
      • ③struct pollfd
      • ④poll的优缺点
    • (3)epoll
      • ①epoll代码逻辑
      • ②epoll_create
      • ③epoll_ctl
      • ④epoll_wait
      • ⑤struct epoll_event
      • ⑥epoll的优点

1.五种IO模型

(1)理解IO和高效IO

IO即input和output,网络通信的效率问题本质上是IO问题,read和write默认是阻塞式IO,对于本机IO来说,硬件收到数据就会发送硬件中断,根据中断号处理。网络通信的话就是网卡进行硬件中断,系统读取数据到数据链路层,再向上交互。因此,无论本机IO还是网络IO,input和output是站在进程角度的。

我们如何更好地理解IO?进而理解什么叫做高效的IO?

IO == 等文件缓冲区有数据 + 拷贝数据工作。 网络通信中,当我们调用read函数时,文件缓冲区不一定有数据,这个时候就会阻塞,也就是等。发送数据也要等,因为文件缓冲区满的时候要等刷新。 本机的文件操作也是这样,要等相关的文件缓冲区、inode、struct file准备好后才能写入数据,只不过本地等的时间非常短,相比而言网络IO的感知更明显。

等和不用等之间的转换中,双方涉及某个条件,如缓冲区空间变化。这种条件变化统一叫做一次IO事件,发送缓冲区为空、满都称之为IO事件就绪(分别对应可以写和可以读的标志)。

既然等是主要矛盾,那什么叫做高效的IO?首先我们要明确一个思想,在任何通信场景下,IO一定是有上限的,IO效率提升到一定程度就会受到硬件的限制,花盆里面不可能长出参天大树,我们要有预期。最高效IO ≈ 拷贝,即充分利用硬件资源、网络资源。 在网络中高效的IO就是尽量减少通信主机read和write阻塞,比如客户端发数据每次都发几字节,而服务端一直在等。我们要提高IO效率,应该怎么做?减少IO中等的比重,即单位时间内,等待的比重越低,IO效率越高。 因此就引入了如下五种IO模型。

(2)形象认识五种IO模型

以钓鱼为例子,我们把钓鱼视作一种IO,因为钓鱼 = 等 + 钓,和IO = 等 + 拷贝很相似。 对于钓鱼这件事来说,其实钓鱼佬大部分时间就在等,相比而言,高效的钓鱼等的时间比重一定低。

①阻塞IO

张三拿着鱼竿坐在岸边,静静地等,他的眼睛死死盯者鱼漂,鱼漂不动他不动,谁叫他都不好使,鱼漂动了就钓上来一条鱼(新手)。

②非阻塞IO

过了一会,来了个李四,坐在张三旁边钓鱼。李四扔出鱼竿开始等,等的时候李四玩手机、看报纸、尝试跟张三说话(张三显然不理他),再看看鱼漂,循环做这些事,有鱼了就开始钓。张三一直不动,李四一直在动。

③信号驱动式IO

王五来了,把铃铛放在鱼竿顶部,扔出鱼竿之后,一直玩手机,看都不看鱼漂,头都不抬。只有他听到铃铛响了之后,他就开始钓鱼。

④多路转接 / 多路复用IO

赵六是个有钱人,他拉了一车鱼竿(100个鱼竿),然后他把每个鱼竿都挂上鱼饵扔河里,并且来回踱步、来回检测,一直在动,有鱼就钓上来。

⑤异步IO

田七是个首富,他坐在自己的专车后座,他深刻的知道,他不是喜欢钓鱼,他喜欢吃鱼。于是看到一群人在钓鱼之后,田七一边自己开车赶着回公司开会,一边给司机小王一个鱼竿、桶、电话等设备,让小王开始钓鱼,桶满了就给田七打电话,田七就会开车回来接小王。

(3)不同IO的效率对比、同步IO和异步IO

拷贝时一致,但是它们等的方式不同,阻塞IO一直在等,而非阻塞IO等的时候在干其他事。但是,阻塞IO和非阻塞IO的效率并没有本质区别,在同样的时间里面非阻塞IO干的事情要多一点,但这个高效性是体现在“其他事情”上(包括其他IO),但如果只针对于这一单事件IO来说,两者本质上没有区别。 从一个更形象的角度来理解,你现在是一条在河里自由的鱼,你现在看到了两个钩子,一个是阻塞IO的,一个是非阻塞IO的。你咬哪个?概率一样,咬哪个都没区别。

因此,从鱼的视角上看,多路转接 / 多路复用IO的效率最高,等的时间很少,因为鱼竿数量特别多,任意一个鱼竿被咬钩的概率很高。

信号驱动式IO的特点是逆转了获取事件就绪的方式(捕捉SIGIO信号,接收到信号就去IO),即等的方式不一样。

阻塞IO、非阻塞轮询IO、信号驱动IO都是等的方式不一样(在单事件上IO效率一样),即钓鱼都要自己去干。因此,只要参与了IO过程(等或者拷贝过程)中的任意一个,都称为同步IO。包括多路转接也是同步IO的一种

同步IO vs 异步IO:

线程的同步指的是排队以及交替唤醒机制,和同步IO没有关系。同步IO和异步IO的本质区别是有没有参与IO过程,包括等和拷贝,只要有一个就叫参与,都会受到IO的影响,就是同步IO。

异步IO的话进程往往扮演IO任务的发起者,实际是让小王(操作系统)去IO,直接将数据放到对应的用户缓冲区后再通知进程,全程进程不需要等、拷贝。 如今异步IO使用率逐渐下降。

(4)非阻塞IO的实现

钓鱼就是调用系统调用、函数调用的过程。对于阻塞IO来说,在数据准备好之前,系统调用会一直等待,因为所有套接字默认都是阻塞方式。如果采用非阻塞IO的话,read不到数据会直接返回,这时就需要我们反复轮询调用。

像recv、open等很多函数参数中都有文件阻塞、非阻塞的选项,但这并不统一。 由于Linux下一切皆文件,用户的所有操作本质上都是跟fd(文件)打交道,而文件描述符默认阻塞,所以我们可以统一设置文件描述符为非阻塞,这样关于文件的一切操作都是非阻塞的了,这种方式更通用 。

int fcntl(int fd, int op, ...) // 设置文件标记位的函数,struct file里面有相应的标记字段

第二个参数op,可以有多个选项 (文件标记位相关的有F_GETFL和F_SETFL,用于非阻塞IO的设置)。后面的可变参数可以传一些set参数的值。fcntl函数失败的话返回-1,成功的话会根据op选项返回。

下面是设置非阻塞IO的函数,非常定式。

// 调用这个函数之后,关于该fd的IO均为非阻塞
void SetNonBlock(int fd)
{int flag = fcntl(fd, F_GETFL, 0); // 获取文件描述符的属性if (flag < 0)return;fcntl(fd, F_SETFL, flag | O_NONBLOCK); // 设置文件描述符为非阻塞
}

阻塞IO调用read时,如果缓冲区里面没数据会直接阻塞,对方关闭连接时或是读取到文件末尾EOF时返回0,正常读取到数据返回具体读取大小(n > 0),read失败会返回-1。非阻塞IO的返回值和阻塞IO的很相似,但在非阻塞下read失败,以及底层数据没就绪时,其返回值都会是-1,也就是没数据时不会阻塞。

底层数据没就绪严格意义上并不算失败,人家按规矩办事,只是缺失数据没准备好,凭什么说人家犯错?因此在返回-1之后,对该fd后续的处理需要分类,这个区分的方式就是错误码。读取errno,数据没就绪对应的errno == EAGAIN | errno == EWOULDBLOCK。

// 文件操作返回-1之后的处理
if(errno == EAGAIN || errno == EWOULDBLOCK) // 数据没就绪(仅当在非阻塞IO中需要处理,阻塞IO返回-1不可能是这个情况)break;
else if(errno == EINTR) // 被信号打断,重新读(阻塞IO和非阻塞IO都需要处理)continue;
else
{Excepter(); // 触发读写错误,需要异常处理(阻塞IO和非阻塞IO都需要处理)return;
}

上述代码中,errno == EINTR(被信号打断)是什么场景呢?

无论是阻塞IO还是非阻塞IO,在返回-1之后都需要进行这个判断。 原因是进程执行阻塞式系统调用时,如果收到了一个信号,内核会强制中断该系统调用,使其返回-1,并将errno设为EINTR,表示调用被信号打断。对于阻塞IO或是非阻塞IO来说,当数据就绪需要拷贝数据时,系统调用均会阻塞,所以这两种IO都可能会被信号打断。 一般来说这种打断概率极低,几乎只存在于理论上,我们很难去验证,所以默认加上就好。
不过对于阻塞IO来说,errno == EINTR还有个作用是处理等待数据到来时被打断的情况,因为阻塞IO只要没有数据就绪会一直等待,这期间极有可能被信号唤醒,此时判断errno == EINTR之后continue是必要的,continue就意味着处理信号之后阻塞IO会重新调用阻塞函数,重新回去阻塞。

以read为例子,read是系统调用,有系统调用号。当处于read函数的阻塞状态(S状态,浅度睡眠 / 拷贝场景)时,进程可能收到信号被唤醒,这个时候函数就被打断了。 那么被唤醒处理信号后是回到阻塞状态继续拷贝数据,还是执行后续代码?

执行continue,这个时候就会重回原来的函数,如果有数据就重新读取,如果还没有数据就重新挂起(阻塞IO)。

2.多路转接

IO = 等 + 拷贝,如recv、recvfrom、read、write等系统调用都是等 + 拷贝组成的。多路转接的操作就是将等这个过程给提取出来,一次性等的fd更多。 就像有钱的钓鱼佬一次性等100个鱼竿,效率自然高很多,鱼儿咬钩的概率自然大。所以,解决对多个文件描述符进行等待,通知上层哪些fd已经就绪,这本质是一种对IO事件就绪的通知机制,这个“等 + 通知机制”就是多路转接。 多路转接一般更适合网络。

多路转接的方案有三种,select、poll、epoll,终极解决方案是epoll,集合了所有的优点并杜绝了所有的缺点,其底层自然更复杂。select、poll主要就是引子,我们了解他们的处理即可。

(1)select

①select代码逻辑

这是select启动服务后进入的处理循环,我们理解其逻辑。

Init函数

void Init()
{// 这是父类继承下来的,继承到不同子类实现不同的功能,即多态_listen_socket->BuildTcpSocketMethod(_port); // 这个时候服务端就已经搭建好了,IP + port已经被绑定好了,我们也完全不需要关心socket本身,我们面向TcpSocket这个对象操作即可// 遍历fd_setfor (int i = 0; i < MAX_FD; i++)_fd_array[i] = DefaultFd;           // 遍历时只要不是-1说明这个位置的fd就是我们需要去关心的_fd_array[0] = _listen_socket->GetFd(); // 辅助数组的第0位保存监听fd,也是当前唯一能确定需要关心的fd
}

Loop函数

void Loop()
{fd_set readfds; // 获取到读事件的fd集合_isrunning = true;while (_isrunning){// 一直循环,每一次都要根据辅助数组遍历得到我们关心的fd,将其放进readfds集合中,统一交给select处理,一次性可以等待多个fd// 每次进入循环都要对readfds进行清空FD_ZERO(&readfds);struct timeval timeout = {10, 0}; // 10s的超时时间int maxfd = DefaultFd;// 遍历辅助数组,将所有的fd都添加到readfds中for (int i = 0; i < MAX_FD; i++){if (_fd_array[i] != DefaultFd) // 辅助数组中保存的fd要么是初始化时填的-1,要么就有实际的作用{FD_SET(_fd_array[i], &readfds);        // 将当前要关心的fd都写进readfds集合中,便于后续统一传到select中maxfd = std::max(maxfd, _fd_array[i]); // 找出最大的fd,用于select的第1个参数}}// 调用select函数阻塞式等待多个fd,即可以一次性等待辅助数组里面关心的所有fd,目前只关心读事件,写事件、异常事件暂不关心,默认置为空// 调用select函数后,select负责阻塞式等待多个文件描述符的数据,数据准备好后recvfrom直接读就可以了。select可以传递多个文件描述符,一次等多个fdint n = select(maxfd + 1, &readfds, nullptr, nullptr, &timeout);  // 等待事件发生,返回值是就绪的fd的数量。最后一个参数可以传NULL,即无限时阻塞等待,多个fd的时候至少有一个就绪的时候fd就会返回// 只要有读事件就绪了,就退出阻塞状态if (n == 0) // 表示超时了LOG(LogLevel::INFO) << "单次超时,10s内无读事件就绪";else if (n == -1) // 表示出错了perror("select出错");else{// 有读事件就绪了,根据n和输入输出参数(readfds)对每个fd进行处理。readfds输出时就表示当前就绪的fd集合,n表示就绪的fd的数量LOG(LogLevel::INFO) << n << "个读事件就绪";Dispatcher(readfds); // 将fd_set这个结构体传过去,让Dispatcher去处理里面标记好的fd// 这个函数出来后所有的读事件就处理完成了。处理完成之后在返回循环最顶部,清空readfds,根据辅助数组重新设置要关心的fd,进入select继续等待}}_isrunning = false;
}

派发函数

void Dispatcher(fd_set &readfds) // 输出型参数,fd_set里面对应的fd就都是需要进行处理的
{// fd_set能够标记的最大数量就是MAX_FD,跟辅助数组的元素个数一致,所以可以直接遍历辅助数组,结合readfds一起进行判断哪个fd是就绪的,不能直接从fd_set里面直接获取fdfor (int i = 0; i < MAX_FD; i++){if (_fd_array[i] != DefaultFd && FD_ISSET(_fd_array[i], &readfds)) // 不在辅助数组关心的fd集合里面,不需要关心{// 从这里开始根据不同的fd类型进行分发if(_fd_array[i] == _listen_socket->GetFd()) // 监听fd就绪了,说明有新的客户端连接请求Accepter(); // 连接器,监听fd就绪了就应该去建立和客户端的连接                    else // 目前来讲分发器只关心监听fd和客户端连接的fd,后者就说明客户端开始发消息了,就去调用消息接收器Recver(i); // 消息接收器,客户端fd就绪了就应该去接收客户端的消息。这里只能传下标,因为Recver可能需要知道下标(关闭fd后清理辅助数组)}}
}

②select函数

select是输入关心的fd_set(读、写、异常事件),nfds(关心的最大fd + 1),阻塞时间timeval(为空表示一直阻塞),最后数据就绪时退出阻塞,fd_set作为输出参数,因此我们可以轮询得到就绪的事件并进行派发处理。

select返回值n > 0表示就绪了n个fd,n < 0表示等待失败(有错误的fd),n == 0表示底层fd还没有就绪,也没有出错,就是超时了。

select只有第一个参数是输入型参数,其余都是输入输出型参数。

readfds做输入的时候,用户告诉内核需要关心readfds位图中被设置了的fd上的读事件,其中bit位位置可以表示fd具体值。readfds做输出的时候,内核告诉用户readfds位图中用户关心的fd中哪些fd就绪了,对我们关心的已就绪的fd对应位置置为1。但注意,返回的fd_set位图中一定是我们关心的fd,我们不关心的或者关心却没就绪的fd位置都会是0,就算我们不关心的fd就绪了。

对于服务端来说,获取新连接accept本质就是IO,listenfd只关心读事件,connect其实就是服务端向listenfd里面写数据,所以我们将listenfd添加到读事件中,只要有事件未处理就会一直通知。

每次调用select,都要重新设置fd_set,因为输出时fd_set会被修改,所以我们要有一个设置源,即辅助数组。

③fd_set、timeval

中间三个参数表示读、写、异常文件描述符集fd_set*。fd_set是集合数据类型,是OS提供给用户的,定义的变量里面可以添加多个文件描述符。那么问题来了,fd_set具体是什么数据结构(用来保存fd的)?位图!0号描述符对应位图的0号位置,该位置设置为1就表示关心该fd。

关心读事件 -> fd是否可读 -> 接收缓冲区是否有数据
关心写事件 -> fd是否可写 -> 发送缓冲区是否有空间
关心异常事件 -> fd是否出现异常

我们可以把一个fd添加到不同集合,通过参数传过去,表示我们关心该fd上的该事件。

位图结构里面是int bits[N],例如第34个fd在下标为34 / 32 = 1里存着,34 % 32 = 2表示第2个bit,即第34个fd的关心信息存储在下标为1的元素的第2个bit位。 所以我们设置或者获取关心的fd本质上都是对位图进行操作,宏方法FD_SET可以直接帮我们设置fd到位图中,不用我们自己做位移操作,FD_ISSET同理。

但是fd_set并不是一个单纯的int bits[N],它的外面是一个结构体,fd_set是作为一个结构体来处理的(包装数组位图的struct)。 为什么?使用结构体方便传参、拷贝、修改,数组的话传参操作传着传着就会变成指针。

fd_set是一个具体的数据类型,既然是一个数据类型,那就有大小,fd_set是固定大小,所以fd_set的fd个数有上限,即select能关心的fd个数是有上限的,这个值在1000级别。

timeval结构体中tv_sec表示等待秒数,tv_usec表示等待微秒数。 当等待时间超过或者有至少一个事件就绪之后会立即返回。

④select的优缺点

多路转接是一种等的手段(等多个fd + 通知上层),而等其实本身也是一个手段,我们真正需要的是数据,等就是为了得到数据。我们需要深刻体会多路转接(select、poll、epoll)只是处理等这个过程。

select大概能同时处理1000多个fd,这取决于sizeof(fd_set),但总的来说支持的文件太少了!进程能打开的文件有上限,select可处理的fd也有上限,这就是缺点,为什么?OS的缺点不是select有缺点的理由。况且OS能对文件描述符数量进行扩容。

ulimit -a的open files表示是一个进程能打开的文件描述符个数。

还有个缺点,每次调用select前都需要手动设置fd_set(读、写、异常三个集合),这是因为输入参数会被输出覆盖,本质上是同一个位图输入输出导致的。 select的每次调用都要将集合从用户到内核拷贝,从内核到用户拷贝,拷贝开销大。并且每次都需要遍历集合,如果我们想要关心的fd本来就少,但每次都会遍历fd_set,高频起来后消耗不小。

面对select的缺点,如fd_set多次重置,fd个数有上限,我们引入了第二种多路转接—— poll。

(2)poll

①poll代码逻辑

Init函数

void Init()
{// 这是父类继承下来的,继承到不同子类实现不同的功能,即多态_listen_socket->BuildTcpSocketMethod(_port); // 这个时候服务端就已经搭建好了,IP + port已经被绑定好了,我们也完全不需要关心socket本身,我们面向TcpSocket这个对象操作即可for (int i = 0; i < MAX_FD; i++) // 初始化辅助数组,这个辅助数组是poll关心的fd和事件,默认fd都是-1{_fds[i].fd = DefaultFd;_fds[i].events = 0; // 我们设置关心的和返回的做了参数分离,这是对select的改进_fds[i].revents = 0;}_fds[0].fd = _listen_socket->GetFd(); // 辅助数组的第0位保存监听fd,也是当前唯一能确定需要关心的fd_fds[0].events = POLLIN;              // 关心读事件
}

_fds是poll中的辅助数组,这在后面会用到

Loop函数

void Loop()
{_isrunning = true;// 阻塞式等待的话poll直接传负数即可,select阻塞式等待需要传NULL// poll第三个参数timeout需要直接传整数,=0就是非阻塞,<0叫做一直阻塞式等待(select没有有这种写法)int timeout = 10 * 1000; // 10s,1表示1mswhile (_isrunning){// 有事件就绪后,revents就会被设置为就绪的事件,这是个阻塞函数int n = poll(_fds, MAX_FD, timeout); // 等待事件发生,返回值是就绪的fd的数量// 只要有读事件就绪了,就退出阻塞状态if (n == 0) // 表示超时了LOG(LogLevel::INFO) << "单次超时,10s内无读事件就绪";else if (n == -1) // 表示出错了perror("poll出错");else{LOG(LogLevel::INFO) << n << "个读事件就绪";Dispatcher(); // 遍历fds即可// 这个函数出来后所有的读事件就处理完成了。处理完成之后在返回循环最顶部}}_isrunning = false;
}

派发函数

// 处理就绪的事件,根据不同的fd进行分发处理
void Dispatcher()
{for (int i = 0; i < MAX_FD; i++){if (_fds[i].fd != DefaultFd && _fds[i].revents & POLLIN) // 必须是规范的fd并且读事件就绪{// 从这里开始根据不同的fd类型进行分发if (_fds[i].fd == _listen_socket->GetFd())   // 监听fd就绪了,说明有新的客户端连接请求Accepter();                              // 连接器,监听fd就绪了就应该去建立和客户端的连接else                                         // 目前来讲分发器只关心监听fd和客户端连接的fd,后者就说明客户端开始发消息了,就去调用消息接收器Recver(i);                               // 消息接收器,客户端fd就绪了就应该去接收客户端的消息。这里只能传下标,因为Recver可能需要知道下标(关闭fd后清理辅助数组)}}
}

②poll函数

第一个参数是结构体数组指针struct pollfd*,第二个是nfds表示其大小,这两个参数组成了一个数组的描述字段,因此我们可以关心多个fd,pollfd*告诉内核第一个元素地址,整数告诉数组有多少个元素。

③struct pollfd

pollfd的fd指的是用户关心的fd,short events是对应用户关心的fd的关心事件(用户 -> 内核)。revents是OS返回的,告诉这个fd哪些时间已经就绪了(内核 -> 用户),可以看到在struct pollfd层面,输入(events)输出(revents)分离,这样当我们调用poll之前我们就不需要重新设置传入的结构体。

调用poll时,用户告诉内核关心哪个fd上的哪些事件,之后内核返回告诉用户所关心的fd上所关心的事件哪些事件已经就绪了。 我们可以清晰的理解到,用户和内核之间插入了一个poll,用户到内核用poll,内核到用户用poll。 poll的定位就是对多个fd的IO事件的等待机制,poll函数只负责等,达到事件派发的目的,和select功能一致。

events和revents事件怎么组成的?

同理,这两个也是位图,用宏定义表示事件。其中最常用的是POLLPRI紧急读事件(类似紧急指针),POLLIN读事件,POLLOUT写事件。short是16位,我们最多关心16个事件,需要通过按位或设置,按位与获取,OS没有提供接口。

用户设置自己关心的事件时只管events,不管revents,revents这是内核设置的

④poll的优缺点

优点

struct pollfd是数组,nfds是数组元素的个数,这个数组理论上并没有上限,换句话说这个上限由用户 / 系统 / 硬件决定,但就是不关poll的事,因为你传多大的数组都可以。 nfds理论上你可以传无穷大,这个时候能限制你的就是系统了,但始终和poll的设计没关系,我们要深刻理解这点。

缺点

依旧需要内核在底层遍历式地访问我们的struct pollfd数组,检测所有文件描述符fd是否有事件就绪。这还是有很多循环,导致效率到一定程度上就减缓了,fd越多,遍历成本变高,效率也就越低。同时事件就绪后也需要用户完全遍历数组,每个元素判断是否就绪。 同时poll也需要不断在内核和用户之间拷贝,处理revents和events,但不是主要问题,也没有模型能解决。

基于poll的缺点,我们需要引入多路转接的终极方案,即epoll模型。它继承了select和poll的所有优点并解决了所有缺点,其底层依赖一种回调机制,后续会讲到。

(3)epoll

①epoll代码逻辑

epoll模型相关的就是epoll_create、epoll_ctl、epoll_wait三个接口,epoll的定位是关注多个fd的IO事件的等待机制,是一种就绪事件的通知机制,达到事件派发的目的。

Init函数

void Init()
{// 这是父类继承下来的,继承到不同子类实现不同的功能,即多态_listen_socket->BuildTcpSocketMethod(_port); // 这个时候服务端就已经搭建好了,IP + port已经被绑定好了,我们也完全不需要关心socket本身,我们面向TcpSocket这个对象操作即可// 创建epoll模型_epfd = epoll_create(256);if (_epfd < 0){LOG(LogLevel::FATAL) << "epoll模型创建失败";exit(static_cast<int>(myError::EPOLL_CREATE_ERROR));}LOG(LogLevel::INFO) << "epoll模型创建成功,_epfd为" << _epfd;// 将一定服务器的listenfd添加到epoll模型的红黑树中struct epoll_event ev;ev.events = EPOLLIN;                                                   // 关心读事件ev.data.fd = _listen_socket->GetFd();                                  // data是一个联合体,我们写入fdint n = epoll_ctl(_epfd, EPOLL_CTL_ADD, _listen_socket->GetFd(), &ev); // 将我们关心的fd和事件写入到epoll模型的红黑树中// 红黑树就相当于poll和select的辅助数组,只不过在epoll这里不需要我们进行维护。除了添加到红黑树中,ctl还会注册服务,使得后续数据准备就绪后会自动添加到就绪队列中if (n < 0){LOG(LogLevel::FATAL) << "epoll模型添加监听fd失败";exit(static_cast<int>(myError::EPOLL_CTL_ERROR));}LOG(LogLevel::INFO) << "epoll模型添加监听fd成功,监听fd为" << _listen_socket->GetFd();
}

Loop函数

void Loop(){// 阻塞式等待的话poll直接传负数即可,select阻塞式等待需要传NULL// poll、epoll的最后一个参数必须是int类型int timeout = 10 * 1000; // 10s_isrunning = true;while (_isrunning){// 有事件就绪后,revents就会被设置为就绪的事件int n = epoll_wait(_epfd, _revs, revs_num, timeout); // 阻塞函数,等待事件发生,返回值是就绪的fd的数量// 只要有读事件就绪了,就退出阻塞状态,接收到的_revs一定不会越界,传参时revs_num限制了。并且_revs里面装的全部都是就绪的事件,不需要像辅助数组那样进一步判断if (n == 0) // 表示超时了LOG(LogLevel::INFO) << "单次超时,10s内无读事件就绪";else if (n == -1) // 表示出错了perror("epoll出错");else{LOG(LogLevel::INFO) << n << "个读事件就绪";Dispatcher(n); // 告诉派发函数现在有多少个读事件就绪了,派发函数就会循环_revs处理所有读事件// 这个函数出来后所有的读事件就处理完成了。处理完成之后在返回循环最顶部}}_isrunning = false;}

派发函数

// 处理就绪的事件,根据不同的fd进行分发处理
void Dispatcher(int n)
{for (int i = 0; i < n; i++){int events = _revs[i].events; // 获取就绪事件int fd = _revs[i].data.fd;    // 获取就绪fdif (events & EPOLLIN) // fd一定有效,读事件就绪{// 从这里开始根据不同的fd类型进行分发if (fd == _listen_socket->GetFd()) // 监听fd就绪了,说明有新的客户端连接请求Accepter();                    // 连接器,监听fd就绪了就应该去建立和客户端的连接else                               // 目前来讲分发器只关心监听fd和客户端连接的fd,后者就说明客户端开始发消息了,就去调用消息接收器Recver(fd);                    // 消息接收器,客户端fd就绪了就应该去接收客户端的消息。}}
}

②epoll_create

这是Init函数中需要做的,即创建epoll模型。调用epoll_create即可创建一个epoll模型,里面的int size参数被忽略无意义,我们不管它,但必须大于0。函数返回一个文件描述符fd,如果创建epoll模型失败就会返回-1,并设置错误码。

我们后续就可以通过这个fd找到模型,后续的操作都是针对该模型进行操作的。

③epoll_ctl

epoll_ctl第一个参数就是epoll模型的fd,第二个参数op操作有EPOLL_CTL_ADD、DEL、MOD(分别针对添加事件关心、删除事件关心、修改事件关心),第三个参数是我们要设置关心的fd,第四个参数是关心的事件(位图)。 这个事件需要我们用结构体传入,代码如下:

struct epoll_event ev;
ev.events = EPOLLIN;                                                   
ev.data.fd = _listen_socket->GetFd();                                  
int n = epoll_ctl(_epfd, EPOLL_CTL_ADD, _listen_socket->GetFd(), &ev); 

EPOLLIN表示关心读,EPOLLOUT表示关心写,EPOLLPRI表示紧急读,和poll的类似。在设置和获取中,都是自己用位运算获取结果,和poll类似。

通过ctl,我们可以添加关心的fd和关心的功能,这是用户告诉内核,成功返回0,失败返回-1。

④epoll_wait

wait是内核告诉用户。 events是输出型参数,是一个数组指针,maxevents是这个数组的长度。用户传入数组和个数后,可以获取就绪的事件,并且返回值就是epoll写入的events的有效长度。

ctl是用户向内核,wait是内核向用户,在poll和select上,用户到内核、内核到用户都集成在一个函数中,但在epoll上直接从接口上做了区分,参数也进行了分离。

⑤struct epoll_event


events数组是struct epoll_event类型的指针,这个类型表示epoll事件,该结构体包含两个参数,第一个是事件类型events(输入输出都用它来进行事件表示),第二个参数data是联合体类型。


这个联合体是fd、ptr等字段联合的,我们一般就使用fd即可。

⑥epoll的优点

epoll在接口层面将用户到内核,内核到用户两个过程做了接口分离。用户设置的时候自己调用ctl即可,需要开启多路转接就阻塞在wait等待,并且能够一次性处理的fd无上限。除此之外,wait返回n后,我们的struct epoll_event数组里面所有的前n个元素全部都是有效的事件,不像poll那样我们还需要自己去判断一下当前元素有没有就绪。

if (_fds[i].fd != DefaultFd && _fds[i].revents & POLLIN) // poll中需要遍历判断

但最大的优势,在于底层epoll不会循环式遍历我们关心的fd和事件了,判断它们有没有就绪。而是利用一种回调机制,使得每次epoll调用wait时,都只会到一个就绪队列里面取数据。 至于如何到就绪队列的,完全自动触发。这个底层结构我们后续文章会讲到。

http://www.dtcms.com/a/513362.html

相关文章:

  • jq动画效果网站wordpress 两个数据库 互通
  • 网站如何做淘宝推广食品经营许可证
  • wordpress 架站 电子书三水区网站建设
  • opendds初入门之monitor监控的简单练习
  • 清新区住房和城乡建设局网站企业建设H5响应式网站的5大好处
  • 做营利网站的风险太原网站建设制作报价
  • 在Jetson 上使用Intel RealSense D435运行RTAB-Map就行建图
  • 怎么建立自己的网站域名咸阳软件开发公司
  • 织梦做的网站怎样做小程序的平台
  • 律师在哪个网站做推广好深圳哪个网站发布做网站
  • 三字型布局的网站中国肩章大全图解
  • 响应式网站成本品牌官网建设内容
  • 提供网站备案建设服务大连甘井子区社区工作者招聘
  • 公司网站开发费用大概多少商品交易平台
  • 徐州网页设计seo前景
  • 【Linux】基本指令(入门篇)(下)
  • 湛江企业网站黄山旅游攻略自由行攻略
  • 音酷网站建设那些网站后台做推广效果好
  • Polar 逆向(简单难度)
  • 青岛网站建设比较好做外贸都做哪些网站好免费
  • 做自己的网站有什么用农村自建房设计图120平方二层
  • FITC-PEG-Silane|荧光素-聚乙二醇-硅烷|化学特性与功能
  • MyBatis-Plus-使用
  • 企业网站的常见服务是什么施工企业成立技术中心的好处
  • 软件下载大全网站家里电脑可以做网站空间吗
  • [MySQL] 联合查询
  • 黑客马拉松竞赛中产品成功要素与商业价值实现[特殊字符]
  • 做物流网站的公司网络规划设计师报名
  • 网站项目建设的必要性德国红点设计奖官网
  • 能支持微信公众号的网站建设网站策划的重要性