Linux网络:多路转接

news/2024/11/7 1:45:01/

高级IO

1. IO的相关概念

内存和外设进行沟通的动作叫做IO。在网络层面,数据往网络里写的本质是将数据从内存拷贝到网卡上,从网络里读的本质是将数据从网卡拷贝到内存中。

当程序运行到IO函数时一般都在阻塞式等待,这也算作IO过程中的一个环节,但真正意义上的IO就是讲数据拷贝到缓冲区中。

任何IO过程都要包含两个步骤:等待和拷贝。在实际的应用场景中,等待消耗的时间往往都远远高于拷贝的时间,所以提高IO效率的本质是:尽可能地减少单位时间内“等待”的比重

提高IO效率的方式有两种:改变等待的方式和减少等待的比重。

1.1 五种IO模型

IO模型简单对比解释
阻塞IO一心一意地等待数据到来
非阻塞IO三心二意地轮询式等待
信号驱动信号递达时再来读取或写入数据
多路转接让大批线程等待,自身读取数据
异步通信让其他进程或线程进行等待和读取,自身获取结果

除了最后一种是异步IO,其他都是同步IO。分辨二者的方式就是看自身是否参与IO。

阻塞IO

阻塞IO是最简单最常见的一种IO方式,任何IO函数的核心工作都有两部分组成:等和拷贝,且默认使用的都是阻塞方式。

非阻塞IO

等待子进程时,我们也使用过一次轮询检测退出的方案。思路和非阻塞IO是一致的。

信号驱动

当内核准备好数据时,使用SIGIO信号通知进程进行IO操作。可以说,信号驱动只使用了recv的拷贝功能。等待数据的过程是异步的,但拷贝数据是同步的,所以我们认为信号驱动也是同步IO。

信号驱动的使用成本有点高,一般也不常用。

多路转接

操作系统提供接口select,poll,epoll给程序提供等待功能,最高同时可等待几百个文件。将IO任务拆分,拷贝数据的任务仍由进程完成,等待数据的任务交给系统接口。

异步通信

将缓冲区变量提供给异步接口,接口会等待并将数据放到缓冲区中,并通知进程。进程可以直接处理数据,并不会参与任何IO过程,所以是异步的。

异步IO系统提供有一些对应的系统接口,但大多使用复杂,也不建议使用。异步IO也有更好的替代方案。

1.2 高级IO的概念

同步IO和异步IO

同步和异步关注的是消息通信机制。

  • 同步:没有得到结果就不返回,调用返回就是得到返回值。换句话说,就是由调用者主动等待调用的结果。
  • 异步:调用发出之后就直接返回,所以没有返回结果。换句话说,当异步调用发出后,不会立刻得到结果,而是被调用者通过状态通知来调用者,或通过回调函数处理这个调用。

此外,多线程提到同步和互斥和这里的是完全不想干的概念。

阻塞和非阻塞

阻塞和非阻塞关注的是程序在等待调用结果(消息、返回值)时的状态。

  • 阻塞调用是指调用结果返回之前,当前线程会被挂起,调用线程只有在得到结果之后才会返回。

  • 非阻塞调用指在不能立刻得到结果,该调用不会阻塞当前线程。

如果非阻塞调用没有获取到数据时,是以出错的形式返回的,但并不算真正的错误。通过错误码errno区分出错和条件未就绪

fcntl接口

fcntl系统调用可以帮我们将读写设置为非阻塞状态。

void SetNonBlock(int fd)
{int fl = fcntl(fd, F_GETFL);if (fl < 0) {perror("fcntl error\n");return;}fcntl(fd, F_SETFL, fl | O_NONBLOCK);
}
int main()
{SetNonBlock(0);char buffer[1024];while (1) {errno = 0;ssize_t s = read(0, buffer, sizeof(buffer) - 1);if (s > 0) {buffer[s] = 0;write(1, buffer, strlen(buffer));printf("read success, s: %ld, errno: %d\n", s, errno);}else {if (errno == EAGAIN || errno == EWOULDBLOCK) {printf("read failed, s: %ld, errno: %d\n", s, errno);// Try Again}else {printf("read error, s: %ld, errno: %d\n", s, errno);}}sleep(1);}return 0;
}

IO事件就绪

一旦等的事件就绪,我们就可以进行拷贝了。如何理解IO事件就绪呢?

IO事件就绪可以理解为两方面:一是读事件就绪,一是写事件就绪。可以认为接收缓冲区有数据就是读事件就绪,发送缓冲区里无数据就是写事件就绪。

但事实上,频繁读写系统内核中的发送接收缓冲区会进行状态切换,期间会进行一系列的处理工作,会带来效率的下降。

所以一般接收缓冲区中设有高水位,发送缓冲区中设有低水位的概念。

 

2. 多路转接IO模型

Linux下多路转接的方案常见的有三种:selectpollepoll,select出现是最早的,使用也是最繁琐的。

2.1 select

select的接口介绍

select函数没有读写的功能,只负责IO当中等的环节,一旦条件就绪会通知上层进行读取和写入。

read/write,send/recv等IO接口本身也具有等待的功能,不过只能等1个文件条件就绪,select可以同时等多个fd。

NAMEselect, FD_CLR, FD_ISSET, FD_SET, FD_ZERO
SYNOPSIS#include <sys/select.h>int select(int nfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds,struct timeval *timeout);// fd_set操作函数void FD_CLR  (int fd, fd_set *set); // 清除int  FD_ISSET(int fd, fd_set *set); // 检测void FD_SET  (int fd, fd_set *set); // 设置void FD_ZERO (        fd_set *set); // 置零

fd_set是一种位图结构,本质是从0开始的小整数。

参数解释
nfds所有被等待的fd的最大值+1
readfds等待读事件就绪,表示等待结束可以读取
writefds等待写事件就绪,表示等待结束可以写入
exceptfds等待异常事件就绪,如对端关闭,读写异常等
timeouttimeval结构类型,设置等待时间,时间一到立马返回
返回值大于0表示事件就绪的fd总数,为0表示超时并且没有任何事件就绪,-1表示出错

使用三个fd位图结构参数readfdswritefdsexecptfds分别表示只关心读、写、异常的事件的就绪。

#include <sys/time.h> 
struct timeval {long    tv_sec;         /* seconds */long    tv_usec;        /* microseconds */
};

select的核心功能

  1. 用户告知内核,你要帮我关心指定的fd上的读写事件是否就绪
  2. 内核告知用户,你所关心的指定的fd中那些fd上的读写事件已经就绪
fd_set

用户如何告知内核,指定的多个文件描述符呢?

fd_set是表示文件描述符的位图结构,本质是从0开始的小整数,就是用来指定多个文件的fd的。

  • 位图相当于数据,下标表示文件描述符的编号;
  • 位图每个比特位值为1或0,分别表示需要等待和不需等待。

  • readfdswritefdsexecptfds这些参数是输入输出型参数:
  1. 输入时,用户告诉内核,你要帮我关心的fd的集合;
  2. 输出时,内核告诉用户,哪些fd的事件已经就绪,事件就绪的fd会被置1,其他被置0。
struct timeval
  • timeout也是输入输出型参数,表示此次返回所剩余的等待时间。
  • 为空表示阻塞等待,如果设置时间就表示指定时间内阻塞等待。

fd_set的操作接口

安全统一起见,系统会为我们提供文件描述符集的操作接口。

// fd_set操作函数
void FD_CLR  (int fd, fd_set *set); // 清除
int  FD_ISSET(int fd, fd_set *set); // 检测
void FD_SET  (int fd, fd_set *set); // 设置
void FD_ZERO (        fd_set *set); // 置零

fd_set是个位图结构,只要是变量就一定有大小,说明select能等待的fd个数也是有限的。

select的执行流程

  1. 执行 fd_set set; FD_ZERO(&set); 则set用位表示是0000,0000;
  2. 若 fd=5, 执行 FD_SET(fd, &set); 后set变为0001,0000;
  3. 若再加入fd=2, fd=1,则set变为0001,0011;
  4. 执行 select(6,&set,0,0,0); 进行阻塞等待;
  5. 若 fd=1,fd=2 上都发生可读事件,则select返回,此时set变为 0000,0011,fd=5被清空。

位图被修改,下次该如何设置?

因为 select 用输入输出型参数表示不同的含义,意味着之后每一次调用都要对 fd_set 重新设置内容。用户必须将需要等待的fd用数组或其他容器保存起来。

//select_server.cc
#include <iostream>
#include <string>
#include <vector>
#include <algorithm>
#include <unistd.h>
#include <sys/select.h>
#include <sys/types.h>
#include "Socket.hpp"static void Usage(std::string proc) {std::cout << "Usage: \n\t" << proc << " port" << std::endl;exit(1);
}
std::vector<int> fd_v;
inline int GetMaxFd() {int max = 0;for (auto e : fd_v) {if (max < e) {max = e;}}return max;
}
int GetIndex(int key) {auto pos = std::find(fd_v.begin(), fd_v.end(), key);if (pos != fd_v.end()) {return pos - fd_v.begin();}else {return -1;}
}
// ./select_server 8080
int main(int argc, char* argv[])
{if (argc != 2) {Usage(argv[0]);}uint16_t port = atoi(argv[1]);int listen_sock = TcpSocket::Socket();TcpSocket::Bind(listen_sock, port);TcpSocket::Listen(listen_sock);//先交给select等待,事件就绪后才能acceptfd_v.push_back(listen_sock);//事件循环for (;;){fd_set read_fds;FD_ZERO(&read_fds); // 清空fd集for (auto e : fd_v) {if (e != -1) {FD_SET(e, &read_fds); // 数组内容设置进fd集}}struct timeval timeout = { 5, 0 };switch (select(GetMaxFd() + 1, &read_fds, nullptr, nullptr, nullptr)){case 0:std::cout << "select timeout" << std::endl;break;case -1:std::cerr << "select failed" << std::endl;break;default:std::cout << "select get fds readly" << std::endl;for (auto& fd : fd_v){if (fd != -1) // 首先排除非法fd{if (FD_ISSET(fd, &read_fds)) // 读事件就绪{std::cout << "sock: " << fd << " 读事件就绪" << std::endl;if (fd == listen_sock) // 监听读事件就绪{int sock = TcpSocket::Accept(fd);std::cout << "事件为建立新链接: " << sock << std::endl;if (sock >= 0){//push到数组中,以待交给select等待int pos = 1;for (; pos < fd_v.size(); pos++) { // 查找可用位置if (fd_v[pos] == -1) {break;}}if (pos < fd_v.size()) {fd_v[pos] = sock; // 提取连接到来不代表读事件就绪}else {fd_v.push_back(sock);}std::cout << "新连接被添加在数组位置[" << pos << "]" << std::endl;}else {close(fd);}}else // 普通读事件就绪{std::cout << "事件为普通读取事件" << std::endl;// 本次读取不会阻塞,但不能保证读取的可靠性,并且没有场景就没办法定制协议std::string recv_buffer;if (TcpSocket::Recv(fd, recv_buffer)) {std::cout << "client [" << fd << "]# " << recv_buffer << std::endl;}else {std::cout << "sock: " << fd << "关闭连接,客户端退出,数组位置["<< GetIndex(fd) << "]已被清理" << std::endl;fd = -1; //数组位置为-1close(fd);}}}}}break;}}return 0;
}

select的优缺点

优点

一次等待多个fd,使IO等待时间重叠,一定程度上提高IO效率。

缺点
  1. 每次调用前要重新设置fd集,调用后要遍历数组检测就绪fd
  2. fd_set结构导致select能够检测fd的个数有上限
  3. select内部实现是通过遍历修改fd_set结构的
  4. select可能需要高频地进行用户内核空间之间的相互拷贝

 

2.2 poll

poll 相比 select 是进步的表现,在使用和实现上都有一定的进步。不过我们重点仍是epoll,对于 poll 我们只会介绍一下接口,再把 select的代码改成 poll 的。

poll的接口介绍

NAMEpoll – synchronous I/O multiplexingSYNOPSIS#include <poll.h>int poll(struct pollfd* fds, nfds_t nfds, int timeout);// pollfd结构体struct pollfd {int    fd;       /* file descriptor */short  events;   /* events to look for */short  revents;  /* events returned */};
  • timeout仍然是阻塞等待时间,作用和select一样,不过直接采用整数,单位是毫秒。
  • struct pollfd* nfds_t可以看成是数组起始地址和长度,数组中元素是fd和事件的组合结构体。

struct pollfd结构体中字段:

  • fd表示事件所在的文件描述符
  • events表示用户所要关心的事件,revents表示内核返回的就绪事件。

输入输出分离,就不用每次输入了。

events和revents的取值,有如下宏定义:

事件描述
POLLIN数据(包括普通数据和优先数据)可读
POLLRDNORM普通数据可读
POLLRDBAND优先级带数据可读(Linux 不支持)
POLLPRI高优先级数据可读,比如 TCP 带外数据
POLLOUT数据(包括普通数据和优先数据)可写
POLLWRNORM普通数据可写
POLLWRBAND优先级带数据可写
POLLRDHUPTCP 连接被对方关闭,或者对方关闭了写操作,它由GNU引入
POLLERR错误
POLLHUP挂起。比如管道的写端被关闭后,读端描述符将收到 POLLHUP 事件
POLLNVAL文件描述符没有打开

添加宏到变量可以使用或运算,检测特定的事件可以用与运算。

events |= POLLIN;
events |= POLLIOUT;if(revents & POLLIOUT) {//...
}

poll的使用介绍

//poll_server.cc
#include <iostream>
#include <string>
#include <vector>
#include <algorithm>
#include <unistd.h>
#include <poll.h>
#include <sys/types.h>
#include "Socket.hpp"
#define NUM 128static void Usage(std::string proc) {std::cout << "Usage: \n\t" << proc << " port" << std::endl;exit(1);
}
struct pollfd nfds[NUM];
int main(int argc, char* argv[])
{if (argc != 2) {Usage(argv[0]);}uint16_t port = atoi(argv[1]);int listen_sock = TcpSocket::Socket();TcpSocket::Bind(listen_sock, port);TcpSocket::Listen(listen_sock);nfds[0].fd = listen_sock;nfds[0].events = POLLIN;nfds[0].revents = 0;//事件循环for (;;) {switch (poll(nfds, NUM, -1)) {case 0:std::cout << "poll timeout" << std::endl;break;case -1:std::cerr << "poll failed" << std::endl;break;default:std::cout << "有事件就绪" << std::endl;for (int i = 0; i < NUM; i++) {if (nfds[i].fd != -1 && nfds[i].revents != 0)  {if (nfds[i].fd == listen_sock) {int sock = TcpSocket::Accept(nfds[i].fd);std::cout << "事件为建立新链接: " << sock << std::endl;if (sock >= 0) {//push到数组中,以待交给select等待int pos = 1;for (; pos < NUM; pos++) { // 查找可用位置if (nfds[pos].fd == -1) {break;}}if (pos < NUM) {nfds[pos].fd = sock; // 提取连接到来不代表读事件就绪nfds[pos].events = POLLIN;std::cout << "新连接被添加在数组位置[" << pos << "],并设置等待事件" << std::endl;}else {close(nfds[i].fd);}}else {close(nfds[i].fd);}}else { // 普通事件就绪if (nfds[i].revents & POLLIN) {std::cout << "事件为普通读取事件" << std::endl;std::string recv_buffer;if (TcpSocket::Recv(nfds[i].fd, recv_buffer)) {std::cout << "client [" << nfds[i].fd << "]# " << recv_buffer << std::endl;}else {std::cout << "sock: " << nfds[i].fd<< "关闭连接,客户端退出,数组位置["                                   << i << "]已被清理" << std::endl;nfds[i].fd = -1; //数组位置为-1nfds[i].events = 0;close(nfds[i].fd);}}else {std::cout << "事件为普通写入事件" << std::endl;}}}}break;}}return 0;
}

监控标准输入的小程序

#include <iostream>
#include <cstring>
#include <unistd.h>
#include <poll.h>
int main() {struct pollfd rfds;rfds.fd = 0;rfds.events = POLLIN;rfds.revents = 0;while (1) {int n = poll(&rfds, 1, 1000);if (n < 0) {std::cerr << "poll error" << std::endl;}else if (n == 0) {std::cout << "remote close" << std::endl;}else {if (rfds.revents & POLLIN) {char buffer[128];size_t s = read(0, buffer, sizeof(buffer));if (s > 0) {buffer[s] = 0;std::cout << "client# " << buffer << std::endl;}write(1, buffer, strlen(buffer));}}}return 0;
}

poll的优缺点

优点
  1. 改fd集为pollfd数组,理论上解决了监视fd的个数有上限的问题。
  2. 将fd和事件集合到结构体中,使用更方便
  3. 将事件输入输出分离,避免原始数据被修改,每次都要重新导入。
缺点
  1. 和select一样,poll返回后,仍要轮询pollfd数组检测就绪的事件
  2. 每次调用都要将pollfd结构从内核空间拷贝到用户空间。

 

2.3 epoll

epoll才是使用和面试的重点。在效率和易用性方面都有提升。

epoll和其他多路转接方案一样,都是只负责等。方式都是用户告知内核fd和事件,内核告诉用户,fd的事件就绪情况。

epoll的接口介绍

epoll_create
       #include <sys/epoll.h>int epoll_create(int size);
  • 自Linux内核版本2.6.8之后,size参数是默认忽略的,但为了向前兼容,大多写成128或256。
  • 返回值是epoll句柄,对应创建一个epoll模型。

如图所示,epoll_create 返回值本质是个文件描述符,值为3。

这个返回值的具体内容会在epoll的工作原理处讲解。

epoll_ctl
       int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);

不管是哪种多路转接方案,都要进行的工作步骤是:用户告诉内核和内核告诉用户。而epoll_ctl负责的就是用户告诉内核的任务。

参数解释
epfdepoll_create 接口的返回值
op用来指定对fd上的事件的操作,一共有三种:添加修改删除,如下所示
EPOLL_CTL_ADD:注册新的fd到epfd中
EPOLL_CTL_MOD:修改已经注册的fd的监听事件
EPOLL_CTL_DEL:从epfd中删除一个fd
fd用来指定关心的文件描述符
epoll_event结构体,用来指定参数fd上对应的事件
       //event结构体typedef union epoll_data {void    *ptr;int      fd;uint32_t u32;uint64_t u64;} epoll_data_t;struct epoll_event {uint32_t     events;    /* Epoll events */epoll_data_t data;      /* User data variable */};
  • epoll_event 中的events和poll一样都是用的是宏常量,具体内容如下所示:
解释
EPOLLIN表示对应的文件描述符可以读(包括对端SOCKET正常关闭)
EPOLLOUT表示对应的文件描述符可以写
EPOLLPRI表示对应的文件描述符有紧急的数据可读(带外数据)
EPOLLERR表示对应的文件描述符发生错误
EPOLLHUP表示对应的文件描述符被挂断
EPOLLET将EPOLL设为边缘触发(Edge Triggered)模式
EPOLLONESHOT只监听一次事件,本次之后自动将该fd删去

epoll_data_t字段挺重要,但短期内不用,留到后面解释。

和 select 和 poll 不同,epoll_ctl 向内核提供用户所关心的fd和事件时只用提供一次,如果要删除或修改就再调用时修改下op。

epoll_wait
       int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout);

显然epoll_wait负责的就是内核告诉用户特定fd事件就绪的任务。

参数解释
epfdepoll_create的返回值,epoll模型
epoll_event输出缓冲区,存放内核通知的已就绪的哪些fd的哪些事件
maxevents缓冲区的长度
timeout等待的阻塞时间
返回值有事件就绪的fd的个数

和 select 和 poll 不同,epoll_wait 不需要遍历第三方数组或容器来检测哪些fd的事件已经就绪,epoll_wait 会将其封装成 epoll_event 结构按顺序输出到缓冲区,具体个数就是返回值。

使用示例
#include <iostream>
#include <string>
#include <sys/epoll.h>
#include <unistd.h>
#include "Socket.hpp"
#define NUM 128static void Usage(std::string proc) {std::cerr << "Usage: \n\t" << proc << " port" << std::endl;exit(1);
}int main(int argc, char* argv[])
{if (argc != 2) {Usage(argv[0]);}uint16_t port = atoi(argv[1]);//1. 建立tcp socketint listen_sock = TcpSocket::Socket();TcpSocket::Bind(listen_sock, port);TcpSocket::Listen(listen_sock);//2. 创建epoll模型int epfd = epoll_create(128);//3. 添加listen_sock和事件到内核struct epoll_event ev;ev.events = EPOLLIN;ev.data.fd = listen_sock;epoll_ctl(epfd, EPOLL_CTL_ADD, listen_sock, &ev);//4. 事件循环struct epoll_event repevs[NUM];volatile bool quit = false;while (!quit) {int timeout = -1;//repevs就绪事件int n = epoll_wait(epfd, repevs, NUM, timeout);switch (n) {case -1:std::cerr << "poll failed" << std::endl;break;case 0:std::cout << "poll timeout" << std::endl;break;default:std::cout << "有事件就绪" << std::endl;//5. 处理就绪for (int i = 0; i < n; i++) {int fd = repevs[i].data.fd; // 暂时方案std::cout << "fd: " << fd << " 上有事件就绪啦" << std::endl;if (repevs[i].events & EPOLLIN) {std::cout << "fd: " << fd << " 读事件就绪" << std::endl;if (fd == listen_sock) { // 处理链接事件 std::cout << "fd: " << fd << " 是链接读事件" << std::endl;int sock = TcpSocket::Accept(listen_sock);if (sock >= 0) {std::cout << "fd: " << fd << " 链接获取成功 sock: " << sock << std::endl;struct epoll_event _ev;_ev.data.fd = sock;_ev.events = EPOLLIN; // | EPOLLOUT | EPOLLERRepoll_ctl(epfd, EPOLL_CTL_ADD, sock, &_ev);std::cout << "sock: " << sock << " 交给epoll托管成功" << std::endl;}else {}}else { // 处理普通读取事件std::cout << "fd: " << fd << " 普通读事件就绪" << std::endl;std::string recv_buffer;if (TcpSocket::Recv(fd, recv_buffer)) {std::cout << "client[" << fd << "]# " << recv_buffer << std::endl;}else {epoll_ctl(epfd, EPOLL_CTL_DEL, fd, nullptr);close(fd);std::cout << "fd: " << fd << " 出错已从epoll模型中移除" << std::endl;}}}else if (repevs[i].events & EPOLLOUT) // 处理写入事件{}else {}}break;}}close(epfd);close(listen_sock);return 0;
}

 

epoll的工作原理

调用epoll_create一定是一个进程,而进程会有一个关联的文件描述符数组。

当我们创建一个 epoll 模型时,在模型中内核为我们维护一棵红黑树,节点中存储是用户让内核关注的fd和相关事件并携带其他的一些信息,例如:

struct rb_tree_node {int fd;uint32_t events;struct rb_tree* left;struct rb_tree* right;enum color;//...
}

数据从硬件拷贝到内核,操作系统在将其拷贝到进行相关的各种缓冲区中,此时数据的事件才算就绪。操作系统会顺便遍历检测下其他fd下的的数据是否就绪是否需要拷贝,然后CPU将进程从等待队列中唤醒,操作系统在通知上层哪些fd的那些事件已经就绪了,select和poll是这样的策略。

epoll会更进一步,在用户向内核中注册fd和相关事件的时候,epoll 会触发底层相应的回调机制:维护一个就绪队列,队列节点中保存fd和就绪的事件。

在操作系统将数据拷贝到缓冲区中后,执行回调机制:拿缓冲区内容和对应fd在就绪队列中新增一个节点,然后再唤醒进程,epoll_wait 就会检测这个就绪队列,再向上层通知就绪情况。

epoll_create的作用

创建epoll模型:也就是存储组注册fd及其事件的红黑树、底层缓冲区内有数据后内核触发的回调机制以及保存就绪fd及其事件的就绪队列。

红黑树、回调机制、就绪队列都是以文件的形式链接在进程相关文件结构中的,上层可直接用fd找到创建的 epoll 模型。

epoll_ctl的作用
  1. 在红黑树中新增一个节点,增删改fd及其事件其实是对红黑树中的节点进行相应的增删改操作。
  2. 建立该 fd 的回调策略,这个回调方法会将发生的事件添加到就绪队列中。
epoll_wait的作用

以O(1)的事件复杂度,检测是否有事件就绪。

检查是否有事件发生时,只需要检查队列是否有元素即可。如果队列不为空,则把就绪事件复制到用户态,同时将事件数量返回给用户。

 

epoll的优缺点

  • 接口使用方便:接口分离解耦,更方便高效。不需要重新设置fd及事件集合,做到输入输出参数分离。
  • 轻量数据拷贝:只在合适的时候调用 epoll_ctl 将文件描述符结构拷贝到内核中,这个操作并不频繁。
  • 事件回调机制:避免使用遍历,将就绪的fd事件集合加入到就绪队列中,通过就绪队列获取就绪情况。
  • 没有数量限制:文件描述符数目无上限。

 

epoll的工作方式

epoll的工作方式有两种,分别是LT方式(水平触发)和ET方式(边缘触发)。这是epoll特有的模式概念,select和poll是没有的。

触发策略对比解释
LT水平触发底层只要有事件就绪,只要不被取走就一直通知上层
ET边缘触发底层只有当事件就绪的个数发生变化时,也就是从无到有或者个数增多,才会通知一次上层

水平触发像是一个尽职的快递员,只要你不去取就会一直在提醒你。边缘触发干活很随便,只为了完成任务通知一次,之后爱来不来爱取不取。

LT水平触发

epoll 默认状态下就是 LT 工作模式。

当epoll_wait返回后,可以不立刻进行处理或者只处理就绪事件的一部分。之后再调用epoll_wait仍会通知事件就绪,直到缓冲区的所有数据都被处理

ET边缘触发

在添加fd及事件时如果使用EPOLLET标志,表示epoll进入ET模式,事件就绪时,必须立即将缓冲区内所有数据处理完毕,否则再调用epoll_wait的时候是不会通知该事件就绪的

可以说,ET模式下事件就绪只有一次处理机会,所以必须采用非阻塞方式循环读写。ET模式能减少epoll的触发次数,代价是倒逼程序一次响应就把所有数据处理完。

一般情况下,ET的性能比LT性能更高,因为其返回的次数少。如果LT模式下也能做到每次事件就绪都立刻处理,使其不被重复提示的话,性能也是一样的。


http://www.ppmy.cn/news/681698.html

相关文章

FX5U与台达变频器ModBus通讯

1、接线&#xff1a; 2、台达变频器参数设置&#xff1a;首先进行格式化 3、地址介绍 3、PLC设置&#xff1a; 4、程序&#xff1a; 参考视频&#xff1a;https://v.youku.com/v_show/id_XNDE5NTc3MTc3Mg%3D

这几款软件可以在线视频转音频

对于从事视频剪辑行业的小伙伴来说&#xff0c;常常需要将视频转换为音频文件。特别是影视剧对话配音或者是音乐视频MV都需要转为音频文件的形式储存。那么你们知道视频转音频软件哪个好用&#xff1f;下面我就来跟大家分享几个还不错的转换软件给你们&#xff0c;大家可千万别…

sprd9820 来电归属地

一、来电归属地没有出来 sprd\packages\apps\InCallUI\src\com\sprd\incallui\geocode\GeocodeHelper.java private static final String KEY "ro.device.support.geocode"; private static final boolean SUPPORT_SPRD_GROCODE SystemProperties.getBoolean(KEY,…

如何在iPhone上转接电话

You’d think call forwarding would be an easy thing to set up on your iPhone. While it isn’t really complicated, how you enable it depends on whether your phone provider uses the GSM or CDMA standard. 您会认为在您的iPhone上设置呼叫转移将是一件容易的事。 虽…

命令模式(Command)

别名 动作&#xff08;Action&#xff09;事务&#xff08;Transaction&#xff09; 定义 命令是一种行为设计模式&#xff0c;它可将请求转换为一个包含与请求相关的所有信息的独立对象。该转换让你能根据不同的请求将方法参数化、延迟请求执行或将其放入队列中&#xff0c…

360 se html,360安全浏览器

360浏览器 正式版 13.1.1348.0 1、更多的画报素材可以设置为壁纸 2、办公、全屏、双屏、投影等场景下判断逻辑优化&#xff0c;减少打扰 3、优化浏览器安装时的快捷方式图标 4、优化青少年模式 5、优化与360快资讯的登录状态同步 6、小程序实名认证接入中宣部系统 7、修复页面…

如何用360极速浏览器下载网页视频

1.打开扩展中心 2.找到FVD Downloader 3.呐&#xff0c;就是这个&#xff0c;点击就好了

360浏览器扩展下载网页视频

第一步&#xff0c;打开360浏览器&#xff0c;点击扩展,然后在弹窗那里点击更多扩展 然后在弹窗那里点击更多扩展 当跳转到360应用市场时&#xff0c;再搜索“网页媒体” 选择安装&#xff0c;和添加 打开你要下载视频的网站&#xff08;我以68ps里面的视频下载为例&#xff09…